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.
@@ -3,6 +3,8 @@
3
3
  require 'delegate'
4
4
  require 'singleton'
5
5
 
6
+ require 'media_types/formatter'
7
+
6
8
  module MediaTypes
7
9
  class Constructable < SimpleDelegator
8
10
 
@@ -13,22 +15,22 @@ module MediaTypes
13
15
 
14
16
  def type(name = NO_ARG)
15
17
  return opts[:type] if name == NO_ARG
16
- Constructable.new(__getobj__, **with(type: name))
18
+ with(type: name)
17
19
  end
18
20
 
19
21
  def version(version = NO_ARG)
20
22
  return opts[:version] if version == NO_ARG
21
- Constructable.new(__getobj__, **with(version: version))
23
+ with(version: version)
22
24
  end
23
25
 
24
26
  def view(view = NO_ARG)
25
27
  return opts[:view] if view == NO_ARG
26
- Constructable.new(__getobj__, **with(view: view))
28
+ with(view: view)
27
29
  end
28
30
 
29
31
  def suffix(suffix = NO_ARG)
30
32
  return opts[:suffix] if suffix == NO_ARG
31
- Constructable.new(__getobj__, **with(suffix: suffix))
33
+ with(suffix: suffix)
32
34
  end
33
35
 
34
36
  def collection
@@ -79,13 +81,7 @@ module MediaTypes
79
81
  # TODO: remove warning by slicing out these arguments if they don't appear in the format
80
82
  qualified(
81
83
  qualifier,
82
- format(
83
- opts.fetch(:format),
84
- version: opts.fetch(:version),
85
- suffix: opts.fetch(:suffix) { :json },
86
- type: opts.fetch(:type),
87
- view: format_view(opts[:view])
88
- )
84
+ Formatter.call(opts)
89
85
  )
90
86
  end
91
87
 
@@ -123,16 +119,16 @@ module MediaTypes
123
119
  attr_accessor :opts
124
120
 
125
121
  def with(more_opts)
126
- Hash(opts).clone.merge(more_opts)
122
+ merged_options = Kernel::Hash(opts).clone.tap do |cloned|
123
+ cloned.merge!(more_opts)
124
+ end
125
+
126
+ Constructable.new(__getobj__, **merged_options)
127
127
  end
128
128
 
129
129
  def qualified(qualifier, media_type)
130
130
  return media_type unless qualifier
131
131
  format('%<media_type>s; q=%<q>s', media_type: media_type, q: qualifier)
132
132
  end
133
-
134
- def format_view(view)
135
- MediaTypes::Object.new(view).present? && ".#{view}" || ''
136
- end
137
133
  end
138
134
  end
@@ -48,6 +48,13 @@ module MediaTypes
48
48
  private
49
49
 
50
50
  def media_type(name, defaults: {})
51
+
52
+ unless defined?(:base_format)
53
+ define_method(:base_format) do
54
+ raise format('Implement the class method "base_format" in %<klass>s', klass: self)
55
+ end
56
+ end
57
+
51
58
  self.media_type_constructable = Constructable.new(self, format: base_format, type: name)
52
59
  .version(defaults.fetch(:version) { nil })
53
60
  .suffix(defaults.fetch(:suffix) { nil })
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'media_types/object'
4
+
5
+ module MediaTypes
6
+ class Formatter < DelegateClass(Hash)
7
+
8
+ class << self
9
+ def call(*args, **options)
10
+ new(*args, **options).call
11
+ end
12
+ end
13
+
14
+ def call
15
+ filtered_arguments = arguments
16
+ return template if MediaTypes::Object.new(filtered_arguments).empty?
17
+
18
+ format(rework_template(filtered_arguments), filtered_arguments)
19
+ end
20
+
21
+ private
22
+
23
+ def template
24
+ fetch(:format)
25
+ end
26
+
27
+ def rework_template(filtered_arguments)
28
+ filtered_arguments.reduce(template) do |reworked, (key, value)|
29
+ next reworked if MediaTypes::Object.new(value).present?
30
+ start_of_template_variable = "%<#{key}>"
31
+
32
+ # noinspection RubyBlockToMethodReference
33
+ reworked.gsub("[\\.+](#{start_of_template_variable})") { start_of_template_variable }
34
+ end
35
+ end
36
+
37
+ def format_view(view)
38
+ MediaTypes::Object.new(view).present? && ".#{view}" || ''
39
+ end
40
+
41
+ def arguments
42
+ # noinspection RubyBlockToMethodReference
43
+ {
44
+ version: self[:version],
45
+ suffix: self[:suffix],
46
+ type: self[:type],
47
+ view: self[:view]
48
+ }.select { |argument,| argument_present?(argument) }
49
+ end
50
+
51
+ def argument_present?(argument)
52
+ template.include?("%<#{argument}>")
53
+ end
54
+ end
55
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'media_types/scheme/validation_options'
4
4
  require 'media_types/scheme/enumeration_context'
5
+ require 'media_types/scheme/errors'
6
+ require 'media_types/scheme/rules'
5
7
 
6
8
  require 'media_types/scheme/allow_nil'
7
9
  require 'media_types/scheme/any_of'
@@ -11,6 +13,12 @@ require 'media_types/scheme/links'
11
13
  require 'media_types/scheme/missing_validation'
12
14
  require 'media_types/scheme/not_strict'
13
15
 
16
+ require 'media_types/scheme/output_empty_guard'
17
+ require 'media_types/scheme/output_type_guard'
18
+ require 'media_types/scheme/rules_exhausted_guard'
19
+
20
+ require 'awesome_print'
21
+
14
22
  module MediaTypes
15
23
  ##
16
24
  # Media Type Schemes can validate content to a media type, by itself. Used by the `validations` dsl.
@@ -35,35 +43,16 @@ module MediaTypes
35
43
  #
36
44
  class Scheme
37
45
 
38
- # Base class for all validations errors
39
- class ValidationError < ArgumentError; end
40
-
41
- # Raised when it expected more data but there wasn't any left
42
- class ExhaustedOutputError < ValidationError; end
43
-
44
- # Raised when it did not expect more data, but there was more left
45
- class StrictValidationError < ValidationError; end
46
-
47
- # Raised when it expected not to be empty, but it was
48
- class EmptyOutputError < ValidationError; end
49
-
50
- # Raised when a value did not have the expected collection type
51
- class CollectionTypeError < ValidationError; end
52
-
53
46
  ##
54
47
  # Creates a new scheme
55
48
  #
56
49
  # @param [TrueClass, FalseClass] allow_empty if true allows to be empty, if false raises EmptyOutputError if empty
57
- # @param [NilClass, Class] force forces the type to be this type, if given
50
+ # @param [NilClass, Class] expected_type forces the type to be this type, if given
58
51
  #
59
52
  # @see MissingValidation
60
53
  #
61
- def initialize(allow_empty: false, force: nil, &block)
62
- self.validations = {}
63
- self.allow_empty = allow_empty
64
- self.force = force
65
-
66
- validations.default = MissingValidation.new
54
+ def initialize(allow_empty: false, expected_type: ::Object, &block)
55
+ self.rules = Rules.new(allow_empty: allow_empty, expected_type: expected_type)
67
56
 
68
57
  instance_exec(&block) if block_given?
69
58
  end
@@ -116,19 +105,9 @@ module MediaTypes
116
105
  # @private
117
106
  #
118
107
  def validate!(output, call_options, **_opts)
119
- empty_guard!(output, call_options)
120
-
121
- exhaustive_guard!(validations.keys, call_options) do |mark|
122
- all?(output, call_options) do |key, value, options:, context:|
123
- mark.call(key)
124
-
125
- validations[key].validate!(
126
- value,
127
- options.trace(key),
128
- context: context
129
- )
130
- end
131
- end
108
+ OutputTypeGuard.call(output, call_options, rules: rules)
109
+ OutputEmptyGuard.call(output, call_options, rules: rules)
110
+ RulesExhaustedGuard.call(output, call_options, rules: rules)
132
111
  end
133
112
 
134
113
  ##
@@ -171,16 +150,16 @@ module MediaTypes
171
150
  # MyMedia.valid?({ foo: { bar: 'my-string' }})
172
151
  # # => true
173
152
  #
174
- def attribute(key, type = String, **opts, &block)
153
+ def attribute(key, type = String, optional: false, **opts, &block)
175
154
  if block_given?
176
- return collection(key, force: ::Hash, **opts, &block)
155
+ return collection(key, expected_type: ::Hash, **opts, &block)
177
156
  end
178
157
 
179
158
  if type.is_a?(Scheme)
180
- return validations[String(key)] = type
159
+ return rules.add(key, type, optional: optional)
181
160
  end
182
161
 
183
- validations[String(key)] = Attribute.new(type, **opts, &block)
162
+ rules.add(key, Attribute.new(type, **opts, &block), optional: optional)
184
163
  end
185
164
 
186
165
  ##
@@ -189,7 +168,7 @@ module MediaTypes
189
168
  #
190
169
  # @param [Scheme, NilClass] scheme scheme to use if no +&block+ is given
191
170
  # @param [TrueClass, FalseClass] allow_empty if true, empty (no key/value present) is allowed
192
- # @param [Class] force forces the validated object to have this type
171
+ # @param [Class] expected_type forces the validated object to have this type
193
172
  #
194
173
  # @see Scheme
195
174
  #
@@ -210,12 +189,29 @@ module MediaTypes
210
189
  # MyMedia.valid?({ foo: [{ anything: { bar: 'my-string' }, other_thing: { bar: 'other-string' } }] })
211
190
  # # => true
212
191
  #
213
- def any(scheme = nil, force: ::Hash, allow_empty: false, &block)
192
+ # @example Any key, but all of them String or Numeric
193
+ #
194
+ # class MyMedia
195
+ # include MediaTypes::Dsl
196
+ #
197
+ # validations do
198
+ # any AnyOf(String, Numeric)
199
+ # end
200
+ # end
201
+ #
202
+ # MyMedia.valid?({ foo: 'my-string', bar: 42 })
203
+ # # => true
204
+ #
205
+ def any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block)
214
206
  unless block_given?
215
- return validations.default = scheme
207
+ if scheme.is_a?(Scheme)
208
+ return rules.default = scheme
209
+ end
210
+
211
+ return rules.default = Attribute.new(scheme)
216
212
  end
217
213
 
218
- validations.default = Scheme.new(allow_empty: allow_empty, force: force, &block)
214
+ rules.default = Scheme.new(allow_empty: allow_empty, expected_type: expected_type, &block)
219
215
  end
220
216
 
221
217
  ##
@@ -224,7 +220,7 @@ module MediaTypes
224
220
  # @param [Scheme] scheme the scheme to merge into this
225
221
  #
226
222
  def merge(scheme, &block)
227
- self.validations = validations.merge(scheme.send(:validations).clone)
223
+ self.rules = rules.merge(scheme.send(:rules))
228
224
  instance_exec(&block) if block_given?
229
225
  end
230
226
 
@@ -250,7 +246,7 @@ module MediaTypes
250
246
  # # => true
251
247
  #
252
248
  def not_strict
253
- validations.default = NotStrict.new
249
+ rules.default = NotStrict.new
254
250
  end
255
251
 
256
252
  ##
@@ -260,7 +256,7 @@ module MediaTypes
260
256
  # @param [Symbol] key key of the collection (same as #attribute)
261
257
  # @param [NilClass, Scheme, Class] scheme scheme to use if no +&block+ is given, or type of each item in collection
262
258
  # @param [TrueClass, FalseClass] allow_empty if true accepts 0 items in an enumerable
263
- # @param [Class] force forces the value of this collection to be this type, defaults to Array.
259
+ # @param [Class] expected_type forces the value of this collection to be this type, defaults to Array.
264
260
  #
265
261
  # @see Scheme
266
262
  #
@@ -293,20 +289,24 @@ module MediaTypes
293
289
  # MyMedia.valid?({ foo: [{ required: 'test', number: 42 }, { required: 'other', number: 0 }] })
294
290
  # # => true
295
291
  #
296
- def collection(key, scheme = nil, allow_empty: false, force: Array, &block)
292
+ def collection(key, scheme = nil, allow_empty: false, expected_type: ::Array, optional: false, &block)
297
293
  unless block_given?
298
294
  if scheme.is_a?(Scheme)
299
- return validations[String(key)] = scheme
295
+ return rules.add(key, scheme, optional: optional)
300
296
  end
301
297
 
302
- return validations[String(key)] = EnumerationOfType.new(
303
- scheme,
304
- enumeration_type: force,
305
- allow_empty: allow_empty
298
+ return rules.add(
299
+ key,
300
+ EnumerationOfType.new(
301
+ scheme,
302
+ enumeration_type: expected_type,
303
+ allow_empty: allow_empty
304
+ ),
305
+ optional: optional
306
306
  )
307
307
  end
308
308
 
309
- validations[String(key)] = Scheme.new(allow_empty: allow_empty, force: force, &block)
309
+ rules.add(key, Scheme.new(allow_empty: allow_empty, expected_type: expected_type, &block), optional: optional)
310
310
  end
311
311
 
312
312
  ##
@@ -344,91 +344,19 @@ module MediaTypes
344
344
  # # => true
345
345
  #
346
346
  def link(*args, **opts, &block)
347
- validations.fetch('_links') do
347
+ rules.fetch(:_links) do
348
348
  Links.new.tap do |links|
349
- validations['_links'] = links
349
+ rules.add(:_links, links)
350
350
  end
351
351
  end.link(*args, **opts, &block)
352
352
  end
353
353
 
354
- private
355
-
356
- attr_accessor :validations, :allow_empty, :force
357
-
358
- ##
359
- # Checks if the output is nil or empty
360
- #
361
- # @private
362
- #
363
- def empty_guard!(output, options)
364
- return unless MediaTypes::Object.new(output).empty?
365
- throw(:end, true) if allow_empty
366
- raise_empty!(backtrace: options.backtrace)
367
- end
368
-
369
- ##
370
- # Mimics Enumerable#all? with mandatory +&block+
371
- #
372
- def all?(enumerable, options, &block)
373
- context = EnumerationContext.new(validations: validations)
374
-
375
- if force && !(force === enumerable) # rubocop:disable Style/CaseEquality
376
- raise_forced_type_error!(type: enumerable.class, backtrace: options.backtrace)
377
- end
378
-
379
- if enumerable.is_a?(Hash) || enumerable.respond_to?(:key)
380
- return enumerable.all? do |key, value|
381
- yield String(key), value, options: options, context: context.enumerate(key)
382
- end
383
- end
384
-
385
- without_forcing_type do
386
- enumerable.each_with_index.all? do |array_like_element, i|
387
- all?(array_like_element, options.trace(i), &block)
388
- end
389
- end
390
- end
391
-
392
- def raise_forced_type_error!(type:, backtrace:)
393
- raise CollectionTypeError, format(
394
- 'Expected a %<expected>s, got a %<actual>s at %<backtrace>s',
395
- expected: force,
396
- actual: type,
397
- backtrace: backtrace.join('->')
398
- )
399
- end
400
-
401
- def raise_empty!(backtrace:)
402
- raise EmptyOutputError, format('Expected output, got empty at %<backtrace>s', backtrace: backtrace.join('->'))
403
- end
404
-
405
- def raise_exhausted!(backtrace:, missing_keys:)
406
- raise ExhaustedOutputError, format(
407
- 'Missing keys in output: %<missing_keys>s at [%<backtrace>s]',
408
- missing_keys: missing_keys,
409
- backtrace: backtrace.join('->')
410
- )
354
+ def inspect
355
+ "[Scheme]#{rules}[/Scheme]"
411
356
  end
412
357
 
413
- def exhaustive_guard!(keys, options)
414
- unless options.exhaustive
415
- return yield(->(_) {})
416
- end
417
-
418
- exhaustive_keys = keys.clone.map(&:to_s)
419
- # noinspection RubyScope
420
- result = yield ->(key) { exhaustive_keys.delete(String(key)) }
421
- return result if exhaustive_keys.empty?
422
-
423
- raise_exhausted!(missing_keys: exhaustive_keys, backtrace: options.backtrace)
424
- end
358
+ private
425
359
 
426
- def without_forcing_type
427
- before_force = force
428
- self.force = nil
429
- result = yield
430
- self.force = before_force
431
- result
432
- end
360
+ attr_accessor :rules
433
361
  end
434
362
  end
@@ -4,22 +4,14 @@ require 'delegate'
4
4
 
5
5
  module MediaTypes
6
6
  class Scheme
7
- class CaseEqualityWithNil < SimpleDelegator
8
-
9
- # Same as the wrapped {Object#===}, but also allows for NilCLass
10
- def ===(other)
11
- other.nil? || super
12
- end
13
- end
14
-
15
7
  # noinspection RubyInstanceMethodNamingConvention
16
8
  ##
17
9
  # Allows the wrapped +klazz+ to be nil
18
10
  #
19
11
  # @param [Class] klazz the class that +it+ must be the if +it+ is not NilClass
20
- # @return [CaseEqualityWithNil]
12
+ # @return [CaseEqualityWithList]
21
13
  def AllowNil(klazz) # rubocop:disable Naming/MethodName
22
- CaseEqualityWithNil.new(klazz)
14
+ AnyOf(NilClass, klazz)
23
15
  end
24
16
  end
25
17
  end