json_schemer 0.2.18 → 2.2.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -7
  3. data/CHANGELOG.md +89 -0
  4. data/Gemfile.lock +35 -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 -29
  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