media_types 0.4.1 → 0.5.0

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