json_skooma 0.1.0 → 0.2.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -1
  3. data/README.md +39 -0
  4. data/lib/json_skooma/formatters.rb +7 -5
  5. data/lib/json_skooma/inflector.rb +1 -1
  6. data/lib/json_skooma/json_node.rb +22 -18
  7. data/lib/json_skooma/json_pointer.rb +11 -7
  8. data/lib/json_skooma/json_schema.rb +28 -17
  9. data/lib/json_skooma/keywords/applicator/additional_properties.rb +3 -2
  10. data/lib/json_skooma/keywords/applicator/all_of.rb +1 -1
  11. data/lib/json_skooma/keywords/applicator/pattern_properties.rb +1 -1
  12. data/lib/json_skooma/keywords/applicator/prefix_items.rb +1 -1
  13. data/lib/json_skooma/keywords/applicator/properties.rb +1 -1
  14. data/lib/json_skooma/keywords/applicator/property_names.rb +1 -1
  15. data/lib/json_skooma/keywords/core/ref.rb +1 -16
  16. data/lib/json_skooma/keywords/draft_2019_09/items.rb +1 -1
  17. data/lib/json_skooma/keywords/draft_2019_09/unevaluated_items.rb +5 -10
  18. data/lib/json_skooma/keywords/unevaluated/unevaluated_items.rb +7 -11
  19. data/lib/json_skooma/keywords/unevaluated/unevaluated_properties.rb +5 -4
  20. data/lib/json_skooma/keywords/validation/const.rb +1 -1
  21. data/lib/json_skooma/keywords/validation/enum.rb +1 -1
  22. data/lib/json_skooma/keywords/validation/minimum.rb +1 -1
  23. data/lib/json_skooma/keywords/validation/required.rb +2 -3
  24. data/lib/json_skooma/metaschema.rb +1 -1
  25. data/lib/json_skooma/result.rb +71 -33
  26. data/lib/json_skooma/validators/ipv4.rb +4 -7
  27. data/lib/json_skooma/validators/ipv6.rb +8 -6
  28. data/lib/json_skooma/validators/iri.rb +1 -1
  29. data/lib/json_skooma/validators/uri.rb +2 -7
  30. data/lib/json_skooma/version.rb +1 -1
  31. data/lib/json_skooma.rb +1 -1
  32. metadata +3 -4
  33. data/lib/json_skooma/memoizable.rb +0 -21
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1be68fa71fa29965063cf60e9bab950139b697e0222371eb8ce1eec8fc5bf95
4
- data.tar.gz: 83599b9f53a2d41483f2b700200971088db482d57bccf1ce9cee91d641dc27e6
3
+ metadata.gz: 41c787af86379c5c95a01c3180116d12854d11ca7def23569967d35352998da2
4
+ data.tar.gz: 4f3fbca4d35931fe884598c1c827e4434102c25b76973293af31b2af5ce62371
5
5
  SHA512:
6
- metadata.gz: '0691050d428ab71fa386697900c51d6322a7ebb7e2086a86c45079fbae4e2d61cf53d799e504dddde6041afae2668cd792f9d0acc195168ceb7cf7eb49643f16'
7
- data.tar.gz: ecfe5c2115714a08df92dd5ebcd0ddd723a3636e0fdec668d76f4b0471aab0ef3f33bf0d3ab2a8896f0d479a7f10181024b52e004d70efe26575590a97544b9a
6
+ metadata.gz: bef3119c2f4de1880ebde8036ad6517154b82373c80896b7d3d97ce70cb0d39f5b4c458ca2836d8f23439dcde2965b62a61e02586d7d2dfc12892e1f64fef815
7
+ data.tar.gz: 5aadc6b0af540a0d7d5eacae5959f5051a7b932ce9d92bd049bd5348f6d67e1740bba94a9b51be325aace1cdd1c28bf0521a8d325f137f5207afa79b1dd4c0c3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2023-10-23
11
+
12
+ ### Added
13
+
14
+ - Add JSON Schema evaluation against a reference. ([@skryukov])
15
+
16
+ ### Changed
17
+
18
+ - Optimized JSON Schema evaluation. ([@skryukov])
19
+
20
+ ### Fixed
21
+
22
+ - Fix compatibility issues for Ruby < 3.1 and JRuby. ([@skryukov])
23
+ - Fix Zeitwerk eager loading. ([@skryukov])
24
+
10
25
  ## [0.1.0] - 2023-09-27
11
26
 
12
27
  ### Added
@@ -15,7 +30,8 @@ and this project adheres to [Semantic Versioning].
15
30
 
16
31
  [@skryukov]: https://github.com/skryukov
17
32
 
18
- [Unreleased]: https://github.com/skryukov/json_skooma/compare/v0.1.0...HEAD
33
+ [Unreleased]: https://github.com/skryukov/json_skooma/compare/v0.2.0...HEAD
34
+ [0.2.0]: https://github.com/skryukov/json_skooma/compare/v0.1.0...v0.2.0
19
35
  [0.1.0]: https://github.com/skryukov/json_skooma/commits/v0.1.0
20
36
 
21
37
  [Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/json_skooma.svg)](https://rubygems.org/gems/json_skooma)
4
4
  [![Ruby](https://github.com/skryukov/json_skooma/actions/workflows/main.yml/badge.svg)](https://github.com/skryukov/json_skooma/actions/workflows/main.yml)
5
5
 
6
+ <img align="right" height="150" width="150" title="JSONSkooma logo" src="./assets/logo.svg">
7
+
6
8
  JSONSkooma is a Ruby library for validating JSONs against JSON Schemas.
7
9
 
8
10
  ### Features
@@ -78,6 +80,43 @@ result.output(:basic)
78
80
  # "The instance value \"Human\" must be equal to one of the elements in the defined enumeration: [\"Nord\", \"Khajiit\", \"Argonian\", \"Breton\", \"Redguard\", \"Dunmer\", \"Altmer\", \"Bosmer\", \"Orc\", \"Imperial\"]"}]}
79
81
  ```
80
82
 
83
+ ### Evaluating against a reference
84
+
85
+ ```ruby
86
+ require "json_skooma"
87
+
88
+ # Create a registry to store schemas, vocabularies, dialects, etc.
89
+ JSONSkooma.create_registry("2020-12", assert_formats: true)
90
+
91
+ # Load a schema
92
+ schema_hash = {
93
+ "$schema" => "https://json-schema.org/draft/2020-12/schema",
94
+ "$defs" => {
95
+ "Foo": {
96
+ "type" => "object",
97
+ "properties" => {
98
+ "foo" => {"enum" => ["baz"]}
99
+ },
100
+ }
101
+ }
102
+ }
103
+
104
+ schema = JSONSkooma::JSONSchema.new(schema_hash)
105
+
106
+ result = schema.evaluate({foo: "bar"}, ref: "#/$defs/Foo")
107
+
108
+ result.valid? # => false
109
+
110
+ result.output(:basic)
111
+ # {"valid"=>false,
112
+ # "errors"=>
113
+ # [{"instanceLocation"=>"", "keywordLocation"=>"/properties", "absoluteKeywordLocation"=>"urn:uuid:cb8fb0a0-ce16-416f-b5ba-2a6531992be9#/$defs/Foo/properties", "error"=>"Properties [\"foo\"] are invalid"},
114
+ # {"instanceLocation"=>"/foo",
115
+ # "keywordLocation"=>"/properties/foo/enum",
116
+ # "absoluteKeywordLocation"=>"urn:uuid:cb8fb0a0-ce16-416f-b5ba-2a6531992be9#/$defs/Foo/properties/foo/enum",
117
+ # "error"=>"The instance value \"bar\" must be equal to one of the elements in the defined enumeration: [\"baz\"]"}]}
118
+ ```
119
+
81
120
  ## Alternatives
82
121
 
83
122
  - [json_schemer](https://github.com/davishmcclurg/json_schemer) – Draft 4, 6, 7, 2019-09 and 2020-12 compliant
@@ -43,7 +43,7 @@ module JSONSkooma
43
43
  key = valid ? "annotation" : "error"
44
44
  result << node_data(node, key) if node.public_send(key)
45
45
 
46
- node.children.each do |_, child|
46
+ node.children.each_children do |child|
47
47
  collect_nodes(child, valid, result)
48
48
  end
49
49
 
@@ -83,8 +83,9 @@ module JSONSkooma
83
83
  child_key = valid ? "annotations" : "errors"
84
84
  msg_key = valid ? "annotation" : "error"
85
85
 
86
- child_data = node.children.filter_map do |_, child|
87
- node_data(child, valid) if child.valid? == valid
86
+ child_data = []
87
+ node.each_children do |child|
88
+ child_data << node_data(child, valid) if child.valid? == valid
88
89
  end
89
90
 
90
91
  if first || child_data.length > 1
@@ -121,8 +122,9 @@ module JSONSkooma
121
122
  data[msg_key] = node.public_send(msg_key) if node.public_send(msg_key)
122
123
 
123
124
  child_key = valid ? "annotations" : "errors"
124
- child_data = node.children.map do |_, child|
125
- node_data(child)
125
+ child_data = []
126
+ node.each_children do |child|
127
+ child_data << node_data(child)
126
128
  end
127
129
  data[child_key] = child_data if child_data.length > 0
128
130
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSkooma
4
- class Inflector < Zeitwerk::Inflector
4
+ class Inflector < Zeitwerk::GemInflector
5
5
  def camelize(basename, _abspath)
6
6
  if basename.include?("json_")
7
7
  super.gsub("Json", "JSON")
@@ -4,14 +4,11 @@ require "delegate"
4
4
 
5
5
  module JSONSkooma
6
6
  class JSONNode < SimpleDelegator
7
- extend Memoizable
8
-
9
- attr_reader :parent, :root, :key, :type
7
+ attr_reader :parent, :key, :type
10
8
 
11
9
  def initialize(value, key: nil, parent: nil, item_class: JSONNode, **item_params)
12
10
  @key = key
13
11
  @parent = parent
14
- @root = parent&.root || self
15
12
  @item_class = item_class
16
13
  @item_params = item_params
17
14
  @type, data = parse_value(value)
@@ -19,6 +16,10 @@ module JSONSkooma
19
16
  super(data)
20
17
  end
21
18
 
19
+ def root
20
+ @root ||= parent&.root || self
21
+ end
22
+
22
23
  def [](key)
23
24
  case type
24
25
  when "array"
@@ -31,23 +32,24 @@ module JSONSkooma
31
32
  end
32
33
 
33
34
  def value
34
- case type
35
- when "array"
36
- map(&:value)
37
- when "object"
38
- transform_values(&:value)
39
- else
40
- __getobj__
41
- end
35
+ return @value if instance_variable_defined?(:@value)
36
+
37
+ @value =
38
+ case type
39
+ when "array"
40
+ map(&:value)
41
+ when "object"
42
+ transform_values(&:value)
43
+ else
44
+ __getobj__
45
+ end
42
46
  end
43
- memoize :value
44
47
 
45
48
  def path
46
- result = []
47
- each_parent { |node| result.unshift(node.key) }
48
- JSONPointer.new(result)
49
+ return @path if instance_variable_defined?(:@path)
50
+
51
+ @path = @parent.nil? ? JSONPointer.new([]) : @parent.path.child(@key)
49
52
  end
50
- memoize :path
51
53
 
52
54
  def ==(other)
53
55
  return super(other.__getobj__) if other.is_a?(self.class)
@@ -94,7 +96,9 @@ module JSONSkooma
94
96
  end
95
97
 
96
98
  def map_object_value(value)
97
- value.map { |k, v| [k.to_s, @item_class.new(v, key: k.to_s, parent: self, **@item_params)] }.to_h
99
+ value.each_with_object({}) do |(k, v), h|
100
+ h[k.to_s] = @item_class.new(v, key: k.to_s, parent: self, **@item_params)
101
+ end
98
102
  end
99
103
  end
100
104
  end
@@ -5,7 +5,6 @@ require "cgi"
5
5
 
6
6
  module JSONSkooma
7
7
  class JSONPointer < Hana::Pointer
8
- extend Memoizable
9
8
  ESC_REGEX = /[\/^~]/.freeze
10
9
  ESC2 = {"^" => "^^", "~" => "~0", "/" => "~1"}.freeze
11
10
  ESCAPE_REGEX = /([^ a-zA-Z0-9_.\-~\/!$&'()*+,;=]+)/.freeze
@@ -47,14 +46,19 @@ module JSONSkooma
47
46
  end
48
47
 
49
48
  def to_s
50
- return "" if @path == []
51
- return "/" if @path == [""]
52
-
53
- "/" + @path.map(&method(:escape)).join("/")
49
+ return @to_s if instance_variable_defined?(:@to_s)
50
+
51
+ @to_s =
52
+ case @path
53
+ when []
54
+ ""
55
+ when [""]
56
+ "/"
57
+ else
58
+ "/" + @path.map(&method(:escape)).join("/")
59
+ end
54
60
  end
55
61
 
56
- memoize :to_s
57
-
58
62
  def ==(other)
59
63
  return super unless other.is_a?(self.class)
60
64
  other.path == path
@@ -2,10 +2,7 @@
2
2
 
3
3
  module JSONSkooma
4
4
  class JSONSchema < JSONNode
5
- extend Memoizable
6
-
7
5
  attr_reader :uri, :cache_id, :registry
8
-
9
6
  attr_writer :metaschema_uri
10
7
 
11
8
  def initialize(value, registry: Registry::DEFAULT_NAME, cache_id: "default", uri: nil, metaschema_uri: nil, parent: nil, key: nil)
@@ -25,7 +22,9 @@ module JSONSkooma
25
22
  resolve_references if parent.nil?
26
23
  end
27
24
 
28
- def evaluate(instance, result = nil)
25
+ def evaluate(instance, result = nil, ref: nil)
26
+ return resolve_ref(ref).evaluate(instance, result) if ref
27
+
29
28
  instance = JSONSkooma::JSONNode.new(instance) unless instance.is_a?(JSONNode)
30
29
 
31
30
  result ||= Result.new(self, instance)
@@ -43,7 +42,7 @@ module JSONSkooma
43
42
  end
44
43
  end
45
44
 
46
- if result.children.any? { |_, child| !child.passed? && child.instance.path == instance.path }
45
+ if result.children[instance.path]&.any? { |_, child| !child.passed? }
47
46
  result.failure
48
47
  end
49
48
  end
@@ -51,19 +50,32 @@ module JSONSkooma
51
50
  result
52
51
  end
53
52
 
53
+ def resolve_uri(uri)
54
+ uri = URI.parse(uri)
55
+ return uri if uri.absolute?
56
+ return base_uri + uri if base_uri
57
+
58
+ raise Error, "No base URI against which to resolve uri `#{uri}`"
59
+ end
60
+
61
+ def resolve_ref(uri)
62
+ registry.schema(resolve_uri(uri), metaschema_uri: metaschema_uri, cache_id: cache_id)
63
+ end
64
+
54
65
  def validate
55
66
  metaschema.evaluate(self)
56
67
  end
57
68
 
58
69
  def parent_schema
70
+ return @parent_schema if instance_variable_defined?(:@parent_schema)
71
+
59
72
  node = parent
60
73
  while node
61
- return node if node.is_a?(JSONSchema)
74
+ return @parent_schema = node if node.is_a?(JSONSchema)
62
75
 
63
76
  node = node.parent
64
77
  end
65
78
  end
66
- memoize :parent_schema
67
79
 
68
80
  def uri=(uri)
69
81
  return if @uri == uri
@@ -81,9 +93,8 @@ module JSONSkooma
81
93
  end
82
94
 
83
95
  def metaschema_uri
84
- @metaschema_uri || parent_schema&.metaschema_uri
96
+ @metaschema_uri ||= parent_schema&.metaschema_uri
85
97
  end
86
- memoize :metaschema_uri
87
98
 
88
99
  def base_uri
89
100
  return parent_schema&.base_uri unless uri
@@ -93,6 +104,7 @@ module JSONSkooma
93
104
 
94
105
  def canonical_uri
95
106
  return uri if uri
107
+ return @canonical_uri if instance_variable_defined?(:@canonical_uri)
96
108
 
97
109
  keys = []
98
110
  node = self
@@ -102,14 +114,13 @@ module JSONSkooma
102
114
 
103
115
  if node.is_a?(JSONSchema) && node.uri
104
116
  fragment = JSONPointer.new(node.uri.fragment || "") << keys
105
- return node.uri.dup.tap { |u| u.fragment = fragment.to_s }
117
+ return @canonical_uri = node.uri.dup.tap { |u| u.fragment = fragment.to_s }
106
118
  end
107
119
  end
108
120
  end
109
- memoize :canonical_uri
110
121
 
111
122
  def resolve_references
112
- @keywords.each_value { |kw| kw.resolve }
123
+ @keywords.each_value(&:resolve)
113
124
  end
114
125
 
115
126
  private
@@ -150,19 +161,19 @@ module JSONSkooma
150
161
  end
151
162
 
152
163
  def dependencies_in_order(kw_classes)
153
- dependencies = kw_classes.map do |_, kw_class|
154
- [kw_class, kw_class.depends_on.map { |dep| kw_classes[dep] }.compact]
155
- end.to_h
164
+ dependencies = kw_classes.each_value.with_object({}) do |kw_class, res|
165
+ res[kw_class] = kw_class.depends_on.filter_map { |dep| kw_classes[dep] }
166
+ end
156
167
 
157
168
  while dependencies.any?
158
169
  kw_class, _ = dependencies.find { |_, depclasses| depclasses.empty? }
159
170
  dependencies.delete(kw_class)
160
- dependencies.each { |_, deps| deps.delete(kw_class) }
171
+ dependencies.each_value { |deps| deps.delete(kw_class) }
161
172
  yield kw_class
162
173
  end
163
174
  end
164
175
 
165
- def parse_value(value, **options)
176
+ def parse_value(value)
166
177
  case value
167
178
  when true, false
168
179
  ["boolean", value]
@@ -11,13 +11,14 @@ module JSONSkooma
11
11
 
12
12
  def evaluate(instance, result)
13
13
  known_property_names = result.sibling(instance, "properties")&.schema_node&.keys || []
14
- known_property_patterns = result.sibling(instance, "patternProperties")&.schema_node&.keys || []
14
+ known_property_patterns = (result.sibling(instance, "patternProperties")&.schema_node&.keys || [])
15
+ .map { |pattern| Regexp.new(pattern) }
15
16
 
16
17
  annotation = []
17
18
  error = []
18
19
 
19
20
  instance.each do |name, item|
20
- if !known_property_names.include?(name) && !known_property_patterns.any? { |pattern| Regexp.new(pattern).match?(name) }
21
+ if !known_property_names.include?(name) && !known_property_patterns.any? { |pattern| pattern.match?(name) }
21
22
  if json.evaluate(item, result).passed?
22
23
  annotation << name
23
24
  else
@@ -17,7 +17,7 @@ module JSONSkooma
17
17
  end
18
18
  return if err_indices.empty?
19
19
 
20
- result.failure("The instance is invalid against subschemas #{err_indices}")
20
+ result.failure("The instance is invalid against subschemas #{err_indices.join(", ")}")
21
21
  end
22
22
  end
23
23
  end
@@ -35,7 +35,7 @@ module JSONSkooma
35
35
  end
36
36
 
37
37
  if err_names.any?
38
- result.failure("Properties #{err_names} are invalid")
38
+ result.failure("Properties #{err_names.join(", ")} are invalid")
39
39
  else
40
40
  result.annotate(matched_names.to_a)
41
41
  end
@@ -11,7 +11,7 @@ module JSONSkooma
11
11
  def evaluate(instance, result)
12
12
  annotation = nil
13
13
  error = []
14
- instance.take(json.length).each_with_index do |item, index|
14
+ instance.take(json.value.length).each_with_index do |item, index|
15
15
  annotation = index
16
16
  result.call(item, index.to_s) do |subresult|
17
17
  unless json[index].evaluate(item, subresult).passed?
@@ -26,7 +26,7 @@ module JSONSkooma
26
26
 
27
27
  return result.annotate(annotation) if err_names.empty?
28
28
 
29
- result.failure("Properties #{err_names} are invalid")
29
+ result.failure("Properties #{err_names.join(", ")} are invalid")
30
30
  end
31
31
  end
32
32
  end
@@ -10,7 +10,7 @@ module JSONSkooma
10
10
 
11
11
  def evaluate(instance, result)
12
12
  error = []
13
- instance.each do |name,|
13
+ instance.each_key do |name|
14
14
  next if json.evaluate(JSONNode.new(name, key: name, parent: instance), result).passed?
15
15
 
16
16
  error << name
@@ -7,28 +7,13 @@ module JSONSkooma
7
7
  self.key = "$ref"
8
8
 
9
9
  def resolve
10
- uri = resolve_uri
11
- @ref_schema = parent_schema.registry.schema(
12
- uri,
13
- metaschema_uri: parent_schema.metaschema_uri,
14
- cache_id: parent_schema.cache_id
15
- )
10
+ @ref_schema = parent_schema.resolve_ref(json)
16
11
  end
17
12
 
18
13
  def evaluate(instance, result)
19
14
  @ref_schema.evaluate(instance, result)
20
15
  result.ref_schema = @ref_schema
21
16
  end
22
-
23
- private
24
-
25
- def resolve_uri
26
- uri = URI.parse(json)
27
- return uri if uri.absolute?
28
- return parent_schema.base_uri + uri if parent_schema.base_uri
29
-
30
- raise Error, "No base URI against which to resolve the `$ref` value `#{uri}`"
31
- end
32
17
  end
33
18
  end
34
19
  end
@@ -22,7 +22,7 @@ module JSONSkooma
22
22
  annotation = nil
23
23
  error = []
24
24
 
25
- instance.take(json.length).each_with_index do |item, index|
25
+ instance.take(json.value.length).each_with_index do |item, index|
26
26
  annotation = index
27
27
  result.call(item, index.to_s) do |subresult|
28
28
  unless json[index].evaluate(item, subresult).passed?
@@ -12,21 +12,16 @@ module JSONSkooma
12
12
  if then else allOf anyOf oneOf not
13
13
  ]
14
14
 
15
+ LOOKUP_KEYWORDS = %w[items additionalItems unevaluatedItems].freeze
16
+
15
17
  def evaluate(instance, result)
16
18
  last_evaluated_item = -1
17
19
 
18
- result.parent.collect_annotations(instance, "items") do |i|
20
+ result.parent.collect_annotations(instance, keys: LOOKUP_KEYWORDS) do |node|
21
+ i = node.annotation
19
22
  next result.discard if i == true
20
23
 
21
- last_evaluated_item = i if i > last_evaluated_item
22
- end
23
-
24
- result.parent.collect_annotations(instance, "additionalItems") do |i|
25
- next result.discard if i == true
26
- end
27
-
28
- result.parent.collect_annotations(instance, "unevaluatedItems") do |i|
29
- next result.discard if i == true
24
+ last_evaluated_item = i if node.key == "items" && i > last_evaluated_item
30
25
  end
31
26
 
32
27
  annotation = nil
@@ -12,25 +12,21 @@ module JSONSkooma
12
12
  if then else allOf anyOf oneOf not
13
13
  ]
14
14
 
15
+ LOOKUP_KEYWORDS = %w[items unevaluatedItems prefixItems contains].freeze
16
+
15
17
  def evaluate(instance, result)
18
+ contains_indices = Set.new
16
19
  last_evaluated_item = -1
17
20
 
18
- result.parent.collect_annotations(instance, "prefixItems") do |i|
19
- next result.discard if i == true
20
-
21
- last_evaluated_item = i if i > last_evaluated_item
22
- end
21
+ result.parent.collect_annotations(instance, keys: LOOKUP_KEYWORDS) do |node|
22
+ next contains_indices += node.annotation if node.key == "contains"
23
23
 
24
- result.parent.collect_annotations(instance, "items") do |i|
24
+ i = node.annotation
25
25
  next result.discard if i == true
26
- end
27
26
 
28
- result.parent.collect_annotations(instance, "unevaluatedItems") do |i|
29
- next result.discard if i == true
27
+ last_evaluated_item = i if node.key == "prefixItems" && i > last_evaluated_item
30
28
  end
31
29
 
32
- contains_indices = Set.new
33
- result.parent.collect_annotations(instance, "contains") { |i| contains_indices += Set.new(i) }
34
30
  annotation = nil
35
31
  error = []
36
32
 
@@ -12,12 +12,13 @@ module JSONSkooma
12
12
  if then else dependentSchemas allOf anyOf oneOf not
13
13
  ]
14
14
 
15
+ LOOKUP_KEYWORDS = %w[properties patternProperties additionalProperties unevaluatedProperties].freeze
16
+
15
17
  def evaluate(instance, result)
16
18
  evaluated_names = Set.new
17
- result.parent.collect_annotations(instance, "properties") { |name| evaluated_names += Set.new(name) }
18
- result.parent.collect_annotations(instance, "patternProperties") { |name| evaluated_names += Set.new(name) }
19
- result.parent.collect_annotations(instance, "additionalProperties") { |name| evaluated_names += Set.new(name) }
20
- result.parent.collect_annotations(instance, "unevaluatedProperties") { |name| evaluated_names += Set.new(name) }
19
+ result.parent.collect_annotations(instance, keys: LOOKUP_KEYWORDS) do |node|
20
+ evaluated_names += node.annotation
21
+ end
21
22
 
22
23
  annotation = []
23
24
  error = []
@@ -9,7 +9,7 @@ module JSONSkooma
9
9
  def evaluate(instance, result)
10
10
  return if instance == json
11
11
 
12
- result.failure("The instance value #{instance.value.inspect} must be equal to the defined constant #{json.value.inspect}")
12
+ result.failure("The instance value #{instance.value} must be equal to the defined constant #{json.value}")
13
13
  end
14
14
  end
15
15
  end
@@ -10,7 +10,7 @@ module JSONSkooma
10
10
  return if json.include?(instance)
11
11
 
12
12
  result.failure(
13
- "The instance value #{instance.value.inspect} must be equal to one of the elements in the defined enumeration: #{json.value}"
13
+ "The instance value #{instance.value} must be equal to one of the elements in the defined enumeration: #{json.value.join(", ")}"
14
14
  )
15
15
  end
16
16
  end
@@ -8,7 +8,7 @@ module JSONSkooma
8
8
  self.instance_types = "number"
9
9
 
10
10
  def evaluate(instance, result)
11
- if instance < json
11
+ if instance < json.value
12
12
  result.failure("The value may not be less than #{json.value})")
13
13
  end
14
14
  end
@@ -8,10 +8,9 @@ module JSONSkooma
8
8
  self.instance_types = "object"
9
9
 
10
10
  def evaluate(instance, result)
11
- missing_keys = json - instance.keys
12
- return if missing_keys.empty?
11
+ return if json.value.all? { |val| instance.key?(val) }
13
12
 
14
- result.failure("The object is missing required properties #{json.value}")
13
+ result.failure("The object is missing required properties #{json.value.join(", ")}")
15
14
  end
16
15
  end
17
16
  end
@@ -21,7 +21,7 @@ module JSONSkooma
21
21
  def bootstrap(value)
22
22
  super
23
23
 
24
- kw = Keywords::Core::Vocabulary.new(self, value.fetch("$vocabulary", default_vocabulary_urls))
24
+ kw = Keywords::Core::Vocabulary.new(self, value.fetch("$vocabulary") { default_vocabulary_urls })
25
25
  add_keyword(kw) if value.key?("$vocabulary")
26
26
  end
27
27
 
@@ -4,36 +4,68 @@ module JSONSkooma
4
4
  class Result
5
5
  attr_writer :ref_schema
6
6
 
7
- attr_reader :children, :path, :relative_path, :schema, :instance, :parent, :annotation, :error, :key, :root
7
+ attr_reader :schema, :instance, :parent, :annotation, :error, :key
8
8
 
9
9
  def initialize(schema, instance, parent: nil, key: nil)
10
- @schema = schema
11
- @instance = instance
10
+ reset_with(instance, key, parent, schema)
11
+ end
12
+
13
+ def children
14
+ @children ||= {}
15
+ end
16
+
17
+ def each_children
18
+ children.each_value do |child|
19
+ child.each_value do |grandchild|
20
+ yield grandchild
21
+ end
22
+ end
23
+ end
24
+
25
+ def path
26
+ @path ||= @parent.nil? ? JSONPointer.new([]) : parent.path.child(key)
27
+ end
28
+
29
+ def relative_path
30
+ @relative_path ||=
31
+ if @parent.nil?
32
+ JSONPointer.new([])
33
+ elsif schema.equal?(parent.schema)
34
+ parent.relative_path.child(key)
35
+ else
36
+ JSONPointer.new_root(key)
37
+ end
38
+ end
39
+
40
+ def root
41
+ @root ||= parent&.root || self
42
+ end
43
+
44
+ def reset_with(instance, key, parent, schema = nil)
45
+ @schema = schema if schema
12
46
  @parent = parent
13
- @root = parent&.root || self
14
47
  @key = key
15
- @children = {}
16
48
  @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
49
+ @instance = instance
50
+
51
+ @children = nil
52
+ @annotation = nil
53
+ @discard = false
54
+ @skip_assertion = false
55
+ @error = nil
56
+ @path = nil
57
+ @ref_schema = nil
58
+ @relative_path = nil
29
59
  end
30
60
 
31
- def call(instance, key, schema = nil, subclass: self.class)
32
- child = subclass.new(schema || @schema, instance, parent: self, key: key)
61
+ def call(instance, key, schema = nil)
62
+ child = dup
63
+ child.reset_with(instance, key, self, schema)
33
64
 
34
65
  yield child
35
66
 
36
- @children[[key, instance.path]] = child unless child.discard?
67
+ return if child.discard?
68
+ (children[instance.path] ||= {})[key] = child
37
69
  end
38
70
 
39
71
  def schema_node
@@ -41,7 +73,7 @@ module JSONSkooma
41
73
  end
42
74
 
43
75
  def sibling(instance, key)
44
- @parent.children[[key, instance.path]] if @parent
76
+ @parent&.children&.dig(instance.path, key)
45
77
  end
46
78
 
47
79
  def annotate(value)
@@ -86,34 +118,36 @@ module JSONSkooma
86
118
  schema.canonical_uri.dup.tap { |u| u.fragment = path.to_s }
87
119
  end
88
120
 
89
- def collect_annotations(instance = nil, key = nil)
121
+ def collect_annotations(instance, key: nil, keys: nil)
90
122
  return if !valid? || discard?
91
123
 
92
124
  if @annotation &&
93
125
  (key.nil? || key == @key) &&
94
- (instance.nil? || instance.path == @instance.path)
95
- yield @annotation
126
+ (keys.nil? || keys.include?(@key)) &&
127
+ (instance.path == @instance.path)
128
+ yield self
96
129
  end
97
130
 
98
- @children.each do |_, child|
99
- child.collect_annotations(instance, key) do |annotation|
100
- yield annotation
131
+ children[instance.path]&.each_value do |child|
132
+ child.collect_annotations(instance, key: key, keys: keys) do |node|
133
+ yield node
101
134
  end
102
135
  end
103
136
  end
104
137
 
105
- def collect_errors(instance: nil, key: nil)
138
+ def collect_errors(instance, key: nil, keys: nil)
106
139
  return if valid? || discard?
107
140
 
108
141
  if @error &&
109
142
  (key.nil? || key == @key) &&
110
- (instance.nil? || instance.path == @instance.path)
111
- yield @error
143
+ (keys.nil? || keys.include?(@key)) &&
144
+ (instance.path == @instance.path)
145
+ yield self
112
146
  end
113
147
 
114
- @children.each do |_, child|
115
- child.collect_errors(instance, key) do |error|
116
- yield error
148
+ children[instance.path]&.each_value do |child|
149
+ child.collect_errors(instance, key: key, keys: keys) do |node|
150
+ yield node
117
151
  end
118
152
  end
119
153
  end
@@ -121,5 +155,9 @@ module JSONSkooma
121
155
  def output(format, **options)
122
156
  Formatters[format].call(self, **options)
123
157
  end
158
+
159
+ def to_s
160
+ output(:simple)
161
+ end
124
162
  end
125
163
  end
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ipaddr"
4
-
5
3
  module JSONSkooma
6
4
  module Validators
7
5
  class Ipv4 < Base
8
- self.class
6
+ IPV4_ADDRESS = /((25[0-5]|(2[0-4]|1\d|[1-9])?\d)\.?\b){4}/
7
+ REGEXP = /\A#{IPV4_ADDRESS}\z/
9
8
 
10
9
  class << self
11
10
  def call(data)
12
- ip = IPAddr.new "#{data.value}/24"
13
- raise FormatError, "must be a valid IPv4 address" unless ip.ipv4?
14
- rescue IPAddr::Error => e
15
- raise FormatError, e.message
11
+ match = REGEXP.match(data)
12
+ raise FormatError, "must be a valid IPv4 address" if match.nil?
16
13
  end
17
14
  end
18
15
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ipaddr"
4
-
5
3
  module JSONSkooma
6
4
  module Validators
7
5
  class Ipv6 < Base
6
+ H16 = /\h{1,4}/
7
+ LS32 = /(?:#{H16}:#{H16})|#{Ipv4::IPV4_ADDRESS}/
8
+ IPV6_ADDRESS = /(?:#{H16}:){6}#{LS32}|::(?:#{H16}:){5}#{LS32}|(?:#{H16})?::(?:#{H16}:){4}#{LS32}|(?:(?:#{H16}:){0,1}#{H16})?::(?:#{H16}:){3}#{LS32}|(?:(?:#{H16}:){0,2}#{H16})?::(?:#{H16}:){2}#{LS32}|(?:(?:#{H16}:){0,3}#{H16})?::(?:#{H16}:){1}#{LS32}|(?:(?:#{H16}:){0,4}#{H16})?::#{LS32}|(?:(?:#{H16}:){0,5}#{H16})?::#{H16}|(?:(?:#{H16}:){0,6}#{H16})?::/.freeze
9
+
10
+ REGEXP = /\A#{IPV6_ADDRESS}\z/
11
+
8
12
  def call(data)
9
- ip = IPAddr.new "#{data.value}/64"
10
- raise FormatError, "must be a valid IPv6 address" unless ip.ipv6? && ip.zone_id.nil?
11
- rescue IPAddr::Error => e
12
- raise FormatError, e.message
13
+ match = REGEXP.match(data)
14
+ raise FormatError, "must be a valid IPv6 address" if match.nil?
13
15
  end
14
16
  end
15
17
  end
@@ -23,7 +23,7 @@ module JSONSkooma
23
23
  IPATH_OLD = /#{IPATH_ABEMPTY}|#{IPATH_ABSOLUTE}|#{IPATH_NOSCHEME}|#{IPATH_ROOTLESS}|/.freeze
24
24
 
25
25
  IREG_NAME = /(?:#{IUNRESERVED}|#{Uri::PCT_ENCODED}|#{Uri::SUB_DELIMS})*/.freeze
26
- IHOST = /#{Uri::IP_LITERAL}|#{Uri::IPV4_ADDRESS}|#{IREG_NAME}/.freeze
26
+ IHOST = /#{Uri::IP_LITERAL}|#{Ipv4::IPV4_ADDRESS}|#{IREG_NAME}/.freeze
27
27
  IUSERINFO = /(?:#{IUNRESERVED}|#{Uri::PCT_ENCODED}|#{Uri::SUB_DELIMS}|:)*/.freeze
28
28
  IAUTHORITY = /(?:#{IUSERINFO}@)?#{IHOST}(?::#{Uri::PORT})?/.freeze
29
29
 
@@ -22,17 +22,12 @@ module JSONSkooma
22
22
  PATH_ROOTLESS = /#{SEGMENT_NZ}(?:\/#{SEGMENT})*/.freeze
23
23
  PATH = /(?:#{PATH_ABEMPTY}|#{PATH_ABSOLUTE}|#{PATH_NOSCHEME}|#{PATH_ROOTLESS})?/.freeze
24
24
 
25
- IPV4_ADDRESS = /((25[0-5]|(2[0-4]|1\d|[1-9])?\d)\.?\b){4}/
26
- H16 = /\h{1,4}/
27
- LS32 = /(?:#{H16}:#{H16})|#{IPV4_ADDRESS}/
28
- IPV6_ADDRESS = /(?:#{H16}:){6}#{LS32}|::(?:#{H16}:){5}#{LS32}|(?:#{H16})?::(?:#{H16}:){4}#{LS32}|(?:(?:#{H16}:){0,1}#{H16})?::(?:#{H16}:){3}#{LS32}|(?:(?:#{H16}:){0,2}#{H16})?::(?:#{H16}:){2}#{LS32}|(?:(?:#{H16}:){0,3}#{H16})?::(?:#{H16}:){1}#{LS32}|(?:(?:#{H16}:){0,4}#{H16})?::#{LS32}|(?:(?:#{H16}:){0,5}#{H16})?::#{H16}|(?:(?:#{H16}:){0,6}#{H16})?::/.freeze
29
-
30
25
  IPV_FUTURE = /v\h+\.(?:#{UNRESERVED}|#{SUB_DELIMS}|:)+/.freeze
31
- IP_LITERAL = /\[(?:#{IPV6_ADDRESS}|#{IPV_FUTURE})\]/.freeze
26
+ IP_LITERAL = /\[(?:#{Ipv6::IPV6_ADDRESS}|#{IPV_FUTURE})\]/.freeze
32
27
 
33
28
  PORT = /\d*/.freeze
34
29
  REG_NAME = /(?:#{UNRESERVED}|#{PCT_ENCODED}|#{SUB_DELIMS})*/.freeze
35
- HOST = /(?:#{IP_LITERAL}|#{IPV4_ADDRESS}|#{REG_NAME})/.freeze
30
+ HOST = /(?:#{IP_LITERAL}|#{Ipv4::IPV4_ADDRESS}|#{REG_NAME})/.freeze
36
31
  USERINFO = /(?:#{UNRESERVED}|#{PCT_ENCODED}|#{SUB_DELIMS}|:)*/.freeze
37
32
  AUTHORITY = /(?:#{USERINFO}@)?#{HOST}(?::#{PORT})?/.freeze
38
33
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONSkooma
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/json_skooma.rb CHANGED
@@ -6,7 +6,7 @@ require "zeitwerk"
6
6
  require_relative "json_skooma/inflector"
7
7
 
8
8
  loader = Zeitwerk::Loader.for_gem
9
- loader.inflector = JSONSkooma::Inflector.new
9
+ loader.inflector = JSONSkooma::Inflector.new(__FILE__)
10
10
  loader.setup
11
11
 
12
12
  module JSONSkooma
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_skooma
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-09-26 00:00:00.000000000 Z
11
+ date: 2023-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -68,7 +68,7 @@ dependencies:
68
68
  version: '0.1'
69
69
  description: I bring some sugar for your JSONs.
70
70
  email:
71
- - s.g.kryukov@yandex.ru
71
+ - me@skryukov.dev
72
72
  executables: []
73
73
  extensions: []
74
74
  extra_rdoc_files: []
@@ -184,7 +184,6 @@ files:
184
184
  - lib/json_skooma/keywords/validation/type.rb
185
185
  - lib/json_skooma/keywords/validation/unique_items.rb
186
186
  - lib/json_skooma/keywords/value_schemas.rb
187
- - lib/json_skooma/memoizable.rb
188
187
  - lib/json_skooma/metaschema.rb
189
188
  - lib/json_skooma/registry.rb
190
189
  - lib/json_skooma/result.rb
@@ -1,21 +0,0 @@
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