simple_json_schema 0.1.3
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/.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
|