media_types 0.6.2 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/debian.yml +42 -0
- data/.github/workflows/ruby.yml +22 -0
- data/.gitignore +10 -10
- data/CHANGELOG.md +76 -41
- data/Gemfile +6 -6
- data/Gemfile.lock +17 -83
- data/LICENSE +21 -0
- data/README.md +364 -91
- data/Rakefile +12 -12
- data/lib/media_types.rb +58 -2
- data/lib/media_types/constructable.rb +36 -10
- data/lib/media_types/dsl.rb +110 -29
- data/lib/media_types/dsl/errors.rb +18 -0
- data/lib/media_types/errors.rb +19 -0
- data/lib/media_types/scheme.rb +153 -2
- data/lib/media_types/scheme/errors.rb +66 -0
- data/lib/media_types/scheme/links.rb +15 -0
- data/lib/media_types/scheme/missing_validation.rb +12 -4
- data/lib/media_types/scheme/output_empty_guard.rb +5 -4
- data/lib/media_types/scheme/output_iterator_with_predicate.rb +13 -2
- data/lib/media_types/scheme/output_type_guard.rb +1 -1
- data/lib/media_types/scheme/rules.rb +53 -1
- data/lib/media_types/scheme/rules_exhausted_guard.rb +15 -4
- data/lib/media_types/scheme/validation_options.rb +17 -5
- data/lib/media_types/testing/assertions.rb +20 -0
- data/lib/media_types/validations.rb +15 -5
- data/lib/media_types/version.rb +1 -1
- data/media_types.gemspec +4 -7
- metadata +20 -62
- data/.travis.yml +0 -19
- data/lib/media_types/defaults.rb +0 -31
- data/lib/media_types/integrations.rb +0 -32
- data/lib/media_types/integrations/actionpack.rb +0 -21
- data/lib/media_types/integrations/http.rb +0 -47
- data/lib/media_types/minitest/assert_media_type_format.rb +0 -10
- data/lib/media_types/minitest/assert_media_types_registered.rb +0 -166
- data/lib/media_types/registrar.rb +0 -148
@@ -2,10 +2,49 @@
|
|
2
2
|
|
3
3
|
module MediaTypes
|
4
4
|
class Scheme
|
5
|
+
class ConflictingTypeDefinitionError < ArgumentError; end
|
5
6
|
|
6
7
|
# Base class for all validations errors
|
7
8
|
class ValidationError < ArgumentError; end
|
8
9
|
|
10
|
+
# Raised when trying to register an attribute with a non-string key
|
11
|
+
class KeyTypeError < ArgumentError; end
|
12
|
+
|
13
|
+
# Raised when trying to register a key twice
|
14
|
+
class DuplicateKeyError < ArgumentError; end
|
15
|
+
|
16
|
+
class DuplicateSymbolKeyError < DuplicateKeyError
|
17
|
+
MESSAGE_TEMPLATE = '%<rule_type>s rule with key :%<key>s has already been defined. Please remove one of the two.'
|
18
|
+
|
19
|
+
def initialize(rule_type, key)
|
20
|
+
super(format(MESSAGE_TEMPLATE, rule_type: rule_type, key: key))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class DuplicateStringKeyError < DuplicateKeyError
|
25
|
+
MESSAGE_TEMPLATE = '%<rule_type>s rule with key %<key>s has already been defined. Please remove one of the two.'
|
26
|
+
|
27
|
+
def initialize(rule_type, key)
|
28
|
+
super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class StringOverwritingSymbolError < DuplicateKeyError
|
33
|
+
MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule String key %<key>s while a Symbol with the same name already exists. Please remove one of the two.'
|
34
|
+
|
35
|
+
def initialize(rule_type, key)
|
36
|
+
super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class SymbolOverwritingStringError < DuplicateKeyError
|
41
|
+
MESSAGE_TEMPLATE = 'Trying to add %<rule_type>s rule with Symbol key :%<key>s while a String key with the same name already exists. Please remove one of the two.'
|
42
|
+
|
43
|
+
def initialize(rule_type, key)
|
44
|
+
super(format(MESSAGE_TEMPLATE, { rule_type: rule_type, key: key }))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
9
48
|
# Raised when it did not expect more data, but there was more left
|
10
49
|
class StrictValidationError < ValidationError; end
|
11
50
|
|
@@ -17,5 +56,32 @@ module MediaTypes
|
|
17
56
|
|
18
57
|
# Raised when it expected more data but there wasn't any left
|
19
58
|
class ExhaustedOutputError < ValidationError; end
|
59
|
+
|
60
|
+
# Raised when trying to override a non default rule scheme in the Rules Hash's default object method
|
61
|
+
class OverwritingRuleError < ArgumentError; end
|
62
|
+
|
63
|
+
class DuplicateAnyRuleError < OverwritingRuleError
|
64
|
+
def message
|
65
|
+
"An 'any' rule has already been defined. Please remove one of the two."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class DuplicateNotStrictRuleError < OverwritingRuleError
|
70
|
+
def message
|
71
|
+
"The 'not_strict' rule has already been defined. Please remove one of the two."
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class NotStrictOverwritingAnyError < OverwritingRuleError
|
76
|
+
def message
|
77
|
+
"An 'any' rule has already been defined. Setting 'not_strict' will override that rule. Please remove one of the two."
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class AnyOverwritingNotStrictError < OverwritingRuleError
|
82
|
+
def message
|
83
|
+
"The 'not_strict' rule has already been defined. Setting 'any' will override that rule. Please remove one of the two."
|
84
|
+
end
|
85
|
+
end
|
20
86
|
end
|
21
87
|
end
|
@@ -31,6 +31,21 @@ module MediaTypes
|
|
31
31
|
"[Scheme::Links #{links.keys}]"
|
32
32
|
end
|
33
33
|
|
34
|
+
def run_fixture_validations(expect_symbol_keys, backtrace = [])
|
35
|
+
fixture_errors = @links.flat_map {|key, rule|
|
36
|
+
if rule.is_a?(Scheme)
|
37
|
+
begin
|
38
|
+
rule.run_fixture_validations(expect_symbol_keys, backtrace.dup.append(key))
|
39
|
+
nil
|
40
|
+
rescue AssertionError => e
|
41
|
+
e.fixture_errors
|
42
|
+
end
|
43
|
+
end
|
44
|
+
}.compact
|
45
|
+
|
46
|
+
raise AssertionError.new(fixture_errors) unless fixture_errors.empty?
|
47
|
+
end
|
48
|
+
|
34
49
|
private
|
35
50
|
|
36
51
|
attr_accessor :links
|
@@ -9,18 +9,21 @@ module MediaTypes
|
|
9
9
|
def validate!(_output, options, context:, **_opts)
|
10
10
|
# Check that no unknown keys are present
|
11
11
|
return true unless options.strict
|
12
|
-
raise_strict!(key: context.key, strict_keys: context.rules, backtrace: options.backtrace)
|
12
|
+
raise_strict!(key: context.key, strict_keys: context.rules, backtrace: options.backtrace, found: options.scoped_output)
|
13
13
|
end
|
14
14
|
|
15
|
-
def raise_strict!(key:, backtrace:, strict_keys:)
|
15
|
+
def raise_strict!(key:, backtrace:, strict_keys:, found:)
|
16
16
|
raise StrictValidationError, format(
|
17
17
|
"Unknown key %<key>s in data.\n" \
|
18
18
|
"\tFound at: %<backtrace>s\n" \
|
19
19
|
"\tExpected:\n\n" \
|
20
|
-
|
20
|
+
"%<strict_keys>s\n\n" \
|
21
|
+
"\tBut I Found:\n\n" \
|
22
|
+
'%<found>s',
|
21
23
|
key: key.inspect,
|
22
24
|
backtrace: backtrace.join('->'),
|
23
|
-
strict_keys: strict_keys.
|
25
|
+
strict_keys: keys_to_str(strict_keys.keys),
|
26
|
+
found: (found.respond_to? :keys) ? keys_to_str(found.keys) : found.class.name
|
24
27
|
)
|
25
28
|
end
|
26
29
|
|
@@ -28,6 +31,11 @@ module MediaTypes
|
|
28
31
|
'((raise when strict))'
|
29
32
|
end
|
30
33
|
|
34
|
+
def keys_to_str(keys)
|
35
|
+
converted = keys.map { |k| k.is_a?(Symbol) ? ":#{k}" : "'#{k}'" }
|
36
|
+
"[#{converted.join ', '}]"
|
37
|
+
end
|
38
|
+
|
31
39
|
end
|
32
40
|
end
|
33
41
|
end
|
@@ -21,7 +21,7 @@ module MediaTypes
|
|
21
21
|
def call
|
22
22
|
return unless MediaTypes::Object.new(output).empty?
|
23
23
|
throw(:end, true) if allow_empty?
|
24
|
-
raise_empty!(backtrace: options.backtrace)
|
24
|
+
raise_empty!(backtrace: options.backtrace, found: options.scoped_output)
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -32,11 +32,12 @@ module MediaTypes
|
|
32
32
|
rules.allow_empty? || rules.required.empty?
|
33
33
|
end
|
34
34
|
|
35
|
-
def raise_empty!(backtrace:)
|
35
|
+
def raise_empty!(backtrace:, found:)
|
36
36
|
raise EmptyOutputError, format(
|
37
|
-
'Expected output, got empty at %<backtrace>s. Required are: %<required>s.',
|
37
|
+
'Expected output, got empty at %<backtrace>s. Required are: %<required>s. Found: %<found>s',
|
38
38
|
backtrace: backtrace.join('->'),
|
39
|
-
required: rules.required.keys
|
39
|
+
required: rules.required.keys,
|
40
|
+
found: (found.is_a? Hash) ? found.keys : found.class.name,
|
40
41
|
)
|
41
42
|
end
|
42
43
|
end
|
@@ -26,7 +26,11 @@ module MediaTypes
|
|
26
26
|
return iterate_hash { |*args, **opts| yield(*args, **opts) }
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
if array?
|
30
|
+
return iterate { |*args, **opts| yield(*args, **opts) }
|
31
|
+
end
|
32
|
+
|
33
|
+
raise "Internal consistency error, unexpected: #{enumerable.class}"
|
30
34
|
end
|
31
35
|
|
32
36
|
private
|
@@ -37,6 +41,10 @@ module MediaTypes
|
|
37
41
|
enumerable.is_a?(::Hash) || enumerable.respond_to?(:key)
|
38
42
|
end
|
39
43
|
|
44
|
+
def array?
|
45
|
+
enumerable.is_a?(::Array)
|
46
|
+
end
|
47
|
+
|
40
48
|
def iterate_hash
|
41
49
|
context = EnumerationContext.new(rules: rules)
|
42
50
|
|
@@ -46,7 +54,10 @@ module MediaTypes
|
|
46
54
|
end
|
47
55
|
|
48
56
|
def iterate(&block)
|
49
|
-
|
57
|
+
hash_rule = Rules.new(allow_empty: false, expected_type: ::Hash)
|
58
|
+
|
59
|
+
enumerable.each_with_index.all? do |array_like_element, i|
|
60
|
+
OutputTypeGuard.call(array_like_element, options.trace(1), rules: hash_rule)
|
50
61
|
OutputIteratorWithPredicate.call(array_like_element, options.trace(i), rules: rules, &block)
|
51
62
|
end
|
52
63
|
end
|
@@ -28,7 +28,7 @@ module MediaTypes
|
|
28
28
|
|
29
29
|
def raise_type_error!(type:, backtrace:)
|
30
30
|
raise OutputTypeMismatch, format(
|
31
|
-
'Expected
|
31
|
+
'Expected %<expected>s, got %<actual>s at %<backtrace>s',
|
32
32
|
expected: expected_type,
|
33
33
|
actual: type,
|
34
34
|
backtrace: backtrace.join('->')
|
@@ -14,6 +14,7 @@ module MediaTypes
|
|
14
14
|
self.allow_empty = allow_empty
|
15
15
|
self.expected_type = expected_type
|
16
16
|
self.optional_keys = []
|
17
|
+
self.original_key_type = {}
|
17
18
|
|
18
19
|
self.default = MissingValidation.new
|
19
20
|
end
|
@@ -27,13 +28,44 @@ module MediaTypes
|
|
27
28
|
end
|
28
29
|
|
29
30
|
def add(key, val, optional: false)
|
31
|
+
validate_input(key, val)
|
32
|
+
|
30
33
|
normalized_key = normalize_key(key)
|
31
34
|
__getobj__[normalized_key] = val
|
32
35
|
optional_keys << normalized_key if optional
|
36
|
+
original_key_type[normalized_key] = key.class
|
33
37
|
|
34
38
|
self
|
35
39
|
end
|
36
40
|
|
41
|
+
def validate_input(key, val)
|
42
|
+
raise KeyTypeError, "Unexpected key type #{key.class.name}, please use either a symbol or string." unless key.is_a?(String) || key.is_a?(Symbol)
|
43
|
+
|
44
|
+
validate_key_name(key, val)
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_key_name(key, val)
|
48
|
+
return unless has_key?(key)
|
49
|
+
|
50
|
+
if key.is_a?(Symbol)
|
51
|
+
duplicate_symbol_key_name(key, val)
|
52
|
+
else
|
53
|
+
duplicate_string_key_name(key, val)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def duplicate_symbol_key_name(key, val)
|
58
|
+
raise DuplicateSymbolKeyError.new(val.class.name.split('::').last, key) if get_original_key_type(key) == Symbol
|
59
|
+
|
60
|
+
raise SymbolOverwritingStringError.new(val.class.name.split('::').last, key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def duplicate_string_key_name(key, val)
|
64
|
+
raise DuplicateStringKeyError.new(val.class.name.split('::').last, key) if get_original_key_type(key) == String
|
65
|
+
|
66
|
+
raise StringOverwritingSymbolError.new(val.class.name.split('::').last, key)
|
67
|
+
end
|
68
|
+
|
37
69
|
def []=(key, val)
|
38
70
|
add(key, val, optional: false)
|
39
71
|
end
|
@@ -105,12 +137,32 @@ module MediaTypes
|
|
105
137
|
].join(': ')
|
106
138
|
end
|
107
139
|
|
140
|
+
def has_key?(key)
|
141
|
+
__getobj__.key?(normalize_key(key))
|
142
|
+
end
|
143
|
+
|
144
|
+
def get_original_key_type(key)
|
145
|
+
raise format('Key %<key>s does not exist', key: key) unless has_key?(key)
|
146
|
+
|
147
|
+
original_key_type[normalize_key(key)]
|
148
|
+
end
|
149
|
+
|
150
|
+
def default=(input_default)
|
151
|
+
unless default.nil?
|
152
|
+
raise DuplicateAnyRuleError if !(default.is_a?(MissingValidation) || default.is_a?(NotStrict)) && !(input_default.is_a?(MissingValidation) || input_default.is_a?(NotStrict))
|
153
|
+
raise DuplicateNotStrictRuleError if default.is_a?(NotStrict) && input_default.is_a?(NotStrict)
|
154
|
+
raise NotStrictOverwritingAnyError if !(default.is_a?(MissingValidation) || default.is_a?(NotStrict)) && input_default.is_a?(NotStrict)
|
155
|
+
raise AnyOverwritingNotStrictError if default.is_a?(NotStrict) && !(input_default.is_a?(MissingValidation) || input_default.is_a?(NotStrict))
|
156
|
+
end
|
157
|
+
super(input_default)
|
158
|
+
end
|
159
|
+
|
108
160
|
alias get []
|
109
161
|
alias remove delete
|
110
162
|
|
111
163
|
private
|
112
164
|
|
113
|
-
attr_accessor :allow_empty, :optional_keys
|
165
|
+
attr_accessor :allow_empty, :optional_keys, :original_key_type
|
114
166
|
attr_writer :expected_type
|
115
167
|
|
116
168
|
def normalize_key(key)
|
@@ -31,11 +31,21 @@ module MediaTypes
|
|
31
31
|
result = iterate(->(key) { required_rules.remove(key) })
|
32
32
|
return result if required_rules.empty?
|
33
33
|
|
34
|
-
raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace)
|
34
|
+
raise_exhausted!(missing_keys: required_rules.keys, backtrace: options.backtrace, found: output)
|
35
35
|
end
|
36
36
|
|
37
37
|
def iterate(mark)
|
38
38
|
OutputIteratorWithPredicate.call(output, options, rules: rules) do |key, value, options:, context:|
|
39
|
+
unless key.class == options.expected_key_type || rules.get(key).class == NotStrict
|
40
|
+
raise ValidationError,
|
41
|
+
format(
|
42
|
+
'Expected key as %<type>s, got %<actual>s at [%<backtrace>s]',
|
43
|
+
type: options.expected_key_type,
|
44
|
+
actual: key.class,
|
45
|
+
backtrace: options.trace(key).backtrace.join('->')
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
39
49
|
mark.call(key)
|
40
50
|
|
41
51
|
rules.get(key).validate!(
|
@@ -50,11 +60,12 @@ module MediaTypes
|
|
50
60
|
|
51
61
|
attr_accessor :rules, :options, :output
|
52
62
|
|
53
|
-
def raise_exhausted!(missing_keys:, backtrace:)
|
63
|
+
def raise_exhausted!(missing_keys:, backtrace:, found:)
|
54
64
|
raise ExhaustedOutputError, format(
|
55
|
-
'Missing keys in output: %<missing_keys>s at [%<backtrace>s]',
|
65
|
+
'Missing keys in output: %<missing_keys>s at [%<backtrace>s]. I did find: %<found>s',
|
56
66
|
missing_keys: missing_keys,
|
57
|
-
backtrace: backtrace.join('->')
|
67
|
+
backtrace: backtrace.join('->'),
|
68
|
+
found: (found.is_a? Hash) ? found.keys : found.class.name,
|
58
69
|
)
|
59
70
|
end
|
60
71
|
end
|
@@ -3,20 +3,32 @@
|
|
3
3
|
module MediaTypes
|
4
4
|
class Scheme
|
5
5
|
class ValidationOptions
|
6
|
-
attr_accessor :exhaustive, :strict, :backtrace
|
6
|
+
attr_accessor :exhaustive, :strict, :backtrace, :context, :expected_key_type
|
7
7
|
|
8
|
-
def initialize(exhaustive: true, strict: true, backtrace: [])
|
8
|
+
def initialize(context = {}, exhaustive: true, strict: true, backtrace: [], expected_key_type:)
|
9
9
|
self.exhaustive = exhaustive
|
10
10
|
self.strict = strict
|
11
11
|
self.backtrace = backtrace
|
12
|
+
self.context = context
|
13
|
+
self.expected_key_type = expected_key_type
|
12
14
|
end
|
13
15
|
|
14
16
|
def inspect
|
15
|
-
"backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, exhaustive: #{exhaustive}"
|
17
|
+
"backtrack: #{backtrace.inspect}, strict: #{strict.inspect}, exhaustive: #{exhaustive}, current_obj: #{scoped_output.to_json}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def scoped_output
|
21
|
+
current = context
|
22
|
+
|
23
|
+
backtrace.drop(1).first([0, backtrace.size - 2].max).each do |e|
|
24
|
+
current = current[e] unless current.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
current
|
16
28
|
end
|
17
29
|
|
18
30
|
def with_backtrace(backtrace)
|
19
|
-
ValidationOptions.new(exhaustive: exhaustive, strict: strict, backtrace: backtrace)
|
31
|
+
ValidationOptions.new(context, exhaustive: exhaustive, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
|
20
32
|
end
|
21
33
|
|
22
34
|
def trace(*traces)
|
@@ -24,7 +36,7 @@ module MediaTypes
|
|
24
36
|
end
|
25
37
|
|
26
38
|
def exhaustive!
|
27
|
-
ValidationOptions.new(exhaustive: true, strict: strict, backtrace: backtrace)
|
39
|
+
ValidationOptions.new(context, exhaustive: true, strict: strict, backtrace: backtrace, expected_key_type: expected_key_type)
|
28
40
|
end
|
29
41
|
end
|
30
42
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MediaTypes
|
4
|
+
module Testing
|
5
|
+
module Assertions
|
6
|
+
def assert_media_type_format(media_type, output, **opts)
|
7
|
+
return pass unless media_type.validatable?
|
8
|
+
|
9
|
+
assert media_type.validate!(output, **opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def assert_mediatype(mediatype)
|
13
|
+
mediatype.assert_sane!
|
14
|
+
assert mediatype.media_type_validations.scheme.asserted_sane?
|
15
|
+
rescue MediaTypes::AssertionError => e
|
16
|
+
flunk e.message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -12,6 +12,8 @@ module MediaTypes
|
|
12
12
|
#
|
13
13
|
class Validations
|
14
14
|
|
15
|
+
attr_reader :scheme
|
16
|
+
|
15
17
|
##
|
16
18
|
# Creates a new stack of validations
|
17
19
|
#
|
@@ -25,7 +27,7 @@ module MediaTypes
|
|
25
27
|
#
|
26
28
|
def initialize(media_type, registry = {}, scheme = Scheme.new, &block)
|
27
29
|
self.media_type = media_type
|
28
|
-
self.registry = registry.merge!(media_type.
|
30
|
+
self.registry = registry.merge!(media_type.as_key => scheme)
|
29
31
|
self.scheme = scheme
|
30
32
|
|
31
33
|
instance_exec(&block) if block_given?
|
@@ -39,14 +41,17 @@ module MediaTypes
|
|
39
41
|
# @return [Scheme] the scheme for the given +media_type+
|
40
42
|
#
|
41
43
|
def find(media_type, default = -> { Scheme.new(allow_empty: true) { not_strict } })
|
42
|
-
registry.fetch(
|
44
|
+
registry.fetch(media_type.as_key) do
|
43
45
|
default.call
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
47
|
-
def method_missing(method_name, *arguments, &block)
|
49
|
+
def method_missing(method_name, *arguments, **kwargs, &block)
|
48
50
|
if scheme.respond_to?(method_name)
|
49
|
-
|
51
|
+
media_type.__getobj__.media_type_combinations ||= Set.new
|
52
|
+
media_type.__getobj__.media_type_combinations.add(media_type.as_key)
|
53
|
+
|
54
|
+
return scheme.send(method_name, *arguments, **kwargs, &block)
|
50
55
|
end
|
51
56
|
|
52
57
|
super
|
@@ -58,7 +63,8 @@ module MediaTypes
|
|
58
63
|
|
59
64
|
private
|
60
65
|
|
61
|
-
attr_accessor :media_type, :registry
|
66
|
+
attr_accessor :media_type, :registry
|
67
|
+
attr_writer :scheme
|
62
68
|
|
63
69
|
##
|
64
70
|
# Switches the inner block to a specific version
|
@@ -77,5 +83,9 @@ module MediaTypes
|
|
77
83
|
def view(view, &block)
|
78
84
|
Validations.new(media_type.view(view), registry, &block)
|
79
85
|
end
|
86
|
+
|
87
|
+
def suffix(name)
|
88
|
+
scheme.type_attributes[:suffix] = name
|
89
|
+
end
|
80
90
|
end
|
81
91
|
end
|