media_types 0.1.3 → 0.2.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.
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Registerable
5
+ def initialize(media_type, symbol:, aliases: [])
6
+ self.media_type = media_type
7
+ self.symbol = symbol
8
+ self.aliases = aliases
9
+
10
+ freeze
11
+ end
12
+
13
+ def to_s
14
+ String(media_type)
15
+ end
16
+
17
+ def to_sym
18
+ symbol
19
+ end
20
+
21
+ attr_accessor :media_type, :symbol, :aliases
22
+
23
+ alias synonyms aliases
24
+ alias mime_type to_s
25
+
26
+ end
27
+
28
+ ##
29
+ # Holds all the configuration options in order to call {MediaTypes.register} exactly enough times to register the
30
+ # media type in its entirety.
31
+ #
32
+ # @see Registerable
33
+ #
34
+ class Registrar
35
+
36
+ ##
37
+ # Creates a new registry with default values
38
+ #
39
+ # @param [Class] klazz the class that has {MediaTypes::Dsl} included
40
+ # @param [Symbol, NilClass] symbol the symbol the base view (nil view) should be registered under
41
+ #
42
+ def initialize(klazz, symbol: nil, &block)
43
+ self.base_media_type = klazz.to_constructable
44
+
45
+ self.registered_views = symbol ? { nil => { symbol: String(symbol).to_sym } } : {}
46
+ self.registered_versions = [base_media_type.version]
47
+ self.registered_aliases = []
48
+ self.registered_suffixes = [String(base_media_type.suffix).to_sym]
49
+
50
+ instance_exec(&block) if block_given?
51
+ end
52
+
53
+ ##
54
+ # Resolves all the permutations and returns a list of {Registerable}
55
+ #
56
+ # @return [Array<Registerable>] the registerables based on all the permutations of +self+
57
+ #
58
+ def to_a
59
+ result = []
60
+
61
+ each_resolved do |media_type, symbol|
62
+ result << Registerable.new(media_type, symbol: symbol, aliases: aliases(media_type))
63
+ end
64
+
65
+ result
66
+ end
67
+
68
+ private
69
+
70
+ attr_accessor :registered_views, :registered_versions, :registered_aliases, :registered_suffixes,
71
+ :symbol_base, :base_media_type
72
+
73
+ ##
74
+ # Registers a +view+ with a +symbol+
75
+ #
76
+ # @param [String, Symbol] view the view
77
+ # @param [String, Symbol] symbol the symbol base for this view
78
+ #
79
+ def view(view, symbol)
80
+ registered_views[String(view)] = { symbol: String(symbol).to_sym }
81
+ self
82
+ end
83
+
84
+ ##
85
+ # Registers +versions+
86
+ #
87
+ # @param [*Numeric, Array<Numeric>] versions the versions to register
88
+ #
89
+ def versions(*versions)
90
+ registered_versions.push(*Array(versions))
91
+ self
92
+ end
93
+
94
+ ##
95
+ # Registers a type alias. This will become a synonym when registering the media type
96
+ #
97
+ # @param [String] alias_name the name of the alias
98
+ #
99
+ def type_alias(alias_name)
100
+ registered_aliases.push(String(alias_name))
101
+ self
102
+ end
103
+
104
+ ##
105
+ # Registers a suffix
106
+ #
107
+ # @param [String, Symbol] suffix the suffix to register
108
+ #
109
+ def suffix(suffix)
110
+ registered_suffixes.push(String(suffix).to_sym)
111
+ self
112
+ end
113
+
114
+ ##
115
+ # Calculates the aliased media types for a media type
116
+ #
117
+ # @param [Constructable] media_type the media type constructable
118
+ # @return [Array<String>] the resolved aliases
119
+ #
120
+ def aliases(media_type)
121
+ registered_aliases.map { |a| media_type.type(a).to_s }
122
+ end
123
+
124
+ def each_combination
125
+ iterable(registered_versions).each do |version|
126
+ iterable(registered_views).each do |view, view_opts|
127
+ iterable(registered_suffixes).each do |suffix|
128
+ opts = { view: view_opts }
129
+ yield version, view, suffix, opts
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ def iterable(source)
136
+ source.compact.uniq
137
+ end
138
+
139
+ def each_resolved
140
+ each_combination do |version, view, suffix, opts|
141
+ media_type = base_media_type.version(version).view(view).suffix(suffix)
142
+ symbol = iterable([opts[:view][:symbol], "v#{version}", suffix]).join('_').to_sym
143
+
144
+ yield media_type, symbol
145
+ end
146
+ end
147
+ end
148
+ end
@@ -5,14 +5,21 @@ require 'delegate'
5
5
  module MediaTypes
6
6
  class Scheme
7
7
  class CaseEqualityWithNil < SimpleDelegator
8
+
9
+ # Same as the wrapped {Object#===}, but also allows for NilCLass
8
10
  def ===(other)
9
11
  other.nil? || super
10
12
  end
11
13
  end
12
14
 
13
15
  # noinspection RubyInstanceMethodNamingConvention
16
+ ##
17
+ # Allows the wrapped +klazz+ to be nil
18
+ #
19
+ # @param [Class] klazz the class that +it+ must be the if +it+ is not NilClass
20
+ # @return [CaseEqualityWithNil]
14
21
  def AllowNil(klazz) # rubocop:disable Naming/MethodName
15
- CaseEqualityWithNil.new(klazz)
22
+ CaseEqualityWithNil.new(klazz)
16
23
  end
17
24
  end
18
25
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module MediaTypes
6
+ class Scheme
7
+ class CaseEqualityWithList < SimpleDelegator
8
+
9
+ # True for Enumerable#any? {Object#===}
10
+ def ===(other)
11
+ any? { |it| it === other } # rubocop:disable Style/CaseEquality
12
+ end
13
+ end
14
+
15
+ # noinspection RubyInstanceMethodNamingConvention
16
+ ##
17
+ # Allows +it+ to be any of the wrapped +klazzes+
18
+ #
19
+ # @param [Array<Class>] klazzes the classes that are valid for +it+
20
+ # @return [CaseEqualityWithList]
21
+ def AnyOf(*klazzes) # rubocop:disable Naming/MethodName
22
+ CaseEqualityWithList.new(klazzes)
23
+ end
24
+ end
25
+ end
@@ -3,6 +3,16 @@
3
3
  module MediaTypes
4
4
  class Scheme
5
5
  class Attribute
6
+
7
+ ##
8
+ # An attribute that expects a value of type +type+
9
+ #
10
+ # @see AllowNil
11
+ # @see AnyOf
12
+ #
13
+ # @param [Class] type the class +it+ must be
14
+ # @param [TrueClass, FalseClass] allow_nil if true, nil? is allowed
15
+ #
6
16
  def initialize(type, allow_nil: false)
7
17
  self.type = type
8
18
  self.allow_nil = allow_nil
@@ -0,0 +1,16 @@
1
+ module MediaTypes
2
+ class Scheme
3
+ class EnumerationContext
4
+ def initialize(validations:)
5
+ self.validations = validations
6
+ end
7
+
8
+ def enumerate(val)
9
+ self.key = val
10
+ self
11
+ end
12
+
13
+ attr_accessor :validations, :key
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+ class EnumerationOfType
6
+
7
+ ##
8
+ # An attribute that expects a value of type +enumeration_type+ and each item of type +item_type+
9
+ #
10
+ # @param [Class] item_type the type of each item
11
+ # @param [Class] enumeration_type the type of the enumeration as a whole
12
+ # @param [TrueClass, FalseClass] allow_empty if true, an empty instance of +enumeration_type+ is valid
13
+ #
14
+ def initialize(item_type, enumeration_type: Array, allow_empty: false)
15
+ self.item_type = item_type
16
+ self.enumeration_type = enumeration_type
17
+ self.allow_empty = allow_empty
18
+
19
+ freeze
20
+ end
21
+
22
+ def validate!(output, options, **_opts)
23
+ validate_enumeration!(output, options) &&
24
+ validate_not_empty!(output, options) &&
25
+ validate_items!(output, options)
26
+ end
27
+
28
+ private
29
+
30
+ attr_accessor :allow_empty, :enumeration_type, :item_type
31
+
32
+ def validate_enumeration!(output, options)
33
+ return true if enumeration_type === output # rubocop:disable Style/CaseEquality
34
+ raise ValidationError,
35
+ format(
36
+ 'Expected collection as %<type>s, got %<actual>s at [%<backtrace>s]',
37
+ type: enumeration_type,
38
+ actual: output.inspect,
39
+ backtrace: options.backtrace.join('->')
40
+ )
41
+ end
42
+
43
+ def validate_not_empty!(output, options)
44
+ return true if allow_empty
45
+ return true if output.respond_to?(:length) && output.length.positive?
46
+ return true if output.respond_to?(:empty?) && !output.empty?
47
+
48
+ raise EmptyOutputError,
49
+ format(
50
+ 'Expected collection to be not empty, got empty at [%<backtrace>s]',
51
+ backtrace: options.backtrace.join('->')
52
+ )
53
+ end
54
+
55
+ def validate_items!(output, options)
56
+ output.all? do |item|
57
+ next true if item_type === item # rubocop:disable Style/CaseEquality
58
+ raise ValidationError,
59
+ format(
60
+ 'Expected collection item as %<type>s, got %<actual>s at [%<backtrace>s]',
61
+ type: item_type,
62
+ actual: item.inspect,
63
+ backtrace: options.backtrace.join('->')
64
+ )
65
+ end
66
+ end
67
+
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MediaTypes
4
+ class Scheme
5
+ class ValidationOptions
6
+ attr_accessor :exhaustive, :strict, :backtrace
7
+
8
+ def initialize(exhaustive: true, strict: true, backtrace: [])
9
+ self.exhaustive = exhaustive
10
+ self.strict = strict
11
+ self.backtrace = backtrace
12
+ end
13
+
14
+ def with_backtrace(backtrace)
15
+ ValidationOptions.new(exhaustive: exhaustive, strict: strict, backtrace: backtrace)
16
+ end
17
+
18
+ def trace(*traces)
19
+ with_backtrace(backtrace.dup.concat(traces))
20
+ end
21
+
22
+ def exhaustive!
23
+ ValidationOptions.new(exhaustive: true, strict: strict, backtrace: backtrace)
24
+ end
25
+ end
26
+ end
27
+ end