reform 2.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.travis.yml +11 -0
  4. data/CHANGES.md +415 -0
  5. data/Gemfile +19 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +339 -0
  8. data/Rakefile +15 -0
  9. data/TODO.md +45 -0
  10. data/gemfiles/Gemfile.disposable-0.3 +6 -0
  11. data/lib/reform.rb +8 -0
  12. data/lib/reform/contract.rb +77 -0
  13. data/lib/reform/contract/errors.rb +43 -0
  14. data/lib/reform/contract/validate.rb +33 -0
  15. data/lib/reform/form.rb +94 -0
  16. data/lib/reform/form/call.rb +23 -0
  17. data/lib/reform/form/coercion.rb +3 -0
  18. data/lib/reform/form/composition.rb +34 -0
  19. data/lib/reform/form/dry.rb +67 -0
  20. data/lib/reform/form/module.rb +27 -0
  21. data/lib/reform/form/mongoid.rb +37 -0
  22. data/lib/reform/form/orm.rb +26 -0
  23. data/lib/reform/form/populator.rb +123 -0
  24. data/lib/reform/form/prepopulate.rb +24 -0
  25. data/lib/reform/form/validate.rb +60 -0
  26. data/lib/reform/mongoid.rb +4 -0
  27. data/lib/reform/validation.rb +40 -0
  28. data/lib/reform/validation/groups.rb +73 -0
  29. data/lib/reform/version.rb +3 -0
  30. data/reform.gemspec +29 -0
  31. data/test/benchmarking.rb +26 -0
  32. data/test/call_test.rb +23 -0
  33. data/test/changed_test.rb +41 -0
  34. data/test/coercion_test.rb +66 -0
  35. data/test/composition_test.rb +149 -0
  36. data/test/contract_test.rb +77 -0
  37. data/test/default_test.rb +22 -0
  38. data/test/deprecation_test.rb +27 -0
  39. data/test/deserialize_test.rb +104 -0
  40. data/test/errors_test.rb +165 -0
  41. data/test/feature_test.rb +65 -0
  42. data/test/fixtures/dry_error_messages.yml +44 -0
  43. data/test/form_option_test.rb +24 -0
  44. data/test/form_test.rb +57 -0
  45. data/test/from_test.rb +75 -0
  46. data/test/inherit_test.rb +119 -0
  47. data/test/module_test.rb +142 -0
  48. data/test/parse_pipeline_test.rb +15 -0
  49. data/test/populate_test.rb +270 -0
  50. data/test/populator_skip_test.rb +28 -0
  51. data/test/prepopulator_test.rb +112 -0
  52. data/test/read_only_test.rb +3 -0
  53. data/test/readable_test.rb +30 -0
  54. data/test/readonly_test.rb +14 -0
  55. data/test/reform_test.rb +223 -0
  56. data/test/save_test.rb +89 -0
  57. data/test/setup_test.rb +48 -0
  58. data/test/skip_if_test.rb +74 -0
  59. data/test/skip_setter_and_getter_test.rb +54 -0
  60. data/test/test_helper.rb +49 -0
  61. data/test/validate_test.rb +420 -0
  62. data/test/validation/dry_test.rb +60 -0
  63. data/test/validation/dry_validation_test.rb +352 -0
  64. data/test/validation/errors.yml +4 -0
  65. data/test/virtual_test.rb +24 -0
  66. data/test/writeable_test.rb +29 -0
  67. metadata +265 -0
@@ -0,0 +1,26 @@
1
+ module Reform::Form::ORM
2
+ def model_for_property(name)
3
+ return model unless is_a?(Reform::Form::Composition) # i am too lazy for proper inheritance. there should be a ActiveRecord::Composition that handles this.
4
+
5
+ model_name = options_for(name)[:on]
6
+ model[model_name]
7
+ end
8
+
9
+ module UniquenessValidator
10
+ # when calling validates it should create the Vali instance already and set @klass there! # TODO: fix this in AM.
11
+ def validate(form)
12
+ property = attributes.first
13
+
14
+ # here is the thing: why does AM::UniquenessValidator require a filled-out record to work properly? also, why do we need to set
15
+ # the class? it would be way easier to pass #validate a hash of attributes and get back an errors hash.
16
+ # the class for the finder could either be infered from the record or set in the validator instance itself in the call to ::validates.
17
+ record = form.model_for_property(property)
18
+ record.send("#{property}=", form.send(property))
19
+
20
+ @klass = record.class # this is usually done in the super-sucky #setup method.
21
+ super(record).tap do |res|
22
+ form.errors.add(property, record.errors.first.last) if record.errors.present?
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,123 @@
1
+ # Implements the :populator option.
2
+ #
3
+ # populator: -> (fragment:, model:, :binding)
4
+ # populator: -> (fragment:, collection:, index:, binding:)
5
+ #
6
+ # For collections, the entire collection and the currently deserialised index is passed in.
7
+ class Reform::Form::Populator
8
+ def initialize(user_proc)
9
+ @user_proc = user_proc # the actual `populator: ->{}` block from the user, via ::property.
10
+ @value = Declarative::Option(user_proc, instance_exec: true) # we can now process Callable, procs, :symbol.
11
+ end
12
+
13
+ def call(input, options)
14
+ model = get(options)
15
+ twin = call!(options.merge(model: model, collection: model))
16
+
17
+ return twin if twin == Representable::Pipeline::Stop
18
+
19
+ # this kinda sucks. the proc may call self.composer = Artist.new, but there's no way we can
20
+ # return the twin instead of the model from the #composer= setter.
21
+ twin = get(options) unless options[:binding].array?
22
+
23
+ # we always need to return a twin/form here so we can call nested.deserialize().
24
+ handle_fail(twin, options)
25
+
26
+ twin
27
+ end
28
+
29
+ private
30
+ def call!(options)
31
+ form = options[:represented]
32
+
33
+ deprecate_positional_args(form, @user_proc, options) do
34
+ @value.(form, options)
35
+ end
36
+ end
37
+
38
+ def handle_fail(twin, options)
39
+ raise "[Reform] Your :populator did not return a Reform::Form instance for `#{options[:binding].name}`." if options[:binding][:nested] && !twin.is_a?(Reform::Form)
40
+ end
41
+
42
+ def get(options)
43
+ Representable::GetValue.(nil, options)
44
+ end
45
+
46
+ def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
47
+ arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
48
+ return yield if arity == 1
49
+ warn "[Reform] Positional arguments for :populator and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
50
+ args = []
51
+ args << options[:index] if options[:index]
52
+ args << options[:representable_options]
53
+ form.instance_exec(options[:fragment], options[:model], *args, &proc)
54
+ end
55
+
56
+
57
+ class IfEmpty < self # Populator
58
+ def call!(options)
59
+ binding, twin, index, fragment = options[:binding], options[:model], options[:index], options[:fragment] # TODO: remove once we drop 2.0.
60
+ form = options[:represented]
61
+
62
+ if binding.array?
63
+ item = twin.original[index] and return item
64
+
65
+ new_index = [index, twin.count].min # prevents nil items with initially empty/smaller collections and :skip_if's.
66
+ # this means the fragment index and populated nested form index might be different.
67
+
68
+ twin.insert(new_index, run!(form, fragment, options)) # form.songs.insert(Song.new)
69
+ else
70
+ return if twin
71
+
72
+ form.send(binding.setter, run!(form, fragment, options)) # form.artist=(Artist.new)
73
+ end
74
+ end
75
+
76
+ private
77
+ def run!(form, fragment, options)
78
+ return @user_proc.new if @user_proc.is_a?(Class) # handle populate_if_empty: Class. this excludes using Callables, though.
79
+
80
+ deprecate_positional_args(form, @user_proc, options) do
81
+ @value.(form, options)
82
+ end
83
+ end
84
+
85
+ def deprecate_positional_args(form, proc, options) # TODO: remove in 2.2.
86
+ arity = proc.is_a?(Symbol) ? form.method(proc).arity : proc.arity
87
+ return yield if arity == 1
88
+ warn "[Reform] Positional arguments for :prepopulate and friends are deprecated. Please use ->(options) and enjoy the rest of your day. Learn more at http://trailblazerb.org/gems/reform/upgrading-guide.html#to-21"
89
+
90
+ @value.(form, options[:fragment], options[:user_options])
91
+ end
92
+
93
+ end
94
+
95
+ # Sync (default) blindly grabs the corresponding form twin and returns it. This might imply that nil is returned,
96
+ # and in turn #validate! is called on nil.
97
+ class Sync < self
98
+ def call!(options)
99
+ if options[:binding].array?
100
+ return options[:model][options[:index]]
101
+ else
102
+ options[:model]
103
+ end
104
+ end
105
+ end
106
+
107
+ # This function is added to the deserializer's pipeline.
108
+ #
109
+ # When deserializing, the representer will call this function and thereby delegate the
110
+ # entire population process to the form. The form's :internal_populator will run its
111
+ # :populator option function and return the new/existing form instance.
112
+ # The deserializing representer will then continue on that returned form.
113
+ #
114
+ # Goal of this indirection is to leave all population logic in the form, while the
115
+ # representer really just traverses an incoming document and dispatches business logic
116
+ # (which population is) to the form.
117
+ class External
118
+ def call(input, options)
119
+ options[:represented].class.definitions.
120
+ get(options[:binding][:name])[:internal_populator].(input, options)
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,24 @@
1
+ # prepopulate!(options)
2
+ # prepopulator: ->(model, user_options)
3
+ module Reform::Form::Prepopulate
4
+ def prepopulate!(options={})
5
+ prepopulate_local!(options) # call #prepopulate! on local properties.
6
+ prepopulate_nested!(options) # THEN call #prepopulate! on nested forms.
7
+
8
+ self
9
+ end
10
+
11
+ private
12
+ def prepopulate_local!(options)
13
+ schema.each do |dfn|
14
+ next unless block = dfn[:prepopulator]
15
+ Declarative::Option(block, instance_exec: true).(self, options)
16
+ end
17
+ end
18
+
19
+ def prepopulate_nested!(options)
20
+ schema.each(twin: true) do |dfn|
21
+ Disposable::Twin::PropertyProcessor.new(dfn, self).() { |form| form.prepopulate!(options) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,60 @@
1
+ # Mechanics for writing to forms in #validate.
2
+ module Reform::Form::Validate
3
+ module Skip
4
+ class AllBlank
5
+ include Uber::Callable
6
+
7
+ def call(form, options)
8
+ params = options[:input]
9
+ # TODO: Schema should provide property names as plain list.
10
+ properties = options[:binding][:nested].definitions.collect { |dfn| dfn[:name] }
11
+
12
+ properties.each { |name| (!params[name].nil? && params[name] != "") and return false }
13
+ true # skip
14
+ end
15
+ end
16
+ end
17
+
18
+
19
+ def validate(params)
20
+ # allow an external deserializer.
21
+ block_given? ? yield(params) : deserialize(params)
22
+
23
+ super() # run the actual validation on self.
24
+ end
25
+
26
+ def deserialize(params)
27
+ params = deserialize!(params)
28
+ deserializer.new(self).from_hash(params)
29
+ end
30
+
31
+ private
32
+ # Meant to return params processable by the representer. This is the hook for munching date fields, etc.
33
+ def deserialize!(params)
34
+ # NOTE: it is completely up to the form user how they want to deserialize (e.g. using an external JSON-API representer).
35
+ # use the deserializer as an external instance to operate on the Twin API,
36
+ # e.g. adding new items in collections using #<< etc.
37
+ # DISCUSS: using self here will call the form's setters like title= which might be overridden.
38
+ params
39
+ end
40
+
41
+ # Default deserializer for hash.
42
+ # This is input-specific, e.g. Hash, JSON, or XML.
43
+ def deserializer(source=self.class, options={}) # called on top-level, only, for now.
44
+ deserializer = Disposable::Rescheme.from(source,
45
+ {
46
+ include: [Representable::Hash::AllowSymbols, Representable::Hash],
47
+ superclass: Representable::Decorator,
48
+ definitions_from: lambda { |inline| inline.definitions },
49
+ options_from: :deserializer,
50
+ exclude_options: [:default, :populator] # Reform must not copy Disposable/Reform-only options that might confuse representable.
51
+ }.merge(options)
52
+ )
53
+
54
+ deserializer
55
+ end
56
+
57
+
58
+ class DeserializeError < RuntimeError
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ require 'reform/form/active_model'
2
+ require 'reform/form/orm'
3
+ require 'reform/form/mongoid'
4
+ require 'reform/form/active_model/model_reflections' # only load this in AR context as simple_form currently is bound to AR.
@@ -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=:default, 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).(to_nested_hash, 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
@@ -0,0 +1,3 @@
1
+ module Reform
2
+ VERSION = "2.2.4"
3
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'reform/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "reform"
7
+ spec.version = Reform::VERSION
8
+ spec.authors = ["Nick Sutterer", "Garrett Heinlen"]
9
+ spec.email = ["apotonick@gmail.com", "heinleng@gmail.com"]
10
+ spec.description = %q{Form object decoupled from models.}
11
+ spec.summary = %q{Form object decoupled from models with validation, population and presentation.}
12
+ spec.homepage = "https://github.com/apotonick/reform"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "disposable", ">= 0.4.1"
21
+ spec.add_dependency "representable", ">= 2.4.0", "< 3.1.0"
22
+
23
+ spec.add_development_dependency "bundler"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "minitest"
26
+ spec.add_development_dependency "dry-types"
27
+ spec.add_development_dependency "multi_json"
28
+ spec.add_development_dependency "dry-validation", ">= 0.10.0"
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'reform'
2
+ require 'ostruct'
3
+ require 'benchmark'
4
+
5
+ class BandForm < Reform::Form
6
+ property :name, validates: {presence: true}
7
+
8
+ collection :songs do
9
+ property :title, validates: {presence: true}
10
+ end
11
+ end
12
+
13
+ songs = 50.times.collect { OpenStruct.new(title: "Be Stag") }
14
+ band = OpenStruct.new(name: "Teenage Bottlerock", songs: songs)
15
+
16
+ songs_params = 50.times.collect { {title: "Commando"} }
17
+
18
+ time = Benchmark.measure do
19
+ 100.times.each do
20
+ form = BandForm.new(band)
21
+ form.validate("name" => "Ramones", "songs" => songs_params)
22
+ form.save
23
+ end
24
+ end
25
+
26
+ puts time
@@ -0,0 +1,23 @@
1
+ require "test_helper"
2
+
3
+ class CallTest < Minitest::Spec
4
+ Song = Struct.new(:title)
5
+
6
+ class SongForm < Reform::Form
7
+ property :title
8
+
9
+ validation do
10
+ key(:title).required
11
+ end
12
+ end
13
+
14
+ let (:form) { SongForm.new(Song.new) }
15
+
16
+ it { form.(title: "True North").success?.must_equal true }
17
+ it { form.(title: "True North").failure?.must_equal false }
18
+ it { form.(title: "").success?.must_equal false }
19
+ it { form.(title: "").failure?.must_equal true }
20
+
21
+ it { form.(title: "True North").errors.messages.must_equal({}) }
22
+ it { form.(title: "").errors.messages.must_equal({:title=>["must be filled"]}) }
23
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+ require 'reform/form/coercion'
3
+
4
+ class ChangedTest < MiniTest::Spec
5
+ Song = Struct.new(:title, :album, :composer)
6
+ Album = Struct.new(:name, :songs, :artist)
7
+ Artist = Struct.new(:name)
8
+
9
+ class AlbumForm < Reform::Form
10
+ property :name
11
+
12
+ collection :songs do
13
+ property :title
14
+
15
+ property :composer do
16
+ property :name
17
+ end
18
+ end
19
+ end
20
+
21
+ let (:song_with_composer) { Song.new("Resist Stance", nil, composer) }
22
+ let (:composer) { Artist.new("Greg Graffin") }
23
+ let (:album) { Album.new("The Dissent Of Man", [song_with_composer]) }
24
+
25
+ let (:form) { AlbumForm.new(album) }
26
+
27
+ # nothing changed after setup.
28
+ it do
29
+ form.changed?(:name).must_equal false
30
+ form.songs[0].changed?(:title).must_equal false
31
+ form.songs[0].composer.changed?(:name).must_equal false
32
+ end
33
+
34
+ # after validate, things might have changed.
35
+ it do
36
+ form.validate("name" => "Out Of Bounds", "songs" => [{"composer" => {"name" => "Ingemar Jansson & Mikael Danielsson"}}])
37
+ form.changed?(:name).must_equal true
38
+ form.songs[0].changed?(:title).must_equal false
39
+ form.songs[0].composer.changed?(:name).must_equal true
40
+ end
41
+ end