media_types 1.0.0 → 2.1.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/debian.yml +43 -0
  3. data/.github/workflows/ruby.yml +3 -0
  4. data/.gitignore +10 -10
  5. data/CHANGELOG.md +80 -54
  6. data/Gemfile +6 -6
  7. data/Gemfile.lock +43 -114
  8. data/LICENSE +21 -0
  9. data/README.md +278 -85
  10. data/Rakefile +12 -12
  11. data/lib/media_types.rb +46 -3
  12. data/lib/media_types/constructable.rb +15 -9
  13. data/lib/media_types/dsl.rb +66 -31
  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 +127 -13
  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 +29 -7
  28. data/lib/media_types/version.rb +1 -1
  29. data/media_types.gemspec +4 -7
  30. metadata +19 -63
  31. data/.travis.yml +0 -19
  32. data/lib/media_types/.dsl.rb.swp +0 -0
  33. data/lib/media_types/defaults.rb +0 -31
  34. data/lib/media_types/integrations.rb +0 -32
  35. data/lib/media_types/integrations/actionpack.rb +0 -21
  36. data/lib/media_types/integrations/http.rb +0 -47
  37. data/lib/media_types/minitest/assert_media_type_format.rb +0 -10
  38. data/lib/media_types/minitest/assert_media_types_registered.rb +0 -166
  39. 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
  #
@@ -44,14 +46,29 @@ module MediaTypes
44
46
  end
45
47
  end
46
48
 
47
- def method_missing(method_name, *arguments, &block)
48
- if scheme.respond_to?(method_name)
49
- media_type.__getobj__.media_type_combinations.add(media_type.as_key)
50
-
51
- return scheme.send(method_name, *arguments, &block)
49
+
50
+ if Gem::Version.new(RUBY_VERSION) > Gem::Version.new('2.7.0')
51
+ def method_missing(method_name, *arguments, **kwargs, &block)
52
+ if scheme.respond_to?(method_name)
53
+ media_type.__getobj__.media_type_combinations ||= Set.new
54
+ media_type.__getobj__.media_type_combinations.add(media_type.as_key)
55
+
56
+ return scheme.send(method_name, *arguments, **kwargs, &block)
57
+ end
58
+
59
+ super
52
60
  end
61
+ else
62
+ def method_missing(method_name, *arguments, &block)
63
+ if scheme.respond_to?(method_name)
64
+ media_type.__getobj__.media_type_combinations ||= Set.new
65
+ media_type.__getobj__.media_type_combinations.add(media_type.as_key)
53
66
 
54
- super
67
+ return scheme.send(method_name, *arguments, &block)
68
+ end
69
+
70
+ super
71
+ end
55
72
  end
56
73
 
57
74
  def respond_to_missing?(method_name, include_private = false)
@@ -60,7 +77,8 @@ module MediaTypes
60
77
 
61
78
  private
62
79
 
63
- attr_accessor :media_type, :registry, :scheme
80
+ attr_accessor :media_type, :registry
81
+ attr_writer :scheme
64
82
 
65
83
  ##
66
84
  # Switches the inner block to a specific version
@@ -79,5 +97,9 @@ module MediaTypes
79
97
  def view(view, &block)
80
98
  Validations.new(media_type.view(view), registry, &block)
81
99
  end
100
+
101
+ def suffix(name)
102
+ scheme.type_attributes[:suffix] = name
103
+ end
82
104
  end
83
105
  end