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