media_types 2.0.1 → 2.1.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 +70 -63
- data/Gemfile.lock +43 -41
- data/LICENSE +21 -0
- data/README.md +276 -32
- data/lib/media_types.rb +46 -2
- data/lib/media_types/constructable.rb +8 -2
- data/lib/media_types/dsl.rb +60 -17
- data/lib/media_types/dsl/errors.rb +18 -0
- data/lib/media_types/errors.rb +19 -0
- data/lib/media_types/scheme.rb +123 -18
- data/lib/media_types/scheme/errors.rb +60 -0
- data/lib/media_types/scheme/links.rb +15 -0
- data/lib/media_types/scheme/output_type_guard.rb +1 -3
- data/lib/media_types/scheme/rules.rb +54 -3
- data/lib/media_types/scheme/rules_exhausted_guard.rb +10 -0
- data/lib/media_types/scheme/validation_options.rb +5 -4
- data/lib/media_types/testing/assertions.rb +20 -0
- data/lib/media_types/validations.rb +7 -4
- data/lib/media_types/version.rb +1 -1
- data/media_types.gemspec +1 -1
- metadata +12 -9
- data/lib/media_types/minitest/assert_media_type_format.rb +0 -10
data/lib/media_types.rb
CHANGED
@@ -7,6 +7,7 @@ require 'media_types/hash'
|
|
7
7
|
require 'media_types/object'
|
8
8
|
require 'media_types/scheme'
|
9
9
|
require 'media_types/dsl'
|
10
|
+
require 'media_types/errors'
|
10
11
|
|
11
12
|
require 'media_types/views'
|
12
13
|
|
@@ -16,14 +17,57 @@ module MediaTypes
|
|
16
17
|
@organisation_prefixes[mod.name] = organisation
|
17
18
|
end
|
18
19
|
|
20
|
+
def self.expect_string_keys(mod)
|
21
|
+
set_key_expectation(mod, false)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.expect_symbol_keys(mod)
|
25
|
+
set_key_expectation(mod, true)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Keep track of modules setting their key expectations
|
29
|
+
def self.set_key_expectation(mod, expect_symbol_keys)
|
30
|
+
@key_expectations ||= {}
|
31
|
+
@key_expectations_used ||= {}
|
32
|
+
|
33
|
+
raise KeyExpectationSetError.new(mod: mod) unless @key_expectations[mod.name].nil?
|
34
|
+
raise KeyExpectationUsedError.new(mod: mod) if @key_expectations_used[mod.name]
|
35
|
+
|
36
|
+
@key_expectations[mod.name] = expect_symbol_keys
|
37
|
+
end
|
38
|
+
|
39
|
+
SYMBOL_KEYS_DEFAULT = true
|
40
|
+
|
41
|
+
def self.get_key_expectation(mod)
|
42
|
+
@key_expectations ||= {}
|
43
|
+
@key_expectations_used ||= {}
|
44
|
+
|
45
|
+
expect_symbol = find_key_expectation(mod)
|
46
|
+
|
47
|
+
expect_symbol.nil? ? SYMBOL_KEYS_DEFAULT : expect_symbol
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.find_key_expectation(mod)
|
51
|
+
modules = mod.name.split('::')
|
52
|
+
expect_symbol = nil
|
53
|
+
|
54
|
+
while modules.any? && expect_symbol.nil?
|
55
|
+
current_module = modules.join('::')
|
56
|
+
expect_symbol = @key_expectations[current_module]
|
57
|
+
@key_expectations_used[current_module] = true
|
58
|
+
modules.pop
|
59
|
+
end
|
60
|
+
|
61
|
+
expect_symbol
|
62
|
+
end
|
63
|
+
|
19
64
|
def self.get_organisation(mod)
|
20
65
|
name = mod.name
|
21
66
|
prefixes = @organisation_prefixes.keys.select { |p| name.start_with? p }
|
22
67
|
return nil unless prefixes.any?
|
68
|
+
|
23
69
|
best = prefixes.max_by { |p| p.length }
|
24
70
|
|
25
71
|
@organisation_prefixes[best]
|
26
72
|
end
|
27
73
|
end
|
28
|
-
|
29
|
-
|
@@ -76,7 +76,13 @@ module MediaTypes
|
|
76
76
|
as_key.hash
|
77
77
|
end
|
78
78
|
|
79
|
+
def override_suffix(suffix)
|
80
|
+
with(suffix: suffix)
|
81
|
+
end
|
82
|
+
|
79
83
|
def suffix
|
84
|
+
return opts[:suffix] if opts.key?(:suffix)
|
85
|
+
|
80
86
|
schema = schema_for(self)
|
81
87
|
schema.type_attributes.fetch(:suffix, 'json')
|
82
88
|
end
|
@@ -92,7 +98,7 @@ module MediaTypes
|
|
92
98
|
)
|
93
99
|
)
|
94
100
|
end
|
95
|
-
|
101
|
+
|
96
102
|
def available_validations
|
97
103
|
return [] if !validatable?
|
98
104
|
[self]
|
@@ -120,7 +126,7 @@ module MediaTypes
|
|
120
126
|
|
121
127
|
def validatable?
|
122
128
|
return false unless media_type_combinations.include? as_key
|
123
|
-
|
129
|
+
|
124
130
|
__getobj__.validatable?(self)
|
125
131
|
end
|
126
132
|
|
data/lib/media_types/dsl.rb
CHANGED
@@ -3,44 +3,61 @@
|
|
3
3
|
require 'media_types/constructable'
|
4
4
|
require 'media_types/validations'
|
5
5
|
|
6
|
+
require 'media_types/dsl/errors'
|
7
|
+
|
6
8
|
module MediaTypes
|
7
9
|
module Dsl
|
8
|
-
|
9
10
|
def self.included(base)
|
10
11
|
base.extend ClassMethods
|
11
12
|
base.class_eval do
|
12
13
|
class << self
|
13
|
-
attr_accessor :media_type_name_for, :media_type_combinations
|
14
|
+
attr_accessor :media_type_name_for, :media_type_combinations, :media_type_validations, :symbol_keys
|
14
15
|
|
15
16
|
private
|
16
17
|
|
17
|
-
attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar
|
18
|
+
attr_accessor :media_type_constructable, :symbol_base, :media_type_registrar
|
18
19
|
end
|
19
20
|
base.media_type_combinations = Set.new
|
20
21
|
end
|
21
22
|
end
|
22
23
|
|
23
24
|
module ClassMethods
|
24
|
-
|
25
25
|
def to_constructable
|
26
|
+
raise UninitializedConstructable if media_type_constructable.nil?
|
27
|
+
|
26
28
|
media_type_constructable.dup.tap do |constructable|
|
27
29
|
constructable.__setobj__(self)
|
28
30
|
end
|
29
31
|
end
|
30
32
|
|
33
|
+
def symbol_keys?
|
34
|
+
if symbol_keys.nil?
|
35
|
+
MediaTypes.get_key_expectation(self)
|
36
|
+
else
|
37
|
+
symbol_keys
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def string_keys?
|
42
|
+
!symbol_keys?
|
43
|
+
end
|
44
|
+
|
31
45
|
def valid?(output, **opts)
|
32
46
|
to_constructable.valid?(output, **opts)
|
33
47
|
end
|
34
48
|
|
35
49
|
def valid_unsafe?(output, media_type = to_constructable, **opts)
|
50
|
+
opts[:expected_key_type] = string_keys? ? String : Symbol
|
36
51
|
validations.find(media_type).valid?(output, backtrace: ['.'], **opts)
|
37
52
|
end
|
38
|
-
|
53
|
+
|
39
54
|
def validate!(output, **opts)
|
55
|
+
assert_sane!
|
40
56
|
to_constructable.validate!(output, **opts)
|
41
57
|
end
|
42
58
|
|
43
59
|
def validate_unsafe!(output, media_type = to_constructable, **opts)
|
60
|
+
opts[:expected_key_type] = string_keys? ? String : Symbol
|
44
61
|
validations.find(media_type).validate(output, backtrace: ['.'], **opts)
|
45
62
|
end
|
46
63
|
|
@@ -58,16 +75,17 @@ module MediaTypes
|
|
58
75
|
registerable
|
59
76
|
end
|
60
77
|
end
|
61
|
-
|
78
|
+
|
62
79
|
def view(v)
|
63
80
|
to_constructable.view(v)
|
64
81
|
end
|
82
|
+
|
65
83
|
def version(v)
|
66
84
|
to_constructable.version(v)
|
67
85
|
end
|
68
|
-
|
86
|
+
|
69
87
|
def identifier_format
|
70
|
-
self.media_type_name_for =
|
88
|
+
self.media_type_name_for = proc do |type:, view:, version:, suffix:|
|
71
89
|
yield(type: type, view: view, version: version, suffix: suffix)
|
72
90
|
end
|
73
91
|
end
|
@@ -77,7 +95,7 @@ module MediaTypes
|
|
77
95
|
end
|
78
96
|
|
79
97
|
def available_validations
|
80
|
-
|
98
|
+
media_type_combinations.map do |a|
|
81
99
|
_, view, version = a
|
82
100
|
view(view).version(version)
|
83
101
|
end
|
@@ -87,18 +105,27 @@ module MediaTypes
|
|
87
105
|
validations.find(constructable)
|
88
106
|
end
|
89
107
|
|
108
|
+
def assert_sane!
|
109
|
+
return if media_type_validations.scheme.asserted_sane?
|
110
|
+
|
111
|
+
media_type_validations.run_fixture_validations(symbol_keys?)
|
112
|
+
end
|
113
|
+
|
90
114
|
private
|
91
115
|
|
92
116
|
def use_name(name)
|
93
|
-
if
|
94
|
-
self.media_type_name_for =
|
117
|
+
if media_type_name_for.nil?
|
118
|
+
self.media_type_name_for = proc do |type:, view:, version:, suffix:|
|
95
119
|
resolved_org = nil
|
96
120
|
if defined?(organisation)
|
97
121
|
resolved_org = organisation
|
98
122
|
else
|
99
|
-
resolved_org = MediaTypes
|
123
|
+
resolved_org = MediaTypes.get_organisation(self)
|
100
124
|
|
101
|
-
|
125
|
+
if resolved_org.nil?
|
126
|
+
raise OrganisationNotSetError,
|
127
|
+
format('Implement the class method "organisation" in %<klass>s or specify a global organisation using MediaTypes::set_organisation', klass: self)
|
128
|
+
end
|
102
129
|
end
|
103
130
|
raise ArgumentError, 'Unable to create a name for a schema with a nil name.' if type.nil?
|
104
131
|
raise ArgumentError, 'Unable to create a name for a schema with a nil organisation.' if resolved_org.nil?
|
@@ -113,15 +140,31 @@ module MediaTypes
|
|
113
140
|
self.media_type_constructable = Constructable.new(self, type: name)
|
114
141
|
end
|
115
142
|
|
143
|
+
def expect_string_keys
|
144
|
+
raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
|
145
|
+
|
146
|
+
self.symbol_keys = false
|
147
|
+
end
|
148
|
+
|
149
|
+
def expect_symbol_keys
|
150
|
+
raise KeyTypeExpectationError, 'Key expectation already set' unless symbol_keys.nil?
|
151
|
+
|
152
|
+
self.symbol_keys = true
|
153
|
+
end
|
154
|
+
|
116
155
|
def validations(&block)
|
117
|
-
unless block_given?
|
118
|
-
|
119
|
-
return media_type_validations
|
120
|
-
end
|
156
|
+
return lookup_validations unless block_given?
|
157
|
+
|
121
158
|
self.media_type_validations = Validations.new(to_constructable, &block)
|
122
159
|
|
123
160
|
self
|
124
161
|
end
|
162
|
+
|
163
|
+
def lookup_validations
|
164
|
+
raise MissingValidationError, "No validations defined for #{name}" if media_type_validations.nil?
|
165
|
+
|
166
|
+
media_type_validations
|
167
|
+
end
|
125
168
|
end
|
126
169
|
end
|
127
170
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MediaTypes
|
4
|
+
module Dsl
|
5
|
+
class UninitializedConstructable < RuntimeError
|
6
|
+
def message
|
7
|
+
'Unable to generate constructable without a name, make sure to have called `use_name(name)` before.'
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Raised when an error occurs during setting expected key type
|
12
|
+
class KeyTypeExpectationError < StandardError; end
|
13
|
+
|
14
|
+
class MissingValidationError < StandardError; end
|
15
|
+
|
16
|
+
class OrganisationNotSetError < StandardError; end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MediaTypes
|
4
|
+
module Errors
|
5
|
+
# Raised when trying to set a module key expectation twice
|
6
|
+
class KeyExpectationSetError < StandardError
|
7
|
+
def initialize(mod:)
|
8
|
+
super(format('%<mod>s already has a key expectation set', mod: mod.name))
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Raised when trying to set a module key expectation while default expectation already used
|
13
|
+
class KeyExpectationUsedError < StandardError
|
14
|
+
def initialize(mod:)
|
15
|
+
super(format('Unable to change key type expectation for %<mod>s since its current expectation is already used', mod: mod.name))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/media_types/scheme.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'json'
|
4
|
+
|
3
5
|
require 'media_types/scheme/validation_options'
|
4
6
|
require 'media_types/scheme/enumeration_context'
|
5
7
|
require 'media_types/scheme/errors'
|
@@ -17,10 +19,47 @@ require 'media_types/scheme/output_empty_guard'
|
|
17
19
|
require 'media_types/scheme/output_type_guard'
|
18
20
|
require 'media_types/scheme/rules_exhausted_guard'
|
19
21
|
|
20
|
-
require 'json'
|
21
|
-
|
22
22
|
module MediaTypes
|
23
23
|
class AssertionError < StandardError
|
24
|
+
def initialize(errors)
|
25
|
+
@fixture_errors = errors
|
26
|
+
end
|
27
|
+
|
28
|
+
def message
|
29
|
+
fixture_errors.map(&:message).join(', ')
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :fixture_errors
|
33
|
+
end
|
34
|
+
|
35
|
+
class UnexpectedValidationResultError < StandardError
|
36
|
+
def initialize(fixture_caller, error)
|
37
|
+
self.fixture_caller = fixture_caller
|
38
|
+
self.error = error
|
39
|
+
end
|
40
|
+
|
41
|
+
def message
|
42
|
+
format(
|
43
|
+
'%<caller_path>s:%<caller_line>s -> %<error>s',
|
44
|
+
caller_path: fixture_caller.path,
|
45
|
+
caller_line: fixture_caller.lineno,
|
46
|
+
error: error.is_a?(MediaTypes::Scheme::ValidationError) ? "#{error.class}:#{error.message}" : error
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
attr_accessor :fixture_caller, :error
|
51
|
+
end
|
52
|
+
|
53
|
+
class FixtureData
|
54
|
+
def initialize(caller:, fixture:, expect_to_pass:)
|
55
|
+
self.caller = caller
|
56
|
+
self.fixture = fixture
|
57
|
+
self.expect_to_pass = expect_to_pass
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_accessor :caller, :fixture, :expect_to_pass
|
61
|
+
|
62
|
+
alias expect_to_pass? expect_to_pass
|
24
63
|
end
|
25
64
|
|
26
65
|
##
|
@@ -57,11 +96,16 @@ module MediaTypes
|
|
57
96
|
def initialize(allow_empty: false, expected_type: ::Object, &block)
|
58
97
|
self.rules = Rules.new(allow_empty: allow_empty, expected_type: expected_type)
|
59
98
|
self.type_attributes = {}
|
99
|
+
self.fixtures = []
|
100
|
+
self.asserted_sane = false
|
60
101
|
|
61
102
|
instance_exec(&block) if block_given?
|
62
103
|
end
|
63
104
|
|
64
|
-
attr_accessor :type_attributes
|
105
|
+
attr_accessor :type_attributes, :fixtures
|
106
|
+
attr_reader :rules, :asserted_sane
|
107
|
+
|
108
|
+
alias asserted_sane? asserted_sane
|
65
109
|
|
66
110
|
##
|
67
111
|
# Checks if the +output+ is valid
|
@@ -158,11 +202,10 @@ module MediaTypes
|
|
158
202
|
# MyMedia.valid?({ foo: { bar: 'my-string' }})
|
159
203
|
# # => true
|
160
204
|
#
|
161
|
-
def attribute(key, type =
|
162
|
-
raise
|
163
|
-
|
164
|
-
|
165
|
-
raise DuplicateKeyError, "A symbol attribute with the same string representation as the string '#{key}' already exists. Please remove one of the two." if key.is_a?(String) && rules.has_key?(key.to_sym)
|
205
|
+
def attribute(key, type = nil, optional: false, **opts, &block)
|
206
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed attribute, either remove the type or the block' if type != ::Hash && block_given? && !type.nil?
|
207
|
+
|
208
|
+
type ||= ::Object
|
166
209
|
|
167
210
|
if block_given?
|
168
211
|
return collection(key, expected_type: ::Hash, optional: optional, **opts, &block)
|
@@ -216,6 +259,8 @@ module MediaTypes
|
|
216
259
|
# # => true
|
217
260
|
#
|
218
261
|
def any(scheme = nil, expected_type: ::Hash, allow_empty: false, &block)
|
262
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed property, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
|
263
|
+
|
219
264
|
unless block_given?
|
220
265
|
if scheme.is_a?(Scheme)
|
221
266
|
return rules.default = scheme
|
@@ -303,6 +348,8 @@ module MediaTypes
|
|
303
348
|
# # => true
|
304
349
|
#
|
305
350
|
def collection(key, scheme = nil, allow_empty: false, expected_type: ::Array, optional: false, &block)
|
351
|
+
raise ConflictingTypeDefinitionError, 'You cannot apply a block to a non-hash typed collection, either remove the type or the block' if scheme != ::Hash && block_given? && !scheme.nil?
|
352
|
+
|
306
353
|
unless block_given?
|
307
354
|
return rules.add(
|
308
355
|
key,
|
@@ -385,24 +432,82 @@ module MediaTypes
|
|
385
432
|
end
|
386
433
|
|
387
434
|
def assert_pass(fixture)
|
388
|
-
|
389
|
-
|
390
|
-
validate(json)
|
435
|
+
reduced_stack = remove_current_dir_from_stack(caller_locations)
|
436
|
+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: true)
|
391
437
|
end
|
392
|
-
|
438
|
+
|
393
439
|
def assert_fail(fixture)
|
394
|
-
|
440
|
+
reduced_stack = remove_current_dir_from_stack(caller_locations)
|
441
|
+
@fixtures << FixtureData.new(caller: reduced_stack.first, fixture: fixture, expect_to_pass: false)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Removes all calls originating in current dir from given stack
|
445
|
+
# We need this so that we find out the caller of an assert_pass/fail in the caller_locations
|
446
|
+
# Which gets polluted by Scheme consecutively executing blocks within the validation blocks
|
447
|
+
def remove_current_dir_from_stack(stack)
|
448
|
+
stack.reject { |location| location.path.include?(__dir__) }
|
449
|
+
end
|
450
|
+
|
451
|
+
def validate_scheme_fixtures(expect_symbol_keys, backtrace)
|
452
|
+
@fixtures.map do |fixture_data|
|
453
|
+
begin
|
454
|
+
validate_fixture(fixture_data, expect_symbol_keys, backtrace)
|
455
|
+
nil
|
456
|
+
rescue UnexpectedValidationResultError => e
|
457
|
+
e
|
458
|
+
end
|
459
|
+
end.compact
|
460
|
+
end
|
461
|
+
|
462
|
+
def validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
|
463
|
+
@rules.flat_map do |key, rule|
|
464
|
+
next unless rule.is_a?(Scheme) || rule.is_a?(Links)
|
465
|
+
|
466
|
+
begin
|
467
|
+
rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
|
468
|
+
nil
|
469
|
+
rescue AssertionError => e
|
470
|
+
e.fixture_errors
|
471
|
+
end
|
472
|
+
end.compact
|
473
|
+
end
|
474
|
+
|
475
|
+
def validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
|
476
|
+
return [] unless @rules.default.is_a?(Scheme)
|
477
|
+
|
478
|
+
@rules.default.run_fixture_validations(expect_symbol_keys, backtrace.dup.append('*'))
|
479
|
+
[]
|
480
|
+
rescue AssertionError => e
|
481
|
+
e.fixture_errors
|
482
|
+
end
|
483
|
+
|
484
|
+
def run_fixture_validations(expect_symbol_keys, backtrace = [])
|
485
|
+
fixture_errors = validate_scheme_fixtures(expect_symbol_keys, backtrace)
|
486
|
+
fixture_errors += validate_nested_scheme_fixtures(expect_symbol_keys, backtrace)
|
487
|
+
fixture_errors += validate_default_scheme_fixtures(expect_symbol_keys, backtrace)
|
488
|
+
|
489
|
+
raise AssertionError, fixture_errors unless fixture_errors.empty?
|
490
|
+
|
491
|
+
self.asserted_sane = true
|
492
|
+
end
|
493
|
+
|
494
|
+
def validate_fixture(fixture_data, expect_symbol_keys, backtrace = [])
|
495
|
+
json = JSON.parse(fixture_data.fixture, { symbolize_names: expect_symbol_keys })
|
496
|
+
expected_key_type = expect_symbol_keys ? Symbol : String
|
395
497
|
|
396
498
|
begin
|
397
|
-
validate(json)
|
398
|
-
|
399
|
-
|
499
|
+
validate(json, expected_key_type: expected_key_type, backtrace: backtrace)
|
500
|
+
unless fixture_data.expect_to_pass?
|
501
|
+
raise UnexpectedValidationResultError.new(fixture_data.caller, 'No error encounterd whilst expecting to')
|
502
|
+
end
|
503
|
+
rescue MediaTypes::Scheme::ValidationError => e
|
504
|
+
raise UnexpectedValidationResultError.new(fixture_data.caller, e) if fixture_data.expect_to_pass?
|
400
505
|
end
|
401
|
-
raise AssertionError
|
402
506
|
end
|
403
507
|
|
404
508
|
private
|
405
509
|
|
406
|
-
|
510
|
+
attr_writer :rules, :asserted_sane
|
511
|
+
|
407
512
|
end
|
408
513
|
end
|