json_skooma 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +107 -0
  5. data/data/draft-2019-09/README.md +60 -0
  6. data/data/draft-2019-09/hyper-schema.json +26 -0
  7. data/data/draft-2019-09/links.json +91 -0
  8. data/data/draft-2019-09/meta/applicator.json +56 -0
  9. data/data/draft-2019-09/meta/content.json +17 -0
  10. data/data/draft-2019-09/meta/core.json +57 -0
  11. data/data/draft-2019-09/meta/format.json +14 -0
  12. data/data/draft-2019-09/meta/hyper-schema.json +29 -0
  13. data/data/draft-2019-09/meta/meta-data.json +37 -0
  14. data/data/draft-2019-09/meta/validation.json +98 -0
  15. data/data/draft-2019-09/output/hyper-schema.json +62 -0
  16. data/data/draft-2019-09/output/schema.json +86 -0
  17. data/data/draft-2019-09/output/verbose-example.json +130 -0
  18. data/data/draft-2019-09/schema.json +42 -0
  19. data/data/draft-2020-12/README.md +89 -0
  20. data/data/draft-2020-12/adr/README.md +15 -0
  21. data/data/draft-2020-12/archive/hyper-schema.json +28 -0
  22. data/data/draft-2020-12/archive/links.json +93 -0
  23. data/data/draft-2020-12/archive/meta/hyper-schema.json +30 -0
  24. data/data/draft-2020-12/hyper-schema.json +27 -0
  25. data/data/draft-2020-12/links.json +85 -0
  26. data/data/draft-2020-12/meta/applicator.json +48 -0
  27. data/data/draft-2020-12/meta/content.json +17 -0
  28. data/data/draft-2020-12/meta/core.json +51 -0
  29. data/data/draft-2020-12/meta/format-annotation.json +14 -0
  30. data/data/draft-2020-12/meta/format-assertion.json +14 -0
  31. data/data/draft-2020-12/meta/hyper-schema.json +29 -0
  32. data/data/draft-2020-12/meta/meta-data.json +37 -0
  33. data/data/draft-2020-12/meta/unevaluated.json +15 -0
  34. data/data/draft-2020-12/meta/validation.json +98 -0
  35. data/data/draft-2020-12/output/hyper-schema.json +62 -0
  36. data/data/draft-2020-12/output/schema.json +96 -0
  37. data/data/draft-2020-12/output/verbose-example.json +130 -0
  38. data/data/draft-2020-12/schema.json +58 -0
  39. data/lib/json_skooma/dialects/draft201909.rb +137 -0
  40. data/lib/json_skooma/dialects/draft202012.rb +146 -0
  41. data/lib/json_skooma/formatters.rb +135 -0
  42. data/lib/json_skooma/inflector.rb +13 -0
  43. data/lib/json_skooma/json_node.rb +100 -0
  44. data/lib/json_skooma/json_pointer.rb +79 -0
  45. data/lib/json_skooma/json_schema.rb +176 -0
  46. data/lib/json_skooma/keywords/applicator/additional_properties.rb +37 -0
  47. data/lib/json_skooma/keywords/applicator/all_of.rb +25 -0
  48. data/lib/json_skooma/keywords/applicator/any_of.rb +26 -0
  49. data/lib/json_skooma/keywords/applicator/contains.rb +31 -0
  50. data/lib/json_skooma/keywords/applicator/dependent_schemas.rb +35 -0
  51. data/lib/json_skooma/keywords/applicator/else.rb +22 -0
  52. data/lib/json_skooma/keywords/applicator/if.rb +17 -0
  53. data/lib/json_skooma/keywords/applicator/items.rb +36 -0
  54. data/lib/json_skooma/keywords/applicator/not.rb +19 -0
  55. data/lib/json_skooma/keywords/applicator/one_of.rb +35 -0
  56. data/lib/json_skooma/keywords/applicator/pattern_properties.rb +46 -0
  57. data/lib/json_skooma/keywords/applicator/prefix_items.rb +31 -0
  58. data/lib/json_skooma/keywords/applicator/properties.rb +34 -0
  59. data/lib/json_skooma/keywords/applicator/property_names.rb +25 -0
  60. data/lib/json_skooma/keywords/applicator/then.rb +22 -0
  61. data/lib/json_skooma/keywords/base.rb +74 -0
  62. data/lib/json_skooma/keywords/base_annotation.rb +12 -0
  63. data/lib/json_skooma/keywords/content/content_encoding.rb +12 -0
  64. data/lib/json_skooma/keywords/content/content_media_type.rb +12 -0
  65. data/lib/json_skooma/keywords/content/content_schema.rb +19 -0
  66. data/lib/json_skooma/keywords/core/anchor.rb +22 -0
  67. data/lib/json_skooma/keywords/core/comment.rb +12 -0
  68. data/lib/json_skooma/keywords/core/defs.rb +13 -0
  69. data/lib/json_skooma/keywords/core/dynamic_anchor.rb +22 -0
  70. data/lib/json_skooma/keywords/core/dynamic_ref.rb +67 -0
  71. data/lib/json_skooma/keywords/core/id.rb +28 -0
  72. data/lib/json_skooma/keywords/core/ref.rb +35 -0
  73. data/lib/json_skooma/keywords/core/schema.rb +26 -0
  74. data/lib/json_skooma/keywords/core/vocabulary.rb +34 -0
  75. data/lib/json_skooma/keywords/draft_2019_09/additional_items.rb +40 -0
  76. data/lib/json_skooma/keywords/draft_2019_09/items.rb +41 -0
  77. data/lib/json_skooma/keywords/draft_2019_09/recursive_anchor.rb +12 -0
  78. data/lib/json_skooma/keywords/draft_2019_09/recursive_ref.rb +46 -0
  79. data/lib/json_skooma/keywords/draft_2019_09/unevaluated_items.rb +56 -0
  80. data/lib/json_skooma/keywords/format_annotation/format.rb +27 -0
  81. data/lib/json_skooma/keywords/meta_data/default.rb +11 -0
  82. data/lib/json_skooma/keywords/meta_data/deprecated.rb +11 -0
  83. data/lib/json_skooma/keywords/meta_data/description.rb +11 -0
  84. data/lib/json_skooma/keywords/meta_data/examples.rb +11 -0
  85. data/lib/json_skooma/keywords/meta_data/read_only.rb +11 -0
  86. data/lib/json_skooma/keywords/meta_data/title.rb +11 -0
  87. data/lib/json_skooma/keywords/meta_data/write_only.rb +11 -0
  88. data/lib/json_skooma/keywords/unevaluated/unevaluated_items.rb +59 -0
  89. data/lib/json_skooma/keywords/unevaluated/unevaluated_properties.rb +43 -0
  90. data/lib/json_skooma/keywords/unknown.rb +21 -0
  91. data/lib/json_skooma/keywords/validation/const.rb +17 -0
  92. data/lib/json_skooma/keywords/validation/dependent_required.rb +24 -0
  93. data/lib/json_skooma/keywords/validation/enum.rb +19 -0
  94. data/lib/json_skooma/keywords/validation/exclusive_maximum.rb +18 -0
  95. data/lib/json_skooma/keywords/validation/exclusive_minimum.rb +18 -0
  96. data/lib/json_skooma/keywords/validation/max_contains.rb +24 -0
  97. data/lib/json_skooma/keywords/validation/max_items.rb +18 -0
  98. data/lib/json_skooma/keywords/validation/max_length.rb +18 -0
  99. data/lib/json_skooma/keywords/validation/max_properties.rb +18 -0
  100. data/lib/json_skooma/keywords/validation/maximum.rb +18 -0
  101. data/lib/json_skooma/keywords/validation/min_contains.rb +31 -0
  102. data/lib/json_skooma/keywords/validation/min_items.rb +18 -0
  103. data/lib/json_skooma/keywords/validation/min_length.rb +18 -0
  104. data/lib/json_skooma/keywords/validation/min_properties.rb +18 -0
  105. data/lib/json_skooma/keywords/validation/minimum.rb +18 -0
  106. data/lib/json_skooma/keywords/validation/multiple_of.rb +20 -0
  107. data/lib/json_skooma/keywords/validation/pattern.rb +23 -0
  108. data/lib/json_skooma/keywords/validation/required.rb +19 -0
  109. data/lib/json_skooma/keywords/validation/type.rb +26 -0
  110. data/lib/json_skooma/keywords/validation/unique_items.rb +20 -0
  111. data/lib/json_skooma/keywords/value_schemas.rb +87 -0
  112. data/lib/json_skooma/memoizable.rb +21 -0
  113. data/lib/json_skooma/metaschema.rb +32 -0
  114. data/lib/json_skooma/registry.rb +130 -0
  115. data/lib/json_skooma/result.rb +125 -0
  116. data/lib/json_skooma/sources.rb +55 -0
  117. data/lib/json_skooma/validators/base.rb +31 -0
  118. data/lib/json_skooma/validators/date.rb +18 -0
  119. data/lib/json_skooma/validators/date_time.rb +24 -0
  120. data/lib/json_skooma/validators/duration.rb +25 -0
  121. data/lib/json_skooma/validators/email.rb +36 -0
  122. data/lib/json_skooma/validators/hostname.rb +17 -0
  123. data/lib/json_skooma/validators/idn_email.rb +30 -0
  124. data/lib/json_skooma/validators/idn_hostname.rb +15 -0
  125. data/lib/json_skooma/validators/ipv4.rb +20 -0
  126. data/lib/json_skooma/validators/ipv6.rb +16 -0
  127. data/lib/json_skooma/validators/iri.rb +47 -0
  128. data/lib/json_skooma/validators/iri_reference.rb +15 -0
  129. data/lib/json_skooma/validators/json_pointer.rb +19 -0
  130. data/lib/json_skooma/validators/regex.rb +15 -0
  131. data/lib/json_skooma/validators/relative_json_pointer.rb +18 -0
  132. data/lib/json_skooma/validators/time.rb +32 -0
  133. data/lib/json_skooma/validators/uri.rb +60 -0
  134. data/lib/json_skooma/validators/uri_reference.rb +15 -0
  135. data/lib/json_skooma/validators/uri_template.rb +26 -0
  136. data/lib/json_skooma/validators/uuid.rb +15 -0
  137. data/lib/json_skooma/validators.rb +17 -0
  138. data/lib/json_skooma/version.rb +5 -0
  139. data/lib/json_skooma/vocabulary.rb +12 -0
  140. data/lib/json_skooma.rb +39 -0
  141. 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