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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -1
  3. data/CHANGES.md +12 -0
  4. data/Gemfile +12 -2
  5. data/README.md +9 -14
  6. data/Rakefile +1 -1
  7. data/database.sqlite3 +0 -0
  8. data/lib/reform.rb +1 -0
  9. data/lib/reform/contract.rb +13 -20
  10. data/lib/reform/contract/validate.rb +9 -7
  11. data/lib/reform/form.rb +45 -31
  12. data/lib/reform/form/active_model.rb +10 -10
  13. data/lib/reform/form/active_model/form_builder_methods.rb +5 -4
  14. data/lib/reform/form/active_model/model_reflections.rb +2 -2
  15. data/lib/reform/form/active_model/model_validations.rb +3 -3
  16. data/lib/reform/form/active_model/validations.rb +49 -32
  17. data/lib/reform/form/dry.rb +55 -0
  18. data/lib/reform/form/lotus.rb +4 -1
  19. data/lib/reform/form/module.rb +3 -17
  20. data/lib/reform/form/multi_parameter_attributes.rb +0 -9
  21. data/lib/reform/form/populator.rb +72 -30
  22. data/lib/reform/form/validate.rb +19 -43
  23. data/lib/reform/form/validation/unique_validator.rb +39 -6
  24. data/lib/reform/validation.rb +40 -0
  25. data/lib/reform/validation/groups.rb +73 -0
  26. data/lib/reform/version.rb +1 -1
  27. data/reform.gemspec +3 -1
  28. data/test/active_record_test.rb +2 -0
  29. data/test/contract_test.rb +2 -2
  30. data/test/deprecation_test.rb +27 -0
  31. data/test/deserialize_test.rb +29 -8
  32. data/test/dummy/config/locales/en.yml +4 -1
  33. data/test/errors_test.rb +4 -4
  34. data/test/feature_test.rb +2 -2
  35. data/test/fixtures/dry_error_messages.yml +43 -0
  36. data/test/form_builder_test.rb +10 -8
  37. data/test/form_test.rb +1 -36
  38. data/test/inherit_test.rb +20 -8
  39. data/test/module_test.rb +2 -30
  40. data/test/parse_pipeline_test.rb +15 -0
  41. data/test/populate_test.rb +41 -12
  42. data/test/populator_skip_test.rb +28 -0
  43. data/test/reform_test.rb +1 -1
  44. data/test/skip_if_test.rb +10 -3
  45. data/test/test_helper.rb +11 -2
  46. data/test/unique_test.rb +72 -1
  47. data/test/validate_test.rb +6 -7
  48. data/test/validation/activemodel_validation_test.rb +252 -0
  49. data/test/validation/dry_validation_test.rb +330 -0
  50. metadata +63 -10
  51. data/lib/reform/schema.rb +0 -13
@@ -1,4 +1,33 @@
1
- # Reform's own implementation for uniqueness which does not write to model.
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
- # if model persisted, excluded own model from query
10
- query = query.merge(model.class.where("id <> ?", model.id)) if model.persisted?
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 any models found, add error on attribute
13
- form.errors.add(attribute, "#{attribute} must be unique.") if query.any?
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
@@ -1,3 +1,3 @@
1
1
  module Reform
2
- VERSION = "2.0.5"
2
+ VERSION = "2.1.0.rc1"
3
3
  end
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", "~> 0.1.11"
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
@@ -21,6 +21,8 @@ require 'reform/active_record'
21
21
 
22
22
  class ActiveRecordTest < MiniTest::Spec
23
23
  class SongForm < Reform::Form
24
+ feature Reform::Form::ActiveModel::Validations
25
+
24
26
  include Reform::Form::ActiveRecord
25
27
  model :song
26
28
 
@@ -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.must_match "#<Representable::Definition ==>name @options" }
44
- it { AlbumForm.new(album).options_for(:name).inspect.must_match "#<Representable::Definition ==>name @options" }
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
@@ -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
- deserializer = Disposable::Twin::Schema.from(self.class,
18
+ Disposable::Rescheme.from(self.class,
20
19
  include: [Representable::JSON],
21
20
  superclass: Representable::Decorator,
22
- representer_from: lambda { |inline| inline.representer_class },
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::Twin::Schema.from(AlbumForm,
90
+ deserializer = Disposable::Rescheme.from(AlbumForm,
70
91
  include: [Representable::JSON],
71
92
  superclass: Representable::Decorator,
72
- representer_from: lambda { |inline| inline.representer_class },
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
- before { @result = form.validate("title"=>"", "band"=>{"label"=>{:name => "Fat Wreck"}}) }
87
-
88
- it { @result.must_equal false }
89
- it { form.errors.messages.must_equal({:title=>["can't be blank"]}) }
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(base)
14
- base.representer_class.representable_attrs.features << self # TODO: register_feature
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!!"
@@ -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) { AlbumForm.new(OpenStruct.new(
41
- :artist => Artist.new(:name => "Propagandhi"),
42
- :songs => [song],
43
- :label => Label.new,
40
+ let (:form) {
41
+ AlbumForm.new(OpenStruct.new(
42
+ :artist => Artist.new(:name => "Propagandhi"),
43
+ :songs => [song],
44
+ :label => Label.new,
44
45
 
45
- :band => Band.new(OpenStruct.new(location: OpenStruct.new))
46
- )) }
46
+ :band => Band.new(OpenStruct.new(location: OpenStruct.new))
47
+ ))
48
+ }
47
49
 
48
- it "xxxrespects _attributes params hash" do
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"}},