reform 1.1.1 → 1.2.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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