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