media_types 0.4.1 → 0.5.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.
@@ -10,6 +10,10 @@ module MediaTypes
10
10
  def ===(other)
11
11
  any? { |it| it === other } # rubocop:disable Style/CaseEquality
12
12
  end
13
+
14
+ def inspect
15
+ "[Scheme::AnyOf(#{__getobj__})]"
16
+ end
13
17
  end
14
18
 
15
19
  # noinspection RubyInstanceMethodNamingConvention
@@ -35,6 +35,10 @@ module MediaTypes
35
35
  )
36
36
  end
37
37
 
38
+ def inspect
39
+ "[Scheme::Attribute type=#{type} nil=#{allow_nil}]"
40
+ end
41
+
38
42
  private
39
43
 
40
44
  attr_accessor :allow_nil, :type
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module MediaTypes
2
4
  class Scheme
3
5
  class EnumerationContext
4
- def initialize(validations:)
5
- self.validations = validations
6
+ def initialize(rules:)
7
+ self.rules = rules
6
8
  end
7
9
 
8
10
  def enumerate(val)
@@ -10,7 +12,7 @@ module MediaTypes
10
12
  self
11
13
  end
12
14
 
13
- attr_accessor :validations, :key
15
+ attr_accessor :rules, :key
14
16
  end
15
17
  end
16
18
  end
@@ -25,6 +25,10 @@ module MediaTypes
25
25
  validate_items!(output, options)
26
26
  end
27
27
 
28
+ def inspect
29
+ "[Scheme::EnumerationOfType #{item_type} collection=#{enumeration_type} empty=#{allow_empty}]"
30
+ end
31
+
28
32
  private
29
33
 
30
34
  attr_accessor :allow_empty, :enumeration_type, :item_type
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+
6
+ # Base class for all validations errors
7
+ class ValidationError < ArgumentError; end
8
+
9
+ # Raised when it did not expect more data, but there was more left
10
+ class StrictValidationError < ValidationError; end
11
+
12
+ # Raised when it expected not to be empty, but it was
13
+ class EmptyOutputError < ValidationError; end
14
+
15
+ # Raised when a value did not have the expected type
16
+ class OutputTypeMismatch < ValidationError; end
17
+
18
+ # Raised when it expected more data but there wasn't any left
19
+ class ExhaustedOutputError < ValidationError; end
20
+ end
21
+ end
@@ -1,27 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'media_types/scheme/rules'
4
+
3
5
  module MediaTypes
4
6
  class Scheme
5
7
  class Links
6
8
  def initialize
7
- self.links = {}
9
+ self.links = Rules.new(allow_empty: false, expected_type: ::Hash)
8
10
  end
9
11
 
10
- def link(key, allow_nil: false, &block)
11
- scheme = Scheme.new
12
- scheme.attribute :href, String, allow_nil: allow_nil
13
- scheme.instance_exec(&block) if block_given?
12
+ def link(key, allow_nil: false, optional: false, &block)
13
+ links.add(
14
+ key,
15
+ Scheme.new do
16
+ attribute :href, String, allow_nil: allow_nil
17
+ instance_exec(&block) if block_given?
18
+ end,
19
+ optional: optional
20
+ )
14
21
 
15
- links[String(key)] = scheme
22
+ self
16
23
  end
17
24
 
18
25
  def validate!(output, options, **_opts)
19
- links.all? do |key, value|
20
- value.validate!(
21
- output[key] || output[key.to_sym],
22
- options.trace(key).exhaustive!
23
- )
24
- end
26
+ RulesExhaustedGuard.call(output, options, rules: links)
27
+ end
28
+
29
+ def inspect
30
+ "[Scheme::Links #{links.keys}]"
25
31
  end
26
32
 
27
33
  private
@@ -7,7 +7,7 @@ module MediaTypes
7
7
  def validate!(_output, options, context:, **_opts)
8
8
  # Check that no unknown keys are present
9
9
  return true unless options.strict
10
- raise_strict!(key: context.key, strict_keys: context.validations, backtrace: options.backtrace)
10
+ raise_strict!(key: context.key, strict_keys: context.rules, backtrace: options.backtrace)
11
11
  end
12
12
 
13
13
  def raise_strict!(key:, backtrace:, strict_keys:)
@@ -6,6 +6,10 @@ module MediaTypes
6
6
  def validate!(*_args, **_opts)
7
7
  true
8
8
  end
9
+
10
+ def inspect
11
+ '[Scheme::NotStrict]'
12
+ end
9
13
  end
10
14
  end
11
15
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/errors'
4
+ require 'media_types/object'
5
+
6
+ module MediaTypes
7
+ class Scheme
8
+ class OutputEmptyGuard
9
+ class << self
10
+ def call(*args, **opts, &block)
11
+ new(*args, **opts).call(&block)
12
+ end
13
+ end
14
+
15
+ def initialize(output, options, rules:)
16
+ self.output = output
17
+ self.options = options
18
+ self.rules = rules
19
+ end
20
+
21
+ def call
22
+ return unless MediaTypes::Object.new(output).empty?
23
+ throw(:end, true) if allow_empty?
24
+ raise_empty!(backtrace: options.backtrace)
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :output, :options, :rules
30
+
31
+ def allow_empty?
32
+ rules.allow_empty? || rules.required.empty?
33
+ end
34
+
35
+ def raise_empty!(backtrace:)
36
+ raise EmptyOutputError, format('Expected output, got empty at %<backtrace>s', backtrace: backtrace.join('->'))
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/enumeration_context'
4
+
5
+ module MediaTypes
6
+ class Scheme
7
+ class OutputIteratorWithPredicate
8
+
9
+ class << self
10
+ def call(*args, **opts, &block)
11
+ new(*args, **opts).call(&block)
12
+ end
13
+ end
14
+
15
+ def initialize(enumerable, options, rules:)
16
+ self.enumerable = enumerable
17
+ self.options = options
18
+ self.rules = rules
19
+ end
20
+
21
+ ##
22
+ # Mimics Enumerable#all? with mandatory +&block+
23
+ #
24
+ def call
25
+ if hash?
26
+ return iterate_hash { |*args, **opts| yield(*args, **opts) }
27
+ end
28
+
29
+ iterate { |*args, **opts| yield(*args, **opts) }
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :enumerable, :options, :rules
35
+
36
+ def hash?
37
+ enumerable.is_a?(::Hash) || enumerable.respond_to?(:key)
38
+ end
39
+
40
+ def iterate_hash
41
+ context = EnumerationContext.new(rules: rules)
42
+
43
+ enumerable.all? do |key, value|
44
+ yield key, value, options: options, context: context.enumerate(key)
45
+ end
46
+ end
47
+
48
+ def iterate(&block)
49
+ Array(enumerable).each_with_index.all? do |array_like_element, i|
50
+ OutputIteratorWithPredicate.call(array_like_element, options.trace(i), rules: rules, &block)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/scheme/errors'
4
+
5
+ module MediaTypes
6
+ class Scheme
7
+ class OutputTypeGuard
8
+ class << self
9
+ def call(*args, **opts, &block)
10
+ new(*args, **opts).call(&block)
11
+ end
12
+ end
13
+
14
+ def initialize(output, options, rules:)
15
+ self.output = output
16
+ self.options = options
17
+ self.expected_type = rules.expected_type
18
+ end
19
+
20
+ def call
21
+ return unless expected_type && !(expected_type === output) # rubocop:disable Style/CaseEquality
22
+ raise_type_error!(type: output.class, backtrace: options.backtrace)
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :output, :options, :expected_type
28
+
29
+ def raise_type_error!(type:, backtrace:)
30
+ raise OutputTypeMismatch, format(
31
+ 'Expected a %<expected>s, got a %<actual>s at %<backtrace>s',
32
+ expected: expected_type,
33
+ actual: type,
34
+ backtrace: backtrace.join('->')
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+ class Rules < DelegateClass(::Hash)
6
+
7
+ attr_reader :expected_type
8
+
9
+ def initialize(allow_empty:, expected_type:)
10
+ super({})
11
+
12
+ self.allow_empty = allow_empty
13
+ self.expected_type = expected_type
14
+ self.optional_keys = []
15
+
16
+ self.default = MissingValidation.new
17
+ end
18
+
19
+ def allow_empty?
20
+ allow_empty
21
+ end
22
+
23
+ def [](key)
24
+ __getobj__[normalize_key(key)]
25
+ end
26
+
27
+ def add(key, val, optional: false)
28
+ normalized_key = normalize_key(key)
29
+ __getobj__[normalized_key] = val
30
+ optional_keys << normalized_key if optional
31
+
32
+ self
33
+ end
34
+
35
+ def []=(key, val)
36
+ add(key, val, optional: false)
37
+ end
38
+
39
+ def fetch(key, &block)
40
+ __getobj__.fetch(normalize_key(key), &block)
41
+ end
42
+
43
+ def delete(key)
44
+ __getobj__.delete(normalize_key(key))
45
+ self
46
+ end
47
+
48
+ def required
49
+ clone.tap do |cloned|
50
+ optional_keys.each do |key|
51
+ cloned.delete(key)
52
+ end
53
+ end
54
+ end
55
+
56
+ def clone
57
+ super.tap do |cloned|
58
+ cloned.__setobj__(__getobj__.clone)
59
+ end
60
+ end
61
+
62
+ def merge(rules)
63
+ __getobj__.merge!(rules)
64
+ self
65
+ end
66
+
67
+ def inspect
68
+ "[Scheme::Rules n=#{keys.length} default=#{default}]"
69
+ end
70
+
71
+ alias get []
72
+ alias remove delete
73
+
74
+ private
75
+
76
+ attr_accessor :allow_empty, :optional_keys
77
+ attr_writer :expected_type
78
+
79
+ def normalize_key(key)
80
+ String(key).to_sym
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,62 @@
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)
35
+ end
36
+
37
+ def iterate(mark)
38
+ OutputIteratorWithPredicate.call(output, options, rules: rules) do |key, value, options:, context:|
39
+ mark.call(key)
40
+
41
+ rules.get(key).validate!(
42
+ value,
43
+ options.trace(key),
44
+ context: context
45
+ )
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ attr_accessor :rules, :options, :output
52
+
53
+ def raise_exhausted!(missing_keys:, backtrace:)
54
+ raise ExhaustedOutputError, format(
55
+ 'Missing keys in output: %<missing_keys>s at [%<backtrace>s]',
56
+ missing_keys: missing_keys,
57
+ backtrace: backtrace.join('->')
58
+ )
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module MediaTypes
4
- VERSION = '0.4.1'
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ VERSION = '0.5.0'
5
+ end