simple_json_schema 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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