validates-structure 0.2.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fabdbd7d01016e5ded1f1cfe683c8e8f998ef94d
4
+ data.tar.gz: f2dc477eb6004acdac22e983e914b677f2929778
5
+ SHA512:
6
+ metadata.gz: 0d74f74ac673327c6f134370c86ecfb210f8056974773d44c3fc43c71d164e5939c74a43cd70723a3857aa307403fb6dd020ce9b1ba3937e7309ee4428db2fe2
7
+ data.tar.gz: 8b790670773538b6890270f04d4ae621a2fa5e4c0bf36d49e5661bb11177f7956ea0d15ff9d07b1897a367b4580b2bc79264a563780e389dfb050552ca578b25
@@ -0,0 +1,20 @@
1
+ Copyright 2013 PugglePay
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Validates Structure
2
+ ===================
3
+
4
+ Validates Structure allows you to easily validate hash-structures using ActiveModel::Validations.
5
+
6
+
7
+ Dependencies
8
+ ------------
9
+ The gem works for ActiveModel 4 and above.
10
+
11
+
12
+ Installation
13
+ ------------
14
+ Simply add the gem to your gemfile:
15
+
16
+ ```ruby
17
+ gem 'validates-structure'
18
+ ```
19
+
20
+ and make sure your activemodel version is at least 4.0.0:
21
+
22
+ ```ruby
23
+ gem 'activemodel', '>=4.0.0'
24
+ ```
25
+
26
+ Remember to
27
+
28
+ ```ruby
29
+ require 'validates-structure'
30
+ ```
31
+
32
+ at the top of the file when defining a new structure.
33
+
34
+ Canonical Usage Example
35
+ ------------
36
+
37
+ ```ruby
38
+ require 'validates-structure'
39
+
40
+ class MyCanonicalValidator < ValidatesStructure::Validator
41
+ key 'foo', Hash do
42
+ key 'bar', Array do
43
+ value Integer, allow_nil: true
44
+ end
45
+ key 'baz', String, format: /\A[0-f]\z/
46
+ end
47
+ end
48
+
49
+ validator = MyCanonicalValidator.new({foo: {bar: [1, 2, nil, 'invalid']}})
50
+ validator.valid?
51
+ # => false
52
+ puts validator.errors.full_messages
53
+ # => /foo/bar[3] has class "String" but should be a "Integer"
54
+ # => /foo/baz is invalid
55
+ # => /foo/baz must not be nil
56
+ ```
57
+
58
+ Quick facts about Validates Structure
59
+ -------------------------------------
60
+ * Validates Structure uses ActiveModel::Validations to validate your hash.
61
+ * Validates Structure automatically validates the type of each declared entry and will also give an error when undeclared keys are present.
62
+ * You can validate that a value are true or false by using the `Boolean` class (even though there are no Boolean class i Ruby).
63
+ * You can make compound hashes by setting a subclass to ValidatesStructure::Validator as the class in a key or value declaration.
64
+ * It doesn't matter if your structure uses symbols or strings as hash keys.
65
+ * Just like when validating attributes in a model, you can use your own custom validations.
66
+
67
+ Examples
68
+ --------
69
+
70
+ ### Minimal example
71
+
72
+ ```ruby
73
+ class MySimpleValidator < ValidatesStructure::Validator
74
+ key 'apa', Integer
75
+ end
76
+
77
+ MySimpleValidator.new(apa: 3).valid?
78
+ # => true
79
+ ```
80
+
81
+ ### Boolean example
82
+
83
+ ```ruby
84
+ class MyBooleanValidator < ValidatesStructure::Validator
85
+ key 'apa', Boolean
86
+ end
87
+
88
+ MyBooleanValidator.new(apa: true).valid?
89
+ # => true
90
+ ```
91
+
92
+ ### Nested example
93
+
94
+ ```ruby
95
+ class MyNestedValidator < ValidatesStructure::Validator
96
+ key 'apa', Hash do
97
+ key 'bepa', String, presence: true
98
+ end
99
+ end
100
+
101
+ validator = MyNestedValidator.new(apa: { bepa: "" })
102
+ validator.valid?
103
+ # => false
104
+ puts validator.errors.full_messages
105
+ # => /apa/bepa can't be blank
106
+ ```
107
+
108
+ ### Array example
109
+
110
+ ```ruby
111
+ class MyArrayValidator < ValidatesStructure::Validator
112
+ key 'apa', Hash do
113
+ key 'bepa', Array do
114
+ value Integer
115
+ end
116
+ end
117
+ end
118
+
119
+ validator = MyArrayValidator.new(apa: { bepa: [1, 2, "3"] })
120
+ validator.valid?
121
+ # => true
122
+ puts validator.errors.full_messages
123
+ # => /apa/bepa[2] has class "String" but should be a "Integer"
124
+ ```
125
+
126
+ ### Compound example
127
+
128
+ ```ruby
129
+ class MyInnerValidator < ValidatesStructure::Validator
130
+ key 'bepa', Integer
131
+ end
132
+
133
+ class MyOuterValidator < ValidatesStructure::Validator
134
+ key 'apa', MyInnerValidator
135
+ end
136
+
137
+ MyOuterValidator.new(apa: { bepa: 3 }).valid?
138
+ # => true
139
+ ```
140
+
141
+ ### Custom validator example
142
+
143
+ ```ruby
144
+ class OddValidator < ActiveModel::EachValidator
145
+ def validate_each(record, attribute, value)
146
+ record.errors.add attribute, "can't be even." if value.even?
147
+ end
148
+ end
149
+
150
+ class MyCustomValidator < ValidatesStructure::Validator
151
+ key 'apa', Integer, odd: true
152
+ end
153
+
154
+ MyCustomValidator.new(apa: 3).valid?
155
+ # => true
156
+ ```
157
+
158
+
159
+ Documentation
160
+ -------------
161
+ This documentation is about the modules, classes, methods and options of ValidatesStructure. For documentation on ActiveModel::Validations see [the ActiveModel documentation.](http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates)
162
+
163
+ ### ValidatesStructure::Validator
164
+
165
+ #### self.key(index, klass, validations={}, &block)
166
+ Sets up a requirement on the form ```'index' => klass``` that are validated with _validations_ and containing children on the form specified in _&block_.
167
+
168
+ **Parameters**
169
+
170
+ _index_ - The string or symbol by which to retrieve the value
171
+
172
+ _klass_ - The required class of the value. If klass is a subclass of ValidatesStructure::Validator then the value is validated as specified in its definition.
173
+
174
+ _validations_ - A hash with [ActiveModel:Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html) on the same format as for the [validates](http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates) method.
175
+
176
+ _&block_ - A block of nested _key_ and/or _value_ declarations. Only applicable if klass is an Array or Hashe.
177
+
178
+
179
+ #### self.value(klass, validations={},&block)
180
+ Sets up a requirement like self.key but without an index. Useful for structures that are accessed by a numeric index such as Arrays.
181
+
182
+ **Parameters**
183
+
184
+ _klass_ - The required class of the value. If klass is a subclass of ValidatesStructure::Validator then the value is validated as specified in its definition.
185
+
186
+ _validations_ - A hash with [ActiveModel:Validations](http://api.rubyonrails.org/classes/ActiveModel/Validations/HelperMethods.html) on the same format as for the [validates](http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates) method.
187
+
188
+ _&block_ - A block of nested _key_ and/or _value_ declarations. Only applicable if klass is an Array or Hashe.
189
+
190
+
191
+ Some History
192
+ ------------
193
+ This project was initiated by the good fellows at [PugglePay](https://github.com/PugglePay) who felt there should be some easy, familiar way of validating their incoming json requests and wanted to share their solution with the world.
194
+
195
+
196
+ Contributing
197
+ ------------
198
+ 1. Fork the project
199
+ 2. Create a feature branch (git checkout -b my-new-feature)
200
+ 3. Commit your changes (git commit -am 'Add some feature')
201
+ 4. Push branch to remote (git push origin my-new-feature)
202
+ 5. Make a Pull Request
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ValidatesStructure'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+
24
+
25
+
26
+ Bundler::GemHelper.install_tasks
27
+
28
+ require 'rake/testtask'
29
+
30
+ Rake::TestTask.new(:test) do |t|
31
+ t.libs << 'lib'
32
+ t.libs << 'test'
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = false
35
+ end
36
+
37
+
38
+ task :default => :test
@@ -0,0 +1,206 @@
1
+ require 'active_model'
2
+ require 'securerandom'
3
+
4
+ module ValidatesStructure
5
+
6
+ class Validator
7
+ include ActiveModel::Validations
8
+
9
+ class_attribute :keys, instance_writer: false
10
+ class_attribute :values, instance_writer: false
11
+ class_attribute :nested_validators, instance_writer: false
12
+
13
+ attr_accessor :keys
14
+
15
+ def initialize(hash)
16
+ self.class.initialize_class_attributes
17
+ self.keys = []
18
+
19
+ return unless hash.is_a?(Hash)
20
+
21
+ hash.each do |key, value|
22
+ self.keys << key.to_s
23
+ send "#{key}=", value if respond_to? "#{key}="
24
+ end
25
+ end
26
+
27
+ def self.key(key, klass, validations={}, &block)
28
+ initialize_class_attributes
29
+ key = key.to_s
30
+
31
+ if klass.class != Class
32
+ raise ArgumentError.new("Types must be given as classes")
33
+ end
34
+ unless @nested_array_key.nil?
35
+ raise ArgumentError.new("Key can not only appear within a block of a key of type Array")
36
+ end
37
+ if keys.include?(key)
38
+ raise ArgumentError.new("Dublicate key \"#{key}\"")
39
+ end
40
+
41
+ keys << key
42
+ attr_accessor key
43
+
44
+ validations = prepare_validations(klass, validations, &block)
45
+ validates key, validations
46
+
47
+ nest_dsl(klass, key, &block)
48
+ end
49
+
50
+ def self.value(klass, validations={}, &block)
51
+ initialize_class_attributes
52
+
53
+ if klass.class != Class
54
+ raise ArgumentError.new("Types must be given as classes")
55
+ end
56
+ if @nested_array_key.nil?
57
+ raise ArgumentError.new("Value can only appear within the block of a key of Array type")
58
+ end
59
+ if values[@nested_array_key]
60
+ raise ArgumentError.new("Value can only appear once within a block")
61
+ end
62
+ values[@nested_array_key] = klass
63
+
64
+ validations = prepare_validations(klass, validations, &block)
65
+ validates @nested_array_key, enumerable: validations
66
+ nest_dsl(klass, @nested_array_key, &block)
67
+ end
68
+
69
+ def self.human_attribute_name(attr, *)
70
+ "/#{attr}"
71
+ end
72
+
73
+ validate do
74
+ (keys - self.class.keys).each do |key|
75
+ errors.add(key, "is not a known key")
76
+ end
77
+ end
78
+
79
+ protected
80
+
81
+ def self.initialize_class_attributes
82
+ self.keys ||= []
83
+ self.values ||= {}
84
+ self.nested_validators ||= {}
85
+ end
86
+
87
+ def self.prepare_validations(klass, validations, &block)
88
+ validations = validations.dup
89
+ validations[:klass] = { klass: (klass.ancestors.include?(Validator) ? Hash : klass) }
90
+ unless validations[:allow_nil] == true || validations[:allow_blank] == true
91
+ if klass == String
92
+ validations[:not_blank] = true
93
+ else
94
+ validations[:not_nil] = true
95
+ end
96
+ end
97
+ validations[:nested] = true if block_given? || klass.ancestors.include?(Validator)
98
+ validations
99
+ end
100
+
101
+ def self.nest_dsl(klass, key, &block)
102
+ if klass.ancestors.include?(Validator)
103
+ self.nested_validators[key] = klass
104
+ elsif block_given?
105
+ case
106
+ when klass == Hash
107
+ klass_name = "Annonimous#{ key.to_s.camelize }Validator#{SecureRandom.uuid.tr('-','')}"
108
+ const_set(klass_name, Class.new(self.superclass))
109
+ validator = const_get(klass_name)
110
+ validator.instance_eval(&block)
111
+ self.nested_validators[key] = validator
112
+ when klass == Array
113
+ @nested_array_key = key
114
+ yield
115
+ @nested_array_key = nil
116
+ else
117
+ raise ArgumentError.new("Didn't expect a block for the type \"#{klass}\"")
118
+ end
119
+ end
120
+ end
121
+
122
+ def self.model_name
123
+ ActiveModel::Name.new(self, nil, "temp")
124
+ end
125
+
126
+ class Boolean
127
+ end
128
+
129
+ class KlassValidator < ActiveModel::EachValidator
130
+ def validate_each(record, attribute, value)
131
+ return if value.nil?
132
+ klass = options[:klass]
133
+ if klass == Boolean
134
+ unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
135
+ record.errors.add attribute, "has class \"#{value.class}\" but should be boolean"
136
+ end
137
+ elsif !(value.is_a?(klass))
138
+ record.errors.add attribute, "has class \"#{value.class}\" but should be a \"#{klass}\""
139
+ end
140
+ end
141
+ end
142
+
143
+ class NotNilValidator < ActiveModel::EachValidator
144
+ def validate_each(record, attribute, value)
145
+ if value.nil?
146
+ record.errors.add attribute, "must not be nil"
147
+ end
148
+ end
149
+ end
150
+
151
+ class NotBlankValidator < ActiveModel::EachValidator
152
+ def validate_each(record, attribute, value)
153
+ if value.blank?
154
+ record.errors.add attribute, "must not be empty"
155
+ end
156
+ end
157
+ end
158
+
159
+ class NestedValidator < ActiveModel::EachValidator
160
+ def validate_each(record, attribute, value)
161
+ return unless value.is_a?(Hash)
162
+ return if record.errors.keys.map(&:to_s).include?(attribute)
163
+
164
+ validator = record.class.nested_validators[attribute]
165
+ return if validator.nil?
166
+
167
+ nested = validator.new(value)
168
+ nested.valid?
169
+ nested.errors.each do |nested_attribute, message|
170
+ record.errors.add("#{attribute}/#{nested_attribute}", message)
171
+ end
172
+ end
173
+ end
174
+
175
+ class EnumerableValidator < ActiveModel::EachValidator
176
+ # Validates each value in an enumerable class using ActiveModel validations.
177
+ # Adapted from a snippet by Milovan Zogovic (http://stackoverflow.com/a/12744945)
178
+ def validate_each(record, attribute, values)
179
+ return unless values.respond_to?(:each_with_index)
180
+ values.each_with_index do |value, index|
181
+ options.each do |key, args|
182
+ validator_options = { attributes: attribute }
183
+ validator_options.merge!(args) if args.is_a?(Hash)
184
+
185
+ next if value.nil? && validator_options[:allow_nil]
186
+ next if value.blank? && validator_options[:allow_blank]
187
+ next if key.to_s == "allow_nil"
188
+ next if key.to_s == "allow_blank"
189
+
190
+ validator_class_name = "#{key.to_s.camelize}Validator"
191
+ validator_class = self.class.parent.const_get(validator_class_name)
192
+ validator = validator_class.new(validator_options)
193
+
194
+ # TODO: There should be a better way!
195
+ tmp_record = record.dup
196
+ validator.validate_each(tmp_record, attribute, value)
197
+ tmp_record.errors.each do |nested_attribute, error|
198
+ indexed_attribute = nested_attribute.to_s.sub(/^#{attribute}/, "#{attribute}[#{index}]")
199
+ record.errors.add(indexed_attribute, error)
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,3 @@
1
+ module ValidatesStructure
2
+ VERSION = "0.2.0"
3
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: validates-structure
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Magnus Rex
8
+ - Daniel Ström
9
+ - Jean-Louis Giordano
10
+ - Patrik Kårlin
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2014-07-11 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activemodel
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: 4.1.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 4.1.1
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ - !ruby/object:Gem::Dependency
45
+ name: debugger
46
+ requirement: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ type: :development
52
+ prerelease: false
53
+ version_requirements: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - '>='
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard
60
+ requirement: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ - !ruby/object:Gem::Dependency
73
+ name: guard-rspec
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ type: :development
80
+ prerelease: false
81
+ version_requirements: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ description: Uses the power and familiarity of ActiveModel::Validations to validate
87
+ hash structures. Designed for detecting and providing feedback on bad requests to
88
+ your RESTful Web Service.
89
+ email:
90
+ - dev@pugglepay.com
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - lib/validates-structure/version.rb
96
+ - lib/validates-structure.rb
97
+ - LICENSE.txt
98
+ - Rakefile
99
+ - README.md
100
+ homepage: https://github.com/PugglePay/validates-structure
101
+ licenses: []
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.0.3
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: ActiveModel validations for nested structures like params.
123
+ test_files: []
124
+ has_rdoc: