media_types 2.2.0 → 2.3.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/debian.yml +43 -43
- data/.github/workflows/publish-bookworm.yml +33 -0
- data/.github/workflows/publish-sid.yml +33 -0
- data/.github/workflows/ruby.yml +22 -22
- data/.gitignore +20 -10
- data/.rubocop.yml +29 -29
- data/CHANGELOG.md +175 -164
- data/Gemfile +6 -6
- data/LICENSE +21 -21
- data/README.md +666 -664
- data/Rakefile +12 -12
- data/bin/console +15 -15
- data/bin/setup +8 -8
- data/lib/media_types/constructable.rb +161 -160
- data/lib/media_types/dsl/errors.rb +18 -18
- data/lib/media_types/dsl.rb +172 -172
- data/lib/media_types/errors.rb +25 -19
- data/lib/media_types/formatter.rb +56 -56
- data/lib/media_types/hash.rb +21 -21
- data/lib/media_types/object.rb +35 -35
- data/lib/media_types/scheme/allow_nil.rb +30 -30
- data/lib/media_types/scheme/any_of.rb +41 -41
- data/lib/media_types/scheme/attribute.rb +46 -46
- data/lib/media_types/scheme/enumeration_context.rb +18 -18
- data/lib/media_types/scheme/enumeration_of_type.rb +80 -80
- data/lib/media_types/scheme/errors.rb +87 -87
- data/lib/media_types/scheme/links.rb +54 -54
- data/lib/media_types/scheme/missing_validation.rb +41 -41
- data/lib/media_types/scheme/not_strict.rb +15 -15
- data/lib/media_types/scheme/output_empty_guard.rb +45 -45
- data/lib/media_types/scheme/output_iterator_with_predicate.rb +66 -66
- data/lib/media_types/scheme/output_type_guard.rb +39 -39
- data/lib/media_types/scheme/rules.rb +186 -173
- data/lib/media_types/scheme/rules_exhausted_guard.rb +73 -73
- data/lib/media_types/scheme/validation_options.rb +44 -43
- data/lib/media_types/scheme.rb +535 -513
- data/lib/media_types/testing/assertions.rb +20 -20
- data/lib/media_types/validations.rb +118 -105
- data/lib/media_types/version.rb +5 -5
- data/lib/media_types/views.rb +12 -12
- data/lib/media_types.rb +73 -73
- data/media_types.gemspec +33 -33
- metadata +8 -6
@@ -1,173 +1,186 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'media_types/scheme/missing_validation'
|
4
|
-
|
5
|
-
module MediaTypes
|
6
|
-
class Scheme
|
7
|
-
class Rules < DelegateClass(::Hash)
|
8
|
-
|
9
|
-
attr_reader :expected_type
|
10
|
-
|
11
|
-
def initialize(allow_empty:, expected_type:)
|
12
|
-
super({})
|
13
|
-
|
14
|
-
self.allow_empty = allow_empty
|
15
|
-
self.expected_type = expected_type
|
16
|
-
self.optional_keys = []
|
17
|
-
self.
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'media_types/scheme/missing_validation'
|
4
|
+
|
5
|
+
module MediaTypes
|
6
|
+
class Scheme
|
7
|
+
class Rules < DelegateClass(::Hash)
|
8
|
+
|
9
|
+
attr_reader :expected_type
|
10
|
+
|
11
|
+
def initialize(allow_empty:, expected_type:)
|
12
|
+
super({})
|
13
|
+
|
14
|
+
self.allow_empty = allow_empty
|
15
|
+
self.expected_type = expected_type
|
16
|
+
self.optional_keys = []
|
17
|
+
self.strict_keys = []
|
18
|
+
self.original_key_type = {}
|
19
|
+
|
20
|
+
self.default = MissingValidation.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def allow_empty?
|
24
|
+
allow_empty
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](key)
|
28
|
+
__getobj__[normalize_key(key)]
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(key, val, optional: false)
|
32
|
+
validate_input(key, val)
|
33
|
+
|
34
|
+
normalized_key = normalize_key(key)
|
35
|
+
__getobj__[normalized_key] = val
|
36
|
+
if optional == :loose
|
37
|
+
strict_keys << normalized_key
|
38
|
+
else
|
39
|
+
optional_keys << normalized_key if optional
|
40
|
+
end
|
41
|
+
original_key_type[normalized_key] = key.class
|
42
|
+
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_input(key, val)
|
47
|
+
raise KeyTypeError, "Unexpected key type #{key.class.name}, please use either a symbol or string." unless key.is_a?(String) || key.is_a?(Symbol)
|
48
|
+
|
49
|
+
validate_key_name(key, val)
|
50
|
+
end
|
51
|
+
|
52
|
+
def validate_key_name(key, val)
|
53
|
+
return unless has_key?(key)
|
54
|
+
|
55
|
+
if key.is_a?(Symbol)
|
56
|
+
duplicate_symbol_key_name(key, val)
|
57
|
+
else
|
58
|
+
duplicate_string_key_name(key, val)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def duplicate_symbol_key_name(key, val)
|
63
|
+
raise DuplicateSymbolKeyError.new(val.class.name.split('::').last, key) if get_original_key_type(key) == Symbol
|
64
|
+
|
65
|
+
raise SymbolOverwritingStringError.new(val.class.name.split('::').last, key)
|
66
|
+
end
|
67
|
+
|
68
|
+
def duplicate_string_key_name(key, val)
|
69
|
+
raise DuplicateStringKeyError.new(val.class.name.split('::').last, key) if get_original_key_type(key) == String
|
70
|
+
|
71
|
+
raise StringOverwritingSymbolError.new(val.class.name.split('::').last, key)
|
72
|
+
end
|
73
|
+
|
74
|
+
def []=(key, val)
|
75
|
+
add(key, val, optional: false)
|
76
|
+
end
|
77
|
+
|
78
|
+
def fetch(key, &block)
|
79
|
+
__getobj__.fetch(normalize_key(key), &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete(key)
|
83
|
+
__getobj__.delete(normalize_key(key))
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
##
|
88
|
+
# Returns the keys that are not options
|
89
|
+
#
|
90
|
+
# @see #add
|
91
|
+
# #see #merge
|
92
|
+
#
|
93
|
+
# @return [Array<Symbol>] required keys
|
94
|
+
#
|
95
|
+
def required(loose:)
|
96
|
+
clone.tap do |cloned|
|
97
|
+
optional_keys.each do |key|
|
98
|
+
cloned.delete(key)
|
99
|
+
end
|
100
|
+
if loose
|
101
|
+
strict_keys.each do |key|
|
102
|
+
cloned.delete(key)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def clone
|
109
|
+
super.tap do |cloned|
|
110
|
+
cloned.__setobj__(__getobj__.clone)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
##
|
115
|
+
# Merges another set of rules into a clone of this one
|
116
|
+
#
|
117
|
+
# @param [Rules, ::Hash] the other rules
|
118
|
+
# @return [Rules] a clone
|
119
|
+
#
|
120
|
+
def merge(rules)
|
121
|
+
clone.instance_exec do
|
122
|
+
__setobj__(__getobj__.merge(rules))
|
123
|
+
if rules.respond_to?(:optional_keys, true)
|
124
|
+
optional_keys.push(*rules.send(:optional_keys))
|
125
|
+
end
|
126
|
+
if rules.respond_to?(:strict_keys, true)
|
127
|
+
strict_keys.push(*rules.send(:strict_keys))
|
128
|
+
end
|
129
|
+
|
130
|
+
self
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def inspect(indent = 0)
|
135
|
+
prefix = ' ' * indent
|
136
|
+
return "#{prefix}[Error]Depth limit reached[/Error]" if indent > 5_000
|
137
|
+
|
138
|
+
[
|
139
|
+
"#{prefix}[Rules n=#{keys.length} optional=#{optional_keys.length} strict=#{strict_keys.length} allow_empty=#{allow_empty?}]",
|
140
|
+
"#{prefix} #{inspect_format_attribute(indent, '*', default)}",
|
141
|
+
*keys.map { |key| "#{prefix} #{inspect_format_attribute(indent, key)}" },
|
142
|
+
"#{prefix}[/Rules]"
|
143
|
+
].join("\n")
|
144
|
+
end
|
145
|
+
|
146
|
+
def inspect_format_attribute(indent, key, value = self[key])
|
147
|
+
[
|
148
|
+
[key.to_s, optional_keys.include?(key) && '(optional)' || nil].compact.join(' '),
|
149
|
+
value.is_a?(Scheme) || value.is_a?(Rules) ? "\n#{value.inspect(indent + 2)}" : value.inspect
|
150
|
+
].join(': ')
|
151
|
+
end
|
152
|
+
|
153
|
+
def has_key?(key)
|
154
|
+
__getobj__.key?(normalize_key(key))
|
155
|
+
end
|
156
|
+
|
157
|
+
def get_original_key_type(key)
|
158
|
+
raise format('Key %<key>s does not exist', key: key) unless has_key?(key)
|
159
|
+
|
160
|
+
original_key_type[normalize_key(key)]
|
161
|
+
end
|
162
|
+
|
163
|
+
def default=(input_default)
|
164
|
+
unless default.nil?
|
165
|
+
raise DuplicateAnyRuleError if !(default.is_a?(MissingValidation) || default.is_a?(NotStrict)) && !(input_default.is_a?(MissingValidation) || input_default.is_a?(NotStrict))
|
166
|
+
raise DuplicateNotStrictRuleError if default.is_a?(NotStrict) && input_default.is_a?(NotStrict)
|
167
|
+
raise NotStrictOverwritingAnyError if !(default.is_a?(MissingValidation) || default.is_a?(NotStrict)) && input_default.is_a?(NotStrict)
|
168
|
+
raise AnyOverwritingNotStrictError if default.is_a?(NotStrict) && !(input_default.is_a?(MissingValidation) || input_default.is_a?(NotStrict))
|
169
|
+
end
|
170
|
+
super(input_default)
|
171
|
+
end
|
172
|
+
|
173
|
+
alias get []
|
174
|
+
alias remove delete
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
attr_accessor :allow_empty, :strict_keys, :optional_keys, :original_key_type
|
179
|
+
attr_writer :expected_type
|
180
|
+
|
181
|
+
def normalize_key(key)
|
182
|
+
String(key).to_sym
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
@@ -1,73 +1,73 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'media_types/scheme/errors'
|
4
|
-
require 'media_types/scheme/output_iterator_with_predicate'
|
5
|
-
|
6
|
-
module MediaTypes
|
7
|
-
class Scheme
|
8
|
-
class RulesExhaustedGuard
|
9
|
-
|
10
|
-
EMPTY_MARK = ->(_) {}
|
11
|
-
|
12
|
-
class << self
|
13
|
-
def call(*args, **opts, &block)
|
14
|
-
new(*args, **opts).call(&block)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def initialize(output, options, rules:)
|
19
|
-
self.rules = rules
|
20
|
-
self.output = output
|
21
|
-
self.options = options
|
22
|
-
end
|
23
|
-
|
24
|
-
def call
|
25
|
-
unless options.exhaustive
|
26
|
-
return iterate(EMPTY_MARK)
|
27
|
-
end
|
28
|
-
|
29
|
-
required_rules = rules.required
|
30
|
-
# noinspection RubyScope
|
31
|
-
result = iterate(->(key) { required_rules.remove(key) })
|
32
|
-
return result if required_rules.empty?
|
33
|
-
|
34
|
-
raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace, found: output)
|
35
|
-
end
|
36
|
-
|
37
|
-
def iterate(mark)
|
38
|
-
OutputIteratorWithPredicate.call(output, options, rules: rules) do |key, value, options:, context:|
|
39
|
-
unless key.class == options.expected_key_type || rules.get(key).class == NotStrict
|
40
|
-
raise ValidationError,
|
41
|
-
format(
|
42
|
-
'Expected key as %<type>s, got %<actual>s at [%<backtrace>s]',
|
43
|
-
type: options.expected_key_type,
|
44
|
-
actual: key.class,
|
45
|
-
backtrace: options.trace(key).backtrace.join('->')
|
46
|
-
)
|
47
|
-
end
|
48
|
-
|
49
|
-
mark.call(key)
|
50
|
-
|
51
|
-
rules.get(key).validate!(
|
52
|
-
value,
|
53
|
-
options.trace(key),
|
54
|
-
context: context
|
55
|
-
)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
60
|
-
|
61
|
-
attr_accessor :rules, :options, :output
|
62
|
-
|
63
|
-
def raise_exhausted!(missing_keys:, backtrace:, found:)
|
64
|
-
raise ExhaustedOutputError, format(
|
65
|
-
'Missing keys in output: %<missing_keys>s at [%<backtrace>s]. I did find: %<found>s',
|
66
|
-
missing_keys: missing_keys,
|
67
|
-
backtrace: backtrace.join('->'),
|
68
|
-
found: (found.is_a? Hash) ? found.keys : found.class.name,
|
69
|
-
)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'media_types/scheme/errors'
|
4
|
+
require 'media_types/scheme/output_iterator_with_predicate'
|
5
|
+
|
6
|
+
module MediaTypes
|
7
|
+
class Scheme
|
8
|
+
class RulesExhaustedGuard
|
9
|
+
|
10
|
+
EMPTY_MARK = ->(_) {}
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def call(*args, **opts, &block)
|
14
|
+
new(*args, **opts).call(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(output, options, rules:)
|
19
|
+
self.rules = rules
|
20
|
+
self.output = output
|
21
|
+
self.options = options
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
unless options.exhaustive
|
26
|
+
return iterate(EMPTY_MARK)
|
27
|
+
end
|
28
|
+
|
29
|
+
required_rules = rules.required(loose: options.loose)
|
30
|
+
# noinspection RubyScope
|
31
|
+
result = iterate(->(key) { required_rules.remove(key) })
|
32
|
+
return result if required_rules.empty?
|
33
|
+
|
34
|
+
raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace, found: output)
|
35
|
+
end
|
36
|
+
|
37
|
+
def iterate(mark)
|
38
|
+
OutputIteratorWithPredicate.call(output, options, rules: rules) do |key, value, options:, context:|
|
39
|
+
unless key.class == options.expected_key_type || rules.get(key).class == NotStrict
|
40
|
+
raise ValidationError,
|
41
|
+
format(
|
42
|
+
'Expected key as %<type>s, got %<actual>s at [%<backtrace>s]',
|
43
|
+
type: options.expected_key_type,
|
44
|
+
actual: key.class,
|
45
|
+
backtrace: options.trace(key).backtrace.join('->')
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
mark.call(key)
|
50
|
+
|
51
|
+
rules.get(key).validate!(
|
52
|
+
value,
|
53
|
+
options.trace(key),
|
54
|
+
context: context
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
attr_accessor :rules, :options, :output
|
62
|
+
|
63
|
+
def raise_exhausted!(missing_keys:, backtrace:, found:)
|
64
|
+
raise ExhaustedOutputError, format(
|
65
|
+
'Missing keys in output: %<missing_keys>s at [%<backtrace>s]. I did find: %<found>s',
|
66
|
+
missing_keys: missing_keys,
|
67
|
+
backtrace: backtrace.join('->'),
|
68
|
+
found: (found.is_a? Hash) ? found.keys : found.class.name,
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -1,43 +1,44 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module MediaTypes
|
4
|
-
class Scheme
|
5
|
-
class ValidationOptions
|
6
|
-
attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type
|
7
|
-
|
8
|
-
def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], expected_key_type:)
|
9
|
-
self.exhaustive = exhaustive
|
10
|
-
self.strict = strict
|
11
|
-
self.backtrace = backtrace
|
12
|
-
self.context = context
|
13
|
-
self.expected_key_type = expected_key_type
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MediaTypes
|
4
|
+
class Scheme
|
5
|
+
class ValidationOptions
|
6
|
+
attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type, :loose
|
7
|
+
|
8
|
+
def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], loose: false, expected_key_type:)
|
9
|
+
self.exhaustive = exhaustive
|
10
|
+
self.strict = strict
|
11
|
+
self.backtrace = backtrace
|
12
|
+
self.context = context
|
13
|
+
self.expected_key_type = expected_key_type
|
14
|
+
self.loose = loose
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, loose: #{loose}, exhaustive: #{exhaustive}, current_obj: #{scoped_output.to_json}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def scoped_output
|
22
|
+
current = context
|
23
|
+
|
24
|
+
backtrace.drop(1).first([0, backtrace.size - 2].max).each do |e|
|
25
|
+
current = current[e] unless current.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
current
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_backtrace(backtrace)
|
32
|
+
ValidationOptions.new(context, exhaustive: exhaustive, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type, loose: loose)
|
33
|
+
end
|
34
|
+
|
35
|
+
def trace(*traces)
|
36
|
+
with_backtrace(backtrace.dup.concat(traces))
|
37
|
+
end
|
38
|
+
|
39
|
+
def exhaustive!
|
40
|
+
ValidationOptions.new(context, exhaustive: true, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type, loose: loose)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|