reform 2.2.4

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