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