rasti-form 2.2.0 → 4.0.0

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 (42) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +7 -10
  4. data/README.md +1 -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 -54
  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 -28
  24. data/lib/rasti/form/types/string.rb +0 -23
  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 -20
  39. data/spec/types/string_spec.rb +0 -16
  40. data/spec/types/symbol_spec.rb +0 -16
  41. data/spec/types/time_spec.rb +0 -25
  42. data/spec/types/uuid_spec.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c52204d2ee5fd0dec69e737cb54dcaadc0e1aefa
4
- data.tar.gz: '08a1332b2f45c622de662393a551cda4797f1ce8'
2
+ SHA256:
3
+ metadata.gz: f480a81f5d023100e579147654b5026d29f6d00d43099756ec80304e96fbd71d
4
+ data.tar.gz: 31c790f10607e20335262383955d7919d33aa6f54aa4fe0d7a9caa75c1bc973c
5
5
  SHA512:
6
- metadata.gz: d83646128669902f9d7bbfd367ac7b92f9a683258687675b15725f8fab3b0c6778f6fb1553f8c5b7149f419d9b2768f6a3004471ecb28b4b1463086bd6371af8
7
- data.tar.gz: 3600ad2b86770649044ce09d55025d6d1b51dda7e772f016c7bd7f134a634100b6e0d7bce45a3028071b51cf3762685109bfe6820cfc91e9aeb19a48c0f47fcc
6
+ metadata.gz: 83aaf5a613afd59afc64486b0228226dd9122e2096cdb38f8b8faad6fb07a19c8ac427bc677cc90bb804d63d2a39cef37b05f6215658ea1867d96c8e5ddf50ef
7
+ data.tar.gz: 55ba10cfb9a398999f928c5a803ad751c32989416c1bc2a164f105d436a46fe63ff75a47893e29436ec8cf85b93a51b49f09aa94e2a6ac8481ce6b628c39e4ab
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
4
  - 2.0
6
5
  - 2.1
7
6
  - 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
7
+ - 2.3
8
+ - 2.4
9
+ - 2.5
10
+ - 2.6
11
+ - 2.7
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
 
@@ -94,6 +93,7 @@ form.to # => 2016-10-28 00:00:00 -0300
94
93
  - Form
95
94
  - Hash
96
95
  - Integer
96
+ - IO
97
97
  - Regexp
98
98
  - String
99
99
  - Symbol
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 = '4.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', '~> 1.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