classy_hash 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9462ba9bbda68a8f041537f21350db0341030dfd
4
+ data.tar.gz: bc4645ff1e3da6c864c96de07930130b32723ede
5
+ SHA512:
6
+ metadata.gz: 1cd58d6a703ddbd622c412c3d826b048a188af021deff0e9c0e02e53568315103473889ffcc5d1657da207a9f1184886e47758bdf7ab24e904f3856cae59a9e8
7
+ data.tar.gz: df4c9a612073aa3a437b9d9b003f8394dfd14e6a9ffbc843701ae21d5ae78c4f97cd7330b9f3391b8ac10f656778f3eeaf3f3251e8217910c33646d5a47ac0b7
@@ -0,0 +1,163 @@
1
+ # Classy Hash: Keep Your Hashes Classy
2
+ # Created May 2014 by Mike Bourgeous, DeseretBook.com
3
+ # Copyright (C)2014 Deseret Book
4
+ # See LICENSE and README.md for details.
5
+
6
+ # This module contains the ClassyHash methods for making sure Ruby Hash objects
7
+ # match a given schema. ClassyHash runs fast by taking advantage of Ruby
8
+ # language features and avoiding object creation during validation.
9
+ module ClassyHash
10
+ # Validates a +hash+ against a +schema+. The +parent_path+ parameter is used
11
+ # internally to generate error messages.
12
+ def self.validate(hash, schema, parent_path=nil)
13
+ raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one?
14
+ raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations?
15
+
16
+ schema.each do |key, constraint|
17
+ if hash.include?(key)
18
+ self.check_one(key, hash[key], constraint, parent_path)
19
+ elsif !(constraint.is_a?(Array) && constraint.include?(:optional))
20
+ self.raise_error(parent_path, key, "present")
21
+ end
22
+ end
23
+
24
+ nil
25
+ end
26
+
27
+ # As with #validate, but members not specified in the +schema+ are forbidden.
28
+ # Only the top-level schema is strictly validated.
29
+ def self.validate_strict(hash, schema, parent_path=nil)
30
+ raise 'Must validate a Hash' unless hash.is_a?(Hash) # TODO: Allow validating other types by passing to #check_one?
31
+ raise 'Schema must be a Hash' unless schema.is_a?(Hash) # TODO: Allow individual element validations?
32
+
33
+ unless (hash.keys - schema.keys).empty?
34
+ raise "Hash contains members not specified in schema"
35
+ end
36
+
37
+ # TODO: Strict validation for nested schemas as well
38
+
39
+ self.validate(hash, schema, parent_path)
40
+ end
41
+
42
+ # Raises an error unless the given +value+ matches one of the given multiple
43
+ # choice +constraints+.
44
+ def self.check_multi(key, value, constraints, parent_path=nil)
45
+ if constraints.length == 0
46
+ self.raise_error(parent_path, key, "a valid multiple choice constraint (array must not be empty)")
47
+ end
48
+
49
+ # Optimize the common case of a direct class match
50
+ return if constraints.include?(value.class)
51
+
52
+ error = nil
53
+ constraints.each do |c|
54
+ next if c == :optional
55
+ begin
56
+ self.check_one(key, value, c, parent_path)
57
+ return
58
+ rescue => e
59
+ # Throw schema and array errors immediately
60
+ if (c.is_a?(Hash) && value.is_a?(Hash)) ||
61
+ (c.is_a?(Array) && value.is_a?(Array) && c.length == 1 && c.first.is_a?(Array))
62
+ raise e
63
+ end
64
+ end
65
+ end
66
+
67
+ self.raise_error(parent_path, key, "one of #{multiconstraint_string(constraints)}")
68
+ end
69
+
70
+ # Generates a semi-compact String describing the given +constraints+.
71
+ def self.multiconstraint_string constraints
72
+ constraints.map{|c|
73
+ if c.is_a?(Hash)
74
+ "{...schema...}"
75
+ elsif c.is_a?(Array)
76
+ "[#{self.multiconstraint_string(c)}]"
77
+ elsif c == :optional
78
+ nil
79
+ else
80
+ c.inspect
81
+ end
82
+ }.compact.join(', ')
83
+ end
84
+
85
+ # Checks a single value against a single constraint.
86
+ def self.check_one(key, value, constraint, parent_path=nil)
87
+ case constraint
88
+ when Class
89
+ # Constrain value to be a specific class
90
+ if constraint == TrueClass || constraint == FalseClass
91
+ unless value == true || value == false
92
+ self.raise_error(parent_path, key, "true or false")
93
+ end
94
+ elsif !value.is_a?(constraint)
95
+ self.raise_error(parent_path, key, "a/an #{constraint}")
96
+ end
97
+
98
+ when Hash
99
+ # Recursively check nested Hashes
100
+ self.raise_error(parent_path, key, "a Hash") unless value.is_a?(Hash)
101
+ self.validate(value, constraint, self.join_path(parent_path, key))
102
+
103
+ when Array
104
+ # Multiple choice or array validation
105
+ if constraint.length == 1 && constraint.first.is_a?(Array)
106
+ # Array validation
107
+ self.raise_error(parent_path, key, "an Array") unless value.is_a?(Array)
108
+
109
+ constraints = constraint.first
110
+ value.each_with_index do |v, idx|
111
+ self.check_multi(idx, v, constraints, self.join_path(parent_path, key))
112
+ end
113
+ else
114
+ # Multiple choice
115
+ self.check_multi(key, value, constraint, parent_path)
116
+ end
117
+
118
+ when Proc
119
+ # User-specified validator
120
+ result = constraint.call(value)
121
+ if result != true
122
+ self.raise_error(parent_path, key, result.is_a?(String) ? result : "accepted by Proc")
123
+ end
124
+
125
+ when Range
126
+ # Range (with type checking for common classes)
127
+ if constraint.min.is_a?(Integer) && constraint.max.is_a?(Integer)
128
+ self.raise_error(parent_path, key, "an Integer") unless value.is_a?(Integer)
129
+ elsif constraint.min.is_a?(Numeric)
130
+ self.raise_error(parent_path, key, "a Numeric") unless value.is_a?(Numeric)
131
+ elsif constraint.min.is_a?(String)
132
+ self.raise_error(parent_path, key, "a String") unless value.is_a?(String)
133
+ end
134
+
135
+ unless constraint.cover?(value)
136
+ self.raise_error(parent_path, key, "in range #{constraint.inspect}")
137
+ end
138
+
139
+ when :optional
140
+ # Optional key marker in multiple choice validators
141
+ nil
142
+
143
+ else
144
+ # Unknown schema constraint
145
+ self.raise_error(parent_path, key, "a valid schema constraint: #{constraint.inspect}")
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ def self.join_path(parent_path, key)
152
+ parent_path ? "#{parent_path}[#{key.inspect}]" : key.inspect
153
+ end
154
+
155
+ # Raises an error indicating that the given +key+ under the given
156
+ # +parent_path+ fails because the value "is not #{+message+}".
157
+ def self.raise_error(parent_path, key, message)
158
+ # TODO: Ability to validate all keys
159
+ raise "#{self.join_path(parent_path, key)} is not #{message}"
160
+ end
161
+ end
162
+
163
+ require 'classy_hash/generate'
@@ -0,0 +1,103 @@
1
+
2
+ module ClassyHash
3
+ # This module contains helpers that generate constraints for common
4
+ # ClassyHash validation tasks.
5
+ module Generate
6
+ # Generates a ClassyHash constraint that ensures a value is equal to one of
7
+ # the arguments in +args+.
8
+ #
9
+ # Example:
10
+ # schema = {
11
+ # a: ClassyHash::Generate.enum(1, 2, 3, 4)
12
+ # }
13
+ # ClassyHash.validate({ a: 1 }, schema)
14
+ def self.enum *args
15
+ lambda {|v|
16
+ args.include?(v) || "an element of #{args.inspect}"
17
+ }
18
+ end
19
+
20
+ # Generates a constraint that imposes a length limitation (an exact length
21
+ # or a range) on any type that responds to the :length method (e.g. String,
22
+ # Array, Hash).
23
+ #
24
+ # Example:
25
+ # schema = {
26
+ # a: ClassyHash::Generate.length(0..5)
27
+ # }
28
+ # ClassyHash.validate({a: '12345'}, schema)
29
+ # ClassyHash.validate({a: [1, 2, 3, 4, 5]}, schema)
30
+ def self.length length
31
+ raise "length must be an Integer or a Range" unless length.is_a?(Integer) || length.is_a?(Range)
32
+
33
+ if length.is_a?(Range) && !(length.min.is_a?(Integer) && length.max.is_a?(Integer))
34
+ raise "Range length endpoints must be Integers"
35
+ end
36
+
37
+ lambda {|v|
38
+ if v.respond_to?(:length)
39
+ if v.length == length || (length.is_a?(Range) && length.cover?(v.length))
40
+ true
41
+ else
42
+ "of length #{length}"
43
+ end
44
+ else
45
+ "a type that responds to :length"
46
+ end
47
+ }
48
+ end
49
+
50
+ # Generates a constraint that validates an Array's +length+ and contents
51
+ # using one or more +constraints+.
52
+ #
53
+ # Example:
54
+ # schema = {
55
+ # a: ClassyHash::Generate.array_length(4..5, Integer, String)
56
+ # }
57
+ # ClassyHash.validate({ a: [ 1, 2, 3, 'four', 5 ] }, schema)
58
+ def self.array_length length, *constraints
59
+ raise 'one or more constraints must be provided' if constraints.empty?
60
+
61
+ length_lambda = self.length(length)
62
+ msg = "an Array of length #{length}"
63
+
64
+ lambda {|v|
65
+ if v.is_a?(Array)
66
+ result = length_lambda.call(v)
67
+ if result == true
68
+ begin
69
+ ClassyHash.validate({array: v}, {array: [constraints]})
70
+ true
71
+ rescue => e
72
+ "valid: #{e}"
73
+ end
74
+ else
75
+ msg
76
+ end
77
+ else
78
+ msg
79
+ end
80
+ }
81
+ end
82
+
83
+ # Generates a constraint that validates the +length+ of a String.
84
+ #
85
+ # Example:
86
+ # schema = {
87
+ # a: ClassyHash::Generate.string_length(3)
88
+ # }
89
+ # ClassyHash.validate({a: '123'}, schema)
90
+ def self.string_length length
91
+ length_lambda = self.length(length)
92
+ msg = "a String of length #{length}"
93
+
94
+ lambda {|v|
95
+ if v.is_a?(String)
96
+ length_lambda.call(v) == true || msg
97
+ else
98
+ msg
99
+ end
100
+ }
101
+ end
102
+ end
103
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: classy_hash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Deseret Book
8
+ - Mike Bourgeous
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-06-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: |2
15
+ Classy Hash is a schema validator for Ruby Hashes. You provide a simple
16
+ schema Hash, and Classy Hash will make sure your data matches, providing
17
+ helpful error messages if it doesn't.
18
+ email: mike@mikebourgeous.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - lib/classy_hash.rb
24
+ - lib/classy_hash/generate.rb
25
+ homepage: https://github.com/deseretbook/classy_hash
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.2.2
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: 'Classy Hash: Keep your Hashes classy; a Hash schema validator'
49
+ test_files: []