terrestrial 0.1.0 → 0.1.1

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