rom-repository 1.0.0.beta3 → 1.0.0.rc1
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 +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +3 -7
- data/CHANGELOG.md +1 -0
- data/README.md +1 -1
- data/lib/rom/repository/changeset/associated.rb +76 -0
- data/lib/rom/repository/changeset/create.rb +2 -52
- data/lib/rom/repository/changeset/delete.rb +7 -9
- data/lib/rom/repository/changeset/pipe.rb +3 -0
- data/lib/rom/repository/changeset/stateful.rb +240 -0
- data/lib/rom/repository/changeset/update.rb +11 -34
- data/lib/rom/repository/changeset.rb +64 -144
- data/lib/rom/repository/class_interface.rb +4 -4
- data/lib/rom/repository/command_compiler.rb +17 -15
- data/lib/rom/repository/command_proxy.rb +2 -0
- data/lib/rom/repository/relation_proxy/combine.rb +3 -4
- data/lib/rom/repository/relation_proxy.rb +24 -7
- data/lib/rom/repository/root.rb +14 -1
- data/lib/rom/repository/session.rb +3 -0
- data/lib/rom/repository/struct_builder.rb +1 -1
- data/lib/rom/repository/version.rb +1 -1
- data/lib/rom/repository.rb +92 -32
- data/lib/rom/struct.rb +40 -2
- data/rom-repository.gemspec +2 -2
- data/spec/integration/changeset_spec.rb +27 -0
- data/spec/unit/changeset_spec.rb +45 -7
- data/spec/unit/repository/changeset_spec.rb +59 -22
- data/spec/unit/repository/transaction_spec.rb +15 -1
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6bc1f6b343814740048fa138304b11012da7116
|
4
|
+
data.tar.gz: f61f8773af19190816e3d372fb44f3c94e6b5cef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57f9e84fa9ed7296a5d792c4d887777083c01685f63f80fe94f22ee50ed0f12f82b06ebfe8f48930a4a1c42aa7004a85e75fa38c2d330419636a26ce14b17a1a
|
7
|
+
data.tar.gz: cbf38fe68a94152d598025f496061f2d25cfcea5670712e426b47325b12bb9bafeaf6ea34b410931949f41ed56d9aaa4bb5f5c73a478e5c78d4a1525be33f82c
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -10,18 +10,14 @@ after_success:
|
|
10
10
|
- '[ "${TRAVIS_JOB_NUMBER#*.}" = "1" ] && [ "$TRAVIS_BRANCH" = "master" ] && bundle exec codeclimate-test-reporter'
|
11
11
|
rvm:
|
12
12
|
- 2.4.0
|
13
|
-
- 2.3
|
14
|
-
- 2.2
|
13
|
+
- 2.3
|
14
|
+
- 2.2
|
15
15
|
- rbx-3
|
16
|
-
- jruby
|
16
|
+
- jruby
|
17
17
|
env:
|
18
18
|
global:
|
19
19
|
- JRUBY_OPTS='--dev -J-Xmx1024M'
|
20
20
|
- COVERAGE='true'
|
21
|
-
matrix:
|
22
|
-
allow_failures:
|
23
|
-
- rvm: rbx-3
|
24
|
-
- rvm: jruby-9.1.6.0
|
25
21
|
notifications:
|
26
22
|
webhooks:
|
27
23
|
urls:
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -13,7 +13,7 @@
|
|
13
13
|
[][codeclimate]
|
14
14
|
[][inchpages]
|
15
15
|
|
16
|
-
|
16
|
+
Repositories for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping, changesets and commands.
|
17
17
|
|
18
18
|
Resources:
|
19
19
|
|
@@ -0,0 +1,76 @@
|
|
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] right
|
16
|
+
# @return [Changeset::Create, Hash, #to_hash] Parent changeset or data
|
17
|
+
param :right
|
18
|
+
|
19
|
+
# @!attribute [r] association
|
20
|
+
# @return [Symbol] Association identifier from relation schema
|
21
|
+
option :association, reader: true
|
22
|
+
|
23
|
+
# Commit changeset's composite command
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# task_changeset = task_repo.
|
27
|
+
# changeset(title: 'Task One').
|
28
|
+
# associate(user, :user).
|
29
|
+
# commit
|
30
|
+
# # {:id => 1, :user_id => 1, title: 'Task One'}
|
31
|
+
#
|
32
|
+
# @return [Array<Hash>, Hash]
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
def commit
|
36
|
+
command.call
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a composed command
|
40
|
+
#
|
41
|
+
# @example using existing parent data
|
42
|
+
# user_changeset = user_repo.changeset(name: 'Jane')
|
43
|
+
# task_changeset = task_repo.changeset(title: 'Task One')
|
44
|
+
#
|
45
|
+
# user = user_repo.create(user_changeset)
|
46
|
+
# task = task_repo.create(task_changeset.associate(user, :user))
|
47
|
+
#
|
48
|
+
# @example saving both parent and child in one go
|
49
|
+
# user_changeset = user_repo.changeset(name: 'Jane')
|
50
|
+
# task_changeset = task_repo.changeset(title: 'Task One')
|
51
|
+
#
|
52
|
+
# task = task_repo.create(task_changeset.associate(user, :user))
|
53
|
+
#
|
54
|
+
# This works *only* with parent => child(ren) changeset hierarchy
|
55
|
+
#
|
56
|
+
# @return [ROM::Command::Composite]
|
57
|
+
#
|
58
|
+
# @api public
|
59
|
+
def command
|
60
|
+
case right
|
61
|
+
when Changeset
|
62
|
+
left.command.curry(left) >> right.command.with_association(association).curry(right)
|
63
|
+
when Associated
|
64
|
+
left.command.curry(left) >> right.command.with_association(association)
|
65
|
+
else
|
66
|
+
left.command.with_association(association).curry(left, right)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @api private
|
71
|
+
def relation
|
72
|
+
left.relation
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -3,58 +3,8 @@ module ROM
|
|
3
3
|
# Changeset specialization for create commands
|
4
4
|
#
|
5
5
|
# @api public
|
6
|
-
class Create <
|
7
|
-
|
8
|
-
# @return [Array] Associated changesets with its association name
|
9
|
-
option :association, reader: true, optional: true
|
10
|
-
|
11
|
-
# @api public
|
12
|
-
def associate(other, assoc)
|
13
|
-
with(association: [other, assoc])
|
14
|
-
end
|
15
|
-
|
16
|
-
# Return false
|
17
|
-
#
|
18
|
-
# @return [FalseClass]
|
19
|
-
#
|
20
|
-
# @api public
|
21
|
-
def update?
|
22
|
-
false
|
23
|
-
end
|
24
|
-
|
25
|
-
# Return true
|
26
|
-
#
|
27
|
-
# @return [TrueClass]
|
28
|
-
#
|
29
|
-
# @api public
|
30
|
-
def create?
|
31
|
-
true
|
32
|
-
end
|
33
|
-
|
34
|
-
# @api private
|
35
|
-
def command
|
36
|
-
if options[:association]
|
37
|
-
other, assoc = options[:association]
|
38
|
-
|
39
|
-
if other.is_a?(Changeset)
|
40
|
-
create_command.curry(self) >> other.command.with_association(assoc)
|
41
|
-
else
|
42
|
-
create_command.with_association(assoc).curry(self, other)
|
43
|
-
end
|
44
|
-
else
|
45
|
-
create_command.curry(self)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# @api private
|
50
|
-
def create_command
|
51
|
-
command_compiler.(command_type, relation, mapper: false, result: result)
|
52
|
-
end
|
53
|
-
|
54
|
-
# @api private
|
55
|
-
def default_command_type
|
56
|
-
:create
|
57
|
-
end
|
6
|
+
class Create < Stateful
|
7
|
+
command_type :create
|
58
8
|
end
|
59
9
|
end
|
60
10
|
end
|
@@ -1,15 +1,13 @@
|
|
1
1
|
module ROM
|
2
2
|
class Changeset
|
3
|
+
# Changeset specialization for delete commands
|
4
|
+
#
|
5
|
+
# Delete changesets will execute delete command for its relation, which
|
6
|
+
# means proper restricted relations should be used with this changeset.
|
7
|
+
#
|
8
|
+
# @api public
|
3
9
|
class Delete < Changeset
|
4
|
-
|
5
|
-
def command
|
6
|
-
command_compiler.(command_type, relation, mapper: false)
|
7
|
-
end
|
8
|
-
|
9
|
-
# @api private
|
10
|
-
def default_command_type
|
11
|
-
:delete
|
12
|
-
end
|
10
|
+
command_type :delete
|
13
11
|
end
|
14
12
|
end
|
15
13
|
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
require 'rom/repository/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__, reader: true, optional: true, default: proc { nil }
|
17
|
+
|
18
|
+
# @!attribute [r] pipe
|
19
|
+
# @return [Changeset::Pipe] data transformation pipe
|
20
|
+
# @api private
|
21
|
+
option :pipe, reader: true, accept: [Proc, Pipe], default: -> changeset {
|
22
|
+
changeset.class.default_pipe(changeset)
|
23
|
+
}
|
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, Transproc::Function>]
|
56
|
+
#
|
57
|
+
# @api public
|
58
|
+
def self.map(&block)
|
59
|
+
if block.arity.zero?
|
60
|
+
pipes << Class.new(Pipe, &block).new
|
61
|
+
else
|
62
|
+
pipes << Pipe.new(block)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Build default pipe object
|
67
|
+
#
|
68
|
+
# This can be overridden in a custom changeset subclass
|
69
|
+
#
|
70
|
+
# @return [Pipe]
|
71
|
+
def self.default_pipe(context)
|
72
|
+
pipes.size > 0 ? pipes.map { |p| p.bind(context) }.reduce(:>>) : EMPTY_PIPE
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def self.inherited(klass)
|
77
|
+
return if klass == ROM::Changeset
|
78
|
+
super
|
79
|
+
klass.instance_variable_set(:@__pipes__, pipes ? pipes.dup : EMPTY_ARRAY)
|
80
|
+
end
|
81
|
+
|
82
|
+
# @api private
|
83
|
+
def self.pipes
|
84
|
+
@__pipes__
|
85
|
+
end
|
86
|
+
|
87
|
+
# Pipe changeset's data using custom steps define on the pipe
|
88
|
+
#
|
89
|
+
# @overload map(*steps)
|
90
|
+
# Apply mapping using built-in transformations
|
91
|
+
#
|
92
|
+
# @example
|
93
|
+
# changeset.map(:add_timestamps)
|
94
|
+
#
|
95
|
+
# @param [Array<Symbol>] steps A list of mapping steps
|
96
|
+
|
97
|
+
# @overload map(&block)
|
98
|
+
# Apply mapping using a custom block
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# changeset.map { |tuple| tuple.merge(created_at: Time.now) }
|
102
|
+
#
|
103
|
+
# @param [Array<Symbol>] steps A list of mapping steps
|
104
|
+
#
|
105
|
+
# @overload map(*steps, &block)
|
106
|
+
# Apply mapping using built-in transformations and a custom block
|
107
|
+
#
|
108
|
+
# @example
|
109
|
+
# changeset.map(:touch) { |tuple| tuple.merge(status: 'published') }
|
110
|
+
#
|
111
|
+
# @param [Array<Symbol>] steps A list of mapping steps
|
112
|
+
#
|
113
|
+
# @return [Changeset]
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
def map(*steps, &block)
|
117
|
+
if block
|
118
|
+
if steps.size > 0
|
119
|
+
map(*steps).map(&block)
|
120
|
+
else
|
121
|
+
with(pipe: pipe >> Pipe.new(block).bind(self))
|
122
|
+
end
|
123
|
+
else
|
124
|
+
with(pipe: steps.reduce(pipe) { |a, e| a >> pipe[e] })
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return changeset with data
|
129
|
+
#
|
130
|
+
# @param [Hash] data
|
131
|
+
#
|
132
|
+
# @return [Changeset]
|
133
|
+
#
|
134
|
+
# @api public
|
135
|
+
def data(data)
|
136
|
+
with(__data__: data)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Coerce changeset to a hash
|
140
|
+
#
|
141
|
+
# This will send the data through the pipe
|
142
|
+
#
|
143
|
+
# @return [Hash]
|
144
|
+
#
|
145
|
+
# @api public
|
146
|
+
def to_h
|
147
|
+
pipe.call(__data__)
|
148
|
+
end
|
149
|
+
alias_method :to_hash, :to_h
|
150
|
+
|
151
|
+
# Coerce changeset to an array
|
152
|
+
#
|
153
|
+
# This will send the data through the pipe
|
154
|
+
#
|
155
|
+
# @return [Array]
|
156
|
+
#
|
157
|
+
# @api public
|
158
|
+
def to_a
|
159
|
+
result == :one ? [to_h] : __data__.map { |element| pipe.call(element) }
|
160
|
+
end
|
161
|
+
alias_method :to_ary, :to_a
|
162
|
+
|
163
|
+
# Commit stateful changeset
|
164
|
+
#
|
165
|
+
# @see Changeset#commit
|
166
|
+
#
|
167
|
+
# @api public
|
168
|
+
def commit
|
169
|
+
command.call(self)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Associate a changeset with another changeset or hash-like object
|
173
|
+
#
|
174
|
+
# @example with another changeset
|
175
|
+
# new_user = user_repo.changeset(name: 'Jane')
|
176
|
+
# new_task = user_repo.changeset(:tasks, title: 'A task')
|
177
|
+
#
|
178
|
+
# new_task.associate(new_user, :users)
|
179
|
+
#
|
180
|
+
# @example with a hash-like object
|
181
|
+
# user = user_repo.users.by_pk(1).one
|
182
|
+
# new_task = user_repo.changeset(:tasks, title: 'A task')
|
183
|
+
#
|
184
|
+
# new_task.associate(user, :users)
|
185
|
+
#
|
186
|
+
# @param [#to_hash, Changeset] other Other changeset or hash-like object
|
187
|
+
# @param [Symbol] assoc The association identifier from schema
|
188
|
+
#
|
189
|
+
# @api public
|
190
|
+
def associate(other, name)
|
191
|
+
Associated.new(self, other, association: name)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return command result type
|
195
|
+
#
|
196
|
+
# @return [Symbol]
|
197
|
+
#
|
198
|
+
# @api private
|
199
|
+
def result
|
200
|
+
__data__.is_a?(Hash) ? :one : :many
|
201
|
+
end
|
202
|
+
|
203
|
+
# @api public
|
204
|
+
def command
|
205
|
+
command_compiler.(command_type, relation_identifier, DEFAULT_COMMAND_OPTS.merge(result: result))
|
206
|
+
end
|
207
|
+
|
208
|
+
# Return string representation of the changeset
|
209
|
+
#
|
210
|
+
# @return [String]
|
211
|
+
#
|
212
|
+
# @api public
|
213
|
+
def inspect
|
214
|
+
%(#<#{self.class} relation=#{relation.name.inspect} data=#{__data__}>)
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
# @api private
|
220
|
+
def respond_to_missing?(meth, include_private = false)
|
221
|
+
super || __data__.respond_to?(meth)
|
222
|
+
end
|
223
|
+
|
224
|
+
# @api private
|
225
|
+
def method_missing(meth, *args, &block)
|
226
|
+
if __data__.respond_to?(meth)
|
227
|
+
response = __data__.__send__(meth, *args, &block)
|
228
|
+
|
229
|
+
if response.is_a?(__data__.class)
|
230
|
+
with(__data__: response)
|
231
|
+
else
|
232
|
+
response
|
233
|
+
end
|
234
|
+
else
|
235
|
+
super
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -2,11 +2,16 @@ module ROM
|
|
2
2
|
class Changeset
|
3
3
|
# Changeset specialization for update commands
|
4
4
|
#
|
5
|
+
# Update changesets will only execute their commands when
|
6
|
+
# the data is different from the original tuple. Original tuple
|
7
|
+
# is fetched from changeset's relation using `by_pk` relation view.
|
8
|
+
# This means the underlying adapter must provide this view, or you
|
9
|
+
# you need to implement it yourself in your relations if you want to
|
10
|
+
# use Update changesets.
|
11
|
+
#
|
5
12
|
# @api public
|
6
|
-
class Update <
|
7
|
-
|
8
|
-
# @return [Symbol] The name of the relation's primary key attribute
|
9
|
-
option :primary_key, reader: true
|
13
|
+
class Update < Stateful
|
14
|
+
command_type :update
|
10
15
|
|
11
16
|
# Commit update changeset if there's a diff
|
12
17
|
#
|
@@ -21,31 +26,13 @@ module ROM
|
|
21
26
|
diff? ? super : original
|
22
27
|
end
|
23
28
|
|
24
|
-
# Return true
|
25
|
-
#
|
26
|
-
# @return [TrueClass]
|
27
|
-
#
|
28
|
-
# @api public
|
29
|
-
def update?
|
30
|
-
true
|
31
|
-
end
|
32
|
-
|
33
|
-
# Return false
|
34
|
-
#
|
35
|
-
# @return [FalseClass]
|
36
|
-
#
|
37
|
-
# @api public
|
38
|
-
def create?
|
39
|
-
false
|
40
|
-
end
|
41
|
-
|
42
29
|
# Return original tuple that this changeset may update
|
43
30
|
#
|
44
31
|
# @return [Hash]
|
45
32
|
#
|
46
33
|
# @api public
|
47
34
|
def original
|
48
|
-
@original ||= relation.
|
35
|
+
@original ||= Hash(relation.one)
|
49
36
|
end
|
50
37
|
|
51
38
|
# Return diff hash sent through the pipe
|
@@ -78,7 +65,7 @@ module ROM
|
|
78
65
|
|
79
66
|
# Calculate the diff between the original and changeset data
|
80
67
|
#
|
81
|
-
# @return [Hash
|
68
|
+
# @return [Hash, Array]
|
82
69
|
#
|
83
70
|
# @api public
|
84
71
|
def diff
|
@@ -90,16 +77,6 @@ module ROM
|
|
90
77
|
Hash[new_tuple - (new_tuple & ori_tuple)]
|
91
78
|
end
|
92
79
|
end
|
93
|
-
|
94
|
-
# @api private
|
95
|
-
def command
|
96
|
-
command_compiler.(command_type, relation, mapper: false).curry(to_h) if diff?
|
97
|
-
end
|
98
|
-
|
99
|
-
# @api private
|
100
|
-
def default_command_type
|
101
|
-
:update
|
102
|
-
end
|
103
80
|
end
|
104
81
|
end
|
105
82
|
end
|