reform 2.0.5 → 2.1.0.rc1
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/.travis.yml +3 -1
- data/CHANGES.md +12 -0
- data/Gemfile +12 -2
- data/README.md +9 -14
- data/Rakefile +1 -1
- data/database.sqlite3 +0 -0
- data/lib/reform.rb +1 -0
- data/lib/reform/contract.rb +13 -20
- data/lib/reform/contract/validate.rb +9 -7
- data/lib/reform/form.rb +45 -31
- data/lib/reform/form/active_model.rb +10 -10
- data/lib/reform/form/active_model/form_builder_methods.rb +5 -4
- data/lib/reform/form/active_model/model_reflections.rb +2 -2
- data/lib/reform/form/active_model/model_validations.rb +3 -3
- data/lib/reform/form/active_model/validations.rb +49 -32
- data/lib/reform/form/dry.rb +55 -0
- data/lib/reform/form/lotus.rb +4 -1
- data/lib/reform/form/module.rb +3 -17
- data/lib/reform/form/multi_parameter_attributes.rb +0 -9
- data/lib/reform/form/populator.rb +72 -30
- data/lib/reform/form/validate.rb +19 -43
- data/lib/reform/form/validation/unique_validator.rb +39 -6
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +1 -1
- data/reform.gemspec +3 -1
- data/test/active_record_test.rb +2 -0
- data/test/contract_test.rb +2 -2
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +29 -8
- data/test/dummy/config/locales/en.yml +4 -1
- data/test/errors_test.rb +4 -4
- data/test/feature_test.rb +2 -2
- data/test/fixtures/dry_error_messages.yml +43 -0
- data/test/form_builder_test.rb +10 -8
- data/test/form_test.rb +1 -36
- data/test/inherit_test.rb +20 -8
- data/test/module_test.rb +2 -30
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +41 -12
- data/test/populator_skip_test.rb +28 -0
- data/test/reform_test.rb +1 -1
- data/test/skip_if_test.rb +10 -3
- data/test/test_helper.rb +11 -2
- data/test/unique_test.rb +72 -1
- data/test/validate_test.rb +6 -7
- data/test/validation/activemodel_validation_test.rb +252 -0
- data/test/validation/dry_validation_test.rb +330 -0
- metadata +63 -10
- data/lib/reform/schema.rb +0 -13
@@ -1,4 +1,33 @@
|
|
1
|
-
#
|
1
|
+
# === Unique Validation
|
2
|
+
# Reform's own implementation for uniqueness which does not write to model
|
3
|
+
#
|
4
|
+
# == Usage
|
5
|
+
# Pass a true boolean value to validate a field against all values available in
|
6
|
+
# the database:
|
7
|
+
# validates :title, unique: true
|
8
|
+
#
|
9
|
+
# == Options
|
10
|
+
# = Scope
|
11
|
+
# A scope can be use to filter the records that need to be compare with the
|
12
|
+
# current value to validate. A scope array can have one to many fields define.
|
13
|
+
#
|
14
|
+
# A scope can be define the following ways:
|
15
|
+
# validates :title, unique: { scope: :album_id }
|
16
|
+
# validates :title, unique: { scope: [:album_id] }
|
17
|
+
# validates :title, unique: { scope: [:album_id, ...] }
|
18
|
+
#
|
19
|
+
# All fields included in a scope must be declared as a property like this:
|
20
|
+
# property :album_id
|
21
|
+
# validates :title, unique: { scope: :album_id }
|
22
|
+
#
|
23
|
+
# Just remove write access to the property if the field must not be change:
|
24
|
+
# property :album_id, writeable: false
|
25
|
+
# validates :title, unique: { scope: :album_id }
|
26
|
+
#
|
27
|
+
# This use case is useful if album_id is set to a Song this way:
|
28
|
+
# song = album.songs.new
|
29
|
+
# album_id is automatically set and can't be change by the operation
|
30
|
+
|
2
31
|
class Reform::Form::UniqueValidator < ActiveModel::EachValidator
|
3
32
|
def validate_each(form, attribute, value)
|
4
33
|
model = form.model_for_property(attribute)
|
@@ -6,11 +35,15 @@ class Reform::Form::UniqueValidator < ActiveModel::EachValidator
|
|
6
35
|
# search for models with attribute equals to form field value
|
7
36
|
query = model.class.where(attribute => value)
|
8
37
|
|
9
|
-
#
|
10
|
-
|
38
|
+
# apply scope if options has been declared
|
39
|
+
Array(options[:scope]).each do |field|
|
40
|
+
# add condition to only check unique value with the same scope
|
41
|
+
query = query.where(field => form.send(field))
|
42
|
+
end
|
11
43
|
|
12
|
-
# if
|
13
|
-
|
44
|
+
# if model persisted, query may return 0 or 1 rows, else 0
|
45
|
+
allow_count = model.persisted? ? 1 : 0
|
46
|
+
form.errors.add(attribute, :taken) if query.count > allow_count
|
14
47
|
end
|
15
48
|
end
|
16
49
|
|
@@ -18,4 +51,4 @@ end
|
|
18
51
|
# make the new :unique validator available here.
|
19
52
|
Reform::Form::ActiveModel::Validations::Validator.class_eval do
|
20
53
|
UniqueValidator = Reform::Form::UniqueValidator
|
21
|
-
end
|
54
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# Adds ::validates and friends, and #valid? to the object.
|
2
|
+
# This is completely form-independent.
|
3
|
+
module Reform::Validation
|
4
|
+
module ClassMethods
|
5
|
+
def validation_groups
|
6
|
+
@groups ||= Groups.new(validation_group_class) # TODO: inheritable_attr with Inheritable::Hash
|
7
|
+
end
|
8
|
+
|
9
|
+
# DSL.
|
10
|
+
def validation(name, options={}, &block)
|
11
|
+
heritage.record(:validation, name, options, &block)
|
12
|
+
|
13
|
+
group = validation_groups.add(name, options)
|
14
|
+
|
15
|
+
group.instance_exec(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validates(*args, &block)
|
19
|
+
validation(:default, inherit: true) { validates *args, &block }
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate(*args, &block)
|
23
|
+
validation(:default, inherit: true) { validate *args, &block }
|
24
|
+
end
|
25
|
+
|
26
|
+
def validates_with(*args, &block)
|
27
|
+
validation(:default, inherit: true) { validates_with *args, &block }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.included(includer)
|
32
|
+
includer.extend(ClassMethods)
|
33
|
+
end
|
34
|
+
|
35
|
+
def valid?
|
36
|
+
Groups::Result.new(self.class.validation_groups).(@fields, errors, self)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
require "reform/validation/groups"
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Reform::Validation
|
2
|
+
# A Group is a set of native validations, targeting a validation backend (AM, Lotus, Dry).
|
3
|
+
# Group receives configuration via #validates and #validate and translates that to its
|
4
|
+
# internal backend.
|
5
|
+
#
|
6
|
+
# The #call method will run those validations on the provided objects.
|
7
|
+
|
8
|
+
# Set of Validation::Group objects.
|
9
|
+
# This implements adding, iterating, and finding groups, including "inheritance" and insertions.
|
10
|
+
class Groups < Array
|
11
|
+
def initialize(group_class)
|
12
|
+
@group_class = group_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(name, options)
|
16
|
+
if options[:inherit]
|
17
|
+
return self[name] if self[name]
|
18
|
+
end
|
19
|
+
|
20
|
+
i = index_for(options)
|
21
|
+
|
22
|
+
self.insert(i, [name, group = @group_class.new, options]) # Group.new
|
23
|
+
group
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def index_for(options)
|
29
|
+
return find_index { |el| el.first == options[:after] } + 1 if options[:after]
|
30
|
+
size # default index: append.
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](name)
|
34
|
+
cfg = find { |cfg| cfg.first == name }
|
35
|
+
return unless cfg
|
36
|
+
cfg[1]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Runs all validations groups according to their rules and returns result.
|
41
|
+
# Populates errors passed into #call.
|
42
|
+
class Result # DISCUSS: could be in Groups.
|
43
|
+
def initialize(groups)
|
44
|
+
@groups = groups
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(fields, errors, form)
|
48
|
+
result = true
|
49
|
+
results = {}
|
50
|
+
|
51
|
+
@groups.each do |cfg|
|
52
|
+
name, group, options = cfg
|
53
|
+
depends_on = options[:if]
|
54
|
+
|
55
|
+
if evaluate_if(depends_on, results, form)
|
56
|
+
# puts "evaluating #{group.instance_variable_get(:@validator).instance_variable_get(:@checker).inspect}"
|
57
|
+
results[name] = group.(fields, errors, form).empty? # validate.
|
58
|
+
end
|
59
|
+
|
60
|
+
result &= errors.empty?
|
61
|
+
end
|
62
|
+
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def evaluate_if(depends_on, results, form)
|
67
|
+
return true if depends_on.nil?
|
68
|
+
return results[depends_on] if depends_on.is_a?(Symbol)
|
69
|
+
form.instance_exec(results, &depends_on)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/reform/version.rb
CHANGED
data/reform.gemspec
CHANGED
@@ -17,8 +17,9 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
-
spec.add_dependency "disposable", "
|
20
|
+
spec.add_dependency "disposable", ">= 0.2.1", "< 0.3.0"
|
21
21
|
spec.add_dependency "uber", "~> 0.0.11"
|
22
|
+
spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
|
22
23
|
|
23
24
|
spec.add_development_dependency "bundler"
|
24
25
|
spec.add_development_dependency "rake"
|
@@ -31,5 +32,6 @@ Gem::Specification.new do |spec|
|
|
31
32
|
spec.add_development_dependency "multi_json"
|
32
33
|
|
33
34
|
spec.add_development_dependency "lotus-validations"
|
35
|
+
spec.add_development_dependency "dry-validation"
|
34
36
|
spec.add_development_dependency "actionpack"
|
35
37
|
end
|
data/test/active_record_test.rb
CHANGED
data/test/contract_test.rb
CHANGED
@@ -40,7 +40,7 @@ class ContractTest < MiniTest::Spec
|
|
40
40
|
end
|
41
41
|
|
42
42
|
describe "#options_for" do
|
43
|
-
it { AlbumForm.options_for(:name).inspect.
|
44
|
-
it { AlbumForm.new(album).options_for(:name).inspect.
|
43
|
+
it { AlbumForm.options_for(:name).extend(Declarative::Inspect).inspect.must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
|
44
|
+
it { AlbumForm.new(album).options_for(:name).extend(Declarative::Inspect).inspect.must_equal "#<Disposable::Twin::Definition: @options={:private_name=>:name, :name=>\"name\"}>" }
|
45
45
|
end
|
46
46
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class DeprecationRemoveMePopulatorTest < MiniTest::Spec
|
5
|
+
Album = Struct.new(:songs)
|
6
|
+
Song = Struct.new(:title)
|
7
|
+
|
8
|
+
|
9
|
+
class AlbumForm < Reform::Form
|
10
|
+
collection :songs, populator: ->(fragment, collection, index, *) { return Representable::Pipeline::Stop if fragment[:title]=="Good"
|
11
|
+
songs[index]
|
12
|
+
} do
|
13
|
+
property :title
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it do
|
18
|
+
form = AlbumForm.new(Album.new([Song.new, Song.new]))
|
19
|
+
hash = {songs: [{title: "Good"}, {title: "Bad"}]}
|
20
|
+
|
21
|
+
form.validate(hash)
|
22
|
+
|
23
|
+
form.songs.size.must_equal 2
|
24
|
+
form.songs[0].title.must_equal nil
|
25
|
+
form.songs[1].title.must_equal "Bad"
|
26
|
+
end
|
27
|
+
end
|
data/test/deserialize_test.rb
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
require 'test_helper'
|
2
|
+
require "representable/json"
|
2
3
|
|
3
4
|
class DeserializeTest < MiniTest::Spec
|
4
5
|
Song = Struct.new(:title, :album, :composer)
|
5
6
|
Album = Struct.new(:title, :artist)
|
6
|
-
Artist = Struct.new(:name)
|
7
|
+
Artist = Struct.new(:name, :callname)
|
7
8
|
|
8
9
|
class JsonAlbumForm < Reform::Form
|
9
10
|
module Json
|
10
11
|
def deserialize(params)
|
11
|
-
# params = deserialize!(params) # DON'T call those hash hooks.
|
12
|
-
|
13
12
|
deserializer.new(self).
|
14
13
|
# extend(Representable::Debug).
|
15
14
|
from_json(params)
|
16
15
|
end
|
17
16
|
|
18
17
|
def deserializer
|
19
|
-
|
18
|
+
Disposable::Rescheme.from(self.class,
|
20
19
|
include: [Representable::JSON],
|
21
20
|
superclass: Representable::Decorator,
|
22
|
-
|
23
|
-
options_from: :deserializer
|
21
|
+
definitions_from: lambda { |inline| inline.definitions },
|
22
|
+
options_from: :deserializer,
|
23
|
+
exclude_options: [:populator]
|
24
24
|
)
|
25
25
|
end
|
26
26
|
end
|
@@ -33,6 +33,7 @@ class DeserializeTest < MiniTest::Spec
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
|
36
37
|
let (:artist) { Artist.new("A-ha") }
|
37
38
|
it do
|
38
39
|
artist_id = artist.object_id
|
@@ -46,6 +47,26 @@ class DeserializeTest < MiniTest::Spec
|
|
46
47
|
form.artist.name.must_equal "Mute"
|
47
48
|
form.artist.model.object_id.must_equal artist_id
|
48
49
|
end
|
50
|
+
|
51
|
+
describe "infering the deserializer from another form should NOT copy its populators" do
|
52
|
+
class CompilationForm < Reform::Form
|
53
|
+
property :artist, populator: ->(options) { self.artist = Artist.new(nil, options[:fragment].to_s) } do
|
54
|
+
property :name
|
55
|
+
end
|
56
|
+
|
57
|
+
def deserializer
|
58
|
+
super(JsonAlbumForm, include: [Representable::Hash])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# also tests the Form#deserializer API. # FIXME.
|
63
|
+
it "uses deserializer inferred from JsonAlbumForm but deserializes/populates to CompilationForm" do
|
64
|
+
form = CompilationForm.new(Album.new)
|
65
|
+
form.validate("artist"=> {"name" => "Horowitz"}) # the deserializer doesn't know symbols.
|
66
|
+
form.sync
|
67
|
+
form.artist.model.must_equal Artist.new("Horowitz", %{{"name"=>"Horowitz"}})
|
68
|
+
end
|
69
|
+
end
|
49
70
|
end
|
50
71
|
|
51
72
|
|
@@ -66,10 +87,10 @@ class ValidateWithBlockTest < MiniTest::Spec
|
|
66
87
|
form = AlbumForm.new(album)
|
67
88
|
json = {title: "Apocalypse Soon", artist: {name: "Mute"}}.to_json
|
68
89
|
|
69
|
-
deserializer = Disposable::
|
90
|
+
deserializer = Disposable::Rescheme.from(AlbumForm,
|
70
91
|
include: [Representable::JSON],
|
71
92
|
superclass: Representable::Decorator,
|
72
|
-
|
93
|
+
definitions_from: lambda { |inline| inline.definitions },
|
73
94
|
options_from: :deserializer
|
74
95
|
)
|
75
96
|
|
@@ -1,7 +1,9 @@
|
|
1
1
|
---
|
2
2
|
en:
|
3
3
|
# custom validation error messages
|
4
|
-
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
taken: has already been taken
|
5
7
|
activemodel:
|
6
8
|
errors:
|
7
9
|
models:
|
@@ -9,3 +11,4 @@ en:
|
|
9
11
|
attributes:
|
10
12
|
title:
|
11
13
|
custom_error_message: Custom Error Message
|
14
|
+
taken: has already been taken
|
data/test/errors_test.rb
CHANGED
@@ -83,10 +83,10 @@ class ErrorsTest < MiniTest::Spec
|
|
83
83
|
|
84
84
|
|
85
85
|
describe "#validate with main form invalid" do
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
it do
|
87
|
+
form.validate("title"=>"", "band"=>{"label"=>{:name => "Fat Wreck"}}).must_equal false
|
88
|
+
form.errors.messages.must_equal({:title=>["can't be blank"]})
|
89
|
+
end
|
90
90
|
end
|
91
91
|
|
92
92
|
|
data/test/feature_test.rb
CHANGED
@@ -10,8 +10,8 @@ class FeatureInheritanceTest < BaseTest
|
|
10
10
|
"May 16"
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.included(
|
14
|
-
|
13
|
+
def self.included(includer)
|
14
|
+
includer.send :register_feature, self
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
array?: "%{name} must be an array"
|
2
|
+
|
3
|
+
empty?: "%{name} cannot be empty"
|
4
|
+
|
5
|
+
exclusion?: "%{name} must not be one of: %{list}"
|
6
|
+
|
7
|
+
eql?: "%{name} must be equal to %{eql_value}"
|
8
|
+
|
9
|
+
filled?: "%{name} must be filled"
|
10
|
+
|
11
|
+
format?: "%{name} is in invalid format"
|
12
|
+
|
13
|
+
gt?: "%{name} must be greater than %{num} (%{value} was given)"
|
14
|
+
|
15
|
+
gteq?: "%{name} must be greater than or equal to %{num}"
|
16
|
+
|
17
|
+
hash?: "%{name} must be a hash"
|
18
|
+
|
19
|
+
inclusion?: "%{name} must be one of: %{list}"
|
20
|
+
|
21
|
+
int?: "%{name} must be an integer"
|
22
|
+
|
23
|
+
key?: "%{name} is missing"
|
24
|
+
|
25
|
+
lt?: "%{name} must be less than %{num} (%{value} was given)"
|
26
|
+
|
27
|
+
lteq?: "%{name} must be less than or equal to %{num}"
|
28
|
+
|
29
|
+
max_size?: "%{name} size cannot be greater than %{num}"
|
30
|
+
|
31
|
+
min_size?: "%{name} size cannot be less than %{num}"
|
32
|
+
|
33
|
+
nil?: "%{name} cannot be nil"
|
34
|
+
|
35
|
+
size?:
|
36
|
+
range: "%{name} size must be within %{left} - %{right}"
|
37
|
+
default: "%{name} size must be %{num}"
|
38
|
+
|
39
|
+
str?: "%{name} must be a string"
|
40
|
+
|
41
|
+
good_musical_taste?: "you're a bad person"
|
42
|
+
|
43
|
+
form_access_validation?: "this doesn't look like a Reform form dude!!"
|
data/test/form_builder_test.rb
CHANGED
@@ -12,7 +12,7 @@ class FormBuilderCompatTest < BaseTest
|
|
12
12
|
end
|
13
13
|
|
14
14
|
collection :songs do
|
15
|
-
feature Reform::Form::ActiveModel::FormBuilderMethods
|
15
|
+
# feature Reform::Form::ActiveModel::FormBuilderMethods
|
16
16
|
property :title
|
17
17
|
property :release_date, :multi_params => true
|
18
18
|
validates :title, :presence => true
|
@@ -37,15 +37,17 @@ class FormBuilderCompatTest < BaseTest
|
|
37
37
|
|
38
38
|
|
39
39
|
let (:song) { OpenStruct.new }
|
40
|
-
let (:form) {
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
let (:form) {
|
41
|
+
AlbumForm.new(OpenStruct.new(
|
42
|
+
:artist => Artist.new(:name => "Propagandhi"),
|
43
|
+
:songs => [song],
|
44
|
+
:label => Label.new,
|
44
45
|
|
45
|
-
|
46
|
-
))
|
46
|
+
:band => Band.new(OpenStruct.new(location: OpenStruct.new))
|
47
|
+
))
|
48
|
+
}
|
47
49
|
|
48
|
-
it "
|
50
|
+
it "respects _attributes params hash" do
|
49
51
|
form.validate(
|
50
52
|
"artist_attributes" => {"name" => "Blink 182"},
|
51
53
|
"songs_attributes" => {"0" => {"title" => "Damnit"}},
|