rom-changeset 1.0.0.beta1

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.
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: []