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.
- checksums.yaml +4 -4
- data/CHANGES.md +35 -1
- data/Gemfile +1 -1
- data/README.md +83 -21
- data/TODO.md +8 -0
- data/database.sqlite3 +0 -0
- data/gemfiles/Gemfile.rails-4.0 +1 -0
- data/lib/reform.rb +4 -2
- data/lib/reform/active_record.rb +2 -1
- data/lib/reform/composition.rb +2 -2
- data/lib/reform/contract.rb +24 -7
- data/lib/reform/contract/setup.rb +21 -9
- data/lib/reform/contract/validate.rb +0 -6
- data/lib/reform/form.rb +6 -8
- data/lib/reform/form/active_model.rb +3 -2
- data/lib/reform/form/active_model/model_validations.rb +13 -1
- data/lib/reform/form/active_record.rb +1 -7
- data/lib/reform/form/changed.rb +9 -0
- data/lib/reform/form/json.rb +13 -0
- data/lib/reform/form/model_reflections.rb +18 -0
- data/lib/reform/form/save.rb +25 -3
- data/lib/reform/form/scalar.rb +4 -2
- data/lib/reform/form/sync.rb +82 -12
- data/lib/reform/form/validate.rb +38 -0
- data/lib/reform/rails.rb +1 -1
- data/lib/reform/representer.rb +14 -23
- data/lib/reform/schema.rb +23 -0
- data/lib/reform/twin.rb +20 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +2 -2
- data/test/active_model_test.rb +2 -2
- data/test/active_record_test.rb +7 -4
- data/test/changed_test.rb +69 -0
- data/test/custom_validation_test.rb +47 -0
- data/test/deserialize_test.rb +2 -7
- data/test/empty_test.rb +30 -0
- data/test/fields_test.rb +24 -0
- data/test/form_composition_test.rb +24 -2
- data/test/form_test.rb +84 -0
- data/test/inherit_test.rb +12 -0
- data/test/model_reflections_test.rb +65 -0
- data/test/read_only_test.rb +28 -0
- data/test/reform_test.rb +2 -175
- data/test/representer_test.rb +47 -0
- data/test/save_test.rb +51 -1
- data/test/scalar_test.rb +0 -18
- data/test/skip_if_test.rb +62 -0
- data/test/skip_unchanged_test.rb +86 -0
- data/test/sync_option_test.rb +83 -0
- data/test/twin_test.rb +23 -0
- data/test/validate_test.rb +9 -1
- metadata +37 -9
- 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
|
data/lib/reform/twin.rb
ADDED
@@ -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
|
data/lib/reform/version.rb
CHANGED
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
|
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.
|
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"
|
data/test/active_model_test.rb
CHANGED
@@ -100,8 +100,8 @@ class ActiveModelWithCompositionTest < MiniTest::Spec
|
|
100
100
|
include Composition
|
101
101
|
include Reform::Form::ActiveModel
|
102
102
|
|
103
|
-
property :title,
|
104
|
-
properties
|
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
|
data/test/active_record_test.rb
CHANGED
@@ -46,13 +46,16 @@ class ActiveRecordTest < MiniTest::Spec
|
|
46
46
|
end
|
47
47
|
|
48
48
|
# uniqueness
|
49
|
-
it "
|
50
|
-
form.validate("title" => "The Gargoyle", "artist_id" => artist.id, "album" => album.id, "created_at" => "November 6, 1966")
|
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 "
|
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)
|
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
|
data/test/deserialize_test.rb
CHANGED
@@ -1,16 +1,11 @@
|
|
1
1
|
require 'test_helper'
|
2
|
-
require '
|
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
|
-
|
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}
|
data/test/empty_test.rb
ADDED
@@ -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
|
data/test/fields_test.rb
ADDED
@@ -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
|
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
|