classy_hash 0.1.1

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.
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: []