chronicle-core 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +1 -1
- data/.gitignore +3 -1
- data/.rubocop-plugin.yml +4 -0
- data/.rubocop.yml +16 -2
- data/Gemfile +2 -2
- data/Guardfile +3 -3
- data/LICENSE.txt +1 -1
- data/README.md +87 -2
- data/Rakefile +63 -1
- data/bin/console +6 -6
- data/chronicle-core.gemspec +32 -26
- data/lib/chronicle/core/version.rb +1 -3
- data/lib/chronicle/core.rb +1 -3
- data/lib/chronicle/models/base.rb +96 -0
- data/lib/chronicle/models/builder.rb +35 -0
- data/lib/chronicle/models/generation.rb +89 -0
- data/lib/chronicle/models/model_factory.rb +63 -0
- data/lib/chronicle/models.rb +17 -0
- data/lib/chronicle/schema/rdf_parsing/graph_transformer.rb +122 -0
- data/lib/chronicle/schema/rdf_parsing/rdf_serializer.rb +138 -0
- data/lib/chronicle/schema/rdf_parsing/schemaorg.rb +50 -0
- data/lib/chronicle/schema/rdf_parsing/ttl_graph_builder.rb +142 -0
- data/lib/chronicle/schema/rdf_parsing.rb +11 -0
- data/lib/chronicle/schema/schema_graph.rb +145 -0
- data/lib/chronicle/schema/schema_property.rb +81 -0
- data/lib/chronicle/schema/schema_type.rb +110 -0
- data/lib/chronicle/schema/types.rb +9 -0
- data/lib/chronicle/schema/validation/base_contract.rb +22 -0
- data/lib/chronicle/schema/validation/contract_factory.rb +133 -0
- data/lib/chronicle/schema/validation/edge_validator.rb +53 -0
- data/lib/chronicle/schema/validation/generation.rb +29 -0
- data/lib/chronicle/schema/validation/validator.rb +23 -0
- data/lib/chronicle/schema/validation.rb +41 -0
- data/lib/chronicle/schema.rb +9 -2
- data/lib/chronicle/serialization/hash_serializer.rb +5 -11
- data/lib/chronicle/serialization/jsonapi_serializer.rb +41 -26
- data/lib/chronicle/serialization/jsonld_serializer.rb +38 -0
- data/lib/chronicle/serialization/record.rb +90 -0
- data/lib/chronicle/serialization/serializer.rb +31 -18
- data/lib/chronicle/serialization.rb +6 -4
- data/lib/chronicle/utils/hash_utils.rb +19 -16
- data/schema/chronicle_schema_v1.json +1008 -0
- data/schema/chronicle_schema_v1.rb +147 -0
- data/schema/chronicle_schema_v1.ttl +562 -0
- metadata +107 -15
- data/lib/chronicle/schema/activity.rb +0 -5
- data/lib/chronicle/schema/base.rb +0 -79
- data/lib/chronicle/schema/entity.rb +0 -5
- data/lib/chronicle/schema/raw.rb +0 -9
- data/lib/chronicle/schema/validator.rb +0 -55
@@ -0,0 +1,110 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
# Represents a type in the RDF graph
|
4
|
+
#
|
5
|
+
# TODO: rename `class` to `type` to match new class name
|
6
|
+
class SchemaType
|
7
|
+
attr_reader :id
|
8
|
+
attr_accessor :properties,
|
9
|
+
:subtype_ids,
|
10
|
+
:comment,
|
11
|
+
:namespace,
|
12
|
+
:subtypes,
|
13
|
+
:supertypes,
|
14
|
+
:see_also
|
15
|
+
|
16
|
+
def initialize(id)
|
17
|
+
@id = id
|
18
|
+
@subtype_ids = []
|
19
|
+
@properties = []
|
20
|
+
|
21
|
+
yield self if block_given?
|
22
|
+
end
|
23
|
+
|
24
|
+
def inspect
|
25
|
+
"#<SchemaType:#{id}>"
|
26
|
+
end
|
27
|
+
|
28
|
+
def pretty_print(pp)
|
29
|
+
pp.text("SchemaType: #{id}")
|
30
|
+
pp.nest(2) do
|
31
|
+
pp.breakable
|
32
|
+
pp.text("subtypeIds: #{subtype_ids.map(&:id)}")
|
33
|
+
pp.breakable
|
34
|
+
pp.text("Comment: #{comment}")
|
35
|
+
pp.breakable
|
36
|
+
pp.text("Properties: #{properties.map(&:id)}")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def short_id
|
41
|
+
id.gsub(@namespace, '')
|
42
|
+
end
|
43
|
+
|
44
|
+
# FIXME
|
45
|
+
def identifier
|
46
|
+
short_id.to_sym
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_h
|
50
|
+
output = {
|
51
|
+
id:,
|
52
|
+
subtype_ids:
|
53
|
+
}
|
54
|
+
output[:see_also] = @see_also if @see_also
|
55
|
+
output[:comment] = @comment if @comment
|
56
|
+
output
|
57
|
+
end
|
58
|
+
|
59
|
+
def ==(other)
|
60
|
+
id == other.id
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_subtype_id(subtype_id)
|
64
|
+
@subtype_ids << subtype_id unless @subtype_ids.include?(subtype_id)
|
65
|
+
end
|
66
|
+
|
67
|
+
def ancestors
|
68
|
+
@ancestors ||= begin
|
69
|
+
ancestors = []
|
70
|
+
|
71
|
+
queue = supertypes.dup
|
72
|
+
until queue.empty?
|
73
|
+
current = queue.shift
|
74
|
+
ancestors << current
|
75
|
+
queue.concat(current.supertypes)
|
76
|
+
end
|
77
|
+
ancestors
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def descendants
|
82
|
+
@descendants ||= begin
|
83
|
+
descendants = []
|
84
|
+
|
85
|
+
queue = subtypes.dup
|
86
|
+
until queue.empty?
|
87
|
+
current = queue.shift
|
88
|
+
descendants << current
|
89
|
+
queue.concat(current.subtypes)
|
90
|
+
end
|
91
|
+
descendants
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def all_properties
|
96
|
+
@all_properties ||= begin
|
97
|
+
properties = @properties.dup
|
98
|
+
ancestors.each do |ancestor|
|
99
|
+
properties.concat(ancestor.properties)
|
100
|
+
end
|
101
|
+
properties
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_property(property)
|
106
|
+
@properties << property unless @properties.include?(property)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'dry/validation'
|
2
|
+
|
3
|
+
module Chronicle
|
4
|
+
module Schema
|
5
|
+
module Validation
|
6
|
+
class BaseContract < Dry::Validation::Contract
|
7
|
+
option :edge_validator, default: -> { Chronicle::Schema::Validation::EdgeValidator.new }
|
8
|
+
|
9
|
+
# I think this doesn't work for nested objects because
|
10
|
+
# we don't enumerate all the properties and rely on the edgevalidator
|
11
|
+
# to do the validation. Commenting out for now.
|
12
|
+
# config.validate_keys = true
|
13
|
+
|
14
|
+
attr_accessor :type_name
|
15
|
+
|
16
|
+
def self.type(type_name)
|
17
|
+
@type_name = type_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module Validation
|
4
|
+
class ContractFactory
|
5
|
+
def self.process_errors(errors)
|
6
|
+
messages = []
|
7
|
+
errors.each do |key, value|
|
8
|
+
base_path = key == :base ? [] : [key]
|
9
|
+
if value.is_a?(Hash)
|
10
|
+
value.each do |k, v|
|
11
|
+
messages << [base_path + [k], v.first.to_s]
|
12
|
+
end
|
13
|
+
elsif value.is_a?(Array)
|
14
|
+
messages << [base_path, value.first.to_s]
|
15
|
+
else
|
16
|
+
messages << [base_path, value.to_s]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
messages
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create(type_id:, properties: [])
|
23
|
+
Class.new(Chronicle::Schema::Validation::BaseContract) do
|
24
|
+
type type_id
|
25
|
+
|
26
|
+
params(Chronicle::Schema::Validation::ContractFactory.create_schema(type_id:, properties:))
|
27
|
+
|
28
|
+
properties.each do |property|
|
29
|
+
edge_name = property.id_snakecase
|
30
|
+
|
31
|
+
if property.many?
|
32
|
+
rule(edge_name).each do |index:|
|
33
|
+
errors = edge_validator.validate(type_id, edge_name, value)
|
34
|
+
|
35
|
+
error_path = [edge_name, index]
|
36
|
+
messages = Chronicle::Schema::Validation::ContractFactory.process_errors(errors)
|
37
|
+
messages.each do |path, message|
|
38
|
+
key(error_path + path).failure(message)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
rule(edge_name) do
|
43
|
+
# handle nils. FIXME: this is a hack
|
44
|
+
next unless value
|
45
|
+
|
46
|
+
errors = edge_validator.validate(type_id, edge_name, value)
|
47
|
+
error_path = [edge_name]
|
48
|
+
messages = Chronicle::Schema::Validation::ContractFactory.process_errors(errors)
|
49
|
+
messages.each do |path, message|
|
50
|
+
key(error_path + path).failure(message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.create_schema(type_id:, properties: [])
|
59
|
+
Dry::Schema.JSON do
|
60
|
+
required(:@type).value(:str?).filled(eql?: type_id.to_s)
|
61
|
+
|
62
|
+
before(:key_coercer) do |result|
|
63
|
+
result.to_h.transform_keys!(&:to_sym)
|
64
|
+
|
65
|
+
valid_property_ids = properties.map(&:id_snakecase)
|
66
|
+
valid_property_ids << :@type
|
67
|
+
invalid_properties = result.to_h.keys - valid_property_ids
|
68
|
+
|
69
|
+
invalid_properties.each do |invalid_property|
|
70
|
+
result.add_error([:unexpected_key, [invalid_property.to_sym, []]])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Attempt to coerce chronicle edges recursively
|
75
|
+
# I tried to use a custom type in the schema and use the constructor
|
76
|
+
# method to do the coercion but it didn't work consistently
|
77
|
+
before(:value_coercer) do |obj|
|
78
|
+
obj.to_h.transform_values do |value|
|
79
|
+
case value
|
80
|
+
when ::Array
|
81
|
+
value.map do |v|
|
82
|
+
Chronicle::Schema::Validation::ContractFactory.coerce_chronicle_edge(v)
|
83
|
+
end
|
84
|
+
when ::Hash
|
85
|
+
Chronicle::Schema::Validation::ContractFactory.coerce_chronicle_edge(value)
|
86
|
+
else
|
87
|
+
value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
properties.each do |property|
|
93
|
+
property_name = property.id_snakecase
|
94
|
+
type = Chronicle::Schema::Validation::ContractFactory.build_type(property.range_identifiers)
|
95
|
+
|
96
|
+
outer_macro = property.required? ? :required : :optional
|
97
|
+
|
98
|
+
if property.many?
|
99
|
+
send(outer_macro, property_name).value(:array).each(type)
|
100
|
+
else
|
101
|
+
send(outer_macro, property_name).value(type)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.coerce_chronicle_edge(value)
|
108
|
+
return value unless value.is_a?(Hash)
|
109
|
+
|
110
|
+
type = (value[:@type] || value['@type']).to_sym
|
111
|
+
contract_klass = Chronicle::Schema::Validation.get_contract(type)
|
112
|
+
return value unless contract_klass
|
113
|
+
|
114
|
+
result = contract_klass.schema.call(value)
|
115
|
+
result.success? ? result.to_h : value
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.build_type(range)
|
119
|
+
literals = []
|
120
|
+
literals << :integer if range.include?(:Integer)
|
121
|
+
literals << :float if range.include?(:Float)
|
122
|
+
literals << :string if range.include?(:Text)
|
123
|
+
literals << :string if range.include?(:URL)
|
124
|
+
literals << :time if range.include?(:DateTime)
|
125
|
+
|
126
|
+
literals << :hash
|
127
|
+
# puts "building type for #{range}. literals: #{literals}"
|
128
|
+
literals.uniq
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module Validation
|
4
|
+
class EdgeValidator
|
5
|
+
def validate(type, edge, value)
|
6
|
+
errors = {}
|
7
|
+
return errors unless value.is_a?(Hash)
|
8
|
+
|
9
|
+
value_type = (value[:@type] || value['@type']).to_sym
|
10
|
+
|
11
|
+
property = fetch_property(type, edge)
|
12
|
+
unless property
|
13
|
+
errors[:base] = 'not a valid edge'
|
14
|
+
return errors
|
15
|
+
end
|
16
|
+
|
17
|
+
complete_range = fetch_complete_range(property)
|
18
|
+
|
19
|
+
unless complete_range.include?(value_type)
|
20
|
+
errors[:base] =
|
21
|
+
"#{value_type} is not a valid type for #{edge}. Valid types are #{complete_range.join(', ')}"
|
22
|
+
return errors
|
23
|
+
end
|
24
|
+
|
25
|
+
contract_klass = Chronicle::Schema::Validation.get_contract(value_type)
|
26
|
+
unless contract_klass
|
27
|
+
errors[:base] = "no contract found for #{value_type}"
|
28
|
+
return errors
|
29
|
+
end
|
30
|
+
|
31
|
+
result = contract_klass.new.call(value)
|
32
|
+
|
33
|
+
result.errors.to_h
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def fetch_property(type, edge)
|
39
|
+
klass = Chronicle::Schema::Validation::Generation.graph.find_type(type)
|
40
|
+
klass&.all_properties&.find { |p| edge == p.id_snakecase }
|
41
|
+
end
|
42
|
+
|
43
|
+
def fetch_complete_range(property)
|
44
|
+
property.range.each_with_object([]) do |range, memo|
|
45
|
+
range_klass = Chronicle::Schema::Validation::Generation.graph.find_type_by_id(range)
|
46
|
+
memo << range_klass.short_id
|
47
|
+
memo.concat(range_klass.descendants.map(&:short_id))
|
48
|
+
end.uniq.map(&:to_sym)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module Validation
|
4
|
+
module Generation
|
5
|
+
@contracts_generated = false
|
6
|
+
|
7
|
+
def self.generate_contracts(graph)
|
8
|
+
return if @contracts_generated
|
9
|
+
|
10
|
+
@graph = graph
|
11
|
+
|
12
|
+
graph.types.each do |klass|
|
13
|
+
type_id = klass.short_id.to_sym
|
14
|
+
type_contract_class = Chronicle::Schema::Validation::ContractFactory.create(type_id:,
|
15
|
+
properties: klass.all_properties)
|
16
|
+
|
17
|
+
Chronicle::Schema::Validation.set_contract(type_id, type_contract_class)
|
18
|
+
end
|
19
|
+
|
20
|
+
@contracts_generated = true
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.graph
|
24
|
+
@graph
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module Validation
|
4
|
+
class Validator
|
5
|
+
def validate(data)
|
6
|
+
type = data[:@type] || data['@type']
|
7
|
+
raise Chronicle::Schema::ValidationError, 'Data does not contain a typed object' unless type
|
8
|
+
|
9
|
+
contract = Chronicle::Schema::Validation.get_contract(type.to_sym)
|
10
|
+
|
11
|
+
# binding.pry unless contract
|
12
|
+
raise Chronicle::Schema::ValidationError, "#{type} is not a valid type" unless contract
|
13
|
+
|
14
|
+
contract.new.call(data)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.call(data)
|
18
|
+
new.validate(data)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Schema
|
3
|
+
module Validation
|
4
|
+
# FIXME:
|
5
|
+
# - refactor all of this
|
6
|
+
# - handle different serialization flavours
|
7
|
+
# - move to model of memoizing individual contracts and generating them on demand
|
8
|
+
@contracts = {}
|
9
|
+
@graph = nil
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :graph, :contracts
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.unload_contracts
|
16
|
+
@contracts = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.set_contract(name, contract)
|
20
|
+
@contracts[name] = contract
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.get_contract(name)
|
24
|
+
# FIXME:
|
25
|
+
# Chronicle::Schema::Validation::Generation.generate_contracts
|
26
|
+
|
27
|
+
@contracts[name]
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.contracts_generated?
|
31
|
+
!@contracts_generated.nil?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require_relative 'validation/generation'
|
38
|
+
require_relative 'validation/validator'
|
39
|
+
require_relative 'validation/edge_validator'
|
40
|
+
require_relative 'validation/base_contract'
|
41
|
+
require_relative 'validation/contract_factory'
|
data/lib/chronicle/schema.rb
CHANGED
@@ -1,7 +1,14 @@
|
|
1
1
|
module Chronicle
|
2
2
|
module Schema
|
3
|
+
class Error < StandardError; end
|
4
|
+
class ValidationError < Error; end
|
3
5
|
end
|
4
6
|
end
|
5
7
|
|
6
|
-
require_relative "schema/
|
7
|
-
|
8
|
+
# require_relative "schema/validator"
|
9
|
+
|
10
|
+
require_relative 'schema/types'
|
11
|
+
require_relative 'schema/validation'
|
12
|
+
require_relative 'schema/schema_property'
|
13
|
+
require_relative 'schema/schema_type'
|
14
|
+
require_relative 'schema/schema_graph'
|
@@ -1,14 +1,8 @@
|
|
1
|
-
module Chronicle
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
v.map{|record| HashSerializer.new(record).serializable_hash}
|
7
|
-
elsif v.is_a?(Chronicle::Schema::Base)
|
8
|
-
HashSerializer.new(v).serializable_hash
|
9
|
-
else
|
10
|
-
v
|
11
|
-
end
|
1
|
+
module Chronicle
|
2
|
+
module Serialization
|
3
|
+
class HashSerializer < Chronicle::Serialization::Serializer
|
4
|
+
def serializable_hash
|
5
|
+
@record.to_h
|
12
6
|
end
|
13
7
|
end
|
14
8
|
end
|
@@ -1,34 +1,49 @@
|
|
1
|
-
module Chronicle
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
meta: @record.meta
|
8
|
-
})
|
9
|
-
end
|
1
|
+
module Chronicle
|
2
|
+
module Serialization
|
3
|
+
class JSONAPISerializer < Chronicle::Serialization::Serializer
|
4
|
+
def serializable_hash
|
5
|
+
{ data: serialize_record(@record) }
|
6
|
+
end
|
10
7
|
|
11
|
-
|
8
|
+
private
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
10
|
+
def serialize_record(record)
|
11
|
+
identifier_hash(record).merge({
|
12
|
+
attributes: attribute_hash(record),
|
13
|
+
relationships: associations_hash(record),
|
14
|
+
meta: record.meta
|
15
|
+
})
|
16
|
+
end
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
def identifier_hash(record)
|
19
|
+
{
|
20
|
+
type: record.type.to_s,
|
21
|
+
id: record.id
|
22
|
+
}.compact
|
23
|
+
end
|
24
|
+
|
25
|
+
def attribute_hash(record)
|
26
|
+
record.attribute_properties.compact.transform_values do |v|
|
27
|
+
if v.is_a?(Chronicle::Serialization::Record)
|
28
|
+
serialize_record(v)
|
29
|
+
else
|
30
|
+
v
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
23
34
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
35
|
+
def associations_hash(record)
|
36
|
+
record.association_properties.compact.to_h do |k, v|
|
37
|
+
if v.is_a?(Array)
|
38
|
+
[k, { data: v.map { |record| serialize_record(record) } }]
|
39
|
+
elsif v.is_a?(Chronicle::Serialization::Record)
|
40
|
+
[k, { data: serialize_record(v) }]
|
41
|
+
else
|
42
|
+
# [k, { data: v }]
|
43
|
+
[k, v]
|
44
|
+
end
|
30
45
|
end
|
31
|
-
end
|
46
|
+
end
|
32
47
|
end
|
33
48
|
end
|
34
49
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Serialization
|
3
|
+
class JSONLDSerializer < Chronicle::Serialization::Serializer
|
4
|
+
DEFAULT_CONTEXT = 'https://schema.chronicle.app/'.freeze
|
5
|
+
|
6
|
+
def serializable_hash
|
7
|
+
{
|
8
|
+
'@context': DEFAULT_CONTEXT
|
9
|
+
}.merge(serialize_record(@record))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def serialize_record(record)
|
15
|
+
properties = record.properties.to_h.compact.transform_values do |value|
|
16
|
+
if value.is_a?(Array)
|
17
|
+
value.map { |v| serialize_value(v) }
|
18
|
+
else
|
19
|
+
serialize_value(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
{
|
24
|
+
'@type': record.type.to_s,
|
25
|
+
'@id': record.id
|
26
|
+
}.merge(properties).compact
|
27
|
+
end
|
28
|
+
|
29
|
+
def serialize_value(value)
|
30
|
+
if value.is_a?(Chronicle::Serialization::Record)
|
31
|
+
serialize_record(value)
|
32
|
+
else
|
33
|
+
value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Chronicle
|
2
|
+
module Serialization
|
3
|
+
class Record
|
4
|
+
attr_reader :properties,
|
5
|
+
:type,
|
6
|
+
:id,
|
7
|
+
:meta,
|
8
|
+
:schema
|
9
|
+
|
10
|
+
def initialize(properties: {}, type: nil, id: nil, meta: {}, schema: nil)
|
11
|
+
@properties = properties
|
12
|
+
@type = type
|
13
|
+
@id = id
|
14
|
+
@meta = meta
|
15
|
+
@schema = schema
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.build_from_model(model)
|
19
|
+
raise ArgumentError, 'model must be a Chronicle::Models::Base' unless model.is_a?(Chronicle::Models::Base)
|
20
|
+
|
21
|
+
properties = model.properties.transform_values do |v|
|
22
|
+
if v.is_a?(Array)
|
23
|
+
v.map do |record|
|
24
|
+
if record.is_a?(Chronicle::Models::Base)
|
25
|
+
Record.build_from_model(record)
|
26
|
+
else
|
27
|
+
record
|
28
|
+
end
|
29
|
+
end
|
30
|
+
elsif v.is_a?(Chronicle::Models::Base)
|
31
|
+
Record.build_from_model(v)
|
32
|
+
else
|
33
|
+
v
|
34
|
+
end
|
35
|
+
end.compact
|
36
|
+
|
37
|
+
new(
|
38
|
+
properties:,
|
39
|
+
type: model.type_id,
|
40
|
+
id: model.id,
|
41
|
+
meta: model.meta,
|
42
|
+
schema: :chronicle
|
43
|
+
)
|
44
|
+
end
|
45
|
+
|
46
|
+
def association_properties
|
47
|
+
# select properties whose values are a record, or are an array that contain a record
|
48
|
+
properties.select do |_k, v|
|
49
|
+
if v.is_a?(Array)
|
50
|
+
v.any? { |record| record.is_a?(Chronicle::Serialization::Record) }
|
51
|
+
else
|
52
|
+
v.is_a?(Chronicle::Serialization::Record)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def attribute_properties
|
58
|
+
properties.except(*association_properties.keys)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_h
|
62
|
+
properties.transform_values do |v|
|
63
|
+
if v.is_a?(Array)
|
64
|
+
v.map do |item|
|
65
|
+
value_to_h(item)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
value_to_h(v)
|
69
|
+
end
|
70
|
+
end.merge({ type:, id: }).compact
|
71
|
+
end
|
72
|
+
|
73
|
+
def value_to_h(v)
|
74
|
+
if v.is_a?(Array)
|
75
|
+
v.map do |item|
|
76
|
+
if item.is_a?(Chronicle::Serialization::Record)
|
77
|
+
item.to_h
|
78
|
+
else
|
79
|
+
item
|
80
|
+
end
|
81
|
+
end
|
82
|
+
elsif v.is_a?(Chronicle::Serialization::Record)
|
83
|
+
v.to_h
|
84
|
+
else
|
85
|
+
v
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|