media_types 0.6.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/debian.yml +42 -0
  3. data/.github/workflows/ruby.yml +22 -0
  4. data/.gitignore +10 -10
  5. data/CHANGELOG.md +76 -41
  6. data/Gemfile +6 -6
  7. data/Gemfile.lock +17 -83
  8. data/LICENSE +21 -0
  9. data/README.md +364 -91
  10. data/Rakefile +12 -12
  11. data/lib/media_types.rb +58 -2
  12. data/lib/media_types/constructable.rb +36 -10
  13. data/lib/media_types/dsl.rb +110 -29
  14. data/lib/media_types/dsl/errors.rb +18 -0
  15. data/lib/media_types/errors.rb +19 -0
  16. data/lib/media_types/scheme.rb +153 -2
  17. data/lib/media_types/scheme/errors.rb +66 -0
  18. data/lib/media_types/scheme/links.rb +15 -0
  19. data/lib/media_types/scheme/missing_validation.rb +12 -4
  20. data/lib/media_types/scheme/output_empty_guard.rb +5 -4
  21. data/lib/media_types/scheme/output_iterator_with_predicate.rb +13 -2
  22. data/lib/media_types/scheme/output_type_guard.rb +1 -1
  23. data/lib/media_types/scheme/rules.rb +53 -1
  24. data/lib/media_types/scheme/rules_exhausted_guard.rb +15 -4
  25. data/lib/media_types/scheme/validation_options.rb +17 -5
  26. data/lib/media_types/testing/assertions.rb +20 -0
  27. data/lib/media_types/validations.rb +15 -5
  28. data/lib/media_types/version.rb +1 -1
  29. data/media_types.gemspec +4 -7
  30. metadata +20 -62
  31. data/.travis.yml +0 -19
  32. data/lib/media_types/defaults.rb +0 -31
  33. data/lib/media_types/integrations.rb +0 -32
  34. data/lib/media_types/integrations/actionpack.rb +0 -21
  35. data/lib/media_types/integrations/http.rb +0 -47
  36. data/lib/media_types/minitest/assert_media_type_format.rb +0 -10
  37. data/lib/media_types/minitest/assert_media_types_registered.rb +0 -166
  38. data/lib/media_types/registrar.rb +0 -148
@@ -2,10 +2,49 @@
2
2
 
3
3
  module MediaTypes
4
4
  class Scheme
5
+ class ConflictingTypeDefinitionError < ArgumentError; end
5
6
 
6
7
  # Base class for all validations errors
7
8
  class ValidationError < ArgumentError; end
8
9
 
10
+ # Raised when trying to register an attribute with a non-string key
11
+ class KeyTypeError < ArgumentError; end
12
+
13
+ # Raised when trying to register a key twice
14
+ class DuplicateKeyError < ArgumentError; end
15
+
16
+ class DuplicateSymbolKeyError < DuplicateKeyError
17
+ MESSAGE_TEMPLATE = '%<rule_type>s rule with key :%<key>s has already been defined. Please remove one of the two.'
18
+
19
+ def initialize(rule_type, key)
20
+ super(format(MESSAGE_TEMPLATE, rule_type: rule_type, key: key))
21
+ end
22
+ end
23
+
24
+ class DuplicateStringKeyError < DuplicateKeyError
25
+ MESSAGE_TEMPLATE = '%<rule_type>s rule with key %<key>s has already been defined. Please remove one of the two.'
26
+
27
+ def initialize(rule_type, key)
28
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
29
+ end
30
+ end
31
+
32
+ class StringOverwritingSymbolError < DuplicateKeyError
33
+ MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule String key %<key>s while a Symbol with the same name already exists. Please remove one of the two.'
34
+
35
+ def initialize(rule_type, key)
36
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
37
+ end
38
+ end
39
+
40
+ class SymbolOverwritingStringError < DuplicateKeyError
41
+ MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule with Symbol key :%<key>s while a String key with the same name already exists. Please remove one of the two.'
42
+
43
+ def initialize(rule_type, key)
44
+ super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
45
+ end
46
+ end
47
+
9
48
  # Raised when it did not expect more data, but there was more left
10
49
  class StrictValidationError < ValidationError; end
11
50
 
@@ -17,5 +56,32 @@ module MediaTypes
17
56
 
18
57
  # Raised when it expected more data but there wasn't any left
19
58
  class ExhaustedOutputError < ValidationError; end
59
+
60
+ # Raised when trying to override a non default rule scheme in the Rules Hash's default object method
61
+ class OverwritingRuleError < ArgumentError; end
62
+
63
+ class DuplicateAnyRuleError < OverwritingRuleError
64
+ def message
65
+ "An 'any' rule has already been defined. Please remove one of the two."
66
+ end
67
+ end
68
+
69
+ class DuplicateNotStrictRuleError < OverwritingRuleError
70
+ def message
71
+ "The 'not_strict' rule has already been defined. Please remove one of the two."
72
+ end
73
+ end
74
+
75
+ class NotStrictOverwritingAnyError < OverwritingRuleError
76
+ def message
77
+ "An 'any' rule has already been defined. Setting 'not_strict' will override that rule. Please remove one of the two."
78
+ end
79
+ end
80
+
81
+ class AnyOverwritingNotStrictError < OverwritingRuleError
82
+ def message
83
+ "The 'not_strict' rule has already been defined. Setting 'any' will override that rule. Please remove one of the two."
84
+ end
85
+ end
20
86
  end
21
87
  end
@@ -31,6 +31,21 @@ module MediaTypes
31
31
  "[Scheme::Links #{links.keys}]"
32
32
  end
33
33
 
34
+ def run_fixture_validations(expect_symbol_keys, backtrace = [])
35
+ fixture_errors = @links.flat_map {|key, rule|
36
+ if rule.is_a?(Scheme)
37
+ begin
38
+ rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
39
+ nil
40
+ rescue AssertionError => e
41
+ e.fixture_errors
42
+ end
43
+ end
44
+ }.compact
45
+
46
+ raise AssertionError.new(fixture_errors) unless fixture_errors.empty?
47
+ end
48
+
34
49
  private
35
50
 
36
51
  attr_accessor :links
@@ -9,18 +9,21 @@ module MediaTypes
9
9
  def validate!(_output, options, context:, **_opts)
10
10
  # Check that no unknown keys are present
11
11
  return true unless options.strict
12
- raise_strict!(key: context.key, strict_keys: context.rules, backtrace: options.backtrace)
12
+ raise_strict!(key: context.key, strict_keys: context.rules, backtrace: options.backtrace, found: options.scoped_output)
13
13
  end
14
14
 
15
- def raise_strict!(key:, backtrace:, strict_keys:)
15
+ def raise_strict!(key:, backtrace:, strict_keys:, found:)
16
16
  raise StrictValidationError, format(
17
17
  "Unknown key %<key>s in data.\n" \
18
18
  "\tFound at: %<backtrace>s\n" \
19
19
  "\tExpected:\n\n" \
20
- '%<strict_keys>s',
20
+ "%<strict_keys>s\n\n" \
21
+ "\tBut I Found:\n\n" \
22
+ '%<found>s',
21
23
  key: key.inspect,
22
24
  backtrace: backtrace.join('->'),
23
- strict_keys: strict_keys.inspect(1)
25
+ strict_keys: keys_to_str(strict_keys.keys),
26
+ found: (found.respond_to? :keys) ? keys_to_str(found.keys) : found.class.name
24
27
  )
25
28
  end
26
29
 
@@ -28,6 +31,11 @@ module MediaTypes
28
31
  '((raise when strict))'
29
32
  end
30
33
 
34
+ def keys_to_str(keys)
35
+ converted = keys.map { |k| k.is_a?(Symbol) ? ":#{k}" : "'#{k}'" }
36
+ "[#{converted.join ', '}]"
37
+ end
38
+
31
39
  end
32
40
  end
33
41
  end
@@ -21,7 +21,7 @@ module MediaTypes
21
21
  def call
22
22
  return unless MediaTypes::Object.new(output).empty?
23
23
  throw(:end, true) if allow_empty?
24
- raise_empty!(backtrace: options.backtrace)
24
+ raise_empty!(backtrace: options.backtrace, found: options.scoped_output)
25
25
  end
26
26
 
27
27
  private
@@ -32,11 +32,12 @@ module MediaTypes
32
32
  rules.allow_empty? || rules.required.empty?
33
33
  end
34
34
 
35
- def raise_empty!(backtrace:)
35
+ def raise_empty!(backtrace:, found:)
36
36
  raise EmptyOutputError, format(
37
- 'Expected output, got empty at %<backtrace>s. Required are: %<required>s.',
37
+ 'Expected output, got empty at %<backtrace>s. Required are: %<required>s. Found: %<found>s',
38
38
  backtrace: backtrace.join('->'),
39
- required: rules.required.keys
39
+ required: rules.required.keys,
40
+ found: (found.is_a? Hash) ? found.keys : found.class.name,
40
41
  )
41
42
  end
42
43
  end
@@ -26,7 +26,11 @@ module MediaTypes
26
26
  return iterate_hash { |*args, **opts| yield(*args, **opts) }
27
27
  end
28
28
 
29
- iterate { |*args, **opts| yield(*args, **opts) }
29
+ if array?
30
+ return iterate { |*args, **opts| yield(*args, **opts) }
31
+ end
32
+
33
+ raise "Internal consistency error, unexpected: #{enumerable.class}"
30
34
  end
31
35
 
32
36
  private
@@ -37,6 +41,10 @@ module MediaTypes
37
41
  enumerable.is_a?(::Hash) || enumerable.respond_to?(:key)
38
42
  end
39
43
 
44
+ def array?
45
+ enumerable.is_a?(::Array)
46
+ end
47
+
40
48
  def iterate_hash
41
49
  context = EnumerationContext.new(rules: rules)
42
50
 
@@ -46,7 +54,10 @@ module MediaTypes
46
54
  end
47
55
 
48
56
  def iterate(&block)
49
- Array(enumerable).each_with_index.all? do |array_like_element, i|
57
+ hash_rule = Rules.new(allow_empty: false, expected_type: ::Hash)
58
+
59
+ enumerable.each_with_index.all? do |array_like_element, i|
60
+ OutputTypeGuard.call(array_like_element, options.trace(1), rules: hash_rule)
50
61
  OutputIteratorWithPredicate.call(array_like_element, options.trace(i), rules: rules, &block)
51
62
  end
52
63
  end
@@ -28,7 +28,7 @@ module MediaTypes
28
28
 
29
29
  def raise_type_error!(type:, backtrace:)
30
30
  raise OutputTypeMismatch, format(
31
- 'Expected a %<expected>s, got a %<actual>s at %<backtrace>s',
31
+ 'Expected %<expected>s, got %<actual>s at %<backtrace>s',
32
32
  expected: expected_type,
33
33
  actual: type,
34
34
  backtrace: backtrace.join('->')
@@ -14,6 +14,7 @@ module MediaTypes
14
14
  self.allow_empty = allow_empty
15
15
  self.expected_type = expected_type
16
16
  self.optional_keys = []
17
+ self.original_key_type = {}
17
18
 
18
19
  self.default = MissingValidation.new
19
20
  end
@@ -27,13 +28,44 @@ module MediaTypes
27
28
  end
28
29
 
29
30
  def add(key, val, optional: false)
31
+ validate_input(key, val)
32
+
30
33
  normalized_key = normalize_key(key)
31
34
  __getobj__[normalized_key] = val
32
35
  optional_keys << normalized_key if optional
36
+ original_key_type[normalized_key] = key.class
33
37
 
34
38
  self
35
39
  end
36
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
+
37
69
  def []=(key, val)
38
70
  add(key, val, optional: false)
39
71
  end
@@ -105,12 +137,32 @@ module MediaTypes
105
137
  ].join(': ')
106
138
  end
107
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
+
108
160
  alias get []
109
161
  alias remove delete
110
162
 
111
163
  private
112
164
 
113
- attr_accessor :allow_empty, :optional_keys
165
+ attr_accessor :allow_empty, :optional_keys, :original_key_type
114
166
  attr_writer :expected_type
115
167
 
116
168
  def normalize_key(key)
@@ -31,11 +31,21 @@ module MediaTypes
31
31
  result = iterate(->(key) { required_rules.remove(key) })
32
32
  return result if required_rules.empty?
33
33
 
34
- raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace)
34
+ raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace, found: output)
35
35
  end
36
36
 
37
37
  def iterate(mark)
38
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
+
39
49
  mark.call(key)
40
50
 
41
51
  rules.get(key).validate!(
@@ -50,11 +60,12 @@ module MediaTypes
50
60
 
51
61
  attr_accessor :rules, :options, :output
52
62
 
53
- def raise_exhausted!(missing_keys:, backtrace:)
63
+ def raise_exhausted!(missing_keys:, backtrace:, found:)
54
64
  raise ExhaustedOutputError, format(
55
- 'Missing keys in output: %<missing_keys>s at [%<backtrace>s]',
65
+ 'Missing keys in output: %<missing_keys>s at [%<backtrace>s]. I did find: %<found>s',
56
66
  missing_keys: missing_keys,
57
- backtrace: backtrace.join('->')
67
+ backtrace: backtrace.join('->'),
68
+ found: (found.is_a? Hash) ? found.keys : found.class.name,
58
69
  )
59
70
  end
60
71
  end
@@ -3,20 +3,32 @@
3
3
  module MediaTypes
4
4
  class Scheme
5
5
  class ValidationOptions
6
- attr_accessor :exhaustive, :strict, :backtrace
6
+ attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type
7
7
 
8
- def initialize(exhaustive: true, strict: true, backtrace: [])
8
+ def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], expected_key_type:)
9
9
  self.exhaustive = exhaustive
10
10
  self.strict = strict
11
11
  self.backtrace = backtrace
12
+ self.context = context
13
+ self.expected_key_type = expected_key_type
12
14
  end
13
15
 
14
16
  def inspect
15
- "backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, exhaustive: #{exhaustive}"
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
16
28
  end
17
29
 
18
30
  def with_backtrace(backtrace)
19
- ValidationOptions.new(exhaustive: exhaustive, strict: strict, backtrace: backtrace)
31
+ ValidationOptions.new(context, exhaustive: exhaustive, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
20
32
  end
21
33
 
22
34
  def trace(*traces)
@@ -24,7 +36,7 @@ module MediaTypes
24
36
  end
25
37
 
26
38
  def exhaustive!
27
- ValidationOptions.new(exhaustive: true, strict: strict, backtrace: backtrace)
39
+ ValidationOptions.new(context, exhaustive: true, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ module Testing
5
+ module Assertions
6
+ def assert_media_type_format(media_type, output, **opts)
7
+ return pass unless media_type.validatable?
8
+
9
+ assert media_type.validate!(output, **opts)
10
+ end
11
+
12
+ def assert_mediatype(mediatype)
13
+ mediatype.assert_sane!
14
+ assert mediatype.media_type_validations.scheme.asserted_sane?
15
+ rescue MediaTypes::AssertionError => e
16
+ flunk e.message
17
+ end
18
+ end
19
+ end
20
+ end
@@ -12,6 +12,8 @@ module MediaTypes
12
12
  #
13
13
  class Validations
14
14
 
15
+ attr_reader :scheme
16
+
15
17
  ##
16
18
  # Creates a new stack of validations
17
19
  #
@@ -25,7 +27,7 @@ module MediaTypes
25
27
  #
26
28
  def initialize(media_type, registry = {}, scheme = Scheme.new, &block)
27
29
  self.media_type = media_type
28
- self.registry = registry.merge!(media_type.to_s => scheme)
30
+ self.registry = registry.merge!(media_type.as_key => scheme)
29
31
  self.scheme = scheme
30
32
 
31
33
  instance_exec(&block) if block_given?
@@ -39,14 +41,17 @@ module MediaTypes
39
41
  # @return [Scheme] the scheme for the given +media_type+
40
42
  #
41
43
  def find(media_type, default = -> { Scheme.new(allow_empty: true) { not_strict } })
42
- registry.fetch(String(media_type)) do
44
+ registry.fetch(media_type.as_key) do
43
45
  default.call
44
46
  end
45
47
  end
46
48
 
47
- def method_missing(method_name, *arguments, &block)
49
+ def method_missing(method_name, *arguments, **kwargs, &block)
48
50
  if scheme.respond_to?(method_name)
49
- return scheme.send(method_name, *arguments, &block)
51
+ media_type.__getobj__.media_type_combinations ||= Set.new
52
+ media_type.__getobj__.media_type_combinations.add(media_type.as_key)
53
+
54
+ return scheme.send(method_name, *arguments, **kwargs, &block)
50
55
  end
51
56
 
52
57
  super
@@ -58,7 +63,8 @@ module MediaTypes
58
63
 
59
64
  private
60
65
 
61
- attr_accessor :media_type, :registry, :scheme
66
+ attr_accessor :media_type, :registry
67
+ attr_writer :scheme
62
68
 
63
69
  ##
64
70
  # Switches the inner block to a specific version
@@ -77,5 +83,9 @@ module MediaTypes
77
83
  def view(view, &block)
78
84
  Validations.new(media_type.view(view), registry, &block)
79
85
  end
86
+
87
+ def suffix(name)
88
+ scheme.type_attributes[:suffix] = name
89
+ end
80
90
  end
81
91
  end