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 +7 -0
- data/lib/classy_hash.rb +163 -0
- data/lib/classy_hash/generate.rb +103 -0
- metadata +49 -0
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
|
data/lib/classy_hash.rb
ADDED
@@ -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: []
|