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.
@@ -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