jsi 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +15 -0
  4. data/README.md +105 -38
  5. data/lib/jsi/base.rb +349 -155
  6. data/lib/jsi/jsi_coder.rb +5 -4
  7. data/lib/jsi/metaschema_node/bootstrap_schema.rb +100 -0
  8. data/lib/jsi/metaschema_node.rb +156 -129
  9. data/lib/jsi/pathed_node.rb +47 -49
  10. data/lib/jsi/ptr.rb +292 -0
  11. data/lib/jsi/schema/application/child_application/contains.rb +16 -0
  12. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  13. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  14. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  15. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  16. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  17. data/lib/jsi/schema/application/child_application.rb +40 -0
  18. data/lib/jsi/schema/application/draft04.rb +8 -0
  19. data/lib/jsi/schema/application/draft06.rb +8 -0
  20. data/lib/jsi/schema/application/draft07.rb +8 -0
  21. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  22. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  23. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  24. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  25. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  26. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  27. data/lib/jsi/schema/application/inplace_application/someof.rb +29 -0
  28. data/lib/jsi/schema/application/inplace_application.rb +46 -0
  29. data/lib/jsi/schema/application.rb +12 -0
  30. data/lib/jsi/schema/draft04.rb +14 -0
  31. data/lib/jsi/schema/draft06.rb +14 -0
  32. data/lib/jsi/schema/draft07.rb +14 -0
  33. data/lib/jsi/schema/issue.rb +36 -0
  34. data/lib/jsi/schema/ref.rb +159 -0
  35. data/lib/jsi/schema/schema_ancestor_node.rb +119 -0
  36. data/lib/jsi/schema/validation/array.rb +69 -0
  37. data/lib/jsi/schema/validation/const.rb +20 -0
  38. data/lib/jsi/schema/validation/contains.rb +25 -0
  39. data/lib/jsi/schema/validation/core.rb +39 -0
  40. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  41. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  42. data/lib/jsi/schema/validation/draft04.rb +112 -0
  43. data/lib/jsi/schema/validation/draft06.rb +122 -0
  44. data/lib/jsi/schema/validation/draft07.rb +159 -0
  45. data/lib/jsi/schema/validation/enum.rb +25 -0
  46. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  47. data/lib/jsi/schema/validation/items.rb +54 -0
  48. data/lib/jsi/schema/validation/not.rb +20 -0
  49. data/lib/jsi/schema/validation/numeric.rb +121 -0
  50. data/lib/jsi/schema/validation/object.rb +45 -0
  51. data/lib/jsi/schema/validation/pattern.rb +34 -0
  52. data/lib/jsi/schema/validation/properties.rb +101 -0
  53. data/lib/jsi/schema/validation/property_names.rb +32 -0
  54. data/lib/jsi/schema/validation/ref.rb +40 -0
  55. data/lib/jsi/schema/validation/required.rb +27 -0
  56. data/lib/jsi/schema/validation/someof.rb +90 -0
  57. data/lib/jsi/schema/validation/string.rb +47 -0
  58. data/lib/jsi/schema/validation/type.rb +49 -0
  59. data/lib/jsi/schema/validation.rb +51 -0
  60. data/lib/jsi/schema.rb +486 -133
  61. data/lib/jsi/schema_classes.rb +157 -42
  62. data/lib/jsi/schema_registry.rb +141 -0
  63. data/lib/jsi/schema_set.rb +141 -0
  64. data/lib/jsi/simple_wrap.rb +2 -2
  65. data/lib/jsi/typelike_modules.rb +52 -37
  66. data/lib/jsi/util/attr_struct.rb +106 -0
  67. data/lib/jsi/util.rb +141 -25
  68. data/lib/jsi/validation/error.rb +34 -0
  69. data/lib/jsi/validation/result.rb +210 -0
  70. data/lib/jsi/validation.rb +15 -0
  71. data/lib/jsi/version.rb +3 -1
  72. data/lib/jsi.rb +55 -9
  73. data/lib/schemas/json-schema.org/draft-04/schema.rb +8 -3
  74. data/lib/schemas/json-schema.org/draft-06/schema.rb +8 -3
  75. data/lib/schemas/json-schema.org/draft-07/schema.rb +12 -0
  76. data/readme.rb +138 -0
  77. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  78. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  79. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  80. metadata +69 -118
  81. data/.simplecov +0 -3
  82. data/Rakefile.rb +0 -9
  83. data/jsi.gemspec +0 -28
  84. data/lib/jsi/base/to_rb.rb +0 -128
  85. data/lib/jsi/json/node.rb +0 -203
  86. data/lib/jsi/json/pointer.rb +0 -419
  87. data/lib/jsi/json-schema-fragments.rb +0 -61
  88. data/lib/jsi/json.rb +0 -10
  89. data/resources/icons/AGPL-3.0.png +0 -0
  90. data/test/base_array_test.rb +0 -323
  91. data/test/base_hash_test.rb +0 -337
  92. data/test/base_test.rb +0 -486
  93. data/test/jsi_coder_test.rb +0 -85
  94. data/test/jsi_json_arraynode_test.rb +0 -150
  95. data/test/jsi_json_hashnode_test.rb +0 -132
  96. data/test/jsi_json_node_test.rb +0 -257
  97. data/test/jsi_json_pointer_test.rb +0 -102
  98. data/test/jsi_test.rb +0 -11
  99. data/test/jsi_typelike_as_json_test.rb +0 -53
  100. data/test/metaschema_node_test.rb +0 -19
  101. data/test/schema_module_test.rb +0 -21
  102. data/test/schema_test.rb +0 -208
  103. data/test/spreedly_openapi_test.rb +0 -8
  104. data/test/test_helper.rb +0 -97
  105. data/test/util_test.rb +0 -62
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::Dependencies
5
+ # @private
6
+ def internal_applicate_dependencies(instance, visited_refs, &block)
7
+ if schema_content.key?('dependencies')
8
+ value = schema_content['dependencies']
9
+ # This keyword's value MUST be an object. Each property specifies a dependency. Each dependency
10
+ # value MUST be an array or a valid JSON Schema.
11
+ if value.respond_to?(:to_hash)
12
+ value.each_pair do |property_name, dependency|
13
+ if dependency.respond_to?(:to_ary)
14
+ # noop: array-form dependencies has no inplace applicator schema
15
+ else
16
+ # If the dependency value is a subschema, and the dependency key is a
17
+ # property in the instance, the entire instance must validate against
18
+ # the dependency value.
19
+ if instance.respond_to?(:to_hash) && instance.key?(property_name)
20
+ subschema(['dependencies', property_name]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::Draft04
5
+ include Schema::Application::InplaceApplication
6
+ include Schema::Application::InplaceApplication::Ref
7
+ include Schema::Application::InplaceApplication::Dependencies
8
+ include Schema::Application::InplaceApplication::SomeOf
9
+
10
+ # @private
11
+ def internal_inplace_applicate_keywords(instance, visited_refs, &block)
12
+ internal_applicate_ref(instance, visited_refs, throw_done: true, &block)
13
+
14
+ # self is the first applicator schema if $ref has not short-circuited it
15
+ yield self
16
+
17
+ # 5.4.5. dependencies
18
+ internal_applicate_dependencies(instance, visited_refs, &block)
19
+
20
+ # 5.5.3. allOf
21
+ # 5.5.4. anyOf
22
+ # 5.5.5. oneOf
23
+ internal_applicate_someOf(instance, visited_refs, &block)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::Draft06
5
+ include Schema::Application::InplaceApplication
6
+ include Schema::Application::InplaceApplication::Ref
7
+ include Schema::Application::InplaceApplication::Dependencies
8
+ include Schema::Application::InplaceApplication::SomeOf
9
+
10
+ # @private
11
+ def internal_inplace_applicate_keywords(instance, visited_refs, &block)
12
+ # json-schema 8. Schema references with $ref
13
+ internal_applicate_ref(instance, visited_refs, throw_done: true, &block)
14
+
15
+ # self is the first applicator schema if $ref has not short-circuited it
16
+ yield self
17
+
18
+ # json-schema-validation 6.21. dependencies
19
+ internal_applicate_dependencies(instance, visited_refs, &block)
20
+
21
+ # json-schema-validation 6.26. allOf
22
+ # json-schema-validation 6.27. anyOf
23
+ # json-schema-validation 6.28. oneOf
24
+ internal_applicate_someOf(instance, visited_refs, &block)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::Draft07
5
+ include Schema::Application::InplaceApplication
6
+ include Schema::Application::InplaceApplication::Ref
7
+ include Schema::Application::InplaceApplication::Dependencies
8
+ include Schema::Application::InplaceApplication::IfThenElse
9
+ include Schema::Application::InplaceApplication::SomeOf
10
+
11
+ # @private
12
+ def internal_inplace_applicate_keywords(instance, visited_refs, &block)
13
+ # json-schema 8. Schema references with $ref
14
+ internal_applicate_ref(instance, visited_refs, throw_done: true, &block)
15
+
16
+ # self is the first applicator schema if $ref has not short-circuited it
17
+ block.call(self)
18
+
19
+ # 6.5.7. dependencies
20
+ internal_applicate_dependencies(instance, visited_refs, &block)
21
+
22
+ # 6.6.1. if
23
+ # 6.6.2. then
24
+ # 6.6.3. else
25
+ internal_applicate_ifthenelse(instance, visited_refs, &block)
26
+
27
+ # 6.7.1. allOf
28
+ # 6.7.2. anyOf
29
+ # 6.7.3. oneOf
30
+ internal_applicate_someOf(instance, visited_refs, &block)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::IfThenElse
5
+ # @private
6
+ def internal_applicate_ifthenelse(instance, visited_refs, &block)
7
+ if schema_content.key?('if')
8
+ if subschema(['if']).instance_valid?(instance)
9
+ if schema_content.key?('then')
10
+ subschema(['then']).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
11
+ end
12
+ else
13
+ if schema_content.key?('else')
14
+ subschema(['else']).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::Ref
5
+ # @private
6
+ def internal_applicate_ref(instance, visited_refs, throw_done: false, &block)
7
+ if schema_content['$ref'].respond_to?(:to_str)
8
+ ref = jsi_memoize(:ref) { Schema::Ref.new(schema_content['$ref'], self) }
9
+ unless visited_refs.include?(ref)
10
+ ref.deref_schema.each_inplace_applicator_schema(instance, visited_refs: visited_refs + [ref], &block)
11
+ if throw_done
12
+ throw(:jsi_application_done)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication::SomeOf
5
+ # @private
6
+ def internal_applicate_someOf(instance, visited_refs, &block)
7
+ if schema_content['allOf'].respond_to?(:to_ary)
8
+ schema_content['allOf'].each_index do |i|
9
+ subschema(['allOf', i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
10
+ end
11
+ end
12
+ if schema_content['anyOf'].respond_to?(:to_ary)
13
+ schema_content['anyOf'].each_index do |i|
14
+ if subschema(['anyOf', i]).instance_valid?(instance)
15
+ subschema(['anyOf', i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
16
+ end
17
+ end
18
+ end
19
+ if schema_content['oneOf'].respond_to?(:to_ary)
20
+ one_i = schema_content['oneOf'].each_index.detect do |i|
21
+ subschema(['oneOf', i]).instance_valid?(instance)
22
+ end
23
+ if one_i
24
+ subschema(['oneOf', one_i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application::InplaceApplication
5
+ autoload :Draft04, 'jsi/schema/application/inplace_application/draft04'
6
+ autoload :Draft06, 'jsi/schema/application/inplace_application/draft06'
7
+ autoload :Draft07, 'jsi/schema/application/inplace_application/draft07'
8
+
9
+ autoload :Ref, 'jsi/schema/application/inplace_application/ref'
10
+ autoload :SomeOf, 'jsi/schema/application/inplace_application/someof'
11
+ autoload :IfThenElse, 'jsi/schema/application/inplace_application/ifthenelse'
12
+ autoload :Dependencies, 'jsi/schema/application/inplace_application/dependencies'
13
+
14
+ # a set of inplace applicator schemas of this schema (from $ref, allOf, etc.) which apply to the
15
+ # given instance.
16
+ #
17
+ # the returned set will contain this schema itself, unless this schema contains a $ref keyword.
18
+ #
19
+ # @param instance [Object] the instance to check any applicators against
20
+ # @return [JSI::SchemaSet] matched applicator schemas
21
+ def inplace_applicator_schemas(instance)
22
+ SchemaSet.new(each_inplace_applicator_schema(instance))
23
+ end
24
+
25
+ # yields each inplace applicator schema which applies to the given instance.
26
+ #
27
+ # @param instance (see #inplace_applicator_schemas)
28
+ # @param visited_refs [Enumerable<JSI::Schema::Ref>]
29
+ # @yield [JSI::Schema]
30
+ # @return [nil, Enumerator] an Enumerator if invoked without a block; otherwise nil
31
+ def each_inplace_applicator_schema(instance, visited_refs: [], &block)
32
+ return to_enum(__method__, instance, visited_refs: visited_refs) unless block
33
+
34
+ catch(:jsi_application_done) do
35
+ if schema_content.respond_to?(:to_hash)
36
+ internal_inplace_applicate_keywords(instance, visited_refs, &block)
37
+ else
38
+ # self is the only applicator schema if there are no keywords
39
+ yield self
40
+ end
41
+ end
42
+
43
+ nil
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Application
5
+ autoload :InplaceApplication, 'jsi/schema/application/inplace_application'
6
+ autoload :ChildApplication, 'jsi/schema/application/child_application'
7
+
8
+ autoload :Draft04, 'jsi/schema/application/draft04'
9
+ autoload :Draft06, 'jsi/schema/application/draft06'
10
+ autoload :Draft07, 'jsi/schema/application/draft07'
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema
5
+ module Draft04
6
+ include Schema
7
+ include OldId
8
+ include IdWithAnchor
9
+ include IntegerDisallows0Fraction
10
+ include Schema::Application::Draft04
11
+ include Schema::Validation::Draft04
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema
5
+ module Draft06
6
+ include Schema
7
+ include BigMoneyId
8
+ include IdWithAnchor
9
+ include IntegerAllows0Fraction
10
+ include Schema::Application::Draft06
11
+ include Schema::Validation::Draft06
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema
5
+ module Draft07
6
+ include Schema
7
+ include BigMoneyId
8
+ include IdWithAnchor
9
+ include IntegerAllows0Fraction
10
+ include Schema::Application::Draft07
11
+ include Schema::Validation::Draft07
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema
5
+ Issue = Util::AttrStruct[*%w(
6
+ level
7
+ message
8
+ keyword
9
+ schema
10
+ )]
11
+
12
+ # an issue or problem with a schema.
13
+ #
14
+ # when the `level` is `:error`, the schema is invalid according to its specification,
15
+ # violating some "MUST" or "MUST NOT".
16
+ #
17
+ # when the `level` is `:warning`, the issue does not mean the schema is invalid, but contains something
18
+ # that does not make sense. for example, specifying `additionalItems` without an adjacent `items` has
19
+ # no effect (in specifications which define `additionalItems`), but is not an invalid schema.
20
+ #
21
+ # @!attribute level
22
+ # :error or :warning
23
+ # @return [Symbol]
24
+ # @!attribute message
25
+ # a message describing the issue
26
+ # @return [String]
27
+ # @!attribute keyword
28
+ # the keyword of the schema that has an issue
29
+ # @return [String]
30
+ # @!attribute schema
31
+ # the schema that has an issue
32
+ # @return [JSI::Schema]
33
+ class Issue
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # JSI::Schema::Ref is a reference to another schema (the result of #deref_schema), resolved using a ref URI
5
+ # from a ref schema (the ref URI typically the contents of the ref_schema's "$ref" keyword)
6
+ class Schema::Ref
7
+ # @param ref [String] a reference URI
8
+ # @param ref_schema [JSI::Schema] a schema from which the reference originated
9
+ def initialize(ref, ref_schema = nil)
10
+ raise(ArgumentError, "ref is not a string") unless ref.respond_to?(:to_str)
11
+ @ref = ref
12
+ @ref_uri = Addressable::URI.parse(ref)
13
+ @ref_schema = ref_schema ? Schema.ensure_schema(ref_schema) : nil
14
+ end
15
+
16
+ attr_reader :ref
17
+
18
+ attr_reader :ref_uri
19
+
20
+ attr_reader :ref_schema
21
+
22
+ # finds the schema this ref points to
23
+ # @return [JSI::Schema]
24
+ # @raise [JSI::Schema::NotASchemaError] when the thing this ref points to is not a schema
25
+ # @raise [JSI::Schema::ReferenceError] when this reference cannot be resolved
26
+ def deref_schema
27
+ return @deref_schema if instance_variable_defined?(:@deref_schema)
28
+
29
+ schema_resource_root = nil
30
+ check_schema_resource_root = -> {
31
+ unless schema_resource_root
32
+ raise(Schema::ReferenceError, [
33
+ "cannot find schema by ref: #{ref}",
34
+ ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema),
35
+ ].compact.join("\n"))
36
+ end
37
+ }
38
+
39
+ ref_uri_nofrag = ref_uri.merge(fragment: nil)
40
+
41
+ if ref_uri_nofrag.empty?
42
+ unless ref_schema
43
+ raise(Schema::ReferenceError, [
44
+ "cannot find schema by ref: #{ref}",
45
+ "with no ref schema",
46
+ ].join("\n"))
47
+ end
48
+
49
+ # the URI only consists of a fragment (or is empty).
50
+ # for a fragment pointer, resolve using Schema#resource_root_subschema on the ref_schema.
51
+ # for a fragment anchor, bootstrap does not support anchors; otherwise use the ref_schema's schema_resource_root.
52
+ schema_resource_root = ref_schema.is_a?(MetaschemaNode::BootstrapSchema) ? nil : ref_schema.schema_resource_root
53
+ resolve_fragment_ptr = ref_schema.method(:resource_root_subschema)
54
+ else
55
+ # find the schema_resource_root from the non-fragment URI. we will resolve any fragment, either pointer or anchor, from there.
56
+ schema_resource_root = nil
57
+
58
+ if ref_uri_nofrag.absolute?
59
+ ref_abs_uri = ref_uri_nofrag
60
+ elsif ref_schema && ref_schema.jsi_resource_ancestor_uri
61
+ ref_abs_uri = ref_schema.jsi_resource_ancestor_uri.join(ref_uri_nofrag)
62
+ else
63
+ ref_abs_uri = nil
64
+ end
65
+ if ref_abs_uri
66
+ schema_resource_root = JSI.schema_registry.find(ref_abs_uri)
67
+ end
68
+
69
+ unless schema_resource_root
70
+ # HAX for how google does refs and ids
71
+ if ref_schema && ref_schema.jsi_document.respond_to?(:to_hash) && ref_schema.jsi_document['schemas'].respond_to?(:to_hash)
72
+ ref_schema.jsi_document['schemas'].each_key do |k|
73
+ if Addressable::URI.parse(ref_schema.jsi_document['schemas'][k]['id']) == ref_uri_nofrag
74
+ schema_resource_root = ref_schema.resource_root_subschema(['schemas', k])
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ check_schema_resource_root.call
81
+
82
+ if schema_resource_root.is_a?(Schema)
83
+ resolve_fragment_ptr = schema_resource_root.method(:resource_root_subschema)
84
+ else
85
+ # Note: Schema#resource_root_subschema will reinstantiate nonschemas as schemas.
86
+ # not implemented for remote refs when the schema_resource_root is not a schema.
87
+ resolve_fragment_ptr = -> (ptr) { ptr.evaluate(schema_resource_root) }
88
+ end
89
+ end
90
+
91
+ fragment = ref_uri.fragment
92
+
93
+ if fragment
94
+ begin
95
+ ptr_from_fragment = Ptr.from_fragment(fragment)
96
+ rescue Ptr::PointerSyntaxError
97
+ end
98
+ end
99
+
100
+ if ptr_from_fragment
101
+ begin
102
+ result_schema = resolve_fragment_ptr.call(ptr_from_fragment)
103
+ rescue Ptr::ResolutionError
104
+ raise(Schema::ReferenceError, [
105
+ "could not resolve pointer: #{ptr_from_fragment.pointer.inspect}",
106
+ ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema),
107
+ ("in schema resource root: #{schema_resource_root.pretty_inspect.chomp}" if schema_resource_root),
108
+ ].compact.join("\n"))
109
+ end
110
+ elsif fragment.nil?
111
+ check_schema_resource_root.call
112
+ result_schema = schema_resource_root
113
+ else
114
+ check_schema_resource_root.call
115
+
116
+ # find an anchor that resembles the fragment
117
+ result_schemas = schema_resource_root.jsi_anchor_subschemas(fragment)
118
+
119
+ if result_schemas.size == 1
120
+ result_schema = result_schemas.first
121
+ elsif result_schemas.size == 0
122
+ raise(Schema::ReferenceError, [
123
+ "could not find schema by fragment: #{fragment.inspect}",
124
+ "in schema resource root: #{schema_resource_root.pretty_inspect.chomp}",
125
+ ].join("\n"))
126
+ else
127
+ raise(Schema::ReferenceError, [
128
+ "found multiple schemas for plain name fragment #{fragment.inspect}:",
129
+ *result_schemas.map { |s| s.pretty_inspect.chomp },
130
+ ].join("\n"))
131
+ end
132
+ end
133
+
134
+ Schema.ensure_schema(result_schema, msg: "object identified by uri #{ref} is not a schema:")
135
+ return @deref_schema = result_schema
136
+ end
137
+
138
+ # @return [String]
139
+ def inspect
140
+ %Q(\#<#{self.class.name} #{ref}>)
141
+ end
142
+
143
+ # pretty-prints a representation of self to the given printer
144
+ # @return [void]
145
+ def pretty_print(q)
146
+ q.text '#<'
147
+ q.text self.class.name
148
+ q.text ' '
149
+ q.text ref
150
+ q.text '>'
151
+ end
152
+
153
+ # @private
154
+ def jsi_fingerprint
155
+ {class: self.class, ref: ref, ref_schema: ref_schema}
156
+ end
157
+ include Util::FingerprintHash
158
+ end
159
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ # a node in a document which may contain a schema somewhere within is extended with SchemaAncestorNode, for
5
+ # tracking things necessary for a schema to function correctly
6
+ module Schema::SchemaAncestorNode
7
+ # the base URI used to resolve the ids of schemas at or below this JSI.
8
+ # this is always an absolute URI (with no fragment).
9
+ # this may be the absolute schema URI of a parent schema or the URI from which the document was retrieved.
10
+ # @private
11
+ # @return [Addressable::URI, nil]
12
+ attr_reader :jsi_schema_base_uri
13
+
14
+ # resources which are ancestors of this JSI in the document. this does not include self.
15
+ # @private
16
+ # @return [Array<JSI::Schema>]
17
+ def jsi_schema_resource_ancestors
18
+ return @jsi_schema_resource_ancestors if instance_variable_defined?(:@jsi_schema_resource_ancestors)
19
+ [].freeze
20
+ end
21
+
22
+ # the URI of the resource containing this node.
23
+ # this is always an absolute URI (with no fragment).
24
+ # if this node is a schema with an id, this is its absolute URI; otherwise a parent resource's URI,
25
+ # or nil if not contained by a resource with a URI.
26
+ # @return [Addressable::URI, nil]
27
+ def jsi_resource_ancestor_uri
28
+ if is_a?(Schema) && schema_absolute_uri
29
+ schema_absolute_uri
30
+ else
31
+ jsi_schema_base_uri
32
+ end
33
+ end
34
+
35
+ # a schema at or below this node with the given anchor.
36
+ #
37
+ # @return [JSI::Schema, nil]
38
+ def jsi_anchor_subschema(anchor)
39
+ subschemas = jsi_anchor_subschemas_map[anchor]
40
+ if subschemas.size == 1
41
+ subschemas.first
42
+ else
43
+ nil
44
+ end
45
+ end
46
+
47
+ # schemas at or below node with the given anchor.
48
+ #
49
+ # @return [Array<JSI::Schema>]
50
+ def jsi_anchor_subschemas(anchor)
51
+ jsi_anchor_subschemas_map[anchor]
52
+ end
53
+
54
+ private
55
+
56
+ def jsi_document=(jsi_document)
57
+ @jsi_document = jsi_document
58
+ end
59
+
60
+ def jsi_ptr=(jsi_ptr)
61
+ unless jsi_ptr.is_a?(Ptr)
62
+ raise(TypeError, "jsi_ptr must be a JSI::Ptr; got: #{jsi_ptr.inspect}")
63
+ end
64
+ @jsi_ptr = jsi_ptr
65
+ end
66
+
67
+ def jsi_schema_base_uri=(jsi_schema_base_uri)
68
+ if jsi_schema_base_uri
69
+ unless jsi_schema_base_uri.respond_to?(:to_str)
70
+ raise(TypeError, "jsi_schema_base_uri must be string or Addressable::URI; got: #{jsi_schema_base_uri.inspect}")
71
+ end
72
+ @jsi_schema_base_uri = Addressable::URI.parse(jsi_schema_base_uri).freeze
73
+ unless @jsi_schema_base_uri.absolute? && !@jsi_schema_base_uri.fragment
74
+ raise(ArgumentError, "jsi_schema_base_uri must be an absolute URI with no fragment; got: #{jsi_schema_base_uri.inspect}")
75
+ end
76
+ else
77
+ @jsi_schema_base_uri = nil
78
+ end
79
+ end
80
+
81
+ def jsi_schema_resource_ancestors=(jsi_schema_resource_ancestors)
82
+ if jsi_schema_resource_ancestors
83
+ unless jsi_schema_resource_ancestors.respond_to?(:to_ary)
84
+ raise(TypeError, "jsi_schema_resource_ancestors must be an array; got: #{jsi_schema_resource_ancestors.inspect}")
85
+ end
86
+ jsi_schema_resource_ancestors.each { |a| Schema.ensure_schema(a) }
87
+ # sanity check the ancestors are in order
88
+ last_anc_ptr = nil
89
+ jsi_schema_resource_ancestors.each do |anc|
90
+ if last_anc_ptr.nil?
91
+ # pass
92
+ elsif last_anc_ptr == anc.jsi_ptr
93
+ raise(Bug, "duplicate ancestors in #{jsi_schema_resource_ancestors.pretty_inspect}")
94
+ elsif !last_anc_ptr.contains?(anc.jsi_ptr)
95
+ raise(Bug, "ancestor ptr #{anc.jsi_ptr} not contained by previous: #{last_anc_ptr} in #{jsi_schema_resource_ancestors.pretty_inspect}")
96
+ end
97
+ if anc.jsi_ptr == jsi_ptr
98
+ raise(Bug, "ancestor is self")
99
+ elsif !anc.jsi_ptr.contains?(jsi_ptr)
100
+ raise(Bug, "ancestor does not contain self")
101
+ end
102
+ last_anc_ptr = anc.jsi_ptr
103
+ end
104
+
105
+ @jsi_schema_resource_ancestors = jsi_schema_resource_ancestors.to_ary.freeze
106
+ else
107
+ @jsi_schema_resource_ancestors = [].freeze
108
+ end
109
+ end
110
+
111
+ def jsi_anchor_subschemas_map
112
+ jsi_memomap(__method__) do |anchor|
113
+ jsi_each_child_node.select do |node|
114
+ node.is_a?(Schema) && node.respond_to?(:anchor) && node.anchor == anchor
115
+ end.freeze
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Validation::ArrayLength
5
+ # @private
6
+ def internal_validate_maxItems(result_builder)
7
+ if schema_content.key?('maxItems')
8
+ value = schema_content['maxItems']
9
+ # The value of this keyword MUST be a non-negative integer.
10
+ if internal_integer?(value) && value >= 0
11
+ if result_builder.instance.respond_to?(:to_ary)
12
+ # An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword.
13
+ result_builder.validate(
14
+ result_builder.instance.to_ary.size <= value,
15
+ 'instance array size is greater than `maxItems` value',
16
+ keyword: 'maxItems',
17
+ )
18
+ end
19
+ else
20
+ result_builder.schema_error('`maxItems` is not a non-negative integer', 'maxItems')
21
+ end
22
+ end
23
+ end
24
+
25
+ # @private
26
+ def internal_validate_minItems(result_builder)
27
+ if schema_content.key?('minItems')
28
+ value = schema_content['minItems']
29
+ # The value of this keyword MUST be a non-negative integer.
30
+ if internal_integer?(value) && value >= 0
31
+ if result_builder.instance.respond_to?(:to_ary)
32
+ # An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword.
33
+ result_builder.validate(
34
+ result_builder.instance.to_ary.size >= value,
35
+ 'instance array size is less than `minItems` value',
36
+ keyword: 'minItems',
37
+ )
38
+ end
39
+ else
40
+ result_builder.schema_error('`minItems` is not a non-negative integer', 'minItems')
41
+ end
42
+ end
43
+ end
44
+ end
45
+ module Schema::Validation::UniqueItems
46
+ # @private
47
+ def internal_validate_uniqueItems(result_builder)
48
+ if schema_content.key?('uniqueItems')
49
+ value = schema_content['uniqueItems']
50
+ # The value of this keyword MUST be a boolean.
51
+ if value == false
52
+ # If this keyword has boolean value false, the instance validates successfully.
53
+ # (noop)
54
+ elsif value == true
55
+ if result_builder.instance.respond_to?(:to_ary)
56
+ # If it has boolean value true, the instance validates successfully if all of its elements are unique.
57
+ result_builder.validate(
58
+ result_builder.instance.uniq.size == result_builder.instance.size,
59
+ "instance array items' uniqueness does not match `uniqueItems` value",
60
+ keyword: 'uniqueItems',
61
+ )
62
+ end
63
+ else
64
+ result_builder.schema_error('`uniqueItems` is not a boolean', 'uniqueItems')
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end