jsi 0.4.0 → 0.7.0

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