media_types 0.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 +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +29 -0
- data/.travis.yml +20 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +42 -0
- data/README.md +150 -0
- data/Rakefile +10 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/media_types.rb +65 -0
- data/lib/media_types/base.rb +193 -0
- data/lib/media_types/base/collector.rb +44 -0
- data/lib/media_types/constructable_mime_type.rb +126 -0
- data/lib/media_types/minitest/assert_media_type_format.rb +20 -0
- data/lib/media_types/minitest/assert_media_types_registered.rb +143 -0
- data/lib/media_types/scheme.rb +238 -0
- data/lib/media_types/scheme/allow_nil.rb +18 -0
- data/lib/media_types/scheme/attribute.rb +34 -0
- data/lib/media_types/scheme/links.rb +32 -0
- data/lib/media_types/scheme/missing_validation.rb +24 -0
- data/lib/media_types/scheme/not_strict.rb +11 -0
- data/lib/media_types/version.rb +3 -0
- data/media_types.gemspec +32 -0
- metadata +151 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module MediaTypes
|
6
|
+
class Base
|
7
|
+
class Collector < SimpleDelegator
|
8
|
+
|
9
|
+
def index(*args, **options)
|
10
|
+
view(INDEX_VIEW, *args, **options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def create(*args, **options, &block)
|
14
|
+
view(CREATE_VIEW, *args, **options, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def collection(*args, **options)
|
18
|
+
view(COLLECTION_VIEW, *args, **options)
|
19
|
+
end
|
20
|
+
|
21
|
+
def view(view, *args, **options)
|
22
|
+
register_type(*args, **options.merge(view: view))
|
23
|
+
end
|
24
|
+
|
25
|
+
def version(*args, **options, &block)
|
26
|
+
register_version(*args, **options, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# This is similar to having a decorator / interface only exposing certain methods. In this private section
|
32
|
+
# the +register_type+ and +register_version+ methods are made available
|
33
|
+
#
|
34
|
+
|
35
|
+
def register_type(*args, **options, &block)
|
36
|
+
__getobj__.instance_exec { register_type(*args, options, &block) }
|
37
|
+
end
|
38
|
+
|
39
|
+
def register_version(*args, **options, &block)
|
40
|
+
__getobj__.instance_exec { register_version(*args, options, &block) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
require 'singleton'
|
5
|
+
|
6
|
+
module MediaTypes
|
7
|
+
class ConstructableMimeType < SimpleDelegator
|
8
|
+
|
9
|
+
def initialize(klazz, **opts)
|
10
|
+
super klazz
|
11
|
+
self.opts = opts
|
12
|
+
end
|
13
|
+
|
14
|
+
def version(version = NO_ARG)
|
15
|
+
return opts[:version] if version == NO_ARG
|
16
|
+
ConstructableMimeType.new(__getobj__, **with(version: version))
|
17
|
+
end
|
18
|
+
|
19
|
+
def view(view = NO_ARG)
|
20
|
+
return opts[:view] if view == NO_ARG
|
21
|
+
ConstructableMimeType.new(__getobj__, **with(view: view))
|
22
|
+
end
|
23
|
+
|
24
|
+
def suffix(suffix = NO_ARG)
|
25
|
+
return opts[:suffix] if suffix == NO_ARG
|
26
|
+
ConstructableMimeType.new(__getobj__, **with(suffix: suffix))
|
27
|
+
end
|
28
|
+
|
29
|
+
def collection
|
30
|
+
view(COLLECTION_VIEW)
|
31
|
+
end
|
32
|
+
|
33
|
+
def collection?
|
34
|
+
opts[:view] == COLLECTION_VIEW
|
35
|
+
end
|
36
|
+
|
37
|
+
def create
|
38
|
+
view(CREATE_VIEW)
|
39
|
+
end
|
40
|
+
|
41
|
+
def create?
|
42
|
+
opts[:view] == CREATE_VIEW
|
43
|
+
end
|
44
|
+
|
45
|
+
def index
|
46
|
+
view(INDEX_VIEW)
|
47
|
+
end
|
48
|
+
|
49
|
+
def index?
|
50
|
+
opts[:view] == INDEX_VIEW
|
51
|
+
end
|
52
|
+
|
53
|
+
def ===(other)
|
54
|
+
to_str.send(:===, other)
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
to_str.send(:==, other)
|
59
|
+
end
|
60
|
+
|
61
|
+
def +(other)
|
62
|
+
to_str + other
|
63
|
+
end
|
64
|
+
|
65
|
+
def split(pattern = nil, *limit)
|
66
|
+
to_str.split(pattern, *limit)
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash
|
70
|
+
to_str.hash
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_str(qualifier = nil)
|
74
|
+
# TODO: remove warning by slicing out these arguments if they don't appear in the format
|
75
|
+
qualified(qualifier, @to_str ||= format(
|
76
|
+
opts.fetch(:format),
|
77
|
+
version: opts.fetch(:version),
|
78
|
+
suffix: opts.fetch(:suffix) { :json },
|
79
|
+
type: opts.fetch(:type),
|
80
|
+
view: format_view(opts[:view])
|
81
|
+
))
|
82
|
+
end
|
83
|
+
|
84
|
+
def valid?(output, **validation_opts)
|
85
|
+
__getobj__.valid?(
|
86
|
+
output,
|
87
|
+
version: opts[:version],
|
88
|
+
**validation_opts
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate!(output, **validation_opts)
|
93
|
+
__getobj__.validate!(
|
94
|
+
output,
|
95
|
+
version: opts[:version],
|
96
|
+
**validation_opts
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
alias inspect to_str
|
101
|
+
alias to_s to_str
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
class NoArgumentGiven
|
106
|
+
include Singleton
|
107
|
+
end
|
108
|
+
|
109
|
+
NO_ARG = NoArgumentGiven.instance
|
110
|
+
|
111
|
+
attr_accessor :opts
|
112
|
+
|
113
|
+
def with(more_opts)
|
114
|
+
Hash(opts).merge(more_opts).dup
|
115
|
+
end
|
116
|
+
|
117
|
+
def qualified(qualifier, media_type)
|
118
|
+
return media_type unless qualifier
|
119
|
+
format('%<media_type>s; q=%<q>s', media_type: media_type, q: qualifier)
|
120
|
+
end
|
121
|
+
|
122
|
+
def format_view(view)
|
123
|
+
MediaTypes::Object.new(view).present? && ".#{view}" || ''
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Assertions
|
4
|
+
module MediaTypes
|
5
|
+
def assert_media_type_format(media_type, output, **opts)
|
6
|
+
if media_type.collection?
|
7
|
+
output[:_embedded].each do |embedded|
|
8
|
+
assert_media_type_format(media_type.view(nil), embedded, **opts)
|
9
|
+
end
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
if media_type.index?
|
14
|
+
return output[:_index] # TODO: sub_schema the "self" link
|
15
|
+
end
|
16
|
+
|
17
|
+
assert media_type.validate!(output, **opts)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/mock'
|
4
|
+
|
5
|
+
module MediaTypes
|
6
|
+
module Assertions
|
7
|
+
class << self
|
8
|
+
def block_exec_instance(instance, &block)
|
9
|
+
case block.arity
|
10
|
+
when 1, -1
|
11
|
+
instance.instance_exec(instance, &block)
|
12
|
+
else
|
13
|
+
instance.instance_exec(&block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_media_types_registered(media_type, &block)
|
19
|
+
collector = Collector.new(media_type)
|
20
|
+
Assertions.block_exec_instance(collector, &block)
|
21
|
+
assert_registered_types(media_type, collector.prepare_verify)
|
22
|
+
end
|
23
|
+
|
24
|
+
class Collector
|
25
|
+
def initialize(media_type)
|
26
|
+
self.media_type = media_type
|
27
|
+
self.registers = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def mime_type(mime_type, symbol:, synonyms: [])
|
31
|
+
registers[mime_type] = { symbol: symbol, synonyms: synonyms }
|
32
|
+
end
|
33
|
+
|
34
|
+
def formatted_mime_type(mime_type_format, &block)
|
35
|
+
collector = FormattedCollector.new(mime_type_format, {})
|
36
|
+
Assertions.block_exec_instance(collector, &block)
|
37
|
+
registers.merge!(collector.to_h)
|
38
|
+
end
|
39
|
+
|
40
|
+
def prepare_verify
|
41
|
+
expected_types_hash = registers.dup
|
42
|
+
expected_types_hash.each do |key, value|
|
43
|
+
expected_types_hash[key] = [value[:symbol], Array(value[:synonyms])]
|
44
|
+
end
|
45
|
+
|
46
|
+
expected_types_hash
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_accessor :media_type, :registers
|
52
|
+
end
|
53
|
+
|
54
|
+
class FormattedCollector
|
55
|
+
def initialize(format, args = {})
|
56
|
+
self.mime_type_format = format
|
57
|
+
self.format_args = args
|
58
|
+
self.registers = {}
|
59
|
+
end
|
60
|
+
|
61
|
+
def version(version, **opts, &block)
|
62
|
+
new_format_args = format_args.merge(version: version)
|
63
|
+
register(new_format_args, **opts, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def view(view, **opts, &block)
|
67
|
+
new_format_args = format_args.merge(view: view)
|
68
|
+
register(new_format_args, **opts, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def create(**opts, &block)
|
72
|
+
view(Base::CREATE_VIEW, **opts, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def collection(**opts, &block)
|
76
|
+
view(Base::COLLECTION_VIEW, **opts, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def index(**opts, &block)
|
80
|
+
view(Base::INDEX_VIEW, **opts, &block)
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_h
|
84
|
+
Hash(registers)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
attr_accessor :mime_type_format, :registers, :format_args
|
90
|
+
|
91
|
+
def register(new_format_args, symbol: nil, synonyms: [], &block)
|
92
|
+
if block_given?
|
93
|
+
collector = FormattedCollector.new(mime_type_format, new_format_args)
|
94
|
+
Assertions.block_exec_instance(collector, &block)
|
95
|
+
registers.merge!(collector.to_h)
|
96
|
+
else
|
97
|
+
formatted_mime_type_format = format(mime_type_format, **new_format_args)
|
98
|
+
registers[formatted_mime_type_format] = { symbol: symbol, synonyms: synonyms }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
104
|
+
def assert_registered_types(media_type, expected_types_hash)
|
105
|
+
mock = Minitest::Mock.new
|
106
|
+
uncalled = expected_types_hash.dup
|
107
|
+
|
108
|
+
uncalled.length.times do
|
109
|
+
mock.expect(:call, nil) do |arguments|
|
110
|
+
type = arguments.fetch(:mime_type)
|
111
|
+
symbol = arguments.fetch(:symbol)
|
112
|
+
synonyms = arguments.fetch(:synonyms)
|
113
|
+
|
114
|
+
options = uncalled.delete(type)
|
115
|
+
options && options == [symbol, synonyms] || raise(
|
116
|
+
MockExpectationError,
|
117
|
+
format(
|
118
|
+
'Called with [type: %<type>s, symbol: %<symbol>s, synonyms: %<synonyms>s]' + "\n"\
|
119
|
+
'Resolved options: [%<resolved>s]' + "\n"\
|
120
|
+
'Uncalled options: [%<uncalled>s]',
|
121
|
+
type: type,
|
122
|
+
symbol: symbol,
|
123
|
+
synonyms: synonyms,
|
124
|
+
resolved: options,
|
125
|
+
uncalled: uncalled
|
126
|
+
)
|
127
|
+
)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
MediaTypes.stub(:register, mock) do
|
132
|
+
if block_given?
|
133
|
+
yield media_type
|
134
|
+
else
|
135
|
+
media_type.register.flatten
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
assert_mock mock
|
140
|
+
end
|
141
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'media_types/scheme/allow_nil'
|
4
|
+
require 'media_types/scheme/attribute'
|
5
|
+
require 'media_types/scheme/links'
|
6
|
+
require 'media_types/scheme/missing_validation'
|
7
|
+
require 'media_types/scheme/not_strict'
|
8
|
+
|
9
|
+
module MediaTypes
|
10
|
+
class ValidationError < ArgumentError
|
11
|
+
end
|
12
|
+
|
13
|
+
class ExhaustedOutputError < ValidationError
|
14
|
+
end
|
15
|
+
|
16
|
+
class StrictValidationError < ValidationError
|
17
|
+
end
|
18
|
+
|
19
|
+
class EmptyOutputError < ValidationError
|
20
|
+
end
|
21
|
+
|
22
|
+
##
|
23
|
+
# Media Type Schemes can validate content to a media type, by itself.
|
24
|
+
#
|
25
|
+
class Scheme
|
26
|
+
def initialize(allow_empty: false)
|
27
|
+
self.validations = {}
|
28
|
+
self.allow_empty = allow_empty
|
29
|
+
|
30
|
+
validations.default = MissingValidation.new
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Checks if the +output+ is valid
|
35
|
+
#
|
36
|
+
# @param [#each] output
|
37
|
+
# @param [Hash] opts
|
38
|
+
# @option exhaustive [Boolean] opts
|
39
|
+
# @option strict [Boolean] opts
|
40
|
+
#
|
41
|
+
# @return [Boolean] true if valid, false otherwise
|
42
|
+
#
|
43
|
+
def valid?(output, **opts)
|
44
|
+
validate(output, **opts)
|
45
|
+
rescue ExhaustedOutputError
|
46
|
+
!opts.fetch(:exhaustive) { true }
|
47
|
+
rescue ValidationError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
class ValidationOptions
|
52
|
+
attr_accessor :exhaustive, :strict, :backtrace
|
53
|
+
|
54
|
+
def initialize(exhaustive: true, strict: true, backtrace: [])
|
55
|
+
self.exhaustive = exhaustive
|
56
|
+
self.strict = strict
|
57
|
+
self.backtrace = backtrace
|
58
|
+
end
|
59
|
+
|
60
|
+
def with_backtrace(backtrace)
|
61
|
+
ValidationOptions.new(exhaustive: exhaustive, strict: strict, backtrace: backtrace)
|
62
|
+
end
|
63
|
+
|
64
|
+
def trace(*traces)
|
65
|
+
with_backtrace(backtrace.dup.concat(traces))
|
66
|
+
end
|
67
|
+
|
68
|
+
def exhaustive!
|
69
|
+
ValidationOptions.new(exhaustive: true, strict: strict, backtrace: backtrace)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
##
|
74
|
+
# Validates the +output+ and raises on certain validation errors
|
75
|
+
#
|
76
|
+
# @param [#each] output output to validate
|
77
|
+
# @option opts [Boolean] exhaustive if true, the entire schema needs to be consumed
|
78
|
+
# @option opts [Boolean] strict if true, no extra keys may be present in +output+
|
79
|
+
# @option opts[Array<String>] backtrace the current backtrace for error messages
|
80
|
+
#
|
81
|
+
# @raise ExhaustedOutputError
|
82
|
+
# @raise StrictValidationError
|
83
|
+
# @raise EmptyOutputError
|
84
|
+
# @raise ValidationError
|
85
|
+
#
|
86
|
+
# @return [TrueClass]
|
87
|
+
#
|
88
|
+
def validate(output, options = nil, **opts)
|
89
|
+
options ||= ValidationOptions.new(**opts)
|
90
|
+
|
91
|
+
catch(:end) do
|
92
|
+
validate!(output, options, context: nil)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate!(output, call_options, **_opts)
|
97
|
+
empty_guard!(output, call_options)
|
98
|
+
|
99
|
+
exhaustive_guard!(validations.keys, call_options) do |mark|
|
100
|
+
all?(output, call_options) do |key, value, options:, context:|
|
101
|
+
mark.call(key)
|
102
|
+
|
103
|
+
validations[key].validate!(
|
104
|
+
value,
|
105
|
+
options.trace(key),
|
106
|
+
context: context
|
107
|
+
)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# Adds an attribute to the schema
|
114
|
+
#
|
115
|
+
# @param key [Symbol] the attribute name
|
116
|
+
# @param type [Class, #===] The type of the value, can be anything that responds to #===
|
117
|
+
# @param opts [Hash] options
|
118
|
+
#
|
119
|
+
# @example Add an attribute named foo, expecting a string
|
120
|
+
#
|
121
|
+
# class MyMedia < Base
|
122
|
+
# current_schema do
|
123
|
+
# attribute :foo, String
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
def attribute(key, type = String, **opts, &block)
|
128
|
+
validations[key] = Attribute.new(type, **opts, &block)
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
# Allow for any key.
|
133
|
+
# The +block+ defines the Schema for each value.
|
134
|
+
#
|
135
|
+
# @param [Boolean] allow_empty if true, empty (no key/value present) is allowed
|
136
|
+
#
|
137
|
+
def any(allow_empty: false, &block)
|
138
|
+
scheme = Scheme.new(allow_empty: allow_empty)
|
139
|
+
scheme.instance_exec(&block)
|
140
|
+
|
141
|
+
validations.default = scheme
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Allow for extra keys in the schema/collection
|
146
|
+
# even when passing strict: true to #validate!
|
147
|
+
#
|
148
|
+
def not_strict
|
149
|
+
validations.default = NotStrict.new
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
# Expect a collection such as an array or hash.
|
154
|
+
# The +block+ defines the Schema for each item in that collection.
|
155
|
+
#
|
156
|
+
# @param [Symbol] key
|
157
|
+
# @param [Boolean] allow_empty, if true accepts 0 items in an array / hash
|
158
|
+
#
|
159
|
+
def collection(key, allow_empty: false, &block)
|
160
|
+
scheme = Scheme.new(allow_empty: allow_empty)
|
161
|
+
scheme.instance_exec(&block)
|
162
|
+
|
163
|
+
validations[key] = scheme
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Expect a link
|
168
|
+
#
|
169
|
+
def link(*args, **opts, &block)
|
170
|
+
validations.fetch(:_links) do
|
171
|
+
Links.new.tap do |links|
|
172
|
+
validations[:_links] = links
|
173
|
+
end
|
174
|
+
end.link(*args, **opts, &block)
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
attr_accessor :validations, :allow_empty
|
180
|
+
|
181
|
+
def empty_guard!(output, options)
|
182
|
+
return unless output.nil? || output.empty?
|
183
|
+
throw(:end, true) if allow_empty
|
184
|
+
raise_empty!(backtrace: options.backtrace)
|
185
|
+
end
|
186
|
+
|
187
|
+
class EnumerationContext
|
188
|
+
def initialize(validations:)
|
189
|
+
self.validations = validations
|
190
|
+
end
|
191
|
+
|
192
|
+
def enumerate(val)
|
193
|
+
self.key = val
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
attr_accessor :validations, :key
|
198
|
+
end
|
199
|
+
|
200
|
+
def all?(enumerable, options, &block)
|
201
|
+
context = EnumerationContext.new(validations: validations)
|
202
|
+
|
203
|
+
if enumerable.is_a?(Hash) || enumerable.respond_to?(:key)
|
204
|
+
return enumerable.all? do |key, value|
|
205
|
+
yield key, value, options: options, context: context.enumerate(key)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
enumerable.each_with_index.all? do |array_like_element, i|
|
210
|
+
all?(array_like_element, options.trace(i), &block)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def raise_empty!(backtrace:)
|
215
|
+
raise EmptyOutputError, format('Expected output, got empty at %<backtrace>s', backtrace: backtrace.join('->'))
|
216
|
+
end
|
217
|
+
|
218
|
+
def raise_exhausted!(backtrace:, missing_keys:)
|
219
|
+
raise ExhaustedOutputError, format(
|
220
|
+
'Missing keys in output: %<missing_keys>s at [%<backtrace>s]',
|
221
|
+
missing_keys: missing_keys,
|
222
|
+
backtrace: backtrace.join('->')
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
def exhaustive_guard!(keys, options)
|
227
|
+
unless options.exhaustive
|
228
|
+
return yield(->(_) {})
|
229
|
+
end
|
230
|
+
|
231
|
+
exhaustive_keys = keys.dup
|
232
|
+
result = yield ->(key) { exhaustive_keys.delete(key) }
|
233
|
+
return result if exhaustive_keys.empty?
|
234
|
+
|
235
|
+
raise_exhausted!(missing_keys: exhaustive_keys, backtrace: options.backtrace)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|