media_types-serialization 0.8.1 → 1.0.1
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/.github/workflows/ci.yml +10 -1
- data/.gitignore +12 -12
- data/.idea/.rakeTasks +5 -5
- data/.idea/inspectionProfiles/Project_Default.xml +5 -5
- data/.idea/runConfigurations/test.xml +19 -19
- data/CHANGELOG.md +18 -0
- data/CODE_OF_CONDUCT.md +74 -74
- data/Gemfile +4 -4
- data/Gemfile.lock +58 -61
- data/LICENSE.txt +21 -21
- data/README.md +640 -173
- data/Rakefile +10 -10
- data/bin/console +14 -14
- data/bin/setup +8 -8
- data/lib/media_types/problem.rb +64 -0
- data/lib/media_types/serialization.rb +431 -172
- data/lib/media_types/serialization/base.rb +111 -91
- data/lib/media_types/serialization/error.rb +178 -0
- data/lib/media_types/serialization/fake_validator.rb +52 -0
- data/lib/media_types/serialization/serialization_dsl.rb +117 -0
- data/lib/media_types/serialization/serialization_registration.rb +235 -0
- data/lib/media_types/serialization/serializers/api_viewer.rb +133 -0
- data/lib/media_types/serialization/serializers/common_css.rb +168 -0
- data/lib/media_types/serialization/serializers/endpoint_description_serializer.rb +80 -0
- data/lib/media_types/serialization/serializers/fallback_not_acceptable_serializer.rb +85 -0
- data/lib/media_types/serialization/serializers/fallback_unsupported_media_type_serializer.rb +58 -0
- data/lib/media_types/serialization/serializers/input_validation_error_serializer.rb +89 -0
- data/lib/media_types/serialization/serializers/problem_serializer.rb +87 -0
- data/lib/media_types/serialization/version.rb +1 -1
- data/media_types-serialization.gemspec +50 -50
- metadata +40 -43
- data/.travis.yml +0 -17
- data/lib/generators/media_types/serialization/api_viewer/api_viewer_generator.rb +0 -25
- data/lib/generators/media_types/serialization/api_viewer/templates/api_viewer.html.erb +0 -98
- data/lib/generators/media_types/serialization/api_viewer/templates/initializer.rb +0 -33
- data/lib/generators/media_types/serialization/api_viewer/templates/template_controller.rb +0 -23
- data/lib/media_types/serialization/media_type/register.rb +0 -4
- data/lib/media_types/serialization/migrations_command.rb +0 -38
- data/lib/media_types/serialization/migrations_support.rb +0 -50
- data/lib/media_types/serialization/mime_type_support.rb +0 -64
- data/lib/media_types/serialization/no_content_type_given.rb +0 -11
- data/lib/media_types/serialization/no_media_type_serializers.rb +0 -11
- data/lib/media_types/serialization/no_serializer_for_content_type.rb +0 -15
- data/lib/media_types/serialization/renderer.rb +0 -41
- data/lib/media_types/serialization/renderer/register.rb +0 -4
- data/lib/media_types/serialization/wrapper.rb +0 -13
- data/lib/media_types/serialization/wrapper/html_wrapper.rb +0 -45
- data/lib/media_types/serialization/wrapper/media_collection_wrapper.rb +0 -61
- data/lib/media_types/serialization/wrapper/media_index_wrapper.rb +0 -61
- data/lib/media_types/serialization/wrapper/media_object_wrapper.rb +0 -55
- data/lib/media_types/serialization/wrapper_support.rb +0 -38
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'delegate'
|
4
|
+
|
5
|
+
module MediaTypes
|
6
|
+
module Serialization
|
7
|
+
# Provides the serialization convenience methods
|
8
|
+
class SerializationDSL < SimpleDelegator
|
9
|
+
def initialize(serializer, links = [], vary = ['Accept'], value = {}, context: nil)
|
10
|
+
self.serialization_dsl_result = value
|
11
|
+
@serialization_links = links
|
12
|
+
@serialization_context = context
|
13
|
+
@serialization_vary = vary
|
14
|
+
super(serializer)
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_accessor :serialization_dsl_result
|
18
|
+
|
19
|
+
def attribute(key, value = {}, &block)
|
20
|
+
unless block.nil?
|
21
|
+
subcontext = SerializationDSL.new(__getobj__, @serialization_links, @serialization_vary, value, context: @serialization_context)
|
22
|
+
value = subcontext.instance_exec(&block)
|
23
|
+
end
|
24
|
+
|
25
|
+
serialization_dsl_result[key] = value
|
26
|
+
|
27
|
+
serialization_dsl_result
|
28
|
+
end
|
29
|
+
|
30
|
+
def link(rel, href:, emit_header: true, **attributes)
|
31
|
+
serialization_dsl_result[:_links] = {} unless serialization_dsl_result.has_key? :_links
|
32
|
+
|
33
|
+
link = {
|
34
|
+
href: href,
|
35
|
+
rel: rel,
|
36
|
+
}
|
37
|
+
link = link.merge(attributes)
|
38
|
+
|
39
|
+
json = {
|
40
|
+
href: href,
|
41
|
+
}
|
42
|
+
json = json.merge(attributes)
|
43
|
+
|
44
|
+
@serialization_links.append(link) if emit_header
|
45
|
+
serialization_dsl_result[:_links][rel] = json
|
46
|
+
|
47
|
+
serialization_dsl_result
|
48
|
+
end
|
49
|
+
|
50
|
+
def index(array, serializer = __getobj__, version:, view: nil)
|
51
|
+
raise CollectionTypeError, array.class.name unless array.is_a? Array
|
52
|
+
|
53
|
+
links = []
|
54
|
+
identifier = serializer.serializer_validator.view(view).version(version).identifier
|
55
|
+
|
56
|
+
array.each do |e|
|
57
|
+
child_links = []
|
58
|
+
context = SerializationDSL.new(__getobj__, child_links, context: @serialization_context)
|
59
|
+
serializer.serialize(e, identifier, @serialization_context, dsl: context)
|
60
|
+
|
61
|
+
self_links = child_links.select { |l| l[:rel] == :self }
|
62
|
+
raise NoSelfLinkProvidedError, identifier unless self_links.any?
|
63
|
+
raise MultipleSelfLinksProvidedError, identifier if self_links.length > 1
|
64
|
+
|
65
|
+
links.append(self_links.first.reject { |k, _| k == :rel } )
|
66
|
+
end
|
67
|
+
|
68
|
+
serialization_dsl_result[:_index] = links
|
69
|
+
|
70
|
+
serialization_dsl_result
|
71
|
+
end
|
72
|
+
|
73
|
+
def collection(array, serializer = __getobj__, version:, view: nil, &block)
|
74
|
+
raise CollectionTypeError, array.class.name unless array.is_a? Array
|
75
|
+
|
76
|
+
identifier = serializer.serializer_validator.view(view).version(version).identifier
|
77
|
+
|
78
|
+
rendered = []
|
79
|
+
|
80
|
+
array.each do |e|
|
81
|
+
context = SerializationDSL.new(__getobj__, [], @serialization_vary, context: @serialization_context)
|
82
|
+
result = serializer.serialize(e, identifier, @serialization_context, dsl: context, raw: true)
|
83
|
+
|
84
|
+
result = block.call(result) unless block.nil?
|
85
|
+
|
86
|
+
rendered.append(result)
|
87
|
+
end
|
88
|
+
|
89
|
+
serialization_dsl_result[:_embedded] = rendered
|
90
|
+
|
91
|
+
serialization_dsl_result
|
92
|
+
end
|
93
|
+
|
94
|
+
def hidden(&block)
|
95
|
+
context = SerializationDSL.new(__getobj__, @serialization_links, context: @serialization_context)
|
96
|
+
context.instance_exec(&block)
|
97
|
+
|
98
|
+
serialization_dsl_result
|
99
|
+
end
|
100
|
+
|
101
|
+
def render_view(name, context:, **args)
|
102
|
+
context.render_to_string(name, **args)
|
103
|
+
end
|
104
|
+
|
105
|
+
def emit
|
106
|
+
serialization_dsl_result
|
107
|
+
end
|
108
|
+
|
109
|
+
def object(&block)
|
110
|
+
context = SerializationDSL.new(__getobj__, @serialization_links, @serialization_vary, context: @serialization_context)
|
111
|
+
context.instance_exec(&block)
|
112
|
+
|
113
|
+
context.serialization_dsl_result
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'media_types/serialization/error'
|
4
|
+
require 'media_types'
|
5
|
+
|
6
|
+
module MediaTypes
|
7
|
+
module Serialization
|
8
|
+
# A collection that manages media type identifier registrations
|
9
|
+
class SerializationRegistration
|
10
|
+
def initialize(direction)
|
11
|
+
self.registrations = {}
|
12
|
+
self.inout = direction
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_accessor :registrations, :inout
|
16
|
+
|
17
|
+
def has?(identifier)
|
18
|
+
registrations.key? identifier
|
19
|
+
end
|
20
|
+
|
21
|
+
def register_block(serializer, validator, version, block, raw, wildcards: true)
|
22
|
+
identifier = validator.identifier
|
23
|
+
|
24
|
+
raise DuplicateDefinitionError.new(identifier, inout) if registrations.key? identifier
|
25
|
+
|
26
|
+
raise ValidatorNotDefinedError.new(identifier, inout) unless raw || validator.validatable?
|
27
|
+
|
28
|
+
registration = SerializationBlockRegistration.new serializer, inout, validator, identifier, version, block, raw
|
29
|
+
registrations[identifier] = registration
|
30
|
+
|
31
|
+
register_wildcards(identifier, registration) if wildcards && inout == :output
|
32
|
+
end
|
33
|
+
|
34
|
+
def register_alias(serializer, alias_identifier, target_identifier, optional, wildcards: true)
|
35
|
+
raise DuplicateDefinitionError.new(identifier, inout) if registrations.key? alias_identifier
|
36
|
+
|
37
|
+
raise UnbackedAliasDefinitionError.new(target_identifier, inout) unless registrations.key? target_identifier
|
38
|
+
|
39
|
+
target = registrations[target_identifier]
|
40
|
+
|
41
|
+
registration = SerializationAliasRegistration.new serializer, inout, target.validator, alias_identifier, target, optional
|
42
|
+
registrations[alias_identifier] = registration
|
43
|
+
|
44
|
+
register_wildcards(alias_identifier, registration) if wildcards && inout == :output
|
45
|
+
end
|
46
|
+
|
47
|
+
def merge(other)
|
48
|
+
raise Error, 'Trying to merge two SerializationRegistration objects with a different direction.' unless inout == other.inout
|
49
|
+
|
50
|
+
result = SerializationRegistration.new(inout)
|
51
|
+
|
52
|
+
prev_keys = Set.new(registrations.keys)
|
53
|
+
new_keys = Set.new(other.registrations.keys)
|
54
|
+
overlap = prev_keys & new_keys
|
55
|
+
|
56
|
+
result.registrations = registrations.merge(other.registrations)
|
57
|
+
overlap.each do |identifier|
|
58
|
+
prev_item = registrations[identifier]
|
59
|
+
new_item = other.registrations[identifier]
|
60
|
+
merge_result = prev_item.merge(new_item)
|
61
|
+
|
62
|
+
raise DuplicateUsageError.new(identifier, inout, prev_item.serializer, new_item.serializer) if merge_result.nil?
|
63
|
+
|
64
|
+
result.registrations[identifier] = merge_result
|
65
|
+
end
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
def decode(victim, media_type, context)
|
71
|
+
registration = registrations[media_type]
|
72
|
+
raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
|
73
|
+
|
74
|
+
registration.decode(victim, context)
|
75
|
+
end
|
76
|
+
|
77
|
+
def call(victim, media_type, context, dsl: nil, raw: nil)
|
78
|
+
registration = registrations[media_type]
|
79
|
+
raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
|
80
|
+
|
81
|
+
registration.call(victim, context, dsl: dsl, raw: raw)
|
82
|
+
end
|
83
|
+
|
84
|
+
def identifier_for(input_identifier)
|
85
|
+
registration = registrations[input_identifier]
|
86
|
+
raise UnregisteredMediaTypeUsageError.new(media_type, registrations.keys) if registration.nil?
|
87
|
+
|
88
|
+
registration.display_identifier
|
89
|
+
end
|
90
|
+
|
91
|
+
def filter(views:)
|
92
|
+
result = SerializationRegistration.new inout
|
93
|
+
|
94
|
+
registrations.each do |identifier, registration|
|
95
|
+
if views.include? registration.validator.view
|
96
|
+
result.registrations[identifier] = registration
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def register_wildcards(identifier, registration)
|
106
|
+
new_alias = SerializationAliasRegistration.new registration.serializer, registration.inout, registration.validator, identifier, registration, true
|
107
|
+
|
108
|
+
registrations['*/*'] = new_alias unless has? '*/*'
|
109
|
+
|
110
|
+
partial = "#{identifier.split('/')[0]}/*"
|
111
|
+
registrations[partial] = new_alias unless has? partial
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# A registration in a SerializationRegistration collection
|
116
|
+
class SerializationBaseRegistration
|
117
|
+
def initialize(serializer, inout, validator, display_identifier)
|
118
|
+
self.serializer = serializer
|
119
|
+
self.inout = inout
|
120
|
+
self.validator = validator
|
121
|
+
self.display_identifier = display_identifier
|
122
|
+
end
|
123
|
+
|
124
|
+
def merge(_other)
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
128
|
+
def decode(_victim, _context)
|
129
|
+
raise 'Assertion failed, decode function called on base registration.'
|
130
|
+
end
|
131
|
+
def call(_victim, _context, dsl: nil, raw: nil)
|
132
|
+
raise 'Assertion failed, call function called on base registration.'
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_accessor :serializer, :inout, :validator, :display_identifier
|
136
|
+
end
|
137
|
+
|
138
|
+
# A registration with a block to be executed when called.
|
139
|
+
class SerializationBlockRegistration < SerializationBaseRegistration
|
140
|
+
def initialize(serializer, inout, validator, display_identifier, version, block, raw)
|
141
|
+
self.version = version
|
142
|
+
self.block = block
|
143
|
+
self.raw = raw
|
144
|
+
super(serializer, inout, validator, display_identifier)
|
145
|
+
end
|
146
|
+
|
147
|
+
def merge(other)
|
148
|
+
return nil unless other.is_a?(SerializationAliasRegistration)
|
149
|
+
|
150
|
+
return self if other.optional
|
151
|
+
|
152
|
+
nil
|
153
|
+
end
|
154
|
+
|
155
|
+
def decode(victim, _context)
|
156
|
+
raise CannotDecodeOutputError if inout != :input
|
157
|
+
|
158
|
+
unless raw
|
159
|
+
if defined? Oj::ParseError
|
160
|
+
begin
|
161
|
+
victim = MediaTypes::Serialization.json_decoder.call(victim)
|
162
|
+
validator.validate!(victim)
|
163
|
+
rescue MediaTypes::Scheme::ValidationError, Oj::ParseError, JSON::ParserError => inner
|
164
|
+
raise InputValidationFailedError, inner
|
165
|
+
end
|
166
|
+
else
|
167
|
+
begin
|
168
|
+
victim = MediaTypes::Serialization.json_decoder.call(victim)
|
169
|
+
validator.validate!(victim)
|
170
|
+
rescue MediaTypes::Scheme::ValidationError, JSON::ParserError => inner
|
171
|
+
raise InputValidationFailedError, inner
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
victim
|
177
|
+
end
|
178
|
+
|
179
|
+
def call(victim, context, dsl: nil, raw: nil)
|
180
|
+
raw = self.raw if raw.nil?
|
181
|
+
|
182
|
+
result = nil
|
183
|
+
if dsl.nil?
|
184
|
+
result = victim
|
185
|
+
result = block.call(victim, version, context) if block
|
186
|
+
else
|
187
|
+
result = dsl.instance_exec victim, version, context, &block
|
188
|
+
end
|
189
|
+
|
190
|
+
if !raw && inout == :output
|
191
|
+
begin
|
192
|
+
validator.validate!(result)
|
193
|
+
rescue MediaTypes::Scheme::ValidationError => inner
|
194
|
+
raise OutputValidationFailedError, inner
|
195
|
+
end
|
196
|
+
result = MediaTypes::Serialization.json_encoder.call(result)
|
197
|
+
end
|
198
|
+
|
199
|
+
result
|
200
|
+
end
|
201
|
+
|
202
|
+
attr_accessor :version, :block, :raw
|
203
|
+
end
|
204
|
+
|
205
|
+
# A registration that calls another registration when called.
|
206
|
+
class SerializationAliasRegistration < SerializationBaseRegistration
|
207
|
+
def initialize(serializer, inout, validator, display_identifier, target, optional)
|
208
|
+
self.target = target
|
209
|
+
self.optional = optional
|
210
|
+
super(serializer, inout, validator, display_identifier)
|
211
|
+
end
|
212
|
+
|
213
|
+
def merge(other)
|
214
|
+
if optional
|
215
|
+
return other unless other.is_a?(SerializationAliasRegistration)
|
216
|
+
else
|
217
|
+
return nil if other.is_a?(SerializationAliasRegistration) && !other.optional # two non-optional can't merge
|
218
|
+
return self
|
219
|
+
end
|
220
|
+
|
221
|
+
other # if both optional, or other is !optional, newer one wins.
|
222
|
+
end
|
223
|
+
|
224
|
+
def decode(victim, context)
|
225
|
+
target.decode(victim, context)
|
226
|
+
end
|
227
|
+
|
228
|
+
def call(victim, context, dsl: nil, raw: nil)
|
229
|
+
target.call(victim, context, dsl: dsl, raw: raw)
|
230
|
+
end
|
231
|
+
|
232
|
+
attr_accessor :target, :optional
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'media_types/serialization/base'
|
4
|
+
require 'erb'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
module MediaTypes
|
8
|
+
module Serialization
|
9
|
+
module Serializers
|
10
|
+
class ApiViewer < MediaTypes::Serialization::Base
|
11
|
+
unvalidated 'text/html'
|
12
|
+
|
13
|
+
def self.viewerify(uri, current_host, type: 'last')
|
14
|
+
viewer = URI.parse(uri)
|
15
|
+
|
16
|
+
return uri unless viewer.host == current_host
|
17
|
+
|
18
|
+
query_parts = viewer.query&.split('&') || []
|
19
|
+
query_parts = query_parts.select { |p| !p.starts_with? 'api_viewer=' }
|
20
|
+
query_parts.append("api_viewer=#{type}")
|
21
|
+
viewer.query = query_parts.join('&')
|
22
|
+
viewer.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
output_raw do |obj, version, context|
|
26
|
+
original_identifier = obj[:identifier]
|
27
|
+
registrations = obj[:registrations]
|
28
|
+
original_output = obj[:output]
|
29
|
+
original_links = obj[:links]
|
30
|
+
|
31
|
+
api_fied_links = original_links.map do |l|
|
32
|
+
new = l.dup
|
33
|
+
new[:invalid] = false
|
34
|
+
begin
|
35
|
+
uri = viewerify(new[:href], context.request.host)
|
36
|
+
new[:href] = uri.to_s
|
37
|
+
rescue URI::InvalidURIError
|
38
|
+
new[:invalid] = true
|
39
|
+
end
|
40
|
+
|
41
|
+
new
|
42
|
+
end
|
43
|
+
|
44
|
+
media_types = registrations.registrations.keys.map do |identifier|
|
45
|
+
result = {
|
46
|
+
identifier: identifier,
|
47
|
+
href: viewerify(context.request.original_url, context.request.host, type: identifier),
|
48
|
+
selected: identifier == original_identifier,
|
49
|
+
}
|
50
|
+
result[:href] = '#output' if identifier == original_identifier
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
escaped_output = original_output&.split("\n").
|
57
|
+
map { |l| CGI::escapeHTML(l).gsub(/ (?= )/, ' ') }.
|
58
|
+
map { |l| (l.gsub(/\bhttps?:\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;{}]*[-A-Z0-9+@#\/%=}~_|](?![a-z]*;)/i) do |m|
|
59
|
+
converted = m
|
60
|
+
invalid = false
|
61
|
+
begin
|
62
|
+
converted = viewerify(m, context.request.host)
|
63
|
+
rescue URI::InvalidURIError
|
64
|
+
invalid = true
|
65
|
+
end
|
66
|
+
style = ''
|
67
|
+
style = ' style="color: red"' if invalid
|
68
|
+
"<a#{style} href=\"#{converted}\">#{m}</a>"
|
69
|
+
end) }.
|
70
|
+
join("<br>\n")
|
71
|
+
|
72
|
+
|
73
|
+
input = OpenStruct.new(
|
74
|
+
original_identifier: original_identifier,
|
75
|
+
escaped_output: escaped_output,
|
76
|
+
api_fied_links: api_fied_links,
|
77
|
+
media_types: media_types,
|
78
|
+
css: CommonCSS.css,
|
79
|
+
)
|
80
|
+
|
81
|
+
template = ERB.new <<-TEMPLATE
|
82
|
+
<html lang="en">
|
83
|
+
<head>
|
84
|
+
<title>API Viewer [<%= CGI::escapeHTML(original_identifier) %>]</title>
|
85
|
+
<style>
|
86
|
+
<%= css.split("\n").join("\n ") %>
|
87
|
+
</style>
|
88
|
+
</head>
|
89
|
+
<body>
|
90
|
+
<header>
|
91
|
+
<div id="logo"></div>
|
92
|
+
<h1>Api Viewer - <%= CGI::escapeHTML(original_identifier) %></h1>
|
93
|
+
</header>
|
94
|
+
<section id="content">
|
95
|
+
<nav>
|
96
|
+
<section id="representations">
|
97
|
+
<h2>Representations:</h2>
|
98
|
+
<ul>
|
99
|
+
<% media_types.each do |m| %>
|
100
|
+
<li>
|
101
|
+
<a href="<%= m[:href] %>" <%= m[:selected] ? 'class="active" ' : '' %>>
|
102
|
+
<%= CGI::escapeHTML(m[:identifier]) %>
|
103
|
+
</a>
|
104
|
+
</li>
|
105
|
+
<% end %>
|
106
|
+
</ul>
|
107
|
+
<hr>
|
108
|
+
</section>
|
109
|
+
<section id="links">
|
110
|
+
<span class="label">Links: </span>
|
111
|
+
<ul>
|
112
|
+
<% api_fied_links.each do |l| %>
|
113
|
+
<li><a <% if l[:invalid] %> style="color: red" <% end %>href="<%= l[:href] %>"><%= CGI::escapeHTML(l[:rel].to_s) %></a></li>
|
114
|
+
<% end %>
|
115
|
+
</ul>
|
116
|
+
</section>
|
117
|
+
</nav>
|
118
|
+
<main>
|
119
|
+
<code id="output">
|
120
|
+
<%= escaped_output %>
|
121
|
+
</code>
|
122
|
+
</main>
|
123
|
+
</section>
|
124
|
+
<!-- API viewer made with ❤ by: https://delftsolutions.com -->
|
125
|
+
</body>
|
126
|
+
</html>
|
127
|
+
TEMPLATE
|
128
|
+
template.result(input.instance_eval { binding })
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|