json_skooma 0.1.0
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/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
|