reform 1.1.1 → 1.2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +35 -1
  3. data/Gemfile +1 -1
  4. data/README.md +83 -21
  5. data/TODO.md +8 -0
  6. data/database.sqlite3 +0 -0
  7. data/gemfiles/Gemfile.rails-4.0 +1 -0
  8. data/lib/reform.rb +4 -2
  9. data/lib/reform/active_record.rb +2 -1
  10. data/lib/reform/composition.rb +2 -2
  11. data/lib/reform/contract.rb +24 -7
  12. data/lib/reform/contract/setup.rb +21 -9
  13. data/lib/reform/contract/validate.rb +0 -6
  14. data/lib/reform/form.rb +6 -8
  15. data/lib/reform/form/active_model.rb +3 -2
  16. data/lib/reform/form/active_model/model_validations.rb +13 -1
  17. data/lib/reform/form/active_record.rb +1 -7
  18. data/lib/reform/form/changed.rb +9 -0
  19. data/lib/reform/form/json.rb +13 -0
  20. data/lib/reform/form/model_reflections.rb +18 -0
  21. data/lib/reform/form/save.rb +25 -3
  22. data/lib/reform/form/scalar.rb +4 -2
  23. data/lib/reform/form/sync.rb +82 -12
  24. data/lib/reform/form/validate.rb +38 -0
  25. data/lib/reform/rails.rb +1 -1
  26. data/lib/reform/representer.rb +14 -23
  27. data/lib/reform/schema.rb +23 -0
  28. data/lib/reform/twin.rb +20 -0
  29. data/lib/reform/version.rb +1 -1
  30. data/reform.gemspec +2 -2
  31. data/test/active_model_test.rb +2 -2
  32. data/test/active_record_test.rb +7 -4
  33. data/test/changed_test.rb +69 -0
  34. data/test/custom_validation_test.rb +47 -0
  35. data/test/deserialize_test.rb +2 -7
  36. data/test/empty_test.rb +30 -0
  37. data/test/fields_test.rb +24 -0
  38. data/test/form_composition_test.rb +24 -2
  39. data/test/form_test.rb +84 -0
  40. data/test/inherit_test.rb +12 -0
  41. data/test/model_reflections_test.rb +65 -0
  42. data/test/read_only_test.rb +28 -0
  43. data/test/reform_test.rb +2 -175
  44. data/test/representer_test.rb +47 -0
  45. data/test/save_test.rb +51 -1
  46. data/test/scalar_test.rb +0 -18
  47. data/test/skip_if_test.rb +62 -0
  48. data/test/skip_unchanged_test.rb +86 -0
  49. data/test/sync_option_test.rb +83 -0
  50. data/test/twin_test.rb +23 -0
  51. data/test/validate_test.rb +9 -1
  52. metadata +37 -9
  53. data/lib/reform/form/virtual_attributes.rb +0 -22
@@ -0,0 +1,23 @@
1
+ module Reform
2
+ module Schema
3
+ # Converts the Representer->Form->Representer->Form tree into Representer->Representer.
4
+ # It becomes obvious that the form will be the main schema-defining instance in Trb, so this
5
+ # method makes sense. Consider private. This is experimental.
6
+ class Converter
7
+ def self.from(representer_class) # TODO: can we re-use this for all the decorator logic in #validate, etc?
8
+ representer = Class.new(representer_class)
9
+ representer.representable_attrs.each do |dfn|
10
+ next unless form = dfn[:form]
11
+ dfn.merge!(:extend => from(form.representer_class))
12
+ end
13
+
14
+ representer
15
+ end
16
+ end
17
+
18
+ # It's your job to make sure you memoize it correctly.
19
+ def schema
20
+ Converter.from(representer_class)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require 'disposable/twin'
2
+
3
+ module Reform
4
+ module Twin
5
+ def self.included(base)
6
+ base.send :include, Disposable::Twin::Builder
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def twin(twin_class)
12
+ super(twin_class) { |dfn| property dfn.name } # create readers to twin model.
13
+ end
14
+ end
15
+
16
+ def initialize(model, options={})
17
+ super(build_twin(model, options))
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "1.1.1"
2
+ VERSION = "1.2.0.beta1"
3
3
  end
data/reform.gemspec CHANGED
@@ -18,13 +18,13 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_dependency "representable", "~> 2.0.3"
21
+ spec.add_dependency "representable", "~> 2.1.0"
22
22
  spec.add_dependency "disposable", "~> 0.0.5"
23
23
  spec.add_dependency "uber", "~> 0.0.8"
24
24
  spec.add_dependency "activemodel"
25
25
  spec.add_development_dependency "bundler", "~> 1.3"
26
26
  spec.add_development_dependency "rake"
27
- spec.add_development_dependency "minitest", "4.2.0"
27
+ spec.add_development_dependency "minitest", "5.4.1"
28
28
  spec.add_development_dependency "activerecord"
29
29
  spec.add_development_dependency "sqlite3"
30
30
  spec.add_development_dependency "virtus"
@@ -100,8 +100,8 @@ class ActiveModelWithCompositionTest < MiniTest::Spec
100
100
  include Composition
101
101
  include Reform::Form::ActiveModel
102
102
 
103
- property :title, :on => :song
104
- properties [:name, :genre], :on => :artist # we need to check both ::property and ::properties here!
103
+ property :title, :on => :song
104
+ properties :name, :genre, :on => :artist # we need to check both ::property and ::properties here!
105
105
 
106
106
  model :hit, :on => :song
107
107
  end
@@ -46,13 +46,16 @@ class ActiveRecordTest < MiniTest::Spec
46
46
  end
47
47
 
48
48
  # uniqueness
49
- it "is valid when title is unique for the same artist and album" do
50
- form.validate("title" => "The Gargoyle", "artist_id" => artist.id, "album" => album.id, "created_at" => "November 6, 1966").must_equal true
49
+ it "has no errors on title when title is unique for the same artist and album" do
50
+ form.validate("title" => "The Gargoyle", "artist_id" => artist.id, "album" => album.id, "created_at" => "November 6, 1966")
51
+ assert_empty form.errors[:title]
51
52
  end
52
53
 
53
- it "is invalid when title is taken for the same artist and album" do
54
+ it "has errors on title when title is taken for the same artist and album" do
55
+ skip "replace ActiveModel::Validations with our own, working and reusable gem."
54
56
  Song.create(title: "Windowpane", artist_id: artist.id, album_id: album.id)
55
- form.validate("title" => "Windowpane", "artist_id" => artist.id, "album" => album).must_equal false
57
+ form.validate("title" => "Windowpane", "artist_id" => artist.id, "album" => album)
58
+ refute_empty form.errors[:title]
56
59
  end
57
60
 
58
61
  # nested object taken.
@@ -0,0 +1,69 @@
1
+ require 'test_helper'
2
+ require 'reform/form/coercion'
3
+
4
+ class ChangedTest < BaseTest
5
+ class AlbumForm < Reform::Form
6
+ include Coercion
7
+
8
+ property :title
9
+
10
+ property :hit do
11
+ property :title
12
+ property :length, type: Integer
13
+ validates :title, :presence => true
14
+ end
15
+
16
+ collection :songs do
17
+ property :title
18
+ validates :title, :presence => true
19
+ end
20
+
21
+ property :band do # yepp, people do crazy stuff like that.
22
+ property :label do
23
+ property :name
24
+ property :location
25
+ validates :name, :presence => true
26
+ end
27
+ # TODO: make band a required object.
28
+ end
29
+
30
+ validates :title, :presence => true
31
+ end
32
+
33
+ Label = Struct.new(:name, :location)
34
+
35
+ # setup: changed? is always false
36
+ let (:form) { AlbumForm.new(Album.new("Drawn Down The Moon", Song.new("The Ripper", 9), [Song.new("Black Candles"), Song.new("The Ripper")], Band.new(Label.new("Cleopatra Records")))) }
37
+
38
+ it { form.changed?(:title).must_equal false }
39
+ it { form.changed?("title").must_equal false }
40
+ it { form.hit.changed?(:title).must_equal false }
41
+ it { form.hit.changed?.must_equal false }
42
+
43
+
44
+ describe "#validate" do
45
+ before { form.validate(
46
+ "title" => "Five", # changed.
47
+ "hit" => {"title" => "The Ripper", # same, but overridden.
48
+ "length" => "9"}, # gets coerced, then compared, so not changed.
49
+ "band" => {"label" => {"name" => "Shrapnel Records"}} # only label.name changes.
50
+ ) }
51
+
52
+ it { form.changed?(:title).must_equal true }
53
+
54
+ # it { form.changed?(:hit).must_equal false }
55
+
56
+ # overridden with same value is no change.
57
+ it { form.hit.changed?(:title).must_equal false }
58
+ # coerced value is identical to form's => not changed.
59
+ it { form.hit.changed?(:length).must_equal false }
60
+
61
+ # it { form.changed?(:band).must_equal true }
62
+ # it { form.band.changed?(:label).must_equal true }
63
+ it { form.band.label.changed?(:name).must_equal true }
64
+
65
+ # not present key/value in #validate is no change.
66
+ it { form.band.label.changed?(:location).must_equal false }
67
+ # TODO: parent form changed when child has changed!
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ require 'test_helper'
2
+
3
+ unless ActiveModel::VERSION::MAJOR == 3 and ActiveModel::VERSION::MINOR == 0
4
+
5
+ class UnexistantTitleValidator < ActiveModel::Validator
6
+ def validate record
7
+ if record.title == 'unexistant_song'
8
+ record.errors.add(:title, 'this title does not exist!')
9
+ end
10
+ end
11
+ end
12
+
13
+ class CustomValidationTest < MiniTest::Spec
14
+
15
+ class Album
16
+ include ActiveModel::Validations
17
+ attr_accessor :title, :artist
18
+
19
+ validates_with UnexistantTitleValidator
20
+ end
21
+
22
+ class AlbumForm < Reform::Form
23
+ extend ActiveModel::ModelValidations
24
+
25
+ property :title
26
+ property :artist_name, as: :artist
27
+ copy_validations_from Album
28
+ end
29
+
30
+ let(:album) { Album.new }
31
+
32
+ describe 'non-composite form' do
33
+
34
+ let(:album_form) { AlbumForm.new(album) }
35
+
36
+ it 'is not valid when title is unexistant_song' do
37
+ album_form.validate(artist_name: 'test', title: 'unexistant_song').must_equal false
38
+ end
39
+
40
+ it 'is valid when title is something existant' do
41
+ album_form.validate(artist_name: 'test', title: 'test').must_equal true
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
@@ -1,16 +1,11 @@
1
1
  require 'test_helper'
2
- require 'representable/json'
2
+ require 'reform/form/json'
3
3
 
4
4
  class DeserializeTest < BaseTest
5
5
  class AlbumContract < Reform::Form
6
6
  include Reform::Form::ActiveModel::FormBuilderMethods # overrides #update!, too.
7
7
 
8
- self.representer_class.class_eval do
9
- include Representable::JSON
10
- end
11
- def deserialize_method
12
- :from_json
13
- end
8
+ include Reform::Form::JSON
14
9
 
15
10
  property :title
16
11
  validates :title, :presence => true, :length => {:minimum => 3}
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+
3
+ class EmptyAttributesTest < MiniTest::Spec
4
+ Credentials = Struct.new(:password)
5
+
6
+ class PasswordForm < Reform::Form
7
+ property :password
8
+ property :password_confirmation, :empty => true
9
+ end
10
+
11
+ let (:cred) { Credentials.new }
12
+ let (:form) { PasswordForm.new(cred) }
13
+
14
+ it {
15
+ form.validate("password" => "123", "password_confirmation" => "321")
16
+
17
+ form.password.must_equal "123"
18
+ form.password_confirmation.must_equal "321"
19
+
20
+ form.sync
21
+ cred.password.must_equal "123"
22
+
23
+ hash = {}
24
+ form.save do |nested|
25
+ hash = nested
26
+ end
27
+
28
+ hash.must_equal("password"=> "123", "password_confirmation" => "321")
29
+ }
30
+ end
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+
3
+ class FieldsTest < MiniTest::Spec
4
+ describe "#new" do
5
+ it "accepts list of properties" do
6
+ fields = Reform::Contract::Fields.new([:name, :title])
7
+ fields.name.must_equal nil
8
+ fields.title.must_equal nil
9
+ end
10
+
11
+ it "accepts list of properties and values" do
12
+ fields = Reform::Contract::Fields.new(["name", "title"], "title" => "The Body")
13
+ fields.name.must_equal nil
14
+ fields.title.must_equal "The Body"
15
+ end
16
+
17
+ it "processes value syms" do
18
+ skip "we don't need to test this as representer.to_hash always returns strings"
19
+ fields = Reform::Fields.new(["name", "title"], :title => "The Body")
20
+ fields.name.must_equal nil
21
+ fields.title.must_equal "The Body"
22
+ end
23
+ end
24
+ end
@@ -10,7 +10,7 @@ class FormCompositionTest < MiniTest::Spec
10
10
 
11
11
  property :name, :on => :requester
12
12
  property :requester_id, :on => :requester, :as => :id
13
- properties [:title, :id], :on => :song
13
+ properties :title, :id, :on => :song
14
14
  # property :channel # FIXME: what about the "main model"?
15
15
  property :channel, :empty => true, :on => :song
16
16
  property :requester, :on => :requester
@@ -78,7 +78,7 @@ class FormCompositionTest < MiniTest::Spec
78
78
  )
79
79
  end
80
80
 
81
- it "pushes data to models and calls #save when no block passed" do
81
+ it "xxx pushes data to models and calls #save when no block passed" do
82
82
  song.extend(Saveable)
83
83
  requester.extend(Saveable)
84
84
  band.extend(Saveable)
@@ -95,6 +95,28 @@ class FormCompositionTest < MiniTest::Spec
95
95
  song.band.title.must_equal "Duran^2"
96
96
  song.band.saved?.must_equal true
97
97
  end
98
+
99
+ it "returns true when models all save successfully" do
100
+ song.extend(Saveable)
101
+ requester.extend(Saveable)
102
+ band.extend(Saveable)
103
+
104
+ form.save.must_equal true
105
+ end
106
+
107
+ it "returns false when one or more models don't save successfully" do
108
+ module Unsaveable
109
+ def save
110
+ false
111
+ end
112
+ end
113
+
114
+ song.extend(Unsaveable)
115
+ requester.extend(Saveable)
116
+ band.extend(Saveable)
117
+
118
+ form.save.must_equal false
119
+ end
98
120
  end
99
121
  end
100
122
 
data/test/form_test.rb ADDED
@@ -0,0 +1,84 @@
1
+ require 'test_helper'
2
+
3
+ class FormTest < MiniTest::Spec
4
+ class AlbumForm < Reform::Form
5
+ property :title
6
+
7
+ property :hit do
8
+ property :title
9
+ end
10
+
11
+ collection :songs do
12
+ property :title
13
+ end
14
+
15
+ property :band do # yepp, people do crazy stuff like that.
16
+ property :label do
17
+ property :name
18
+ end
19
+ end
20
+ end
21
+
22
+ # combined property/validates syntax.
23
+ class SongForm < Reform::Form
24
+ property :composer
25
+ property :title, validates: {presence: true}
26
+ properties :genre, :band, validates: {presence: true}
27
+ end
28
+ it do
29
+ form = SongForm.new(OpenStruct.new)
30
+ form.validate({})
31
+ form.errors.to_s.must_equal "{:title=>[\"can't be blank\"], :genre=>[\"can't be blank\"], :band=>[\"can't be blank\"]}"
32
+ end
33
+
34
+ # ::schema
35
+ describe "::schema" do
36
+ let (:schema) { AlbumForm.schema }
37
+
38
+ # it must be a clone
39
+ it { schema.wont_equal AlbumForm.representer_class }
40
+ it { assert schema < Representable::Decorator }
41
+ it { schema.representable_attrs.get(:title).name.must_equal "title" }
42
+
43
+ # hit is clone.
44
+ it { schema.representable_attrs.get(:hit).representer_module.object_id.wont_equal AlbumForm.representer_class.representable_attrs.get(:hit).representer_module.object_id }
45
+ it { assert schema.representable_attrs.get(:hit).representer_module < Representable::Decorator }
46
+
47
+ # band:label is clone.
48
+ # this test might look ridiculous but it is mission-critical to assert that schema is really a clone and doesn't mess up the original structure.
49
+ let (:label) { schema.representable_attrs.get(:band).representer_module.representable_attrs.get(:label) }
50
+ it { assert label.representer_module < Representable::Decorator }
51
+ it { label.representer_module.object_id.wont_equal AlbumForm.representer_class.representable_attrs.get(:band).representer_module.representer_class.representable_attrs.get(:label).representer_module.object_id }
52
+
53
+ # #apply
54
+ it do
55
+ properties = []
56
+
57
+ schema.apply do |dfn|
58
+ properties << dfn.name
59
+ end
60
+
61
+ properties.must_equal ["title", "hit", "title", "songs", "title", "band", "label", "name"]
62
+ end
63
+ end
64
+
65
+
66
+ describe "::dup" do
67
+ let (:cloned) { AlbumForm.clone }
68
+
69
+ # #dup is called in Op.inheritable_attr(:contract_class), it must be subclass of the original one.
70
+ it { cloned.wont_equal AlbumForm }
71
+ it { AlbumForm.representer_class.wont_equal cloned.representer_class }
72
+
73
+ it do
74
+ # currently, forms need a name for validation, even without AM.
75
+ cloned.singleton_class.class_eval do
76
+ def name
77
+ "Album"
78
+ end
79
+ end
80
+ cloned.validates :title, presence: true
81
+ cloned.new(OpenStruct.new).validate({})
82
+ end
83
+ end
84
+ end
data/test/inherit_test.rb CHANGED
@@ -2,6 +2,18 @@ require 'test_helper'
2
2
  require 'representable/json'
3
3
 
4
4
  class InheritTest < BaseTest
5
+ class AlbumForm < Reform::Form
6
+ property :title
7
+
8
+ property :hit do
9
+ property :title
10
+ end
11
+
12
+ collection :songs do
13
+ property :title
14
+ end
15
+ end
16
+
5
17
  class CompilationForm < AlbumForm
6
18
 
7
19
  property :hit, :inherit => true do