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/test/virtual_test.rb CHANGED
@@ -2,8 +2,6 @@ require 'test_helper'
2
2
 
3
3
  class VirtualTest < MiniTest::Spec
4
4
  class CreditCardForm < Reform::Form
5
- reform_2_0!
6
-
7
5
  property :credit_card_number, virtual: true # no read, no write, it's virtual.
8
6
  end
9
7
 
@@ -12,7 +10,7 @@ class VirtualTest < MiniTest::Spec
12
10
  it {
13
11
  form.validate("credit_card_number" => "123")
14
12
 
15
- form.credit_card_number.must_equal "123"
13
+ form.credit_card_number.must_equal "123" # this is still readable in the UI.
16
14
 
17
15
  form.sync
18
16
 
@@ -1,19 +1,18 @@
1
1
  require 'test_helper'
2
2
 
3
- class WriteableTest < MiniTest::Spec # TODO: remove me in 2.0.
3
+ class WriteableTest < MiniTest::Spec
4
4
  Location = Struct.new(:country)
5
5
 
6
6
  class LocationForm < Reform::Form
7
- reform_2_0!
8
-
9
7
  property :country, writeable: false
10
8
  end
11
9
 
12
10
  let (:loc) { Location.new("Australia") }
13
11
  let (:form) { LocationForm.new(loc) }
14
12
 
15
- it { form.country.must_equal "Australia" }
16
13
  it do
14
+ form.country.must_equal "Australia"
15
+
17
16
  form.validate("country" => "Germany") # this usually won't change when submitting.
18
17
  form.country.must_equal "Germany"
19
18
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reform
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.6
4
+ version: 2.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Sutterer
@@ -9,36 +9,36 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-02-03 00:00:00.000000000 Z
12
+ date: 2015-06-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: representable
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 2.1.0
20
+ version: 2.2.2
21
21
  type: :runtime
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: 2.1.0
27
+ version: 2.2.2
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: disposable
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: 0.0.5
34
+ version: 0.1.2
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: 0.0.5
41
+ version: 0.1.2
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: uber
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -71,16 +71,16 @@ dependencies:
71
71
  name: bundler
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
- - - "~>"
74
+ - - ">="
75
75
  - !ruby/object:Gem::Version
76
- version: '1.3'
76
+ version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
- - - "~>"
81
+ - - ">="
82
82
  - !ruby/object:Gem::Version
83
- version: '1.3'
83
+ version: '0'
84
84
  - !ruby/object:Gem::Dependency
85
85
  name: rake
86
86
  requirement: !ruby/object:Gem::Requirement
@@ -99,16 +99,16 @@ dependencies:
99
99
  name: minitest
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
- - - '='
102
+ - - ">="
103
103
  - !ruby/object:Gem::Version
104
- version: 5.4.1
104
+ version: '0'
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - '='
109
+ - - ">="
110
110
  - !ruby/object:Gem::Version
111
- version: 5.4.1
111
+ version: '0'
112
112
  - !ruby/object:Gem::Dependency
113
113
  name: activerecord
114
114
  requirement: !ruby/object:Gem::Requirement
@@ -179,7 +179,7 @@ dependencies:
179
179
  - - ">="
180
180
  - !ruby/object:Gem::Version
181
181
  version: '0'
182
- description: Freeing your AR models from form logic.
182
+ description: Form object decoupled from models.
183
183
  email:
184
184
  - apotonick@gmail.com
185
185
  - heinleng@gmail.com
@@ -202,29 +202,26 @@ files:
202
202
  - gemfiles/Gemfile.rails-4.0
203
203
  - lib/reform.rb
204
204
  - lib/reform/active_record.rb
205
- - lib/reform/composition.rb
206
205
  - lib/reform/contract.rb
207
206
  - lib/reform/contract/errors.rb
208
- - lib/reform/contract/setup.rb
209
207
  - lib/reform/contract/validate.rb
210
208
  - lib/reform/form.rb
211
209
  - lib/reform/form/active_model.rb
212
210
  - lib/reform/form/active_model/model_validations.rb
213
211
  - lib/reform/form/active_record.rb
214
- - lib/reform/form/changed.rb
215
212
  - lib/reform/form/coercion.rb
216
213
  - lib/reform/form/composition.rb
217
214
  - lib/reform/form/json.rb
218
215
  - lib/reform/form/model_reflections.rb
219
216
  - lib/reform/form/module.rb
220
217
  - lib/reform/form/multi_parameter_attributes.rb
218
+ - lib/reform/form/populator.rb
221
219
  - lib/reform/form/prepopulate.rb
222
220
  - lib/reform/form/save.rb
223
221
  - lib/reform/form/scalar.rb
224
- - lib/reform/form/sync.rb
225
222
  - lib/reform/form/validate.rb
223
+ - lib/reform/form/validation/unique_validator.rb
226
224
  - lib/reform/rails.rb
227
- - lib/reform/representer.rb
228
225
  - lib/reform/schema.rb
229
226
  - lib/reform/twin.rb
230
227
  - lib/reform/version.rb
@@ -262,19 +259,18 @@ files:
262
259
  - test/dummy/db/test.sqlite3
263
260
  - test/dummy/log/production.log
264
261
  - test/dummy/log/server.log
265
- - test/empty_test.rb
266
262
  - test/errors_test.rb
267
263
  - test/feature_test.rb
268
264
  - test/fields_test.rb
269
265
  - test/form_builder_test.rb
270
- - test/form_composition_test.rb
266
+ - test/form_option_test.rb
271
267
  - test/form_test.rb
272
268
  - test/from_test.rb
273
269
  - test/inherit_test.rb
274
270
  - test/model_reflections_test.rb
275
271
  - test/model_validations_test.rb
276
- - test/nested_form_test.rb
277
- - test/prepopulate_test.rb
272
+ - test/populate_test.rb
273
+ - test/prepopulator_test.rb
278
274
  - test/rails/integration_test.rb
279
275
  - test/read_only_test.rb
280
276
  - test/readable_test.rb
@@ -283,12 +279,13 @@ files:
283
279
  - test/representer_test.rb
284
280
  - test/save_test.rb
285
281
  - test/scalar_test.rb
282
+ - test/setup_test.rb
286
283
  - test/skip_if_test.rb
284
+ - test/skip_setter_and_getter_test.rb
287
285
  - test/skip_unchanged_test.rb
288
- - test/sync_option_test.rb
289
- - test/sync_test.rb
290
286
  - test/test_helper.rb
291
287
  - test/twin_test.rb
288
+ - test/uniqueness_test.rb
292
289
  - test/validate_test.rb
293
290
  - test/virtual_test.rb
294
291
  - test/writeable_test.rb
@@ -307,16 +304,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
307
304
  version: '0'
308
305
  required_rubygems_version: !ruby/object:Gem::Requirement
309
306
  requirements:
310
- - - ">="
307
+ - - ">"
311
308
  - !ruby/object:Gem::Version
312
- version: '0'
309
+ version: 1.3.1
313
310
  requirements: []
314
311
  rubyforge_project:
315
- rubygems_version: 2.2.2
312
+ rubygems_version: 2.4.8
316
313
  signing_key:
317
314
  specification_version: 4
318
- summary: Decouples your models from form by giving you form objects with validation,
319
- presentation, workflows and security.
315
+ summary: Form object decoupled from models with validation, population and presentation.
320
316
  test_files:
321
317
  - test/active_model_test.rb
322
318
  - test/active_record_test.rb
@@ -351,19 +347,18 @@ test_files:
351
347
  - test/dummy/db/test.sqlite3
352
348
  - test/dummy/log/production.log
353
349
  - test/dummy/log/server.log
354
- - test/empty_test.rb
355
350
  - test/errors_test.rb
356
351
  - test/feature_test.rb
357
352
  - test/fields_test.rb
358
353
  - test/form_builder_test.rb
359
- - test/form_composition_test.rb
354
+ - test/form_option_test.rb
360
355
  - test/form_test.rb
361
356
  - test/from_test.rb
362
357
  - test/inherit_test.rb
363
358
  - test/model_reflections_test.rb
364
359
  - test/model_validations_test.rb
365
- - test/nested_form_test.rb
366
- - test/prepopulate_test.rb
360
+ - test/populate_test.rb
361
+ - test/prepopulator_test.rb
367
362
  - test/rails/integration_test.rb
368
363
  - test/read_only_test.rb
369
364
  - test/readable_test.rb
@@ -372,12 +367,13 @@ test_files:
372
367
  - test/representer_test.rb
373
368
  - test/save_test.rb
374
369
  - test/scalar_test.rb
370
+ - test/setup_test.rb
375
371
  - test/skip_if_test.rb
372
+ - test/skip_setter_and_getter_test.rb
376
373
  - test/skip_unchanged_test.rb
377
- - test/sync_option_test.rb
378
- - test/sync_test.rb
379
374
  - test/test_helper.rb
380
375
  - test/twin_test.rb
376
+ - test/uniqueness_test.rb
381
377
  - test/validate_test.rb
382
378
  - test/virtual_test.rb
383
379
  - test/writeable_test.rb
@@ -1,63 +0,0 @@
1
- require 'disposable/composition'
2
-
3
- # TODO: replace that with lazy Twin and Composition from Disposable.
4
- module Reform
5
- class Expose
6
- include Disposable::Composition
7
-
8
- # DISCUSS: this might be moved to Disposable::Twin::Expose.
9
- class << self
10
- # Builder for a concrete Composition class with configurations from the form's representer.
11
- def from(representer)
12
- options = {}
13
-
14
- representer.representable_attrs.each do |definition|
15
- process_definition!(options, definition)
16
- end
17
-
18
- Class.new(self).tap do |composition| # for 1.8 compat. you're welcome.
19
- composition.map(options)
20
- # puts composition@map.inspect
21
- end
22
- end
23
-
24
- private
25
- def process_definition!(options, definition)
26
- options[:model] ||= []
27
- options[:model] << [definition[:private_name], definition.name].compact
28
- end
29
- end
30
- end
31
-
32
- # Keeps composition of models and knows how to transform a plain hash into a nested hash.
33
- class Composition < Expose
34
-
35
- # DISCUSS: this might be moved to Disposable::Twin::Composition.
36
- class << self
37
- # Builder for a concrete Composition class with configurations from the form's representer.
38
- def process_definition!(options, definition)
39
- options[definition[:on]] ||= []
40
- options[definition[:on]] << [definition[:private_name], definition.name].compact
41
- end
42
- end
43
-
44
- def save
45
- each.collect { |model| model.save }.all?
46
- end
47
-
48
- def nested_hash_for(attrs)
49
- {}.tap do |hsh|
50
- attrs.each do |name, val|
51
- #obj = self.class.model_for_property(name)
52
- config = self.class.instance_variable_get(:@map)[name.to_sym]
53
-
54
- model = config[:model]
55
- method = config[:method]
56
-
57
- hsh[model] ||= {}
58
- hsh[model][method] = val
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,50 +0,0 @@
1
- module Reform
2
- class Contract
3
- module Setup
4
- def initialize(model)
5
- @model = model # we need this for #save.
6
- @fields = setup_fields # delegate all methods to Fields instance.
7
- end
8
-
9
- # Setup#to_hash will create a nested hash of property values from the model.
10
- # Nested properties will be recursively wrapped in a form instance.
11
- def setup_representer
12
- self.class.representer(:setup) do |dfn| # only nested forms.
13
- dfn.merge!(
14
- :representable => false, # don't call #to_hash, only prepare.
15
- :prepare => lambda { |model, args| args.binding[:form].new(model) } # wrap nested properties in form.
16
- )
17
- end
18
- end
19
-
20
- def setup_fields
21
- representer = setup_representer.new(aliased_model)
22
- options = setup_options(Reform::Representer::Options[]) # handles :empty.
23
-
24
- # populate the internal @fields set with data from the model.
25
- create_fields(mapper.fields, representer.to_hash(options))
26
- end
27
-
28
- def create_fields(field_names, fields)
29
- Fields.new(field_names, fields)
30
- end
31
-
32
- module SetupOptions
33
- def setup_options(options)
34
- options
35
- end
36
- end
37
- include SetupOptions
38
-
39
-
40
- module Readable
41
- def setup_options(options)
42
- empty_fields = mapper.representable_attrs.find_all { |d| d[:_readable] == false }.collect { |d| d.name.to_sym }
43
-
44
- options.exclude!(empty_fields)
45
- end
46
- end
47
- include Readable
48
- end
49
- end # Setup
50
- end
@@ -1,9 +0,0 @@
1
- module Reform::Form::Changed
2
- def changed?(name=nil)
3
- !! changed[name.to_s]
4
- end
5
-
6
- def changed
7
- @changed ||= {}
8
- end
9
- end
@@ -1,116 +0,0 @@
1
- # #sync!
2
- # 1. assign scalars to model (respecting virtual, excluded attributes)
3
- # 2. call sync! on nested
4
- module Reform::Form::Sync
5
- def sync_models(options={})
6
- sync!(options)
7
- end
8
- alias_method :sync, :sync_models
9
-
10
- # reading from fields allows using readers in form for presentation
11
- # and writers still pass to fields in #validate????
12
- def sync!(options) # semi-public.
13
- options = Reform::Representer::Options[options.merge(:form => self)] # options local for this form, only.
14
-
15
- input = sync_hash(options)
16
- # if aliased_model was a proper Twin, we could do changed? stuff there.
17
-
18
- options.delete(:exclude) # TODO: can we use 2 options?
19
-
20
- dynamic_sync_representer.new(aliased_model).from_hash(input, options) # sync properties to Song.
21
-
22
- model
23
- end
24
-
25
- private
26
-
27
- # Transforms form input into what actually gets written to model.
28
- # output: {title: "Mint Car", hit: <Form>}
29
- def input_representer
30
- self.class.representer(:input, :all => true) do |dfn|
31
- if dfn[:form]
32
- dfn.merge!(
33
- :representable => false,
34
- :prepare => lambda { |obj, *| obj },
35
- )
36
- else
37
- dfn.merge!(:render_nil => true) # do sync nil values back to the model for scalars.
38
- end
39
- end
40
- end
41
-
42
- # Writes input to model.
43
- def sync_representer
44
- self.class.representer(:sync, :all => true) do |dfn|
45
- if dfn[:form]
46
- dfn.merge!(
47
- :instance => lambda { |fragment, *| fragment }, # use model's nested property for syncing.
48
- # FIXME: do we allow options for #sync for nested forms?
49
- :deserialize => lambda { |object, *| model = object.sync!({}) } # sync! returns the synced model.
50
- # representable's :setter will do collection=([..]) or property=(..) for us on the model.
51
- )
52
- end
53
- end
54
- end
55
-
56
- # This representer inherits from sync_representer and add functionality on top of that.
57
- # It allows running custom dynamic blocks for properties when syncing.
58
- def dynamic_sync_representer
59
- self.class.representer(:dynamic_sync, superclass: sync_representer, :all => true) do |dfn|
60
- next unless setter = dfn[:sync]
61
-
62
- setter_proc = lambda do |value, options|
63
- if options.binding[:sync] == true # sync: true will call the runtime lambda from the options hash.
64
- options.user_options[options.binding.name.to_sym].call(value, options)
65
- next
66
- end
67
-
68
- # evaluate the :sync block in form context (should we do that everywhere?).
69
- options.user_options[:form].instance_exec(value, options, &setter)
70
- end
71
-
72
- dfn.merge!(:setter => setter_proc)
73
- end
74
- end
75
-
76
-
77
- # API: semi-public.
78
- module SyncHash
79
- # This hash goes into the Writer that writes properties back to the model. It only contains "writeable" attributes.
80
- def sync_hash(options)
81
- input_representer.new(fields).to_hash(options)
82
- end
83
- end
84
- include SyncHash
85
-
86
-
87
- # Excludes :virtual and readonly properties from #sync in this form.
88
- module Writeable
89
- def sync_hash(options)
90
- readonly_fields = mapper.fields { |dfn| dfn[:_writeable] == false }
91
-
92
- options.exclude!(readonly_fields.map(&:to_sym))
93
-
94
- super
95
- end
96
- end
97
- include Writeable
98
-
99
-
100
- # This will skip unchanged properties in #sync. To use this for all nested form do as follows.
101
- #
102
- # class SongForm < Reform::Form
103
- # feature Synd::SkipUnchanged
104
- module SkipUnchanged
105
- def sync_hash(options)
106
- # DISCUSS: we currently don't track if nested forms have changed (only their attributes). that's why i include them all here, which
107
- # is additional sync work/slightly wrong. solution: allow forms to form.changed? not sure how to do that with collections.
108
- scalars = mapper.fields { |dfn| !dfn[:form] }
109
- unchanged = scalars - changed.keys
110
-
111
- # exclude unchanged scalars, nested forms and changed scalars still go in here!
112
- options.exclude!(unchanged.map(&:to_sym))
113
- super
114
- end
115
- end
116
- end