Skip to content

Commit

Permalink
Merge pull request #1665 from BallAerospace/metadata_constraint
Browse files Browse the repository at this point in the history
Metadata constraint
  • Loading branch information
jmthomas authored Jun 2, 2022
2 parents 38a0bb7 + c498a06 commit 272912f
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 40 deletions.
54 changes: 39 additions & 15 deletions cosmos/lib/cosmos/models/metadata_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,27 @@ def self.pk(scope)
"#{scope}#{PRIMARY_KEY}"
end

attr_reader :color, :metadata, :type
attr_reader :color, :metadata, :constraints, :type

# @param [Integer] start - time metadata is active in seconds from Epoch
# @param [Integer] start - Time metadata is active in seconds from Epoch
# @param [String] color - The event color
# @param [String] metadata - Key value pair object to link to name
# @param [Hash] metadata - Hash of metadata values
# @param [Hash] constraints - Constraints to apply to the metadata
# @param [String] scope - Cosmos scope to track event to
def initialize(
scope:,
start:,
color: nil,
metadata:,
constraints: nil,
type: METADATA_TYPE,
updated_at: 0
)
super(start: start, scope: scope, updated_at: updated_at)
@start = start
@color = color
@metadata = metadata
@constraints = constraints if constraints
@type = type # For the as_json, from_json round trip
end

Expand All @@ -56,6 +59,7 @@ def validate(update: false)
validate_start(update: update)
validate_color()
validate_metadata()
validate_constraints() if @constraints
end

def validate_color()
Expand All @@ -72,13 +76,34 @@ def validate_metadata()
unless @metadata.is_a?(Hash)
raise SortedInputError.new "Metadata must be a hash/object: #{@metadata}"
end
# Convert keys to strings. This isn't quite as efficient as symbols
# but we store as JSON which is all strings and it makes comparisons easier.
@metadata = @metadata.transform_keys(&:to_s)
end

def validate_constraints()
unless @constraints.is_a?(Hash)
raise SortedInputError.new "Constraints must be a hash/object: #{@constraints}"
end
# Convert keys to strings. This isn't quite as efficient as symbols
# but we store as JSON which is all strings and it makes comparisons easier.
@constraints = @constraints.transform_keys(&:to_s)
unless (@constraints.keys - @metadata.keys).empty?
raise SortedInputError.new "Constraints keys must be subset of metadata: #{@constraints.keys} subset #{@metadata.keys}"
end
@constraints.each do |key, constraint|
unless constraint.include?(@metadata[key])
raise SortedInputError.new "Constraint violation! key:#{key} value:#{@metadata[key]} constraint:#{constraint}"
end
end
end

# Update the Redis hash at primary_key based on the initial passed start
# The member is set to the JSON generated via calling as_json
def create(update: false)
validate(update: update)
@updated_at = Time.now.to_nsec_from_epoch
MetadataModel.destroy(scope: @scope, start: update) if update
Store.zadd(@primary_key, @start, JSON.generate(as_json()))
if update
notify(kind: 'updated')
Expand All @@ -87,29 +112,28 @@ def create(update: false)
end
end

# Update the Redis hash at primary_key
def update(start:, color:, metadata:)
@start = start
@color = color
@metadata = metadata
create(update: true)
# Update the model. All arguments are optional, only those set will be updated.
def update(start: nil, color: nil, metadata: nil, constraints: nil)
orig_start = @start
@start = start if start
@color = color if color
@metadata = metadata if metadata
@constraints = constraints if constraints
create(update: orig_start)
end

# @return [Hash] generated from the MetadataModel
def as_json
return {
{
'scope' => @scope,
'start' => @start,
'color' => @color,
'metadata' => @metadata,
'constraints' => @constraints,
'type' => METADATA_TYPE,
'updated_at' => @updated_at,
}
end

# @return [String] string view of metadata
def to_s
return "<MetadataModel s: #{@start}, c: #{@color}, m: #{@metadata}>"
end
alias to_s as_json
end
end
10 changes: 4 additions & 6 deletions cosmos/lib/cosmos/models/note_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def validate_color()
def create(update: false)
validate(update: update)
@updated_at = Time.now.to_nsec_from_epoch
NoteModel.destroy(scope: @scope, start: update) if update
Store.zadd(@primary_key, @start, JSON.generate(as_json()))
if update
notify(kind: 'updated')
Expand All @@ -96,11 +97,12 @@ def create(update: false)

# Update the Redis hash at primary_key
def update(start:, stop:, color:, description:)
orig_start = @start
@start = start
@stop = stop
@color = color
@description = description
create(update: true)
create(update: orig_start)
end

# @return [Hash] generated from the NoteModel
Expand All @@ -115,10 +117,6 @@ def as_json
'updated_at' => @updated_at,
}
end

# @return [String] string view of NoteModel
def to_s
return "<NoteModel s: #{@start}, x: #{@stop}, c: #{@color}, d: #{@description}>"
end
alias to_s as_json
end
end
4 changes: 3 additions & 1 deletion cosmos/lib/cosmos/models/sorted_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def validate_start(update: false)
def create(update: false)
validate_start(update: update)
@updated_at = Time.now.to_nsec_from_epoch
SortedModel.destroy(scope: @scope, start: update) if update
Store.zadd(@primary_key, @start, JSON.generate(as_json()))
if update
notify(kind: 'updated')
Expand All @@ -129,8 +130,9 @@ def create(update: false)

# Update the Redis hash at primary_key
def update(start:)
orig_start = @start
@start = start
create(update: true)
create(update: orig_start)
end

# destroy the activity from the redis database
Expand Down
69 changes: 58 additions & 11 deletions cosmos/spec/models/metadata_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ module Cosmos
end

def create_model(start: Time.now.to_i, scope: 'DEFAULT', color: '#FF0000',
metadata: {'cat' => 'dog', 'version' => 'v1'})
metadata: {'cat' => 'dog', 'version' => 'v1'}, constraints: nil)
model = MetadataModel.new(
scope: scope,
start: start,
color: color,
metadata: metadata,
constraints: constraints,
)
model.create()
model
Expand Down Expand Up @@ -77,34 +78,80 @@ def create_model(start: Time.now.to_i, scope: 'DEFAULT', color: '#FF0000',
expect { create_model(metadata: 'foo') }.to raise_error(SortedInputError)
expect { create_model(metadata: ['one', 'two']) }.to raise_error(SortedInputError)
end

it "validates metadata with constraints" do
now = Time.now.to_i
# Verify we can create and validate with mix and match key strings vs symbols
create_model(start: now, metadata: {'key' => 1}, constraints: {'key' => (1..4)})
create_model(start: now + 1, metadata: {'key' => 1}, constraints: {key: (1..4)})
create_model(start: now + 2, metadata: {key: 1}, constraints: {'key' => (1..4)})
create_model(start: now + 3, metadata: {key: 1}, constraints: {key: (1..4)})
expect { create_model(start: now + 5, metadata: {key: 0}, constraints: {key: (1..4)}) }.to raise_error(SortedInputError, /Constraint violation/)
create_model(start: now + 10, metadata: {key: 'one'}, constraints: {key: ['one', 'two']})
expect { create_model(start: now + 15, metadata: {key: 'other'}, constraints: {key: ['one', 'two']}) }.to raise_error(SortedInputError, /Constraint violation/)
end

it "raises error due to invalid constraints" do
expect { create_model(constraints: 'foo') }.to raise_error(SortedInputError)
expect { create_model(constraints: ['one', 'two']) }.to raise_error(SortedInputError)
expect { create_model(constraints: {'key': ['one', 'two']}) }.to raise_error(SortedInputError)
end
end

describe "update" do
it "updates metadata" do
it "updates start and color" do
now = Time.now.to_i
model = create_model(start: now)
model.update(
start: now,
color: '#00AA00',
metadata: {'bird' => 'update'}
)
expect(model.start).to eql(now)
model = create_model(start: now, metadata: {val: 1})
expect(MetadataModel.count(scope: 'DEFAULT')).to eql(1)
model.update(start: now + 100, color: '#00AA00')
expect(MetadataModel.count(scope: 'DEFAULT')).to eql(1)
expect(model.start).to eql(now + 100)
expect(model.color).to eql('#00AA00')
expect(model.metadata).to eql({'bird' => 'update'})
expect(model.metadata).to eql({'val' => 1})

hash = MetadataModel.get(scope: 'DEFAULT', start: now + 100)
# Test that the hash returned by get is updated
expect(model.start).to eql(hash['start'])
expect(model.color).to eql(hash['color'])
expect(model.metadata).to eql(hash['metadata'])
end

it "updates metadata and constraints" do
now = Time.now.to_i
model = create_model(start: now, metadata: {val: 1}, constraints: {val: [1,2,3]})
expect(MetadataModel.count(scope: 'DEFAULT')).to eql(1)
model.update(metadata: {val: 4}, constraints: {val: (1..5)})
expect(MetadataModel.count(scope: 'DEFAULT')).to eql(1)
expect(model.start).to eql(now)
expect(model.metadata).to eql({'val' => 4})
expect(model.constraints).to eql({'val' => 1..5})

hash = MetadataModel.get(scope: 'DEFAULT', start: now)
# Test that the hash returned by get is updated
expect(model.start).to eql(hash['start'])
expect(model.color).to eql(hash['color'])
expect(model.metadata).to eql(hash['metadata'])
expect({'val' => '1..5'}).to eql(hash['constraints'])
end

it "rejects update if constraints violated" do
now = Time.now.to_i
model = create_model(start: now, metadata: {val: 1}, constraints: {val: [1,2,3]})
expect { model.update(metadata: {val: 4}) }.to raise_error(SortedInputError, /Constraint violation/)
hash = MetadataModel.get(scope: 'DEFAULT', start: now)
# Test that the hash returned by get is NOT updated
expect(model.start).to eql(hash['start'])
expect(model.color).to eql(hash['color'])
expect({'val' => 1}).to eql(hash['metadata'])
end
end

describe "as_json" do
describe "as_json, to_s" do
it "encodes all the input parameters" do
now = Time.now.to_i
model = create_model(start: now, color: '#123456', metadata: {'test' => 'one', 'foo' => 'bar'})
json = model.as_json
expect(json).to eql(model.to_s)
expect(json["start"]).to eql(now)
expect(json["color"]).to eql('#123456')
expect(json["metadata"]).to eql({'test' => 'one', 'foo' => 'bar'})
Expand Down
2 changes: 2 additions & 0 deletions cosmos/spec/models/note_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ def create_model(start: Time.now.to_i, stop: Time.now.to_i + 10,
it "updates all the attributes" do
now = Time.now.to_i
model = create_model(start: now)
expect(NoteModel.count(scope: 'DEFAULT')).to eql(1)
model.update(start: now - 100, stop: now - 50, color: "#FFFFFF", description: "update")
expect(NoteModel.count(scope: 'DEFAULT')).to eql(1)
expect(model.start).to eql(now - 100)
expect(model.stop).to eql(now - 50)
expect(model.color).to eql('#FFFFFF')
Expand Down
2 changes: 2 additions & 0 deletions cosmos/spec/models/sorted_model_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,11 @@ def create_model(start: Time.now.to_i, scope: 'DEFAULT')
it "updates the sorted item" do
now = Time.now.to_i
model = create_model(start: now)
expect(SortedModel.count(scope: 'DEFAULT')).to eql(1)
model.update(
start: now + 10,
)
expect(SortedModel.count(scope: 'DEFAULT')).to eql(1)
hash = SortedModel.get(scope: 'DEFAULT', start: now + 10)
expect(hash['start']).to eql(now + 10)
end
Expand Down
10 changes: 3 additions & 7 deletions playwright/storageState.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,12 @@
"value": "DEFAULT"
},
{
"name": "cosmosToken",
"value": "password"
},
{
"name": "suppresswarning__clock_out_of_sync_with_server",
"name": "notoast",
"value": "true"
},
{
"name": "notoast",
"value": "true"
"name": "cosmosToken",
"value": "password"
}
]
}
Expand Down

0 comments on commit 272912f

Please sign in to comment.