rom 5.4.2 → 6.0.0.alpha1

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -71
  3. data/LICENSE +1 -1
  4. data/README.md +7 -6
  5. data/lib/rom/array_dataset.rb +46 -0
  6. data/lib/rom/associations/abstract.rb +217 -0
  7. data/lib/rom/associations/definitions/abstract.rb +150 -0
  8. data/lib/rom/associations/definitions/many_to_many.rb +29 -0
  9. data/lib/rom/associations/definitions/many_to_one.rb +14 -0
  10. data/lib/rom/associations/definitions/one_to_many.rb +14 -0
  11. data/lib/rom/associations/definitions/one_to_one.rb +14 -0
  12. data/lib/rom/associations/definitions/one_to_one_through.rb +14 -0
  13. data/lib/rom/associations/definitions.rb +7 -0
  14. data/lib/rom/associations/many_to_many.rb +128 -0
  15. data/lib/rom/associations/many_to_one.rb +65 -0
  16. data/lib/rom/associations/one_to_many.rb +65 -0
  17. data/lib/rom/associations/one_to_one.rb +13 -0
  18. data/lib/rom/associations/one_to_one_through.rb +13 -0
  19. data/lib/rom/associations/through_identifier.rb +41 -0
  20. data/lib/rom/attribute.rb +425 -0
  21. data/lib/rom/auto_curry.rb +70 -0
  22. data/lib/rom/cache.rb +87 -0
  23. data/lib/rom/changeset/associated.rb +110 -0
  24. data/lib/rom/changeset/create.rb +18 -0
  25. data/lib/rom/changeset/delete.rb +15 -0
  26. data/lib/rom/changeset/extensions/relation.rb +26 -0
  27. data/lib/rom/changeset/pipe.rb +81 -0
  28. data/lib/rom/changeset/pipe_registry.rb +27 -0
  29. data/lib/rom/changeset/stateful.rb +285 -0
  30. data/lib/rom/changeset/update.rb +81 -0
  31. data/lib/rom/changeset.rb +185 -0
  32. data/lib/rom/command.rb +351 -0
  33. data/lib/rom/command_compiler.rb +201 -0
  34. data/lib/rom/command_proxy.rb +36 -0
  35. data/lib/rom/commands/class_interface.rb +236 -0
  36. data/lib/rom/commands/composite.rb +55 -0
  37. data/lib/rom/commands/create.rb +15 -0
  38. data/lib/rom/commands/delete.rb +16 -0
  39. data/lib/rom/commands/graph/class_interface.rb +64 -0
  40. data/lib/rom/commands/graph/input_evaluator.rb +94 -0
  41. data/lib/rom/commands/graph.rb +88 -0
  42. data/lib/rom/commands/lazy/create.rb +35 -0
  43. data/lib/rom/commands/lazy/delete.rb +39 -0
  44. data/lib/rom/commands/lazy/update.rb +46 -0
  45. data/lib/rom/commands/lazy.rb +106 -0
  46. data/lib/rom/commands/update.rb +16 -0
  47. data/lib/rom/commands.rb +5 -0
  48. data/lib/rom/compat/auto_registration.rb +115 -0
  49. data/lib/rom/compat/auto_registration_strategies/base.rb +29 -0
  50. data/lib/rom/compat/auto_registration_strategies/custom_namespace.rb +84 -0
  51. data/lib/rom/compat/auto_registration_strategies/no_namespace.rb +33 -0
  52. data/lib/rom/compat/auto_registration_strategies/with_namespace.rb +29 -0
  53. data/lib/rom/compat/command.rb +74 -0
  54. data/lib/rom/compat/components/dsl/schema.rb +130 -0
  55. data/lib/rom/compat/components.rb +91 -0
  56. data/lib/rom/compat/global.rb +17 -0
  57. data/lib/rom/compat/mapper.rb +22 -0
  58. data/lib/rom/compat/registries.rb +47 -0
  59. data/lib/rom/compat/relation.rb +40 -0
  60. data/lib/rom/compat/schema/dsl.rb +260 -0
  61. data/lib/rom/compat/setting_proxy.rb +44 -0
  62. data/lib/rom/compat/setup.rb +151 -0
  63. data/lib/rom/compat/transformer.rb +49 -0
  64. data/lib/rom/compat.rb +22 -0
  65. data/lib/rom/components/association.rb +26 -0
  66. data/lib/rom/components/command.rb +24 -0
  67. data/lib/rom/components/core.rb +148 -0
  68. data/lib/rom/components/dataset.rb +60 -0
  69. data/lib/rom/components/dsl/association.rb +47 -0
  70. data/lib/rom/components/dsl/command.rb +60 -0
  71. data/lib/rom/components/dsl/core.rb +126 -0
  72. data/lib/rom/components/dsl/dataset.rb +33 -0
  73. data/lib/rom/components/dsl/gateway.rb +14 -0
  74. data/lib/rom/components/dsl/mapper.rb +70 -0
  75. data/lib/rom/components/dsl/relation.rb +49 -0
  76. data/lib/rom/components/dsl/schema.rb +150 -0
  77. data/lib/rom/components/dsl/view.rb +82 -0
  78. data/lib/rom/components/dsl.rb +255 -0
  79. data/lib/rom/components/gateway.rb +50 -0
  80. data/lib/rom/components/mapper.rb +29 -0
  81. data/lib/rom/components/provider.rb +160 -0
  82. data/lib/rom/components/registry.rb +154 -0
  83. data/lib/rom/components/relation.rb +41 -0
  84. data/lib/rom/components/schema.rb +61 -0
  85. data/lib/rom/components/view.rb +55 -0
  86. data/lib/rom/components.rb +55 -0
  87. data/lib/rom/configuration_dsl.rb +4 -0
  88. data/lib/rom/constants.rb +135 -0
  89. data/lib/rom/container.rb +182 -0
  90. data/lib/rom/core.rb +125 -0
  91. data/lib/rom/data_proxy.rb +97 -0
  92. data/lib/rom/enumerable_dataset.rb +70 -0
  93. data/lib/rom/gateway.rb +232 -0
  94. data/lib/rom/global.rb +56 -0
  95. data/lib/rom/header/attribute.rb +190 -0
  96. data/lib/rom/header.rb +198 -0
  97. data/lib/rom/inferrer.rb +55 -0
  98. data/lib/rom/initializer.rb +80 -0
  99. data/lib/rom/lint/enumerable_dataset.rb +56 -0
  100. data/lib/rom/lint/gateway.rb +120 -0
  101. data/lib/rom/lint/linter.rb +79 -0
  102. data/lib/rom/lint/spec.rb +22 -0
  103. data/lib/rom/lint/test.rb +98 -0
  104. data/lib/rom/loader.rb +161 -0
  105. data/lib/rom/mapper/attribute_dsl.rb +480 -0
  106. data/lib/rom/mapper/dsl.rb +107 -0
  107. data/lib/rom/mapper/model_dsl.rb +61 -0
  108. data/lib/rom/mapper.rb +99 -0
  109. data/lib/rom/mapper_compiler.rb +84 -0
  110. data/lib/rom/memory/associations/many_to_many.rb +12 -0
  111. data/lib/rom/memory/associations/many_to_one.rb +12 -0
  112. data/lib/rom/memory/associations/one_to_many.rb +12 -0
  113. data/lib/rom/memory/associations/one_to_one.rb +12 -0
  114. data/lib/rom/memory/associations.rb +6 -0
  115. data/lib/rom/memory/commands.rb +60 -0
  116. data/lib/rom/memory/dataset.rb +127 -0
  117. data/lib/rom/memory/gateway.rb +66 -0
  118. data/lib/rom/memory/mapper_compiler.rb +10 -0
  119. data/lib/rom/memory/relation.rb +91 -0
  120. data/lib/rom/memory/schema.rb +32 -0
  121. data/lib/rom/memory/storage.rb +61 -0
  122. data/lib/rom/memory/types.rb +11 -0
  123. data/lib/rom/memory.rb +7 -0
  124. data/lib/rom/model_builder.rb +103 -0
  125. data/lib/rom/open_struct.rb +112 -0
  126. data/lib/rom/pipeline.rb +111 -0
  127. data/lib/rom/plugin.rb +130 -0
  128. data/lib/rom/plugins/class_methods.rb +37 -0
  129. data/lib/rom/plugins/command/schema.rb +45 -0
  130. data/lib/rom/plugins/command/timestamps.rb +149 -0
  131. data/lib/rom/plugins/dsl.rb +53 -0
  132. data/lib/rom/plugins/relation/changeset.rb +97 -0
  133. data/lib/rom/plugins/relation/instrumentation.rb +66 -0
  134. data/lib/rom/plugins/relation/registry_reader.rb +36 -0
  135. data/lib/rom/plugins/schema/timestamps.rb +59 -0
  136. data/lib/rom/plugins.rb +100 -0
  137. data/lib/rom/processor/composer.rb +37 -0
  138. data/lib/rom/processor/transformer.rb +415 -0
  139. data/lib/rom/processor.rb +30 -0
  140. data/lib/rom/registries/associations.rb +26 -0
  141. data/lib/rom/registries/commands.rb +11 -0
  142. data/lib/rom/registries/container.rb +12 -0
  143. data/lib/rom/registries/datasets.rb +21 -0
  144. data/lib/rom/registries/gateways.rb +8 -0
  145. data/lib/rom/registries/mappers.rb +21 -0
  146. data/lib/rom/registries/nestable.rb +32 -0
  147. data/lib/rom/registries/relations.rb +8 -0
  148. data/lib/rom/registries/root.rb +203 -0
  149. data/lib/rom/registries/schemas.rb +44 -0
  150. data/lib/rom/registries/views.rb +11 -0
  151. data/lib/rom/relation/class_interface.rb +61 -0
  152. data/lib/rom/relation/combined.rb +160 -0
  153. data/lib/rom/relation/commands.rb +65 -0
  154. data/lib/rom/relation/composite.rb +53 -0
  155. data/lib/rom/relation/curried.rb +129 -0
  156. data/lib/rom/relation/graph.rb +107 -0
  157. data/lib/rom/relation/loaded.rb +136 -0
  158. data/lib/rom/relation/materializable.rb +62 -0
  159. data/lib/rom/relation/name.rb +122 -0
  160. data/lib/rom/relation/wrap.rb +64 -0
  161. data/lib/rom/relation.rb +625 -0
  162. data/lib/rom/repository/class_interface.rb +162 -0
  163. data/lib/rom/repository/relation_reader.rb +48 -0
  164. data/lib/rom/repository/root.rb +75 -0
  165. data/lib/rom/repository/session.rb +60 -0
  166. data/lib/rom/repository.rb +179 -0
  167. data/lib/rom/schema/associations_dsl.rb +222 -0
  168. data/lib/rom/schema/inferrer.rb +106 -0
  169. data/lib/rom/schema.rb +471 -0
  170. data/lib/rom/settings.rb +141 -0
  171. data/lib/rom/setup.rb +297 -0
  172. data/lib/rom/struct.rb +99 -0
  173. data/lib/rom/struct_compiler.rb +114 -0
  174. data/lib/rom/support/configurable.rb +213 -0
  175. data/lib/rom/support/inflector.rb +31 -0
  176. data/lib/rom/support/memoizable.rb +61 -0
  177. data/lib/rom/support/notifications.rb +238 -0
  178. data/lib/rom/transaction.rb +26 -0
  179. data/lib/rom/transformer.rb +46 -0
  180. data/lib/rom/types.rb +74 -0
  181. data/lib/rom/version.rb +1 -1
  182. data/lib/rom-changeset.rb +4 -0
  183. data/lib/rom-core.rb +3 -0
  184. data/lib/rom-repository.rb +4 -0
  185. data/lib/rom.rb +3 -3
  186. metadata +302 -23
@@ -0,0 +1,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/equalizer"
4
+
5
+ require "rom/initializer"
6
+ require "rom/support/memoizable"
7
+ require "rom/types"
8
+
9
+ module ROM
10
+ # Schema attributes provide meta information about types and an API
11
+ # for additional operations. This class can be extended by adapters to provide
12
+ # database-specific features. In example rom-sql provides SQL::Attribute
13
+ # with more features like creating SQL expressions for queries.
14
+ #
15
+ # Schema attributes are accessible through canonical relation schemas and
16
+ # instance-level schemas.
17
+ #
18
+ # @api public
19
+ class Attribute
20
+ include Dry::Equalizer(:type, :options)
21
+ include Memoizable
22
+
23
+ extend Initializer
24
+
25
+ META_OPTIONS = %i[primary_key foreign_key source target relation].freeze
26
+
27
+ # @!attribute [r] type
28
+ # @return [Dry::Types::Nominal, Dry::Types::Sum, Dry::Types::Constrained] The attribute's type object
29
+ param :type
30
+
31
+ # @!attribute [r] name
32
+ #
33
+ # Return the canonical name of this attribute name
34
+ #
35
+ # This *always* returns the name that is used in the datastore, even when
36
+ # an attribute is aliased
37
+ #
38
+ # @example
39
+ # class Users < ROM::Relation[:memory]
40
+ # schema do
41
+ # attribute :user_id, Types::Integer, alias: :id
42
+ # attribute :email, Types::String
43
+ # end
44
+ # end
45
+ #
46
+ # users[:user_id].name
47
+ # # => :user_id
48
+ #
49
+ # users[:email].name
50
+ # # => :email
51
+ #
52
+ # @return [Symbol]
53
+ #
54
+ # @api public
55
+ option :name, optional: true, type: Types::Strict::Symbol
56
+
57
+ # @!attribute [r] type
58
+ # @return [Symbol, nil] Alias to use instead of attribute name
59
+ option :alias, optional: true, type: Types::Strict::Symbol.optional
60
+
61
+ # @api private
62
+ def [](value = Undefined)
63
+ type[value]
64
+ end
65
+
66
+ # Return true if this attribute type is a primary key
67
+ #
68
+ # @example
69
+ # class Users < ROM::Relation[:memory]
70
+ # schema do
71
+ # attribute :id, Types::Integer
72
+ # attribute :name, Types::String
73
+ #
74
+ # primary_key :id
75
+ # end
76
+ # end
77
+ #
78
+ # Users.schema[:id].primary_key?
79
+ # # => true
80
+ #
81
+ # Users.schema[:name].primary_key?
82
+ # # => false
83
+ #
84
+ # @return [TrueClass,FalseClass]
85
+ #
86
+ # @api public
87
+ def primary_key?
88
+ meta[:primary_key].equal?(true)
89
+ end
90
+
91
+ # Return true if this attribute type is a foreign key
92
+ #
93
+ # @example
94
+ # class Tasks < ROM::Relation[:memory]
95
+ # schema do
96
+ # attribute :id, Types::Integer
97
+ # attribute :user_id, Types.ForeignKey(:users)
98
+ # end
99
+ # end
100
+ #
101
+ # Users.schema[:user_id].foreign_key?
102
+ # # => true
103
+ #
104
+ # Users.schema[:id].foreign_key?
105
+ # # => false
106
+ #
107
+ # @return [TrueClass,FalseClass]
108
+ #
109
+ # @api public
110
+ def foreign_key?
111
+ meta[:foreign_key].equal?(true)
112
+ end
113
+
114
+ # Return true if this attribute has a configured alias
115
+ #
116
+ # @example
117
+ # class Tasks < ROM::Relation[:memory]
118
+ # schema do
119
+ # attribute :user_id, Types::Integer, alias: :id
120
+ # attribute :name, Types::String
121
+ # end
122
+ # end
123
+ #
124
+ # Users.schema[:user_id].aliased?
125
+ # # => true
126
+ #
127
+ # Users.schema[:name].aliased?
128
+ # # => false
129
+ #
130
+ # @return [TrueClass,FalseClass]
131
+ #
132
+ # @api public
133
+ def aliased?
134
+ !self.alias.nil?
135
+ end
136
+
137
+ # Return source relation of this attribute type
138
+ #
139
+ # @example
140
+ # class Tasks < ROM::Relation[:memory]
141
+ # schema do
142
+ # attribute :id, Types::Integer
143
+ # attribute :user_id, Types.ForeignKey(:users)
144
+ # end
145
+ # end
146
+ #
147
+ # Users.schema[:id].source
148
+ # # => :tasks
149
+ #
150
+ # Users.schema[:user_id].source
151
+ # # => :tasks
152
+ #
153
+ # @return [Symbol, Relation::Name]
154
+ #
155
+ # @api public
156
+ def source
157
+ meta[:source]
158
+ end
159
+
160
+ # Return target relation of this attribute type
161
+ #
162
+ # @example
163
+ # class Tasks < ROM::Relation[:memory]
164
+ # schema do
165
+ # attribute :id, Types::Integer
166
+ # attribute :user_id, Types.ForeignKey(:users)
167
+ # end
168
+ # end
169
+ #
170
+ # Users.schema[:id].target
171
+ # # => nil
172
+ #
173
+ # Users.schema[:user_id].target
174
+ # # => :users
175
+ #
176
+ # @return [NilClass, Symbol, Relation::Name]
177
+ #
178
+ # @api public
179
+ def target
180
+ meta[:target]
181
+ end
182
+
183
+ # Return tuple key
184
+ #
185
+ # When schemas are projected with aliased attributes, we need a simple access to tuple keys
186
+ #
187
+ # @example
188
+ # class Tasks < ROM::Relation[:memory]
189
+ # schema do
190
+ # attribute :user_id, Types::Integer, alias: :id
191
+ # attribute :name, Types::String
192
+ # end
193
+ # end
194
+ #
195
+ # Users.schema[:id].key
196
+ # # :id
197
+ #
198
+ # Users.schema.project(Users.schema[:id].aliased(:user_id)).key
199
+ # # :user_id
200
+ #
201
+ # @return [Symbol]
202
+ #
203
+ # @api public
204
+ def key
205
+ self.alias || name
206
+ end
207
+
208
+ # Return new attribute type with provided alias
209
+ #
210
+ # @example
211
+ # class Tasks < ROM::Relation[:memory]
212
+ # schema do
213
+ # attribute :user_id, Types::Integer
214
+ # attribute :name, Types::String
215
+ # end
216
+ # end
217
+ #
218
+ # aliased_user_id = Users.schema[:user_id].aliased(:id)
219
+ #
220
+ # aliased_user_id.aliased?
221
+ # # => true
222
+ #
223
+ # aliased_user_id.name
224
+ # # => :user_id
225
+ #
226
+ # aliased_user_id.alias
227
+ # # => :id
228
+ #
229
+ # @param [Symbol] name The alias
230
+ #
231
+ # @return [Attribute]
232
+ #
233
+ # @api public
234
+ def aliased(name)
235
+ with(alias: name)
236
+ end
237
+ alias_method :as, :aliased
238
+
239
+ # Return new attribute type with an alias using provided prefix
240
+ #
241
+ # @example
242
+ # class Users < ROM::Relation[:memory]
243
+ # schema do
244
+ # attribute :id, Types::Integer
245
+ # attribute :name, Types::String
246
+ # end
247
+ # end
248
+ #
249
+ # prefixed_id = Users.schema[:id].prefixed
250
+ #
251
+ # prefixed_id.aliased?
252
+ # # => true
253
+ #
254
+ # prefixed_id.name
255
+ # # => :id
256
+ #
257
+ # prefixed_id.alias
258
+ # # => :users_id
259
+ #
260
+ # prefixed_id = Users.schema[:id].prefixed(:user)
261
+ #
262
+ # prefixed_id.alias
263
+ # # => :user_id
264
+ #
265
+ # @param [Symbol] prefix The prefix (defaults to source.dataset)
266
+ #
267
+ # @return [Attribute]
268
+ #
269
+ # @api public
270
+ def prefixed(prefix = source.dataset)
271
+ aliased(:"#{prefix}_#{name}")
272
+ end
273
+
274
+ # Return if the attribute type is from a wrapped relation
275
+ #
276
+ # Wrapped attributes are used when two schemas from different relations
277
+ # are merged together. This way we can identify them easily and handle
278
+ # correctly in places like auto-mapping.
279
+ #
280
+ # @api public
281
+ def wrapped?
282
+ meta[:wrapped].equal?(true)
283
+ end
284
+
285
+ # Return attribute type wrapped for the specified relation name
286
+ #
287
+ # @param [Symbol] name The name of the source relation (defaults to source.dataset)
288
+ #
289
+ # @return [Attribute]
290
+ #
291
+ # @api public
292
+ def wrapped(name = source.dataset)
293
+ prefixed(name).meta(wrapped: true)
294
+ end
295
+
296
+ # Return attribute type with additional meta information
297
+ #
298
+ # Return meta information hash if no opts are provided
299
+ #
300
+ # @param [Hash] opts The meta options
301
+ #
302
+ # @return [Attribute]
303
+ #
304
+ # @api public
305
+ def meta(opts = nil)
306
+ if opts
307
+ self.class.new(type.meta(opts), **options)
308
+ else
309
+ type.meta
310
+ end
311
+ end
312
+
313
+ # Return string representation of the attribute type
314
+ #
315
+ # @return [String]
316
+ #
317
+ # @api public
318
+ def inspect
319
+ opts = options.reject { |k| %i[type name].include?(k) }
320
+ meta_and_opts = meta.merge(opts).map { |k, v| "#{k}=#{v.inspect}" }
321
+ %(#<#{self.class}[#{type.name}] name=#{name.inspect} #{meta_and_opts.join(" ")}>)
322
+ end
323
+ alias_method :pretty_inspect, :inspect
324
+
325
+ # Check if the attribute type is equal to another
326
+ #
327
+ # @param [Dry::Type, Attribute] other
328
+ #
329
+ # @return [TrueClass,FalseClass]
330
+ #
331
+ # @api public
332
+ def eql?(other)
333
+ other.is_a?(self.class) ? super : type.eql?(other)
334
+ end
335
+
336
+ # Return if this attribute type has additional attribute type for reading
337
+ # tuple values
338
+ #
339
+ # @return [TrueClass, FalseClass]
340
+ #
341
+ # @api private
342
+ def read?
343
+ !meta[:read].nil?
344
+ end
345
+
346
+ # Return read type
347
+ #
348
+ # @return [Dry::Types::Type]
349
+ #
350
+ # @api private
351
+ def to_read_type
352
+ read? ? meta[:read] : type
353
+ end
354
+
355
+ # Return write type
356
+ #
357
+ # @return [Dry::Types::Type]
358
+ #
359
+ # @api private
360
+ def to_write_type
361
+ type
362
+ end
363
+
364
+ # Return nullable attribute
365
+ #
366
+ # @return [Attribute]
367
+ #
368
+ # @api public
369
+ def optional
370
+ sum = self.class.new(super, **options)
371
+ read? ? sum.meta(read: meta[:read].optional) : sum
372
+ end
373
+
374
+ # @api private
375
+ def respond_to_missing?(name, include_private = false)
376
+ type.respond_to?(name) || super
377
+ end
378
+
379
+ # Return AST for the type
380
+ #
381
+ # @return [Array]
382
+ #
383
+ # @api public
384
+ def to_ast
385
+ [:attribute, [name, type.to_ast(meta: false), meta_options_ast]]
386
+ end
387
+
388
+ # Return AST for the read type
389
+ #
390
+ # @return [Array]
391
+ #
392
+ # @api public
393
+ def to_read_ast
394
+ [:attribute, [name, to_read_type.to_ast(meta: false), meta_options_ast]]
395
+ end
396
+
397
+ # @api private
398
+ def meta_options_ast
399
+ keys = %i[wrapped primary_key alias]
400
+ ast = meta.merge(options).select { |k, _| keys.include?(k) }
401
+ ast[:source] = source.to_sym if source
402
+ ast
403
+ end
404
+
405
+ memoize :to_ast, :to_read_ast, :meta_options_ast
406
+
407
+ private
408
+
409
+ # @api private
410
+ def method_missing(meth, *args, &block)
411
+ if type.respond_to?(meth)
412
+ response = type.__send__(meth, *args, &block)
413
+
414
+ if response.is_a?(type.class)
415
+ self.class.new(response, **options)
416
+ else
417
+ response
418
+ end
419
+ else
420
+ super
421
+ end
422
+ end
423
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
424
+ end
425
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ # Relation extension which provides auto-currying of relation view methods
5
+ #
6
+ # @api private
7
+ module AutoCurry
8
+ def self.extended(klass)
9
+ klass.define_singleton_method(:method_added) do |name|
10
+ return if auto_curry_busy?
11
+
12
+ auto_curry_guard { auto_curry(name) }
13
+ super(name)
14
+ end
15
+ end
16
+
17
+ # @api private
18
+ def auto_curry_guard
19
+ @__auto_curry_busy__ = true
20
+ yield
21
+ ensure
22
+ @__auto_curry_busy__ = false
23
+ end
24
+
25
+ # @api private
26
+ def auto_curry_busy?
27
+ @__auto_curry_busy__ ||= false
28
+ end
29
+
30
+ # @api private
31
+ def auto_curried_methods
32
+ @__auto_curried_methods__ ||= Set.new
33
+ end
34
+
35
+ # Auto-curry a method
36
+ #
37
+ # @param [Symbol] name The name of a method
38
+ #
39
+ # @api private
40
+ def auto_curry(name, &block)
41
+ arity = instance_method(name).arity
42
+
43
+ return unless public_instance_methods.include?(name) && arity != 0
44
+
45
+ mod = Module.new
46
+
47
+ mod.module_eval do
48
+ define_method(name) do |*args, &mblock|
49
+ response =
50
+ if arity.negative? || arity == args.size
51
+ super(*args, &mblock)
52
+ else
53
+ self.class.curried.new(self, view: name, curry_args: args, arity: arity)
54
+ end
55
+
56
+ if block
57
+ response.instance_exec(&block)
58
+ else
59
+ response
60
+ end
61
+ end
62
+ ruby2_keywords(name) if respond_to?(:ruby2_keywords, true)
63
+ end
64
+
65
+ auto_curried_methods << name
66
+
67
+ prepend(mod)
68
+ end
69
+ end
70
+ end
data/lib/rom/cache.rb ADDED
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module ROM
6
+ # Thread-safe cache used by various rom components
7
+ #
8
+ # @api private
9
+ class Cache
10
+ attr_reader :objects
11
+
12
+ # @api private
13
+ class Namespaced
14
+ # @api private
15
+ attr_reader :cache
16
+
17
+ # @api private
18
+ attr_reader :namespace
19
+
20
+ # @api private
21
+ attr_reader :parent
22
+
23
+ # @api private
24
+ def initialize(cache, namespace, parent:)
25
+ @cache = cache
26
+ @namespace = namespace
27
+ @parent = parent
28
+ end
29
+
30
+ # @api private
31
+ def namespaced(namespace)
32
+ parent.namespaced(namespace)
33
+ end
34
+
35
+ # @api private
36
+ def [](key)
37
+ cache[[namespace, key].hash]
38
+ end
39
+
40
+ # @api private
41
+ def fetch_or_store(*args, &block)
42
+ cache.fetch_or_store([namespace, args.hash].hash, &block)
43
+ end
44
+
45
+ # @api private
46
+ def size
47
+ cache.size
48
+ end
49
+
50
+ # @api private
51
+ def inspect
52
+ %(#<#{self.class} size=#{size}>)
53
+ end
54
+ end
55
+
56
+ # @api private
57
+ def initialize
58
+ @objects = Concurrent::Map.new
59
+ @namespaced = {}
60
+ end
61
+
62
+ # @api private
63
+ def [](key)
64
+ cache[key]
65
+ end
66
+
67
+ # @api private
68
+ def fetch_or_store(*args, &block)
69
+ objects.fetch_or_store(args.hash, &block)
70
+ end
71
+
72
+ # @api private
73
+ def size
74
+ objects.size
75
+ end
76
+
77
+ # @api private
78
+ def namespaced(namespace)
79
+ @namespaced[namespace] ||= Namespaced.new(objects, namespace, parent: self)
80
+ end
81
+
82
+ # @api private
83
+ def inspect
84
+ %(#<#{self.class} size=#{size} namespaced=#{@namespaced.inspect}>)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/initializer"
4
+
5
+ module ROM
6
+ class Changeset
7
+ # Associated changesets automatically set up FKs
8
+ #
9
+ # @api public
10
+ class Associated
11
+ extend Initializer
12
+
13
+ # @!attribute [r] left
14
+ # @return [Changeset::Create] Child changeset
15
+ param :left
16
+
17
+ # @!attribute [r] associations
18
+ # @return [Array] List of association identifiers from relation schema
19
+ option :associations
20
+
21
+ # Infer association name from an object with a schema
22
+ #
23
+ # This expects other to be an object with a schema that includes a primary key
24
+ # attribute with :source meta information. This makes it work with both structs
25
+ # and relations
26
+ #
27
+ # @see Stateful#associate
28
+ #
29
+ # @api private
30
+ def self.infer_assoc_name(other)
31
+ schema = other.class.schema
32
+ attrs = schema.is_a?(Hash) ? schema.values : schema
33
+ pk = attrs.detect { |attr| attr.meta[:primary_key] }
34
+
35
+ if pk
36
+ pk.meta[:source]
37
+ else
38
+ raise ArgumentError, "can't infer association name for #{other}"
39
+ end
40
+ end
41
+
42
+ # Commit changeset's composite command
43
+ #
44
+ # @example
45
+ # task_changeset = tasks.
46
+ # changeset(title: 'Task One').
47
+ # associate(user, :user).
48
+ # commit
49
+ # # {:id => 1, :user_id => 1, title: 'Task One'}
50
+ #
51
+ # @return [Array<Hash>, Hash]
52
+ #
53
+ # @api public
54
+ def commit
55
+ command.call
56
+ end
57
+
58
+ # Associate with other changesets
59
+ #
60
+ # @see Changeset#associate
61
+ #
62
+ # @return [Associated]
63
+ #
64
+ # @api public
65
+ def associate(other, name = Associated.infer_assoc_name(other))
66
+ self.class.new(left, associations: associations.merge(name => other))
67
+ end
68
+
69
+ # Create a composed command
70
+ #
71
+ # @example using existing parent data
72
+ # user_changeset = users.changeset(name: 'Jane')
73
+ # task_changeset = tasks.changeset(title: 'Task One')
74
+ #
75
+ # user = users.create(user_changeset)
76
+ # task = tasks.create(task_changeset.associate(user, :user))
77
+ #
78
+ # @example saving both parent and child in one go
79
+ # user_changeset = users.changeset(name: 'Jane')
80
+ # task_changeset = tasks.changeset(title: 'Task One')
81
+ #
82
+ # task = tasks.create(task_changeset.associate(user, :user))
83
+ #
84
+ # This works *only* with parent => child(ren) changeset hierarchy
85
+ #
86
+ # @return [ROM::Command::Composite]
87
+ #
88
+ # @api public
89
+ def command
90
+ associations.reduce(left.command.curry(left)) do |a, (assoc, other)|
91
+ case other
92
+ when Changeset
93
+ a >> other.command.with_association(assoc).curry(other)
94
+ when Associated
95
+ a >> other.command.with_association(assoc)
96
+ when Array
97
+ raise NotImplementedError, "Changeset::Associate does not support arrays yet"
98
+ else
99
+ a.with_association(assoc, parent: other)
100
+ end
101
+ end
102
+ end
103
+
104
+ # @api private
105
+ def relation
106
+ left.relation
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ class Changeset
5
+ # Changeset specialization for create commands
6
+ #
7
+ # @see Changeset::Stateful
8
+ #
9
+ # @api public
10
+ class Create < Stateful
11
+ command_type :create
12
+
13
+ def command
14
+ super.new(relation)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ class Changeset
5
+ # Changeset specialization for delete commands
6
+ #
7
+ # Delete changesets will execute delete command for its relation, which
8
+ # means proper restricted relations should be used with this changeset.
9
+ #
10
+ # @api public
11
+ class Delete < Changeset
12
+ command_type :delete
13
+ end
14
+ end
15
+ end