sequel_mapper 0.0.1 → 0.0.3

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CODE_OF_CONDUCT.md +28 -0
  4. data/Gemfile.lock +32 -2
  5. data/MissingFeatures.md +64 -0
  6. data/README.md +141 -72
  7. data/Rakefile +29 -0
  8. data/TODO.md +16 -11
  9. data/features/env.rb +57 -0
  10. data/features/example.feature +121 -0
  11. data/features/step_definitions/example_steps.rb +46 -0
  12. data/lib/sequel_mapper.rb +6 -2
  13. data/lib/sequel_mapper/abstract_record.rb +53 -0
  14. data/lib/sequel_mapper/association_loaders.rb +52 -0
  15. data/lib/sequel_mapper/collection_mutability_proxy.rb +77 -0
  16. data/lib/sequel_mapper/configurations/conventional_association_configuration.rb +187 -0
  17. data/lib/sequel_mapper/configurations/conventional_configuration.rb +269 -0
  18. data/lib/sequel_mapper/dataset.rb +37 -0
  19. data/lib/sequel_mapper/deleted_record.rb +16 -0
  20. data/lib/sequel_mapper/dirty_map.rb +31 -0
  21. data/lib/sequel_mapper/graph_loader.rb +48 -0
  22. data/lib/sequel_mapper/graph_serializer.rb +107 -0
  23. data/lib/sequel_mapper/identity_map.rb +22 -0
  24. data/lib/sequel_mapper/lazy_object_proxy.rb +51 -0
  25. data/lib/sequel_mapper/many_to_many_association.rb +181 -0
  26. data/lib/sequel_mapper/many_to_one_association.rb +60 -0
  27. data/lib/sequel_mapper/mapper_facade.rb +180 -0
  28. data/lib/sequel_mapper/one_to_many_association.rb +51 -0
  29. data/lib/sequel_mapper/public_conveniencies.rb +27 -0
  30. data/lib/sequel_mapper/query_order.rb +32 -0
  31. data/lib/sequel_mapper/queryable_lazy_dataset_loader.rb +70 -0
  32. data/lib/sequel_mapper/relation_mapping.rb +35 -0
  33. data/lib/sequel_mapper/serializer.rb +18 -0
  34. data/lib/sequel_mapper/short_inspection_string.rb +18 -0
  35. data/lib/sequel_mapper/subset_queries_proxy.rb +11 -0
  36. data/lib/sequel_mapper/upserted_record.rb +15 -0
  37. data/lib/sequel_mapper/version.rb +1 -1
  38. data/sequel_mapper.gemspec +3 -0
  39. data/spec/config_override_spec.rb +167 -0
  40. data/spec/custom_serializers_spec.rb +77 -0
  41. data/spec/deletion_spec.rb +104 -0
  42. data/spec/graph_persistence_spec.rb +83 -88
  43. data/spec/graph_traversal_spec.rb +32 -31
  44. data/spec/new_graph_persistence_spec.rb +69 -0
  45. data/spec/object_identity_spec.rb +70 -0
  46. data/spec/ordered_association_spec.rb +46 -16
  47. data/spec/persistence_efficiency_spec.rb +186 -0
  48. data/spec/predefined_queries_spec.rb +73 -0
  49. data/spec/proxying_spec.rb +25 -19
  50. data/spec/querying_spec.rb +24 -27
  51. data/spec/readme_examples_spec.rb +35 -0
  52. data/spec/sequel_mapper/abstract_record_spec.rb +179 -0
  53. data/spec/sequel_mapper/{association_proxy_spec.rb → collection_mutability_proxy_spec.rb} +6 -6
  54. data/spec/sequel_mapper/deleted_record_spec.rb +59 -0
  55. data/spec/sequel_mapper/lazy_object_proxy_spec.rb +140 -0
  56. data/spec/sequel_mapper/public_conveniencies_spec.rb +49 -0
  57. data/spec/sequel_mapper/queryable_lazy_dataset_loader_spec.rb +103 -0
  58. data/spec/sequel_mapper/upserted_record_spec.rb +59 -0
  59. data/spec/spec_helper.rb +7 -10
  60. data/spec/support/blog_schema.rb +29 -0
  61. data/spec/support/have_persisted_matcher.rb +19 -0
  62. data/spec/support/mapper_setup.rb +234 -0
  63. data/spec/support/mock_sequel.rb +0 -1
  64. data/spec/support/object_graph_setup.rb +106 -0
  65. data/spec/support/seed_data_setup.rb +122 -0
  66. data/spec/support/sequel_persistence_setup.rb +19 -0
  67. data/spec/support/sequel_test_support.rb +159 -0
  68. metadata +121 -15
  69. data/lib/sequel_mapper/association_proxy.rb +0 -54
  70. data/lib/sequel_mapper/belongs_to_association_proxy.rb +0 -27
  71. data/lib/sequel_mapper/graph.rb +0 -174
  72. data/lib/sequel_mapper/queryable_association_proxy.rb +0 -23
  73. data/spec/sequel_mapper/belongs_to_association_proxy_spec.rb +0 -65
  74. data/spec/support/graph_fixture.rb +0 -331
  75. data/spec/support/query_counter.rb +0 -29
@@ -0,0 +1,187 @@
1
+ require "sequel_mapper/query_order"
2
+
3
+ module SequelMapper
4
+ module Configurations
5
+ require "sequel_mapper/one_to_many_association"
6
+ require "sequel_mapper/many_to_many_association"
7
+ require "sequel_mapper/many_to_one_association"
8
+ require "sequel_mapper/collection_mutability_proxy"
9
+ require "sequel_mapper/queryable_lazy_dataset_loader"
10
+ require "sequel_mapper/lazy_object_proxy"
11
+
12
+ class ConventionalAssociationConfiguration
13
+ def initialize(mapping_name, mappings, datastore)
14
+ @local_mapping_name = mapping_name
15
+ @mappings = mappings
16
+ @local_mapping = mappings.fetch(local_mapping_name)
17
+ @datastore = datastore
18
+ end
19
+
20
+ attr_reader :local_mapping_name, :local_mapping, :mappings, :datastore
21
+ private :local_mapping_name, :local_mapping, :mappings, :datastore
22
+
23
+ DEFAULT = Module.new
24
+
25
+ def has_many(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT, order_fields: DEFAULT, order_direction: DEFAULT)
26
+ defaults = {
27
+ mapping_name: association_name,
28
+ foreign_key: [INFLECTOR.singularize(local_mapping_name), "_id"].join.to_sym,
29
+ key: :id,
30
+ order_fields: [],
31
+ order_direction: "ASC",
32
+ }
33
+
34
+ specified = {
35
+ mapping_name: mapping_name,
36
+ foreign_key: foreign_key,
37
+ key: key,
38
+ order_fields: order_fields,
39
+ order_direction: order_direction,
40
+ }.reject { |_k,v|
41
+ v == DEFAULT
42
+ }
43
+
44
+ config = defaults.merge(specified)
45
+ associated_mapping_name = config.fetch(:mapping_name)
46
+ associated_mapping = mappings.fetch(associated_mapping_name)
47
+
48
+ local_mapping.add_association(
49
+ association_name,
50
+ has_many_mapper(**config)
51
+ )
52
+ end
53
+
54
+ def belongs_to(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT)
55
+ defaults = {
56
+ key: :id,
57
+ foreign_key: [association_name, "_id"].join.to_sym,
58
+ mapping_name: INFLECTOR.pluralize(association_name).to_sym,
59
+ }
60
+
61
+ specified = {
62
+ mapping_name: mapping_name,
63
+ foreign_key: foreign_key,
64
+ key: key,
65
+ }.reject { |_k,v|
66
+ v == DEFAULT
67
+ }
68
+
69
+ config = defaults.merge(specified)
70
+
71
+ associated_mapping_name = config.fetch(:mapping_name)
72
+ associated_mapping = mappings.fetch(associated_mapping_name)
73
+
74
+ local_mapping.add_association(
75
+ association_name,
76
+ belongs_to_mapper(**config)
77
+ )
78
+ end
79
+
80
+ def has_many_through(association_name, key: DEFAULT, foreign_key: DEFAULT, mapping_name: DEFAULT, through_mapping_name: DEFAULT, association_key: DEFAULT, association_foreign_key: DEFAULT, order_fields: DEFAULT, order_direction: DEFAULT)
81
+ defaults = {
82
+ mapping_name: association_name,
83
+ key: :id,
84
+ association_key: :id,
85
+ foreign_key: [INFLECTOR.singularize(local_mapping_name), "_id"].join.to_sym,
86
+ association_foreign_key: [INFLECTOR.singularize(association_name), "_id"].join.to_sym,
87
+ order_fields: [],
88
+ order_direction: "ASC",
89
+ }
90
+
91
+ specified = {
92
+ mapping_name: mapping_name,
93
+ key: key,
94
+ association_key: association_key,
95
+ foreign_key: foreign_key,
96
+ association_foreign_key: association_foreign_key,
97
+ order_fields: order_fields,
98
+ order_direction: order_direction,
99
+ }.reject { |_k,v|
100
+ v == DEFAULT
101
+ }
102
+
103
+ config = defaults.merge(specified)
104
+ associated_mapping = mappings.fetch(config.fetch(:mapping_name))
105
+
106
+ if through_mapping_name == DEFAULT
107
+ through_mapping_name = [
108
+ associated_mapping.name,
109
+ local_mapping.name,
110
+ ].sort.join("_to_").to_sym
111
+ end
112
+
113
+ join_table_name = mappings.fetch(through_mapping_name).namespace
114
+ config = config
115
+ .merge(
116
+ through_mapping_name: through_mapping_name,
117
+ through_dataset: datastore[join_table_name.to_sym],
118
+ )
119
+
120
+ local_mapping.add_association(
121
+ association_name,
122
+ has_many_through_mapper(**config)
123
+ )
124
+ end
125
+
126
+ private
127
+
128
+ def has_many_mapper(mapping_name:, key:, foreign_key:, order_fields:, order_direction:)
129
+ OneToManyAssociation.new(
130
+ mapping_name: mapping_name,
131
+ foreign_key: foreign_key,
132
+ key: key,
133
+ order: query_order(order_fields, order_direction),
134
+ proxy_factory: collection_proxy_factory,
135
+ )
136
+ end
137
+
138
+ def belongs_to_mapper(mapping_name:, key:, foreign_key:)
139
+ ManyToOneAssociation.new(
140
+ mapping_name: mapping_name,
141
+ foreign_key: foreign_key,
142
+ key: key,
143
+ proxy_factory: single_object_proxy_factory,
144
+ )
145
+ end
146
+
147
+ def has_many_through_mapper(mapping_name:, key:, foreign_key:, association_key:, association_foreign_key:, through_mapping_name:, through_dataset:, order_fields:, order_direction:)
148
+ ManyToManyAssociation.new(
149
+ mapping_name: mapping_name,
150
+ key: key,
151
+ foreign_key: foreign_key,
152
+ association_key: association_key,
153
+ association_foreign_key: association_foreign_key,
154
+ join_mapping_name: through_mapping_name,
155
+ join_dataset: through_dataset,
156
+ proxy_factory: collection_proxy_factory,
157
+ order: query_order(order_fields, order_direction),
158
+ )
159
+ end
160
+
161
+ def single_object_proxy_factory
162
+ ->(query:, loader:, preloaded_data:) {
163
+ LazyObjectProxy.new(
164
+ ->{ loader.call(query.first) },
165
+ preloaded_data,
166
+ )
167
+ }
168
+ end
169
+
170
+ def collection_proxy_factory
171
+ ->(query:, loader:, mapping_name:) {
172
+ CollectionMutabilityProxy.new(
173
+ QueryableLazyDatasetLoader.new(
174
+ query,
175
+ loader,
176
+ mappings.fetch(mapping_name).subsets,
177
+ )
178
+ )
179
+ }
180
+ end
181
+
182
+ def query_order(fields, direction)
183
+ QueryOrder.new(fields: fields, direction: direction)
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,269 @@
1
+ require "sequel_mapper/configurations/conventional_association_configuration"
2
+ require "sequel_mapper/relation_mapping"
3
+ require "sequel_mapper/subset_queries_proxy"
4
+ require "sequel_mapper/struct_factory"
5
+
6
+ module SequelMapper
7
+ module Configurations
8
+ require "active_support/inflector"
9
+ class Inflector
10
+ include ActiveSupport::Inflector
11
+ end
12
+
13
+ INFLECTOR = Inflector.new
14
+
15
+ require "fetchable"
16
+ class ConventionalConfiguration
17
+ include Fetchable
18
+
19
+ def initialize(datastore)
20
+ @datastore = datastore
21
+ @overrides = {}
22
+ @subset_queries = {}
23
+ @associations_by_mapping = {}
24
+ end
25
+
26
+ attr_reader :datastore, :mappings
27
+ private :datastore, :mappings
28
+
29
+ def [](mapping_name)
30
+ mappings[mapping_name]
31
+ end
32
+
33
+ def setup_mapping(mapping_name, &block)
34
+ @associations_by_mapping[mapping_name] ||= []
35
+
36
+ block.call(
37
+ RelationConfigOptionsProxy.new(
38
+ method(:add_override).to_proc.curry.call(mapping_name),
39
+ method(:add_subset).to_proc.curry.call(mapping_name),
40
+ @associations_by_mapping.fetch(mapping_name),
41
+ )
42
+ ) if block
43
+
44
+ # TODO: more madness in this silly config this, kill it with fire.
45
+ explicit_settings = @overrides[mapping_name] ||= {}
46
+ explicit_settings[:factory] ||= raise_if_not_found_factory(mapping_name)
47
+
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ class RelationConfigOptionsProxy
54
+ def initialize(config_override, subset_adder, association_register)
55
+ @config_override = config_override
56
+ @subset_adder = subset_adder
57
+ @association_register = association_register
58
+ end
59
+
60
+ def relation_name(name)
61
+ @config_override.call(relation_name: name)
62
+ end
63
+ alias_method :table_name, :relation_name
64
+
65
+ def subset(subset_name, &block)
66
+ @subset_adder.call(subset_name, block)
67
+ end
68
+
69
+ def has_many(*args)
70
+ @association_register.push([:has_many, args])
71
+ end
72
+
73
+ def has_many_through(*args)
74
+ @association_register.push([:has_many_through, args])
75
+ end
76
+
77
+ def belongs_to(*args)
78
+ @association_register.push([:belongs_to, args])
79
+ end
80
+
81
+ def factory(callable)
82
+ @config_override.call(factory: callable)
83
+ end
84
+
85
+ def class(entity_class)
86
+ @config_override.call('class': entity_class)
87
+ end
88
+
89
+ def class_name(class_name)
90
+ @config_override.call(class_name: class_name)
91
+ end
92
+
93
+ def serializer(serializer_func)
94
+ @config_override.call(serializer: serializer_func)
95
+ end
96
+ end
97
+
98
+ def mappings
99
+ @mappings ||= generate_mappings
100
+ end
101
+
102
+ def add_override(mapping_name, attrs)
103
+ overrides = @overrides.fetch(mapping_name, {}).merge(attrs)
104
+
105
+ @overrides.store(mapping_name, overrides)
106
+ end
107
+
108
+ def add_subset(mapping_name, subset_name, block)
109
+ @subset_queries.store(
110
+ mapping_name,
111
+ @subset_queries.fetch(mapping_name, {}).merge(
112
+ subset_name => block,
113
+ )
114
+ )
115
+ end
116
+
117
+ def association_configurator(mappings, mapping_name)
118
+ ConventionalAssociationConfiguration.new(
119
+ mapping_name,
120
+ mappings,
121
+ datastore,
122
+ )
123
+ end
124
+
125
+ def generate_mappings
126
+ Hash[
127
+ tables
128
+ .map { |table_name|
129
+ mapping_name, overrides = overrides_for_table(table_name)
130
+
131
+ [
132
+ mapping_name,
133
+ mapping(
134
+ **default_mapping_args(table_name, mapping_name).merge(overrides)
135
+ ),
136
+ ]
137
+ }
138
+ ].tap { |mappings|
139
+ generate_associations_config(mappings)
140
+ }
141
+ end
142
+
143
+ def generate_associations_config(mappings)
144
+ # TODO: the ConventionalAssociationConfiguration takes all the mappings
145
+ # as a dependency and then sends mutating messages to them.
146
+ # This mutation based approach was originally a spike but now just
147
+ # seems totally bananas!
148
+ @associations_by_mapping.each do |mapping_name, associations|
149
+ associations.each do |(assoc_type, assoc_args)|
150
+ association_configurator(mappings, mapping_name)
151
+ .public_send(assoc_type, *assoc_args)
152
+ end
153
+ end
154
+ end
155
+
156
+ def default_mapping_args(table_name, mapping_name)
157
+ {
158
+ name: mapping_name,
159
+ relation_name: table_name,
160
+ fields: get_fields(table_name),
161
+ primary_key: get_primary_key(table_name),
162
+ factory: ok_if_it_doesnt_exist_factory(mapping_name),
163
+ serializer: hash_coercion_serializer,
164
+ associations: {},
165
+ subsets: subset_queries_proxy(@subset_queries.fetch(mapping_name, {})),
166
+ }
167
+ end
168
+
169
+ def overrides_for_table(table_name)
170
+ mapping_name, overrides = @overrides
171
+ .find { |(_mapping_name, config)|
172
+ table_name == config.fetch(:relation_name, nil)
173
+ } || [table_name, @overrides.fetch(table_name, {})]
174
+
175
+ [mapping_name, consolidate_overrides(overrides)]
176
+ end
177
+
178
+ def consolidate_overrides(opts)
179
+ new_opts = opts.dup
180
+
181
+ if new_opts.has_key?(:class_name)
182
+ new_opts.merge!(factory: string_to_factory(new_opts.fetch(:class_name)))
183
+ new_opts.delete(:class_name)
184
+ end
185
+
186
+ if new_opts.has_key?(:class)
187
+ new_opts.merge!(factory: class_to_factory(new_opts.fetch(:class)))
188
+ new_opts.delete(:class)
189
+ end
190
+
191
+ new_opts
192
+ end
193
+
194
+ def get_fields(table_name)
195
+ datastore[table_name].columns
196
+ end
197
+
198
+ def get_primary_key(table_name)
199
+ datastore.schema(table_name)
200
+ .select { |field_name, properties|
201
+ properties.fetch(:primary_key)
202
+ }
203
+ .map { |field_name, _| field_name }
204
+ end
205
+
206
+ def tables
207
+ (datastore.tables - [:schema_migrations])
208
+ end
209
+
210
+ def hash_coercion_serializer
211
+ ->(o) { o.to_h }
212
+ end
213
+
214
+ def subset_queries_proxy(subset_map)
215
+ SubsetQueriesProxy.new(subset_map)
216
+ end
217
+
218
+ def mapping(name:, relation_name:, primary_key:, factory:, serializer:, fields:, associations:, subsets:)
219
+ RelationMapping.new(
220
+ name: name,
221
+ namespace: relation_name,
222
+ primary_key: primary_key,
223
+ factory: factory,
224
+ serializer: serializer,
225
+ fields: fields,
226
+ associations: associations,
227
+ subsets: subsets,
228
+ )
229
+ end
230
+
231
+ FactoryNotFoundError = Class.new(StandardError) do
232
+ def initialize(specified)
233
+ @specified = specified
234
+ end
235
+
236
+ def message
237
+ "Could not find factory for #{@specified}"
238
+ end
239
+ end
240
+
241
+ def raise_if_not_found_factory(name)
242
+ ->(attrs) {
243
+ class_to_factory(string_to_class(name)).call(attrs)
244
+ }
245
+ end
246
+
247
+ def ok_if_it_doesnt_exist_factory(name)
248
+ ->(attrs) {
249
+ factory = class_to_factory(string_to_class(name)) rescue nil
250
+ factory && factory.call(attrs)
251
+ }
252
+ end
253
+
254
+ def class_to_factory(klass)
255
+ if klass.ancestors.include?(Struct)
256
+ StructFactory.new(klass)
257
+ else
258
+ klass.method(:new)
259
+ end
260
+ end
261
+
262
+ def string_to_class(string)
263
+ klass_name = INFLECTOR.classify(string)
264
+
265
+ Object.const_get(klass_name)
266
+ end
267
+ end
268
+ end
269
+ end