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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +44 -44
- data/README.md +379 -347
- data/lib/media_types/constructable.rb +12 -16
- data/lib/media_types/dsl.rb +7 -0
- data/lib/media_types/formatter.rb +55 -0
- data/lib/media_types/scheme.rb +59 -131
- data/lib/media_types/scheme/allow_nil.rb +2 -10
- data/lib/media_types/scheme/any_of.rb +4 -0
- data/lib/media_types/scheme/attribute.rb +4 -0
- data/lib/media_types/scheme/enumeration_context.rb +5 -3
- data/lib/media_types/scheme/enumeration_of_type.rb +4 -0
- data/lib/media_types/scheme/errors.rb +21 -0
- data/lib/media_types/scheme/links.rb +18 -12
- data/lib/media_types/scheme/missing_validation.rb +1 -1
- data/lib/media_types/scheme/not_strict.rb +4 -0
- data/lib/media_types/scheme/output_empty_guard.rb +40 -0
- data/lib/media_types/scheme/output_iterator_with_predicate.rb +55 -0
- data/lib/media_types/scheme/output_type_guard.rb +39 -0
- data/lib/media_types/scheme/rules.rb +84 -0
- data/lib/media_types/scheme/rules_exhausted_guard.rb +62 -0
- data/lib/media_types/version.rb +5 -5
- metadata +9 -2
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
data/lib/media_types/dsl.rb
CHANGED
@@ -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
|
data/lib/media_types/scheme.rb
CHANGED
@@ -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]
|
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,
|
62
|
-
self.
|
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
|
-
|
120
|
-
|
121
|
-
|
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,
|
155
|
+
return collection(key, expected_type: ::Hash, **opts, &block)
|
177
156
|
end
|
178
157
|
|
179
158
|
if type.is_a?(Scheme)
|
180
|
-
return
|
159
|
+
return rules.add(key, type, optional: optional)
|
181
160
|
end
|
182
161
|
|
183
|
-
|
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]
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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]
|
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,
|
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
|
295
|
+
return rules.add(key, scheme, optional: optional)
|
300
296
|
end
|
301
297
|
|
302
|
-
return
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
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
|
-
|
347
|
+
rules.fetch(:_links) do
|
348
348
|
Links.new.tap do |links|
|
349
|
-
|
349
|
+
rules.add(:_links, links)
|
350
350
|
end
|
351
351
|
end.link(*args, **opts, &block)
|
352
352
|
end
|
353
353
|
|
354
|
-
|
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
|
-
|
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
|
-
|
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 [
|
12
|
+
# @return [CaseEqualityWithList]
|
21
13
|
def AllowNil(klazz) # rubocop:disable Naming/MethodName
|
22
|
-
|
14
|
+
AnyOf(NilClass, klazz)
|
23
15
|
end
|
24
16
|
end
|
25
17
|
end
|