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