terrestrial 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -9
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +28 -0
  6. data/Gemfile.lock +73 -0
  7. data/LICENSE.txt +22 -0
  8. data/MissingFeatures.md +64 -0
  9. data/README.md +161 -16
  10. data/Rakefile +30 -0
  11. data/TODO.md +41 -0
  12. data/features/env.rb +60 -0
  13. data/features/example.feature +120 -0
  14. data/features/step_definitions/example_steps.rb +46 -0
  15. data/lib/terrestrial/abstract_record.rb +99 -0
  16. data/lib/terrestrial/association_loaders.rb +52 -0
  17. data/lib/terrestrial/collection_mutability_proxy.rb +81 -0
  18. data/lib/terrestrial/configurations/conventional_association_configuration.rb +186 -0
  19. data/lib/terrestrial/configurations/conventional_configuration.rb +302 -0
  20. data/lib/terrestrial/dataset.rb +49 -0
  21. data/lib/terrestrial/deleted_record.rb +20 -0
  22. data/lib/terrestrial/dirty_map.rb +42 -0
  23. data/lib/terrestrial/graph_loader.rb +63 -0
  24. data/lib/terrestrial/graph_serializer.rb +91 -0
  25. data/lib/terrestrial/identity_map.rb +22 -0
  26. data/lib/terrestrial/lazy_collection.rb +74 -0
  27. data/lib/terrestrial/lazy_object_proxy.rb +55 -0
  28. data/lib/terrestrial/many_to_many_association.rb +138 -0
  29. data/lib/terrestrial/many_to_one_association.rb +66 -0
  30. data/lib/terrestrial/mapper_facade.rb +137 -0
  31. data/lib/terrestrial/one_to_many_association.rb +66 -0
  32. data/lib/terrestrial/public_conveniencies.rb +139 -0
  33. data/lib/terrestrial/query_order.rb +32 -0
  34. data/lib/terrestrial/relation_mapping.rb +50 -0
  35. data/lib/terrestrial/serializer.rb +18 -0
  36. data/lib/terrestrial/short_inspection_string.rb +18 -0
  37. data/lib/terrestrial/struct_factory.rb +17 -0
  38. data/lib/terrestrial/subset_queries_proxy.rb +11 -0
  39. data/lib/terrestrial/upserted_record.rb +15 -0
  40. data/lib/terrestrial/version.rb +1 -1
  41. data/lib/terrestrial.rb +5 -2
  42. data/sequel_mapper.gemspec +31 -0
  43. data/spec/config_override_spec.rb +193 -0
  44. data/spec/custom_serializers_spec.rb +49 -0
  45. data/spec/deletion_spec.rb +101 -0
  46. data/spec/graph_persistence_spec.rb +313 -0
  47. data/spec/graph_traversal_spec.rb +121 -0
  48. data/spec/new_graph_persistence_spec.rb +71 -0
  49. data/spec/object_identity_spec.rb +70 -0
  50. data/spec/ordered_association_spec.rb +51 -0
  51. data/spec/persistence_efficiency_spec.rb +224 -0
  52. data/spec/predefined_queries_spec.rb +62 -0
  53. data/spec/proxying_spec.rb +88 -0
  54. data/spec/querying_spec.rb +48 -0
  55. data/spec/readme_examples_spec.rb +35 -0
  56. data/spec/sequel_mapper/abstract_record_spec.rb +244 -0
  57. data/spec/sequel_mapper/collection_mutability_proxy_spec.rb +135 -0
  58. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  59. data/spec/sequel_mapper/dirty_map_spec.rb +214 -0
  60. data/spec/sequel_mapper/lazy_collection_spec.rb +119 -0
  61. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  62. data/spec/sequel_mapper/public_conveniencies_spec.rb +58 -0
  63. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  64. data/spec/spec_helper.rb +36 -0
  65. data/spec/support/blog_schema.rb +38 -0
  66. data/spec/support/have_persisted_matcher.rb +19 -0
  67. data/spec/support/mapper_setup.rb +221 -0
  68. data/spec/support/mock_sequel.rb +193 -0
  69. data/spec/support/object_graph_setup.rb +139 -0
  70. data/spec/support/seed_data_setup.rb +165 -0
  71. data/spec/support/sequel_persistence_setup.rb +19 -0
  72. data/spec/support/sequel_test_support.rb +166 -0
  73. metadata +207 -13
  74. data/.travis.yml +0 -4
  75. data/bin/console +0 -14
  76. data/bin/setup +0 -7
  77. data/terrestrial.gemspec +0 -23
@@ -0,0 +1,55 @@
1
+ module Terrestrial
2
+ class LazyObjectProxy
3
+ include ShortInspectionString
4
+
5
+ def initialize(object_loader, key_fields)
6
+ @object_loader = object_loader
7
+ @key_fields = key_fields
8
+ @lazy_object = nil
9
+ end
10
+
11
+ attr_reader :object_loader
12
+ private :object_loader
13
+
14
+ def method_missing(method_id, *args, &block)
15
+ if args.empty? && __key_fields.include?(method_id)
16
+ __key_fields.fetch(method_id)
17
+ else
18
+ lazy_object.public_send(method_id, *args, &block)
19
+ end
20
+ end
21
+
22
+ def loaded?
23
+ !!@lazy_object
24
+ end
25
+
26
+ def __getobj__
27
+ lazy_object
28
+ end
29
+
30
+ def each_loaded(&block)
31
+ [self].each(&block)
32
+ end
33
+
34
+ def __key_fields
35
+ @key_fields
36
+ end
37
+
38
+ private
39
+
40
+ def respond_to_missing?(method_id, _include_private = false)
41
+ __key_fields.include?(method_id) || lazy_object.respond_to?(method_id)
42
+ end
43
+
44
+ def lazy_object
45
+ @lazy_object ||= object_loader.call
46
+ end
47
+
48
+ def inspectable_properties
49
+ [
50
+ :key_fields,
51
+ :lazy_object,
52
+ ]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,138 @@
1
+ require "forwardable"
2
+ require "terrestrial/dataset"
3
+
4
+ module Terrestrial
5
+ class ManyToManyAssociation
6
+ def initialize(mapping_name:, join_mapping_name:, foreign_key:, key:, proxy_factory:, association_foreign_key:, association_key:, order:)
7
+ @mapping_name = mapping_name
8
+ @join_mapping_name = join_mapping_name
9
+ @foreign_key = foreign_key
10
+ @key = key
11
+ @proxy_factory = proxy_factory
12
+ @association_foreign_key = association_foreign_key
13
+ @association_key = association_key
14
+ @order = order
15
+ end
16
+
17
+ def mapping_names
18
+ [mapping_name, join_mapping_name]
19
+ end
20
+
21
+ attr_reader :mapping_name, :join_mapping_name
22
+
23
+ attr_reader :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
24
+ private :foreign_key, :key, :proxy_factory, :association_key, :association_foreign_key, :order
25
+
26
+ def build_proxy(data_superset:, loader:, record:)
27
+ proxy_factory.call(
28
+ query: build_query(data_superset, record),
29
+ loader: ->(record_list) {
30
+ record = record_list.first
31
+ join_records = record_list.last
32
+
33
+ loader.call(record, join_records)
34
+ },
35
+ mapping_name: mapping_name,
36
+ )
37
+ end
38
+
39
+ def eager_superset((superset, join_superset), (associated_dataset))
40
+ join_data = Dataset.new(
41
+ join_superset
42
+ .where(foreign_key => associated_dataset.select(association_key))
43
+ .to_a
44
+ )
45
+
46
+ eager_superset = Dataset.new(
47
+ superset.where(key => join_data.select(association_foreign_key)).to_a
48
+ )
49
+
50
+ [
51
+ eager_superset,
52
+ join_data,
53
+ ]
54
+ end
55
+
56
+ def build_query((superset, join_superset), parent_record)
57
+ ids = join_superset
58
+ .where(foreign_key => foreign_key_value(parent_record))
59
+ .select(association_foreign_key)
60
+
61
+ order
62
+ .apply(
63
+ superset.where(
64
+ key => ids
65
+ )
66
+ )
67
+ .lazy.map { |record|
68
+ [record, [foreign_keys(parent_record, record)]]
69
+ }
70
+ end
71
+
72
+ def dump(parent_record, collection, depth, &block)
73
+ flat_list_of_records_and_join_records(parent_record, collection, depth, &block)
74
+ end
75
+
76
+ def extract_foreign_key(_record)
77
+ {}
78
+ end
79
+
80
+ def delete(parent_record, collection, depth, &block)
81
+ flat_list_of_just_join_records(parent_record, collection, depth, &block)
82
+ end
83
+
84
+ private
85
+
86
+ def flat_list_of_records_and_join_records(parent_record, collection, depth, &block)
87
+ record_join_record_pairs(parent_record, collection, depth, &block).flatten(1)
88
+ end
89
+
90
+ def flat_list_of_just_join_records(parent_record, collection, depth, &block)
91
+ record_join_record_pairs(parent_record, collection, depth, &block)
92
+ .map { |(_records, join_records)| join_records }
93
+ .flatten(1)
94
+ end
95
+
96
+ def record_join_record_pairs(parent_record, collection, depth, &block)
97
+ (collection || []).map { |associated_object|
98
+ record, *other_join_records = block.call(
99
+ mapping_name,
100
+ associated_object,
101
+ no_foreign_key = {},
102
+ depth + depth_modifier,
103
+ )
104
+
105
+ fks = foreign_keys(parent_record, record)
106
+ join_record_depth = depth + join_record_depth_modifier
107
+
108
+ join_records = block.call(
109
+ join_mapping_name,
110
+ fks,
111
+ fks,
112
+ join_record_depth
113
+ ).flatten(1)
114
+
115
+ [record] + other_join_records + join_records
116
+ }
117
+ end
118
+
119
+ def foreign_keys(parent_record, record)
120
+ {
121
+ foreign_key => foreign_key_value(parent_record),
122
+ association_foreign_key => record.fetch(association_key),
123
+ }
124
+ end
125
+
126
+ def foreign_key_value(record)
127
+ record.fetch(key)
128
+ end
129
+
130
+ def depth_modifier
131
+ 0
132
+ end
133
+
134
+ def join_record_depth_modifier
135
+ +1
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,66 @@
1
+ require "terrestrial/dataset"
2
+
3
+ module Terrestrial
4
+ class ManyToOneAssociation
5
+ def initialize(mapping_name:, foreign_key:, key:, proxy_factory:)
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @proxy_factory = proxy_factory
10
+ end
11
+
12
+ def mapping_names
13
+ [mapping_name]
14
+ end
15
+
16
+ attr_reader :mapping_name
17
+
18
+ attr_reader :foreign_key, :key, :proxy_factory
19
+ private :foreign_key, :key, :proxy_factory
20
+
21
+ def build_proxy(data_superset:, loader:, record:)
22
+ proxy_factory.call(
23
+ query: build_query(data_superset, record),
24
+ loader: loader,
25
+ preloaded_data: {
26
+ key => foreign_key_value(record),
27
+ },
28
+ )
29
+ end
30
+
31
+ def eager_superset((superset), (associated_dataset))
32
+ [
33
+ Dataset.new(
34
+ superset.where(key => associated_dataset.select(foreign_key)).to_a
35
+ )
36
+ ]
37
+ end
38
+
39
+ def build_query((superset), record)
40
+ superset.where(key => foreign_key_value(record))
41
+ end
42
+
43
+ def dump(parent_record, collection, depth, &block)
44
+ collection.flat_map { |object|
45
+ block.call(mapping_name, object, _foreign_key_does_not_go_here = {}, depth + depth_modifier)
46
+ }
47
+ end
48
+ alias_method :delete, :dump
49
+
50
+ def extract_foreign_key(record)
51
+ {
52
+ foreign_key => record.fetch(key),
53
+ }
54
+ end
55
+
56
+ private
57
+
58
+ def foreign_key_value(record)
59
+ record.fetch(foreign_key)
60
+ end
61
+
62
+ def depth_modifier
63
+ -1
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,137 @@
1
+ require "terrestrial/graph_serializer"
2
+ require "terrestrial/graph_loader"
3
+
4
+ module Terrestrial
5
+ class MapperFacade
6
+ include Enumerable
7
+
8
+ def initialize(mappings:, mapping_name:, datastore:, dataset:, load_pipeline:, dump_pipeline:)
9
+ @mappings = mappings
10
+ @mapping_name = mapping_name
11
+ @datastore = datastore
12
+ @dataset = dataset
13
+ @load_pipeline = load_pipeline
14
+ @dump_pipeline = dump_pipeline
15
+ @eager_data = {}
16
+ end
17
+
18
+ attr_reader :mappings, :mapping_name, :datastore, :dataset, :load_pipeline, :dump_pipeline
19
+ private :mappings, :mapping_name, :datastore, :dataset, :load_pipeline, :dump_pipeline
20
+
21
+ def save(graph)
22
+ record_dump = graph_serializer.call(mapping_name, graph)
23
+
24
+ dump_pipeline.call(record_dump)
25
+
26
+ self
27
+ end
28
+
29
+ def all
30
+ self
31
+ end
32
+
33
+ def where(query)
34
+ new_with_dataset(
35
+ dataset.where(query)
36
+ )
37
+ end
38
+
39
+ def subset(name, *params)
40
+ new_with_dataset(
41
+ mapping.subsets.execute(dataset, name, *params)
42
+ )
43
+ end
44
+
45
+ def each(&block)
46
+ dataset
47
+ .map { |record|
48
+ graph_loader.call(mapping_name, record, @eager_data)
49
+ }
50
+ .each(&block)
51
+ end
52
+
53
+ def eager_load(association_name_map)
54
+ @eager_data = eager_load_associations(mapping, dataset, association_name_map)
55
+
56
+ self
57
+ end
58
+
59
+ def delete(object, cascade: false)
60
+ dump_pipeline.call(
61
+ graph_serializer.call(mapping_name, object)
62
+ .select { |record| record.depth == 0 }
63
+ .reverse
64
+ .take(1)
65
+ .map { |record|
66
+ DeletedRecord.new(record.namespace, record.identity)
67
+ }
68
+ )
69
+ end
70
+
71
+ private
72
+
73
+ def mapping
74
+ mappings.fetch(mapping_name)
75
+ end
76
+
77
+ def eager_load_associations(mapping, parent_dataset, association_name_map)
78
+ Hash[
79
+ association_name_map.map { |name, deeper_association_names|
80
+ association = mapping.associations.fetch(name)
81
+ association_mapping = mappings.fetch(association.mapping_name)
82
+ association_dataset = get_eager_dataset(association, parent_dataset)
83
+
84
+ [
85
+ name,
86
+ {
87
+ superset: association_dataset,
88
+ associations: eager_load_associations(
89
+ association_mapping,
90
+ association_dataset,
91
+ deeper_association_names,
92
+ ),
93
+ }
94
+ ]
95
+ }
96
+ ]
97
+ end
98
+
99
+ def get_eager_dataset(association, parent_dataset)
100
+ association.eager_superset(
101
+ association_root_datasets(association),
102
+ parent_dataset,
103
+ )
104
+ end
105
+
106
+ def association_root_datasets(association)
107
+ association
108
+ .mapping_names
109
+ .map { |name| mappings.fetch(name) }
110
+ .map(&:namespace)
111
+ .map { |ns| datastore[ns] }
112
+ end
113
+
114
+ def new_with_dataset(new_dataset)
115
+ self.class.new(
116
+ dataset: new_dataset,
117
+ mappings: mappings,
118
+ mapping_name: mapping_name,
119
+ datastore: datastore,
120
+ load_pipeline: load_pipeline,
121
+ dump_pipeline: dump_pipeline,
122
+ )
123
+ end
124
+
125
+ def graph_serializer
126
+ GraphSerializer.new(mappings: mappings)
127
+ end
128
+
129
+ def graph_loader
130
+ GraphLoader.new(
131
+ datasets: datastore,
132
+ mappings: mappings,
133
+ object_load_pipeline: load_pipeline,
134
+ )
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,66 @@
1
+ require "terrestrial/dataset"
2
+
3
+ module Terrestrial
4
+ class OneToManyAssociation
5
+ def initialize(mapping_name:, foreign_key:, key:, order:, proxy_factory:)
6
+ @mapping_name = mapping_name
7
+ @foreign_key = foreign_key
8
+ @key = key
9
+ @order = order
10
+ @proxy_factory = proxy_factory
11
+ end
12
+
13
+ def mapping_names
14
+ [mapping_name]
15
+ end
16
+ attr_reader :mapping_name
17
+
18
+ attr_reader :foreign_key, :key, :order, :proxy_factory
19
+ private :foreign_key, :key, :order, :proxy_factory
20
+
21
+ def build_proxy(data_superset:, loader:, record:)
22
+ proxy_factory.call(
23
+ query: build_query(data_superset, record),
24
+ loader: loader,
25
+ mapping_name: mapping_name,
26
+ )
27
+ end
28
+
29
+ def dump(parent_record, collection, depth, &block)
30
+ foreign_key_pair = {
31
+ foreign_key => parent_record.fetch(key),
32
+ }
33
+
34
+ collection.flat_map { |associated_object|
35
+ block.call(mapping_name, associated_object, foreign_key_pair, depth + depth_modifier)
36
+ }
37
+ end
38
+ alias_method :delete, :dump
39
+
40
+ def extract_foreign_key(_record)
41
+ {}
42
+ end
43
+
44
+ def eager_superset((superset), (associated_dataset))
45
+ [
46
+ Dataset.new(
47
+ superset.where(
48
+ foreign_key => associated_dataset.select(key)
49
+ ).to_a
50
+ )
51
+ ]
52
+ end
53
+
54
+ def build_query((superset), record)
55
+ order.apply(
56
+ superset.where(foreign_key => record.fetch(key))
57
+ )
58
+ end
59
+
60
+ private
61
+
62
+ def depth_modifier
63
+ +1
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,139 @@
1
+ require "terrestrial/identity_map"
2
+ require "terrestrial/dirty_map"
3
+ require "terrestrial/upserted_record"
4
+ require "terrestrial/mapper_facade"
5
+ require "terrestrial/configurations/conventional_configuration"
6
+
7
+ module Terrestrial
8
+ module PublicConveniencies
9
+ def config(database_connection)
10
+ Configurations::ConventionalConfiguration.new(database_connection)
11
+ end
12
+
13
+ def mappers(mappings:, datastore:)
14
+ dirty_map = build_dirty_map
15
+ identity_map = build_identity_map
16
+
17
+ Hash[mappings.map { |name, _mapping|
18
+ [
19
+ name,
20
+ mapper(
21
+ mappings: mappings ,
22
+ name: name,
23
+ datastore: datastore,
24
+ identity_map: identity_map,
25
+ dirty_map: dirty_map,
26
+ )
27
+ ]
28
+ }]
29
+ end
30
+
31
+ private
32
+
33
+ def mapper(mappings:, name:, datastore:, identity_map:, dirty_map:)
34
+ dataset = datastore[mappings.fetch(name).namespace]
35
+
36
+ MapperFacade.new(
37
+ mappings: mappings,
38
+ mapping_name: name,
39
+ datastore: datastore,
40
+ dataset: dataset,
41
+ load_pipeline: build_load_pipeline(
42
+ dirty_map: dirty_map,
43
+ identity_map: identity_map,
44
+ ),
45
+ dump_pipeline: build_dump_pipeline(
46
+ dirty_map: dirty_map,
47
+ transaction: datastore.method(:transaction),
48
+ upsert: method(:upsert_record).curry.call(datastore),
49
+ delete: method(:delete_record).curry.call(datastore),
50
+ )
51
+ )
52
+ end
53
+
54
+ private
55
+
56
+ def build_identity_map(storage = {})
57
+ IdentityMap.new(storage)
58
+ end
59
+
60
+ def build_dirty_map(storage = {})
61
+ DirtyMap.new(storage)
62
+ end
63
+
64
+ def build_load_pipeline(dirty_map:, identity_map:)
65
+ ->(mapping, record, associated_fields = {}) {
66
+ [
67
+ record_factory(mapping),
68
+ dirty_map.method(:load),
69
+ ->(record) {
70
+ attributes = record.to_h.select { |k,_v|
71
+ mapping.fields.include?(k)
72
+ }
73
+
74
+ object = mapping.factory.call(attributes.merge(associated_fields))
75
+ identity_map.call(mapping, record, object)
76
+ },
77
+ ].reduce(record) { |agg, operation|
78
+ operation.call(agg)
79
+ }
80
+ }
81
+ end
82
+
83
+ def build_dump_pipeline(dirty_map:, transaction:, upsert:, delete:)
84
+ ->(records) {
85
+ [
86
+ :uniq.to_proc,
87
+ ->(rs) { rs.select { |r| dirty_map.dirty?(r) } },
88
+ ->(rs) { rs.map { |r| dirty_map.reject_unchanged_fields(r) } },
89
+ ->(rs) { rs.sort_by(&:depth) },
90
+ ->(rs) {
91
+ transaction.call {
92
+ rs.each { |r|
93
+ r.if_upsert(&upsert)
94
+ .if_delete(&delete)
95
+ }
96
+ }
97
+ },
98
+ ].reduce(records) { |agg, operation|
99
+ operation.call(agg)
100
+ }
101
+ }
102
+ end
103
+
104
+ def record_factory(mapping)
105
+ ->(record_hash) {
106
+ identity = Hash[
107
+ mapping.primary_key.map { |field|
108
+ [field, record_hash.fetch(field)]
109
+ }
110
+ ]
111
+
112
+ UpsertedRecord.new(
113
+ mapping.namespace,
114
+ identity,
115
+ record_hash,
116
+ )
117
+ }
118
+ end
119
+
120
+ def upsert_record(datastore, record)
121
+ row_count = 0
122
+ unless record.non_identity_attributes.empty?
123
+ row_count = datastore[record.namespace].
124
+ where(record.identity).
125
+ update(record.non_identity_attributes)
126
+ end
127
+
128
+ if row_count < 1
129
+ row_count = datastore[record.namespace].insert(record.to_h)
130
+ end
131
+
132
+ row_count
133
+ end
134
+
135
+ def delete_record(datastore, record)
136
+ datastore[record.namespace].where(record.identity).delete
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,32 @@
1
+ module Terrestrial
2
+ class QueryOrder
3
+ def initialize(fields:, direction:)
4
+ @fields = fields
5
+ @direction_function = get_direction_function(direction.to_s.upcase)
6
+ end
7
+
8
+ attr_reader :fields, :direction_function
9
+
10
+ def apply(dataset)
11
+ if fields.any?
12
+ apply_direction(dataset.order(fields))
13
+ else
14
+ dataset
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def apply_direction(dataset)
21
+ direction_function.call(dataset)
22
+ end
23
+
24
+ # TODO: Consider a nicer API for this and push this into SequelAdapter
25
+ def get_direction_function(direction)
26
+ {
27
+ "ASC" => ->(x){x},
28
+ "DESC" => :reverse.to_proc,
29
+ }.fetch(direction) { raise "Unsupported sort option #{direction}. Choose one of [ASC, DESC]." }
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ module Terrestrial
2
+ class RelationMapping
3
+ def initialize(name:, namespace:, fields:, primary_key:, factory:, serializer:, associations:, subsets:)
4
+ @name = name
5
+ @namespace = namespace
6
+ @fields = fields
7
+ @primary_key = primary_key
8
+ @factory = factory
9
+ @serializer = serializer
10
+ @associations = associations
11
+ @subsets = subsets
12
+ end
13
+
14
+ attr_reader :name, :namespace, :fields, :primary_key, :factory, :serializer, :associations, :subsets
15
+
16
+ def add_association(name, new_association)
17
+ @associations = associations.merge(name => new_association)
18
+ end
19
+
20
+ def serialize(object, depth, foreign_keys = {})
21
+ object_attributes = serializer.call(object)
22
+
23
+ [
24
+ record(object_attributes, depth, foreign_keys),
25
+ extract_associations(object_attributes)
26
+ ]
27
+ end
28
+
29
+ def record(attributes, depth, foreign_keys)
30
+ UpsertedRecord.new(
31
+ namespace,
32
+ primary_key,
33
+ select_mapped_fields(attributes).merge(foreign_keys),
34
+ depth,
35
+ )
36
+ end
37
+
38
+ def extract_associations(attributes)
39
+ Hash[
40
+ associations.map { |name, _association|
41
+ [ name, attributes.fetch(name) ]
42
+ }
43
+ ]
44
+ end
45
+
46
+ def select_mapped_fields(attributes)
47
+ attributes.select { |name, _value| fields.include?(name) }
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,18 @@
1
+ module Terrestrial
2
+ class Serializer
3
+ def initialize(field_names, object)
4
+ @field_names = field_names
5
+ @object = object
6
+ end
7
+
8
+ attr_reader :field_names, :object
9
+
10
+ def to_h
11
+ Hash[
12
+ field_names.map { |field_name|
13
+ [field_name, object.public_send(field_name)]
14
+ }
15
+ ]
16
+ end
17
+ end
18
+ end