rom-changeset 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4b4010fdeeb763d04e4e1c0cd146bbfd123fc460
4
+ data.tar.gz: 2c25d797f66b6216627905972a354a2623cd032a
5
+ SHA512:
6
+ metadata.gz: de4a0796579499bdd43cd62f7d027c6d7cb8dd8b9c28880e99b1aae447826ad66d1fd7b0cb597297a3d41cb55f2a34631b63a485f2f086d288ce010a6f175634
7
+ data.tar.gz: 0e11da9812bf7f6df158b42a47cea85dc6976d12df223ac008aed426460928c6f657ca58e57d7ba26552b541576fae27e057922eebb3400702b85df4fbf3aa8e
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # 1.0.0 to-be-released
2
+
3
+ rom-changeset was extracted from rom-repository
4
+
5
+ ### Added
6
+
7
+ - `#changeset` interfaced was ported to a relation plugin and now `Relation#changeset` is available (solnic)
8
+
9
+ ### Changed
10
+
11
+ - Changesets are no longer coupled to repositories (solnic)
12
+ - Changesets use relations to retrieve their commands (solnic)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013-2017 rom-rb team
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # rom-changeset
2
+
3
+ Resources:
4
+
5
+ ## License
6
+
7
+ See `LICENSE` file.
@@ -0,0 +1,100 @@
1
+ require 'rom/initializer'
2
+
3
+ module ROM
4
+ class Changeset
5
+ # Associated changesets automatically set up FKs
6
+ #
7
+ # @api public
8
+ class Associated
9
+ extend Initializer
10
+
11
+ # @!attribute [r] left
12
+ # @return [Changeset::Create] Child changeset
13
+ param :left
14
+
15
+ # @!attribute [r] associations
16
+ # @return [Array] List of association identifiers from relation schema
17
+ option :associations
18
+
19
+ # Infer association name from an object with a schema
20
+ #
21
+ # This expects other to be an object with a schema that includes a primary key
22
+ # attribute with :source meta information. This makes it work with both structs
23
+ # and relations
24
+ #
25
+ # @see Stateful#associate
26
+ #
27
+ # @api private
28
+ def self.infer_assoc_name(other)
29
+ schema = other.class.schema
30
+ attrs = schema.is_a?(Hash) ? schema.values : schema
31
+ pk = attrs.detect { |attr| attr.meta[:primary_key] }
32
+
33
+ if pk
34
+ pk.meta[:source]
35
+ else
36
+ raise ArgumentError, "can't infer association name for #{other}"
37
+ end
38
+ end
39
+
40
+ # Commit changeset's composite command
41
+ #
42
+ # @example
43
+ # task_changeset = task_repo.
44
+ # changeset(title: 'Task One').
45
+ # associate(user, :user).
46
+ # commit
47
+ # # {:id => 1, :user_id => 1, title: 'Task One'}
48
+ #
49
+ # @return [Array<Hash>, Hash]
50
+ #
51
+ # @api public
52
+ def commit
53
+ command.call
54
+ end
55
+
56
+ # @api public
57
+ def associate(other, name = Associated.infer_assoc_name(other))
58
+ self.class.new(left, associations: associations.merge(name => other))
59
+ end
60
+
61
+ # Create a composed command
62
+ #
63
+ # @example using existing parent data
64
+ # user_changeset = user_repo.changeset(name: 'Jane')
65
+ # task_changeset = task_repo.changeset(title: 'Task One')
66
+ #
67
+ # user = user_repo.create(user_changeset)
68
+ # task = task_repo.create(task_changeset.associate(user, :user))
69
+ #
70
+ # @example saving both parent and child in one go
71
+ # user_changeset = user_repo.changeset(name: 'Jane')
72
+ # task_changeset = task_repo.changeset(title: 'Task One')
73
+ #
74
+ # task = task_repo.create(task_changeset.associate(user, :user))
75
+ #
76
+ # This works *only* with parent => child(ren) changeset hierarchy
77
+ #
78
+ # @return [ROM::Command::Composite]
79
+ #
80
+ # @api public
81
+ def command
82
+ associations.reduce(left.command.curry(left)) do |a, (assoc, other)|
83
+ case other
84
+ when Changeset
85
+ a >> other.command.with_association(assoc).curry(other)
86
+ when Associated
87
+ a >> other.command.with_association(assoc)
88
+ else
89
+ a.with_association(assoc, parent: other)
90
+ end
91
+ end
92
+ end
93
+
94
+ # @api private
95
+ def relation
96
+ left.relation
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,16 @@
1
+ module ROM
2
+ class Changeset
3
+ # Changeset specialization for create commands
4
+ #
5
+ # @see Changeset::Stateful
6
+ #
7
+ # @api public
8
+ class Create < Stateful
9
+ command_type :create
10
+
11
+ def command
12
+ super.new(relation)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ require 'rom/changeset/restricted'
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
+ include Restricted
13
+
14
+ command_type :delete
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,92 @@
1
+ require 'transproc/all'
2
+ require 'transproc/registry'
3
+ require 'transproc/transformer'
4
+
5
+ module ROM
6
+ class Changeset
7
+ # Transproc Registry useful for pipe
8
+ #
9
+ # @api private
10
+ module PipeRegistry
11
+ extend Transproc::Registry
12
+
13
+ import Transproc::HashTransformations
14
+
15
+ def self.add_timestamps(data)
16
+ now = Time.now
17
+ data.merge(created_at: now, updated_at: now)
18
+ end
19
+
20
+ def self.touch(data)
21
+ data.merge(updated_at: Time.now)
22
+ end
23
+ end
24
+
25
+ # Composable data transformation pipe used by default in changesets
26
+ #
27
+ # @api private
28
+ class Pipe < Transproc::Transformer[PipeRegistry]
29
+ extend Initializer
30
+
31
+ param :processor, default: -> { self.class.transproc }
32
+ option :diff_processor, optional: true
33
+ option :use_for_diff, optional: true, default: -> { true }
34
+
35
+ def self.[](name)
36
+ container[name]
37
+ end
38
+
39
+ def [](name)
40
+ self.class[name]
41
+ end
42
+
43
+ def bind(context)
44
+ if processor.is_a?(Proc)
45
+ self.class.new(Pipe[-> *args { context.instance_exec(*args, &processor) }])
46
+ else
47
+ self
48
+ end
49
+ end
50
+
51
+ def compose(other, for_diff: other.is_a?(Pipe) ? other.use_for_diff : false)
52
+ new_proc = processor ? processor >> other : other
53
+
54
+ if for_diff
55
+ diff_proc = diff_processor ? diff_processor >> other : other
56
+ new(new_proc, diff_processor: diff_proc)
57
+ else
58
+ new(new_proc)
59
+ end
60
+ end
61
+ alias_method :>>, :compose
62
+
63
+ def call(data)
64
+ if processor
65
+ processor.call(data)
66
+ else
67
+ data
68
+ end
69
+ end
70
+
71
+ def for_diff(data)
72
+ if diff_processor
73
+ diff_processor.call(data)
74
+ else
75
+ data
76
+ end
77
+ end
78
+
79
+ def with(opts)
80
+ if opts.empty?
81
+ self
82
+ else
83
+ Pipe.new(processor, options.merge(opts))
84
+ end
85
+ end
86
+
87
+ def new(processor, opts = EMPTY_HASH)
88
+ Pipe.new(processor, options.merge(opts))
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ module ROM
2
+ class Changeset
3
+ module Restricted
4
+ # Return a command restricted by the changeset's relation
5
+ #
6
+ # @see Changeset#command
7
+ #
8
+ # @api private
9
+ def command
10
+ super.new(relation)
11
+ end
12
+
13
+ # Restrict changeset's relation by its PK
14
+ #
15
+ # @example
16
+ # repo.changeset(UpdateUser).by_pk(1).data(name: "Jane")
17
+ #
18
+ # @param [Object] pk
19
+ #
20
+ # @return [Changeset]
21
+ #
22
+ # @api public
23
+ def by_pk(pk, data = EMPTY_HASH)
24
+ new(relation.by_pk(pk), __data__: data)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,280 @@
1
+ require 'rom/changeset/pipe'
2
+
3
+ module ROM
4
+ class Changeset
5
+ # Stateful changesets carry data and can transform it into
6
+ # a different structure compatible with a persistence backend
7
+ #
8
+ # @abstract
9
+ class Stateful < Changeset
10
+ # Default no-op pipe
11
+ EMPTY_PIPE = Pipe.new.freeze
12
+
13
+ # @!attribute [r] __data__
14
+ # @return [Hash] The relation data
15
+ # @api private
16
+ option :__data__, optional: true
17
+
18
+ # @!attribute [r] pipe
19
+ # @return [Changeset::Pipe] data transformation pipe
20
+ # @api private
21
+ option :pipe, reader: false, optional: true
22
+
23
+ # Define a changeset mapping
24
+ #
25
+ # Subsequent mapping definitions will be composed together
26
+ # and applied in the order they way defined
27
+ #
28
+ # @example Transformation DSL
29
+ # class NewUser < ROM::Changeset::Create
30
+ # map do
31
+ # unwrap :address, prefix: true
32
+ # end
33
+ # end
34
+ #
35
+ # @example Using custom block
36
+ # class NewUser < ROM::Changeset::Create
37
+ # map do |tuple|
38
+ # tuple.merge(created_at: Time.now)
39
+ # end
40
+ # end
41
+ #
42
+ # @example Multiple mappings (executed in the order of definition)
43
+ # class NewUser < ROM::Changeset::Create
44
+ # map do
45
+ # unwrap :address, prefix: true
46
+ # end
47
+ #
48
+ # map do |tuple|
49
+ # tuple.merge(created_at: Time.now)
50
+ # end
51
+ # end
52
+ #
53
+ # @return [Array<Pipe>, Transproc::Function>]
54
+ #
55
+ # @see https://github.com/solnic/transproc Transproc
56
+ #
57
+ # @api public
58
+ def self.map(options = EMPTY_HASH, &block)
59
+ if block.parameters.empty?
60
+ pipes << Class.new(Pipe, &block).new(options)
61
+ else
62
+ pipes << Pipe.new(block, options)
63
+ end
64
+ end
65
+
66
+ # Define a changeset mapping excluded from diffs
67
+ #
68
+ # @see Changeset::Stateful.map
69
+ # @see Changeset::Stateful#extend
70
+ #
71
+ # @return [Array<Pipe>, Transproc::Function>]
72
+ #
73
+ # @api public
74
+ def self.extend(*, &block)
75
+ if block
76
+ map(use_for_diff: false, &block)
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ # Build default pipe object
83
+ #
84
+ # This can be overridden in a custom changeset subclass
85
+ #
86
+ # @return [Pipe]
87
+ def self.default_pipe(context)
88
+ pipes.size > 0 ? pipes.map { |p| p.bind(context) }.reduce(:>>) : EMPTY_PIPE
89
+ end
90
+
91
+ # @api private
92
+ def self.inherited(klass)
93
+ return if klass == ROM::Changeset
94
+ super
95
+ klass.instance_variable_set(:@__pipes__, pipes ? pipes.dup : EMPTY_ARRAY)
96
+ end
97
+
98
+ # @api private
99
+ def self.pipes
100
+ @__pipes__
101
+ end
102
+
103
+ # Pipe changeset's data using custom steps define on the pipe
104
+ #
105
+ # @overload map(*steps)
106
+ # Apply mapping using built-in transformations
107
+ #
108
+ # @example
109
+ # changeset.map(:add_timestamps)
110
+ #
111
+ # @param [Array<Symbol>] steps A list of mapping steps
112
+ #
113
+ # @overload map(&block)
114
+ # Apply mapping using a custom block
115
+ #
116
+ # @example
117
+ # changeset.map { |tuple| tuple.merge(created_at: Time.now) }
118
+ #
119
+ # @overload map(*steps, &block)
120
+ # Apply mapping using built-in transformations and a custom block
121
+ #
122
+ # @example
123
+ # changeset.map(:add_timestamps) { |tuple| tuple.merge(status: 'published') }
124
+ #
125
+ # @param [Array<Symbol>] steps A list of mapping steps
126
+ #
127
+ # @return [Changeset]
128
+ #
129
+ # @api public
130
+ def map(*steps, &block)
131
+ extend(*steps, for_diff: true, &block)
132
+ end
133
+
134
+ # Pipe changeset's data using custom steps define on the pipe.
135
+ # You should use #map instead except updating timestamp fields.
136
+ # Calling changeset.extend builds a pipe that excludes certain
137
+ # steps for generating the diff. Currently the only place where
138
+ # it is used is update changesets with the `:touch` step, i.e.
139
+ # `changeset.extend(:touch).diff` will exclude `:updated_at`
140
+ # from the diff.
141
+ #
142
+ # @see Changeset::Stateful#map
143
+ #
144
+ # @return [Changeset]
145
+ #
146
+ # @api public
147
+ def extend(*steps, **options, &block)
148
+ if block
149
+ if steps.size > 0
150
+ extend(*steps, options).extend(options, &block)
151
+ else
152
+ with(pipe: pipe.compose(Pipe.new(block).bind(self), options))
153
+ end
154
+ else
155
+ with(pipe: steps.reduce(pipe.with(options)) { |a, e| a.compose(pipe[e], options) })
156
+ end
157
+ end
158
+
159
+ # Return changeset with data
160
+ #
161
+ # @param [Hash] data
162
+ #
163
+ # @return [Changeset]
164
+ #
165
+ # @api public
166
+ def data(data)
167
+ with(__data__: data)
168
+ end
169
+
170
+ # Coerce changeset to a hash
171
+ #
172
+ # This will send the data through the pipe
173
+ #
174
+ # @return [Hash]
175
+ #
176
+ # @api public
177
+ def to_h
178
+ pipe.call(__data__)
179
+ end
180
+ alias_method :to_hash, :to_h
181
+
182
+ # Coerce changeset to an array
183
+ #
184
+ # This will send the data through the pipe
185
+ #
186
+ # @return [Array]
187
+ #
188
+ # @api public
189
+ def to_a
190
+ result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
191
+ end
192
+ alias_method :to_ary, :to_a
193
+
194
+ # Commit stateful changeset
195
+ #
196
+ # @see Changeset#commit
197
+ #
198
+ # @api public
199
+ def commit
200
+ command.call(self)
201
+ end
202
+
203
+ # Associate a changeset with another changeset or hash-like object
204
+ #
205
+ # @example with another changeset
206
+ # new_user = user_repo.changeset(name: 'Jane')
207
+ # new_task = user_repo.changeset(:tasks, title: 'A task')
208
+ #
209
+ # new_task.associate(new_user, :users)
210
+ #
211
+ # @example with a hash-like object
212
+ # user = user_repo.users.by_pk(1).one
213
+ # new_task = user_repo.changeset(:tasks, title: 'A task')
214
+ #
215
+ # new_task.associate(user, :users)
216
+ #
217
+ # @param [#to_hash, Changeset] other Other changeset or hash-like object
218
+ # @param [Symbol] assoc The association identifier from schema
219
+ #
220
+ # @api public
221
+ def associate(other, name = Associated.infer_assoc_name(other))
222
+ Associated.new(self, associations: { name => other })
223
+ end
224
+
225
+ # Return command result type
226
+ #
227
+ # @return [Symbol]
228
+ #
229
+ # @api private
230
+ def result
231
+ __data__.is_a?(Array) ? :many : :one
232
+ end
233
+
234
+ # @api public
235
+ def command
236
+ relation.command(command_type, DEFAULT_COMMAND_OPTS.merge(result: result))
237
+ end
238
+
239
+ # Return string representation of the changeset
240
+ #
241
+ # @return [String]
242
+ #
243
+ # @api public
244
+ def inspect
245
+ %(#<#{self.class} relation=#{relation.name.inspect} data=#{__data__}>)
246
+ end
247
+
248
+ # Data transformation pipe
249
+ #
250
+ # @return [Changeset::Pipe]
251
+ #
252
+ # @api private
253
+ def pipe
254
+ @pipe ||= self.class.default_pipe(self)
255
+ end
256
+
257
+ private
258
+
259
+ # @api private
260
+ def respond_to_missing?(meth, include_private = false)
261
+ super || __data__.respond_to?(meth)
262
+ end
263
+
264
+ # @api private
265
+ def method_missing(meth, *args, &block)
266
+ if __data__.respond_to?(meth)
267
+ response = __data__.__send__(meth, *args, &block)
268
+
269
+ if response.is_a?(__data__.class)
270
+ with(__data__: response)
271
+ else
272
+ response
273
+ end
274
+ else
275
+ super
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,83 @@
1
+ require 'rom/changeset/restricted'
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 `by_pk` relation view.
10
+ # This means the underlying adapter must provide this view, or you
11
+ # you need to implement it yourself in your relations if you want to
12
+ # use Update changesets.
13
+ #
14
+ # @see Changeset::Stateful
15
+ #
16
+ # @api public
17
+ class Update < Stateful
18
+ include Restricted
19
+
20
+ command_type :update
21
+
22
+ # Commit update changeset if there's a diff
23
+ #
24
+ # This returns original tuple if there's no diff
25
+ #
26
+ # @return [Hash]
27
+ #
28
+ # @see Changeset#commit
29
+ #
30
+ # @api public
31
+ def commit
32
+ diff? ? super : original
33
+ end
34
+
35
+ # Return original tuple that this changeset may update
36
+ #
37
+ # @return [Hash]
38
+ #
39
+ # @api public
40
+ def original
41
+ @original ||= relation.one
42
+ end
43
+
44
+ # Return true if there's a diff between original and changeset data
45
+ #
46
+ # @return [TrueClass, FalseClass]
47
+ #
48
+ # @api public
49
+ def diff?
50
+ ! diff.empty?
51
+ end
52
+
53
+ # Return if there's no diff between the original and changeset data
54
+ #
55
+ # @return [TrueClass, FalseClass]
56
+ #
57
+ # @api public
58
+ def clean?
59
+ diff.empty?
60
+ end
61
+
62
+ # Calculate the diff between the original and changeset data
63
+ #
64
+ # @return [Hash]
65
+ #
66
+ # @api public
67
+ def diff
68
+ @diff ||=
69
+ begin
70
+ source = Hash(original)
71
+ data = pipe.for_diff(__data__)
72
+ data_tuple = data.to_a
73
+ data_keys = data.keys & source.keys
74
+
75
+ new_tuple = data_tuple.to_a.select { |(k, _)| data_keys.include?(k) }
76
+ ori_tuple = source.to_a.select { |(k, _)| data_keys.include?(k) }
77
+
78
+ Hash[new_tuple - (new_tuple & ori_tuple)]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module ROM
2
+ class Changeset
3
+ VERSION = '1.0.0.beta1'.freeze
4
+ end
5
+ end
@@ -0,0 +1,145 @@
1
+ require 'dry/core/class_attributes'
2
+ require 'dry/core/cache'
3
+
4
+ require 'rom/constants'
5
+ require 'rom/initializer'
6
+
7
+ module ROM
8
+ # Abstract Changeset class
9
+ #
10
+ # If you inherit from this class you need to configure additional settings
11
+ #
12
+ # @example define a custom changeset using :upsert command
13
+ # class NewTag < ROM::Changeset[:tags]
14
+ # command_type :upsert
15
+ # end
16
+ #
17
+ # @abstract
18
+ class Changeset
19
+ DEFAULT_COMMAND_OPTS = { mapper: false }.freeze
20
+
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.relation
39
+ # Get or set changeset relation identifier
40
+ #
41
+ # @overload relation
42
+ # Return configured relation identifier for this changeset
43
+ # @return [Symbol]
44
+ #
45
+ # @overload relation(identifier)
46
+ # Set relation identifier for this changeset
47
+ # @param [Symbol] identifier The relation identifier from the ROM container
48
+ # @return [Symbol]
49
+ defines :relation
50
+
51
+ # @!attribute [r] relation
52
+ # @return [Relation] The changeset relation
53
+ param :relation
54
+
55
+ # @!attribute [r] command_type
56
+ # @return [Symbol] a custom command identifier
57
+ option :command_type, default: -> { self.class.command_type }
58
+
59
+ # Create a changeset class preconfigured for a specific relation
60
+ #
61
+ # @example
62
+ # class NewUserChangeset < ROM::Changeset::Create[:users]
63
+ # end
64
+ #
65
+ # user_repo.changeset(NewUserChangeset).data(name: 'Jane')
66
+ #
67
+ # @api public
68
+ def self.[](relation_name)
69
+ fetch_or_store([relation_name, self]) {
70
+ Class.new(self) { relation(relation_name) }
71
+ }
72
+ end
73
+
74
+ # Return a new changeset with updated options
75
+ #
76
+ # @example
77
+ # class NewUser < ROM::Changeset::Create[:users]
78
+ # option :token_generator
79
+ # end
80
+ #
81
+ # changeset = user_repo.changeset(NewUser).with(token_generator: my_token_gen)
82
+ #
83
+ # @param [Hash] new_options The new options
84
+ #
85
+ # @return [Changeset]
86
+ #
87
+ # @api public
88
+ def with(new_options)
89
+ self.class.new(relation, options.merge(new_options))
90
+ end
91
+
92
+ # Return a new changeset with provided relation
93
+ #
94
+ # New options can be provided too
95
+ #
96
+ # @param [Relation] relation
97
+ # @param [Hash] options
98
+ #
99
+ # @return [Changeset]
100
+ #
101
+ # @api public
102
+ def new(relation, new_options = EMPTY_HASH)
103
+ self.class.new(relation, new_options.empty? ? options : options.merge(new_options))
104
+ end
105
+
106
+ # Persist changeset
107
+ #
108
+ # @example
109
+ # changeset = user_repo.changeset(name: 'Jane')
110
+ # changeset.commit
111
+ # # => { id: 1, name: 'Jane' }
112
+ #
113
+ # @return [Hash, Array]
114
+ #
115
+ # @api public
116
+ def commit
117
+ command.call
118
+ end
119
+
120
+ # Return string representation of the changeset
121
+ #
122
+ # @return [String]
123
+ #
124
+ # @api public
125
+ def inspect
126
+ %(#<#{self.class} relation=#{relation.name.inspect}>)
127
+ end
128
+
129
+ # Return a command for this changeset
130
+ #
131
+ # @return [ROM::Command]
132
+ #
133
+ # @api private
134
+ def command
135
+ relation.command(command_type, DEFAULT_COMMAND_OPTS)
136
+ end
137
+ end
138
+ end
139
+
140
+ require 'rom/changeset/stateful'
141
+ require 'rom/changeset/associated'
142
+
143
+ require 'rom/changeset/create'
144
+ require 'rom/changeset/update'
145
+ require 'rom/changeset/delete'
@@ -0,0 +1,50 @@
1
+ require 'rom/support/notifications'
2
+
3
+ require 'rom/changeset/create'
4
+ require 'rom/changeset/update'
5
+ require 'rom/changeset/delete'
6
+
7
+ module ROM
8
+ module Plugins
9
+ module Relation
10
+ module Changeset
11
+ TYPES = {
12
+ create: ROM::Changeset::Create,
13
+ update: ROM::Changeset::Update,
14
+ delete: ROM::Changeset::Delete
15
+ }.freeze
16
+
17
+ extend Notifications::Listener
18
+
19
+ subscribe('configuration.relations.class.ready') do |event|
20
+ event[:relation].include(InstanceMethods)
21
+ end
22
+
23
+ module InstanceMethods
24
+
25
+ # Create a changeset for a relation
26
+ #
27
+ # @return [Changeset]
28
+ #
29
+ # @api public
30
+ def changeset(type, data = EMPTY_HASH)
31
+ klass = type.is_a?(Symbol) ? TYPES.fetch(type) : type
32
+
33
+ unless klass < ROM::Changeset
34
+ raise ArgumentError, "+#{type.name}+ must be a Changeset descendant"
35
+ end
36
+
37
+ if klass < ROM::Changeset::Stateful
38
+ klass.new(self, __data__: data)
39
+ else
40
+ klass.new(self)
41
+ end
42
+ rescue KeyError
43
+ raise ArgumentError,
44
+ "+#{type.inspect}+ is not a valid changeset type. Must be one of: #{TYPES.keys.inspect}"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,2 @@
1
+ require 'rom/changeset'
2
+ require 'rom/plugins/relation/changeset'
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rom-changeset
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.beta1
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Solnica
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.3'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.3.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.3'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: transproc
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '11.2'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '11.2'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rspec
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.5'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.5'
75
+ description: rom-changeset adds support for preprocessing data on top of rom-rb repositories
76
+ email: piotr.solnica+oss@gmail.com
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - CHANGELOG.md
82
+ - LICENSE
83
+ - README.md
84
+ - lib/rom-changeset.rb
85
+ - lib/rom/changeset.rb
86
+ - lib/rom/changeset/associated.rb
87
+ - lib/rom/changeset/create.rb
88
+ - lib/rom/changeset/delete.rb
89
+ - lib/rom/changeset/pipe.rb
90
+ - lib/rom/changeset/restricted.rb
91
+ - lib/rom/changeset/stateful.rb
92
+ - lib/rom/changeset/update.rb
93
+ - lib/rom/changeset/version.rb
94
+ - lib/rom/plugins/relation/changeset.rb
95
+ homepage: http://rom-rb.org
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">"
111
+ - !ruby/object:Gem::Version
112
+ version: 1.3.1
113
+ requirements: []
114
+ rubyforge_project:
115
+ rubygems_version: 2.6.12
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Changeset abstraction for rom-rb
119
+ test_files: []