media_types 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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