json_skooma 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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