rom-rails 0.3.0.beta1 → 0.3.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: dca5938e41a98dc3af6fe2aa5d301c13a6984d04
4
- data.tar.gz: 372f0581874750544b013ff63048ff60ee69085b
3
+ metadata.gz: 2f534a7630fb69accef529f372f2b211f35c7985
4
+ data.tar.gz: c530fd02f02046a61e6f86173f8ba28ef6820474
5
5
  SHA512:
6
- metadata.gz: d0b26883fb509ab4ff4a39294b84d90436bd73c736e653f16ad90b4fb89c8fe6949590ac3f5f1513f0da752291ed96e6d69fb26fd164e47ba22b6a6cff400e94
7
- data.tar.gz: d5fe5b5a9c13523bd2d53c5c193402de75e963edce3a79c6a815eff1c8a2f03010af8520cce4f037850a100dcccd96ba7cc1a692d6fd71bbd80fcc531f3c5ba0
6
+ metadata.gz: 5e70baa4ab07025be46b4f6ff1e19fad7830150298fa90de2e8fcd5f87cba06967057d48e3b3c6b6ecdd35ef6305cf9770db853aa0c24f8f49150f2a6725b902
7
+ data.tar.gz: 6b31f9a2aff08f7a4747bb045061f5f3658bfeff2d204abc0981ebea5d4a27094798b7bacc2ebf126690669b7ea40517f140b6341e28f1bdbec6647f2c5b5d5a
data/CHANGELOG.md CHANGED
@@ -5,9 +5,9 @@
5
5
  * `ROM::Model::Form` for modeling and setting up web-forms (solnic + cflipse)
6
6
  * Support for timestamps attributes in Form objects (kchien)
7
7
 
8
-
9
8
  ### Changed
10
9
 
10
+ * [BREAKING] Model::Params renamed to Model::Attributes (solnic)
11
11
  * Improved initialization process which works with AR-style configurations (aflatter)
12
12
  * Allow setup using a configuration block from railtie (aflatter)
13
13
 
@@ -18,7 +18,7 @@ class Edit<%= model_name %>Form < ROM::Model::Form
18
18
  end
19
19
 
20
20
  def commit!
21
- <%= relation %>.try { <%= relation %>.update.by_id(id).set(params) }
21
+ <%= relation %>.try { <%= relation %>.update.by_id(id).set(attributes) }
22
22
  end
23
23
 
24
24
  end
@@ -18,7 +18,7 @@ class New<%= model_name %>Form < ROM::Model::Form
18
18
  end
19
19
 
20
20
  def commit!
21
- <%= relation %>.try { <%= relation %>.create.call(params) }
21
+ <%= relation %>.try { <%= relation %>.create.call(attributes) }
22
22
  end
23
23
 
24
24
  end
@@ -1,6 +1,6 @@
1
1
  class <%= model_name %>Mapper < ROM::Mapper
2
- # relation :<%= relation %>
3
- #
2
+ relation :<%= relation %>
3
+
4
4
  # specify model and attributes ie
5
5
  #
6
6
  # model <%= model_name %>
data/lib/rom/model.rb CHANGED
@@ -9,6 +9,6 @@ module ROM
9
9
  end
10
10
  end
11
11
 
12
- require 'rom/rails/model/params'
12
+ require 'rom/rails/model/attributes'
13
13
  require 'rom/rails/model/validator'
14
14
  require 'rom/rails/model/form'
@@ -3,44 +3,9 @@ module ROM
3
3
  RelationParamsMissingError = Class.new(StandardError)
4
4
 
5
5
  module ControllerExtension
6
- def self.included(klass)
7
- klass.extend(ClassExtensions)
8
- end
9
-
10
6
  def rom
11
7
  ROM.env
12
8
  end
13
-
14
- module ClassExtensions
15
- def relation(path, options)
16
- root, method = path.split('.').map(&:to_sym)
17
-
18
- name = options.fetch(:as) { root }
19
- requires = Array(options.fetch(:requires) { [] })
20
-
21
- before_filter(options.except(:as, :requires)) do
22
- args = params.values_at(*requires)
23
-
24
- if requires.any? && args.none?
25
- raise RelationParamsMissingError
26
- else
27
- relation =
28
- if args.any?
29
- rom.read(root).send(method, *args)
30
- else
31
- rom.read(root).send(method)
32
- end
33
-
34
- instance_variable_set("@#{name}", relation.to_a)
35
- end
36
- end
37
-
38
- unless respond_to?(name)
39
- attr_reader name
40
- helper_method name
41
- end
42
- end
43
- end
44
9
  end
45
10
  end
46
11
  end
@@ -0,0 +1,133 @@
1
+ require 'virtus'
2
+ require 'active_model/conversion'
3
+
4
+ module ROM
5
+ module Model
6
+ # Mixin for validatable and coercible parameters
7
+ #
8
+ # @example
9
+ #
10
+ # class UserAttributes
11
+ # include ROM::Model::Attributes
12
+ #
13
+ # attribute :email, String
14
+ # attribute :age, Integer
15
+ #
16
+ # validates :email, :age, presence: true
17
+ # end
18
+ #
19
+ # user_attrs = UserAttributes.new(email: '', age: '18')
20
+ #
21
+ # user_attrs.email # => ''
22
+ # user_attrs.age # => 18
23
+ #
24
+ # user_attrs.valid? # => false
25
+ # user_attrs.errors # => #<ActiveModel::Errors:0x007fd2423fadb0 ...>
26
+ #
27
+ # @api public
28
+ module Attributes
29
+ VirtusModel = Virtus.model(nullify_blank: true)
30
+
31
+ # Inclusion hook used to extend a class with required interfaces
32
+ #
33
+ # @api private
34
+ def self.included(base)
35
+ base.class_eval do
36
+ include VirtusModel
37
+ include ActiveModel::Conversion
38
+ end
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ # Return model name for the attributes class
43
+ #
44
+ # The model name object is configurable using `set_model_name` macro
45
+ #
46
+ # @see ClassMethods#set_model_name
47
+ #
48
+ # @return [ActiveModel::Name]
49
+ #
50
+ # @api public
51
+ def model_name
52
+ self.class.model_name
53
+ end
54
+
55
+ # Class extensions for an attributes class
56
+ #
57
+ # @api public
58
+ module ClassMethods
59
+ # Default timestamp attribute names used by `timestamps` method
60
+ DEFAULT_TIMESTAMPS = [:created_at, :updated_at].freeze
61
+
62
+ # Process input and return attributes instance
63
+ #
64
+ # @example
65
+ # class UserAttributes
66
+ # include ROM::Model::Attributes
67
+ #
68
+ # attribute :name, String
69
+ # end
70
+ #
71
+ # UserAttributes[name: 'Jane']
72
+ #
73
+ # @param [Hash,#to_hash] input The input params
74
+ #
75
+ # @return [Attributes]
76
+ #
77
+ # @api public
78
+ def [](input)
79
+ input.is_a?(self) ? input : new(input)
80
+ end
81
+
82
+ # Macro for defining ActiveModel::Name object on the attributes class
83
+ #
84
+ # This is essential for rails helpers to work properly when generating
85
+ # form input names etc.
86
+ #
87
+ # @example
88
+ # class UserAttributes
89
+ # include ROM::Model::Attributes
90
+ #
91
+ # set_model_name 'User'
92
+ # end
93
+ #
94
+ # @return [undefined]
95
+ #
96
+ # @api public
97
+ def set_model_name(name)
98
+ class_eval <<-RUBY
99
+ def self.model_name
100
+ @model_name ||= ActiveModel::Name.new(self, nil, #{name.inspect})
101
+ end
102
+ RUBY
103
+ end
104
+
105
+ # Shortcut for defining timestamp attributes like created_at etc.
106
+ #
107
+ # @example
108
+ # class NewPostAttributes
109
+ # include ROM::Model::Attributes
110
+ #
111
+ # # provide name(s) explicitly
112
+ # timestamps :published_at
113
+ #
114
+ # # defaults to :created_at, :updated_at without args
115
+ # timestamps
116
+ # end
117
+ #
118
+ # @api public
119
+ def timestamps(*attrs)
120
+ if attrs.empty?
121
+ DEFAULT_TIMESTAMPS.each do |t|
122
+ attribute t, DateTime, default: proc { DateTime.now }
123
+ end
124
+ else
125
+ attrs.each do |attr|
126
+ attribute attr, DateTime, default: proc { DateTime.now }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -1,24 +1,85 @@
1
- require 'rom/rails/model/form/dsl'
1
+ require 'rom/rails/model/form/class_interface'
2
2
 
3
3
  module ROM
4
4
  module Model
5
+ # Abstract form class
6
+ #
7
+ # Form objects in ROM are your top-level interface to persist data in the
8
+ # database. They combine many features that you know from ActiveRecord:
9
+ #
10
+ # * params processing with sanitization and coercion
11
+ # * attribute validations
12
+ # * persisting data in the database
13
+ #
14
+ # The major difference is that a ROM form object separates those
15
+ # responsibilities - a ROM form class has its own Attributes, Validator and
16
+ # ROM commands that are accessible within its instance.
17
+ #
18
+ # @example
19
+ # class UserForm < ROM::Model::Form
20
+ # commands users: :create
21
+ #
22
+ # input do
23
+ # set_model_name 'User'
24
+ #
25
+ # attribute :name, String
26
+ # end
27
+ #
28
+ # validations do
29
+ # validates :name, presence: true
30
+ # end
31
+ # end
32
+ #
33
+ # class CreateUserForm < UserForm
34
+ # attributes.timestamps :created_at
35
+ #
36
+ # def commit!
37
+ # users.try { users.create.call(attributes) }
38
+ # end
39
+ # end
40
+ #
41
+ # # then in your controller
42
+ # CreateUserForm.build(params[:user]).save
43
+ #
44
+ # @api public
5
45
  class Form
6
46
  include Equalizer.new(:params, :model, :result)
7
47
 
8
48
  extend ROM::ClassMacros
9
- extend Form::DSL
49
+ extend Form::ClassInterface
10
50
 
11
51
  defines :relation
12
52
 
13
- attr_reader :params, :model, :result
53
+ # Return raw params received from the request
54
+ #
55
+ # @return [Object]
56
+ #
57
+ # @api public
58
+ attr_reader :params
59
+
60
+ # Return model instance representing an ActiveModel object that will be
61
+ # persisted or updated
62
+ #
63
+ # @return [Object]
64
+ #
65
+ # @api public
66
+ attr_reader :model
67
+
68
+ # Return the result of commit!
69
+ #
70
+ # @return [Object]
71
+ #
72
+ # @api public
73
+ attr_reader :result
14
74
 
15
75
  delegate :model_name, :persisted?, :to_key, to: :model
16
76
  alias_method :to_model, :model
17
77
 
18
78
  class << self
19
- delegate :model_name, to: :params
79
+ delegate :model_name, to: :attributes
20
80
  end
21
81
 
82
+ # @api private
22
83
  def initialize(params = {}, options = {})
23
84
  @params = params
24
85
  @model = self.class.model.new(params.merge(options.slice(*self.class.key)))
@@ -27,19 +88,37 @@ module ROM
27
88
  options.each { |key, value| instance_variable_set("@#{key}", value) }
28
89
  end
29
90
 
91
+ # A specialized form object must implement this method
92
+ #
93
+ # @abstract
94
+ #
95
+ # @api public
30
96
  def commit!
31
97
  raise NotImplementedError, "#{self.class}#commit! must be implemented"
32
98
  end
33
99
 
100
+ # Save a form by calling commit! and memoizing result
101
+ #
102
+ # @return [self]
103
+ #
104
+ # @api public
34
105
  def save(*args)
35
106
  @result = commit!(*args)
36
107
  self
37
108
  end
38
109
 
110
+ # Return whether commit was successful
111
+ #
112
+ # @return [TrueClass,FalseClass]
113
+ #
114
+ # @api public
39
115
  def success?
40
116
  errors.nil? || !errors.any?
41
117
  end
42
118
 
119
+ # Trigger validation and store errors (if any)
120
+ #
121
+ # @api public
43
122
  def validate!
44
123
  validator = self.class::Validator.new(attributes)
45
124
  validator.validate
@@ -47,10 +126,22 @@ module ROM
47
126
  @errors = validator.errors
48
127
  end
49
128
 
129
+ # Sanitize and coerce input params
130
+ #
131
+ # This can also set default values
132
+ #
133
+ # @return [Model::Attributes]
134
+ #
135
+ # @api public
50
136
  def attributes
51
- self.class.params[params]
137
+ self.class.attributes[params]
52
138
  end
53
139
 
140
+ # Return errors
141
+ #
142
+ # @return [ActiveModel::Errors]
143
+ #
144
+ # @api public
54
145
  def errors
55
146
  (result && result.error) || @errors
56
147
  end
@@ -0,0 +1,401 @@
1
+ module ROM
2
+ module Model
3
+ class Form
4
+ module ClassInterface
5
+ # Return param handler class
6
+ #
7
+ # This class is used to process input params coming from a request and
8
+ # it's being created using `input` API
9
+ #
10
+ # @example
11
+ #
12
+ # class MyForm < ROM::Model::Form
13
+ # input do
14
+ # attribute :name, String
15
+ # end
16
+ # end
17
+ #
18
+ # MyForm.attributes # => MyForm::Attributes
19
+ #
20
+ # # process input params
21
+ # attributes = MyForm.attributes[name: 'Jane']
22
+ #
23
+ # @return [Class]
24
+ #
25
+ # @api public
26
+ attr_reader :attributes
27
+
28
+ # Return attributes validator
29
+ #
30
+ # @example
31
+ # class MyForm < ROM::Model::Form
32
+ # input do
33
+ # attribute :name, String
34
+ # end
35
+ #
36
+ # validations do
37
+ # validates :name, presence: true
38
+ # end
39
+ # end
40
+ #
41
+ # attributes = MyForm.attributes[name: nil]
42
+ # MyForm::Validator.call(attributes) # raises validation error
43
+ #
44
+ # @return [Class]
45
+ #
46
+ # @api public
47
+ attr_reader :validator
48
+
49
+ # Return model class
50
+ #
51
+ # @return [Class]
52
+ #
53
+ # @api public
54
+ attr_reader :model
55
+
56
+ # relation => command name mapping used to generate commands automatically
57
+ #
58
+ # @return [Hash]
59
+ #
60
+ # @api private
61
+ attr_reader :self_commands
62
+
63
+ # A list of relation names for which commands should be injected from
64
+ # the rom env automatically.
65
+ #
66
+ # This is used only when a given form re-uses existing commands
67
+ #
68
+ # @return [Hash]
69
+ #
70
+ # @api private
71
+ attr_reader :injectible_commands
72
+
73
+ # input block stored to be used in inherited hook
74
+ #
75
+ # @return [Proc]
76
+ #
77
+ # @api private
78
+ attr_reader :input_block
79
+
80
+ # validation block stored to be used in inherited hook
81
+ #
82
+ # @return [Proc]
83
+ #
84
+ # @api private
85
+ attr_reader :validations_block
86
+
87
+ # Copy input attributes, validator and model to the descendant
88
+ #
89
+ # @api private
90
+ def inherited(klass)
91
+ klass.inject_commands_for(*injectible_commands) if injectible_commands
92
+ klass.commands(*self_commands) if self_commands
93
+ klass.input(readers: false, &input_block) if input_block
94
+ klass.validations(&validations_block) if validations_block
95
+ super
96
+ end
97
+
98
+ # Set key for the model that is handled by a form object
99
+ #
100
+ # This defaults to [:id]
101
+ #
102
+ # @example
103
+ # class MyForm < ROM::Model::Form
104
+ # key [:user_id]
105
+ # end
106
+ #
107
+ # @return [Array<Symbol>]
108
+ #
109
+ # @api public
110
+ def key(*keys)
111
+ if keys.any? && !@key
112
+ @key = keys
113
+ attr_reader(*keys)
114
+ elsif !@key
115
+ @key = [:id]
116
+ attr_reader :id
117
+ elsif keys.any?
118
+ @key = keys
119
+ end
120
+ @key
121
+ end
122
+
123
+ # Specify what commands should be generated for a form object
124
+ #
125
+ # @example
126
+ # class MyForm < ROM::Model::Form
127
+ # commands users: :create
128
+ # end
129
+ #
130
+ # @param [Hash] relation => command name map
131
+ #
132
+ # @return [self]
133
+ #
134
+ # @api public
135
+ def commands(names)
136
+ names.each { |relation, _action| attr_reader(relation) }
137
+ @self_commands = names
138
+ self
139
+ end
140
+
141
+ # Specify input params handler class
142
+ #
143
+ # This uses Virtus DSL
144
+ #
145
+ # @example
146
+ # class MyForm < ROM::Model::Form
147
+ # input do
148
+ # set_model_name 'User'
149
+ #
150
+ # attribute :name, String
151
+ # attribute :age, Integer
152
+ # end
153
+ # end
154
+ #
155
+ # MyForm.build(name: 'Jane', age: 21).attributes
156
+ # # => #<MyForm::Attributes:0x007f821f863d48 @name="Jane", @age=21>
157
+ #
158
+ # @return [self]
159
+ #
160
+ # @api public
161
+ def input(options = {}, &block)
162
+ readers = options.fetch(:readers) { true }
163
+ define_attributes!(block)
164
+ define_attribute_readers! if readers
165
+ define_model!
166
+ self
167
+ end
168
+
169
+ # Specify attribute validator class
170
+ #
171
+ # This uses ActiveModel::Validations DSL
172
+ #
173
+ # @example
174
+ # class MyForm < ROM::Model::Form
175
+ # input do
176
+ # set_model_name 'User'
177
+ #
178
+ # attribute :name, String
179
+ # attribute :age, Integer
180
+ # end
181
+ #
182
+ # validations do
183
+ # validates :name, :age, presence: true
184
+ # end
185
+ # end
186
+ #
187
+ # form = MyForm.build(name: 'Jane', age: nil)
188
+ # # => #<MyForm::Attributes:0x007f821f863d48 @name="Jane", @age=21>
189
+ # form.validate! # raises
190
+ #
191
+ # @return [self]
192
+ #
193
+ # @api public
194
+ def validations(&block)
195
+ define_validator!(block)
196
+ self
197
+ end
198
+
199
+ # Inject specific commands from the rom env
200
+ #
201
+ # This can be used when the env has re-usable commands
202
+ #
203
+ # @example
204
+ # class MyForm < ROM::Model::Form
205
+ # inject_commands_for :users
206
+ # end
207
+ #
208
+ # @api public
209
+ def inject_commands_for(*names)
210
+ @injectible_commands = names
211
+ names.each { |name| attr_reader(name) }
212
+ self
213
+ end
214
+
215
+ # Build a form object using input params and options
216
+ #
217
+ # @example
218
+ # class MyForm < ROM::Model::Form
219
+ # input do
220
+ # set_model_name 'User'
221
+ #
222
+ # attribute :name, String
223
+ # attribute :age, Integer
224
+ # end
225
+ # end
226
+ #
227
+ # # form for a new object
228
+ # form = MyForm.build(name: 'Jane')
229
+ #
230
+ # # form for a persisted object
231
+ # form = MyForm.build({ name: 'Jane' }, id: 1)
232
+ #
233
+ # @return [Model::Form]
234
+ #
235
+ # @api public
236
+ def build(input = {}, options = {})
237
+ new(input, options.merge(command_registry))
238
+ end
239
+
240
+ private
241
+
242
+ # @return [Hash<Symbol=>ROM::CommandRegistry>]
243
+ #
244
+ # @api private
245
+ def command_registry
246
+ @command_registry ||= setup_command_registry
247
+ end
248
+
249
+ # Create attribute handler class
250
+ #
251
+ # @return [Class]
252
+ #
253
+ # @api private
254
+ def define_attributes!(block)
255
+ @input_block = block
256
+ @attributes = ClassBuilder.new(name: "#{name}::Attributes", parent: Object).call { |klass|
257
+ klass.send(:include, ROM::Model::Attributes)
258
+ }
259
+ @attributes.class_eval(&block)
260
+ const_set(:Attributes, @attributes)
261
+ end
262
+
263
+ # Define attribute readers for the form
264
+ #
265
+ # This is very unfortunate but rails `form_for` and friends require
266
+ # the object to provide attribute values, hence we need to expose those
267
+ # using the form object itself.
268
+ #
269
+ # @return [Class]
270
+ #
271
+ # @api private
272
+ def define_attribute_readers!
273
+ @attributes.attribute_set.each do |attribute|
274
+ if public_instance_methods.include?(attribute.name)
275
+ raise(
276
+ ArgumentError,
277
+ "#{attribute.name} attribute is in conflict with #{self}##{attribute.name}"
278
+ )
279
+ end
280
+
281
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
282
+ def #{attribute.name}
283
+ attributes[:#{attribute.name}]
284
+ end
285
+ RUBY
286
+ end
287
+ end
288
+
289
+ # Create model class
290
+ #
291
+ # Model instance represents an entity that will be persisted or was
292
+ # already persisted and will be updated.
293
+ #
294
+ # This object is returned via `Form#to_model` which rails uses internally
295
+ # in many places to figure out what to do.
296
+ #
297
+ # Model object provides two crucial pieces of information: whether or not
298
+ # something was persisted and its primary key value
299
+ #
300
+ # @return [Class]
301
+ #
302
+ # @api private
303
+ def define_model!
304
+ @model = ClassBuilder.new(name: "#{name}::Model", parent: @attributes).call { |klass|
305
+ klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
306
+ def persisted?
307
+ to_key.any?
308
+ end
309
+
310
+ def to_key
311
+ to_h.values_at(#{key.map(&:inspect).join(', ')}).compact
312
+ end
313
+ RUBY
314
+ }
315
+ key.each { |name| @model.attribute(name) }
316
+ const_set(:Model, @model)
317
+ end
318
+
319
+ # Define attribute validator class
320
+ #
321
+ # @return [Class]
322
+ #
323
+ # @api private
324
+ def define_validator!(block)
325
+ @validations_block = block
326
+ @validator = ClassBuilder.new(name: "#{name}::Validator", parent: Object).call { |klass|
327
+ klass.send(:include, ROM::Model::Validator)
328
+ }
329
+ @validator.class_eval(&block)
330
+ const_set(:Validator, @validator)
331
+ end
332
+
333
+ # Shortcut to global ROM env
334
+ #
335
+ # @return [ROM::Env]
336
+ #
337
+ # @api private
338
+ def rom
339
+ ROM.env
340
+ end
341
+
342
+ # Return identifier of the default adapter
343
+ #
344
+ # TODO: we need an interface for that in ROM
345
+ #
346
+ # @return [Symbol]
347
+ #
348
+ # @api private
349
+ def adapter
350
+ ROM.adapters.keys.first
351
+ end
352
+
353
+ # Generate a command registry hash which will be auto-injected to a form
354
+ # object.
355
+ #
356
+ # @return [Hash<Symbol=>ROM::CommandRegistry>]
357
+ #
358
+ # @api private
359
+ def setup_command_registry
360
+ commands = {}
361
+
362
+ if self_commands
363
+ self_commands.each do |rel_name, name|
364
+ command = build_command(name, rel_name)
365
+ commands[rel_name] = CommandRegistry.new(name => command)
366
+ end
367
+ end
368
+
369
+ if injectible_commands
370
+ injectible_commands.each do |relation|
371
+ commands[relation] = rom.command(relation)
372
+ end
373
+ end
374
+
375
+ commands
376
+ end
377
+
378
+ # Build a command object with a specific name
379
+ #
380
+ # @param [Symbol] name The name of the command
381
+ # @param [Symbol] rel_name The name of the command's relation
382
+ #
383
+ # @return [ROM::Command]
384
+ #
385
+ # @api private
386
+ def build_command(name, rel_name)
387
+ klass = Command.build_class(name, rel_name, adapter: adapter)
388
+
389
+ klass.result :one
390
+ klass.validator @validator
391
+
392
+ relation = rom.relations[rel_name]
393
+ repository = rom.repositories[relation.repository]
394
+ repository.extend_command_class(klass, relation.dataset)
395
+
396
+ klass.build(relation)
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end