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