nexus_api 1.0.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/.env.template +5 -0
- data/.gitignore +17 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +68 -0
- data/LICENSE.txt +21 -0
- data/README.md +185 -0
- data/Rakefile +15 -0
- data/bin/nexus_api +7 -0
- data/bin/setup +8 -0
- data/bin/test +2 -0
- data/lib/nexus_api/cli.rb +29 -0
- data/lib/nexus_api/cli_commands/commands.rb +6 -0
- data/lib/nexus_api/cli_commands/download.rb +23 -0
- data/lib/nexus_api/cli_commands/list.rb +66 -0
- data/lib/nexus_api/cli_commands/script.rb +38 -0
- data/lib/nexus_api/cli_commands/search.rb +34 -0
- data/lib/nexus_api/cli_commands/tag.rb +47 -0
- data/lib/nexus_api/cli_commands/upload.rb +126 -0
- data/lib/nexus_api/cli_utils.rb +64 -0
- data/lib/nexus_api/config_manager.rb +53 -0
- data/lib/nexus_api/docker_manager.rb +112 -0
- data/lib/nexus_api/docker_shell.rb +32 -0
- data/lib/nexus_api/nexus_connection.rb +145 -0
- data/lib/nexus_api/version.rb +4 -0
- data/lib/nexus_api.rb +372 -0
- data/nexus_api.gemspec +35 -0
- data/team_configs/default.yaml +6 -0
- data/team_configs/template.yaml +18 -0
- metadata +189 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
module NexusAPI
|
2
|
+
class Upload < ::Thor
|
3
|
+
attr_accessor :api
|
4
|
+
|
5
|
+
include NexusAPI::CLIUtils
|
6
|
+
|
7
|
+
desc 'docker', 'Upload a docker image'
|
8
|
+
option :image, :aliases => '-i', :desc => 'Docker image to upload', :required => true
|
9
|
+
option :docker_tag, :aliases => '-t', :desc => 'Docker tag', :required => true
|
10
|
+
def docker
|
11
|
+
setup
|
12
|
+
if_file_exists?(file: options[:image].to_s + ':' + options[:docker_tag].to_s, repository: ENV['DOCKER_PUSH_HOSTNAME']) do
|
13
|
+
@api.upload_docker_component(
|
14
|
+
image: options[:image],
|
15
|
+
tag: options[:docker_tag]
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc 'maven', 'Upload a maven file'
|
21
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
22
|
+
option :group_id, :aliases => '-g', :desc => 'Maven groupId for file', :required => true
|
23
|
+
option :artifact_id, :aliases => '-a', :desc => 'Maven artifactId for file', :required => true
|
24
|
+
option :version, :aliases => '-v', :desc => 'File version', :required => true
|
25
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
26
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
27
|
+
def maven
|
28
|
+
return false unless repository_set?
|
29
|
+
set(repository: :maven_repository)
|
30
|
+
if_file_exists? do
|
31
|
+
@api.upload_maven_component(
|
32
|
+
filename: options[:filename],
|
33
|
+
group_id: options[:group_id],
|
34
|
+
artifact_id: options[:artifact_id],
|
35
|
+
version: options[:version],
|
36
|
+
repository: options[:repository],
|
37
|
+
tag: options[:tag],
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
desc 'npm', 'Upload a npm file'
|
43
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
44
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
45
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
46
|
+
def npm
|
47
|
+
return false unless repository_set?
|
48
|
+
set(repository: :npm_repository)
|
49
|
+
if_file_exists? do
|
50
|
+
@api.upload_npm_component(
|
51
|
+
filename: options[:filename],
|
52
|
+
repository: options[:repository],
|
53
|
+
tag: options[:tag],
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'pypi', 'Upload a pypi file'
|
59
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
60
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
61
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
62
|
+
def pypi
|
63
|
+
return false unless repository_set?
|
64
|
+
set(repository: :pypi_repository)
|
65
|
+
if_file_exists? do
|
66
|
+
@api.upload_pypi_component(
|
67
|
+
filename: options[:filename],
|
68
|
+
repository: options[:repository],
|
69
|
+
tag: options[:tag],
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'raw', 'Upload a raw file'
|
75
|
+
option :filename, :aliases => '-f', :desc => 'Filename', :required => true
|
76
|
+
option :directory, :aliases => '-d', :desc => 'Path to file', :required => true
|
77
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
78
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
79
|
+
def raw
|
80
|
+
return false unless repository_set?
|
81
|
+
set(repository: :raw_repository)
|
82
|
+
if_file_exists? do
|
83
|
+
@api.upload_raw_component(
|
84
|
+
filename: options[:filename],
|
85
|
+
directory: options[:directory],
|
86
|
+
repository: options[:repository],
|
87
|
+
tag: options[:tag],
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc 'rubygems', 'Upload a rubygems file'
|
93
|
+
option :filename, :aliases => '-f', :desc => 'Path of file', :required => true
|
94
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
95
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
96
|
+
def rubygems
|
97
|
+
return false unless repository_set?
|
98
|
+
set(repository: :rubygems_repository)
|
99
|
+
if_file_exists? do
|
100
|
+
@api.upload_rubygems_component(
|
101
|
+
filename: options[:filename],
|
102
|
+
repository: options[:repository],
|
103
|
+
tag: options[:tag],
|
104
|
+
)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
desc 'yum', 'Upload a yum file'
|
109
|
+
option :filename, :aliases => '-f', :desc => 'Filename', :required => true
|
110
|
+
option :directory, :aliases => '-d', :desc => 'Path to file', :required => true
|
111
|
+
option :repository, :aliases => '-r', :desc => 'Repository name to upload file to; Overrides -e/--team-config; Required if -e not provided'
|
112
|
+
option :tag, :aliases => '-t', :desc => 'Tag to add to file (tag MUST already exist!)'
|
113
|
+
def yum
|
114
|
+
return false unless repository_set?
|
115
|
+
set(repository: :yum_repository)
|
116
|
+
if_file_exists? do
|
117
|
+
@api.upload_yum_component(
|
118
|
+
filename: options[:filename],
|
119
|
+
directory: options[:directory],
|
120
|
+
repository: options[:repository],
|
121
|
+
tag: options[:tag],
|
122
|
+
)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'dotenv'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
module CLIUtils
|
5
|
+
def setup
|
6
|
+
Dotenv.load(options[:nexus_config])
|
7
|
+
@api = NexusAPI::API.new(
|
8
|
+
username: ENV['NEXUS_USERNAME'],
|
9
|
+
password: ENV['NEXUS_PASSWORD'],
|
10
|
+
hostname: ENV['NEXUS_HOSTNAME'],
|
11
|
+
docker_pull_hostname: ENV['DOCKER_PULL_HOSTNAME'],
|
12
|
+
docker_push_hostname: ENV['DOCKER_PUSH_HOSTNAME'],
|
13
|
+
team_config: options[:team_config]
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_element(action:, params:, filter:)
|
18
|
+
setup
|
19
|
+
element = @api.send(action, params)
|
20
|
+
puts options[:full] ? element : element[filter]
|
21
|
+
end
|
22
|
+
|
23
|
+
def print_paginating_set(action:, params:, filter:, proc: nil)
|
24
|
+
setup
|
25
|
+
set = Array.new.tap do |set|
|
26
|
+
loop do
|
27
|
+
params[:paginate] = true
|
28
|
+
set.concat(Array(@api.send(action, params)))
|
29
|
+
break unless @api.paginate?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
proc = proc { set.map{ |element| element[filter] } } if proc.nil?
|
33
|
+
puts options[:full] ? set : proc.call(set)
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_set(action:, filter:)
|
37
|
+
setup
|
38
|
+
set = @api.send(action)
|
39
|
+
puts options[:full] ? set : set.map{ |element| element[filter] }
|
40
|
+
end
|
41
|
+
|
42
|
+
def repository_set?
|
43
|
+
if options[:repository].nil? && options[:team_config].nil?
|
44
|
+
puts "No value provided for required option '--repository' or '--team_config' (only need 1)"
|
45
|
+
return false
|
46
|
+
end
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def set(repository:)
|
51
|
+
setup
|
52
|
+
options[:repository] = @api.team_config.send(repository) if options[:repository].nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def if_file_exists?(file: options[:filename], repository: options[:repository])
|
56
|
+
begin
|
57
|
+
puts "Sending '#{file}' to the '#{repository}' repository in Nexus!"
|
58
|
+
yield
|
59
|
+
rescue Errno::ENOENT
|
60
|
+
puts "'#{file}' does not exist locally."
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class ConfigManager
|
5
|
+
def initialize(config_path:)
|
6
|
+
if File.exist?(config_path)
|
7
|
+
@config = YAML.safe_load(File.read(config_path)) || {}
|
8
|
+
else
|
9
|
+
raise "ERROR: Specified config '#{config_path}' does not exist."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def assets_repository
|
14
|
+
@config['assets']
|
15
|
+
end
|
16
|
+
|
17
|
+
def components_repository
|
18
|
+
@config['components']
|
19
|
+
end
|
20
|
+
|
21
|
+
def search_repository
|
22
|
+
@config['search']
|
23
|
+
end
|
24
|
+
|
25
|
+
def tag_repository
|
26
|
+
@config['tag']
|
27
|
+
end
|
28
|
+
|
29
|
+
def maven_repository
|
30
|
+
@config['maven']
|
31
|
+
end
|
32
|
+
|
33
|
+
def npm_repository
|
34
|
+
@config['npm']
|
35
|
+
end
|
36
|
+
|
37
|
+
def pypi_repository
|
38
|
+
@config['pypi']
|
39
|
+
end
|
40
|
+
|
41
|
+
def raw_repository
|
42
|
+
@config['raw']
|
43
|
+
end
|
44
|
+
|
45
|
+
def rubygems_repository
|
46
|
+
@config['rubygems']
|
47
|
+
end
|
48
|
+
|
49
|
+
def yum_repository
|
50
|
+
@config['yum']
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'nexus_api/docker_shell'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class DockerManager
|
5
|
+
|
6
|
+
def initialize(docker:, options:)
|
7
|
+
@docker = docker
|
8
|
+
@username = options['username']
|
9
|
+
@password = options['password']
|
10
|
+
@pull_host = options['pull_host']
|
11
|
+
@push_host = options['push_host']
|
12
|
+
end
|
13
|
+
|
14
|
+
def download(image_name:, tag:)
|
15
|
+
return false unless docker_valid?
|
16
|
+
image_name = image_name(@pull_host, image_name, tag)
|
17
|
+
begin
|
18
|
+
image = @docker.pull_image(@username, @password, image_name)
|
19
|
+
rescue Docker::Error::NotFoundError => error
|
20
|
+
puts "ERROR: Failed to pull Docker image #{image_name}.\nDoes it exist in Nexus?"
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def upload(image_name:, tag:)
|
27
|
+
return false unless docker_valid?
|
28
|
+
begin
|
29
|
+
return false unless login
|
30
|
+
image = find_image(image_name(@pull_host, image_name, tag))
|
31
|
+
return false if image.nil?
|
32
|
+
tag(image, @push_host, image_name, tag)
|
33
|
+
image.push(nil, repo_tag: image_name(@push_host, image_name, tag))
|
34
|
+
rescue StandardError => error
|
35
|
+
puts "ERROR: #{error.inspect}"
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
def exists?(image_name:, tag:)
|
42
|
+
return false unless docker_valid?
|
43
|
+
images = find_images(image_name(@pull_host, image_name, tag))
|
44
|
+
return false if images.empty?
|
45
|
+
true
|
46
|
+
end
|
47
|
+
|
48
|
+
def delete(image_name:, tag:)
|
49
|
+
return false unless docker_valid?
|
50
|
+
begin
|
51
|
+
image = find_image(image_name(@pull_host, image_name, tag))
|
52
|
+
return false if image.nil?
|
53
|
+
return false unless image.remove(:force => true)
|
54
|
+
rescue StandardError => error
|
55
|
+
puts "ERROR: #{error.inspect}"
|
56
|
+
return false
|
57
|
+
end
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def docker_valid?
|
64
|
+
return true if @docker.validate_version!
|
65
|
+
puts 'ERROR: Your installed version of the Docker API is not supported by the docker-api gem!'
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
def image_name(host, name, tag)
|
70
|
+
"#{host}/#{name}:#{tag}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def login
|
74
|
+
return true if @docker.authenticate!(@username, @password, @push_host)
|
75
|
+
puts "ERROR: Failed to authenticate to #{@push_host} as #{@username}"
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
def find_images(name)
|
80
|
+
@docker.list_images.select do |image|
|
81
|
+
image.info['RepoTags'].include?(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def valid?(images)
|
86
|
+
if images.empty?
|
87
|
+
puts 'ERROR: No matching docker images found'
|
88
|
+
return false
|
89
|
+
end
|
90
|
+
if images.count > 1
|
91
|
+
puts "ERROR: Found multiple images that match: #{images.map {|image| image.info['RepoTags']} }"
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
true
|
95
|
+
end
|
96
|
+
|
97
|
+
def find_image(name)
|
98
|
+
images = find_images(name)
|
99
|
+
if valid?(images)
|
100
|
+
images.first
|
101
|
+
else
|
102
|
+
nil
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def tag(image, host, name, tag)
|
107
|
+
unless image.info['RepoTags'].include?(image_name(host, name, tag))
|
108
|
+
image.tag('repo'=>"#{host}/#{name}", 'tag'=>tag)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'docker'
|
2
|
+
|
3
|
+
module NexusAPI
|
4
|
+
class DockerShell
|
5
|
+
def validate_version!
|
6
|
+
Docker.validate_version!
|
7
|
+
end
|
8
|
+
|
9
|
+
def pull_image(username, password, image_name)
|
10
|
+
Docker::Image.create(
|
11
|
+
'username' => username,
|
12
|
+
'password' => password,
|
13
|
+
'fromImage' => image_name
|
14
|
+
)
|
15
|
+
rescue Docker::Error::ClientError => error
|
16
|
+
puts "Error: Could not pull Docker image '#{image_name}'"
|
17
|
+
puts error
|
18
|
+
end
|
19
|
+
|
20
|
+
def authenticate!(username, password, host)
|
21
|
+
Docker.authenticate!(
|
22
|
+
'username' => username,
|
23
|
+
'password' => password,
|
24
|
+
'serveraddress' => host
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def list_images
|
29
|
+
Docker::Image.all
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'rest-client'
|
3
|
+
|
4
|
+
module NexusAPI
|
5
|
+
class NexusConnection
|
6
|
+
VALID_RESPONSE_CODES = [200, 204].freeze
|
7
|
+
|
8
|
+
attr_accessor :continuation_token
|
9
|
+
|
10
|
+
def initialize(username:, password:, hostname:)
|
11
|
+
@username = username
|
12
|
+
@password = password
|
13
|
+
@hostname = hostname
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_response(endpoint:, paginate: false, headers: {'Content-Type' => 'application/json'})
|
17
|
+
response = send_get(endpoint, paginate, headers)
|
18
|
+
response.nil? ? Hash.new : jsonize(response)
|
19
|
+
end
|
20
|
+
|
21
|
+
def get(endpoint:, paginate: false, headers: {'Content-Type' => 'application/json'})
|
22
|
+
valid?(send_get(endpoint, paginate, headers))
|
23
|
+
end
|
24
|
+
|
25
|
+
def post(endpoint:, parameters: '', headers: {'Content-Type' => 'application/json'})
|
26
|
+
response = send_request(
|
27
|
+
:post,
|
28
|
+
endpoint,
|
29
|
+
parameters: parameters,
|
30
|
+
headers: headers
|
31
|
+
)
|
32
|
+
valid?(response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def put(endpoint:, parameters: '', headers: {'Content-Type' => 'application/json'})
|
36
|
+
response = send_request(
|
37
|
+
:put,
|
38
|
+
endpoint,
|
39
|
+
parameters: parameters,
|
40
|
+
headers: headers
|
41
|
+
)
|
42
|
+
valid?(response)
|
43
|
+
end
|
44
|
+
|
45
|
+
def delete(endpoint:, headers: {'Content-Type' => 'application/json'})
|
46
|
+
response = send_request(
|
47
|
+
:delete,
|
48
|
+
endpoint,
|
49
|
+
headers: headers
|
50
|
+
)
|
51
|
+
valid?(response)
|
52
|
+
end
|
53
|
+
|
54
|
+
def head(asset_url:)
|
55
|
+
catch_connection_error do
|
56
|
+
RestClient.head(asset_url)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def content_length(asset_url:)
|
61
|
+
response = head(asset_url: asset_url)
|
62
|
+
return -1 unless response.respond_to?(:headers)
|
63
|
+
response.headers[:content_length]
|
64
|
+
end
|
65
|
+
|
66
|
+
def download(url:)
|
67
|
+
catch_connection_error do
|
68
|
+
RestClient.get(url, authorization_header)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def paginate?
|
73
|
+
!@continuation_token.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def valid?(response)
|
80
|
+
return false if response.nil?
|
81
|
+
VALID_RESPONSE_CODES.include?(response.code) ? true : false
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle(error)
|
85
|
+
puts "ERROR: Request failed"
|
86
|
+
puts error.description if error.is_a?(RestClient::Response)
|
87
|
+
end
|
88
|
+
|
89
|
+
def catch_connection_error
|
90
|
+
begin
|
91
|
+
yield
|
92
|
+
rescue SocketError => error
|
93
|
+
return handle(error)
|
94
|
+
rescue RestClient::Unauthorized => error
|
95
|
+
return handle(error)
|
96
|
+
rescue RestClient::ExceptionWithResponse => error
|
97
|
+
return handle(error.response)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def authorization_header
|
102
|
+
{ :Authorization => 'Basic ' + Base64.strict_encode64( "#{@username}:#{@password}" ) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def send_request(connection_method, endpoint, parameters: '', headers: {})
|
106
|
+
catch_connection_error do
|
107
|
+
RestClient::Request.execute(
|
108
|
+
method: connection_method,
|
109
|
+
url: "https://#{@hostname}/service/rest/v1/#{endpoint}",
|
110
|
+
payload: parameters,
|
111
|
+
headers: authorization_header.merge(headers)
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def send_get(endpoint, paginate, headers)
|
117
|
+
# paginate answers is the user requesting pagination, paginate? answers does a continuation token exist
|
118
|
+
# if an empty continuation token is included in the request we'll get an ArrayIndexOutOfBoundsException
|
119
|
+
endpoint += "&continuationToken=#{@continuation_token}" if paginate && paginate?
|
120
|
+
response = send_request(
|
121
|
+
:get,
|
122
|
+
endpoint,
|
123
|
+
headers: headers
|
124
|
+
)
|
125
|
+
end
|
126
|
+
|
127
|
+
# That's right, nexus has inconsistent null values for its api
|
128
|
+
def continuation_token_for(json)
|
129
|
+
return nil if json['continuationToken'].nil?
|
130
|
+
return nil if json['continuationToken'] == 'nil'
|
131
|
+
json['continuationToken']
|
132
|
+
end
|
133
|
+
|
134
|
+
def jsonize(response)
|
135
|
+
json = JSON.parse(response.body)
|
136
|
+
if json.class == Hash
|
137
|
+
@continuation_token = continuation_token_for(json)
|
138
|
+
json = json["items"] if json["items"]
|
139
|
+
end
|
140
|
+
json
|
141
|
+
rescue JSON::ParserError
|
142
|
+
response.body
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|