reform 2.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.travis.yml +11 -0
- data/CHANGES.md +415 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +22 -0
- data/README.md +339 -0
- data/Rakefile +15 -0
- data/TODO.md +45 -0
- data/gemfiles/Gemfile.disposable-0.3 +6 -0
- data/lib/reform.rb +8 -0
- data/lib/reform/contract.rb +77 -0
- data/lib/reform/contract/errors.rb +43 -0
- data/lib/reform/contract/validate.rb +33 -0
- data/lib/reform/form.rb +94 -0
- data/lib/reform/form/call.rb +23 -0
- data/lib/reform/form/coercion.rb +3 -0
- data/lib/reform/form/composition.rb +34 -0
- data/lib/reform/form/dry.rb +67 -0
- data/lib/reform/form/module.rb +27 -0
- data/lib/reform/form/mongoid.rb +37 -0
- data/lib/reform/form/orm.rb +26 -0
- data/lib/reform/form/populator.rb +123 -0
- data/lib/reform/form/prepopulate.rb +24 -0
- data/lib/reform/form/validate.rb +60 -0
- data/lib/reform/mongoid.rb +4 -0
- data/lib/reform/validation.rb +40 -0
- data/lib/reform/validation/groups.rb +73 -0
- data/lib/reform/version.rb +3 -0
- data/reform.gemspec +29 -0
- data/test/benchmarking.rb +26 -0
- data/test/call_test.rb +23 -0
- data/test/changed_test.rb +41 -0
- data/test/coercion_test.rb +66 -0
- data/test/composition_test.rb +149 -0
- data/test/contract_test.rb +77 -0
- data/test/default_test.rb +22 -0
- data/test/deprecation_test.rb +27 -0
- data/test/deserialize_test.rb +104 -0
- data/test/errors_test.rb +165 -0
- data/test/feature_test.rb +65 -0
- data/test/fixtures/dry_error_messages.yml +44 -0
- data/test/form_option_test.rb +24 -0
- data/test/form_test.rb +57 -0
- data/test/from_test.rb +75 -0
- data/test/inherit_test.rb +119 -0
- data/test/module_test.rb +142 -0
- data/test/parse_pipeline_test.rb +15 -0
- data/test/populate_test.rb +270 -0
- data/test/populator_skip_test.rb +28 -0
- data/test/prepopulator_test.rb +112 -0
- data/test/read_only_test.rb +3 -0
- data/test/readable_test.rb +30 -0
- data/test/readonly_test.rb +14 -0
- data/test/reform_test.rb +223 -0
- data/test/save_test.rb +89 -0
- data/test/setup_test.rb +48 -0
- data/test/skip_if_test.rb +74 -0
- data/test/skip_setter_and_getter_test.rb +54 -0
- data/test/test_helper.rb +49 -0
- data/test/validate_test.rb +420 -0
- data/test/validation/dry_test.rb +60 -0
- data/test/validation/dry_validation_test.rb +352 -0
- data/test/validation/errors.yml +4 -0
- data/test/virtual_test.rb +24 -0
- data/test/writeable_test.rb +29 -0
- 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,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
|
data/reform.gemspec
ADDED
@@ -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
|
data/test/call_test.rb
ADDED
@@ -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
|