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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8deef9fc5ae64394c2845f34d1575ff4f55d503d
4
- data.tar.gz: 746ac682b439a3ffd603cd79e64b2bf52ead6d8b
3
+ metadata.gz: a6bc1f6b343814740048fa138304b11012da7116
4
+ data.tar.gz: f61f8773af19190816e3d372fb44f3c94e6b5cef
5
5
  SHA512:
6
- metadata.gz: c970db02832c84c5287a4c537bcc851abd07013947fdaeccccf6db117bf5b7817b4b10dbbfa0efc6683be30830e826eeaf5aba504b3e47663348a94291c05b06
7
- data.tar.gz: 410546d65acdec9fedb221809ed1e2b098621da1b034eb8580b34c333931fdee16ba5ece929f9ccdfbc9be52150eb7fe89c5531ebea852c2cbd3c090f8a3bc44
6
+ metadata.gz: 57f9e84fa9ed7296a5d792c4d887777083c01685f63f80fe94f22ee50ed0f12f82b06ebfe8f48930a4a1c42aa7004a85e75fa38c2d330419636a26ce14b17a1a
7
+ data.tar.gz: cbf38fe68a94152d598025f496061f2d25cfcea5670712e426b47325b12bb9bafeaf6ea34b410931949f41ed56d9aaa4bb5f5c73a478e5c78d4a1525be33f82c
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  Gemfile.lock
2
2
  log/*.log
3
+ doc
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.3
14
- - 2.2.6
13
+ - 2.3
14
+ - 2.2
15
15
  - rbx-3
16
- - jruby-9.1.6.0
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
@@ -19,6 +19,7 @@
19
19
 
20
20
  * `ROM::Struct` is now based on `Dry::Struct` (solnic)
21
21
  * rom-support dependency was removed (flash-gordon)
22
+ * `update?` and `create?` methods were removed from `Changeset:*` subclasses (solnic)
22
23
 
23
24
  ### Fixed
24
25
 
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
- Repository for [rom-rb](https://github.com/rom-rb/rom) with auto-mapping and commands.
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 < Changeset
7
- # @!attribute [r] association
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
- # @api private
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
@@ -3,6 +3,9 @@ require 'transproc/transformer'
3
3
 
4
4
  module ROM
5
5
  class Changeset
6
+ # Composable data transformation pipe used by default in changesets
7
+ #
8
+ # @api private
6
9
  class Pipe < Transproc::Transformer
7
10
  extend Transproc::Registry
8
11
 
@@ -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 < Changeset
7
- # @!attribute [r] primary_key
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.fetch(primary_key)
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