3scale_toolbox 0.14.0 → 0.15.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/3scale_toolbox.gemspec +2 -1
- data/README.md +21 -3
- data/lib/3scale_toolbox.rb +5 -1
- data/lib/3scale_toolbox/attribute_filters.rb +2 -0
- data/lib/3scale_toolbox/attribute_filters/attribute_filter.rb +9 -0
- data/lib/3scale_toolbox/attribute_filters/service_id_from_ref_filter.rb +30 -0
- data/lib/3scale_toolbox/commands/activedocs_command/apply_command.rb +1 -1
- data/lib/3scale_toolbox/commands/activedocs_command/list_command.rb +18 -1
- data/lib/3scale_toolbox/commands/import_command/openapi.rb +26 -5
- data/lib/3scale_toolbox/commands/import_command/openapi/create_activedocs_step.rb +4 -17
- data/lib/3scale_toolbox/commands/import_command/openapi/create_service_step.rb +1 -5
- data/lib/3scale_toolbox/commands/import_command/openapi/mapping_rule.rb +3 -2
- data/lib/3scale_toolbox/commands/import_command/openapi/step.rb +43 -5
- data/lib/3scale_toolbox/commands/import_command/openapi/update_policies_step.rb +7 -11
- data/lib/3scale_toolbox/commands/import_command/openapi/update_service_oidc_conf_step.rb +2 -17
- data/lib/3scale_toolbox/commands/import_command/openapi/update_service_proxy_step.rb +10 -10
- data/lib/3scale_toolbox/openapi.rb +2 -0
- data/lib/3scale_toolbox/openapi/oas3.rb +232 -0
- data/lib/3scale_toolbox/openapi/swagger.rb +192 -0
- data/lib/3scale_toolbox/tasks/copy_service_proxy_task.rb +1 -0
- data/lib/3scale_toolbox/version.rb +1 -1
- data/licenses.xml +161 -1
- data/resources/oas3_meta_schema.json +1654 -0
- metadata +24 -6
- data/lib/3scale_toolbox/commands/import_command/openapi/threescale_api_spec.rb +0 -80
- data/lib/3scale_toolbox/swagger.rb +0 -1
- data/lib/3scale_toolbox/swagger/swagger.rb +0 -123
@@ -27,31 +27,16 @@ module ThreeScaleToolbox
|
|
27
27
|
|
28
28
|
def add_flow_settings(settings)
|
29
29
|
# only applies to oauth2 sec type
|
30
|
-
return if security.nil? || security
|
30
|
+
return if api_spec.security.nil? || api_spec.security[:type] != 'oauth2'
|
31
31
|
|
32
32
|
oidc_configuration = {
|
33
33
|
standard_flow_enabled: false,
|
34
34
|
implicit_flow_enabled: false,
|
35
35
|
service_accounts_enabled: false,
|
36
36
|
direct_access_grants_enabled: false
|
37
|
-
}.merge(flow => true)
|
37
|
+
}.merge(api_spec.security[:flow] => true)
|
38
38
|
settings.merge!(oidc_configuration)
|
39
39
|
end
|
40
|
-
|
41
|
-
def flow
|
42
|
-
case (flow_f = security.flow)
|
43
|
-
when 'implicit'
|
44
|
-
:implicit_flow_enabled
|
45
|
-
when 'password'
|
46
|
-
:direct_access_grants_enabled
|
47
|
-
when 'application'
|
48
|
-
:service_accounts_enabled
|
49
|
-
when 'accessCode'
|
50
|
-
:standard_flow_enabled
|
51
|
-
else
|
52
|
-
raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_f}"
|
53
|
-
end
|
54
|
-
end
|
55
40
|
end
|
56
41
|
end
|
57
42
|
end
|
@@ -40,29 +40,29 @@ module ThreeScaleToolbox
|
|
40
40
|
end
|
41
41
|
|
42
42
|
def add_api_backend_settings(settings)
|
43
|
-
|
44
|
-
|
45
|
-
settings[:
|
43
|
+
settings[:api_backend] = private_base_url unless private_base_url.nil?
|
44
|
+
settings[:secret_token] = backend_api_secret_token unless backend_api_secret_token.nil?
|
45
|
+
settings[:hostname_rewrite] = backend_api_host_header unless backend_api_host_header.nil?
|
46
46
|
end
|
47
47
|
|
48
48
|
def add_security_proxy_settings(settings)
|
49
49
|
# nothing to add on proxy settings when no security required in openapi
|
50
|
-
return if security.nil?
|
50
|
+
return if api_spec.security.nil?
|
51
51
|
|
52
|
-
case security
|
52
|
+
case (type = api_spec.security[:type])
|
53
53
|
when 'oauth2'
|
54
54
|
settings[:credentials_location] = 'headers'
|
55
55
|
settings[:oidc_issuer_endpoint] = oidc_issuer_endpoint unless oidc_issuer_endpoint.nil?
|
56
56
|
when 'apiKey'
|
57
57
|
settings[:credentials_location] = credentials_location
|
58
|
-
settings[:auth_user_key] = security
|
58
|
+
settings[:auth_user_key] = api_spec.security[:name]
|
59
59
|
else
|
60
|
-
raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{
|
60
|
+
raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{type}"
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
64
|
def credentials_location
|
65
|
-
case (in_f = security
|
65
|
+
case (in_f = api_spec.security[:in_f])
|
66
66
|
when 'query'
|
67
67
|
'query'
|
68
68
|
when 'header'
|
@@ -73,13 +73,13 @@ module ThreeScaleToolbox
|
|
73
73
|
end
|
74
74
|
|
75
75
|
def private_base_url
|
76
|
-
|
76
|
+
override_private_base_url || private_base_url_from_openapi
|
77
77
|
end
|
78
78
|
|
79
79
|
def private_base_url_from_openapi
|
80
80
|
return if api_spec.host.nil?
|
81
81
|
|
82
|
-
"#{api_spec.
|
82
|
+
"#{api_spec.scheme || 'https'}://#{api_spec.host}"
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
module ThreeScaleToolbox
|
2
|
+
module OpenAPI
|
3
|
+
##
|
4
|
+
#
|
5
|
+
# OAS3 object
|
6
|
+
# * OAS3.title -> string
|
7
|
+
# * OAS3.description -> string
|
8
|
+
# * OAS3.version -> string
|
9
|
+
# * OAS3.base_path -> string
|
10
|
+
# * OAS3.host -> string
|
11
|
+
# * OAS3.scheme -> string
|
12
|
+
# * OAS3.operation -> array of operation hash
|
13
|
+
# * operation hash properties
|
14
|
+
# * :verb
|
15
|
+
# * :path
|
16
|
+
# * :description
|
17
|
+
# * :operation_id
|
18
|
+
# * OAS3.security -> security hash
|
19
|
+
# * security hash properties
|
20
|
+
# * :id -> string
|
21
|
+
# * :type -> string
|
22
|
+
# * :name -> string
|
23
|
+
# * :in_f -> string
|
24
|
+
# * :flow -> symbol (:implicit_flow_enabled, :direct_access_grants_enabled, :service_accounts_enabled, :standard_flow_enabled)
|
25
|
+
# * :scopes -> array of string
|
26
|
+
# * OAS3.service_backend_version -> string ('1','2','oidc')
|
27
|
+
# * OAS3.set_server_url -> def(spec, url)
|
28
|
+
# * OAS3.set_oauth2_urls-> def(spec, scheme_id, authorization_url, token_url)
|
29
|
+
class OAS3
|
30
|
+
META_SCHEMA_PATH = File.expand_path('../../../resources/oas3_meta_schema.json', __dir__)
|
31
|
+
|
32
|
+
def self.validate(raw)
|
33
|
+
meta_schema = JSON.parse(File.read(META_SCHEMA_PATH))
|
34
|
+
JSON::Validator.validate!(meta_schema, raw)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.build(path, raw, validate: true)
|
38
|
+
self.validate(raw) if validate
|
39
|
+
|
40
|
+
new(path, raw)
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :definition
|
44
|
+
|
45
|
+
def title
|
46
|
+
definition.info['title']
|
47
|
+
end
|
48
|
+
|
49
|
+
def description
|
50
|
+
definition.info['description']
|
51
|
+
end
|
52
|
+
|
53
|
+
def version
|
54
|
+
definition.info['version']
|
55
|
+
end
|
56
|
+
|
57
|
+
def base_path
|
58
|
+
# If there are many? take first
|
59
|
+
# From https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#openapi-object
|
60
|
+
# If the servers property is not provided, or is an empty array,
|
61
|
+
# the default value would be a Server Object with a url value of /
|
62
|
+
server_objects(&:path).first || '/'
|
63
|
+
end
|
64
|
+
|
65
|
+
def host
|
66
|
+
# If there are many? take first
|
67
|
+
server_objects(&:host).first
|
68
|
+
end
|
69
|
+
|
70
|
+
def scheme
|
71
|
+
# If there are many? take first
|
72
|
+
server_objects(&:scheme).first
|
73
|
+
end
|
74
|
+
|
75
|
+
def operations
|
76
|
+
@operations ||= parse_operations
|
77
|
+
end
|
78
|
+
|
79
|
+
def security
|
80
|
+
@security ||= parse_security
|
81
|
+
end
|
82
|
+
|
83
|
+
def service_backend_version
|
84
|
+
# default authentication mode if no security requirement
|
85
|
+
return '1' if security.nil?
|
86
|
+
|
87
|
+
case security[:type]
|
88
|
+
when 'oauth2'
|
89
|
+
'oidc'
|
90
|
+
when 'apiKey'
|
91
|
+
'1'
|
92
|
+
else
|
93
|
+
raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{security[:type]}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
# Update given spec with urls
|
99
|
+
# It is expected identified security scheme to be oauth2 type
|
100
|
+
def set_oauth2_urls(spec, sec_scheme_id, authorization_url, token_url)
|
101
|
+
sec_scheme_obj = spec.dig('components', 'securitySchemes', sec_scheme_id)
|
102
|
+
if sec_scheme_obj.nil? || sec_scheme_obj['type'] != 'oauth2'
|
103
|
+
raise ThreeScaleToolbox::Error, "Expected security scheme {#{sec_scheme_id}} not found or not oauth2"
|
104
|
+
end
|
105
|
+
|
106
|
+
flow_key, flow_obj = sec_scheme_obj['flows'].first
|
107
|
+
flow_obj['authorizationUrl'] = authorization_url if %w[implicit authorizationCode].include?(flow_key)
|
108
|
+
flow_obj['tokenUrl'] = token_url if %w[password clientCredentials authorizationCode].include?(flow_key)
|
109
|
+
end
|
110
|
+
|
111
|
+
def set_server_url(spec, url)
|
112
|
+
spec['servers'] = [{ 'url' => url }]
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def initialize(path, raw)
|
118
|
+
parser = OasParser::Parser.new(path, raw).resolve
|
119
|
+
@definition = OasParser::Definition.new(parser, path)
|
120
|
+
end
|
121
|
+
|
122
|
+
def server_objects
|
123
|
+
servers.map do |s|
|
124
|
+
yield Helper.parse_uri rendered_url(s)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# OAS3 server object variable substitution
|
129
|
+
def rendered_url(server_object)
|
130
|
+
template = erbfying_template(server_object.fetch('url'))
|
131
|
+
vars = server_object_variables(server_object['variables'])
|
132
|
+
ERB.new(template).result(OpenStruct.new(vars).instance_eval { binding })
|
133
|
+
end
|
134
|
+
|
135
|
+
def server_object_variables(variables)
|
136
|
+
vars = (variables || {}).each_with_object({}) do |(key, value), a|
|
137
|
+
a[key] = value['default']
|
138
|
+
end
|
139
|
+
JSON.parse(vars.to_json, symbolize_names: true)
|
140
|
+
end
|
141
|
+
|
142
|
+
def erbfying_template(template)
|
143
|
+
# A URL is composed from a limited set of characters belonging to the US-ASCII character set.
|
144
|
+
# These characters include digits (0-9), letters(A-Z, a-z), and a few special characters ("-", ".", "_", "~").
|
145
|
+
# https://www.urlencoder.io/learn/
|
146
|
+
tmp = template.gsub '{', '<%='
|
147
|
+
tmp.gsub '}', '%>'
|
148
|
+
end
|
149
|
+
|
150
|
+
def servers
|
151
|
+
definition.servers || []
|
152
|
+
end
|
153
|
+
|
154
|
+
def parse_operations
|
155
|
+
definition.paths.flat_map do |path_obj|
|
156
|
+
path_obj.endpoints.flat_map do |endpoint|
|
157
|
+
{
|
158
|
+
verb: endpoint.method,
|
159
|
+
path: endpoint.path.path,
|
160
|
+
description: endpoint.description,
|
161
|
+
operation_id: endpoint.operationId
|
162
|
+
}
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_security
|
168
|
+
raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple security requirements' \
|
169
|
+
if global_security_requirements.size > 1
|
170
|
+
|
171
|
+
global_security_requirements.first
|
172
|
+
end
|
173
|
+
|
174
|
+
def global_security_requirements
|
175
|
+
@global_security_requirements ||= parse_global_security_reqs
|
176
|
+
end
|
177
|
+
|
178
|
+
def parse_global_security_reqs
|
179
|
+
security_requirements.flat_map do |sec_req|
|
180
|
+
sec_req.map do |sec_item_name, sec_item|
|
181
|
+
sec_def = fetch_security_scheme(sec_item_name)
|
182
|
+
{
|
183
|
+
id: sec_item_name,
|
184
|
+
type: sec_def['type'],
|
185
|
+
name: sec_def['name'],
|
186
|
+
in_f: sec_def['in'],
|
187
|
+
flow: parse_flows(sec_def['flows']),
|
188
|
+
scopes: sec_item
|
189
|
+
}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def fetch_security_scheme(name)
|
195
|
+
security_schemes.fetch(name) do |el|
|
196
|
+
raise ThreeScaleToolbox::Error, "OAS3 parsing error: #{el} not found in security schemes"
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def security_requirements
|
201
|
+
definition.security || []
|
202
|
+
end
|
203
|
+
|
204
|
+
def security_schemes
|
205
|
+
(definition.components || {})['securitySchemes'] || {}
|
206
|
+
end
|
207
|
+
|
208
|
+
def parse_flows(flows_object)
|
209
|
+
return nil if flows_object.nil?
|
210
|
+
|
211
|
+
raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple flows' if flows_object.size > 1
|
212
|
+
|
213
|
+
convert_flow(flows_object.keys.first)
|
214
|
+
end
|
215
|
+
|
216
|
+
def convert_flow(flow_name)
|
217
|
+
case flow_name
|
218
|
+
when 'implicit'
|
219
|
+
:implicit_flow_enabled
|
220
|
+
when 'password'
|
221
|
+
:direct_access_grants_enabled
|
222
|
+
when 'clientCredentials'
|
223
|
+
:service_accounts_enabled
|
224
|
+
when 'authorizationCode'
|
225
|
+
:standard_flow_enabled
|
226
|
+
else
|
227
|
+
raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_name}"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
module ThreeScaleToolbox
|
2
|
+
module OpenAPI
|
3
|
+
##
|
4
|
+
#
|
5
|
+
# Swagger object
|
6
|
+
# * Swagger.title -> string
|
7
|
+
# * Swagger.description -> string
|
8
|
+
# * Swagger.version -> string
|
9
|
+
# * Swagger.basePath -> string
|
10
|
+
# * Swagger.host -> string
|
11
|
+
# * Swagger.scheme -> string
|
12
|
+
# * Swagger.operation -> array of operation hash
|
13
|
+
# * operation hash properties
|
14
|
+
# * :verb
|
15
|
+
# * :path
|
16
|
+
# * :description
|
17
|
+
# * :operation_id
|
18
|
+
# * Swagger.security -> security hash
|
19
|
+
# * security hash properties
|
20
|
+
# * :id -> string
|
21
|
+
# * :type -> string
|
22
|
+
# * :name -> string
|
23
|
+
# * :in_f -> string
|
24
|
+
# * :flow -> symbol (:implicit_flow_enabled, :direct_access_grants_enabled, :service_accounts_enabled, :standard_flow_enabled)
|
25
|
+
# * :scopes -> array of string
|
26
|
+
# * Swagger.service_backend_version -> string ('1','2','oidc')
|
27
|
+
# * Swagger.set_server_url -> def(spec, url)
|
28
|
+
# * Swagger.set_oauth2_urls-> def(spec, scheme_id, authorization_url, token_url)
|
29
|
+
class Swagger
|
30
|
+
META_SCHEMA_PATH = File.expand_path('../../../resources/swagger_meta_schema.json', __dir__)
|
31
|
+
|
32
|
+
def self.validate(raw)
|
33
|
+
meta_schema = JSON.parse(File.read(META_SCHEMA_PATH))
|
34
|
+
JSON::Validator.validate!(meta_schema, raw)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.build(raw, validate: true)
|
38
|
+
self.validate(raw) if validate
|
39
|
+
|
40
|
+
new(raw)
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :raw
|
44
|
+
|
45
|
+
def title
|
46
|
+
raw.dig('info', 'title')
|
47
|
+
end
|
48
|
+
|
49
|
+
def description
|
50
|
+
raw.dig('info', 'description')
|
51
|
+
end
|
52
|
+
|
53
|
+
def version
|
54
|
+
raw.dig('info', 'version')
|
55
|
+
end
|
56
|
+
|
57
|
+
def base_path
|
58
|
+
raw['basePath']
|
59
|
+
end
|
60
|
+
|
61
|
+
def host
|
62
|
+
raw['host']
|
63
|
+
end
|
64
|
+
|
65
|
+
def scheme
|
66
|
+
Array(raw['schemes']).first
|
67
|
+
end
|
68
|
+
|
69
|
+
def operations
|
70
|
+
@operations ||= parse_operations
|
71
|
+
end
|
72
|
+
|
73
|
+
def security
|
74
|
+
@security ||= parse_security
|
75
|
+
end
|
76
|
+
|
77
|
+
def service_backend_version
|
78
|
+
# default authentication mode if no security requirement
|
79
|
+
return '1' if security.nil?
|
80
|
+
|
81
|
+
case security[:type]
|
82
|
+
when 'oauth2'
|
83
|
+
'oidc'
|
84
|
+
when 'apiKey'
|
85
|
+
'1'
|
86
|
+
else
|
87
|
+
raise ThreeScaleToolbox::Error, "Unexpected security scheme type #{security[:type]}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_server_url(spec, url)
|
92
|
+
URI(url).tap do |uri|
|
93
|
+
spec['host'] = "#{uri.host}:#{uri.port}"
|
94
|
+
spec['schemes'] = [uri.scheme]
|
95
|
+
spec['basePath'] = uri.path
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# Update given spec with urls
|
101
|
+
# It is expected identified security scheme to be oauth2 type
|
102
|
+
def set_oauth2_urls(spec, sec_scheme_id, authorization_url, token_url)
|
103
|
+
sec_scheme_obj = spec.dig('securityDefinitions', sec_scheme_id)
|
104
|
+
if sec_scheme_obj.nil? || sec_scheme_obj['type'] != 'oauth2'
|
105
|
+
raise ThreeScaleToolbox::Error, "Expected security scheme {#{sec_scheme_id}} not found or not oauth2"
|
106
|
+
end
|
107
|
+
|
108
|
+
sec_scheme_obj['authorizationUrl'] = authorization_url if %w[implicit accessCode].include?(sec_scheme_obj['flow'])
|
109
|
+
sec_scheme_obj['tokenUrl'] = token_url if %w[password application accessCode].include?(sec_scheme_obj['flow'])
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def initialize(raw)
|
115
|
+
@raw = raw
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_operations
|
119
|
+
raw['paths'].flat_map do |path, path_obj|
|
120
|
+
path_obj.flat_map do |method, operation|
|
121
|
+
next unless %w[get head post put patch delete trace options].include? method
|
122
|
+
|
123
|
+
{
|
124
|
+
verb: method,
|
125
|
+
path: path,
|
126
|
+
description: operation['description'],
|
127
|
+
operation_id: operation['operationId']
|
128
|
+
}
|
129
|
+
end.compact
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_security
|
134
|
+
raise ThreeScaleToolbox::Error, 'Invalid OAS: multiple security requirements' \
|
135
|
+
if global_security_requirements.size > 1
|
136
|
+
|
137
|
+
global_security_requirements.first
|
138
|
+
end
|
139
|
+
|
140
|
+
def global_security_requirements
|
141
|
+
@global_security_requirements ||= parse_global_security_reqs
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_global_security_reqs
|
145
|
+
security_requirements.flat_map do |sec_req|
|
146
|
+
sec_req.map do |sec_item_name, sec_item|
|
147
|
+
sec_def = fetch_security_definition(sec_item_name)
|
148
|
+
{
|
149
|
+
id: sec_item_name,
|
150
|
+
type: sec_def['type'],
|
151
|
+
name: sec_def['name'],
|
152
|
+
in_f: sec_def['in'],
|
153
|
+
flow: convert_flow(sec_def['flow']),
|
154
|
+
scopes: sec_item
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def fetch_security_definition(name)
|
161
|
+
security_definitions.fetch(name) do |el|
|
162
|
+
raise ThreeScaleToolbox::Error, "Swagger parsing error: #{el} not found in security definitions"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def security_requirements
|
167
|
+
raw['security'] || []
|
168
|
+
end
|
169
|
+
|
170
|
+
def security_definitions
|
171
|
+
raw['securityDefinitions'] || {}
|
172
|
+
end
|
173
|
+
|
174
|
+
def convert_flow(flow_name)
|
175
|
+
return nil if flow_name.nil?
|
176
|
+
|
177
|
+
case flow_name
|
178
|
+
when 'implicit'
|
179
|
+
:implicit_flow_enabled
|
180
|
+
when 'password'
|
181
|
+
:direct_access_grants_enabled
|
182
|
+
when 'application'
|
183
|
+
:service_accounts_enabled
|
184
|
+
when 'accessCode'
|
185
|
+
:standard_flow_enabled
|
186
|
+
else
|
187
|
+
raise ThreeScaleToolbox::Error, "Unexpected security flow field #{flow_name}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|