reform 0.2.7 → 1.0.0

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.
@@ -0,0 +1,109 @@
1
+ require 'forwardable'
2
+ require 'uber/inheritable_attr'
3
+
4
+ require 'reform/representer'
5
+
6
+ module Reform
7
+ # Gives you a DSL for defining the object structure and its validations.
8
+ class Contract # DISCUSS: make class?
9
+ extend Forwardable
10
+
11
+ extend Uber::InheritableAttr
12
+ inheritable_attr :representer_class
13
+ self.representer_class = Reform::Representer.for(:form_class => self)
14
+
15
+ inheritable_attr :features
16
+ self.features = []
17
+
18
+
19
+ module PropertyMethods
20
+ extend Forwardable
21
+
22
+ def property(name, options={}, &block)
23
+ options[:private_name] = options.delete(:as)
24
+
25
+ # at this point, :extend is a Form class.
26
+ options[:features] = features if block_given?
27
+ definition = representer_class.property(name, options, &block)
28
+ setup_form_definition(definition) if block_given? or options[:form]
29
+
30
+ create_accessor(name)
31
+ definition
32
+ end
33
+
34
+ def collection(name, options={}, &block)
35
+ options[:collection] = true
36
+
37
+ property(name, options, &block)
38
+ end
39
+
40
+ def properties(names, *args)
41
+ names.each { |name| property(name, *args) }
42
+ end
43
+
44
+ def setup_form_definition(definition)
45
+ options = {
46
+ :form => definition[:form] || definition[:extend].evaluate(nil), # :form is always just a Form class name.
47
+ :pass_options => true, # new style of passing args
48
+ :prepare => lambda { |form, args| form }, # always just return the form without decorating.
49
+ :representable => true, # form: Class must be treated as a typed property.
50
+ }
51
+
52
+ definition.merge!(options)
53
+ end
54
+
55
+ private
56
+ def create_accessor(name)
57
+ # Make a module that contains these very accessors, then include it
58
+ # so they can be overridden but still are callable with super.
59
+ accessors = Module.new do
60
+ extend Forwardable # DISCUSS: do we really need Forwardable here?
61
+ delegate [name, "#{name}="] => :fields
62
+ end
63
+ include accessors
64
+ end
65
+ end
66
+ extend PropertyMethods
67
+
68
+
69
+ # FIXME: make AM optional.
70
+ require 'active_model'
71
+ include ActiveModel::Validations
72
+
73
+
74
+ attr_accessor :model
75
+
76
+ require 'reform/contract/setup'
77
+ include Setup
78
+ require 'reform/contract/validate'
79
+ include Validate
80
+
81
+
82
+ def errors # FIXME: this is needed for Rails 3.0 compatibility.
83
+ @errors ||= Errors.new(self)
84
+ end
85
+
86
+
87
+ private
88
+ attr_accessor :fields
89
+ attr_writer :errors # only used in top form. (is that true?)
90
+
91
+ def mapper
92
+ self.class.representer_class
93
+ end
94
+
95
+ alias_method :aliased_model, :model
96
+
97
+
98
+ # Keeps values of the form fields. What's in here is to be displayed in the browser!
99
+ # we need this intermediate object to display both "original values" and new input from the form after submitting.
100
+ class Fields < OpenStruct
101
+ def initialize(properties, values={})
102
+ fields = properties.inject({}) { |hsh, attr| hsh.merge!(attr => nil) }
103
+ super(fields.merge!(values)) # TODO: stringify value keys!
104
+ end
105
+ end # Fields
106
+ end
107
+ end
108
+
109
+ require 'reform/contract/errors'
@@ -0,0 +1,33 @@
1
+ # The Errors class is planned to replace AM::Errors. It provides proper nested error messages.
2
+ class Reform::Contract::Errors < ActiveModel::Errors
3
+ def messages
4
+ return super unless Reform.rails3_0?
5
+ self
6
+ end
7
+
8
+ # def each
9
+ # messages.each_key do |attribute|
10
+ # self[attribute].each { |error| yield attribute, Array.wrap(error) }
11
+ # end
12
+ # end
13
+
14
+ def merge!(errors, prefix)
15
+ prefixes = prefix.join(".")
16
+
17
+ # TODO: merge into AM.
18
+ errors.messages.each do |field, msgs|
19
+ field = (prefix+[field]).join(".").to_sym # TODO: why is that a symbol in Rails?
20
+
21
+ msgs = [msgs] if Reform.rails3_0? # DISCUSS: fix in #each?
22
+
23
+ msgs.each do |msg|
24
+ next if messages[field] and messages[field].include?(msg)
25
+ add(field, msg)
26
+ end # Forms now contains a plain errors hash. the errors for each item are still available in item.errors.
27
+ end
28
+ end
29
+
30
+ def valid? # TODO: test me in unit test.
31
+ blank?
32
+ end
33
+ end # Errors
@@ -0,0 +1,44 @@
1
+
2
+ module Reform
3
+ class Contract
4
+ module Setup
5
+ def initialize(model)
6
+ @model = model # we need this for #save.
7
+ @fields = setup_fields # delegate all methods to Fields instance.
8
+ end
9
+
10
+ def setup_fields
11
+ representer = mapper.new(aliased_model).extend(Setup::Representer)
12
+
13
+ create_fields(representer.fields, representer.to_hash)
14
+ end
15
+
16
+ # DISCUSS: setting up the Validation (populating with values) will soon be handled with Disposable::Twin logic.
17
+ def create_fields(field_names, fields)
18
+ Fields.new(field_names, fields)
19
+ end
20
+
21
+
22
+ # Mechanics for setting up initial Field values.
23
+ module Representer
24
+ require 'reform/form/virtual_attributes' # FIXME: that shouldn't be here.
25
+
26
+ include Reform::Representer::WithOptions
27
+ include Reform::Form::EmptyAttributesOptions # FIXME: that shouldn't be here.
28
+
29
+ def to_hash(*)
30
+ nested_forms do |attr|
31
+ attr.merge!(
32
+ :representable => false, # don't call #to_hash.
33
+ :prepare => lambda do |model, args|
34
+ args.binding[:form].new(model)
35
+ end
36
+ )
37
+ end
38
+
39
+ super # TODO: allow something like super(:exclude => empty_fields)
40
+ end
41
+ end # Representer
42
+ end
43
+ end # Validation
44
+ end
@@ -0,0 +1,48 @@
1
+ module Reform::Contract::Validate
2
+ module NestedValid
3
+ def to_hash(*)
4
+ nested_forms do |attr|
5
+ # attr.delete(:prepare)
6
+ # attr.delete(:extend)
7
+
8
+ attr.merge!(
9
+ :serialize => lambda { |object, args|
10
+
11
+ # FIXME: merge with Validate::Writer
12
+ options = args.user_options.dup
13
+ options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
14
+ options[:prefix] << args.binding.name # FIXME: should be #as.
15
+
16
+ # puts "======= user_options: #{args.user_options.inspect}"
17
+
18
+ object.validate!(options) # recursively call valid?
19
+ },
20
+ )
21
+ end
22
+
23
+ super
24
+ end
25
+ end
26
+
27
+ def validate
28
+ options = {:errors => errs = Reform::Contract::Errors.new(self), :prefix => []}
29
+
30
+ validate!(options)
31
+
32
+ self.errors = errs # if the AM valid? API wouldn't use a "global" variable this would be better.
33
+
34
+ errors.valid?
35
+ end
36
+ def validate!(options)
37
+ # puts "validate! in #{self.class.name}: #{true.inspect}"
38
+ prefix = options[:prefix]
39
+
40
+ # call valid? recursively and collect nested errors.
41
+ mapper.new(self).extend(NestedValid).to_hash(options)
42
+
43
+ valid? # this validates on <Fields> using AM::Validations, currently.
44
+
45
+ options[:errors].merge!(self.errors, prefix)
46
+ end
47
+
48
+ end
@@ -1,323 +1,27 @@
1
- require 'forwardable'
2
1
  require 'ostruct'
3
2
 
3
+ require 'reform/contract'
4
4
  require 'reform/composition'
5
- require 'reform/representer'
6
-
7
- require 'uber/inheritable_attr'
8
-
9
5
 
10
6
  module Reform
11
- class Form
12
- extend Forwardable
13
-
14
- extend Uber::InheritableAttr
15
- inheritable_attr :representer_class
16
- self.representer_class = Class.new(Reform::Representer)
17
-
18
-
19
- module PropertyMethods
20
- extend Forwardable
21
-
22
- def property(name, options={}, &block)
23
- process_options(name, options, &block)
24
-
25
- definition = representer_class.property(name, options, &block)
26
- setup_form_definition(definition) if block_given? or options[:form]
27
-
28
- create_accessor(name)
29
- end
30
-
31
- def collection(name, options={}, &block)
32
- options[:form_collection] = true
33
-
34
- property(name, options, &block)
35
- end
36
-
37
- def properties(names, *args)
38
- names.each { |name| property(name, *args) }
39
- end
40
-
41
- def setup_form_definition(definition)
42
- # TODO: allow Definition.form?
43
- definition.options[:form] ||= definition.options.delete(:extend)
7
+ class Form < Contract
8
+ self.representer_class = Reform::Representer.for(:form_class => self)
44
9
 
45
- definition.options[:parse_strategy] = :sync
46
- definition.options[:instance] = true # just to make typed? work
47
- end
48
-
49
- private
50
- def create_accessor(name)
51
- # Make a module that contains these very accessors, then include it
52
- # so they can be overridden but still are callable with super.
53
- accessors = Module.new do
54
- extend Forwardable # DISCUSS: do we really need Forwardable here?
55
- delegate [name, "#{name}="] => :fields
56
- end
57
- include accessors
58
- end
59
-
60
- def process_options(name, options) # DISCUSS: do we need that hook?
61
- end
62
- end
63
- extend PropertyMethods
64
-
65
-
66
- def initialize(model)
67
- @model = model # we need this for #save.
68
- @fields = setup_fields(model) # delegate all methods to Fields instance.
10
+ def aliased_model
11
+ # TODO: cache the Expose.from class!
12
+ Reform::Expose.from(mapper).new(:model => model)
69
13
  end
70
14
 
71
- module ValidateMethods # TODO: introduce Base module.
72
- def validate(params)
73
- # here it would be cool to have a validator object containing the validation rules representer-like and then pass it the formed model.
74
- from_hash(params)
75
-
76
- res = valid? # this validates on <Fields> using AM::Validations, currently.
77
- #inject(true) do |res, form| # FIXME: replace that!
78
- mapper.new(@fields).nested_forms do |attr, form| #.collect { |attr, form| nested[attr.from] = form }
79
- next unless form # FIXME: this happens when the model's reader returns nil (property :song => nil). this shouldn't be considered by nested_forms!
80
- res = validate_for(form, res, attr.from)
81
- end
82
-
83
- res
84
- end
15
+ require "reform/form/virtual_attributes"
85
16
 
86
- private
87
- def validate_for(form, res, prefix=nil)
88
- return res if form.valid? # FIXME: we have to call validate here, otherwise this works only one level deep.
17
+ require 'reform/form/validate'
18
+ include Validate # extend Contract#validate with additional behaviour.
19
+ require 'reform/form/sync'
20
+ include Sync
21
+ require 'reform/form/save'
22
+ include Save
89
23
 
90
- errors.merge!(form.errors, prefix)
91
- false
92
- end
93
- end
94
- include ValidateMethods
95
24
  require 'reform/form/multi_parameter_attributes'
96
25
  include MultiParameterAttributes # TODO: make features dynamic.
97
-
98
- def save
99
- # DISCUSS: we should never hit @mapper here (which writes to the models) when a block is passed.
100
- return yield self, to_nested_hash if block_given?
101
-
102
- save_to_models
103
- end
104
-
105
- # Use representer to return current key-value form hash.
106
- def to_hash(*args)
107
- mapper.new(self).to_hash(*args)
108
- end
109
-
110
- require "active_support/hash_with_indifferent_access" # DISCUSS: replace?
111
- def to_nested_hash
112
- map = mapper.new(self)
113
-
114
- ActiveSupport::HashWithIndifferentAccess.new(map.to_hash)
115
- end
116
-
117
- def from_hash(params, *args)
118
- mapper.new(self).from_hash(params) # sets form properties found in params on self.
119
- end
120
-
121
- def errors
122
- @errors ||= Errors.new(self)
123
- @errors
124
- end
125
-
126
- attr_accessor :model
127
-
128
- private
129
- attr_accessor :fields
130
-
131
- def mapper
132
- self.class.representer_class
133
- end
134
-
135
- def setup_fields(model)
136
- representer = mapper.new(model).extend(Setup::Representer)
137
-
138
- create_fields(representer.fields, representer.to_hash)
139
- end
140
-
141
- #representer.to_hash override: { write: lambda { |doc, value| } }
142
-
143
- # DISCUSS: this would be cool in representable:
144
- # to_hash(hit: lambda { |value| form_class.new(..) })
145
-
146
- # steps:
147
- # - bin.get
148
- # - map that: Forms.new( orig ) <-- override only this in representable (how?)
149
- # - mapped.to_hash
150
-
151
-
152
- def create_fields(field_names, fields)
153
- Fields.new(field_names, fields)
154
- end
155
-
156
-
157
- require "reform/form/virtual_attributes"
158
-
159
- # Mechanics for setting up initial Field values.
160
- module Setup
161
- module Representer
162
- include Reform::Representer::WithOptions
163
- include EmptyAttributesOptions
164
-
165
- def to_hash(*)
166
- setup_nested_forms
167
-
168
- super # TODO: allow something like super(:exclude => empty_fields)
169
- end
170
-
171
- private
172
- def setup_nested_forms
173
- nested_forms do |attr, model|
174
- form_class = attr.options[:form]
175
-
176
- attr.options.merge!(
177
- :getter => lambda do |*|
178
- # FIXME: this is where i have to fix stuff.
179
- nested_model = send(attr.getter) # or next # decorated.hit # TODO: use bin.get # DISCUSS: next moves on if property empty. this should be handled with representable's built-in mechanics.
180
-
181
- if attr.options[:form_collection]
182
- nested_model ||= []
183
- Forms.new(nested_model.collect { |mdl| form_class.new(mdl)}, attr.options)
184
- else
185
- next unless nested_model # DISCUSS: do we want that?
186
- form_class.new(nested_model)
187
- end
188
- end,
189
- :instance => false, # that's how we make it non-typed?.
190
- )
191
- end
192
- end
193
- end
194
- end
195
-
196
- # Mechanics for writing input to model.
197
- module Sync
198
- # Writes input to model.
199
- module Representer
200
- def from_hash(*)
201
- nested_forms do |attr, model|
202
- attr.options.merge!(
203
- :decorator => attr.options[:form].representer_class
204
- )
205
-
206
- if attr.options[:form_collection]
207
- attr.options.merge!(
208
- :collection => true
209
- )
210
- end
211
- end
212
-
213
- super
214
- end
215
- end
216
-
217
- # Transforms form input into what actually gets written to model.
218
- module InputRepresenter
219
- include Reform::Representer::WithOptions
220
- # TODO: make dynamic.
221
- include EmptyAttributesOptions
222
- include ReadonlyAttributesOptions
223
- end
224
- end
225
-
226
-
227
- def save_to_models # TODO: rename to #sync_models
228
- representer = mapper.new(model)
229
-
230
- representer.extend(Sync::Representer)
231
-
232
- input_representer = mapper.new(self).extend(Sync::InputRepresenter)
233
-
234
- representer.from_hash(input_representer.to_hash)
235
- end
236
-
237
- # FIXME: make AM optional.
238
- require 'active_model'
239
- include ActiveModel::Validations
240
-
241
- # The Errors class is planned to replace AM::Errors. It provides proper nested error messages.
242
- class Errors < ActiveModel::Errors
243
- def messages
244
- return super unless Reform.rails3_0?
245
- self
246
- end
247
-
248
- # def each
249
- # messages.each_key do |attribute|
250
- # self[attribute].each { |error| yield attribute, Array.wrap(error) }
251
- # end
252
- # end
253
-
254
- def merge!(errors, prefix=nil)
255
- # TODO: merge into AM.
256
- errors.messages.each do |field, msgs|
257
- field = "#{prefix}.#{field}" if prefix
258
-
259
- msgs = [msgs] if Reform.rails3_0? # DISCUSS: fix in #each?
260
-
261
- msgs.each do |msg|
262
- next if messages[field] and messages[field].include?(msg)
263
- add(field, msg)
264
- end # Forms now contains a plain errors hash. the errors for each item are still available in item.errors.
265
- end
266
- end
267
- end
268
-
269
- require "representable/hash/collection"
270
- require 'active_model'
271
- class Forms < Array # DISCUSS: this should be a Form subclass.
272
- def initialize(ary, options)
273
- super(ary)
274
- @options = options
275
- end
276
-
277
- include Form::ValidateMethods
278
-
279
- # TODO: make valid?(errors) the only public method.
280
- def valid?
281
- res= validate_cardinality & validate_items
282
- end
283
-
284
- def errors
285
- @errors ||= Form::Errors.new(self)
286
- end
287
-
288
- # this gives us each { to_hash }
289
- include Representable::Hash::Collection
290
- items :parse_strategy => :sync, :instance => true
291
-
292
- private
293
- def validate_items
294
- inject(true) do |res, form|
295
- res = validate_for(form, res)
296
- end
297
- end
298
-
299
- def validate_cardinality
300
- return true unless @options[:cardinality]
301
- # TODO: use AM's cardinality validator here.
302
- res = size >= @options[:cardinality][:minimum].to_i
303
-
304
- errors.add(:size, "#{@options[:as]} size is 0 but must be #{@options[:cardinality].inspect}") unless res
305
- res
306
- end
307
- end
308
- end
309
-
310
-
311
- # Keeps values of the form fields. What's in here is to be displayed in the browser!
312
- # we need this intermediate object to display both "original values" and new input from the form after submitting.
313
- class Fields < OpenStruct
314
- def initialize(properties, values={})
315
- fields = properties.inject({}) { |hsh, attr| hsh.merge!(attr => nil) }
316
- super(fields.merge!(values)) # TODO: stringify value keys!
317
- end
318
- end
319
-
320
- def self.rails3_0?
321
- ::ActiveModel::VERSION::MAJOR == 3 and ::ActiveModel::VERSION::MINOR == 0
322
26
  end
323
27
  end