json_skooma 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +107 -0
- data/data/draft-2019-09/README.md +60 -0
- data/data/draft-2019-09/hyper-schema.json +26 -0
- data/data/draft-2019-09/links.json +91 -0
- data/data/draft-2019-09/meta/applicator.json +56 -0
- data/data/draft-2019-09/meta/content.json +17 -0
- data/data/draft-2019-09/meta/core.json +57 -0
- data/data/draft-2019-09/meta/format.json +14 -0
- data/data/draft-2019-09/meta/hyper-schema.json +29 -0
- data/data/draft-2019-09/meta/meta-data.json +37 -0
- data/data/draft-2019-09/meta/validation.json +98 -0
- data/data/draft-2019-09/output/hyper-schema.json +62 -0
- data/data/draft-2019-09/output/schema.json +86 -0
- data/data/draft-2019-09/output/verbose-example.json +130 -0
- data/data/draft-2019-09/schema.json +42 -0
- data/data/draft-2020-12/README.md +89 -0
- data/data/draft-2020-12/adr/README.md +15 -0
- data/data/draft-2020-12/archive/hyper-schema.json +28 -0
- data/data/draft-2020-12/archive/links.json +93 -0
- data/data/draft-2020-12/archive/meta/hyper-schema.json +30 -0
- data/data/draft-2020-12/hyper-schema.json +27 -0
- data/data/draft-2020-12/links.json +85 -0
- data/data/draft-2020-12/meta/applicator.json +48 -0
- data/data/draft-2020-12/meta/content.json +17 -0
- data/data/draft-2020-12/meta/core.json +51 -0
- data/data/draft-2020-12/meta/format-annotation.json +14 -0
- data/data/draft-2020-12/meta/format-assertion.json +14 -0
- data/data/draft-2020-12/meta/hyper-schema.json +29 -0
- data/data/draft-2020-12/meta/meta-data.json +37 -0
- data/data/draft-2020-12/meta/unevaluated.json +15 -0
- data/data/draft-2020-12/meta/validation.json +98 -0
- data/data/draft-2020-12/output/hyper-schema.json +62 -0
- data/data/draft-2020-12/output/schema.json +96 -0
- data/data/draft-2020-12/output/verbose-example.json +130 -0
- data/data/draft-2020-12/schema.json +58 -0
- data/lib/json_skooma/dialects/draft201909.rb +137 -0
- data/lib/json_skooma/dialects/draft202012.rb +146 -0
- data/lib/json_skooma/formatters.rb +135 -0
- data/lib/json_skooma/inflector.rb +13 -0
- data/lib/json_skooma/json_node.rb +100 -0
- data/lib/json_skooma/json_pointer.rb +79 -0
- data/lib/json_skooma/json_schema.rb +176 -0
- data/lib/json_skooma/keywords/applicator/additional_properties.rb +37 -0
- data/lib/json_skooma/keywords/applicator/all_of.rb +25 -0
- data/lib/json_skooma/keywords/applicator/any_of.rb +26 -0
- data/lib/json_skooma/keywords/applicator/contains.rb +31 -0
- data/lib/json_skooma/keywords/applicator/dependent_schemas.rb +35 -0
- data/lib/json_skooma/keywords/applicator/else.rb +22 -0
- data/lib/json_skooma/keywords/applicator/if.rb +17 -0
- data/lib/json_skooma/keywords/applicator/items.rb +36 -0
- data/lib/json_skooma/keywords/applicator/not.rb +19 -0
- data/lib/json_skooma/keywords/applicator/one_of.rb +35 -0
- data/lib/json_skooma/keywords/applicator/pattern_properties.rb +46 -0
- data/lib/json_skooma/keywords/applicator/prefix_items.rb +31 -0
- data/lib/json_skooma/keywords/applicator/properties.rb +34 -0
- data/lib/json_skooma/keywords/applicator/property_names.rb +25 -0
- data/lib/json_skooma/keywords/applicator/then.rb +22 -0
- data/lib/json_skooma/keywords/base.rb +74 -0
- data/lib/json_skooma/keywords/base_annotation.rb +12 -0
- data/lib/json_skooma/keywords/content/content_encoding.rb +12 -0
- data/lib/json_skooma/keywords/content/content_media_type.rb +12 -0
- data/lib/json_skooma/keywords/content/content_schema.rb +19 -0
- data/lib/json_skooma/keywords/core/anchor.rb +22 -0
- data/lib/json_skooma/keywords/core/comment.rb +12 -0
- data/lib/json_skooma/keywords/core/defs.rb +13 -0
- data/lib/json_skooma/keywords/core/dynamic_anchor.rb +22 -0
- data/lib/json_skooma/keywords/core/dynamic_ref.rb +67 -0
- data/lib/json_skooma/keywords/core/id.rb +28 -0
- data/lib/json_skooma/keywords/core/ref.rb +35 -0
- data/lib/json_skooma/keywords/core/schema.rb +26 -0
- data/lib/json_skooma/keywords/core/vocabulary.rb +34 -0
- data/lib/json_skooma/keywords/draft_2019_09/additional_items.rb +40 -0
- data/lib/json_skooma/keywords/draft_2019_09/items.rb +41 -0
- data/lib/json_skooma/keywords/draft_2019_09/recursive_anchor.rb +12 -0
- data/lib/json_skooma/keywords/draft_2019_09/recursive_ref.rb +46 -0
- data/lib/json_skooma/keywords/draft_2019_09/unevaluated_items.rb +56 -0
- data/lib/json_skooma/keywords/format_annotation/format.rb +27 -0
- data/lib/json_skooma/keywords/meta_data/default.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/deprecated.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/description.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/examples.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/read_only.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/title.rb +11 -0
- data/lib/json_skooma/keywords/meta_data/write_only.rb +11 -0
- data/lib/json_skooma/keywords/unevaluated/unevaluated_items.rb +59 -0
- data/lib/json_skooma/keywords/unevaluated/unevaluated_properties.rb +43 -0
- data/lib/json_skooma/keywords/unknown.rb +21 -0
- data/lib/json_skooma/keywords/validation/const.rb +17 -0
- data/lib/json_skooma/keywords/validation/dependent_required.rb +24 -0
- data/lib/json_skooma/keywords/validation/enum.rb +19 -0
- data/lib/json_skooma/keywords/validation/exclusive_maximum.rb +18 -0
- data/lib/json_skooma/keywords/validation/exclusive_minimum.rb +18 -0
- data/lib/json_skooma/keywords/validation/max_contains.rb +24 -0
- data/lib/json_skooma/keywords/validation/max_items.rb +18 -0
- data/lib/json_skooma/keywords/validation/max_length.rb +18 -0
- data/lib/json_skooma/keywords/validation/max_properties.rb +18 -0
- data/lib/json_skooma/keywords/validation/maximum.rb +18 -0
- data/lib/json_skooma/keywords/validation/min_contains.rb +31 -0
- data/lib/json_skooma/keywords/validation/min_items.rb +18 -0
- data/lib/json_skooma/keywords/validation/min_length.rb +18 -0
- data/lib/json_skooma/keywords/validation/min_properties.rb +18 -0
- data/lib/json_skooma/keywords/validation/minimum.rb +18 -0
- data/lib/json_skooma/keywords/validation/multiple_of.rb +20 -0
- data/lib/json_skooma/keywords/validation/pattern.rb +23 -0
- data/lib/json_skooma/keywords/validation/required.rb +19 -0
- data/lib/json_skooma/keywords/validation/type.rb +26 -0
- data/lib/json_skooma/keywords/validation/unique_items.rb +20 -0
- data/lib/json_skooma/keywords/value_schemas.rb +87 -0
- data/lib/json_skooma/memoizable.rb +21 -0
- data/lib/json_skooma/metaschema.rb +32 -0
- data/lib/json_skooma/registry.rb +130 -0
- data/lib/json_skooma/result.rb +125 -0
- data/lib/json_skooma/sources.rb +55 -0
- data/lib/json_skooma/validators/base.rb +31 -0
- data/lib/json_skooma/validators/date.rb +18 -0
- data/lib/json_skooma/validators/date_time.rb +24 -0
- data/lib/json_skooma/validators/duration.rb +25 -0
- data/lib/json_skooma/validators/email.rb +36 -0
- data/lib/json_skooma/validators/hostname.rb +17 -0
- data/lib/json_skooma/validators/idn_email.rb +30 -0
- data/lib/json_skooma/validators/idn_hostname.rb +15 -0
- data/lib/json_skooma/validators/ipv4.rb +20 -0
- data/lib/json_skooma/validators/ipv6.rb +16 -0
- data/lib/json_skooma/validators/iri.rb +47 -0
- data/lib/json_skooma/validators/iri_reference.rb +15 -0
- data/lib/json_skooma/validators/json_pointer.rb +19 -0
- data/lib/json_skooma/validators/regex.rb +15 -0
- data/lib/json_skooma/validators/relative_json_pointer.rb +18 -0
- data/lib/json_skooma/validators/time.rb +32 -0
- data/lib/json_skooma/validators/uri.rb +60 -0
- data/lib/json_skooma/validators/uri_reference.rb +15 -0
- data/lib/json_skooma/validators/uri_template.rb +26 -0
- data/lib/json_skooma/validators/uuid.rb +15 -0
- data/lib/json_skooma/validators.rb +17 -0
- data/lib/json_skooma/version.rb +5 -0
- data/lib/json_skooma/vocabulary.rb +12 -0
- data/lib/json_skooma.rb +39 -0
- metadata +244 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Keywords
|
5
|
+
module Validation
|
6
|
+
class Type < Base
|
7
|
+
self.key = "type"
|
8
|
+
|
9
|
+
def evaluate(instance, result)
|
10
|
+
return if json.value.include?(instance.type)
|
11
|
+
return if integer?(instance)
|
12
|
+
|
13
|
+
result.failure("The instance must be of type #{json.value}, but was #{instance.type}")
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def integer?(instance)
|
19
|
+
instance.type == "number" &&
|
20
|
+
json.include?("integer") &&
|
21
|
+
instance == instance.to_i
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Keywords
|
5
|
+
module Validation
|
6
|
+
class UniqueItems < Base
|
7
|
+
self.key = "uniqueItems"
|
8
|
+
self.instance_types = "array"
|
9
|
+
|
10
|
+
def evaluate(instance, result)
|
11
|
+
return unless json.value
|
12
|
+
|
13
|
+
if instance.uniq.size != instance.size
|
14
|
+
result.failure("The array's elements must all be unique")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Keywords
|
5
|
+
module ValueSchemas
|
6
|
+
class << self
|
7
|
+
def [](key)
|
8
|
+
value_schemas&.[](key) or raise "Unknown value schema: #{key}, known schemas: #{value_schemas.keys.inspect}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def register_value_schema(key, klass)
|
12
|
+
(self.value_schemas ||= {})[key] = klass
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_accessor :value_schemas
|
18
|
+
end
|
19
|
+
|
20
|
+
module Schema
|
21
|
+
def wrap_value(value)
|
22
|
+
return super unless value.is_a?(Hash) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
23
|
+
|
24
|
+
(self.class.schema_value_class || JSONSchema).new(
|
25
|
+
value,
|
26
|
+
parent: parent_schema,
|
27
|
+
key: key,
|
28
|
+
registry: parent_schema.registry,
|
29
|
+
cache_id: parent_schema.cache_id
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def each_schema
|
34
|
+
return super unless json.is_a?(JSONSchema)
|
35
|
+
|
36
|
+
yield json
|
37
|
+
end
|
38
|
+
end
|
39
|
+
register_value_schema(:schema, Schema)
|
40
|
+
|
41
|
+
module ArrayOfSchemas
|
42
|
+
def wrap_value(value)
|
43
|
+
return super unless value.is_a?(Array)
|
44
|
+
|
45
|
+
JSONNode.new(
|
46
|
+
value,
|
47
|
+
parent: parent_schema,
|
48
|
+
key: key,
|
49
|
+
item_class: self.class.schema_value_class || JSONSchema,
|
50
|
+
registry: parent_schema.registry,
|
51
|
+
cache_id: parent_schema.cache_id
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def each_schema(&block)
|
56
|
+
return super unless json.type == "array"
|
57
|
+
|
58
|
+
json.each(&block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
register_value_schema(:array_of_schemas, ArrayOfSchemas)
|
63
|
+
|
64
|
+
module ObjectOfSchemas
|
65
|
+
def wrap_value(value)
|
66
|
+
return super unless value.is_a?(Hash)
|
67
|
+
|
68
|
+
JSONNode.new(
|
69
|
+
value,
|
70
|
+
parent: parent_schema,
|
71
|
+
key: key,
|
72
|
+
item_class: self.class.schema_value_class || JSONSchema,
|
73
|
+
registry: parent_schema.registry,
|
74
|
+
cache_id: parent_schema.cache_id
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_schema(&block)
|
79
|
+
return super unless json.type == "object"
|
80
|
+
|
81
|
+
json.each_value(&block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
register_value_schema(:object_of_schemas, ObjectOfSchemas)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Memoizable
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(Memoizable)
|
7
|
+
end
|
8
|
+
|
9
|
+
def memoize(method_name)
|
10
|
+
this = respond_to?(:prepend) ? self : singleton_class
|
11
|
+
var_name = "@memoized_#{method_name}"
|
12
|
+
this.prepend(Module.new do
|
13
|
+
define_method(method_name) do
|
14
|
+
return instance_variable_get(var_name) if instance_variable_defined?(var_name)
|
15
|
+
|
16
|
+
instance_variable_set(var_name, super())
|
17
|
+
end
|
18
|
+
end)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
class Metaschema < JSONSchema
|
5
|
+
attr_accessor :core_vocabulary, :default_vocabularies
|
6
|
+
attr_reader :kw_classes
|
7
|
+
|
8
|
+
def initialize(value, core_vocabulary, *default_vocabularies, **options)
|
9
|
+
@core_vocabulary = core_vocabulary
|
10
|
+
@default_vocabularies = default_vocabularies
|
11
|
+
@kw_classes = {}
|
12
|
+
super(value, cache_id: "__meta__", **options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def kw_class(key)
|
16
|
+
kw_classes[key] ||= Keywords::Unknown[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def bootstrap(value)
|
22
|
+
super
|
23
|
+
|
24
|
+
kw = Keywords::Core::Vocabulary.new(self, value.fetch("$vocabulary", default_vocabulary_urls))
|
25
|
+
add_keyword(kw) if value.key?("$vocabulary")
|
26
|
+
end
|
27
|
+
|
28
|
+
def default_vocabulary_urls
|
29
|
+
(default_vocabularies + [core_vocabulary]).filter_map(&:uri).map { |uri| [uri.to_s, true] }.to_h
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
class RegistryError < Error; end
|
5
|
+
|
6
|
+
class Registry
|
7
|
+
class << self
|
8
|
+
attr_accessor :registries
|
9
|
+
|
10
|
+
def [](key)
|
11
|
+
return key if key.is_a?(Registry)
|
12
|
+
|
13
|
+
raise RegistryError, "Registry `#{key}` not found" unless registries.key?(key)
|
14
|
+
|
15
|
+
registries[key]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
self.registries = {}
|
20
|
+
|
21
|
+
DEFAULT_NAME = "registry"
|
22
|
+
|
23
|
+
def initialize(name: DEFAULT_NAME)
|
24
|
+
@uri_sources = {}
|
25
|
+
@vocabularies = {}
|
26
|
+
@schema_cache = {}
|
27
|
+
@enabled_formats = Set.new
|
28
|
+
|
29
|
+
self.class.registries[name] = self
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_format(key, validator = nil)
|
33
|
+
JSONSkooma::Validators.register(key, validator) if validator
|
34
|
+
raise RegistryError, "Format validator `#{key}` not found" unless Validators.validators.key?(key)
|
35
|
+
|
36
|
+
@enabled_formats << key
|
37
|
+
end
|
38
|
+
|
39
|
+
def format_enabled?(key)
|
40
|
+
@enabled_formats.include?(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_vocabulary(uri, *keywords)
|
44
|
+
@vocabularies[uri.to_s] = Vocabulary.new(uri, *keywords)
|
45
|
+
end
|
46
|
+
|
47
|
+
def vocabulary(uri)
|
48
|
+
@vocabularies[uri.to_s] or raise RegistryError, "vocabulary #{uri} not found"
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_schema(uri, schema, cache_id: "default")
|
52
|
+
@schema_cache[cache_id] ||= {}
|
53
|
+
@schema_cache[cache_id][uri.to_s] = schema
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_schema(uri, cache_id: "default")
|
57
|
+
@schema_cache[cache_id].delete(uri.to_s)
|
58
|
+
end
|
59
|
+
|
60
|
+
def schema(uri, metaschema_uri: nil, cache_id: "default", schema_class: JSONSchema, expected_class: JSONSchema)
|
61
|
+
schema = @schema_cache.dig(cache_id, uri.to_s)
|
62
|
+
return schema if schema
|
63
|
+
|
64
|
+
base_uri = uri.dup.tap { |u| u.fragment = nil }
|
65
|
+
schema = @schema_cache.dig(cache_id, base_uri.to_s) if uri.fragment
|
66
|
+
|
67
|
+
if schema.nil?
|
68
|
+
doc = load_json(base_uri)
|
69
|
+
|
70
|
+
schema = schema_class.new(
|
71
|
+
doc,
|
72
|
+
registry: self,
|
73
|
+
cache_id: cache_id,
|
74
|
+
uri: base_uri,
|
75
|
+
metaschema_uri: metaschema_uri
|
76
|
+
)
|
77
|
+
return @schema_cache.dig(cache_id, uri.to_s) if @schema_cache.dig(cache_id, uri.to_s)
|
78
|
+
end
|
79
|
+
schema = JSONPointer.new(uri.fragment).eval(schema) if uri.fragment
|
80
|
+
return schema if schema.is_a?(expected_class)
|
81
|
+
|
82
|
+
raise RegistryError, "The object referenced by #{uri} is not #{expected_class}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def add_metaschema(uri, default_core_vocabulary_uri = nil, *default_vocabulary_uris)
|
86
|
+
metaschema_doc = load_json(uri)
|
87
|
+
default_core_vocabulary = vocabulary(default_core_vocabulary_uri) if default_core_vocabulary_uri
|
88
|
+
default_vocabularies = default_vocabulary_uris.map { |vocabulary_uri| vocabulary(vocabulary_uri) }
|
89
|
+
|
90
|
+
metaschema = Metaschema.new(
|
91
|
+
metaschema_doc,
|
92
|
+
default_core_vocabulary,
|
93
|
+
*default_vocabularies,
|
94
|
+
registry: self,
|
95
|
+
uri: uri
|
96
|
+
)
|
97
|
+
|
98
|
+
return metaschema if metaschema.validate.valid?
|
99
|
+
|
100
|
+
raise RegistryError, "The metaschema is invalid against its own metaschema #{metaschema_doc["$schema"]}"
|
101
|
+
end
|
102
|
+
|
103
|
+
def metaschema(uri)
|
104
|
+
schema = @schema_cache.dig("__meta__", uri.to_s) || add_metaschema(uri.to_s)
|
105
|
+
return schema if schema
|
106
|
+
|
107
|
+
raise RegistryError, "The schema referenced by #{uri} is not a metaschema"
|
108
|
+
end
|
109
|
+
|
110
|
+
def add_source(uri, source)
|
111
|
+
raise RegistryError, "uri must end with '/'" unless uri.end_with?("/")
|
112
|
+
|
113
|
+
@uri_sources[uri] = source
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def load_json(uri)
|
119
|
+
candidates = @uri_sources
|
120
|
+
.select { |source_uri| uri.to_s.start_with?(source_uri) }
|
121
|
+
.sort_by { |source_uri| -source_uri.length }
|
122
|
+
|
123
|
+
raise RegistryError, "A source is not available for #{uri}" if candidates.empty?
|
124
|
+
|
125
|
+
base_uri, candidate = candidates.first
|
126
|
+
relative_path = uri.to_s.sub(base_uri, "")
|
127
|
+
candidate.call(relative_path)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
class Result
|
5
|
+
attr_writer :ref_schema
|
6
|
+
|
7
|
+
attr_reader :children, :path, :relative_path, :schema, :instance, :parent, :annotation, :error, :key, :root
|
8
|
+
|
9
|
+
def initialize(schema, instance, parent: nil, key: nil)
|
10
|
+
@schema = schema
|
11
|
+
@instance = instance
|
12
|
+
@parent = parent
|
13
|
+
@root = parent&.root || self
|
14
|
+
@key = key
|
15
|
+
@children = {}
|
16
|
+
@valid = true
|
17
|
+
if parent.nil?
|
18
|
+
@path = JSONPointer.new([])
|
19
|
+
@relative_path = JSONPointer.new([])
|
20
|
+
else
|
21
|
+
@path = parent.path.child(key)
|
22
|
+
@relative_path =
|
23
|
+
if schema.equal?(parent.schema)
|
24
|
+
parent.relative_path.child(key)
|
25
|
+
else
|
26
|
+
JSONPointer.new_root(key)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(instance, key, schema = nil, subclass: self.class)
|
32
|
+
child = subclass.new(schema || @schema, instance, parent: self, key: key)
|
33
|
+
|
34
|
+
yield child
|
35
|
+
|
36
|
+
@children[[key, instance.path]] = child unless child.discard?
|
37
|
+
end
|
38
|
+
|
39
|
+
def schema_node
|
40
|
+
relative_path.eval(schema)
|
41
|
+
end
|
42
|
+
|
43
|
+
def sibling(instance, key)
|
44
|
+
@parent.children[[key, instance.path]] if @parent
|
45
|
+
end
|
46
|
+
|
47
|
+
def annotate(value)
|
48
|
+
@annotation = value
|
49
|
+
end
|
50
|
+
|
51
|
+
def failure(error = nil)
|
52
|
+
@valid = false
|
53
|
+
@error = error
|
54
|
+
end
|
55
|
+
|
56
|
+
def success
|
57
|
+
@valid = true
|
58
|
+
@error = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def skip_assertion
|
62
|
+
@skip_assertion = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def discard
|
66
|
+
@discard = true
|
67
|
+
end
|
68
|
+
|
69
|
+
def discard?
|
70
|
+
@discard
|
71
|
+
end
|
72
|
+
|
73
|
+
def valid?
|
74
|
+
@valid
|
75
|
+
end
|
76
|
+
|
77
|
+
def passed?
|
78
|
+
@valid || @skip_assertion
|
79
|
+
end
|
80
|
+
|
81
|
+
def absolute_uri
|
82
|
+
return @ref_schema.canonical_uri if @ref_schema
|
83
|
+
return nil if schema.canonical_uri.nil?
|
84
|
+
|
85
|
+
path = JSONPointer.new(schema.canonical_uri.fragment || "") << relative_path
|
86
|
+
schema.canonical_uri.dup.tap { |u| u.fragment = path.to_s }
|
87
|
+
end
|
88
|
+
|
89
|
+
def collect_annotations(instance = nil, key = nil)
|
90
|
+
return if !valid? || discard?
|
91
|
+
|
92
|
+
if @annotation &&
|
93
|
+
(key.nil? || key == @key) &&
|
94
|
+
(instance.nil? || instance.path == @instance.path)
|
95
|
+
yield @annotation
|
96
|
+
end
|
97
|
+
|
98
|
+
@children.each do |_, child|
|
99
|
+
child.collect_annotations(instance, key) do |annotation|
|
100
|
+
yield annotation
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def collect_errors(instance: nil, key: nil)
|
106
|
+
return if valid? || discard?
|
107
|
+
|
108
|
+
if @error &&
|
109
|
+
(key.nil? || key == @key) &&
|
110
|
+
(instance.nil? || instance.path == @instance.path)
|
111
|
+
yield @error
|
112
|
+
end
|
113
|
+
|
114
|
+
@children.each do |_, child|
|
115
|
+
child.collect_errors(instance, key) do |error|
|
116
|
+
yield error
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def output(format, **options)
|
122
|
+
Formatters[format].call(self, **options)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open-uri"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module JSONSkooma
|
7
|
+
module Sources
|
8
|
+
class Error < JSONSkooma::Error; end
|
9
|
+
|
10
|
+
class Base
|
11
|
+
def initialize(base, suffix: nil)
|
12
|
+
@base = base
|
13
|
+
@suffix = suffix
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(relative_path)
|
17
|
+
YAML.safe_load(read(relative_path))
|
18
|
+
rescue Psych::SyntaxError
|
19
|
+
raise Error, "Could not parse file #{relative_path}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :base, :suffix
|
25
|
+
|
26
|
+
def read(path)
|
27
|
+
raise "Not implemented"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Local < Base
|
32
|
+
private
|
33
|
+
|
34
|
+
def read(relative_path)
|
35
|
+
path = File.expand_path(relative_path, base)
|
36
|
+
path += suffix if suffix
|
37
|
+
File.read(path)
|
38
|
+
rescue Errno::ENOENT
|
39
|
+
raise Error, "Could not find file #{path}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class Remote < Base
|
44
|
+
private
|
45
|
+
|
46
|
+
def read(relative_path)
|
47
|
+
path = suffix ? relative_path + suffix : relative_path
|
48
|
+
url = URI.join(base, path)
|
49
|
+
URI.parse(url).open.read
|
50
|
+
rescue OpenURI::HTTPError, SocketError
|
51
|
+
raise Error, "Could not fetch #{url}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class Base
|
6
|
+
class << self
|
7
|
+
attr_reader :instance_types
|
8
|
+
|
9
|
+
def instance_types=(value)
|
10
|
+
@instance_types = Array(value)
|
11
|
+
end
|
12
|
+
|
13
|
+
def assert?(instance)
|
14
|
+
instance_types.include?(instance.type)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(instance)
|
18
|
+
new.call(instance)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inherited(subclass)
|
22
|
+
subclass.instance_types = "string"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(_instance)
|
27
|
+
raise NotImplementedError, "must be implemented by subclass"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class Date < Base
|
6
|
+
REGEXP = /\A#{DateTime::DATE_REGEXP}\z/
|
7
|
+
|
8
|
+
def call(data)
|
9
|
+
match = REGEXP.match(data)
|
10
|
+
raise FormatError, "must be a valid RFC 3339 date string" if match.nil?
|
11
|
+
|
12
|
+
::Date.new(match[:Y].to_i, match[:M].to_i, match[:D].to_i)
|
13
|
+
rescue ::Date::Error
|
14
|
+
raise FormatError, "must be a valid RFC 3339 date string"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class DateTime < Base
|
6
|
+
DATE_REGEXP = /(?<Y>\d{4})-(?<M>\d{2})-(?<D>\d{2})/
|
7
|
+
PARTIAL_TIME = /(?<h>[01]\d|2[0-3]):(?<m>[0-5]\d):(?<s>[0-5]\d|60)(?<f>\.\d+)?/
|
8
|
+
TIME_OFFSET = /[Zz]|(?<on>[+-])(?<oh>[01]\d|2[0-3]):(?<om>[0-5]\d)/
|
9
|
+
FULL_TIME = /#{PARTIAL_TIME}#{TIME_OFFSET}/
|
10
|
+
|
11
|
+
REGEXP = /\A(?<date>#{DATE_REGEXP})[Tt](?<time>#{FULL_TIME})\z/
|
12
|
+
|
13
|
+
def call(data)
|
14
|
+
match = REGEXP.match(data)
|
15
|
+
raise FormatError, "must be a valid RFC 3339 date string" if match.nil?
|
16
|
+
|
17
|
+
Date.call(match[:date])
|
18
|
+
Time.call(match[:time])
|
19
|
+
rescue FormatError
|
20
|
+
raise FormatError, "must be a valid RFC 3339 date string"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class Duration < Base
|
6
|
+
SECOND = /\d+S/
|
7
|
+
MINUTE = /\d+M#{SECOND}?/
|
8
|
+
HOUR = /\d+H#{MINUTE}?/
|
9
|
+
DAY = /\d+D/
|
10
|
+
WEEK = /\d+W/
|
11
|
+
MONTH = /\d+M#{DAY}?/
|
12
|
+
YEAR = /\d+Y#{MONTH}?/
|
13
|
+
TIME = /T(#{HOUR}|#{MINUTE}|#{SECOND})/
|
14
|
+
DATE = /(#{DAY}|#{MONTH}|#{YEAR})#{TIME}?/
|
15
|
+
DURATION = /P(#{DATE}|#{TIME}|#{WEEK})/
|
16
|
+
REGEXP = /\A#{DURATION}\z/
|
17
|
+
|
18
|
+
def call(data)
|
19
|
+
return if REGEXP.match?(data)
|
20
|
+
|
21
|
+
raise FormatError, "#{data} is not a valid duration"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class Email < Base
|
6
|
+
ATOM = /[a-zA-Z0-9!#$%&'*+\-\/=?^_`{|}~]+/
|
7
|
+
DOT_STRING = /#{ATOM}(\.#{ATOM})*/
|
8
|
+
QUOTED_PAIR_SMTP = /\x5C[\x20-\x7E]/
|
9
|
+
QTEXT_SMTP = /[\x20-\x21\x23-\x5B\x5D-\x7E]/
|
10
|
+
Q_CONTENT_SMTP = /#{QTEXT_SMTP}|#{QUOTED_PAIR_SMTP}/
|
11
|
+
QUOTED_STRING = /"(#{Q_CONTENT_SMTP})*"/
|
12
|
+
LOCAL_PART = /#{DOT_STRING}|#{QUOTED_STRING}/
|
13
|
+
LET_DIG = /[a-zA-Z0-9]/
|
14
|
+
LDH_STR = /[a-zA-Z0-9-]*#{LET_DIG}/
|
15
|
+
SUB_DOMAIN = /#{LET_DIG}(#{LDH_STR})?/
|
16
|
+
DOMAIN = /#{SUB_DOMAIN}(\.#{SUB_DOMAIN})*/
|
17
|
+
IPV4 = /((25[0-5]|(2[0-4]|1\d|[1-9])?\d)\.?\b){4}/
|
18
|
+
IPV6_FULL = /IPv6:\h{1,4}(:\h{1,4}){7}/
|
19
|
+
IPV6_COMP = /IPv6:(\h{1,4}(:\h{1,4}){0,5})?::(\h{1,4}(:\h{1,4}){0,5})?/
|
20
|
+
IPV6V4_FULL = /IPv6:\h{1,4}(:\h{1,4}){5}:\d{1,3}(\.\d{1,3}){3}/
|
21
|
+
IPV6V4_COMP = /IPv6:(\h{1,4}(:\h{1,4}){0,3})?::(\h{1,4}(:\h{1,4}){0,3}:)?\d{1,3}(\.\d{1,3}){3}/
|
22
|
+
IPV6 = /#{IPV6_FULL}|#{IPV6_COMP}|#{IPV6V4_FULL}|#{IPV6V4_COMP}/
|
23
|
+
GENERAL_ADDRESS = /#{LDH_STR}:[\x21-\x5A\x5E-\x7E]+/
|
24
|
+
ADDRESS_LITERAL = /\[(#{IPV4}|#{IPV6}|#{GENERAL_ADDRESS})\]/
|
25
|
+
MAILBOX = /#{LOCAL_PART}@(#{DOMAIN}|#{ADDRESS_LITERAL})/
|
26
|
+
|
27
|
+
REGEXP = /\A#{MAILBOX}\z/
|
28
|
+
|
29
|
+
def call(data)
|
30
|
+
return if REGEXP.match?(data)
|
31
|
+
|
32
|
+
raise FormatError, "#{data} is not a valid email"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class Hostname < Base
|
6
|
+
HOSTNAME = /(?=.{1,253}\.?\z)[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?(?:\.[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?)*\.?/
|
7
|
+
|
8
|
+
REGEXP = /\A#{HOSTNAME}\z/i
|
9
|
+
|
10
|
+
def call(data)
|
11
|
+
return if REGEXP.match?(data.value)
|
12
|
+
|
13
|
+
raise FormatError, "#{data} is not a valid hostname"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module JSONSkooma
|
4
|
+
module Validators
|
5
|
+
class IdnEmail < Base
|
6
|
+
UTF8_NON_ASCII = /[\u0080-\u{10FFFF}]/
|
7
|
+
ATOM = /([a-zA-Z0-9!#$%&'*+\-\/=?^_`{|}~]|#{UTF8_NON_ASCII})+/
|
8
|
+
DOT_STRING = /#{ATOM}(\.#{ATOM})*/
|
9
|
+
QTEXT_SMTP = /[\x20-\x21\x23-\x5B\x5D-\x7E]|#{UTF8_NON_ASCII}/
|
10
|
+
Q_CONTENT_SMTP = /#{QTEXT_SMTP}|#{Email::QUOTED_PAIR_SMTP}/
|
11
|
+
QUOTED_STRING = /"(#{Q_CONTENT_SMTP})*"/
|
12
|
+
LOCAL_PART = /#{DOT_STRING}|#{QUOTED_STRING}/
|
13
|
+
LET_DIG = /[a-zA-Z0-9\u0080-\u{10FFFF}]/
|
14
|
+
LDH_STR = /[a-zA-Z0-9\u0080-\u{10FFFF}-]*#{LET_DIG}/
|
15
|
+
SUB_DOMAIN = /#{LET_DIG}(#{LDH_STR})?/
|
16
|
+
DOMAIN = /#{SUB_DOMAIN}(\.#{SUB_DOMAIN})*/
|
17
|
+
GENERAL_ADDRESS = /#{LDH_STR}:[\x21-\x5A\x5E-\x7E]+/
|
18
|
+
ADDRESS_LITERAL = /\[(#{Email::IPV4}|#{Email::IPV6}|#{GENERAL_ADDRESS})\]/
|
19
|
+
MAILBOX = /#{LOCAL_PART}@(#{DOMAIN}|#{ADDRESS_LITERAL})/
|
20
|
+
|
21
|
+
REGEXP = /\A#{MAILBOX}\z/
|
22
|
+
|
23
|
+
def call(data)
|
24
|
+
return if REGEXP.match?(data.value)
|
25
|
+
|
26
|
+
raise FormatError, "#{data} is not a valid IDN email"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|