rasti-form 3.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +8 -11
  4. data/README.md +0 -1
  5. data/lib/rasti/form.rb +34 -129
  6. data/lib/rasti/form/errors.rb +7 -47
  7. data/lib/rasti/form/validable.rb +9 -3
  8. data/rasti-form.gemspec +3 -14
  9. data/spec/coverage_helper.rb +1 -3
  10. data/spec/minitest_helper.rb +1 -7
  11. data/spec/validations_spec.rb +301 -0
  12. metadata +11 -56
  13. data/lib/rasti/form/castable.rb +0 -25
  14. data/lib/rasti/form/formatable.rb +0 -19
  15. data/lib/rasti/form/types/array.rb +0 -54
  16. data/lib/rasti/form/types/boolean.rb +0 -42
  17. data/lib/rasti/form/types/enum.rb +0 -48
  18. data/lib/rasti/form/types/float.rb +0 -33
  19. data/lib/rasti/form/types/form.rb +0 -40
  20. data/lib/rasti/form/types/hash.rb +0 -39
  21. data/lib/rasti/form/types/integer.rb +0 -21
  22. data/lib/rasti/form/types/io.rb +0 -23
  23. data/lib/rasti/form/types/regexp.rb +0 -23
  24. data/lib/rasti/form/types/string.rb +0 -44
  25. data/lib/rasti/form/types/symbol.rb +0 -23
  26. data/lib/rasti/form/types/time.rb +0 -40
  27. data/lib/rasti/form/types/uuid.rb +0 -19
  28. data/lib/rasti/form/version.rb +0 -5
  29. data/spec/form_spec.rb +0 -350
  30. data/spec/types/array_spec.rb +0 -54
  31. data/spec/types/boolean_spec.rb +0 -24
  32. data/spec/types/enum_spec.rb +0 -28
  33. data/spec/types/float_spec.rb +0 -18
  34. data/spec/types/form_spec.rb +0 -41
  35. data/spec/types/hash_spec.rb +0 -20
  36. data/spec/types/integer_spec.rb +0 -18
  37. data/spec/types/io_spec.rb +0 -18
  38. data/spec/types/regexp_spec.rb +0 -18
  39. data/spec/types/string_formatted_spec.rb +0 -20
  40. data/spec/types/string_spec.rb +0 -16
  41. data/spec/types/symbol_spec.rb +0 -16
  42. data/spec/types/time_spec.rb +0 -25
  43. data/spec/types/uuid_spec.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 9dd0424409c0c7232782c40cde8dbf7fa84752d3
4
- data.tar.gz: a7c8be9e98dcc9e6d52482469486ba643a0ffa33
2
+ SHA256:
3
+ metadata.gz: 4c0a64e1d1120b8c55b78a969c8d53f2e01dc152bf7c54da19950024ae593e88
4
+ data.tar.gz: 3045f978fe98478268719e4d73c76d81b0d4e81ef57cf5559ef9fba6add3b0f7
5
5
  SHA512:
6
- metadata.gz: a2fb881aec7bca876dd58a3c2bf3e6dcca8ac3f13777437992f4e359d07856c9a7075e4b4c51882f41cb30ffacb8ebf85616632898efbfa02ccd7abea6a7e81c
7
- data.tar.gz: c77fed8829801cae565a567f3303b8c4271e5a9b3b79c7ac2f04475e48a9bc6d884e80e2253903a0374a4848d635cf0576a1b191044c850f6cf9aea70a373416
6
+ metadata.gz: 75b3b3c7a0e7f0293c8d4d1b345700fb44f925e17567f93d1d699579338ea7f61952181c5b18de59b0f17f09c08fca1655120b9c51799a4e6d36d58e213e17b0
7
+ data.tar.gz: 906759fe1adec8fb89b55ba07e8d37c0e8f2bfbcef9b8eb693b7552a294ffe6eccae1ab840a7286f2b9113397fd89049f7de597a7df70c041685385cc58ddcda
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-2.4.0
1
+ ruby-2.5.7
data/.travis.yml CHANGED
@@ -1,15 +1,15 @@
1
1
  language: ruby
2
2
 
3
3
  rvm:
4
- - 1.9.3
5
- - 2.0
6
4
  - 2.1
7
5
  - 2.2
8
- - 2.3.0
9
- - 2.4.0
10
- - 2.5.0
11
- - jruby-1.7.25
12
- - jruby-9.1.7.0
6
+ - 2.3
7
+ - 2.4
8
+ - 2.5
9
+ - 2.6
10
+ - 2.7
11
+ - 3.0
12
+ - jruby-9.2.9.0
13
13
  - ruby-head
14
14
  - jruby-head
15
15
 
@@ -17,7 +17,4 @@ matrix:
17
17
  fast_finish: true
18
18
  allow_failures:
19
19
  - rvm: ruby-head
20
- - rvm: jruby-head
21
-
22
- before_install:
23
- - gem install bundler
20
+ - rvm: jruby-head
data/README.md CHANGED
@@ -4,7 +4,6 @@
4
4
  [![Build Status](https://travis-ci.org/gabynaiman/rasti-form.svg?branch=master)](https://travis-ci.org/gabynaiman/rasti-form)
5
5
  [![Coverage Status](https://coveralls.io/repos/github/gabynaiman/rasti-form/badge.svg?branch=master)](https://coveralls.io/github/gabynaiman/rasti-form?branch=master)
6
6
  [![Code Climate](https://codeclimate.com/github/gabynaiman/rasti-form.svg)](https://codeclimate.com/github/gabynaiman/rasti-form)
7
- [![Dependency Status](https://gemnasium.com/gabynaiman/rasti-form.svg)](https://gemnasium.com/gabynaiman/rasti-form)
8
7
 
9
8
  Forms validations and type casting
10
9
 
data/lib/rasti/form.rb CHANGED
@@ -1,167 +1,72 @@
1
- require 'json'
1
+ require 'rasti-model'
2
2
  require 'multi_require'
3
3
 
4
4
  module Rasti
5
- class Form
5
+ class Form < Rasti::Model
6
6
 
7
7
  extend MultiRequire
8
8
 
9
9
  require_relative_pattern 'form/*'
10
- require_relative_pattern 'form/types/*'
11
10
 
12
11
  include Validable
13
12
 
14
- class << self
13
+ def initialize(attributes={})
14
+ begin
15
+ super attributes
15
16
 
16
- def [](attributes)
17
- Class.new(self) do
18
- attributes.each do |name, type, options={}|
19
- attribute name, type, options
20
- end
17
+ cast_attributes!
21
18
 
22
- def self.inherited(subclass)
23
- subclass.instance_variable_set :@attributes, attributes.dup
24
- end
19
+ rescue Rasti::Model::UnexpectedAttributesError => ex
20
+ ex.attributes.each do |attr_name|
21
+ errors[attr_name] << 'unexpected attribute'
25
22
  end
26
- end
27
-
28
- def attribute(name, type, options={})
29
- attributes[name.to_sym] = options.merge(type: type)
30
- attr_reader name
31
- end
32
-
33
- def attributes
34
- @attributes ||= {}
35
- end
36
23
 
37
- def attribute_names
38
- attributes.keys
39
- end
24
+ rescue Rasti::Types::CompoundError => ex
25
+ ex.errors.each do |key, messages|
26
+ errors[key] += messages
27
+ end
40
28
 
41
- def to_s
42
- "#{name || self.superclass.name}[#{attribute_names.map(&:inspect).join(', ')}]"
43
29
  end
44
- alias_method :inspect, :to_s
45
-
46
- end
47
30
 
48
- def initialize(attrs={})
49
- assign_attributes attrs
50
- set_defaults
51
31
  validate!
52
32
  end
53
33
 
54
- def to_s
55
- "#<#{self.class.name || self.class.superclass.name}[#{to_h.map { |n,v| "#{n}: #{v.inspect}" }.join(', ')}]>"
56
- end
57
- alias_method :inspect, :to_s
58
-
59
- def attributes(options={})
60
- attributes_filter = {only: assigned_attribute_names, except: []}.merge(options)
61
- (attributes_filter[:only] - attributes_filter[:except]).each_with_object({}) do |name, hash|
62
- hash[name] = serialize(read_attribute(name))
63
- end
64
- end
65
-
66
- def to_h
67
- attributes
68
- end
69
-
70
- def assigned?(name)
71
- assigned_attribute_names.include? name
72
- end
73
-
74
- def ==(other)
75
- other.kind_of?(self.class) && other.attributes == attributes
76
- end
77
-
78
- def eql?(other)
79
- other.instance_of?(self.class) && other.attributes == attributes
80
- end
81
-
82
- def hash
83
- [self.class, attributes].hash
34
+ def assigned?(attr_name)
35
+ assigned_attribute? attr_name.to_sym
84
36
  end
85
37
 
86
38
  private
87
39
 
88
- def assign_attributes(attrs={})
89
- attrs.each do |name, value|
90
- attr_name = name.to_sym
91
- begin
92
- if self.class.attributes.key? attr_name
93
- write_attribute attr_name, value
94
- else
95
- errors[attr_name] << 'unexpected attribute'
96
- end
97
-
98
- rescue CastError => error
99
- errors[attr_name] << error.message
100
-
101
- rescue MultiCastError, ValidationError => error
102
- error.errors.each do |inner_name, inner_errors|
103
- inner_errors.each { |message| errors["#{attr_name}.#{inner_name}"] << message }
104
- end
105
- end
106
- end
107
- end
108
-
109
- def set_defaults
110
- (self.class.attribute_names - attributes.keys).each do |name|
111
- if self.class.attributes[name].key? :default
112
- value = self.class.attributes[name][:default]
113
- write_attribute name, value.is_a?(Proc) ? value.call(self) : value
114
- end
115
- end
116
- end
117
-
118
- def assigned_attribute_names
119
- self.class.attribute_names & instance_variables.map { |v| v.to_s[1..-1].to_sym }
120
- end
121
-
122
- def serialize(value)
123
- if value.kind_of? Array
124
- value.map { |v| serialize v }
125
- elsif value.kind_of? Form
126
- value.attributes
127
- else
128
- value
40
+ def assert_present(attr_name)
41
+ if !errors.key?(attr_name)
42
+ assert attr_name, assigned?(attr_name) && !public_send(attr_name).nil?, 'not present'
129
43
  end
44
+ rescue Types::Error
45
+ assert attr_name, false, 'not present'
130
46
  end
131
47
 
132
- def read_attribute(name)
133
- instance_variable_get "@#{name}"
134
- end
135
-
136
- def write_attribute(name, value)
137
- typed_value = value.nil? ? nil : self.class.attributes[name][:type].cast(value)
138
- instance_variable_set "@#{name}", typed_value
48
+ def assert_not_present(attr_name)
49
+ assert attr_name, !assigned?(attr_name) || public_send(attr_name).nil?, 'is present'
50
+ rescue Types::Error
51
+ assert attr_name, false, 'is present'
139
52
  end
140
53
 
141
- def fetch(attribute)
142
- attribute.to_s.split('.').inject(self) do |target, attr_name|
143
- target.nil? ? nil : target.public_send(attr_name)
54
+ def assert_not_empty(attr_name)
55
+ if assert_present attr_name
56
+ value = public_send attr_name
57
+ assert attr_name, value.is_a?(String) ? !value.strip.empty? : !value.empty?, 'is empty'
144
58
  end
145
59
  end
146
60
 
147
- def assert_present(attribute)
148
- assert attribute, !fetch(attribute).nil?, 'not present'
149
- end
150
-
151
- def assert_not_empty(attribute)
152
- if assert_present attribute
153
- value = fetch attribute
154
- assert attribute, value.is_a?(String) ? !value.strip.empty? : !value.empty?, 'is empty'
61
+ def assert_included_in(attr_name, set)
62
+ if assert_present attr_name
63
+ assert attr_name, set.include?(public_send(attr_name)), "not included in #{set.map(&:inspect).join(', ')}"
155
64
  end
156
65
  end
157
66
 
158
- def assert_time_range(attribute_from, attribute_to)
159
- assert attribute_from, public_send(attribute_from) <= public_send(attribute_to), 'invalid time range'
160
- end
161
-
162
- def assert_included_in(attribute, set)
163
- if assert_present attribute
164
- assert attribute, set.include?(fetch(attribute)), "not included in #{set.map { |e| e.is_a?(::String) ? "'#{e}'" : e.inspect }.join(', ')}"
67
+ def assert_range(attr_name_from, attr_name_to)
68
+ if assert_present(attr_name_from) && assert_present(attr_name_to)
69
+ assert attr_name_from, public_send(attr_name_from) <= public_send(attr_name_to), 'invalid range'
165
70
  end
166
71
  end
167
72
 
@@ -1,59 +1,19 @@
1
1
  module Rasti
2
2
  class Form
3
-
4
- class CastError < StandardError
5
3
 
6
- attr_reader :type, :value
4
+ class ValidationError < Rasti::Types::CompoundError
7
5
 
8
- def initialize(type, value)
9
- @type = type
10
- @value = value
11
- end
12
-
13
- def message
14
- "Invalid cast: #{display_value} -> #{type}"
15
- end
16
-
17
- private
18
-
19
- def display_value
20
- value.is_a?(::String) ? "'#{value}'" : value.inspect
21
- end
22
-
23
- end
24
-
25
-
26
- class MultiCastError < StandardError
6
+ attr_reader :scope
27
7
 
28
- attr_reader :type, :value, :errors
29
-
30
- def initialize(type, value, errors)
31
- @type = type
32
- @value = value
33
- @errors = errors
34
- end
35
-
36
- def message
37
- "Invalid cast: #{display_value} -> #{type} - #{JSON.dump(errors)}"
38
- end
39
-
40
- def display_value
41
- value.is_a?(::String) ? "'#{value}'" : value.inspect
42
- end
43
-
44
- end
45
-
46
- class ValidationError < StandardError
47
-
48
- attr_reader :scope, :errors
49
-
50
8
  def initialize(scope, errors)
51
9
  @scope = scope
52
- @errors = errors
10
+ super errors
53
11
  end
54
12
 
55
- def message
56
- "Validation error: #{scope} #{JSON.dump(errors)}"
13
+ private
14
+
15
+ def message_title
16
+ 'Validation errors:'
57
17
  end
58
18
 
59
19
  end
@@ -17,9 +17,15 @@ module Rasti
17
17
  end
18
18
 
19
19
  def assert(key, condition, message)
20
- return true if condition
21
-
22
- errors[key] << message
20
+ errors[key] << message unless condition
21
+ condition
22
+ end
23
+
24
+ def assert_not_error(key)
25
+ yield
26
+ true
27
+ rescue => ex
28
+ errors[key] << ex.message
23
29
  false
24
30
  end
25
31
 
data/rasti-form.gemspec CHANGED
@@ -1,11 +1,6 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'rasti/form/version'
5
-
6
1
  Gem::Specification.new do |spec|
7
2
  spec.name = 'rasti-form'
8
- spec.version = Rasti::Form::VERSION
3
+ spec.version = '5.0.0'
9
4
  spec.authors = ['Gabriel Naiman']
10
5
  spec.email = ['gabynaiman@gmail.com']
11
6
  spec.summary = 'Forms validations and type casting'
@@ -19,19 +14,13 @@ Gem::Specification.new do |spec|
19
14
  spec.require_paths = ['lib']
20
15
 
21
16
  spec.add_runtime_dependency 'multi_require', '~> 1.0'
17
+ spec.add_runtime_dependency 'rasti-model', '~> 2.0'
22
18
 
23
- spec.add_development_dependency 'bundler', '~> 1.12'
24
- spec.add_development_dependency 'rake', '~> 11.0'
19
+ spec.add_development_dependency 'rake', '~> 12.0'
25
20
  spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
26
21
  spec.add_development_dependency 'minitest-colorin', '~> 0.1'
27
22
  spec.add_development_dependency 'minitest-line', '~> 0.6'
28
23
  spec.add_development_dependency 'simplecov', '~> 0.12'
29
24
  spec.add_development_dependency 'coveralls', '~> 0.8'
30
25
  spec.add_development_dependency 'pry-nav', '~> 0.2'
31
-
32
- if RUBY_VERSION < '2'
33
- spec.add_development_dependency 'term-ansicolor', '~> 1.3.0'
34
- spec.add_development_dependency 'tins', '~> 1.6.0'
35
- spec.add_development_dependency 'json', '~> 1.8'
36
- end
37
26
  end
@@ -2,6 +2,4 @@ require 'simplecov'
2
2
  require 'coveralls'
3
3
 
4
4
  SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
5
- SimpleCov.start do
6
- add_group 'Types', 'lib/rasti/form/types'
7
- end
5
+ SimpleCov.start
@@ -4,10 +4,4 @@ require 'minitest/colorin'
4
4
  require 'pry-nav'
5
5
  require 'rasti-form'
6
6
 
7
- module Minitest
8
- class Test
9
- def as_string(value)
10
- value.is_a?(::String) ? "'#{value}'" : value.inspect
11
- end
12
- end
13
- end
7
+ T = Rasti::Types
@@ -0,0 +1,301 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Rasti::Form, 'Validations' do
4
+
5
+ def build_form(&block)
6
+ Class.new(Rasti::Form) do
7
+ class_eval(&block)
8
+ end
9
+ end
10
+
11
+ def assert_validation_error(expected_errors, &block)
12
+ error = proc { block.call }.must_raise Rasti::Form::ValidationError
13
+ error.errors.must_equal expected_errors
14
+ end
15
+
16
+ it 'Validation error message' do
17
+ scope = Object.new
18
+ errors = {
19
+ x: ['not present'],
20
+ y: ['error 1', 'error 2']
21
+ }
22
+
23
+ error = Rasti::Form::ValidationError.new scope, errors
24
+
25
+ error.scope.must_equal scope
26
+ error.errors.must_equal errors
27
+ error.message.must_equal "Validation errors:\n- x: [\"not present\"]\n- y: [\"error 1\", \"error 2\"]"
28
+ end
29
+
30
+ it 'Invalid attributes' do
31
+ assert_validation_error(z: ['unexpected attribute']) do
32
+ Rasti::Form[:x, :y].new z: 3
33
+ end
34
+ end
35
+
36
+ it 'Not error' do
37
+ form = build_form do
38
+ attribute :text, T::String
39
+
40
+ def validate
41
+ assert_not_error :text do
42
+ raise 'invalid text' unless assigned? :text
43
+ end
44
+ end
45
+ end
46
+
47
+ proc { form.new text: 'text' }.must_be_silent
48
+
49
+ assert_validation_error(text: ['invalid text']) do
50
+ form.new
51
+ end
52
+ end
53
+
54
+ describe 'Present' do
55
+
56
+ let :form do
57
+ build_form do
58
+ attribute :number, T::Integer
59
+
60
+ def validate
61
+ assert_present :number
62
+ end
63
+ end
64
+ end
65
+
66
+ it 'Success' do
67
+ proc { form.new number: 1 }.must_be_silent
68
+ end
69
+
70
+ it 'Not assigned' do
71
+ assert_validation_error(number: ['not present']) do
72
+ form.new
73
+ end
74
+ end
75
+
76
+ it 'Nil' do
77
+ assert_validation_error(number: ['not present']) do
78
+ form.new number: nil
79
+ end
80
+ end
81
+
82
+ it 'Invalid cast' do
83
+ assert_validation_error(number: ['Invalid cast: true -> Rasti::Types::Integer']) do
84
+ form.new number: true
85
+ end
86
+ end
87
+
88
+ it 'Invalid nested cast' do
89
+ range = build_form do
90
+ attribute :min, T::Integer
91
+ attribute :max, T::Integer
92
+
93
+ def validate
94
+ assert_range :min, :max
95
+ end
96
+ end
97
+
98
+ form = build_form do
99
+ attribute :range, T::Model[range]
100
+
101
+ def validate
102
+ assert_present :range
103
+ end
104
+ end
105
+
106
+ assert_validation_error('range.max' => ['not present'], range: ['not present']) do
107
+ form.new range: {min: 1}
108
+ end
109
+ end
110
+
111
+ it 'With default' do
112
+ form = build_form do
113
+ attribute :number, T::Integer, default: 1
114
+
115
+ def validate
116
+ assert_present :number
117
+ end
118
+ end
119
+
120
+ proc { form.new }.must_be_silent
121
+
122
+ assert_validation_error(number: ['not present']) do
123
+ form.new number: nil
124
+ end
125
+ end
126
+
127
+ end
128
+
129
+ describe 'Not present' do
130
+
131
+ let :form do
132
+ range = build_form do
133
+ attribute :min, T::Integer
134
+ attribute :max, T::Integer
135
+
136
+ def validate
137
+ assert_range :min, :max
138
+ end
139
+ end
140
+
141
+ build_form do
142
+ attribute :range, T::Model[range]
143
+
144
+ def validate
145
+ assert_not_present :range
146
+ end
147
+ end
148
+ end
149
+
150
+ it 'Success not assigned' do
151
+ proc { form.new }.must_be_silent
152
+ end
153
+
154
+ it 'Success with nil' do
155
+ proc { form.new range: nil }.must_be_silent
156
+ end
157
+
158
+ it 'Assigned' do
159
+ assert_validation_error(range: ['is present']) do
160
+ form.new range: {min: 1, max: 2}
161
+ end
162
+ end
163
+
164
+ it 'Invalid nested cast' do
165
+ assert_validation_error('range.max' => ['not present'], range: ['is present']) do
166
+ form.new range: {min: 1}
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ describe 'Not empty' do
173
+
174
+ it 'Must be present' do
175
+ form = build_form do
176
+ attribute :text, T::String
177
+
178
+ def validate
179
+ assert_not_empty :text
180
+ end
181
+ end
182
+
183
+ proc { form.new text: 'text' }.must_be_silent
184
+
185
+ assert_validation_error(text: ['not present']) do
186
+ form.new text: nil
187
+ end
188
+
189
+ assert_validation_error(text: ['not present']) do
190
+ form.new
191
+ end
192
+ end
193
+
194
+ it 'String' do
195
+ form = build_form do
196
+ attribute :text, T::String
197
+
198
+ def validate
199
+ assert_not_empty :text
200
+ end
201
+ end
202
+
203
+ proc { form.new text: 'text' }.must_be_silent
204
+
205
+ assert_validation_error(text: ['is empty']) do
206
+ form.new text: ' '
207
+ end
208
+ end
209
+
210
+ it 'Array' do
211
+ form = build_form do
212
+ attribute :array, T::Array[T::String]
213
+
214
+ def validate
215
+ assert_not_empty :array
216
+ end
217
+ end
218
+
219
+ proc { form.new array: ['text'] }.must_be_silent
220
+
221
+ assert_validation_error(array: ['is empty']) do
222
+ form.new array: []
223
+ end
224
+ end
225
+
226
+ it 'Hash' do
227
+ form = build_form do
228
+ attribute :hash, T::Hash[T::String, T::String]
229
+
230
+ def validate
231
+ assert_not_empty :hash
232
+ end
233
+ end
234
+
235
+ proc { form.new hash: {key: 'value'} }.must_be_silent
236
+
237
+ assert_validation_error(hash: ['is empty']) do
238
+ form.new hash: {}
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+ it 'Included in values list' do
245
+ form = build_form do
246
+ attribute :text, T::String
247
+
248
+ def validate
249
+ assert_included_in :text, %w(value_1 value_2)
250
+ end
251
+ end
252
+
253
+ proc { form.new text: 'value_1' }.must_be_silent
254
+
255
+ assert_validation_error(text: ['not included in "value_1", "value_2"']) do
256
+ form.new text: 'xyz'
257
+ end
258
+ end
259
+
260
+ it 'Time range' do
261
+ form = build_form do
262
+ attribute :from, T::Time['%Y-%m-%d %H:%M:%S']
263
+ attribute :to, T::Time['%Y-%m-%d %H:%M:%S']
264
+
265
+ def validate
266
+ assert_range :from, :to
267
+ end
268
+ end
269
+
270
+ from = '2018-01-01 03:10:00'
271
+ to = '2018-01-01 15:30:00'
272
+
273
+ proc { form.new from: from, to: to }.must_be_silent
274
+
275
+ assert_validation_error(from: ['invalid range']) do
276
+ form.new from: to, to: from
277
+ end
278
+ end
279
+
280
+ it 'Nested validation' do
281
+ range = build_form do
282
+ attribute :min, T::Integer
283
+ attribute :max, T::Integer
284
+
285
+ def validate
286
+ assert_range :min, :max
287
+ end
288
+ end
289
+
290
+ form = build_form do
291
+ attribute :range, T::Model[range]
292
+ end
293
+
294
+ proc { form.new range: {min: 1, max: 2} }.must_be_silent
295
+
296
+ assert_validation_error('range.min' => ['invalid range']) do
297
+ form.new range: {min: 2, max: 1}
298
+ end
299
+ end
300
+
301
+ end