rom-repository 1.0.0.beta3 → 1.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Test Coverage](https://codeclimate.com/github/rom-rb/rom-repository/badges/coverage.svg)][codeclimate]
|
14
14
|
[![Inline docs](http://inch-ci.org/github/rom-rb/rom-repository.svg?branch=master)][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
|