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 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