simple_json_schema 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +13 -0
- data/.gitignore +11 -0
- data/.gitlab-ci.yml +30 -0
- data/.gitmodules +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +29 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +103 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/simple_json_schema.rb +60 -0
- data/lib/simple_json_schema/checker.rb +41 -0
- data/lib/simple_json_schema/concerns/hash_acessor.rb +24 -0
- data/lib/simple_json_schema/items_helper.rb +40 -0
- data/lib/simple_json_schema/properties_helper.rb +56 -0
- data/lib/simple_json_schema/regex_helper.rb +24 -0
- data/lib/simple_json_schema/scope.rb +123 -0
- data/lib/simple_json_schema/validator.rb +84 -0
- data/lib/simple_json_schema/validators/base.rb +25 -0
- data/lib/simple_json_schema/validators/boolean.rb +23 -0
- data/lib/simple_json_schema/validators/concerns/format.rb +134 -0
- data/lib/simple_json_schema/validators/integer.rb +21 -0
- data/lib/simple_json_schema/validators/null.rb +11 -0
- data/lib/simple_json_schema/validators/number.rb +21 -0
- data/lib/simple_json_schema/validators/numeric.rb +22 -0
- data/lib/simple_json_schema/validators/string.rb +33 -0
- data/lib/simple_json_schema/version.rb +5 -0
- data/simple_json_schema.gemspec +48 -0
- metadata +234 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module Checker
|
5
|
+
class << self
|
6
|
+
def at_value(scope, check, operation)
|
7
|
+
over = scope[check]
|
8
|
+
scope.error(check) if over && scope.value&.public_send(operation, over)
|
9
|
+
end
|
10
|
+
|
11
|
+
def at_size(scope, check, operation)
|
12
|
+
over = scope[check]
|
13
|
+
scope.error(check) if over && scope.value&.size&.public_send(operation, over)
|
14
|
+
end
|
15
|
+
|
16
|
+
def required(scope)
|
17
|
+
required = scope[:required]
|
18
|
+
return unless required
|
19
|
+
|
20
|
+
missing_keys = required - scope.value.keys
|
21
|
+
scope.error(:required, missing_keys: missing_keys) if missing_keys.any?
|
22
|
+
end
|
23
|
+
|
24
|
+
def unique_items(scope)
|
25
|
+
value = scope.value
|
26
|
+
scope.error(:uniqueItems) if scope[:uniqueItems] && value.size != value.uniq.size
|
27
|
+
end
|
28
|
+
|
29
|
+
def enum(scope)
|
30
|
+
enum = scope[:enum]
|
31
|
+
scope.error(:enum) if enum && !enum.include?(scope.value)
|
32
|
+
end
|
33
|
+
|
34
|
+
def const(scope)
|
35
|
+
return if scope.segment.is_a?(Array)
|
36
|
+
|
37
|
+
scope.error(:const) if scope.key?(:const) && scope[:const] != scope.value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module Concerns
|
5
|
+
module HashAccessor
|
6
|
+
def hash_accessor(hash_name, *accessors)
|
7
|
+
define_method(hash_name) do
|
8
|
+
instance_variable_get("@#{hash_name}") ||
|
9
|
+
instance_variable_set("@#{hash_name}", {})
|
10
|
+
end
|
11
|
+
|
12
|
+
accessors.each do |name|
|
13
|
+
define_method(name) do
|
14
|
+
instance_variable_get("@#{hash_name}")[name]
|
15
|
+
end
|
16
|
+
|
17
|
+
define_method("#{name}=") do |value|
|
18
|
+
instance_variable_get("@#{hash_name}")[name] = value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module ItemsHelper
|
5
|
+
class << self
|
6
|
+
def checkers(scope)
|
7
|
+
Checker.at_size(scope, :maxItems, :>)
|
8
|
+
Checker.at_size(scope, :minItems, :<)
|
9
|
+
Checker.unique_items(scope)
|
10
|
+
end
|
11
|
+
|
12
|
+
def contains_in_items?(scope, &block)
|
13
|
+
contains = scope[:contains]
|
14
|
+
return if contains.nil?
|
15
|
+
|
16
|
+
scope.error(:contains) unless scope.value.each_index.any?(&block)
|
17
|
+
end
|
18
|
+
|
19
|
+
def each_path(scope)
|
20
|
+
items = scope[:items]
|
21
|
+
additional_items = scope[:additionalItems]
|
22
|
+
many_types = items.is_a?(Array)
|
23
|
+
|
24
|
+
scope.value.each_index do |index|
|
25
|
+
if many_types
|
26
|
+
if index < items.size
|
27
|
+
yield [:items, index], index
|
28
|
+
elsif !additional_items.nil?
|
29
|
+
yield [:additionalItems], index
|
30
|
+
else
|
31
|
+
break # protect over big arrays.
|
32
|
+
end
|
33
|
+
else
|
34
|
+
yield [:items], index
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module PropertiesHelper
|
5
|
+
class << self
|
6
|
+
def checkers(scope)
|
7
|
+
Checker.required(scope)
|
8
|
+
Checker.at_size(scope, :maxProperties, :>)
|
9
|
+
Checker.at_size(scope, :minProperties, :<)
|
10
|
+
end
|
11
|
+
|
12
|
+
def processe_defualt(scope)
|
13
|
+
return unless scope.options[:insert_defaults] == true
|
14
|
+
|
15
|
+
properties = scope[:properties]
|
16
|
+
return unless properties
|
17
|
+
|
18
|
+
value = scope.value
|
19
|
+
properties.each do |property_name, property_schema|
|
20
|
+
if !value.key?(property_name) && property_schema.is_a?(Hash) && property_schema.key?('default')
|
21
|
+
value[property_name] = property_schema.fetch('default').clone
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def each_property_name(scope, &block)
|
27
|
+
property_names = scope[:propertyNames]
|
28
|
+
return if property_names.nil?
|
29
|
+
|
30
|
+
scope.value.each_key(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def map_property_schema_path(scope, property_name)
|
34
|
+
paths = []
|
35
|
+
paths << [:properties, property_name] if scope[:properties]&.key?(property_name)
|
36
|
+
|
37
|
+
if scope.key?(:patternProperties)
|
38
|
+
select_patten_properties(scope, property_name).each do |pattern|
|
39
|
+
paths << [:patternProperties, pattern]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
paths << [:additionalProperties] if paths.empty? && scope.key?(:additionalProperties)
|
44
|
+
paths
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def select_patten_properties(scope, property_name)
|
50
|
+
scope[:patternProperties].keys.select do |pattern|
|
51
|
+
RegexHelper.ecma_262_regex(pattern, scope.cache).match?(property_name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module RegexHelper
|
5
|
+
RUBY_REGEX_ANCHORS_TO_ECMA_262 = {
|
6
|
+
bos: 'A',
|
7
|
+
eos: 'z',
|
8
|
+
bol: '\A',
|
9
|
+
eol: '\z'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def ecma_262_regex(pattern, cache)
|
14
|
+
cache.fetch(pattern) do
|
15
|
+
Regexp.new(
|
16
|
+
Regexp::Scanner.scan(pattern).map do |type, token, text|
|
17
|
+
type == :anchor ? RUBY_REGEX_ANCHORS_TO_ECMA_262.fetch(token, text) : text
|
18
|
+
end.join
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
class Scope
|
5
|
+
extend Concerns::HashAccessor
|
6
|
+
|
7
|
+
CLASS_TYPE_TRANSLATE = {
|
8
|
+
Array => 'array',
|
9
|
+
Float => 'number',
|
10
|
+
Hash => 'object',
|
11
|
+
Integer => 'integer',
|
12
|
+
Numeric => 'number',
|
13
|
+
String => 'string'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
hash_accessor :scope, :data, :schema, :type, :draft, :data_paths, :schema_paths, :errors, :options
|
17
|
+
|
18
|
+
def initialize(**args)
|
19
|
+
scope.merge!(args)
|
20
|
+
|
21
|
+
self.data_paths ||= []
|
22
|
+
self.schema_paths ||= []
|
23
|
+
self.errors ||= []
|
24
|
+
self.options ||= {}
|
25
|
+
self.type ||= evaluate_type
|
26
|
+
end
|
27
|
+
|
28
|
+
def path_to(data_path: nil, schema_path: nil, type: nil)
|
29
|
+
new_data_paths = data_paths + [data_path].flatten.compact
|
30
|
+
new_schema_paths = schema_paths + [schema_path].flatten.compact
|
31
|
+
|
32
|
+
self.class.new(scope.merge(data_paths: new_data_paths, schema_paths: new_schema_paths, type: type))
|
33
|
+
end
|
34
|
+
|
35
|
+
def replace_data(new_data)
|
36
|
+
self.data = new_data
|
37
|
+
self.data_paths = []
|
38
|
+
self.type = evaluate_type
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def error(type, details = nil)
|
43
|
+
error = {
|
44
|
+
type: type,
|
45
|
+
segment: segment,
|
46
|
+
value: value,
|
47
|
+
data_pointer: "/#{data_paths.join('/')}",
|
48
|
+
schema_pointer: "/#{schema_paths.join('/')}"
|
49
|
+
}
|
50
|
+
|
51
|
+
error[:details] = details if details
|
52
|
+
|
53
|
+
errors.push(error)
|
54
|
+
end
|
55
|
+
|
56
|
+
def value
|
57
|
+
dig(data, data_paths)
|
58
|
+
end
|
59
|
+
|
60
|
+
def value=(new_value)
|
61
|
+
return if errors.any? # only convert value until be invalid.
|
62
|
+
|
63
|
+
*steps, leaf = data_paths
|
64
|
+
|
65
|
+
if steps.empty?
|
66
|
+
data[leaf] = new_value
|
67
|
+
else
|
68
|
+
data.dig(*steps)[leaf] = new_value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def segment
|
73
|
+
@segment ||= dig(schema, schema_paths)
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](key)
|
77
|
+
return unless segment.is_a?(Hash)
|
78
|
+
|
79
|
+
segment[key]
|
80
|
+
end
|
81
|
+
|
82
|
+
def key?(key)
|
83
|
+
segment.is_a?(Hash) && segment.key?(key)
|
84
|
+
end
|
85
|
+
|
86
|
+
def segment?
|
87
|
+
if segment == false
|
88
|
+
error('schema')
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
|
92
|
+
!(segment == true || segment.nil?)
|
93
|
+
end
|
94
|
+
|
95
|
+
def cache
|
96
|
+
options[:cache] ||= {}
|
97
|
+
end
|
98
|
+
|
99
|
+
def around_hooks
|
100
|
+
options[:before_property_validation]&.call(self)
|
101
|
+
yield
|
102
|
+
options[:after_property_validation]&.call(self)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def evaluate_type
|
108
|
+
if key?(:type)
|
109
|
+
segment[:type]
|
110
|
+
else
|
111
|
+
CLASS_TYPE_TRANSLATE[value.class]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def dig(hash, paths)
|
116
|
+
return hash if paths.empty?
|
117
|
+
|
118
|
+
hash.dig(*paths) if hash.respond_to?(:dig)
|
119
|
+
rescue TypeError
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
class Validator
|
5
|
+
VALIDATORES = {
|
6
|
+
'null' => Validators::Null.new,
|
7
|
+
'boolean' => Validators::Boolean.new,
|
8
|
+
'integer' => Validators::Integer.new,
|
9
|
+
'number' => Validators::Number.new,
|
10
|
+
'string' => Validators::String.new
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def validate(scope)
|
15
|
+
return unless scope.segment?
|
16
|
+
|
17
|
+
Checker.enum(scope)
|
18
|
+
Checker.const(scope)
|
19
|
+
|
20
|
+
case scope.type
|
21
|
+
when 'object'
|
22
|
+
valid_object(scope)
|
23
|
+
when 'array'
|
24
|
+
valid_array(scope)
|
25
|
+
when Array
|
26
|
+
scope.error(:type) unless scope.type.any? { |type| validate?(scope.path_to(type: type)) }
|
27
|
+
else
|
28
|
+
validate_base_types(scope)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def validate?(scope)
|
35
|
+
scope.errors = []
|
36
|
+
scope.options = { cache: scope.cache }
|
37
|
+
|
38
|
+
validate(scope)
|
39
|
+
scope.errors.none?
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_base_types(scope)
|
43
|
+
VALIDATORES[scope.type]&.valid(scope)
|
44
|
+
end
|
45
|
+
|
46
|
+
def valid_object(scope)
|
47
|
+
value = scope.value
|
48
|
+
|
49
|
+
return scope.error(:object) unless value.is_a?(Hash)
|
50
|
+
|
51
|
+
PropertiesHelper.processe_defualt(scope)
|
52
|
+
PropertiesHelper.checkers(scope)
|
53
|
+
|
54
|
+
PropertiesHelper.each_property_name(scope) do |property_name|
|
55
|
+
validate(scope.path_to(schema_path: :propertyNames).replace_data(property_name))
|
56
|
+
end
|
57
|
+
|
58
|
+
value.each_key do |property_name|
|
59
|
+
PropertiesHelper.map_property_schema_path(scope, property_name).each do |schema_path|
|
60
|
+
new_scope = scope.path_to(schema_path: schema_path, data_path: property_name)
|
61
|
+
new_scope.around_hooks do
|
62
|
+
validate(new_scope)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid_array(scope)
|
69
|
+
value = scope.value
|
70
|
+
return scope.error(:array) unless value.is_a?(Array)
|
71
|
+
|
72
|
+
ItemsHelper.checkers(scope)
|
73
|
+
|
74
|
+
ItemsHelper.contains_in_items?(scope) do |index|
|
75
|
+
validate?(scope.path_to(schema_path: :contains, data_path: index))
|
76
|
+
end
|
77
|
+
|
78
|
+
ItemsHelper.each_path(scope) do |path, index|
|
79
|
+
validate(scope.path_to(schema_path: path, data_path: index))
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module Validators
|
5
|
+
class Base
|
6
|
+
def valid(scope)
|
7
|
+
cast(scope) if scope.options[:cast] == true
|
8
|
+
validate(scope)
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def validate(scope)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def cast(scope)
|
18
|
+
value = casting(scope.value)
|
19
|
+
scope.value = value unless value.nil? && scope.value != value
|
20
|
+
end
|
21
|
+
|
22
|
+
def casting(value); end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SimpleJSONSchema
|
4
|
+
module Validators
|
5
|
+
class Boolean < Base
|
6
|
+
TRUE_VALUES = ['true', '1', 1, 'yes'].freeze
|
7
|
+
FALSE_VALUES = ['false', '0', 0, 'no'].freeze
|
8
|
+
|
9
|
+
BOOLEANS = Set[true, false].freeze
|
10
|
+
|
11
|
+
def validate(scope)
|
12
|
+
value = scope.value
|
13
|
+
|
14
|
+
return scope.error(:boolean) unless BOOLEANS.include?(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def casting(value)
|
18
|
+
return true if TRUE_VALUES.include?(value)
|
19
|
+
return false if FALSE_VALUES.include?(value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|