jsi 0.4.0 → 0.7.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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.md +33 -0
  4. data/LICENSE.md +1 -1
  5. data/README.md +114 -42
  6. data/jsi.gemspec +14 -12
  7. data/lib/jsi/base/node.rb +183 -0
  8. data/lib/jsi/base.rb +388 -220
  9. data/lib/jsi/jsi_coder.rb +8 -7
  10. data/lib/jsi/metaschema.rb +0 -1
  11. data/lib/jsi/metaschema_node/bootstrap_schema.rb +101 -0
  12. data/lib/jsi/metaschema_node.rb +159 -135
  13. data/lib/jsi/ptr.rb +303 -0
  14. data/lib/jsi/schema/application/child_application/contains.rb +25 -0
  15. data/lib/jsi/schema/application/child_application/draft04.rb +22 -0
  16. data/lib/jsi/schema/application/child_application/draft06.rb +29 -0
  17. data/lib/jsi/schema/application/child_application/draft07.rb +29 -0
  18. data/lib/jsi/schema/application/child_application/items.rb +18 -0
  19. data/lib/jsi/schema/application/child_application/properties.rb +25 -0
  20. data/lib/jsi/schema/application/child_application.rb +38 -0
  21. data/lib/jsi/schema/application/draft04.rb +8 -0
  22. data/lib/jsi/schema/application/draft06.rb +8 -0
  23. data/lib/jsi/schema/application/draft07.rb +8 -0
  24. data/lib/jsi/schema/application/inplace_application/dependencies.rb +28 -0
  25. data/lib/jsi/schema/application/inplace_application/draft04.rb +26 -0
  26. data/lib/jsi/schema/application/inplace_application/draft06.rb +27 -0
  27. data/lib/jsi/schema/application/inplace_application/draft07.rb +33 -0
  28. data/lib/jsi/schema/application/inplace_application/ifthenelse.rb +20 -0
  29. data/lib/jsi/schema/application/inplace_application/ref.rb +18 -0
  30. data/lib/jsi/schema/application/inplace_application/someof.rb +44 -0
  31. data/lib/jsi/schema/application/inplace_application.rb +41 -0
  32. data/lib/jsi/schema/application.rb +12 -0
  33. data/lib/jsi/schema/draft04.rb +14 -0
  34. data/lib/jsi/schema/draft06.rb +14 -0
  35. data/lib/jsi/schema/draft07.rb +14 -0
  36. data/lib/jsi/schema/issue.rb +36 -0
  37. data/lib/jsi/schema/ref.rb +160 -0
  38. data/lib/jsi/schema/schema_ancestor_node.rb +113 -0
  39. data/lib/jsi/schema/validation/array.rb +69 -0
  40. data/lib/jsi/schema/validation/const.rb +20 -0
  41. data/lib/jsi/schema/validation/contains.rb +25 -0
  42. data/lib/jsi/schema/validation/core.rb +39 -0
  43. data/lib/jsi/schema/validation/dependencies.rb +49 -0
  44. data/lib/jsi/schema/validation/draft04/minmax.rb +91 -0
  45. data/lib/jsi/schema/validation/draft04.rb +112 -0
  46. data/lib/jsi/schema/validation/draft06.rb +122 -0
  47. data/lib/jsi/schema/validation/draft07.rb +159 -0
  48. data/lib/jsi/schema/validation/enum.rb +25 -0
  49. data/lib/jsi/schema/validation/ifthenelse.rb +46 -0
  50. data/lib/jsi/schema/validation/items.rb +54 -0
  51. data/lib/jsi/schema/validation/not.rb +20 -0
  52. data/lib/jsi/schema/validation/numeric.rb +121 -0
  53. data/lib/jsi/schema/validation/object.rb +45 -0
  54. data/lib/jsi/schema/validation/pattern.rb +34 -0
  55. data/lib/jsi/schema/validation/properties.rb +101 -0
  56. data/lib/jsi/schema/validation/property_names.rb +32 -0
  57. data/lib/jsi/schema/validation/ref.rb +40 -0
  58. data/lib/jsi/schema/validation/required.rb +27 -0
  59. data/lib/jsi/schema/validation/someof.rb +90 -0
  60. data/lib/jsi/schema/validation/string.rb +47 -0
  61. data/lib/jsi/schema/validation/type.rb +49 -0
  62. data/lib/jsi/schema/validation.rb +51 -0
  63. data/lib/jsi/schema.rb +508 -149
  64. data/lib/jsi/schema_classes.rb +199 -59
  65. data/lib/jsi/schema_registry.rb +151 -0
  66. data/lib/jsi/schema_set.rb +181 -0
  67. data/lib/jsi/simple_wrap.rb +23 -4
  68. data/lib/jsi/util/private/attr_struct.rb +127 -0
  69. data/lib/jsi/util/private.rb +204 -0
  70. data/lib/jsi/util/typelike.rb +229 -0
  71. data/lib/jsi/util.rb +89 -53
  72. data/lib/jsi/validation/error.rb +34 -0
  73. data/lib/jsi/validation/result.rb +210 -0
  74. data/lib/jsi/validation.rb +15 -0
  75. data/lib/jsi/version.rb +3 -1
  76. data/lib/jsi.rb +44 -14
  77. data/lib/schemas/json-schema.org/draft-04/schema.rb +10 -3
  78. data/lib/schemas/json-schema.org/draft-06/schema.rb +10 -3
  79. data/lib/schemas/json-schema.org/draft-07/schema.rb +14 -0
  80. data/readme.rb +138 -0
  81. data/{resources}/schemas/json-schema.org/draft-04/schema.json +149 -0
  82. data/{resources}/schemas/json-schema.org/draft-06/schema.json +154 -0
  83. data/{resources}/schemas/json-schema.org/draft-07/schema.json +168 -0
  84. metadata +75 -122
  85. data/.simplecov +0 -3
  86. data/Rakefile.rb +0 -9
  87. data/lib/jsi/base/to_rb.rb +0 -128
  88. data/lib/jsi/json/node.rb +0 -203
  89. data/lib/jsi/json/pointer.rb +0 -419
  90. data/lib/jsi/json-schema-fragments.rb +0 -61
  91. data/lib/jsi/json.rb +0 -10
  92. data/lib/jsi/pathed_node.rb +0 -118
  93. data/lib/jsi/typelike_modules.rb +0 -240
  94. data/resources/icons/AGPL-3.0.png +0 -0
  95. data/test/base_array_test.rb +0 -323
  96. data/test/base_hash_test.rb +0 -337
  97. data/test/base_test.rb +0 -486
  98. data/test/jsi_coder_test.rb +0 -85
  99. data/test/jsi_json_arraynode_test.rb +0 -150
  100. data/test/jsi_json_hashnode_test.rb +0 -132
  101. data/test/jsi_json_node_test.rb +0 -257
  102. data/test/jsi_json_pointer_test.rb +0 -102
  103. data/test/jsi_test.rb +0 -11
  104. data/test/jsi_typelike_as_json_test.rb +0 -53
  105. data/test/metaschema_node_test.rb +0 -19
  106. data/test/schema_module_test.rb +0 -21
  107. data/test/schema_test.rb +0 -208
  108. data/test/spreedly_openapi_test.rb +0 -8
  109. data/test/test_helper.rb +0 -97
  110. data/test/util_test.rb +0 -62
@@ -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 keyword?('$ref') && 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,44 @@
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 keyword?('allOf') && 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 keyword?('anyOf') && schema_content['anyOf'].respond_to?(:to_ary)
13
+ anyOf = schema_content['anyOf'].each_index.map { |i| subschema(['anyOf', i]) }
14
+ validOf = anyOf.select { |schema| schema.instance_valid?(instance) }
15
+ if !validOf.empty?
16
+ applicators = validOf
17
+ else
18
+ # invalid application: if none of the anyOf were valid, we apply them all
19
+ applicators = anyOf
20
+ end
21
+
22
+ applicators.each do |applicator|
23
+ applicator.each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
24
+ end
25
+ end
26
+ if keyword?('oneOf') && schema_content['oneOf'].respond_to?(:to_ary)
27
+ oneOf_idxs = schema_content['oneOf'].each_index
28
+ subschema_idx_valid = Hash.new { |h, i| h[i] = subschema(['oneOf', i]).instance_valid?(instance) }
29
+ # count up to 2 `oneOf` subschemas which `instance` validates against
30
+ nvalid = oneOf_idxs.inject(0) { |n, i| n > 1 ? n : subschema_idx_valid[i] ? n + 1 : n }
31
+ if nvalid == 1
32
+ applicator_idxs = oneOf_idxs.select { |i| subschema_idx_valid[i] }
33
+ else
34
+ # invalid application: if none or multiple of the oneOf were valid, we apply them all
35
+ applicator_idxs = oneOf_idxs
36
+ end
37
+
38
+ applicator_idxs.each do |i|
39
+ subschema(['oneOf', i]).each_inplace_applicator_schema(instance, visited_refs: visited_refs, &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
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
+ internal_inplace_applicate_keywords(instance, visited_refs, &block)
36
+ end
37
+
38
+ nil
39
+ end
40
+ end
41
+ 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,160 @@
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
+
57
+ if ref_uri_nofrag.absolute?
58
+ ref_abs_uri = ref_uri_nofrag
59
+ elsif ref_schema && ref_schema.jsi_resource_ancestor_uri
60
+ ref_abs_uri = ref_schema.jsi_resource_ancestor_uri.join(ref_uri_nofrag)
61
+ else
62
+ ref_abs_uri = nil
63
+ end
64
+ if ref_abs_uri
65
+ schema_resource_root = JSI.schema_registry.find(ref_abs_uri)
66
+ end
67
+
68
+ unless schema_resource_root
69
+ # HAX for how google does refs and ids
70
+ if ref_schema && ref_schema.jsi_document.respond_to?(:to_hash) && ref_schema.jsi_document['schemas'].respond_to?(:to_hash)
71
+ ref_schema.jsi_document['schemas'].each_key do |k|
72
+ if Addressable::URI.parse(ref_schema.jsi_document['schemas'][k]['id']) == ref_uri_nofrag
73
+ schema_resource_root = ref_schema.resource_root_subschema(['schemas', k])
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ check_schema_resource_root.call
80
+
81
+ if schema_resource_root.is_a?(Schema)
82
+ resolve_fragment_ptr = schema_resource_root.method(:resource_root_subschema)
83
+ else
84
+ # Note: Schema#resource_root_subschema will reinstantiate nonschemas as schemas.
85
+ # not implemented for remote refs when the schema_resource_root is not a schema.
86
+ resolve_fragment_ptr = -> (ptr) { schema_resource_root.jsi_descendent_node(ptr) }
87
+ end
88
+ end
89
+
90
+ fragment = ref_uri.fragment
91
+
92
+ if fragment
93
+ begin
94
+ ptr_from_fragment = Ptr.from_fragment(fragment)
95
+ rescue Ptr::PointerSyntaxError
96
+ end
97
+ end
98
+
99
+ if ptr_from_fragment
100
+ begin
101
+ result_schema = resolve_fragment_ptr.call(ptr_from_fragment)
102
+ rescue Ptr::ResolutionError
103
+ raise(Schema::ReferenceError, [
104
+ "could not resolve pointer: #{ptr_from_fragment.pointer.inspect}",
105
+ ("from: #{ref_schema.pretty_inspect.chomp}" if ref_schema),
106
+ ("in schema resource root: #{schema_resource_root.pretty_inspect.chomp}" if schema_resource_root),
107
+ ].compact.join("\n"))
108
+ end
109
+ elsif fragment.nil?
110
+ check_schema_resource_root.call
111
+ result_schema = schema_resource_root
112
+ else
113
+ check_schema_resource_root.call
114
+
115
+ # find an anchor that resembles the fragment
116
+ result_schemas = schema_resource_root.jsi_anchor_subschemas(fragment)
117
+
118
+ if result_schemas.size == 1
119
+ result_schema = result_schemas.first
120
+ elsif result_schemas.size == 0
121
+ raise(Schema::ReferenceError, [
122
+ "could not find schema by fragment: #{fragment.inspect}",
123
+ "in schema resource root: #{schema_resource_root.pretty_inspect.chomp}",
124
+ ].join("\n"))
125
+ else
126
+ raise(Schema::ReferenceError, [
127
+ "found multiple schemas for plain name fragment #{fragment.inspect}:",
128
+ *result_schemas.map { |s| s.pretty_inspect.chomp },
129
+ ].join("\n"))
130
+ end
131
+ end
132
+
133
+ Schema.ensure_schema(result_schema, msg: "object identified by uri #{ref} is not a schema:")
134
+ return @deref_schema = result_schema
135
+ end
136
+
137
+ # @return [String]
138
+ def inspect
139
+ %Q(\#<#{self.class.name} #{ref}>)
140
+ end
141
+
142
+ alias_method :to_s, :inspect
143
+
144
+ # pretty-prints a representation of self to the given printer
145
+ # @return [void]
146
+ def pretty_print(q)
147
+ q.text '#<'
148
+ q.text self.class.name
149
+ q.text ' '
150
+ q.text ref
151
+ q.text '>'
152
+ end
153
+
154
+ # @private
155
+ def jsi_fingerprint
156
+ {class: self.class, ref: ref, ref_schema: ref_schema}
157
+ end
158
+ include Util::FingerprintHash
159
+ end
160
+ end
@@ -0,0 +1,113 @@
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
+ # @api 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
+ # @api 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
+ Util::EMPTY_ARY
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
+ (is_a?(Schema) && schema_absolute_uri) || jsi_schema_base_uri
29
+ end
30
+
31
+ # a schema at or below this node with the given anchor.
32
+ #
33
+ # @return [JSI::Schema, nil]
34
+ def jsi_anchor_subschema(anchor)
35
+ subschemas = jsi_anchor_subschemas_map[anchor: anchor]
36
+ if subschemas.size == 1
37
+ subschemas.first
38
+ else
39
+ nil
40
+ end
41
+ end
42
+
43
+ # schemas at or below node with the given anchor.
44
+ #
45
+ # @return [Array<JSI::Schema>]
46
+ def jsi_anchor_subschemas(anchor)
47
+ jsi_anchor_subschemas_map[anchor: anchor]
48
+ end
49
+
50
+ private
51
+
52
+ def jsi_document=(jsi_document)
53
+ @jsi_document = jsi_document
54
+ end
55
+
56
+ def jsi_ptr=(jsi_ptr)
57
+ raise(Bug, "jsi_ptr not #{Ptr}: #{jsi_ptr.inspect}") unless jsi_ptr.is_a?(Ptr)
58
+ @jsi_ptr = jsi_ptr
59
+ end
60
+
61
+ def jsi_schema_base_uri=(jsi_schema_base_uri)
62
+ if jsi_schema_base_uri
63
+ unless jsi_schema_base_uri.respond_to?(:to_str)
64
+ raise(TypeError, "jsi_schema_base_uri must be string or Addressable::URI; got: #{jsi_schema_base_uri.inspect}")
65
+ end
66
+ @jsi_schema_base_uri = Addressable::URI.parse(jsi_schema_base_uri).freeze
67
+ unless @jsi_schema_base_uri.absolute? && !@jsi_schema_base_uri.fragment
68
+ raise(ArgumentError, "jsi_schema_base_uri must be an absolute URI with no fragment; got: #{jsi_schema_base_uri.inspect}")
69
+ end
70
+ else
71
+ @jsi_schema_base_uri = nil
72
+ end
73
+ end
74
+
75
+ def jsi_schema_resource_ancestors=(jsi_schema_resource_ancestors)
76
+ if jsi_schema_resource_ancestors
77
+ unless jsi_schema_resource_ancestors.respond_to?(:to_ary)
78
+ raise(TypeError, "jsi_schema_resource_ancestors must be an array; got: #{jsi_schema_resource_ancestors.inspect}")
79
+ end
80
+ jsi_schema_resource_ancestors.each { |a| Schema.ensure_schema(a) }
81
+ # sanity check the ancestors are in order
82
+ last_anc_ptr = nil
83
+ jsi_schema_resource_ancestors.each do |anc|
84
+ if last_anc_ptr.nil?
85
+ # pass
86
+ elsif last_anc_ptr == anc.jsi_ptr
87
+ raise(Bug, "duplicate ancestors in #{jsi_schema_resource_ancestors.pretty_inspect}")
88
+ elsif !last_anc_ptr.contains?(anc.jsi_ptr)
89
+ raise(Bug, "ancestor ptr #{anc.jsi_ptr} not contained by previous: #{last_anc_ptr} in #{jsi_schema_resource_ancestors.pretty_inspect}")
90
+ end
91
+ if anc.jsi_ptr == jsi_ptr
92
+ raise(Bug, "ancestor is self")
93
+ elsif !anc.jsi_ptr.contains?(jsi_ptr)
94
+ raise(Bug, "ancestor does not contain self")
95
+ end
96
+ last_anc_ptr = anc.jsi_ptr
97
+ end
98
+
99
+ @jsi_schema_resource_ancestors = jsi_schema_resource_ancestors.to_ary.freeze
100
+ else
101
+ @jsi_schema_resource_ancestors = Util::EMPTY_ARY
102
+ end
103
+ end
104
+
105
+ def jsi_anchor_subschemas_map
106
+ jsi_memomap(__method__) do |anchor: |
107
+ jsi_each_descendent_node.select do |node|
108
+ node.is_a?(Schema) && node.respond_to?(:anchor) && node.anchor == anchor
109
+ end.freeze
110
+ end
111
+ end
112
+ end
113
+ 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 keyword?('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 keyword?('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 keyword?('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
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Validation::Const
5
+ # @private
6
+ def internal_validate_const(result_builder)
7
+ if keyword?('const')
8
+ value = schema_content['const']
9
+ # The value of this keyword MAY be of any type, including null.
10
+ # An instance validates successfully against this keyword if its value is equal to the value of
11
+ # the keyword.
12
+ result_builder.validate(
13
+ result_builder.instance == value,
14
+ 'instance is not equal to `const` value',
15
+ keyword: 'const',
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Validation::Contains
5
+ # @private
6
+ def internal_validate_contains(result_builder)
7
+ if keyword?('contains')
8
+ # An array instance is valid against "contains" if at least one of its elements is valid against
9
+ # the given schema.
10
+ if result_builder.instance.respond_to?(:to_ary)
11
+ results = {}
12
+ result_builder.instance.each_index do |i|
13
+ results[i] = result_builder.child_subschema_validate(['contains'], [i])
14
+ end
15
+ result_builder.validate(
16
+ results.values.any?(&:valid?),
17
+ 'instance array does not contain any items valid against `contains` schema value',
18
+ keyword: 'contains',
19
+ results: results.values,
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Validation::Core
5
+ # validates the given instance against this schema
6
+ #
7
+ # @private
8
+ # @param instance_ptr [JSI::Ptr] a pointer to the instance to validate against the schema, in the instance_document
9
+ # @param instance_document [#to_hash, #to_ary, Object] document containing the instance instance_ptr pointer points to
10
+ # @param validate_only [Boolean] whether to return a full schema validation result or a simple, validation-only result
11
+ # @param visited_refs [Enumerable<JSI::Schema::Ref>]
12
+ # @return [JSI::Validation::Result]
13
+ def internal_validate_instance(instance_ptr, instance_document, validate_only: false, visited_refs: [])
14
+ if validate_only
15
+ result = JSI::Validation::VALID
16
+ else
17
+ result = JSI::Validation::FullResult.new
18
+ end
19
+ result_builder = result.builder(self, instance_ptr, instance_document, validate_only, visited_refs)
20
+
21
+ catch(:jsi_validation_result) do
22
+ # note: true/false are not valid as schemas in draft 4; they are only values of
23
+ # additionalProperties / additionalItems. since their behavior is undefined, though,
24
+ # it's fine for them to behave the same as boolean schemas in later drafts.
25
+ # I don't care about draft 4 to implement a different structuring for that.
26
+ if schema_content == true
27
+ # noop
28
+ elsif schema_content == false
29
+ result_builder.validate(false, 'instance is not valid against `false` schema')
30
+ elsif schema_content.respond_to?(:to_hash)
31
+ internal_validate_keywords(result_builder)
32
+ else
33
+ result_builder.schema_error('schema is not a boolean or a JSON object')
34
+ end
35
+ result
36
+ end.freeze
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSI
4
+ module Schema::Validation::Dependencies
5
+ # @private
6
+ def internal_validate_dependencies(result_builder)
7
+ if keyword?('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
+ # If the dependency value is an array, each element in the array, if
15
+ # any, MUST be a string, and MUST be unique. If the dependency key is
16
+ # a property in the instance, each of the items in the dependency value
17
+ # must be a property that exists in the instance.
18
+ if result_builder.instance.respond_to?(:to_hash) && result_builder.instance.key?(property_name)
19
+ missing_required = dependency.reject { |name| result_builder.instance.key?(name) }
20
+ # TODO include property_name / missing dependent required property names in the validation error
21
+ result_builder.validate(
22
+ missing_required.empty?,
23
+ 'instance object does not contain all dependent required property names specified by `dependencies` value',
24
+ keyword: 'dependencies',
25
+ )
26
+ end
27
+ else
28
+ # If the dependency value is a subschema, and the dependency key is a
29
+ # property in the instance, the entire instance must validate against
30
+ # the dependency value.
31
+ if result_builder.instance.respond_to?(:to_hash) && result_builder.instance.key?(property_name)
32
+ dependency_result = result_builder.inplace_subschema_validate(['dependencies', property_name])
33
+ # TODO include property_name in the validation error
34
+ result_builder.validate(
35
+ dependency_result.valid?,
36
+ 'instance object is not valid against the schema corresponding to a matched property name specified by `dependencies` value',
37
+ keyword: 'dependencies',
38
+ results: [dependency_result],
39
+ )
40
+ end
41
+ end
42
+ end
43
+ else
44
+ result_builder.schema_error('`dependencies` is not an object', 'dependencies')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end