Class: Cosmos::NarrativeModel

Inherits:
Model show all
Defined in:
lib/cosmos/models/narrative_model.rb

Constant Summary collapse

CHRONICLE_TYPE =
'narrative'.freeze
PRIMARY_KEY =
'__NARRATIVE'.freeze

Instance Attribute Summary collapse

Attributes inherited from Model

#name, #plugin, #scope, #updated_at

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Model

#as_config, #deploy, filter, find_all_by_plugin, get_all_models, get_model, handle_config, names, set, #undeploy

Constructor Details

#initialize(scope:, start:, stop:, color:, description:, type: CHRONICLE_TYPE, updated_at: 0, duration: 0) ⇒ NarrativeModel

Returns a new instance of NarrativeModel.

Parameters:

  • scope (String)
    • Cosmos scope to track event to

  • start (Integer)
    • start of the event in seconds from Epoch

  • stop (Integer)
    • stop of the event in seconds from Epoch

  • color (String)
    • The event color

  • description (String)
    • What the event is about



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/cosmos/models/narrative_model.rb', line 111

def initialize(
  scope:,
  start:,
  stop:,
  color:,
  description:,
  type: CHRONICLE_TYPE,
  updated_at: 0,
  duration: 0
)
  super(NarrativeModel.pk(scope), name: start.to_s, scope: scope)
  set_input(
    start: start,
    stop: stop,
    color: color,
    description: description,
  )
  @type = type
  @updated_at = updated_at
end

Instance Attribute Details

#colorObject (readonly)

Returns the value of attribute color.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def color
  @color
end

#descriptionObject (readonly)

Returns the value of attribute description.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def description
  @description
end

#durationObject (readonly)

Returns the value of attribute duration.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def duration
  @duration
end

#startObject (readonly)

Returns the value of attribute start.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def start
  @start
end

#stopObject (readonly)

Returns the value of attribute stop.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def stop
  @stop
end

#typeObject (readonly)

Returns the value of attribute type.



104
105
106
# File 'lib/cosmos/models/narrative_model.rb', line 104

def type
  @type
end

Class Method Details

.all(scope:, limit: 100) ⇒ Array<Hash>

Returns Array up to the limit of the models (as Hash objects) stored under the primary key.

Returns:

  • (Array<Hash>)

    Array up to the limit of the models (as Hash objects) stored under the primary key



56
57
58
59
60
61
62
63
64
# File 'lib/cosmos/models/narrative_model.rb', line 56

def self.all(scope:, limit: 100)
  pk = self.pk(scope) 
  array = Store.zrange(pk, 0, -1, :limit => [0, limit])
  ret_array = Array.new
  array.each do |value|
    ret_array << JSON.parse(value)
  end
  return ret_array
end

.count(scope:) ⇒ Integer

Returns count of the members stored under the primary key.

Returns:

  • (Integer)

    count of the members stored under the primary key



67
68
69
# File 'lib/cosmos/models/narrative_model.rb', line 67

def self.count(scope:)
  return Store.zcard(self.pk(scope))
end

.destroy(scope:, score:) ⇒ Integer

Remove member from a sorted set based on the score.

Returns:

  • (Integer)

    count of the members removed



83
84
85
86
# File 'lib/cosmos/models/narrative_model.rb', line 83

def self.destroy(scope:, score:)
  pk = self.pk(scope) 
  Store.zremrangebyscore(pk, score, score)
end

.from_json(json, scope:) ⇒ NarrativeModel

Returns Model generated from the passed JSON.

Returns:



96
97
98
99
100
101
102
# File 'lib/cosmos/models/narrative_model.rb', line 96

def self.from_json(json, scope:)
  json = JSON.parse(json) if String === json
  raise "json data is nil" if json.nil?

  json.transform_keys!(&:to_sym)
  self.new(**json, scope: scope)
end

.get(start:, stop:, scope:, limit: 100) ⇒ Array|nil

Returns Array up to 100 of this model or empty array.

Returns:

  • (Array|nil)

    Array up to 100 of this model or empty array



42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/cosmos/models/narrative_model.rb', line 42

def self.get(start:, stop:, scope:, limit: 100)
  if start > stop
    raise MetadataInputError.new "start: #{start} must be before stop: #{stop}"
  end
  pk = self.pk(scope)
  array = Store.zrangebyscore(pk, start, stop, :limit => [0, limit])
  ret_array = Array.new
  array.each do |value|
    ret_array << JSON.parse(value)
  end
  return ret_array
end

.pk(scope) ⇒ Object



37
38
39
# File 'lib/cosmos/models/narrative_model.rb', line 37

def self.pk(scope)
  return "#{scope}#{PRIMARY_KEY}"
end

.range_destroy(scope:, min:, max:) ⇒ Integer

Remove members from min to max of the sorted set.

Returns:

  • (Integer)

    count of the members removed



90
91
92
93
# File 'lib/cosmos/models/narrative_model.rb', line 90

def self.range_destroy(scope:, min:, max:)
  pk = self.pk(scope) 
  Store.zremrangebyscore(pk, min, max)
end

.score(score:, scope:) ⇒ String|nil

Returns String of the saved json or nil if score not found under primary_key.

Returns:

  • (String|nil)

    String of the saved json or nil if score not found under primary_key



72
73
74
75
76
77
78
79
# File 'lib/cosmos/models/narrative_model.rb', line 72

def self.score(score:, scope:)
  pk = self.pk(scope) 
  array = Store.zrangebyscore(pk, score, score, :limit => [0, 1])
  array.each do |value|
    return JSON.parse(value)
  end
  return nil
end

Instance Method Details

#as_jsonHash

Returns generated from the NarrativeModel.

Returns:

  • (Hash)

    generated from the NarrativeModel



263
264
265
266
267
268
269
270
271
272
273
# File 'lib/cosmos/models/narrative_model.rb', line 263

def as_json
  return {
    'color' => @color,
    'start' => @start,
    'stop' => @stop,
    'description' => @description,
    'type' => CHRONICLE_TYPE,
    'scope' => @scope,
    'updated_at' => @updated_at,
  }
end

#createObject

Update the Redis hash at primary_key and set the score equal to the start Epoch time the member is set to the JSON generated via calling as_json



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/cosmos/models/narrative_model.rb', line 201

def create
  validate_input(start: @start, stop: @stop, color: @color, description: @description)
  collision = validate_time()
  unless collision.nil?
    raise NarrativeOverlapError.new "no chronicles can overlap, collision: #{collision}"
  end

  @updated_at = Time.now.to_nsec_from_epoch
  Store.zadd(@primary_key, @start, JSON.generate(as_json()))
  notify(kind: 'created')
end

#destroyObject

destroy the activity from the redis database



242
243
244
245
# File 'lib/cosmos/models/narrative_model.rb', line 242

def destroy
  Store.zremrangebyscore(@primary_key, @start, @start)
  notify(kind: 'deleted')
end

#notify(kind:, extra: nil) ⇒ Object

Returns [] update the redis stream / timeline topic that something has changed.

Returns:

  • update the redis stream / timeline topic that something has changed



248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/cosmos/models/narrative_model.rb', line 248

def notify(kind:, extra: nil)
  notification = {
    'data' => JSON.generate(as_json()),
    'kind' => kind,
    'type' => 'calendar',
  }
  notification['extra'] = extra unless extra.nil?
  begin
    CalendarTopic.write_entry(notification, scope: @scope)
  rescue StandardError => e
    raise NarrativeError.new "Failed to write to stream: #{notification}, #{e}"
  end
end

#set_input(start:, stop:, color:, description:) ⇒ Object

Set the values of the instance, @start, @stop, @description…



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/cosmos/models/narrative_model.rb', line 166

def set_input(start:, stop:, color:, description:)
  begin
    DateTime.strptime(start.to_s, '%s')
    DateTime.strptime(stop.to_s, '%s')
  rescue ArgumentError
    raise NarrativeInputError.new "invalid input must be seconds: #{start}, #{stop}"
  end
  @start = start
  @stop = stop
  @duration = @stop - @start
  @color = color
  @description = description
end

#to_sString

Returns string view of NarrativeModel.

Returns:

  • (String)

    string view of NarrativeModel



276
277
278
# File 'lib/cosmos/models/narrative_model.rb', line 276

def to_s
  return "<NarrativeModel ->: #{@start}, x: #{@stop}, c: #{@color}, d: #{@description}>"
end

#update(start:, stop:, color:, description:) ⇒ Object

Update the Redis hash at primary_key and remove the current activity at the current score and update the score to the new score equal to the start Epoch time this uses a multi to execute both the remove and create. The member via the JSON generated via calling as_json



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/cosmos/models/narrative_model.rb', line 216

def update(start:, stop:, color:, description:)
  validate_input(start: start, stop: stop, color: color, description: description)
  old_start = @start

  set_input(
    start: start,
    stop: stop,
    color: color,
    description: description,
  )
  @updated_at = Time.now.to_nsec_from_epoch

  collision = validate_time(ignore_score: old_start)
  unless collision.nil?
    raise NarrativeOverlapError.new "failed to update #{old_start}, no chronicles can overlap, collision: #{collision}"
  end

  Store.multi do |multi|
    multi.zremrangebyscore(@primary_key, old_start, old_start)
    multi.zadd(@primary_key, @start, JSON.generate(as_json()))
  end
  notify(kind: 'updated', extra: old_start)
  return @start
end

#validate_color(color) ⇒ Object

validate color



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/cosmos/models/narrative_model.rb', line 133

def validate_color(color)
  if color.nil?
    color = '#%06x' % (rand * 0xffffff)
  end
  valid_color = color =~ /(#*)([0-9,a-f,A-f]{6})/
  if valid_color.nil?
    raise MetadataInputError.new "invalid color but in hex format. #FF0000"
  end

  color = "##{color}" unless color.start_with?('#')
  return color
end

#validate_input(start:, stop:, color:, description:) ⇒ Object

validate the input to the rules we have created for timelines.

  • An entry’s start MUST be before the stop.

  • An entry’s description MUST a String.



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/cosmos/models/narrative_model.rb', line 149

def validate_input(start:, stop:, color:, description:)
  begin
    DateTime.strptime(start.to_s, '%s')
    DateTime.strptime(stop.to_s, '%s')
  rescue Date::Error
    raise NarrativeInputError.new "failed validation input must be seconds: #{start}, #{stop}"
  end
  validate_color(color)
  duration = stop - start
  if duration <= 0
    raise NarrativeInputError.new "start: #{start} must be before stop: #{stop}"
  elsif description.is_a?(String) == false
    raise NarrativeInputError.new "description must be a String: #{description}"
  end
end

#validate_time(ignore_score: nil) ⇒ Object

validate_time will be called on create and update this will validate that no other chronicle event or metadata had been saved for that time. One event or metadata per second to ensure data can be updated.

Parameters:

  • ignore_score (Integer) (defaults to: nil)
    • should be nil unless you want to ignore

    a time when doing an update



186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/cosmos/models/narrative_model.rb', line 186

def validate_time(ignore_score: nil)
  array = Store.zrangebyscore(@primary_key, @start, @start, :limit => [0, 1])
  array.each do |value|
    entry = JSON.parse(value)
    if ignore_score == entry['start']
      next
    else
      return entry
    end
  end
  return nil
end