rasti-form 3.1.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d63a6c2410b6e7bbb259a0212442d2aad83fa53c4191856baf04466d3e9c73d0
4
- data.tar.gz: 1605263448bb732f3095b5fba45829ece27cecf159c2dbe39d1729dc3dc7981d
3
+ metadata.gz: f480a81f5d023100e579147654b5026d29f6d00d43099756ec80304e96fbd71d
4
+ data.tar.gz: 31c790f10607e20335262383955d7919d33aa6f54aa4fe0d7a9caa75c1bc973c
5
5
  SHA512:
6
- metadata.gz: e5df6a4b5dc7e8b1181a72ef0a1c9ba51b13b643c25c5cb6b8fef47369b181ee0c3598da1d4a22884dee6b5d4b9515f5638a52ae7a15b7cb9dd6c91c92863a76
7
- data.tar.gz: da10879fdf48628f72e4d353ee22b628e7520c2e2201918712c63401bb81cd6a1550916f2d1c93bf68249b03ed98c938266842713c32f9bc3d380b7f3e8834fa
6
+ metadata.gz: 83aaf5a613afd59afc64486b0228226dd9122e2096cdb38f8b8faad6fb07a19c8ac427bc677cc90bb804d63d2a39cef37b05f6215658ea1867d96c8e5ddf50ef
7
+ data.tar.gz: 55ba10cfb9a398999f928c5a803ad751c32989416c1bc2a164f105d436a46fe63ff75a47893e29436ec8cf85b93a51b49f09aa94e2a6ac8481ce6b628c39e4ab
data/lib/rasti/form.rb CHANGED
@@ -1,171 +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
21
- end
22
- end
23
-
24
- def inherited(subclass)
25
- subclass.instance_variable_set :@attributes, attributes.dup
26
- end
27
-
28
- def attribute(name, type, options={})
29
- attributes[name.to_sym] = options.merge(type: type)
30
- attr_reader name
31
- end
17
+ cast_attributes!
32
18
 
33
- def attributes
34
- @attributes ||= {}
35
- end
19
+ rescue Rasti::Model::UnexpectedAttributesError => ex
20
+ ex.attributes.each do |attr_name|
21
+ errors[attr_name] << 'unexpected attribute'
22
+ 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
30
 
46
- end
47
-
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}"
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'
134
52
  end
135
53
 
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
139
- end
140
-
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' unless errors.key? attribute
149
- end
150
-
151
- def assert_not_present(attribute)
152
- assert attribute, fetch(attribute).nil?, 'is present'
153
- end
154
-
155
- def assert_not_empty(attribute)
156
- if assert_present attribute
157
- value = fetch attribute
158
- 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(', ')}"
159
64
  end
160
65
  end
161
66
 
162
- def assert_time_range(attribute_from, attribute_to)
163
- assert attribute_from, public_send(attribute_from) <= public_send(attribute_to), 'invalid time range'
164
- end
165
-
166
- def assert_included_in(attribute, set)
167
- if assert_present attribute
168
- 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'
169
70
  end
170
71
  end
171
72
 
@@ -1,65 +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
- lines = ['Validation errors:']
57
-
58
- errors.each do |key, value|
59
- lines << "- #{key}: #{value}"
60
- end
13
+ private
61
14
 
62
- lines.join("\n")
15
+ def message_title
16
+ 'Validation errors:'
63
17
  end
64
18
 
65
19
  end
@@ -17,17 +17,16 @@ module Rasti
17
17
  end
18
18
 
19
19
  def assert(key, condition, message)
20
- return true if condition
21
-
22
- errors[key] << message
23
- false
20
+ errors[key] << message unless condition
21
+ condition
24
22
  end
25
23
 
26
24
  def assert_not_error(key)
27
25
  yield
28
26
  true
29
- rescue => error
30
- assert key, false, error.message
27
+ rescue => ex
28
+ errors[key] << ex.message
29
+ false
31
30
  end
32
31
 
33
32
  end
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,6 +14,7 @@ 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
19
  spec.add_development_dependency 'rake', '~> 12.0'
24
20
  spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
@@ -27,10 +23,4 @@ Gem::Specification.new do |spec|
27
23
  spec.add_development_dependency 'simplecov', '~> 0.12'
28
24
  spec.add_development_dependency 'coveralls', '~> 0.8'
29
25
  spec.add_development_dependency 'pry-nav', '~> 0.2'
30
-
31
- if RUBY_VERSION < '2'
32
- spec.add_development_dependency 'term-ansicolor', '~> 1.3.0'
33
- spec.add_development_dependency 'tins', '~> 1.6.0'
34
- spec.add_development_dependency 'json', '~> 1.8'
35
- end
36
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