cloudinary 1.18.0 → 1.21.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/.github/ISSUE_TEMPLATE/bug_report.md +15 -14
- data/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
- data/.github/pull_request_template.md +18 -11
- data/CHANGELOG.md +69 -0
- data/cloudinary.gemspec +8 -3
- data/lib/active_storage/service/cloudinary_service.rb +23 -3
- data/lib/cloudinary.rb +54 -64
- data/lib/cloudinary/account_api.rb +226 -0
- data/lib/cloudinary/account_config.rb +30 -0
- data/lib/cloudinary/api.rb +65 -78
- data/lib/cloudinary/auth_token.rb +4 -0
- data/lib/cloudinary/base_api.rb +79 -0
- data/lib/cloudinary/base_config.rb +70 -0
- data/lib/cloudinary/config.rb +43 -0
- data/lib/cloudinary/search.rb +40 -7
- data/lib/cloudinary/uploader.rb +52 -23
- data/lib/cloudinary/utils.rb +216 -29
- data/lib/cloudinary/version.rb +1 -1
- data/vendor/assets/javascripts/cloudinary/jquery.cloudinary.js +16 -4
- metadata +17 -12
@@ -0,0 +1,30 @@
|
|
1
|
+
module Cloudinary
|
2
|
+
module AccountConfig
|
3
|
+
include BaseConfig
|
4
|
+
|
5
|
+
ENV_URL = "CLOUDINARY_ACCOUNT_URL"
|
6
|
+
SCHEME = "account"
|
7
|
+
|
8
|
+
def load_config_from_env
|
9
|
+
load_from_url(ENV[ENV_URL]) if ENV[ENV_URL]
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def env_url
|
15
|
+
ENV_URL
|
16
|
+
end
|
17
|
+
|
18
|
+
def expected_scheme
|
19
|
+
SCHEME
|
20
|
+
end
|
21
|
+
|
22
|
+
def config_from_parsed_url(parsed_url)
|
23
|
+
{
|
24
|
+
"account_id" => parsed_url.host,
|
25
|
+
"provisioning_api_key" => parsed_url.user,
|
26
|
+
"provisioning_api_secret" => parsed_url.password
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/cloudinary/api.rb
CHANGED
@@ -1,37 +1,28 @@
|
|
1
|
-
require 'rest_client'
|
2
|
-
require 'json'
|
3
|
-
|
4
1
|
class Cloudinary::Api
|
5
|
-
|
6
|
-
class NotFound < Error; end
|
7
|
-
class NotAllowed < Error; end
|
8
|
-
class AlreadyExists < Error; end
|
9
|
-
class RateLimited < Error; end
|
10
|
-
class BadRequest < Error; end
|
11
|
-
class GeneralError < Error; end
|
12
|
-
class AuthorizationRequired < Error; end
|
13
|
-
|
14
|
-
class Response < Hash
|
15
|
-
attr_reader :rate_limit_reset_at, :rate_limit_remaining, :rate_limit_allowed
|
16
|
-
|
17
|
-
def initialize(response=nil)
|
18
|
-
if response
|
19
|
-
# This sets the instantiated self as the response Hash
|
20
|
-
update Cloudinary::Api.parse_json_response response
|
21
|
-
|
22
|
-
@rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i if response.headers[:x_featureratelimit_limit]
|
23
|
-
@rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset]) if response.headers[:x_featureratelimit_reset]
|
24
|
-
@rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i if response.headers[:x_featureratelimit_remaining]
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
2
|
+
extend Cloudinary::BaseApi
|
28
3
|
|
29
4
|
def self.ping(options={})
|
30
5
|
call_api(:get, "ping", {}, options)
|
31
6
|
end
|
32
7
|
|
8
|
+
# Gets account usage details
|
9
|
+
#
|
10
|
+
# Get a report on the status of your Cloudinary account usage details, including
|
11
|
+
# storage, bandwidth, requests, number of resources, and add-on usage.
|
12
|
+
# Note that numbers are updated periodically.
|
13
|
+
#
|
14
|
+
# @see https://cloudinary.com/documentation/admin_api#get_account_usage_details Get account usage details
|
15
|
+
#
|
16
|
+
# @param [Hash] options Additional options
|
17
|
+
# @return [Cloudinary::Api::Response]
|
18
|
+
# @raise [Cloudinary::Api:Error]
|
33
19
|
def self.usage(options={})
|
34
|
-
|
20
|
+
uri = 'usage'
|
21
|
+
date = options[:date]
|
22
|
+
|
23
|
+
uri += "/#{Cloudinary::Utils.to_usage_api_date_format(date)}" unless date.nil?
|
24
|
+
|
25
|
+
call_api(:get, uri, {}, options)
|
35
26
|
end
|
36
27
|
|
37
28
|
def self.resource_types(options={})
|
@@ -43,25 +34,25 @@ class Cloudinary::Api
|
|
43
34
|
type = options[:type]
|
44
35
|
uri = "resources/#{resource_type}"
|
45
36
|
uri += "/#{type}" unless type.blank?
|
46
|
-
call_api(:get, uri, only(options, :next_cursor, :max_results, :prefix, :tags, :context, :moderations, :direction, :start_at), options)
|
37
|
+
call_api(:get, uri, only(options, :next_cursor, :max_results, :prefix, :tags, :context, :moderations, :direction, :start_at, :metadata), options)
|
47
38
|
end
|
48
39
|
|
49
40
|
def self.resources_by_tag(tag, options={})
|
50
41
|
resource_type = options[:resource_type] || "image"
|
51
42
|
uri = "resources/#{resource_type}/tags/#{tag}"
|
52
|
-
call_api(:get, uri, only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction), options)
|
43
|
+
call_api(:get, uri, only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction, :metadata), options)
|
53
44
|
end
|
54
45
|
|
55
46
|
def self.resources_by_moderation(kind, status, options={})
|
56
47
|
resource_type = options[:resource_type] || "image"
|
57
48
|
uri = "resources/#{resource_type}/moderations/#{kind}/#{status}"
|
58
|
-
call_api(:get, uri, only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction), options)
|
49
|
+
call_api(:get, uri, only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction, :metadata), options)
|
59
50
|
end
|
60
51
|
|
61
52
|
def self.resources_by_context(key, value=nil, options={})
|
62
53
|
resource_type = options[:resource_type] || "image"
|
63
54
|
uri = "resources/#{resource_type}/context"
|
64
|
-
params = only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction
|
55
|
+
params = only(options, :next_cursor, :max_results, :tags, :context, :moderations, :direction, :key, :value, :metadata)
|
65
56
|
params[:key] = key
|
66
57
|
params[:value] = value
|
67
58
|
call_api(:get, uri, params, options)
|
@@ -91,7 +82,8 @@ class Cloudinary::Api
|
|
91
82
|
:phash,
|
92
83
|
:quality_analysis,
|
93
84
|
:derived_next_cursor,
|
94
|
-
:accessibility_analysis
|
85
|
+
:accessibility_analysis,
|
86
|
+
:versions
|
95
87
|
), options)
|
96
88
|
end
|
97
89
|
|
@@ -99,7 +91,7 @@ class Cloudinary::Api
|
|
99
91
|
resource_type = options[:resource_type] || "image"
|
100
92
|
type = options[:type] || "upload"
|
101
93
|
uri = "resources/#{resource_type}/#{type}/restore"
|
102
|
-
call_api(:post, uri, { :public_ids => public_ids }, options)
|
94
|
+
call_api(:post, uri, { :public_ids => public_ids, :versions => options[:versions] }, options)
|
103
95
|
end
|
104
96
|
|
105
97
|
def self.update(public_id, options={})
|
@@ -185,24 +177,32 @@ class Cloudinary::Api
|
|
185
177
|
end
|
186
178
|
|
187
179
|
def self.transformation(transformation, options={})
|
188
|
-
|
180
|
+
params = only(options, :next_cursor, :max_results)
|
181
|
+
params[:transformation] = Cloudinary::Utils.build_eager(transformation)
|
182
|
+
call_api(:get, "transformations", params, options)
|
189
183
|
end
|
190
184
|
|
191
185
|
def self.delete_transformation(transformation, options={})
|
192
|
-
call_api(:delete, "transformations
|
186
|
+
call_api(:delete, "transformations", {:transformation => Cloudinary::Utils.build_eager(transformation)}, options)
|
193
187
|
end
|
194
188
|
|
195
189
|
# updates - supports:
|
196
190
|
# "allowed_for_strict" boolean
|
197
191
|
# "unsafe_update" transformation params - updates a named transformation parameters without regenerating existing images
|
198
192
|
def self.update_transformation(transformation, updates, options={})
|
199
|
-
params
|
200
|
-
params[:unsafe_update]
|
201
|
-
|
193
|
+
params = only(updates, :allowed_for_strict)
|
194
|
+
params[:unsafe_update] = Cloudinary::Utils.build_eager(updates[:unsafe_update]) if updates[:unsafe_update]
|
195
|
+
params[:transformation] = Cloudinary::Utils.build_eager(transformation)
|
196
|
+
call_api(:put, "transformations", params, options)
|
202
197
|
end
|
203
198
|
|
204
199
|
def self.create_transformation(name, definition, options={})
|
205
|
-
|
200
|
+
params = {
|
201
|
+
:name => name,
|
202
|
+
:transformation => Cloudinary::Utils.build_eager(definition)
|
203
|
+
}
|
204
|
+
|
205
|
+
call_api(:post, "transformations", params, options)
|
206
206
|
end
|
207
207
|
|
208
208
|
# upload presets
|
@@ -220,12 +220,12 @@ class Cloudinary::Api
|
|
220
220
|
|
221
221
|
def self.update_upload_preset(name, options={})
|
222
222
|
params = Cloudinary::Uploader.build_upload_params(options)
|
223
|
-
call_api(:put, "upload_presets/#{name}", params.merge(only(options, :unsigned, :disallow_public_id)), options)
|
223
|
+
call_api(:put, "upload_presets/#{name}", params.merge(only(options, :unsigned, :disallow_public_id, :live)), options)
|
224
224
|
end
|
225
225
|
|
226
226
|
def self.create_upload_preset(options={})
|
227
227
|
params = Cloudinary::Uploader.build_upload_params(options)
|
228
|
-
call_api(:post, "upload_presets", params.merge(only(options, :name, :unsigned, :disallow_public_id)), options)
|
228
|
+
call_api(:post, "upload_presets", params.merge(only(options, :name, :unsigned, :disallow_public_id, :live)), options)
|
229
229
|
end
|
230
230
|
|
231
231
|
def self.root_folders(options={})
|
@@ -486,47 +486,35 @@ class Cloudinary::Api
|
|
486
486
|
call_metadata_api(:post, uri, params, options)
|
487
487
|
end
|
488
488
|
|
489
|
+
# Reorders metadata field datasource. Currently supports only value.
|
490
|
+
#
|
491
|
+
# @param [String] field_external_id The ID of the metadata field
|
492
|
+
# @param [String] order_by Criteria for the order. Currently supports only value
|
493
|
+
# @param [String] direction Optional (gets either asc or desc)
|
494
|
+
# @param [Hash] options Configuration options
|
495
|
+
#
|
496
|
+
# @return [Cloudinary::Api::Response]
|
497
|
+
#
|
498
|
+
# @raise [Cloudinary::Api::Error]
|
499
|
+
def self.reorder_metadata_field_datasource(field_external_id, order_by, direction = nil, options = {})
|
500
|
+
uri = [field_external_id, "datasource", "order"]
|
501
|
+
params = { :order_by => order_by, :direction => direction }
|
502
|
+
|
503
|
+
call_metadata_api(:post, uri, params, options)
|
504
|
+
end
|
505
|
+
|
489
506
|
protected
|
490
507
|
|
491
508
|
def self.call_api(method, uri, params, options)
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
api_url = [cloudinary, "v1_1", cloud_name, uri].join("/")
|
499
|
-
# Add authentication
|
500
|
-
api_url.sub!(%r(^(https?://)), "\\1#{api_key}:#{api_secret}@")
|
501
|
-
|
502
|
-
headers = { "User-Agent" => Cloudinary::USER_AGENT }
|
503
|
-
if options[:content_type]== :json
|
504
|
-
payload = params.to_json
|
505
|
-
headers.merge!("Content-Type"=> 'application/json', "Accept"=> 'application/json')
|
506
|
-
else
|
507
|
-
payload = params.reject { |k, v| v.nil? || v=="" }
|
508
|
-
end
|
509
|
-
call_json_api(method, api_url, payload, timeout, headers)
|
510
|
-
end
|
511
|
-
|
512
|
-
def self.call_json_api(method, api_url, payload, timeout, headers)
|
513
|
-
RestClient::Request.execute(:method => method, :url => api_url, :payload => payload, :timeout => timeout, :headers => headers) do
|
514
|
-
|response, request, tmpresult|
|
515
|
-
return Response.new(response) if response.code == 200
|
516
|
-
exception_class = case response.code
|
517
|
-
when 400 then BadRequest
|
518
|
-
when 401 then AuthorizationRequired
|
519
|
-
when 403 then NotAllowed
|
520
|
-
when 404 then NotFound
|
521
|
-
when 409 then AlreadyExists
|
522
|
-
when 420 then RateLimited
|
523
|
-
when 500 then GeneralError
|
524
|
-
else raise GeneralError.new("Server returned unexpected status code - #{response.code} - #{response.body}")
|
525
|
-
end
|
526
|
-
json = parse_json_response(response)
|
527
|
-
raise exception_class.new(json["error"]["message"])
|
509
|
+
cloud_name = options[:cloud_name] || Cloudinary.config.cloud_name || raise('Must supply cloud_name')
|
510
|
+
api_key = options[:api_key] || Cloudinary.config.api_key || raise('Must supply api_key')
|
511
|
+
api_secret = options[:api_secret] || Cloudinary.config.api_secret || raise('Must supply api_secret')
|
512
|
+
|
513
|
+
call_cloudinary_api(method, uri, api_key, api_secret, params, options) do |cloudinary, inner_uri|
|
514
|
+
[cloudinary, 'v1_1', cloud_name, inner_uri]
|
528
515
|
end
|
529
516
|
end
|
517
|
+
|
530
518
|
def self.parse_json_response(response)
|
531
519
|
return Cloudinary::Utils.json_decode(response.body)
|
532
520
|
rescue => e
|
@@ -593,5 +581,4 @@ class Cloudinary::Api
|
|
593
581
|
params[by_key] = value
|
594
582
|
call_api("post", "resources/#{resource_type}/#{type}/update_access_mode", params, options)
|
595
583
|
end
|
596
|
-
|
597
584
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require "rest_client"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Cloudinary::BaseApi
|
5
|
+
class Error < CloudinaryException; end
|
6
|
+
class NotFound < Error; end
|
7
|
+
class NotAllowed < Error; end
|
8
|
+
class AlreadyExists < Error; end
|
9
|
+
class RateLimited < Error; end
|
10
|
+
class BadRequest < Error; end
|
11
|
+
class GeneralError < Error; end
|
12
|
+
class AuthorizationRequired < Error; end
|
13
|
+
|
14
|
+
class Response < Hash
|
15
|
+
attr_reader :rate_limit_reset_at, :rate_limit_remaining, :rate_limit_allowed
|
16
|
+
|
17
|
+
def initialize(response=nil)
|
18
|
+
if response
|
19
|
+
# This sets the instantiated self as the response Hash
|
20
|
+
update Cloudinary::Api.parse_json_response response
|
21
|
+
|
22
|
+
@rate_limit_allowed = response.headers[:x_featureratelimit_limit].to_i if response.headers[:x_featureratelimit_limit]
|
23
|
+
@rate_limit_reset_at = Time.parse(response.headers[:x_featureratelimit_reset]) if response.headers[:x_featureratelimit_reset]
|
24
|
+
@rate_limit_remaining = response.headers[:x_featureratelimit_remaining].to_i if response.headers[:x_featureratelimit_remaining]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.extended(base)
|
30
|
+
[Error, NotFound, NotAllowed, AlreadyExists, RateLimited, BadRequest, GeneralError, AuthorizationRequired, Response].each do |constant|
|
31
|
+
base.const_set(constant.name.split("::").last, constant)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def call_json_api(method, api_url, payload, timeout, headers, proxy = nil, user = nil, password = nil)
|
36
|
+
RestClient::Request.execute(method: method,
|
37
|
+
url: api_url,
|
38
|
+
payload: payload,
|
39
|
+
timeout: timeout,
|
40
|
+
headers: headers,
|
41
|
+
proxy: proxy,
|
42
|
+
user: user,
|
43
|
+
password: password) do |response|
|
44
|
+
return Response.new(response) if response.code == 200
|
45
|
+
exception_class = case response.code
|
46
|
+
when 400 then BadRequest
|
47
|
+
when 401 then AuthorizationRequired
|
48
|
+
when 403 then NotAllowed
|
49
|
+
when 404 then NotFound
|
50
|
+
when 409 then AlreadyExists
|
51
|
+
when 420 then RateLimited
|
52
|
+
when 500 then GeneralError
|
53
|
+
else raise GeneralError.new("Server returned unexpected status code - #{response.code} - #{response.body}")
|
54
|
+
end
|
55
|
+
json = Cloudinary::Api.parse_json_response(response)
|
56
|
+
raise exception_class.new(json["error"]["message"])
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def call_cloudinary_api(method, uri, user, password, params, options, &api_url_builder)
|
63
|
+
cloudinary = options[:upload_prefix] || Cloudinary.config.upload_prefix || 'https://api.cloudinary.com'
|
64
|
+
api_url = Cloudinary::Utils.smart_escape(api_url_builder.call(cloudinary, uri).flatten.join('/'))
|
65
|
+
timeout = options[:timeout] || Cloudinary.config.timeout || 60
|
66
|
+
proxy = options[:api_proxy] || Cloudinary.config.api_proxy
|
67
|
+
|
68
|
+
headers = { "User-Agent" => Cloudinary::USER_AGENT }
|
69
|
+
|
70
|
+
if options[:content_type] == :json
|
71
|
+
payload = params.to_json
|
72
|
+
headers.merge!("Content-Type" => "application/json", "Accept" => "application/json")
|
73
|
+
else
|
74
|
+
payload = params.reject { |_, v| v.nil? || v == "" }
|
75
|
+
end
|
76
|
+
|
77
|
+
call_json_api(method, api_url, payload, timeout, headers, proxy, user, password)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Cloudinary
|
2
|
+
module BaseConfig
|
3
|
+
def load_from_url(url)
|
4
|
+
return unless url && !url.empty?
|
5
|
+
|
6
|
+
parsed_url = URI.parse(url)
|
7
|
+
scheme = parsed_url.scheme.to_s.downcase
|
8
|
+
|
9
|
+
if expected_scheme != scheme
|
10
|
+
raise(CloudinaryException,
|
11
|
+
"Invalid #{env_url} scheme. Expecting to start with '#{expected_scheme}://'")
|
12
|
+
end
|
13
|
+
|
14
|
+
update(config_from_parsed_url(parsed_url))
|
15
|
+
setup_from_parsed_url(parsed_url)
|
16
|
+
end
|
17
|
+
|
18
|
+
def update(new_config = {})
|
19
|
+
new_config.each{ |k,v| public_send(:"#{k}=", v) unless v.nil?}
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_config_from_env
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def config_from_parsed_url(parsed_url)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def env_url
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def expected_scheme
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def put_nested_key(key, value)
|
41
|
+
chain = key.split(/[\[\]]+/).reject(&:empty?)
|
42
|
+
outer = self
|
43
|
+
lastKey = chain.pop
|
44
|
+
chain.each do |innerKey|
|
45
|
+
inner = outer[innerKey]
|
46
|
+
if inner.nil?
|
47
|
+
inner = OpenStruct.new
|
48
|
+
outer[innerKey] = inner
|
49
|
+
end
|
50
|
+
outer = inner
|
51
|
+
end
|
52
|
+
outer[lastKey] = value
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_nested_key?(key)
|
56
|
+
/\w+\[\w+\]/ =~ key
|
57
|
+
end
|
58
|
+
|
59
|
+
def setup_from_parsed_url(parsed_url)
|
60
|
+
parsed_url.query.to_s.split("&").each do |param|
|
61
|
+
key, value = param.split("=")
|
62
|
+
if is_nested_key? key
|
63
|
+
put_nested_key key, value
|
64
|
+
else
|
65
|
+
update(key => Utils.smart_unescape(value))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Cloudinary
|
2
|
+
module Config
|
3
|
+
include BaseConfig
|
4
|
+
|
5
|
+
ENV_URL = "CLOUDINARY_URL"
|
6
|
+
SCHEME = "cloudinary"
|
7
|
+
|
8
|
+
def load_config_from_env
|
9
|
+
if ENV["CLOUDINARY_CLOUD_NAME"]
|
10
|
+
config_keys = ENV.keys.select! { |key| key.start_with? "CLOUDINARY_" }
|
11
|
+
config_keys -= ["CLOUDINARY_URL"] # ignore it when explicit options are passed
|
12
|
+
config_keys.each do |full_key|
|
13
|
+
conf_key = full_key["CLOUDINARY_".length..-1].downcase # convert "CLOUDINARY_CONFIG_NAME" to "config_name"
|
14
|
+
conf_val = ENV[full_key]
|
15
|
+
conf_val = conf_val == 'true' if %w[true false].include?(conf_val) # cast relevant boolean values
|
16
|
+
update(conf_key => conf_val)
|
17
|
+
end
|
18
|
+
elsif ENV[ENV_URL]
|
19
|
+
load_from_url(ENV[ENV_URL])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def env_url
|
26
|
+
ENV_URL
|
27
|
+
end
|
28
|
+
|
29
|
+
def expected_scheme
|
30
|
+
SCHEME
|
31
|
+
end
|
32
|
+
|
33
|
+
def config_from_parsed_url(parsed_url)
|
34
|
+
{
|
35
|
+
"cloud_name" => parsed_url.host,
|
36
|
+
"api_key" => parsed_url.user,
|
37
|
+
"api_secret" => parsed_url.password,
|
38
|
+
"private_cdn" => !parsed_url.path.blank?,
|
39
|
+
"secure_distribution" => parsed_url.path[1..-1]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|