reform 1.2.6 → 2.0.0.beta1

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.
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