chronicle-core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +26 -0
- 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 +108 -16
- data/lib/chronicle/schema/activity.rb +0 -5
- data/lib/chronicle/schema/base.rb +0 -64
- data/lib/chronicle/schema/entity.rb +0 -5
- data/lib/chronicle/schema/raw.rb +0 -9
- data/lib/chronicle/schema/validator.rb +0 -55
- data/lib/chronicle/utils/hash.rb +0 -15
@@ -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
|