cfoundry 0.4.21 → 0.5.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.
- data/Rakefile +47 -24
- data/lib/cfoundry/auth_token.rb +48 -0
- data/lib/cfoundry/baseclient.rb +96 -277
- data/lib/cfoundry/client.rb +2 -0
- data/lib/cfoundry/concerns/login_helpers.rb +13 -0
- data/lib/cfoundry/errors.rb +21 -13
- data/lib/cfoundry/rest_client.rb +290 -0
- data/lib/cfoundry/test_support.rb +3 -0
- data/lib/cfoundry/trace_helpers.rb +9 -9
- data/lib/cfoundry/uaaclient.rb +66 -74
- data/lib/cfoundry/upload_helpers.rb +2 -0
- data/lib/cfoundry/v1/app.rb +2 -2
- data/lib/cfoundry/v1/base.rb +4 -51
- data/lib/cfoundry/v1/client.rb +7 -30
- data/lib/cfoundry/v1/model.rb +22 -5
- data/lib/cfoundry/v1/model_magic.rb +30 -15
- data/lib/cfoundry/v2/app.rb +2 -5
- data/lib/cfoundry/v2/base.rb +10 -74
- data/lib/cfoundry/v2/client.rb +19 -30
- data/lib/cfoundry/v2/domain.rb +1 -4
- data/lib/cfoundry/v2/model.rb +1 -3
- data/lib/cfoundry/v2/model_magic.rb +13 -23
- data/lib/cfoundry/version.rb +1 -1
- data/lib/cfoundry/zip.rb +1 -1
- data/spec/cfoundry/auth_token_spec.rb +77 -0
- data/spec/cfoundry/baseclient_spec.rb +54 -30
- data/spec/cfoundry/errors_spec.rb +10 -13
- data/spec/cfoundry/rest_client_spec.rb +238 -0
- data/spec/cfoundry/trace_helpers_spec.rb +10 -5
- data/spec/cfoundry/uaaclient_spec.rb +141 -114
- data/spec/cfoundry/upload_helpers_spec.rb +129 -0
- data/spec/cfoundry/v1/base_spec.rb +2 -2
- data/spec/cfoundry/v1/client_spec.rb +17 -0
- data/spec/cfoundry/v1/model_magic_spec.rb +43 -0
- data/spec/cfoundry/v2/base_spec.rb +256 -33
- data/spec/cfoundry/v2/client_spec.rb +68 -0
- data/spec/cfoundry/v2/model_magic_spec.rb +49 -0
- data/spec/fixtures/apps/with_vmcignore/ignored_dir/file_in_ignored_dir.txt +1 -0
- data/spec/fixtures/apps/with_vmcignore/ignored_file.txt +1 -0
- data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/file_in_non_ignored_dir.txt +1 -0
- data/spec/fixtures/apps/with_vmcignore/non_ignored_dir/ignored_file.txt +1 -0
- data/spec/fixtures/apps/with_vmcignore/non_ignored_file.txt +1 -0
- data/spec/fixtures/empty_file +0 -0
- data/spec/spec_helper.rb +4 -4
- data/spec/support/randoms.rb +3 -0
- data/spec/support/shared_examples/client_login_examples.rb +46 -0
- data/spec/support/{summaries.rb → shared_examples/model_summary_examples.rb} +0 -0
- data/spec/support/v1_fake_helper.rb +144 -0
- metadata +101 -37
- data/lib/cfoundry/spec_helper.rb +0 -1
data/Rakefile
CHANGED
@@ -1,41 +1,64 @@
|
|
1
1
|
require "rake"
|
2
|
+
require "rspec/core/rake_task"
|
2
3
|
|
3
4
|
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
4
5
|
require "cfoundry/version"
|
5
6
|
|
6
|
-
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
task :default => :spec
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
namespace :deploy do
|
11
|
+
def last_staging_sha
|
12
|
+
`git rev-parse latest-staging`.strip
|
13
|
+
end
|
10
14
|
|
11
|
-
|
12
|
-
|
13
|
-
end
|
15
|
+
def last_release_sha
|
16
|
+
`git rev-parse latest-release`.strip
|
17
|
+
end
|
14
18
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
+
def last_staging_ref_was_released?
|
20
|
+
last_staging_sha == last_release_sha
|
21
|
+
end
|
19
22
|
|
20
|
-
task :
|
21
|
-
|
23
|
+
task :staging, :version do |_, args|
|
24
|
+
sh "gem bump --push #{"--version #{args.version}" if args.version}" if last_staging_ref_was_released?
|
25
|
+
sh "git tag -f latest-staging"
|
26
|
+
sh "git push origin :latest-staging"
|
27
|
+
sh "git push origin latest-staging"
|
28
|
+
end
|
29
|
+
|
30
|
+
task :gem do
|
31
|
+
sh "git fetch"
|
32
|
+
sh "git checkout #{last_staging_sha}"
|
33
|
+
sh "gem release --tag"
|
34
|
+
sh "git tag -f latest-release"
|
35
|
+
sh "git push origin :latest-release"
|
36
|
+
sh "git push origin latest-release"
|
37
|
+
end
|
22
38
|
end
|
23
39
|
|
24
|
-
|
40
|
+
namespace :release do
|
41
|
+
DEPENDENTS = %w[
|
42
|
+
vmc/vmc.gemspec
|
43
|
+
vmc-plugins/admin/admin-vmc-plugin.gemspec
|
44
|
+
vmc-plugins/console/console-vmc-plugin.gemspec
|
45
|
+
vmc-plugins/manifests/manifests-vmc-plugin.gemspec
|
46
|
+
vmc-plugins/mcf/mcf-vmc-plugin.gemspec
|
47
|
+
vmc-plugins/tunnel/tunnel-vmc-plugin.gemspec
|
48
|
+
].freeze
|
25
49
|
|
26
|
-
|
27
|
-
|
28
|
-
end
|
50
|
+
def bump_dependent(file, dep, ver)
|
51
|
+
puts "Bumping #{dep} to #{ver} in #{file}"
|
29
52
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
53
|
+
old = File.read(file)
|
54
|
+
new = old.sub(/(\.add.+#{dep}\D+)[^'"]+(.+)/, "\\1#{ver}\\2")
|
55
|
+
|
56
|
+
File.open(file, "w") { |io| io.print new }
|
34
57
|
end
|
35
|
-
end
|
36
58
|
|
37
|
-
|
38
|
-
|
39
|
-
|
59
|
+
task :bump_dependents do
|
60
|
+
DEPENDENTS.each do |dep|
|
61
|
+
bump_dependent(File.expand_path("../../#{dep}", __FILE__), "cfoundry", CFoundry::VERSION)
|
62
|
+
end
|
40
63
|
end
|
41
64
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module CFoundry
|
2
|
+
class AuthToken
|
3
|
+
class << self
|
4
|
+
def from_uaa_token_info(token_info)
|
5
|
+
new(
|
6
|
+
token_info.auth_header,
|
7
|
+
token_info.info[:refresh_token]
|
8
|
+
)
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_hash(hash)
|
12
|
+
new(
|
13
|
+
hash[:token],
|
14
|
+
hash[:refresh_token]
|
15
|
+
)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(auth_header, refresh_token = nil)
|
20
|
+
@auth_header = auth_header
|
21
|
+
@refresh_token = refresh_token
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :auth_header
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
{
|
28
|
+
:token => auth_header,
|
29
|
+
:refresh_token => @refresh_token
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
JSON_HASH = /\{.*?\}/.freeze
|
34
|
+
|
35
|
+
# TODO: rename to #data
|
36
|
+
def token_data
|
37
|
+
return @token_data if @token_data
|
38
|
+
|
39
|
+
json_hashes = Base64.decode64(@auth_header.split(" ", 2).last)
|
40
|
+
data_json = json_hashes.sub(JSON_HASH, "")[JSON_HASH]
|
41
|
+
return {} unless data_json
|
42
|
+
|
43
|
+
@token_data = MultiJson.load data_json, :symbolize_keys => true
|
44
|
+
rescue MultiJson::DecodeError
|
45
|
+
{}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/cfoundry/baseclient.rb
CHANGED
@@ -3,335 +3,154 @@ require "net/https"
|
|
3
3
|
require "net/http/post/multipart"
|
4
4
|
require "multi_json"
|
5
5
|
require "fileutils"
|
6
|
-
require "
|
6
|
+
require "forwardable"
|
7
7
|
|
8
8
|
module CFoundry
|
9
9
|
class BaseClient # :nodoc:
|
10
|
-
|
10
|
+
extend Forwardable
|
11
11
|
|
12
|
-
|
12
|
+
attr_reader :rest_client
|
13
13
|
|
14
|
-
|
14
|
+
def_delegators :rest_client, :target, :target=, :token,
|
15
|
+
:proxy, :proxy=, :trace, :backtrace, :backtrace=,
|
16
|
+
:log, :log=
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@trace = false
|
22
|
-
@backtrace = false
|
23
|
-
@log = false
|
24
|
-
end
|
25
|
-
|
26
|
-
# grab the metadata from a token that looks like:
|
27
|
-
#
|
28
|
-
# bearer (base64 ...)
|
29
|
-
def token_data
|
30
|
-
return {} unless @token
|
31
|
-
|
32
|
-
tok = Base64.decode64(@token.sub(/^bearer\s+/, ""))
|
33
|
-
tok.sub!(/\{.+?\}/, "") # clear algo
|
34
|
-
MultiJson.load(tok[/\{.+?\}/], :symbolize_keys => true)
|
35
|
-
|
36
|
-
# can't expect all tokens to be the proper format
|
37
|
-
rescue MultiJson::DecodeError
|
38
|
-
{}
|
39
|
-
end
|
40
|
-
|
41
|
-
def request_path(method, path, options = {})
|
42
|
-
path = url(path) if path.is_a?(Array)
|
43
|
-
|
44
|
-
request(method, path, options)
|
45
|
-
end
|
46
|
-
|
47
|
-
def request(method, path, options = {})
|
48
|
-
request_uri(URI.parse(@target + path), method, options)
|
18
|
+
def initialize(target = "https://api.cloudfoundry.com", token = nil)
|
19
|
+
@rest_client = CFoundry::RestClient.new(target, token)
|
20
|
+
self.trace = false
|
21
|
+
self.backtrace = false
|
22
|
+
self.log = false
|
49
23
|
end
|
50
24
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
content = options.delete(:content)
|
59
|
-
payload = options.delete(:payload)
|
60
|
-
params = options.delete(:params)
|
61
|
-
return_headers = options.delete(:return_headers)
|
62
|
-
return_response = options.delete(:return_response)
|
63
|
-
|
64
|
-
headers = {}
|
65
|
-
headers["Authorization"] = @token if @token
|
66
|
-
headers["Proxy-User"] = @proxy if @proxy
|
67
|
-
|
68
|
-
if accept_type = mimetype(accept)
|
69
|
-
headers["Accept"] = accept_type
|
70
|
-
end
|
71
|
-
|
72
|
-
if content_type = mimetype(content)
|
73
|
-
headers["Content-Type"] = content_type
|
74
|
-
end
|
75
|
-
|
76
|
-
unless payload.is_a?(String)
|
77
|
-
case content
|
78
|
-
when :json
|
79
|
-
payload = MultiJson.dump(payload)
|
80
|
-
when :form
|
81
|
-
payload = encode_params(payload)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
if payload.is_a?(String)
|
86
|
-
headers["Content-Length"] = payload.size
|
87
|
-
elsif !payload
|
88
|
-
headers["Content-Length"] = 0
|
25
|
+
def uaa
|
26
|
+
@uaa ||= begin
|
27
|
+
endpoint = info[:authorization_endpoint]
|
28
|
+
uaa = CFoundry::UAAClient.new(endpoint)
|
29
|
+
uaa.trace = trace
|
30
|
+
uaa.token = token
|
31
|
+
uaa
|
89
32
|
end
|
33
|
+
end
|
90
34
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
if uri.query
|
95
|
-
uri.query += "&" + encode_params(params)
|
96
|
-
else
|
97
|
-
uri.query = encode_params(params)
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
if payload && payload.is_a?(Hash)
|
102
|
-
multipart = method.const_get(:Multipart)
|
103
|
-
request = multipart.new(uri.request_uri, payload)
|
104
|
-
else
|
105
|
-
request = method.new(uri.request_uri)
|
106
|
-
request.body = payload if payload
|
107
|
-
end
|
108
|
-
|
109
|
-
request["Authorization"] = @token if @token
|
110
|
-
request["Proxy-User"] = @proxy if @proxy
|
111
|
-
|
112
|
-
headers.each do |k, v|
|
113
|
-
request[k] = v
|
114
|
-
end
|
115
|
-
|
116
|
-
# TODO: test http proxies
|
117
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
118
|
-
|
119
|
-
if uri.is_a?(URI::HTTPS)
|
120
|
-
http.use_ssl = true
|
121
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
35
|
+
def token=(token)
|
36
|
+
if token.is_a?(String)
|
37
|
+
token = CFoundry::AuthToken.new(token)
|
122
38
|
end
|
123
39
|
|
124
|
-
|
125
|
-
|
126
|
-
before = Time.now
|
127
|
-
http.start do
|
128
|
-
response = http.request(request)
|
129
|
-
time = Time.now - before
|
130
|
-
|
131
|
-
print_response(response) if @trace
|
132
|
-
print_backtrace(caller) if @trace
|
133
|
-
|
134
|
-
log_request(time, request, response)
|
135
|
-
|
136
|
-
if return_headers
|
137
|
-
sane_headers(response)
|
138
|
-
elsif return_response
|
139
|
-
response
|
140
|
-
elsif response.is_a?(Net::HTTPRedirection)
|
141
|
-
request_uri(
|
142
|
-
URI.parse(response["location"]),
|
143
|
-
Net::HTTP::Get,
|
144
|
-
original_options)
|
145
|
-
else
|
146
|
-
handle_response(response, accept, request)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
rescue ::Timeout::Error => e
|
150
|
-
raise Timeout.new(method, uri, e)
|
151
|
-
rescue SocketError, Errno::ECONNREFUSED => e
|
152
|
-
raise TargetRefused, e.message
|
40
|
+
@rest_client.token = token
|
41
|
+
@uaa.token = token if @uaa
|
153
42
|
end
|
154
43
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
if x.empty?
|
159
|
-
raise MultiJson::DecodeError.new("Empty JSON string", [], "")
|
160
|
-
else
|
161
|
-
MultiJson.load(x, :symbolize_keys => true)
|
162
|
-
end
|
44
|
+
def trace=(trace)
|
45
|
+
@rest_client.trace = trace
|
46
|
+
@uaa.trace = trace if @uaa
|
163
47
|
end
|
164
48
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
content
|
169
|
-
when :json
|
170
|
-
"application/json"
|
171
|
-
when :form
|
172
|
-
"application/x-www-form-urlencoded"
|
173
|
-
when nil
|
174
|
-
nil
|
175
|
-
# return request headers (not really Accept)
|
176
|
-
else
|
177
|
-
raise CFoundry::Error, "Unknown mimetype '#{content.inspect}'"
|
178
|
-
end
|
49
|
+
# Cloud metadata
|
50
|
+
def info
|
51
|
+
get("info", :accept => :json)
|
179
52
|
end
|
180
53
|
|
181
|
-
def
|
182
|
-
|
183
|
-
v = hash[k]
|
184
|
-
v = MultiJson.dump(v) if v.is_a?(Hash)
|
185
|
-
v = URI.escape(v.to_s, /[^#{URI::PATTERN::UNRESERVED}]/) if escape
|
186
|
-
"#{k}=#{v}"
|
187
|
-
end.join("&")
|
54
|
+
def get(*args)
|
55
|
+
request("GET", *args)
|
188
56
|
end
|
189
57
|
|
190
|
-
def
|
191
|
-
|
192
|
-
|
193
|
-
request_path(method, url(path), options)
|
58
|
+
def delete(*args)
|
59
|
+
request("DELETE", *args)
|
194
60
|
end
|
195
61
|
|
196
|
-
def
|
197
|
-
|
62
|
+
def post(*args)
|
63
|
+
request("POST", *args)
|
198
64
|
end
|
199
65
|
|
200
|
-
def
|
201
|
-
|
66
|
+
def put(*args)
|
67
|
+
request("PUT", *args)
|
202
68
|
end
|
203
69
|
|
204
|
-
def
|
205
|
-
|
70
|
+
def request(method, *args)
|
71
|
+
path, options = normalize_arguments(args)
|
72
|
+
request, response = request_raw(method, path, options)
|
73
|
+
handle_response(response, options, request)
|
206
74
|
end
|
207
75
|
|
208
|
-
def
|
209
|
-
|
76
|
+
def request_raw(method, path, options)
|
77
|
+
@rest_client.request(method, path, options)
|
210
78
|
end
|
211
79
|
|
212
|
-
|
213
|
-
"/#{safe_path(segments)}"
|
214
|
-
end
|
80
|
+
private
|
215
81
|
|
216
|
-
def
|
217
|
-
|
218
|
-
URI.encode x.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
219
|
-
}.join("/")
|
82
|
+
def status_is_successful?(code)
|
83
|
+
(code >= 200) && (code < 400)
|
220
84
|
end
|
221
85
|
|
222
|
-
def
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
},
|
229
|
-
:response => {
|
230
|
-
:code => response.code,
|
231
|
-
:headers => sane_headers(response)
|
232
|
-
}
|
233
|
-
}
|
86
|
+
def handle_response(response, options, request)
|
87
|
+
if status_is_successful?(response[:status].to_i)
|
88
|
+
handle_successful_response(response, options)
|
89
|
+
else
|
90
|
+
handle_error_response(response, request)
|
91
|
+
end
|
234
92
|
end
|
235
93
|
|
236
|
-
def
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
94
|
+
def handle_successful_response(response, options)
|
95
|
+
if options[:return_response]
|
96
|
+
response
|
97
|
+
elsif options[:accept] == :json
|
98
|
+
parse_json(response[:body])
|
99
|
+
else
|
100
|
+
response[:body]
|
101
|
+
end
|
244
102
|
end
|
245
103
|
|
246
|
-
def
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
case @log
|
252
|
-
when IO
|
253
|
-
log_line(@log, data)
|
254
|
-
return
|
255
|
-
when String
|
256
|
-
if File.exists?(@log)
|
257
|
-
log = File.readlines(@log).last(LOG_LENGTH - 1)
|
258
|
-
elsif !File.exists?(File.dirname(@log))
|
259
|
-
FileUtils.mkdir_p(File.dirname(@log))
|
260
|
-
end
|
104
|
+
def handle_error_response(response, request)
|
105
|
+
body_json = parse_json(response[:body])
|
106
|
+
body_code = body_json && body_json[:code]
|
107
|
+
code = body_code || response[:status].to_i
|
261
108
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
end
|
266
|
-
|
267
|
-
return
|
268
|
-
end
|
269
|
-
|
270
|
-
if @log.respond_to?(:call)
|
271
|
-
@log.call(data)
|
272
|
-
return
|
109
|
+
if body_code
|
110
|
+
error_class = CFoundry::APIError.error_classes[body_code] || CFoundry::APIError
|
111
|
+
raise error_class.new(body_json[:description], body_code, request, response)
|
273
112
|
end
|
274
113
|
|
275
|
-
|
276
|
-
|
277
|
-
|
114
|
+
case code
|
115
|
+
when 404
|
116
|
+
raise CFoundry::NotFound.new(nil, code, request, response)
|
117
|
+
when 403
|
118
|
+
raise CFoundry::Denied.new(nil, code, request, response)
|
119
|
+
else
|
120
|
+
raise CFoundry::BadResponse.new(nil, code, request, response)
|
278
121
|
end
|
279
122
|
end
|
280
123
|
|
281
|
-
def
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
def print_response(response)
|
287
|
-
$stderr.puts response_trace(response)
|
288
|
-
$stderr.puts "<<<"
|
289
|
-
end
|
290
|
-
|
291
|
-
def print_backtrace(locs)
|
292
|
-
return unless @backtrace
|
293
|
-
|
294
|
-
interesting_locs = locs.drop_while { |loc|
|
295
|
-
loc =~ /\/(cfoundry\/|restclient\/|net\/http)/
|
296
|
-
}
|
297
|
-
|
298
|
-
$stderr.puts "--- backtrace:"
|
299
|
-
|
300
|
-
$stderr.puts "... (boring)" unless locs == interesting_locs
|
301
|
-
|
302
|
-
trimmed_locs = interesting_locs[0..5]
|
303
|
-
|
304
|
-
trimmed_locs.each do |loc|
|
305
|
-
$stderr.puts "=== #{loc}"
|
124
|
+
def normalize_arguments(args)
|
125
|
+
if args.last.is_a?(Hash)
|
126
|
+
options = args.pop
|
127
|
+
else
|
128
|
+
options = {}
|
306
129
|
end
|
307
130
|
|
308
|
-
|
131
|
+
[normalize_path(args), options]
|
309
132
|
end
|
310
133
|
|
311
|
-
|
312
|
-
case response
|
313
|
-
when Net::HTTPSuccess, Net::HTTPRedirection
|
314
|
-
accept == :json ? parse_json(response.body) : response.body
|
315
|
-
|
316
|
-
when Net::HTTPNotFound
|
317
|
-
raise CFoundry::NotFound.new(request, response)
|
318
|
-
|
319
|
-
when Net::HTTPForbidden
|
320
|
-
raise CFoundry::Denied.new(request, response)
|
134
|
+
URI_ENCODING_PATTERN = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
321
135
|
|
322
|
-
|
323
|
-
|
136
|
+
def normalize_path(segments)
|
137
|
+
if segments.size == 1 && segments.first =~ /^\//
|
138
|
+
segments.first
|
139
|
+
else
|
140
|
+
segments.flatten.collect { |x|
|
141
|
+
URI.encode(x.to_s, URI_ENCODING_PATTERN)
|
142
|
+
}.join("/")
|
324
143
|
end
|
325
144
|
end
|
326
145
|
|
327
|
-
def
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
146
|
+
def parse_json(x)
|
147
|
+
if x.empty?
|
148
|
+
raise MultiJson::DecodeError.new("Empty JSON string", [], "")
|
149
|
+
else
|
150
|
+
MultiJson.load(x, :symbolize_keys => true)
|
332
151
|
end
|
333
|
-
|
334
|
-
|
152
|
+
rescue MultiJson::DecodeError
|
153
|
+
nil
|
335
154
|
end
|
336
155
|
end
|
337
156
|
end
|