reform 1.2.6 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +6 -1
  3. data/CHANGES.md +14 -0
  4. data/Gemfile +3 -2
  5. data/README.md +225 -283
  6. data/Rakefile +27 -0
  7. data/TODO.md +12 -0
  8. data/database.sqlite3 +0 -0
  9. data/gemfiles/Gemfile.rails-3.0 +1 -0
  10. data/gemfiles/Gemfile.rails-3.1 +1 -0
  11. data/gemfiles/Gemfile.rails-3.2 +1 -0
  12. data/gemfiles/Gemfile.rails-4.0 +1 -0
  13. data/lib/reform.rb +0 -1
  14. data/lib/reform/contract.rb +64 -170
  15. data/lib/reform/contract/validate.rb +10 -13
  16. data/lib/reform/form.rb +74 -19
  17. data/lib/reform/form/active_model.rb +19 -14
  18. data/lib/reform/form/coercion.rb +1 -13
  19. data/lib/reform/form/composition.rb +2 -24
  20. data/lib/reform/form/multi_parameter_attributes.rb +43 -62
  21. data/lib/reform/form/populator.rb +85 -0
  22. data/lib/reform/form/prepopulate.rb +13 -43
  23. data/lib/reform/form/validate.rb +29 -90
  24. data/lib/reform/form/validation/unique_validator.rb +13 -0
  25. data/lib/reform/version.rb +1 -1
  26. data/reform.gemspec +7 -7
  27. data/test/active_model_test.rb +43 -0
  28. data/test/changed_test.rb +23 -51
  29. data/test/coercion_test.rb +1 -7
  30. data/test/composition_test.rb +128 -34
  31. data/test/contract_test.rb +27 -86
  32. data/test/feature_test.rb +43 -6
  33. data/test/fields_test.rb +2 -12
  34. data/test/form_builder_test.rb +28 -25
  35. data/test/form_option_test.rb +19 -0
  36. data/test/from_test.rb +0 -75
  37. data/test/inherit_test.rb +178 -117
  38. data/test/model_reflections_test.rb +1 -1
  39. data/test/populate_test.rb +226 -0
  40. data/test/prepopulator_test.rb +112 -0
  41. data/test/readable_test.rb +2 -4
  42. data/test/save_test.rb +56 -112
  43. data/test/setup_test.rb +48 -0
  44. data/test/skip_if_test.rb +5 -2
  45. data/test/skip_setter_and_getter_test.rb +54 -0
  46. data/test/test_helper.rb +3 -1
  47. data/test/uniqueness_test.rb +41 -0
  48. data/test/validate_test.rb +325 -289
  49. data/test/virtual_test.rb +1 -3
  50. data/test/writeable_test.rb +3 -4
  51. metadata +35 -39
  52. data/lib/reform/composition.rb +0 -63
  53. data/lib/reform/contract/setup.rb +0 -50
  54. data/lib/reform/form/changed.rb +0 -9
  55. data/lib/reform/form/sync.rb +0 -116
  56. data/lib/reform/representer.rb +0 -84
  57. data/test/empty_test.rb +0 -58
  58. data/test/form_composition_test.rb +0 -145
  59. data/test/nested_form_test.rb +0 -197
  60. data/test/prepopulate_test.rb +0 -85
  61. data/test/sync_option_test.rb +0 -83
  62. data/test/sync_test.rb +0 -56
data/Rakefile CHANGED
@@ -5,6 +5,33 @@ task :default => [:test]
5
5
  Rake::TestTask.new(:test) do |test|
6
6
  test.libs << 'test'
7
7
  test.test_files = FileList['test/*_test.rb']
8
+
9
+ test.test_files = ["test/changed_test.rb",
10
+ "test/coercion_test.rb",
11
+ "test/feature_test.rb",
12
+
13
+ "test/contract_test.rb",
14
+
15
+ "test/populate_test.rb", "test/prepopulator_test.rb",
16
+
17
+ "test/readable_test.rb","test/setup_test.rb","test/skip_if_test.rb",
18
+
19
+ "test/validate_test.rb", "test/save_test.rb",
20
+
21
+ "test/writeable_test.rb","test/virtual_test.rb",
22
+
23
+ "test/form_builder_test.rb", "test/active_model_test.rb",
24
+
25
+ "test/readonly_test.rb",
26
+ "test/inherit_test.rb",
27
+ "test/uniqueness_test.rb",
28
+ "test/from_test.rb",
29
+ "test/composition_test.rb",
30
+ "test/form_option_test.rb"
31
+ ]
32
+
33
+
34
+
8
35
  test.verbose = true
9
36
  end
10
37
 
data/TODO.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 2.0
2
+
3
+ * make Coercible optional (include it to activate)
4
+ * all options Uber:::Value with :method support
5
+
6
+
7
+
8
+ # NOTES
9
+ * use the same test setup everywhere (album -> songs -> composer)
10
+ * copy things in tests
11
+ * one test file per "feature": sync_test, sync_option_test.
12
+
1
13
  * fields is a Twin and sorts out all the changed? stuff.
2
14
  * virtual: don't read dont write
3
15
  * empty dont read, but write
data/database.sqlite3 CHANGED
Binary file
@@ -5,3 +5,4 @@ gemspec :path => '../'
5
5
 
6
6
  gem 'railties', '~> 3.0.11'
7
7
  gem 'activerecord', '~> 3.0.11'
8
+ gem "disposable", github: "apotonick/disposable"
@@ -5,3 +5,4 @@ gemspec :path => '../'
5
5
 
6
6
  gem 'railties', '~> 3.1.0'
7
7
  gem 'activerecord', '~> 3.1.0'
8
+ gem "disposable", github: "apotonick/disposable"
@@ -5,3 +5,4 @@ gemspec :path => '../'
5
5
 
6
6
  gem 'railties', '~> 3.2.0'
7
7
  gem 'activerecord', '~> 3.2.0'
8
+ gem "disposable", github: "apotonick/disposable"
@@ -6,3 +6,4 @@ gemspec :path => '../'
6
6
  gem 'railties', '~> 4.0.0'
7
7
  gem 'activerecord', '~> 4.0.0'
8
8
  gem 'minitest', '~> 4.2'
9
+ gem "disposable", github: "apotonick/disposable"
data/lib/reform.rb CHANGED
@@ -9,7 +9,6 @@ require 'reform/form'
9
9
  require 'reform/form/composition'
10
10
  require 'reform/form/active_model'
11
11
  require 'reform/form/module'
12
- require 'reform/composition'
13
12
 
14
13
 
15
14
  if defined?(Rails) # DISCUSS: is everyone ok with this?
@@ -1,172 +1,106 @@
1
1
  require 'forwardable'
2
2
  require 'uber/inheritable_attr'
3
3
  require 'uber/delegates'
4
- require 'ostruct'
5
-
6
- require 'reform/representer'
7
4
 
8
5
  module Reform
9
6
  # Gives you a DSL for defining the object structure and its validations.
10
- class Contract # DISCUSS: make class?
11
- extend Uber::Delegates
12
-
13
- extend Uber::InheritableAttr
14
- # representer_class gets inherited (cloned) to subclasses.
15
- inheritable_attr :representer_class
16
- self.representer_class = Reform::Representer.for(:form_class => self) # only happens in Contract/Form.
17
- # this should be the only mechanism to inherit, features should be stored in this as well.
18
-
19
-
20
- # each contract keeps track of its features and passes them onto its local representer_class.
21
- # gets inherited, features get automatically included into inline representer.
22
- # TODO: the representer class should handle that, e.g. in options (deep-clone when inheriting.)
23
- inheritable_attr :features
24
- self.features = {}
25
-
26
-
27
- RESERVED_METHODS = [:model, :aliased_model, :fields, :mapper] # TODO: refactor that so we don't need that.
28
-
29
-
30
- module PropertyMethods
31
- def property(name, options={}, &block)
32
- deprecate_as!(options)
33
- options[:private_name] = options.delete(:from)
34
- options[:coercion_type] = options.delete(:type)
35
- options[:features] ||= []
36
- options[:features] += features.keys if block_given?
37
- options[:pass_options] = true
38
-
39
- # readable and writeable is true as it's not == false
7
+ require "disposable/twin"
8
+ require "disposable/twin/setup"
9
+ class Contract < Disposable::Twin
10
+ require "disposable/twin/composition" # Expose.
11
+ include Expose
40
12
 
41
- if reform_2_0
42
- if options.delete(:virtual)
43
- options[:_readable] = false
44
- options[:_writeable] = false
45
- else
46
- options[:_readable] = options.delete(:readable)
47
- options[:_writeable] = options.delete(:writeable)
48
- end
49
-
50
- else # TODO: remove me in 2.0.
51
- deprecate_virtual_and_empty!(options)
52
- end
53
-
54
- validates(name, options.delete(:validates).dup) if options[:validates]
55
-
56
- definition = representer_class.property(name, options, &block)
57
- setup_form_definition(definition) if block_given? or options[:form]
58
-
59
- create_accessor(name)
60
- definition
61
- end
62
-
63
- def properties(*args)
64
- options = args.extract_options!
65
-
66
- if args.first.is_a? Array # TODO: REMOVE in 2.0.
67
- warn "[Reform] Deprecation: Please pass a list of names instead of array to ::properties, like `properties :title, :id`."
68
- args = args.first
69
- end
70
- args.each { |name| property(name, options.dup) }
71
- end
13
+ feature Setup
14
+ feature Setup::SkipSetter
72
15
 
73
- def collection(name, options={}, &block)
74
- options[:collection] = true
16
+ extend Uber::Delegates
75
17
 
76
- property(name, options, &block)
18
+ representer_class.instance_eval do
19
+ def default_inline_class
20
+ Contract
77
21
  end
78
-
79
- def setup_form_definition(definition)
80
- options = {
81
- # TODO: make this a bit nicer. why do we need :form at all?
82
- :form => (definition.representer_module) || definition[:form], # :form is always just a Form class name.
83
- :pass_options => true, # new style of passing args
84
- :prepare => lambda { |form, args| form }, # always just return the form without decorating.
85
- :representable => true, # form: Class must be treated as a typed property.
86
- }
87
-
88
- definition.merge!(options)
22
+ end
23
+ # FIXME: THIS sucks because we're building two representers.
24
+ representer_class.instance_eval do
25
+ def default_inline_class
26
+ Contract
89
27
  end
28
+ end
90
29
 
91
- private
92
-
93
- def create_accessor(name)
94
- handle_reserved_names(name)
30
+ def self.property(name, options={}, &block)
31
+ options.merge!(pass_options: true)
95
32
 
96
- delegates :fields, name, "#{name}=" # Uber::Delegates
33
+ if twin = options.delete(:form)
34
+ options[:twin] = twin
97
35
  end
98
36
 
99
- def handle_reserved_names(name)
100
- raise "[Reform] the property name '#{name}' is reserved, please consider something else using :as." if RESERVED_METHODS.include?(name)
101
- end
37
+ super
102
38
  end
103
- extend PropertyMethods
104
39
 
40
+ # FIXME: test me.
41
+ def self.properties(*args)
42
+ options = args.extract_options!
43
+ args.each { |name| property(name, options.dup) }
44
+ end
105
45
 
106
46
  # FIXME: make AM optional.
107
47
  require 'active_model'
108
48
  include ActiveModel::Validations
109
49
 
110
-
111
-
112
- attr_accessor :model
113
-
114
- require 'reform/contract/setup'
115
- include Setup
116
-
117
- def self.representers # keeps all transformation representers for one class.
118
- @representers ||= {}
119
- end
120
-
121
- def self.representer(name=nil, options={}, &block)
122
- return representer_class.each(&block) if name == nil
123
- return representers[name] if representers[name] # don't run block as this representer is already setup for this form class.
124
-
125
- only_forms = options[:all] ? false : true
126
- base = options[:superclass] || representer_class
127
-
128
- representers[name] = Class.new(base).each(only_forms, &block) # let user modify representer.
129
- end
130
-
131
50
  require 'reform/contract/validate'
132
- include Validate
51
+ include Reform::Contract::Validate
133
52
 
134
53
  def errors # FIXME: this is needed for Rails 3.0 compatibility.
135
54
  @errors ||= Errors.new(self)
136
55
  end
137
56
 
138
-
139
57
  private
140
- attr_accessor :fields
141
58
  attr_writer :errors # only used in top form. (is that true?)
142
59
 
143
- def mapper # FIXME: do we need this with class-level representers?
144
- self.class.representer_class
60
+ # DISCUSS: can we achieve that somehow via features in build_inline?
61
+ # TODO: check out if that is needed with Lotus::Validations and make it a AM feature.
62
+ def self.process_inline!(mod, definition)
63
+ _name = definition.name
64
+ mod.instance_eval do
65
+ @_name = _name.singularize.camelize
66
+ def name # this adds Form::name for AM::Validations and I18N. i know it's retarded.
67
+ # something weird happens here: somewhere in Rails, this creates a constant (e.g. User). if this name doesn't represent a valid
68
+ # constant, the reloading in dev will fail with weird messages. i'm not sure if we should just get rid of Rails validations etc.
69
+ # or if i should look into this?
70
+ @_name
71
+ end
72
+ end
145
73
  end
146
74
 
147
- def self.deprecate_as!(options) # TODO: remove me in 2.0.
148
- return unless as = options.delete(:as)
149
- options[:from] = as
150
- warn "[Reform] The :as options got renamed to :from. See https://github.com/apotonick/reform/wiki/Migration-Guide and have a nice day."
151
- end
152
75
 
153
- def self.deprecate_virtual_and_empty!(options) # TODO: remove me in 2.0.
154
- if options.delete(:virtual)
155
- warn "[Reform] The :virtual option has changed! Check https://github.com/apotonick/reform/wiki/Migration-Guide and have a good day."
156
- options[:_readable] = true
157
- options[:_writeable] = false
76
+ # DISCUSS: separate file?
77
+ module Readonly
78
+ def readonly?(name)
79
+ options_for(name)[:writeable] == false
158
80
  end
159
-
160
- if options[:empty]
161
- warn "[Reform] The :empty option has changed! Check https://github.com/apotonick/reform/wiki/Migration-Guide and have a good day."
162
- options[:_readable] = false
163
- options[:_writeable] = false
81
+ def options_for(name)
82
+ self.class.options_for(name)
164
83
  end
165
84
  end
85
+ def self.options_for(name)
86
+ representer_class.representable_attrs.get(name)
87
+ end
88
+ include Readonly
89
+
166
90
 
167
- def self.register_feature(mod)
168
- features[mod] = true
91
+ def self.clone # TODO: test. THIS IS ONLY FOR Trailblazer when contract gets cloned in suboperation.
92
+ Class.new(self)
169
93
  end
94
+ end
95
+
96
+ class Contract_ # DISCUSS: make class?
97
+ extend Uber::InheritableAttr
98
+
99
+ RESERVED_METHODS = [:model] # TODO: refactor that so we don't need that.
100
+ def handle_reserved_names(name)
101
+ raise "[Reform] the property name '#{name}' is reserved, please consider something else using :as." if RESERVED_METHODS.include?(name)
102
+ end
103
+
170
104
 
171
105
  # allows including representers from Representable, Roar or disposable.
172
106
  def self.inherit_module!(representer) # called from Representable::included.
@@ -182,48 +116,8 @@ module Reform
182
116
  end
183
117
  end
184
118
 
185
- def self.clone
186
- Class.new(self)
187
- end
188
-
189
119
  require 'reform/schema'
190
120
  extend Schema
191
-
192
- alias_method :aliased_model, :model
193
-
194
- module Readonly
195
- def readonly?(name)
196
- options_for(name)[:writeable] == false
197
- end
198
-
199
- def options_for(name)
200
- self.class.representer_class.representable_attrs.get(name)
201
- end
202
- end
203
- include Readonly
204
-
205
- # TODO: remove me in 2.0.
206
- module Reform20Switch
207
- def self.included(base)
208
- base.register_feature(Reform20Switch)
209
- end
210
- end
211
- def self.reform_2_0!
212
- include Reform20Switch
213
- end
214
- def self.reform_2_0
215
- features[Reform20Switch]
216
- end
217
-
218
-
219
- # Keeps values of the form fields. What's in here is to be displayed in the browser!
220
- # we need this intermediate object to display both "original values" and new input from the form after submitting.
221
- class Fields < OpenStruct
222
- def initialize(properties, values={})
223
- fields = properties.inject({}) { |hsh, attr| hsh.merge!(attr => nil) }
224
- super(fields.merge!(values)) # TODO: stringify value keys!
225
- end
226
- end # Fields
227
121
  end
228
122
  end
229
123
 
@@ -12,8 +12,7 @@ module Reform::Contract::Validate
12
12
  def validate!(options)
13
13
  prefix = options[:prefix]
14
14
 
15
- # call valid? recursively and collect nested errors.
16
- valid_representer.new(fields).to_hash(options) # TODO: only include nested forms here.
15
+ validate_nested!(options) # call valid? recursively and collect nested errors.
17
16
 
18
17
  valid? # this validates on <Fields> using AM::Validations, currently.
19
18
 
@@ -23,17 +22,15 @@ module Reform::Contract::Validate
23
22
  private
24
23
 
25
24
  # runs form.validate! on all nested forms
26
- def valid_representer
27
- self.class.representer(:valid) do |dfn|
28
- dfn.merge!(
29
- :serialize => lambda { |form, args|
30
- options = args.user_options.dup
31
- options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
32
- options[:prefix] << args.binding.name # FIXME: should be #as.
33
-
34
- form.validate!(options) # recursively call valid? on nested form.
35
- }
36
- )
25
+ def validate_nested!(options)
26
+ schema.each(twin: true) do |dfn|
27
+ property_options = options.dup
28
+
29
+ property_options[:prefix] = options[:prefix].dup # TODO: implement Options#dup.
30
+ property_options[:prefix] << dfn.name
31
+
32
+ # recursively call valid? on nested form.
33
+ Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.validate!(property_options) }
37
34
  end
38
35
  end
39
36
  end
data/lib/reform/form.rb CHANGED
@@ -1,33 +1,88 @@
1
1
  module Reform
2
2
  class Form < Contract
3
- self.representer_class = Reform::Representer.for(:form_class => self)
3
+ representer_class.instance_eval do
4
+ def default_inline_class
5
+ Form
6
+ end
7
+ end
4
8
 
5
9
  require "reform/form/validate"
6
10
  include Validate # extend Contract#validate with additional behaviour.
7
- require "reform/form/sync"
8
- include Sync
9
- require "reform/form/save"
10
- include Save
11
- require "reform/form/prepopulate"
12
- include Prepopulate
13
11
 
14
- require "reform/form/multi_parameter_attributes"
15
- include MultiParameterAttributes # TODO: make features dynamic.
12
+ require "reform/form/populator"
13
+
14
+ module Property
15
+ # add macro logic, e.g. for :populator.
16
+ def property(name, options={}, &block)
17
+ if options.delete(:virtual)
18
+ options[:writeable] = options[:readable] = false # DISCUSS: isn't that like an #option in Twin?
19
+ end
20
+
21
+ if deserializer = options[:deserializer] # this means someone is explicitly specifying :deserializer.
22
+ options[:deserializer] = Representable::Cloneable::Hash[deserializer]
23
+ end
24
+
25
+ definition = super # let representable sort out inheriting of properties, and so on.
26
+ definition.merge!(deserializer: Representable::Cloneable::Hash.new) unless definition[:deserializer] # always keep :deserializer per property.
27
+
28
+
29
+ deserializer_options = definition[:deserializer]
30
+
31
+ # TODO: make this pluggable.
32
+ # DISCUSS: Populators should be a representable concept?
33
+
34
+ # Populators
35
+ # * they assign created data, no :setter (hence the name).
36
+ # * they (ab)use :instance, this is why they need to return a twin form.
37
+ # * they are only used in the deserializer.
38
+
16
39
 
17
- private
18
- def aliased_model
19
- # TODO: cache the Expose.from class!
20
- Reform::Expose.from(mapper).new(:model => model)
40
+
41
+ if populator = options.delete(:populate_if_empty)
42
+ deserializer_options.merge!({instance: Populator::IfEmpty.new(populator)})
43
+ deserializer_options.merge!({setter: nil})
44
+ elsif populator = options.delete(:populator)
45
+ deserializer_options.merge!({instance: Populator.new(populator)})
46
+ deserializer_options.merge!({setter: nil}) #if options[:collection] # collections don't need to get re-assigned, they don't change.
47
+ end
48
+
49
+
50
+ # TODO: shouldn't that go into validate?
51
+ if proc = options.delete(:skip_if)
52
+ proc = Reform::Form::Validate::Skip::AllBlank.new if proc == :all_blank
53
+
54
+ deserializer_options.merge!(skip_parse: proc)
55
+ end
56
+
57
+ # default:
58
+ # add Sync populator to nested forms.
59
+ # FIXME: this is, of course, ridiculous and needs a better structuring.
60
+ if (deserializer_options == {} || deserializer_options.keys == [:skip_parse]) && block_given? && !options[:inherit] # FIXME: hmm. not a fan of this: only add when no other option given?
61
+ deserializer_options.merge!(instance: Populator::Sync.new(nil), setter: nil)
62
+ end
63
+
64
+ # per default, everything should be writeable for the deserializer (we're only writing on the form). however, allow turning it off.
65
+ deserializer_options.merge!(writeable: true) unless deserializer_options.has_key?(:writeable)
66
+
67
+ definition
68
+ end
21
69
  end
70
+ extend Property
71
+
72
+
73
+ require "reform/form/multi_parameter_attributes"
22
74
 
75
+ require "disposable/twin/changed"
76
+ feature Disposable::Twin::Changed
23
77
 
24
- require "reform/form/scalar"
25
- extend Scalar::Property # experimental feature!
78
+ require "disposable/twin/sync"
79
+ feature Disposable::Twin::Sync
80
+ feature Disposable::Twin::Sync::SkipGetter
26
81
 
82
+ require "disposable/twin/save"
83
+ feature Disposable::Twin::Save
27
84
 
28
- # DISCUSS: should that be optional? hooks into #validate, too.
29
- require "reform/form/changed"
30
- register_feature Changed
31
- include Changed
85
+ require "reform/form/prepopulate"
86
+ include Prepopulate
32
87
  end
33
88
  end