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.
@@ -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