rom-rails 0.3.0.beta1 → 0.3.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: 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