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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +20 -0
- data/README.md +7 -0
- data/lib/rom/changeset/associated.rb +100 -0
- data/lib/rom/changeset/create.rb +16 -0
- data/lib/rom/changeset/delete.rb +17 -0
- data/lib/rom/changeset/pipe.rb +92 -0
- data/lib/rom/changeset/restricted.rb +28 -0
- data/lib/rom/changeset/stateful.rb +280 -0
- data/lib/rom/changeset/update.rb +83 -0
- data/lib/rom/changeset/version.rb +5 -0
- data/lib/rom/changeset.rb +145 -0
- data/lib/rom/plugins/relation/changeset.rb +50 -0
- data/lib/rom-changeset.rb +2 -0
- metadata +119 -0
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,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,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,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
|
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: []
|