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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/relation/graph"
4
+
5
+ module ROM
6
+ class Changeset
7
+ # Namespace for changeset extensions
8
+ #
9
+ # @api public
10
+ module Extensions
11
+ # Changeset extenions for combined relations
12
+ #
13
+ # @api public
14
+ class Relation::Graph
15
+ # Build a changeset for a combined relation
16
+ #
17
+ # @raise NotImplementedError
18
+ #
19
+ # @api public
20
+ def changeset(*)
21
+ raise NotImplementedError, "Changeset doesn't support combined relations yet"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/changeset/pipe_registry"
4
+
5
+ module ROM
6
+ class Changeset
7
+ # Composable data transformation pipe used by default in changesets
8
+ #
9
+ # @api private
10
+ class Pipe < Dry::Transformer[PipeRegistry]
11
+ extend Initializer
12
+
13
+ define!(&:identity)
14
+
15
+ param :processor, optional: true
16
+
17
+ option :use_for_diff, optional: true, default: -> { true }
18
+ option :diff_processor, default: -> { self[processor] }
19
+
20
+ def self.new(*args, **opts)
21
+ if args.empty?
22
+ initialize(**opts)
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def self.initialize(**opts)
29
+ transformer = allocate
30
+ transformer.__send__(:initialize, dsl.(transformer), **opts)
31
+ transformer
32
+ end
33
+
34
+ def self.[](name_or_proc)
35
+ container[name_or_proc]
36
+ end
37
+
38
+ def [](*args)
39
+ self.class[*args]
40
+ end
41
+
42
+ def bind(context)
43
+ if processor.is_a?(Proc)
44
+ bound_processor = self[-> *args { context.instance_exec(*args, &processor) }]
45
+ bound_diff_processor = self[-> *args { context.instance_exec(*args, &diff_processor) }]
46
+
47
+ new(bound_processor, diff_processor: bound_diff_processor)
48
+ else
49
+ self
50
+ end
51
+ end
52
+
53
+ def compose(other, for_diff: other.is_a?(self.class) ? other.use_for_diff : false)
54
+ new_proc = processor >> other
55
+
56
+ if for_diff
57
+ diff_proc = diff_processor >> (
58
+ other.is_a?(self.class) ? other.diff_processor : other
59
+ )
60
+
61
+ new(new_proc, use_for_diff: true, diff_processor: diff_proc)
62
+ else
63
+ new(new_proc)
64
+ end
65
+ end
66
+ alias_method :>>, :compose
67
+
68
+ def call(data)
69
+ processor.call(data)
70
+ end
71
+
72
+ def for_diff(data)
73
+ use_for_diff ? diff_processor.call(data) : data
74
+ end
75
+
76
+ def new(processor, **opts)
77
+ self.class.new(processor, **options, **opts)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/transformer/all"
4
+ require "dry/transformer/registry"
5
+
6
+ module ROM
7
+ class Changeset
8
+ # Dry::Transformer Registry useful for pipe
9
+ #
10
+ # @api private
11
+ module PipeRegistry
12
+ extend Dry::Transformer::Registry
13
+
14
+ import Dry::Transformer::Coercions
15
+ import Dry::Transformer::HashTransformations
16
+
17
+ def self.add_timestamps(data)
18
+ now = Time.now
19
+ Hash(created_at: now, updated_at: now).merge(data)
20
+ end
21
+
22
+ def self.touch(data)
23
+ Hash(updated_at: Time.now).merge(data)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rom/changeset/pipe"
4
+
5
+ module ROM
6
+ class Changeset
7
+ # Stateful changesets carry data and can transform it into
8
+ # a different structure compatible with a persistence backend
9
+ #
10
+ # @abstract
11
+ class Stateful < Changeset
12
+ # Default no-op pipe
13
+ EMPTY_PIPE = Pipe.new(use_for_diff: false).freeze
14
+
15
+ # @!attribute [r] __data__
16
+ # @return [Hash] The relation data
17
+ # @api private
18
+ option :__data__, optional: true
19
+
20
+ # @!attribute [r] pipe
21
+ # @return [Changeset::Pipe] data transformation pipe
22
+ # @api private
23
+ option :pipe, reader: false, optional: true
24
+
25
+ # Define a changeset mapping
26
+ #
27
+ # Subsequent mapping definitions will be composed together
28
+ # and applied in the order they way defined
29
+ #
30
+ # @example Transformation DSL
31
+ # class NewUser < ROM::Changeset::Create
32
+ # map do
33
+ # unwrap :address, prefix: true
34
+ # end
35
+ # end
36
+ #
37
+ # @example Using custom block
38
+ # class NewUser < ROM::Changeset::Create
39
+ # map do |tuple|
40
+ # tuple.merge(created_at: Time.now)
41
+ # end
42
+ # end
43
+ #
44
+ # @example Multiple mappings (executed in the order of definition)
45
+ # class NewUser < ROM::Changeset::Create
46
+ # map do
47
+ # unwrap :address, prefix: true
48
+ # end
49
+ #
50
+ # map do |tuple|
51
+ # tuple.merge(created_at: Time.now)
52
+ # end
53
+ # end
54
+ #
55
+ # @return [Array<Pipe>, Dry::Transformer::Function>]
56
+ #
57
+ # @see https://github.com/dry-rb/dry-transformer
58
+ #
59
+ # @api public
60
+ def self.map(**options, &block)
61
+ if block.parameters.empty?
62
+ pipes << Class.new(Pipe).define!(&block).new(**options)
63
+ else
64
+ pipes << Pipe.new(block, **options)
65
+ end
66
+ end
67
+
68
+ # Define a changeset mapping excluded from diffs
69
+ #
70
+ # @see Changeset::Stateful.map
71
+ # @see Changeset::Stateful#extend
72
+ #
73
+ # @return [Array<Pipe>, Dry::Transformer::Function>]
74
+ #
75
+ # @api public
76
+ def self.extend(*, &block)
77
+ if block
78
+ map(use_for_diff: false, &block)
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ # Build default pipe object
85
+ #
86
+ # This can be overridden in a custom changeset subclass
87
+ #
88
+ # @return [Pipe]
89
+ def self.default_pipe(context)
90
+ pipes.empty? ? EMPTY_PIPE : pipes.map { |p| p.bind(context) }.reduce(:>>)
91
+ end
92
+
93
+ # @api private
94
+ def self.inherited(klass)
95
+ return if klass == ROM::Changeset
96
+
97
+ super
98
+ klass.instance_variable_set(:@__pipes__, pipes.dup)
99
+ end
100
+
101
+ # @api private
102
+ def self.pipes
103
+ @__pipes__
104
+ end
105
+ @__pipes__ = EMPTY_ARRAY
106
+
107
+ # Pipe changeset's data using custom steps define on the pipe
108
+ #
109
+ # @overload map(*steps)
110
+ # Apply mapping using built-in transformations
111
+ #
112
+ # @example
113
+ # changeset.map(:add_timestamps)
114
+ #
115
+ # @param [Array<Symbol>] steps A list of mapping steps
116
+ #
117
+ # @overload map(&block)
118
+ # Apply mapping using a custom block
119
+ #
120
+ # @example
121
+ # changeset.map { |tuple| tuple.merge(created_at: Time.now) }
122
+ #
123
+ # @overload map(*steps, &block)
124
+ # Apply mapping using built-in transformations and a custom block
125
+ #
126
+ # @example
127
+ # changeset.map(:add_timestamps) { |tuple| tuple.merge(status: 'published') }
128
+ #
129
+ # @param [Array<Symbol>] steps A list of mapping steps
130
+ #
131
+ # @return [Changeset]
132
+ #
133
+ # @api public
134
+ def map(*steps, &block)
135
+ extend(*steps, for_diff: true, &block)
136
+ end
137
+
138
+ # Pipe changeset's data using custom steps define on the pipe.
139
+ # You should use #map instead except updating timestamp fields.
140
+ # Calling changeset.extend builds a pipe that excludes certain
141
+ # steps for generating the diff. Currently the only place where
142
+ # it is used is update changesets with the `:touch` step, i.e.
143
+ # `changeset.extend(:touch).diff` will exclude `:updated_at`
144
+ # from the diff.
145
+ #
146
+ # @see Changeset::Stateful#map
147
+ #
148
+ # @return [Changeset]
149
+ #
150
+ # @api public
151
+ def extend(*steps, **options, &block)
152
+ if block
153
+ if steps.empty?
154
+ with(pipe: pipe.compose(Pipe.new(block).bind(self), **options))
155
+ else
156
+ extend(*steps, **options).extend(**options, &block)
157
+ end
158
+ else
159
+ with(pipe: steps.reduce(pipe.with(**options)) { |a, e| a.compose(pipe[e], **options) })
160
+ end
161
+ end
162
+
163
+ # Return changeset with data
164
+ #
165
+ # @param [Hash] data
166
+ #
167
+ # @return [Changeset]
168
+ #
169
+ # @api public
170
+ def data(data)
171
+ with(__data__: data)
172
+ end
173
+
174
+ # Coerce changeset to a hash
175
+ #
176
+ # This will send the data through the pipe
177
+ #
178
+ # @return [Hash]
179
+ #
180
+ # @api public
181
+ def to_h
182
+ pipe.call(__data__)
183
+ end
184
+ alias_method :to_hash, :to_h
185
+
186
+ # Coerce changeset to an array
187
+ #
188
+ # This will send the data through the pipe
189
+ #
190
+ # @return [Array]
191
+ #
192
+ # @api public
193
+ def to_a
194
+ result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
195
+ end
196
+ alias_method :to_ary, :to_a
197
+
198
+ # Commit stateful changeset
199
+ #
200
+ # @see Changeset#commit
201
+ #
202
+ # @api public
203
+ def commit
204
+ command.call(self)
205
+ end
206
+
207
+ # Associate a changeset with another changeset or hash-like object
208
+ #
209
+ # @example with another changeset
210
+ # new_user = users.changeset(name: 'Jane')
211
+ # new_task = users.changeset(:tasks, title: 'A task')
212
+ #
213
+ # new_task.associate(new_user, :users)
214
+ #
215
+ # @example with a hash-like object
216
+ # user = users.users.by_pk(1).one
217
+ # new_task = users.changeset(:tasks, title: 'A task')
218
+ #
219
+ # new_task.associate(user, :users)
220
+ #
221
+ # @param [#to_hash, Changeset] other Other changeset or hash-like object
222
+ # @param [Symbol] name The association identifier from schema
223
+ #
224
+ # @api public
225
+ def associate(other, name = Associated.infer_assoc_name(other))
226
+ Associated.new(self, associations: {name => other})
227
+ end
228
+
229
+ # Return command result type
230
+ #
231
+ # @return [Symbol]
232
+ #
233
+ # @api private
234
+ def result
235
+ __data__.is_a?(Array) ? :many : :one
236
+ end
237
+
238
+ # Return string representation of the changeset
239
+ #
240
+ # @return [String]
241
+ #
242
+ # @api public
243
+ def inspect
244
+ %(#<#{self.class} relation=#{relation.name.inspect} data=#{__data__}>)
245
+ end
246
+
247
+ # @api private
248
+ def command_compiler_options
249
+ super.merge(result: result)
250
+ end
251
+
252
+ # Data transformation pipe
253
+ #
254
+ # @return [Changeset::Pipe]
255
+ #
256
+ # @api private
257
+ def pipe
258
+ @pipe ||= self.class.default_pipe(self)
259
+ end
260
+
261
+ private
262
+
263
+ # @api private
264
+ def respond_to_missing?(meth, include_private = false)
265
+ super || __data__.respond_to?(meth)
266
+ end
267
+
268
+ # @api private
269
+ def method_missing(meth, *args, &block)
270
+ if __data__.respond_to?(meth)
271
+ response = __data__.__send__(meth, *args, &block)
272
+
273
+ if response.is_a?(__data__.class)
274
+ with(__data__: response)
275
+ else
276
+ response
277
+ end
278
+ else
279
+ super
280
+ end
281
+ end
282
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
283
+ end
284
+ end
285
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ROM
4
+ class Changeset
5
+ # Changeset specialization for update commands
6
+ #
7
+ # Update changesets will only execute their commands when
8
+ # the data is different from the original tuple. Original tuple
9
+ # is fetched from changeset's relation using `one` method.
10
+ #
11
+ # @example
12
+ # users.by_pk(1).changeset(:update, name: "Jane Doe").commit
13
+ #
14
+ # @see Changeset::Stateful
15
+ #
16
+ # @api public
17
+ class Update < Stateful
18
+ command_type :update
19
+
20
+ # Commit update changeset if there's a diff
21
+ #
22
+ # This returns original tuple if there's no diff
23
+ #
24
+ # @return [Hash]
25
+ #
26
+ # @see Changeset#commit
27
+ #
28
+ # @api public
29
+ def commit
30
+ diff? ? super : original
31
+ end
32
+
33
+ # Return original tuple that this changeset may update
34
+ #
35
+ # @return [Hash]
36
+ #
37
+ # @api public
38
+ def original
39
+ @original ||= relation.one
40
+ end
41
+
42
+ # Return true if there's a diff between original and changeset data
43
+ #
44
+ # @return [TrueClass, FalseClass]
45
+ #
46
+ # @api public
47
+ def diff?
48
+ !diff.empty?
49
+ end
50
+
51
+ # Return if there's no diff between the original and changeset data
52
+ #
53
+ # @return [TrueClass, FalseClass]
54
+ #
55
+ # @api public
56
+ def clean?
57
+ diff.empty?
58
+ end
59
+
60
+ # Calculate the diff between the original and changeset data
61
+ #
62
+ # @return [Hash]
63
+ #
64
+ # @api public
65
+ def diff
66
+ @diff ||=
67
+ begin
68
+ source = original.to_h
69
+ data = pipe.for_diff(__data__)
70
+ data_tuple = data.to_a
71
+ data_keys = data.keys & source.keys
72
+
73
+ new_tuple = data_tuple.to_a.select { |k, _| data_keys.include?(k) }
74
+ ori_tuple = source.to_a.select { |k, _| data_keys.include?(k) }
75
+
76
+ (new_tuple - (new_tuple & ori_tuple)).to_h
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/core/class_attributes"
4
+ require "dry/core/cache"
5
+
6
+ require "rom/constants"
7
+ require "rom/initializer"
8
+
9
+ module ROM
10
+ # Abstract Changeset class
11
+ #
12
+ # If you inherit from this class you need to configure additional settings
13
+ #
14
+ # @example define a custom changeset using :upsert command
15
+ # class NewTag < ROM::Changeset[:tags]
16
+ # command_type :upsert
17
+ # end
18
+ #
19
+ # @abstract
20
+ class Changeset
21
+ extend Initializer
22
+ extend Dry::Core::Cache
23
+ extend Dry::Core::ClassAttributes
24
+
25
+ # @!method self.command_type
26
+ # Get or set changeset command type
27
+ #
28
+ # @overload command_type
29
+ # Return configured command_type
30
+ # @return [Symbol]
31
+ #
32
+ # @overload command_type(identifier)
33
+ # Set relation identifier for this changeset
34
+ # @param [Symbol] identifier The command type identifier
35
+ # @return [Symbol]
36
+ defines :command_type
37
+
38
+ # @!method self.command_options
39
+ # Get or set command options
40
+ #
41
+ # @overload command_options
42
+ # Return configured command_options
43
+ # @return [Hash,nil]
44
+ #
45
+ # @overload command_options(**options)
46
+ # Set command options
47
+ # @param options [Hash] The command options
48
+ # @return [Hash]
49
+ defines :command_options
50
+
51
+ # @!method self.command_plugins
52
+ # Get or set command plugins options
53
+ #
54
+ # @overload command_plugins
55
+ # Return configured command_plugins
56
+ # @return [Hash,nil]
57
+ #
58
+ # @overload command_plugins(**options)
59
+ # Set command plugin options
60
+ # @param options [Hash] The command plugin options
61
+ # @return [Hash]
62
+ defines :command_plugins
63
+
64
+ # @!method self.relation
65
+ # Get or set changeset relation identifier
66
+ #
67
+ # @overload relation
68
+ # Return configured relation identifier for this changeset
69
+ # @return [Symbol]
70
+ #
71
+ # @overload relation(identifier)
72
+ # Set relation identifier for this changeset
73
+ # @param [Symbol] identifier The relation identifier from the ROM container
74
+ # @return [Symbol]
75
+ defines :relation
76
+
77
+ # @!attribute [r] relation
78
+ # @return [Relation] The changeset relation
79
+ param :relation
80
+
81
+ # @!attribute [r] command_type
82
+ # @return [Symbol] a custom command identifier
83
+ option :command_type, default: -> { self.class.command_type }
84
+
85
+ # @!attribute [r] command_options
86
+ # @return [Hash] Configured options for the command
87
+ option :command_options, default: -> { self.class.command_options }
88
+
89
+ # @!attribute [r] command_plugins
90
+ # @return [Hash] Configured plugin options for the command
91
+ option :command_plugins, default: -> { self.class.command_plugins }
92
+
93
+ # Set the default command options
94
+ command_options(mapper: false)
95
+
96
+ # Set the default command plugin options
97
+ command_plugins(EMPTY_HASH)
98
+
99
+ # Create a changeset class preconfigured for a specific relation
100
+ #
101
+ # @example
102
+ # class NewUserChangeset < ROM::Changeset::Create[:users]
103
+ # end
104
+ #
105
+ # users.changeset(NewUserChangeset).data(name: 'Jane')
106
+ #
107
+ # @api public
108
+ def self.[](relation_name)
109
+ fetch_or_store([relation_name, self]) {
110
+ Class.new(self) { relation(relation_name) }
111
+ }
112
+ end
113
+
114
+ # Enable a plugin for the changeset
115
+ #
116
+ # @api public
117
+ def self.use(plugin, **options)
118
+ ROM.plugins[:changeset].fetch(plugin).enable(self).apply(**options)
119
+ end
120
+
121
+ # Return a new changeset with provided relation
122
+ #
123
+ # New options can be provided too
124
+ #
125
+ # @param [Relation] relation
126
+ # @param [Hash] new_options
127
+ #
128
+ # @return [Changeset]
129
+ #
130
+ # @api public
131
+ def new(relation, **new_options)
132
+ self.class.new(relation, **options, **new_options)
133
+ end
134
+
135
+ # Persist changeset
136
+ #
137
+ # @example
138
+ # changeset = users.changeset(name: 'Jane')
139
+ # changeset.commit
140
+ # # => { id: 1, name: 'Jane' }
141
+ #
142
+ # @return [Hash, Array]
143
+ #
144
+ # @api public
145
+ def commit
146
+ command.call
147
+ end
148
+
149
+ # Return string representation of the changeset
150
+ #
151
+ # @return [String]
152
+ #
153
+ # @api public
154
+ def inspect
155
+ %(#<#{self.class} relation=#{relation.name.inspect}>)
156
+ end
157
+
158
+ # Return a command for this changeset
159
+ #
160
+ # @return [ROM::Command]
161
+ #
162
+ # @api private
163
+ def command
164
+ relation.command(command_type, **command_compiler_options)
165
+ end
166
+
167
+ # Return configured command compiler options
168
+ #
169
+ # @return [Hash]
170
+ #
171
+ # @api private
172
+ def command_compiler_options
173
+ command_options.merge(use: command_plugins.keys, plugins_options: command_plugins)
174
+ end
175
+ end
176
+ end
177
+
178
+ require "rom/changeset/stateful"
179
+ require "rom/changeset/associated"
180
+
181
+ require "rom/changeset/create"
182
+ require "rom/changeset/update"
183
+ require "rom/changeset/delete"
184
+
185
+ require "rom/plugins"