Class: Cosmos::MetadataModel

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

Constant Summary collapse

CHRONICLE_TYPE =
'metadata'.freeze
CURRENT_VALUE =
'__current.metadata.value'.freeze
PRIMARY_KEY =
'__METADATA'.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(target:, start:, color: nil, metadata:, scope:, type: CHRONICLE_TYPE, updated_at: 0) ⇒ MetadataModel

Returns a new instance of MetadataModel.

Parameters:

  • target (String)
    • should be the target but can be anything

  • start (Integer)
    • time metadata is active in seconds from Epoch

  • color (String) (defaults to: nil)
    • The event color

  • metadata (String)
    • Key value pair object to link to name

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



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/cosmos/models/metadata_model.rb', line 119

def initialize(
  target:,
  start:,
  color: nil,
  metadata:,
  scope:,
  type: CHRONICLE_TYPE,
  updated_at: 0
)
  super(MetadataModel.pk(scope), name: start.to_s, scope: scope)
  set_input(start: start, color: color, metadata: )
  @target = target
  @type = type
  @updated_at = updated_at
end

Instance Attribute Details

#colorObject (readonly)

Returns the value of attribute color.



112
113
114
# File 'lib/cosmos/models/metadata_model.rb', line 112

def color
  @color
end

#metadataObject (readonly)

Returns the value of attribute metadata.



112
113
114
# File 'lib/cosmos/models/metadata_model.rb', line 112

def 
  @metadata
end

#startObject (readonly)

Returns the value of attribute start.



112
113
114
# File 'lib/cosmos/models/metadata_model.rb', line 112

def start
  @start
end

#targetObject (readonly)

Returns the value of attribute target.



112
113
114
# File 'lib/cosmos/models/metadata_model.rb', line 112

def target
  @target
end

#typeObject (readonly)

Returns the value of attribute type.



112
113
114
# File 'lib/cosmos/models/metadata_model.rb', line 112

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



64
65
66
67
68
69
70
71
72
# File 'lib/cosmos/models/metadata_model.rb', line 64

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



75
76
77
# File 'lib/cosmos/models/metadata_model.rb', line 75

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



91
92
93
94
# File 'lib/cosmos/models/metadata_model.rb', line 91

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

.from_json(json, scope:) ⇒ MetadataModel

Returns Model generated from the passed JSON.

Returns:



104
105
106
107
108
109
110
# File 'lib/cosmos/models/metadata_model.rb', line 104

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



50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/cosmos/models/metadata_model.rb', line 50

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

.get_current_value(target:, scope:) ⇒ String|nil

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

Returns:

  • (String|nil)

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



43
44
45
46
47
# File 'lib/cosmos/models/metadata_model.rb', line 43

def self.get_current_value(target:, scope:)
  json = Store.hget("#{scope}#{CURRENT_VALUE}", target)
  return nil unless json
  return self.from_json(JSON.parse(json), scope: scope)
end

.pk(scope) ⇒ Object



38
39
40
# File 'lib/cosmos/models/metadata_model.rb', line 38

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



98
99
100
101
# File 'lib/cosmos/models/metadata_model.rb', line 98

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



80
81
82
83
84
85
86
87
# File 'lib/cosmos/models/metadata_model.rb', line 80

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 MetadataModel.

Returns:

  • (Hash)

    generated from the MetadataModel



272
273
274
275
276
277
278
279
280
281
282
# File 'lib/cosmos/models/metadata_model.rb', line 272

def as_json
  return {
    'target' => @target,
    'scope' => @scope,
    'updated_at' => @updated_at,
    'start' => @start,
    'color' => @color,
    'metadata' => @metadata,
    'type' => CHRONICLE_TYPE,
  }
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



198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/cosmos/models/metadata_model.rb', line 198

def create
  validate_input(start: @start, color: @color, metadata: @metadata)
  collision = validate_time()
  unless collision.nil?
    raise MetadataOverlapError.new "no chronicle can overlap, collision: #{collision}"
  end

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

#destroyObject

destroy the activity from the redis database



251
252
253
254
# File 'lib/cosmos/models/metadata_model.rb', line 251

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



257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/cosmos/models/metadata_model.rb', line 257

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 MetadataError.new "Failed to write to stream: #{notification}, #{e}"
  end
end

#set_input(start:, color:, metadata:) ⇒ Object

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



168
169
170
171
172
173
174
175
# File 'lib/cosmos/models/metadata_model.rb', line 168

def set_input(start:, color:, metadata:)
  if start.is_a?(Integer) == false
    raise MetadataInputError.new "start input must be integer: #{start}"
  end
  @start = start
  @color = color
  @metadata = 
end

#to_sString

Returns string view of metadata.

Returns:

  • (String)

    string view of metadata



285
286
287
# File 'lib/cosmos/models/metadata_model.rb', line 285

def to_s
  return "<MetadataModel t: #{@target}, s: #{@start}, c: #{@color}, m: #{@metadata}>"
end

#update(start:, color:, metadata:) ⇒ 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



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/cosmos/models/metadata_model.rb', line 214

def update(start:, color:, metadata:)
  validate_input(start: start, color: color, metadata: )
  old_start = @start
  @updated_at = Time.now.to_nsec_from_epoch
  set_input(start: start, color: color, metadata: )
  # copy of create
  collision = validate_time(ignore_score: old_start)
  unless collision.nil?
    raise MetadataOverlapError.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
  update_current_value(old_start: old_start)
  notify(kind: 'updated', extra: old_start)
  return @start
end

#update_current_value(old_start: nil) ⇒ Object

Update the Redis hash at primary_key and check if this metadata instance is newer than the current instance stored in the hash. If the hash does NOT contain an instance or this metadata instance is newer it will update the current hash.



238
239
240
241
242
243
244
245
246
247
248
# File 'lib/cosmos/models/metadata_model.rb', line 238

def update_current_value(old_start: nil)
  update = true
  json = Store.hget("#{@scope}#{CURRENT_VALUE}", @target)
  unless json.nil?
    model = MetadataModel.from_json(JSON.parse(json), scope: @scope)
    update = model.start <= @start || model.start == old_start
  end
  if update
    return Store.hset("#{@scope}#{CURRENT_VALUE}", @target, JSON.generate(as_json()))
  end
end

#validate_color(color) ⇒ Object

validate color



136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/cosmos/models/metadata_model.rb', line 136

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, must be in hex format, e.g. #FF0000"
  end

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

#validate_input(start:, color:, metadata:) ⇒ Object

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

  • An entry’s start MUST be valid.

  • An entry’s start MUST NOT be in the future.

  • An entry’s metadata MUST a hash/object.



153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/cosmos/models/metadata_model.rb', line 153

def validate_input(start:, color:, metadata:)
  if start.is_a?(Integer) == false
    raise MetadataInputError.new "failed validation input must be integer: #{start}"
  end
  now = Time.now.strftime('%s%3N').to_i
  if start > now
    raise MetadataInputError.new "start can not be in the future: #{start} > #{now}"
  end
  validate_color(color)
  if .is_a?(Hash) == false
    raise MetadataInputError.new "Metadata must be a hash/object: #{}"
  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



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/cosmos/models/metadata_model.rb', line 183

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