reform 0.2.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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