json_schemer 0.2.18 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -7
  3. data/CHANGELOG.md +102 -0
  4. data/Gemfile.lock +30 -10
  5. data/README.md +402 -6
  6. data/bin/hostname_character_classes +42 -0
  7. data/bin/rake +29 -0
  8. data/exe/json_schemer +62 -0
  9. data/json_schemer.gemspec +9 -12
  10. data/lib/json_schemer/cached_resolver.rb +16 -0
  11. data/lib/json_schemer/configuration.rb +31 -0
  12. data/lib/json_schemer/content.rb +18 -0
  13. data/lib/json_schemer/draft201909/meta.rb +320 -0
  14. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  15. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  16. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  17. data/lib/json_schemer/draft202012/meta.rb +364 -0
  18. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  19. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  20. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  21. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  22. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  23. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  24. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +104 -0
  25. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  26. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  27. data/lib/json_schemer/draft4/meta.rb +161 -0
  28. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  29. data/lib/json_schemer/draft4/vocab.rb +18 -0
  30. data/lib/json_schemer/draft6/meta.rb +172 -0
  31. data/lib/json_schemer/draft6/vocab.rb +16 -0
  32. data/lib/json_schemer/draft7/meta.rb +183 -0
  33. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  34. data/lib/json_schemer/draft7/vocab.rb +30 -0
  35. data/lib/json_schemer/ecma_regexp.rb +51 -0
  36. data/lib/json_schemer/errors.rb +1 -0
  37. data/lib/json_schemer/format/duration.rb +23 -0
  38. data/lib/json_schemer/format/email.rb +56 -0
  39. data/lib/json_schemer/format/hostname.rb +58 -0
  40. data/lib/json_schemer/format/json_pointer.rb +18 -0
  41. data/lib/json_schemer/format/uri_template.rb +34 -0
  42. data/lib/json_schemer/format.rb +128 -109
  43. data/lib/json_schemer/keyword.rb +56 -0
  44. data/lib/json_schemer/location.rb +25 -0
  45. data/lib/json_schemer/openapi.rb +38 -0
  46. data/lib/json_schemer/openapi30/document.rb +1672 -0
  47. data/lib/json_schemer/openapi30/meta.rb +32 -0
  48. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  49. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  50. data/lib/json_schemer/openapi31/document.rb +1557 -0
  51. data/lib/json_schemer/openapi31/meta.rb +136 -0
  52. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  53. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  54. data/lib/json_schemer/output.rb +56 -0
  55. data/lib/json_schemer/result.rb +242 -0
  56. data/lib/json_schemer/schema.rb +424 -0
  57. data/lib/json_schemer/version.rb +1 -1
  58. data/lib/json_schemer.rb +243 -30
  59. metadata +141 -25
  60. data/lib/json_schemer/cached_ref_resolver.rb +0 -14
  61. data/lib/json_schemer/schema/base.rb +0 -658
  62. data/lib/json_schemer/schema/draft4.rb +0 -44
  63. data/lib/json_schemer/schema/draft6.rb +0 -25
  64. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ BASE_URI = URI('https://spec.openapis.org/oas/3.1/dialect/base')
5
+ # https://spec.openapis.org/oas/v3.1.0#data-types
6
+ FORMATS = {
7
+ 'int32' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 32 },
8
+ 'int64' => proc { |instance, _format| instance.is_a?(Integer) && instance.bit_length <= 64 },
9
+ 'float' => proc { |instance, _format| instance.is_a?(Float) },
10
+ 'double' => proc { |instance, _format| instance.is_a?(Float) },
11
+ 'password' => proc { |_instance, _format| true }
12
+ }
13
+ SCHEMA = {
14
+ '$id' => 'https://spec.openapis.org/oas/3.1/dialect/base',
15
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
16
+
17
+ 'title' => 'OpenAPI 3.1 Schema Object Dialect',
18
+ 'description' => 'A JSON Schema dialect describing schemas found in OpenAPI documents',
19
+
20
+ '$vocabulary' => {
21
+ 'https://json-schema.org/draft/2020-12/vocab/core' => true,
22
+ 'https://json-schema.org/draft/2020-12/vocab/applicator' => true,
23
+ 'https://json-schema.org/draft/2020-12/vocab/unevaluated' => true,
24
+ 'https://json-schema.org/draft/2020-12/vocab/validation' => true,
25
+ 'https://json-schema.org/draft/2020-12/vocab/meta-data' => true,
26
+ 'https://json-schema.org/draft/2020-12/vocab/format-annotation' => true,
27
+ 'https://json-schema.org/draft/2020-12/vocab/content' => true,
28
+ 'https://spec.openapis.org/oas/3.1/vocab/base' => false
29
+ },
30
+
31
+ '$dynamicAnchor' => 'meta',
32
+
33
+ 'allOf' => [
34
+ { '$ref' => 'https://json-schema.org/draft/2020-12/schema' },
35
+ { '$ref' => 'https://spec.openapis.org/oas/3.1/meta/base' }
36
+ ]
37
+ }
38
+
39
+
40
+ module Meta
41
+ BASE = {
42
+ '$id' => 'https://spec.openapis.org/oas/3.1/meta/base',
43
+ '$schema' => 'https://json-schema.org/draft/2020-12/schema',
44
+
45
+ 'title' => 'OAS Base vocabulary',
46
+ 'description' => 'A JSON Schema Vocabulary used in the OpenAPI Schema Dialect',
47
+
48
+ '$vocabulary' => {
49
+ 'https://spec.openapis.org/oas/3.1/vocab/base' => true
50
+ },
51
+
52
+ '$dynamicAnchor' => 'meta',
53
+
54
+ 'type' => ['object', 'boolean'],
55
+ 'properties' => {
56
+ 'example' => true,
57
+ 'discriminator' => { '$ref' => '#/$defs/discriminator' },
58
+ 'externalDocs' => { '$ref' => '#/$defs/external-docs' },
59
+ 'xml' => { '$ref' => '#/$defs/xml' }
60
+ },
61
+
62
+ '$defs' => {
63
+ 'extensible' => {
64
+ 'patternProperties' => {
65
+ '^x-' => true
66
+ }
67
+ },
68
+
69
+ 'discriminator' => {
70
+ '$ref' => '#/$defs/extensible',
71
+ 'type' => 'object',
72
+ 'properties' => {
73
+ 'propertyName' => {
74
+ 'type' => 'string'
75
+ },
76
+ 'mapping' => {
77
+ 'type' => 'object',
78
+ 'additionalProperties' => {
79
+ 'type' => 'string'
80
+ }
81
+ }
82
+ },
83
+ 'required' => ['propertyName'],
84
+ 'unevaluatedProperties' => false
85
+ },
86
+
87
+ 'external-docs' => {
88
+ '$ref' => '#/$defs/extensible',
89
+ 'type' => 'object',
90
+ 'properties' => {
91
+ 'url' => {
92
+ 'type' => 'string',
93
+ 'format' => 'uri-reference'
94
+ },
95
+ 'description' => {
96
+ 'type' => 'string'
97
+ }
98
+ },
99
+ 'required' => ['url'],
100
+ 'unevaluatedProperties' => false
101
+ },
102
+
103
+ 'xml' => {
104
+ '$ref' => '#/$defs/extensible',
105
+ 'type' => 'object',
106
+ 'properties' => {
107
+ 'name' => {
108
+ 'type' => 'string'
109
+ },
110
+ 'namespace' => {
111
+ 'type' => 'string',
112
+ 'format' => 'uri'
113
+ },
114
+ 'prefix' => {
115
+ 'type' => 'string'
116
+ },
117
+ 'attribute' => {
118
+ 'type' => 'boolean'
119
+ },
120
+ 'wrapped' => {
121
+ 'type' => 'boolean'
122
+ }
123
+ },
124
+ 'unevaluatedProperties' => false
125
+ }
126
+ }
127
+ }
128
+
129
+
130
+ SCHEMAS = Draft202012::Meta::SCHEMAS.merge(
131
+ Draft202012::BASE_URI => Draft202012::SCHEMA,
132
+ URI('https://spec.openapis.org/oas/3.1/meta/base') => BASE
133
+ )
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ module Vocab
5
+ module Base
6
+ class AllOf < Draft202012::Vocab::Applicator::AllOf
7
+ attr_accessor :skip_ref_once
8
+
9
+ def validate(instance, instance_location, keyword_location, context)
10
+ nested = []
11
+ parsed.each_with_index do |subschema, index|
12
+ if ref_schema = subschema.parsed['$ref']&.ref_schema
13
+ next if skip_ref_once == ref_schema.absolute_keyword_location
14
+ ref_schema.parsed['discriminator']&.skip_ref_once = schema.absolute_keyword_location
15
+ end
16
+ nested << subschema.validate_instance(instance, instance_location, join_location(keyword_location, index.to_s), context)
17
+ end
18
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested)
19
+ ensure
20
+ self.skip_ref_once = nil
21
+ end
22
+ end
23
+
24
+ class AnyOf < Draft202012::Vocab::Applicator::AnyOf
25
+ def validate(*)
26
+ schema.parsed.key?('discriminator') ? nil : super
27
+ end
28
+ end
29
+
30
+ class OneOf < Draft202012::Vocab::Applicator::OneOf
31
+ def validate(*)
32
+ schema.parsed.key?('discriminator') ? nil : super
33
+ end
34
+ end
35
+
36
+ class Discriminator < Keyword
37
+ # https://spec.openapis.org/oas/v3.1.0#components-object
38
+ FIXED_FIELD_REGEX = /\A[a-zA-Z0-9\.\-_]+$\z/
39
+
40
+ attr_accessor :skip_ref_once
41
+
42
+ def error(formatted_instance_location:, **)
43
+ "value at #{formatted_instance_location} does not match `discriminator` schema"
44
+ end
45
+
46
+ def mapping
47
+ @mapping ||= value['mapping'] || {}
48
+ end
49
+
50
+ def subschemas_by_property_value
51
+ @subschemas_by_property_value ||= if schema.parsed.key?('anyOf') || schema.parsed.key?('oneOf')
52
+ subschemas = schema.parsed['anyOf']&.parsed || []
53
+ subschemas += schema.parsed['oneOf']&.parsed || []
54
+
55
+ subschemas_by_ref = {}
56
+ subschemas_by_schema_name = {}
57
+
58
+ subschemas.each do |subschema|
59
+ subschema_ref = subschema.parsed.fetch('$ref').parsed
60
+ subschemas_by_ref[subschema_ref] = subschema
61
+
62
+ if subschema_ref.start_with?('#/components/schemas/')
63
+ schema_name = subschema_ref.delete_prefix('#/components/schemas/')
64
+ subschemas_by_schema_name[schema_name] = subschema if FIXED_FIELD_REGEX.match?(schema_name)
65
+ end
66
+ end
67
+
68
+ explicit_mapping = mapping.transform_values do |schema_name_or_ref|
69
+ subschemas_by_schema_name.fetch(schema_name_or_ref) { subschemas_by_ref.fetch(schema_name_or_ref) }
70
+ end
71
+
72
+ implicit_mapping = subschemas_by_schema_name.reject do |_schema_name, subschema|
73
+ explicit_mapping.value?(subschema)
74
+ end
75
+
76
+ implicit_mapping.merge(explicit_mapping)
77
+ else
78
+ Hash.new do |hash, property_value|
79
+ schema_name_or_ref = mapping.fetch(property_value, property_value)
80
+
81
+ subschema = nil
82
+
83
+ if FIXED_FIELD_REGEX.match?(schema_name_or_ref)
84
+ subschema = begin
85
+ schema.ref("#/components/schemas/#{schema_name_or_ref}")
86
+ rescue InvalidRefPointer
87
+ nil
88
+ end
89
+ end
90
+
91
+ subschema ||= begin
92
+ schema.ref(schema_name_or_ref)
93
+ rescue InvalidRefResolution, UnknownRef
94
+ nil
95
+ end
96
+
97
+ hash[property_value] = subschema
98
+ end
99
+ end
100
+ end
101
+
102
+ def validate(instance, instance_location, keyword_location, context)
103
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(Hash)
104
+
105
+ property_name = value.fetch('propertyName')
106
+
107
+ return result(instance, instance_location, keyword_location, false) unless instance.key?(property_name)
108
+
109
+ property_value = instance.fetch(property_name)
110
+ subschema = subschemas_by_property_value[property_value]
111
+
112
+ return result(instance, instance_location, keyword_location, false) unless subschema
113
+
114
+ return if skip_ref_once == subschema.absolute_keyword_location
115
+ subschema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
116
+
117
+ subschema_result = subschema.validate_instance(instance, instance_location, keyword_location, context)
118
+
119
+ result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
120
+ ensure
121
+ self.skip_ref_once = nil
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module OpenAPI31
4
+ module Vocab
5
+ # https://spec.openapis.org/oas/latest.html#schema-object
6
+ BASE = {
7
+ # https://spec.openapis.org/oas/latest.html#discriminator-object
8
+ 'discriminator' => Base::Discriminator,
9
+ 'allOf' => Base::AllOf,
10
+ 'anyOf' => Base::AnyOf,
11
+ 'oneOf' => Base::OneOf
12
+ # 'xml' => Base::Xml,
13
+ # 'externalDocs' => Base::ExternalDocs,
14
+ # 'example' => Base::Example
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Output
4
+ FRAGMENT_ENCODE_REGEX = /[^\w?\/:@\-.~!$&'()*+,;=]/
5
+
6
+ attr_reader :keyword, :schema
7
+
8
+ def x_error
9
+ return @x_error if defined?(@x_error)
10
+ @x_error = schema.parsed['x-error']&.message(error_key)
11
+ end
12
+
13
+ private
14
+
15
+ def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false)
16
+ Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, valid ? 'annotations' : 'errors')
17
+ end
18
+
19
+ def escaped_keyword
20
+ @escaped_keyword ||= Location.escape_json_pointer_token(keyword)
21
+ end
22
+
23
+ def join_location(location, keyword)
24
+ Location.join(location, keyword)
25
+ end
26
+
27
+ def fragment_encode(location)
28
+ Format.percent_encode(location, FRAGMENT_ENCODE_REGEX)
29
+ end
30
+
31
+ # :nocov:
32
+ if Symbol.method_defined?(:name)
33
+ def stringify(key)
34
+ key.is_a?(Symbol) ? key.name : key.to_s
35
+ end
36
+ else
37
+ def stringify(key)
38
+ key.to_s
39
+ end
40
+ end
41
+ # :nocov:
42
+
43
+ def deep_stringify_keys(obj)
44
+ case obj
45
+ when Hash
46
+ obj.each_with_object({}) do |(key, value), out|
47
+ out[stringify(key)] = deep_stringify_keys(value)
48
+ end
49
+ when Array
50
+ obj.map { |item| deep_stringify_keys(item) }
51
+ else
52
+ obj
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ CATCHALL = '*'
4
+ I18N_SEPARATOR = "\x1F" # unit separator
5
+ I18N_SCOPE = 'json_schemer'
6
+ I18N_ERRORS_SCOPE = "#{I18N_SCOPE}#{I18N_SEPARATOR}errors"
7
+ X_ERROR_REGEX = /%\{(instance|instanceLocation|keywordLocation|absoluteKeywordLocation)\}/
8
+ CLASSIC_ERROR_TYPES = Hash.new do |hash, klass|
9
+ hash[klass] = klass.name.rpartition('::').last.sub(/\A[[:alpha:]]/, &:downcase)
10
+ end
11
+
12
+ Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
13
+ def output(output_format)
14
+ case output_format
15
+ when 'classic'
16
+ classic
17
+ when 'flag'
18
+ flag
19
+ when 'basic'
20
+ basic
21
+ when 'detailed'
22
+ detailed
23
+ when 'verbose'
24
+ verbose
25
+ else
26
+ raise UnknownOutputFormat, output_format
27
+ end
28
+ end
29
+
30
+ def error
31
+ return @error if defined?(@error)
32
+ if source.x_error
33
+ # not using sprintf because it warns: "too many arguments for format string"
34
+ @error = source.x_error.gsub(
35
+ X_ERROR_REGEX,
36
+ '%{instance}' => instance,
37
+ '%{instanceLocation}' => Location.resolve(instance_location),
38
+ '%{keywordLocation}' => Location.resolve(keyword_location),
39
+ '%{absoluteKeywordLocation}' => source.absolute_keyword_location
40
+ )
41
+ @x_error = true
42
+ else
43
+ resolved_instance_location = Location.resolve(instance_location)
44
+ formatted_instance_location = resolved_instance_location.empty? ? 'root' : "`#{resolved_instance_location}`"
45
+ @error = source.error(:formatted_instance_location => formatted_instance_location, :details => details)
46
+ if i18n?
47
+ begin
48
+ @error = i18n!
49
+ @i18n = true
50
+ rescue I18n::MissingTranslationData
51
+ end
52
+ end
53
+ end
54
+ @error
55
+ end
56
+
57
+ def i18n?
58
+ return @@i18n if defined?(@@i18n)
59
+ @@i18n = defined?(I18n) && I18n.exists?(I18N_SCOPE)
60
+ end
61
+
62
+ def i18n!
63
+ base_uri_str = source.schema.base_uri.to_s
64
+ meta_schema_base_uri_str = source.schema.meta_schema.base_uri.to_s
65
+ resolved_keyword_location = Location.resolve(keyword_location)
66
+ error_key = source.error_key
67
+ I18n.translate!(
68
+ source.absolute_keyword_location,
69
+ :default => [
70
+ "#{base_uri_str}#{I18N_SEPARATOR}##{resolved_keyword_location}",
71
+ "##{resolved_keyword_location}",
72
+ "#{base_uri_str}#{I18N_SEPARATOR}#{error_key}",
73
+ "#{base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
74
+ "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{error_key}",
75
+ "#{meta_schema_base_uri_str}#{I18N_SEPARATOR}#{CATCHALL}",
76
+ error_key,
77
+ CATCHALL
78
+ ].map!(&:to_sym),
79
+ :separator => I18N_SEPARATOR,
80
+ :scope => I18N_ERRORS_SCOPE,
81
+ :instance => instance,
82
+ :instanceLocation => Location.resolve(instance_location),
83
+ :keywordLocation => resolved_keyword_location,
84
+ :absoluteKeywordLocation => source.absolute_keyword_location
85
+ )
86
+ end
87
+
88
+ def to_output_unit
89
+ out = {
90
+ 'valid' => valid,
91
+ 'keywordLocation' => Location.resolve(keyword_location),
92
+ 'absoluteKeywordLocation' => source.absolute_keyword_location,
93
+ 'instanceLocation' => Location.resolve(instance_location)
94
+ }
95
+ if valid
96
+ out['annotation'] = annotation if annotation
97
+ else
98
+ out['error'] = error
99
+ out['x-error'] = true if @x_error
100
+ out['i18n'] = true if @i18n
101
+ end
102
+ out
103
+ end
104
+
105
+ def to_classic
106
+ schema = source.schema
107
+ out = {
108
+ 'data' => instance,
109
+ 'data_pointer' => Location.resolve(instance_location),
110
+ 'schema' => schema.value,
111
+ 'schema_pointer' => schema.schema_pointer,
112
+ 'root_schema' => schema.root.value,
113
+ 'type' => type || CLASSIC_ERROR_TYPES[source.class]
114
+ }
115
+ out['error'] = error
116
+ out['x-error'] = true if @x_error
117
+ out['i18n'] = true if @i18n
118
+ out['details'] = details if details
119
+ out
120
+ end
121
+
122
+ def flag
123
+ { 'valid' => valid }
124
+ end
125
+
126
+ def basic
127
+ out = to_output_unit
128
+ if nested&.any?
129
+ out[nested_key] = Enumerator.new do |yielder|
130
+ results = [self]
131
+ while result = results.pop
132
+ if result.ignore_nested || !result.nested&.any?
133
+ yielder << result.to_output_unit
134
+ else
135
+ previous_results_size = results.size
136
+ result.nested.reverse_each do |nested_result|
137
+ results << nested_result if nested_result.valid == valid
138
+ end
139
+ yielder << result.to_output_unit unless (results.size - previous_results_size) == 1
140
+ end
141
+ end
142
+ end
143
+ end
144
+ out
145
+ end
146
+
147
+ def detailed
148
+ return to_output_unit if ignore_nested || !nested&.any?
149
+ matching_results = nested.select { |nested_result| nested_result.valid == valid }
150
+ if matching_results.size == 1
151
+ matching_results.first.detailed
152
+ else
153
+ out = to_output_unit
154
+ if matching_results.any?
155
+ out[nested_key] = Enumerator.new do |yielder|
156
+ matching_results.each { |nested_result| yielder << nested_result.detailed }
157
+ end
158
+ end
159
+ out
160
+ end
161
+ end
162
+
163
+ def verbose
164
+ out = to_output_unit
165
+ if nested&.any?
166
+ out[nested_key] = Enumerator.new do |yielder|
167
+ nested.each { |nested_result| yielder << nested_result.verbose }
168
+ end
169
+ end
170
+ out
171
+ end
172
+
173
+ def classic
174
+ Enumerator.new do |yielder|
175
+ unless valid
176
+ results = [self]
177
+ while result = results.pop
178
+ if result.ignore_nested || !result.nested&.any?
179
+ yielder << result.to_classic
180
+ else
181
+ previous_results_size = results.size
182
+ result.nested.reverse_each do |nested_result|
183
+ results << nested_result if nested_result.valid == valid
184
+ end
185
+ yielder << result.to_classic if (results.size - previous_results_size) == 0
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ def insert_property_defaults(context)
193
+ instance_locations = {}
194
+ instance_locations.compare_by_identity
195
+
196
+ results = [[self, true]]
197
+ while (result, valid = results.pop)
198
+ next if result.source.is_a?(Schema::NOT_KEYWORD_CLASS)
199
+
200
+ valid &&= result.valid
201
+ result.nested&.each { |nested_result| results << [nested_result, valid] }
202
+
203
+ if result.source.is_a?(Schema::PROPERTIES_KEYWORD_CLASS) && result.instance.is_a?(Hash)
204
+ result.source.parsed.each do |property, schema|
205
+ next if result.instance.key?(property)
206
+ next unless default = default_keyword_instance(schema)
207
+ instance_location = Location.join(result.instance_location, property)
208
+ keyword_location = Location.join(Location.join(result.keyword_location, property), default.keyword)
209
+ default_result = default.validate(nil, instance_location, keyword_location, nil)
210
+ instance_locations[result.instance_location] ||= {}
211
+ instance_locations[result.instance_location][property] ||= []
212
+ instance_locations[result.instance_location][property] << [default_result, valid]
213
+ end
214
+ end
215
+ end
216
+
217
+ inserted = false
218
+
219
+ instance_locations.each do |instance_location, properties|
220
+ original_instance = context.original_instance(instance_location)
221
+ properties.each do |property, results_with_tree_validity|
222
+ property_inserted = yield(original_instance, property, results_with_tree_validity)
223
+ inserted ||= (property_inserted != false)
224
+ end
225
+ end
226
+
227
+ inserted
228
+ end
229
+
230
+ private
231
+
232
+ def default_keyword_instance(schema)
233
+ schema.parsed.fetch('default') do
234
+ schema.parsed.find do |_keyword, keyword_instance|
235
+ next unless keyword_instance.respond_to?(:ref_schema)
236
+ next unless default = default_keyword_instance(keyword_instance.ref_schema)
237
+ break default
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end