json_schemer 2.0.0 → 2.1.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +18 -1
- data/Gemfile.lock +10 -3
- data/README.md +192 -1
- data/json_schemer.gemspec +2 -0
- data/lib/json_schemer/content.rb +18 -0
- data/lib/json_schemer/draft201909/meta.rb +3 -18
- data/lib/json_schemer/draft202012/meta.rb +27 -24
- data/lib/json_schemer/draft202012/vocab/content.rb +10 -2
- data/lib/json_schemer/draft202012/vocab/core.rb +6 -0
- data/lib/json_schemer/draft202012/vocab/format_annotation.rb +1 -9
- data/lib/json_schemer/draft202012/vocab/format_assertion.rb +1 -7
- data/lib/json_schemer/draft202012/vocab.rb +3 -1
- data/lib/json_schemer/draft4/meta.rb +6 -0
- data/lib/json_schemer/draft6/meta.rb +11 -0
- data/lib/json_schemer/draft7/meta.rb +5 -0
- data/lib/json_schemer/draft7/vocab/validation.rb +4 -4
- data/lib/json_schemer/format.rb +113 -117
- data/lib/json_schemer/keyword.rb +4 -0
- data/lib/json_schemer/openapi30/meta.rb +6 -0
- data/lib/json_schemer/openapi31/meta.rb +8 -0
- data/lib/json_schemer/openapi31/vocab/base.rb +65 -27
- data/lib/json_schemer/output.rb +6 -5
- data/lib/json_schemer/result.rb +71 -10
- data/lib/json_schemer/schema.rb +57 -23
- data/lib/json_schemer/version.rb +1 -1
- data/lib/json_schemer.rb +20 -19
- metadata +31 -2
@@ -35,7 +35,7 @@ module JSONSchemer
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
class ContentEncoding <
|
38
|
+
class ContentEncoding < Draft202012::Vocab::Content::ContentEncoding
|
39
39
|
def error(formatted_instance_location:, **)
|
40
40
|
"string at #{formatted_instance_location} could not be decoded using encoding: #{value}"
|
41
41
|
end
|
@@ -43,13 +43,13 @@ module JSONSchemer
|
|
43
43
|
def validate(instance, instance_location, keyword_location, _context)
|
44
44
|
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
|
45
45
|
|
46
|
-
valid, annotation =
|
46
|
+
valid, annotation = parsed.call(instance)
|
47
47
|
|
48
48
|
result(instance, instance_location, keyword_location, valid, :annotation => annotation)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
class ContentMediaType <
|
52
|
+
class ContentMediaType < Draft202012::Vocab::Content::ContentMediaType
|
53
53
|
def error(formatted_instance_location:, **)
|
54
54
|
"string at #{formatted_instance_location} could not be parsed using media type: #{value}"
|
55
55
|
end
|
@@ -58,7 +58,7 @@ module JSONSchemer
|
|
58
58
|
return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
|
59
59
|
|
60
60
|
decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance
|
61
|
-
valid, annotation =
|
61
|
+
valid, annotation = parsed.call(decoded_instance)
|
62
62
|
|
63
63
|
result(instance, instance_location, keyword_location, valid, :annotation => annotation)
|
64
64
|
end
|
data/lib/json_schemer/format.rb
CHANGED
@@ -1,15 +1,76 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module JSONSchemer
|
3
3
|
module Format
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3
|
5
|
+
DATE_TIME = proc do |instance, _format|
|
6
|
+
!instance.is_a?(String) || valid_date_time?(instance)
|
7
|
+
end
|
8
|
+
DATE = proc do |instance, _format|
|
9
|
+
!instance.is_a?(String) || valid_date_time?("#{instance}T04:05:06.123456789+07:00")
|
10
|
+
end
|
11
|
+
TIME = proc do |instance, _format|
|
12
|
+
!instance.is_a?(String) || valid_date_time?("2001-02-03T#{instance}")
|
13
|
+
end
|
14
|
+
DURATION = proc do |instance, _format|
|
15
|
+
!instance.is_a?(String) || valid_duration?(instance)
|
16
|
+
end
|
17
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.2
|
18
|
+
EMAIL = proc do |instance, _format|
|
19
|
+
!instance.is_a?(String) || instance.ascii_only? && valid_email?(instance)
|
20
|
+
end
|
21
|
+
IDN_EMAIL = proc do |instance, _format|
|
22
|
+
!instance.is_a?(String) || valid_email?(instance)
|
23
|
+
end
|
24
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.3
|
25
|
+
HOSTNAME = proc do |instance, _format|
|
26
|
+
!instance.is_a?(String) || instance.ascii_only? && valid_hostname?(instance)
|
27
|
+
end
|
28
|
+
IDN_HOSTNAME = proc do |instance, _format|
|
29
|
+
!instance.is_a?(String) || valid_hostname?(instance)
|
30
|
+
end
|
31
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.4
|
32
|
+
IPV4 = proc do |instance, _format|
|
33
|
+
!instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET)
|
34
|
+
end
|
35
|
+
IPV6 = proc do |instance, _format|
|
36
|
+
!instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET6)
|
37
|
+
end
|
38
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.5
|
39
|
+
URI = proc do |instance, _format|
|
40
|
+
!instance.is_a?(String) || valid_uri?(instance)
|
41
|
+
end
|
42
|
+
URI_REFERENCE = proc do |instance, _format|
|
43
|
+
!instance.is_a?(String) || valid_uri_reference?(instance)
|
44
|
+
end
|
45
|
+
IRI = proc do |instance, _format|
|
46
|
+
!instance.is_a?(String) || valid_uri?(iri_escape(instance))
|
47
|
+
end
|
48
|
+
IRI_REFERENCE = proc do |instance, _format|
|
49
|
+
!instance.is_a?(String) || valid_uri_reference?(iri_escape(instance))
|
50
|
+
end
|
51
|
+
UUID = proc do |instance, _format|
|
52
|
+
!instance.is_a?(String) || valid_uuid?(instance)
|
53
|
+
end
|
54
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.6
|
55
|
+
URI_TEMPLATE = proc do |instance, _format|
|
56
|
+
!instance.is_a?(String) || valid_uri_template?(instance)
|
57
|
+
end
|
58
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.7
|
59
|
+
JSON_POINTER = proc do |instance, _format|
|
60
|
+
!instance.is_a?(String) || valid_json_pointer?(instance)
|
61
|
+
end
|
62
|
+
RELATIVE_JSON_POINTER = proc do |instance, _format|
|
63
|
+
!instance.is_a?(String) || valid_relative_json_pointer?(instance)
|
64
|
+
end
|
65
|
+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.8
|
66
|
+
REGEX = proc do |instance, _format|
|
67
|
+
!instance.is_a?(String) || valid_regex?(instance)
|
68
|
+
end
|
9
69
|
|
10
70
|
DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
|
11
|
-
|
12
|
-
|
71
|
+
DATE_TIME_SEPARATOR_CHARACTER_CLASS = '[Tt\s]'
|
72
|
+
HOUR_24_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}24:/.freeze
|
73
|
+
LEAP_SECOND_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}\d{2}:\d{2}:6/.freeze
|
13
74
|
IP_REGEX = /\A[\h:.]+\z/.freeze
|
14
75
|
INVALID_QUERY_REGEX = /\s/.freeze
|
15
76
|
IRI_ESCAPE_REGEX = /[^[:ascii:]]/
|
@@ -20,6 +81,12 @@ module JSONSchemer
|
|
20
81
|
end.freeze
|
21
82
|
|
22
83
|
class << self
|
84
|
+
include Duration
|
85
|
+
include Email
|
86
|
+
include Hostname
|
87
|
+
include JSONPointer
|
88
|
+
include URITemplate
|
89
|
+
|
23
90
|
def percent_encode(data, regexp)
|
24
91
|
data = data.dup
|
25
92
|
data.force_encoding(Encoding::ASCII_8BIT)
|
@@ -27,126 +94,55 @@ module JSONSchemer
|
|
27
94
|
data.force_encoding(Encoding::US_ASCII)
|
28
95
|
end
|
29
96
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
else
|
39
|
-
raise UnknownContentEncoding, content_encoding
|
40
|
-
end
|
97
|
+
def valid_date_time?(data)
|
98
|
+
return false if HOUR_24_REGEX.match?(data)
|
99
|
+
datetime = DateTime.rfc3339(data)
|
100
|
+
return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
|
101
|
+
DATE_TIME_OFFSET_REGEX.match?(data)
|
102
|
+
rescue ArgumentError
|
103
|
+
false
|
41
104
|
end
|
42
105
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
rescue
|
49
|
-
[false, nil]
|
50
|
-
end
|
51
|
-
else
|
52
|
-
raise UnknownContentMediaType, content_media_type
|
53
|
-
end
|
106
|
+
def valid_ip?(data, family)
|
107
|
+
IPAddr.new(data, family)
|
108
|
+
IP_REGEX.match?(data)
|
109
|
+
rescue IPAddr::Error
|
110
|
+
false
|
54
111
|
end
|
55
|
-
end
|
56
112
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
valid_date_time?("#{data}T04:05:06.123456789+07:00")
|
63
|
-
when 'time'
|
64
|
-
valid_date_time?("2001-02-03T#{data}")
|
65
|
-
when 'email'
|
66
|
-
data.ascii_only? && valid_email?(data)
|
67
|
-
when 'idn-email'
|
68
|
-
valid_email?(data)
|
69
|
-
when 'hostname'
|
70
|
-
data.ascii_only? && valid_hostname?(data)
|
71
|
-
when 'idn-hostname'
|
72
|
-
valid_hostname?(data)
|
73
|
-
when 'ipv4'
|
74
|
-
valid_ip?(data, Socket::AF_INET)
|
75
|
-
when 'ipv6'
|
76
|
-
valid_ip?(data, Socket::AF_INET6)
|
77
|
-
when 'uri'
|
78
|
-
valid_uri?(data)
|
79
|
-
when 'uri-reference'
|
80
|
-
valid_uri_reference?(data)
|
81
|
-
when 'iri'
|
82
|
-
valid_uri?(iri_escape(data))
|
83
|
-
when 'iri-reference'
|
84
|
-
valid_uri_reference?(iri_escape(data))
|
85
|
-
when 'uri-template'
|
86
|
-
valid_uri_template?(data)
|
87
|
-
when 'json-pointer'
|
88
|
-
valid_json_pointer?(data)
|
89
|
-
when 'relative-json-pointer'
|
90
|
-
valid_relative_json_pointer?(data)
|
91
|
-
when 'regex'
|
92
|
-
valid_regex?(data)
|
93
|
-
when 'duration'
|
94
|
-
valid_duration?(data)
|
95
|
-
when 'uuid'
|
96
|
-
valid_uuid?(data)
|
97
|
-
else
|
98
|
-
raise UnknownFormat, format
|
113
|
+
def parse_uri_scheme(data)
|
114
|
+
scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data)
|
115
|
+
# ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
|
116
|
+
raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
|
117
|
+
scheme
|
99
118
|
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def valid_date_time?(data)
|
103
|
-
return false if HOUR_24_REGEX.match?(data)
|
104
|
-
datetime = DateTime.rfc3339(data)
|
105
|
-
return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
|
106
|
-
DATE_TIME_OFFSET_REGEX.match?(data)
|
107
|
-
rescue ArgumentError
|
108
|
-
false
|
109
|
-
end
|
110
|
-
|
111
|
-
def valid_ip?(data, family)
|
112
|
-
IPAddr.new(data, family)
|
113
|
-
IP_REGEX.match?(data)
|
114
|
-
rescue IPAddr::Error
|
115
|
-
false
|
116
|
-
end
|
117
|
-
|
118
|
-
def parse_uri_scheme(data)
|
119
|
-
scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data)
|
120
|
-
# URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
|
121
|
-
raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
|
122
|
-
scheme
|
123
|
-
end
|
124
119
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
120
|
+
def valid_uri?(data)
|
121
|
+
!!parse_uri_scheme(data)
|
122
|
+
rescue ::URI::InvalidURIError
|
123
|
+
false
|
124
|
+
end
|
130
125
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
126
|
+
def valid_uri_reference?(data)
|
127
|
+
parse_uri_scheme(data)
|
128
|
+
true
|
129
|
+
rescue ::URI::InvalidURIError
|
130
|
+
false
|
131
|
+
end
|
137
132
|
|
138
|
-
|
139
|
-
|
140
|
-
|
133
|
+
def iri_escape(data)
|
134
|
+
Format.percent_encode(data, IRI_ESCAPE_REGEX)
|
135
|
+
end
|
141
136
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
137
|
+
def valid_regex?(data)
|
138
|
+
!!EcmaRegexp.ruby_equivalent(data)
|
139
|
+
rescue InvalidEcmaRegexp
|
140
|
+
false
|
141
|
+
end
|
147
142
|
|
148
|
-
|
149
|
-
|
143
|
+
def valid_uuid?(data)
|
144
|
+
UUID_REGEX.match?(data) || NIL_UUID == data
|
145
|
+
end
|
150
146
|
end
|
151
147
|
end
|
152
148
|
end
|
data/lib/json_schemer/keyword.rb
CHANGED
@@ -2,6 +2,12 @@
|
|
2
2
|
module JSONSchemer
|
3
3
|
module OpenAPI30
|
4
4
|
BASE_URI = URI('json-schemer://openapi30/schema')
|
5
|
+
# https://spec.openapis.org/oas/v3.0.3#data-types
|
6
|
+
FORMATS = OpenAPI31::FORMATS.merge(
|
7
|
+
'byte' => proc { |instance, _value| ContentEncoding::BASE64.call(instance).first },
|
8
|
+
'binary' => proc { |instance, _value| instance.is_a?(String) && instance.encoding == Encoding::ASCII_8BIT },
|
9
|
+
'date' => Format::DATE
|
10
|
+
)
|
5
11
|
SCHEMA = {
|
6
12
|
'id' => 'json-schemer://openapi30/schema',
|
7
13
|
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
@@ -2,6 +2,14 @@
|
|
2
2
|
module JSONSchemer
|
3
3
|
module OpenAPI31
|
4
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
|
+
}
|
5
13
|
SCHEMA = {
|
6
14
|
'$id' => 'https://spec.openapis.org/oas/3.1/dialect/base',
|
7
15
|
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
@@ -34,7 +34,8 @@ module JSONSchemer
|
|
34
34
|
end
|
35
35
|
|
36
36
|
class Discriminator < Keyword
|
37
|
-
|
37
|
+
# https://spec.openapis.org/oas/v3.1.0#components-object
|
38
|
+
FIXED_FIELD_REGEX = /\A[a-zA-Z0-9\.\-_]+$\z/
|
38
39
|
|
39
40
|
attr_accessor :skip_ref_once
|
40
41
|
|
@@ -42,43 +43,80 @@ module JSONSchemer
|
|
42
43
|
"value at #{formatted_instance_location} does not match `discriminator` schema"
|
43
44
|
end
|
44
45
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
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 || []
|
48
54
|
|
49
|
-
|
55
|
+
subschemas_by_ref = {}
|
56
|
+
subschemas_by_schema_name = {}
|
50
57
|
|
51
|
-
|
52
|
-
|
58
|
+
subschemas.each do |subschema|
|
59
|
+
subschema_ref = subschema.parsed.fetch('$ref').parsed
|
60
|
+
subschemas_by_ref[subschema_ref] = subschema
|
53
61
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
rescue InvalidRefPointer
|
59
|
-
nil
|
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
|
60
66
|
end
|
61
|
-
end
|
62
|
-
ref_schema ||= root.resolve_ref(URI.join(schema.base_uri, ref))
|
63
67
|
|
64
|
-
|
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
|
65
71
|
|
66
|
-
|
72
|
+
implicit_mapping = subschemas_by_schema_name.reject do |_schema_name, subschema|
|
73
|
+
explicit_mapping.value?(subschema)
|
74
|
+
end
|
67
75
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
74
95
|
end
|
96
|
+
|
97
|
+
hash[property_value] = subschema
|
75
98
|
end
|
76
|
-
else
|
77
|
-
ref_schema.parsed['allOf']&.skip_ref_once = schema.absolute_keyword_location
|
78
|
-
nested << ref_schema.validate_instance(instance, instance_location, keyword_location, context)
|
79
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)
|
80
118
|
|
81
|
-
result(instance, instance_location, keyword_location,
|
119
|
+
result(instance, instance_location, keyword_location, subschema_result.valid, subschema_result.nested)
|
82
120
|
ensure
|
83
121
|
self.skip_ref_once = nil
|
84
122
|
end
|
data/lib/json_schemer/output.rb
CHANGED
@@ -5,14 +5,15 @@ module JSONSchemer
|
|
5
5
|
|
6
6
|
attr_reader :keyword, :schema
|
7
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
|
+
|
8
13
|
private
|
9
14
|
|
10
15
|
def result(instance, instance_location, keyword_location, valid, nested = nil, type: nil, annotation: nil, details: nil, ignore_nested: false)
|
11
|
-
|
12
|
-
Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'annotations')
|
13
|
-
else
|
14
|
-
Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, 'errors')
|
15
|
-
end
|
16
|
+
Result.new(self, instance, instance_location, keyword_location, valid, nested, type, annotation, details, ignore_nested, valid ? 'annotations' : 'errors')
|
16
17
|
end
|
17
18
|
|
18
19
|
def escaped_keyword
|
data/lib/json_schemer/result.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module JSONSchemer
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
7
11
|
|
12
|
+
Result = Struct.new(:source, :instance, :instance_location, :keyword_location, :valid, :nested, :type, :annotation, :details, :ignore_nested, :nested_key) do
|
8
13
|
def output(output_format)
|
9
14
|
case output_format
|
10
15
|
when 'classic'
|
@@ -24,10 +29,59 @@ module JSONSchemer
|
|
24
29
|
|
25
30
|
def error
|
26
31
|
return @error if defined?(@error)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
31
85
|
)
|
32
86
|
end
|
33
87
|
|
@@ -38,8 +92,13 @@ module JSONSchemer
|
|
38
92
|
'absoluteKeywordLocation' => source.absolute_keyword_location,
|
39
93
|
'instanceLocation' => Location.resolve(instance_location)
|
40
94
|
}
|
41
|
-
|
42
|
-
|
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
|
43
102
|
out
|
44
103
|
end
|
45
104
|
|
@@ -54,6 +113,8 @@ module JSONSchemer
|
|
54
113
|
'type' => type || CLASSIC_ERROR_TYPES[source.class]
|
55
114
|
}
|
56
115
|
out['error'] = error
|
116
|
+
out['x-error'] = true if @x_error
|
117
|
+
out['i18n'] = true if @i18n
|
57
118
|
out['details'] = details if details
|
58
119
|
out
|
59
120
|
end
|