artifactory 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +11 -7
  3. data/CHANGELOG.md +18 -0
  4. data/README.md +25 -4
  5. data/Rakefile +5 -2
  6. data/artifactory.gemspec +0 -3
  7. data/lib/artifactory.rb +2 -4
  8. data/lib/artifactory/client.rb +210 -85
  9. data/lib/artifactory/configurable.rb +6 -1
  10. data/lib/artifactory/defaults.rb +52 -3
  11. data/lib/artifactory/errors.rb +21 -14
  12. data/lib/artifactory/resources/artifact.rb +132 -58
  13. data/lib/artifactory/resources/base.rb +120 -6
  14. data/lib/artifactory/resources/build.rb +2 -1
  15. data/lib/artifactory/resources/group.rb +5 -72
  16. data/lib/artifactory/resources/layout.rb +106 -0
  17. data/lib/artifactory/resources/repository.rb +62 -114
  18. data/lib/artifactory/resources/system.rb +5 -1
  19. data/lib/artifactory/resources/user.rb +5 -79
  20. data/lib/artifactory/util.rb +8 -2
  21. data/lib/artifactory/version.rb +1 -1
  22. data/spec/integration/resources/layout_spec.rb +22 -0
  23. data/spec/integration/resources/repository_spec.rb +7 -0
  24. data/spec/integration/resources/system_spec.rb +4 -4
  25. data/spec/support/api_server/repository_endpoints.rb +5 -0
  26. data/spec/support/api_server/system_endpoints.rb +18 -0
  27. data/spec/unit/client_spec.rb +17 -98
  28. data/spec/unit/resources/artifact_spec.rb +99 -13
  29. data/spec/unit/resources/build_spec.rb +1 -1
  30. data/spec/unit/resources/group_spec.rb +1 -1
  31. data/spec/unit/resources/layout_spec.rb +61 -0
  32. data/spec/unit/resources/repository_spec.rb +31 -47
  33. data/spec/unit/resources/system_spec.rb +4 -2
  34. data/spec/unit/resources/user_spec.rb +1 -1
  35. metadata +16 -40
  36. data/locales/en.yml +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6b8e07b7674ce29c0b313ce8d5be4a551fd007ac
4
- data.tar.gz: 37eec0ba96fdb294d4e285977cc0b8d8aec7579a
3
+ metadata.gz: f140751bb7b38da3e211b0c3fbd7332bb60754f3
4
+ data.tar.gz: cf32023c6d7ade23e0237828d4a2f25f6c4ed8aa
5
5
  SHA512:
6
- metadata.gz: e2b460025528de4d36e1addf26d2b32656c05bc68656143c06524afe0ac6bcde325ed85c3f1c63035cf76294d99fe41fe2e474e6a1460984e6885a099ee0218e
7
- data.tar.gz: 6e00717ca7b4a2e90185adc6677eb10fa762c912f9406e5003a15c02be13b44146b901399d8fce08f8ec43a268cc8761fe53abe4bec6ca6f872957794adac792
6
+ metadata.gz: bbc1d3688eee73f034dd8f39c62ad086555aea4345636f2654f428b5d1f89e4b92ee02fbc554187cb42057eaa6d02eb694a82f60afac33cd00987fc033406363
7
+ data.tar.gz: 139a0c2ac153c5440dae9d6893b081ad7730139f621ce36c393af81cb89d0a116bd6ff85d9395dd2a43017baa0bf2fbc03c811bc01edf41894298ceab4203d4d
@@ -1,20 +1,24 @@
1
- # Test on modern rubies
2
1
  rvm:
3
2
  - 1.9.3
4
3
  - 2.0.0
5
- - 2.1.0
4
+ - 2.1
6
5
 
7
- # Setup parallel downloaders
8
6
  bundler_args: --jobs 7
9
7
 
10
- # Only test master (PRs are still tested)
11
8
  branches:
12
9
  only:
13
10
  - master
14
11
 
15
- # Notifications
12
+ script: bundle exec rake travis:ci
13
+
16
14
  notifications:
17
15
  hipchat:
16
+ on_change: true
17
+ on_failure: true
18
+ on_success: false
19
+ on_pull_requests: false
18
20
  rooms:
19
- - secure: eUtMt0CeB2bbIcTeQA3rUJsTtBkq/Ng5AnM59NOCzutcPTnNDeM4kNQEfgJ5QEZZkx08IXyiPJ14gEQUaV0bF0s0FLyzFxjlX9p2jem99FmEA404KJ7axoUiGGxgATbq9V7tUOrp1zG+a8xVQTahZASKadPlC9TSRgKqjUL+Dvg= # Release Engineering
20
- - secure: fQEeBZSIV9mKpM5XeKxmvcgUb19skv3+qW6TiyvuPReB4B/mc5vf3AABWhbdnnHzjsKnq2iQtcppXVG0eJwuRM5D1werMO91zwSRNBU/ClOLHaYBp5l4tVkuHzAd3eo/Z67vxE0BAV9lGWeWv7r3y/BLgJ265nQMyfgZSyz1i44= # Build Statuses
21
+ # Build Statuses
22
+ - secure: fQEeBZSIV9mKpM5XeKxmvcgUb19skv3+qW6TiyvuPReB4B/mc5vf3AABWhbdnnHzjsKnq2iQtcppXVG0eJwuRM5D1werMO91zwSRNBU/ClOLHaYBp5l4tVkuHzAd3eo/Z67vxE0BAV9lGWeWv7r3y/BLgJ265nQMyfgZSyz1i44=
23
+ # Release Engineering
24
+ - secure: eUtMt0CeB2bbIcTeQA3rUJsTtBkq/Ng5AnM59NOCzutcPTnNDeM4kNQEfgJ5QEZZkx08IXyiPJ14gEQUaV0bF0s0FLyzFxjlX9p2jem99FmEA404KJ7axoUiGGxgATbq9V7tUOrp1zG+a8xVQTahZASKadPlC9TSRgKqjUL+Dvg=
@@ -3,6 +3,24 @@ Artifactory Client CHANGELOG
3
3
  This file is used to document the changes between releases of the Artifactory
4
4
  Ruby client.
5
5
 
6
+ v1.2.0 (2014-06-02)
7
+ -------------------
8
+ - Change the airty of Repository#find to align with other resources
9
+ - Add the ability to save/create repositories
10
+ - Remove i18n
11
+ - Make proxy configuration more verbose
12
+ - Remove HTTPClient in favor of raw Net::HTTP
13
+ - Add custom SSL configuration options
14
+ - Make Configurable#proxy_port a string because #ocd
15
+ - Allow file uploads
16
+ - Return an Artifact object after uploading
17
+ - Allow repositories to be deleted
18
+ - Add required attribute for repository layout
19
+ - Move upload method from Repository to Artifact
20
+ - Implement Repository.upload via Artifact.upload
21
+ - Move to_matrix_properties method to base class
22
+ - Add the ability to list/find repository layouts from xml configuration
23
+ - Specify content-type for updating config
6
24
 
7
25
  v1.1.0 (2014-02-11)
8
26
  -------------------
data/README.md CHANGED
@@ -6,6 +6,8 @@ A Ruby client and interface to the Artifactory API. **The majority of API endpoi
6
6
 
7
7
  The Artifactory gem offers a convienent interface for managing various parts of the Artifactory API. It is not a complete API implementation, and should still be considered a work in progress.
8
8
 
9
+ This project is managed by the CHEF Release Engineering team. For more information on the Release Engineering team's contribution, triage, and release process, please consult the [CHEF Release Engineering OSS Management Guide](https://docs.google.com/a/opscode.com/document/d/1oJB0vZb_3bl7_ZU2YMDBkMFdL-EWplW1BJv_FXTUOzg/edit).
10
+
9
11
 
10
12
  Quick start
11
13
  -----------
@@ -54,9 +56,20 @@ Artifactory.configure do |config|
54
56
  config.username = 'admin'
55
57
  config.password = 'password'
56
58
 
59
+ # Speaking of SSL, you can specify the path to a pem file with your custom
60
+ # certificates and the gem will wire it all up for you (NOTE: it must be a
61
+ # valid PEM file).
62
+ config.ssl_pem_file = '/path/to/my.pem'
63
+
64
+ # Or if you are feelying frisky, you can always disable SSL verification
65
+ config.ssl_verify = false
66
+
57
67
  # You can specify any proxy information, including any authentication
58
68
  # information in the URL.
59
- config.proxy = 'https://user:password@my.proxy.server'
69
+ config.proxy_username = 'user'
70
+ config.proxy_password = 'password'
71
+ config.proxy_address = 'my.proxy.server'
72
+ config.proxy_port = '8080'
60
73
  end
61
74
  ```
62
75
 
@@ -73,6 +86,7 @@ Or, if you want to be really Unixy, these parameters are all configurable via en
73
86
  export ARTIFACTORY_ENDPOINT=http://my.storage.server/artifactory
74
87
  export ARTIFACTORY_USERNAME=admin
75
88
  export ARTIFACTORY_PASSWORD=password
89
+ export ARTIFACTORY_SSL_PEM_FILE=/path/to/my.pem
76
90
  ```
77
91
 
78
92
  You can also create a full `Client` object with hash parameters:
@@ -82,10 +96,13 @@ client = Artifactory::Client.new(endpoint: '...', username: '...')
82
96
  ```
83
97
 
84
98
  ### Making requests
85
- The Artifactory gem attempts to make the Artifactory API as object-oriented and Ruby-like as possible. All of the methods and API calls are heavily documented with examples inline using YARD. In order to keep the examples versioned with the code, the README only lists a few examples for using the Artifactory gem. Please see the inline documentation for the full API documentation.
99
+ The Artifactory gem attempts to make the Artifactory API as object-oriented and Ruby-like as possible. All of the methods and API calls are heavily documented with examples inline using YARD. In order to keep the examples versioned with the code, the README only lists a few examples for using the Artifactory gem. Please see the inline documentation for the full API documentation. The tests in the 'spec' directory are an additional source of examples.
86
100
 
87
101
  #### Artifacts
88
102
  ```ruby
103
+ # Upload an artifact to a repository whose key is 'repo_key'
104
+ artifact.upload('/local/path/to/file', 'repo_key', param_1: 'foo')
105
+
89
106
  # Search for an artifact by name
90
107
  artifact = Artifact.search(name: 'package.deb').first
91
108
  artifact #=> "#<Artifactory::Resource::Artifact md5: 'ABCD1234'>"
@@ -123,6 +140,10 @@ repo #=> #<Artifactory::Resource::Repository ...>
123
140
  # Get information about the repository
124
141
  repo.description => "The default storage mechanism for..."
125
142
 
143
+ # Change the repository
144
+ repo.description = "This is a new description"
145
+ repo.save
146
+
126
147
  # Upload an artifact to the repo
127
148
  repo.upload('/local/path/to/file', param_1: 'foo', param_2: 'bar')
128
149
 
@@ -155,8 +176,8 @@ client.get('/some/special/path', param_1: 'foo', param_2: 'bar')
155
176
 
156
177
  For more information on the methods available, please see the [`Artifactory::Client` class](https://github.com/opscode/artifactory-client/blob/master/lib/artifactory/client.rb).
157
178
 
158
- ### Theadsafey
159
- If you plan to use the Artifactory gem in a library, you should be aware that _certain_ pathways for accessing resources is **not** threadsafe. In order to deliver a "Rails-like" experience, accessing a resource without a client object uses a global shared state. Other threads may modify this state, and therefore we do **not** recommend using the Rails-like approach if you are concerned about threadsafety. The following code snippet may better explain the differences:
179
+ ### Threadsafety
180
+ If you plan to use the Artifactory gem in a library, you should be aware that _certain_ pathways for accessing resources are **not** threadsafe. In order to deliver a "Rails-like" experience, accessing a resource without a client object uses a global shared state. Other threads may modify this state, and therefore we do **not** recommend using the Rails-like approach if you are concerned about threadsafety. The following code snippet may better explain the differences:
160
181
 
161
182
  ```ruby
162
183
  # In our current thread...
data/Rakefile CHANGED
@@ -1,6 +1,9 @@
1
1
  require 'bundler/gem_tasks'
2
2
 
3
3
  require 'rspec/core/rake_task'
4
- RSpec::Core::RakeTask.new(:spec)
4
+ RSpec::Core::RakeTask.new(:unit)
5
5
 
6
- task default: :spec
6
+ namespace :travis do
7
+ desc 'Run tests on Travis'
8
+ task :ci => [:unit]
9
+ end
@@ -20,9 +20,6 @@ Gem::Specification.new do |spec|
20
20
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
21
  spec.require_paths = ['lib']
22
22
 
23
- spec.add_dependency 'httpclient', '~> 2.3'
24
- spec.add_dependency 'i18n', '~> 0.5'
25
-
26
23
  spec.add_development_dependency 'bundler'
27
24
  spec.add_development_dependency 'rake'
28
25
  end
@@ -1,3 +1,4 @@
1
+ require 'pathname'
1
2
  require 'artifactory/version'
2
3
 
3
4
  module Artifactory
@@ -17,6 +18,7 @@ module Artifactory
17
18
  autoload :Base, 'artifactory/resources/base'
18
19
  autoload :Build, 'artifactory/resources/build'
19
20
  autoload :Group, 'artifactory/resources/group'
21
+ autoload :Layout, 'artifactory/resources/layout'
20
22
  autoload :Plugin, 'artifactory/resources/plugin'
21
23
  autoload :Repository, 'artifactory/resources/repository'
22
24
  autoload :System, 'artifactory/resources/system'
@@ -70,9 +72,5 @@ module Artifactory
70
72
  end
71
73
  end
72
74
 
73
- require 'i18n'
74
- I18n.enforce_available_locales = false
75
- I18n.load_path << Dir[Artifactory.root.join('locales', '*.yml').to_s]
76
-
77
75
  # Load the initial default values
78
76
  Artifactory.setup
@@ -1,6 +1,5 @@
1
- require 'httpclient'
2
1
  require 'json'
3
- require 'rexml/document'
2
+ require 'net/http'
4
3
  require 'uri'
5
4
 
6
5
  module Artifactory
@@ -33,6 +32,7 @@ module Artifactory
33
32
  include Artifactory::Configurable
34
33
 
35
34
  proxy Resource::Artifact
35
+ proxy Resource::Layout
36
36
  proxy Resource::Repository
37
37
  proxy Resource::User
38
38
  proxy Resource::System
@@ -68,137 +68,245 @@ module Artifactory
68
68
  #
69
69
  # Make a HTTP GET request
70
70
  #
71
- # @param [String] path
72
- # the path to get, relative to {Defaults.endpoint}
71
+ # @param path (see Client#request)
72
+ # @param [Hash] params
73
+ # the list of query params
74
+ # @param headers (see Client#request)
75
+ #
76
+ # @raise (see Client#request)
77
+ # @return (see Client#request)
73
78
  #
74
- def get(path, *args, &block)
75
- request(:get, path, *args, &block)
79
+ def get(path, params = {}, headers = {})
80
+ request(:get, path, params, headers)
76
81
  end
77
82
 
78
83
  #
79
84
  # Make a HTTP POST request
80
85
  #
81
- # @param [String] path
82
- # the path to post, relative to {Defaults.endpoint}
86
+ # @param path (see Client#request)
87
+ # @param [String, #read] data
88
+ # the body to use for the request
89
+ # @param headers (see Client#request)
90
+ #
91
+ # @raise (see Client#request)
92
+ # @return (see Client#request)
83
93
  #
84
- def post(path, *args, &block)
85
- request(:post, path, *args, &block)
94
+ def post(path, data, headers = {})
95
+ request(:post, path, data, headers)
86
96
  end
87
97
 
88
98
  #
89
99
  # Make a HTTP PUT request
90
100
  #
91
- # @param [String] path
92
- # the path to put, relative to {Defaults.endpoint}
101
+ # @param path (see Client#request)
102
+ # @param data (see Client#post)
103
+ # @param headers (see Client#request)
104
+ #
105
+ # @raise (see Client#request)
106
+ # @return (see Client#request)
93
107
  #
94
- def put(path, *args, &block)
95
- request(:put, path, *args, &block)
108
+ def put(path, data, headers = {})
109
+ request(:put, path, data, headers)
96
110
  end
97
111
 
98
112
  #
99
113
  # Make a HTTP PATCH request
100
114
  #
101
- # @param [String] path
102
- # the path to patch, relative to {Defaults.endpoint}
115
+ # @param path (see Client#request)
116
+ # @param data (see Client#post)
117
+ # @param headers (see Client#request)
118
+ #
119
+ # @raise (see Client#request)
120
+ # @return (see Client#request)
103
121
  #
104
- def patch(path, *args, &block)
105
- request(:patch, path, *args, &block)
122
+ def patch(path, data, headers = {})
123
+ request(:patch, path, data, headers)
106
124
  end
107
125
 
108
126
  #
109
127
  # Make a HTTP DELETE request
110
128
  #
111
- # @param [String] path
112
- # the path to delete, relative to {Defaults.endpoint}
129
+ # @param path (see Client#request)
130
+ # @param params (see Client#get)
131
+ # @param headers (see Client#request)
132
+ #
133
+ # @raise (see Client#request)
134
+ # @return (see Client#request)
113
135
  #
114
- def delete(path, *args, &block)
115
- request(:delete, path, *args, &block)
136
+ def delete(path, params = {}, headers = {})
137
+ request(:delete, path, params, headers)
116
138
  end
117
139
 
118
140
  #
119
- # Make a HTTP HEAD request
120
- #
121
- # @param [String] path
122
- # the path to head, relative to {Defaults.endpoint}
141
+ # Make an HTTP request with the given verb, data, params, and headers. If
142
+ # the response has a return type of JSON, the JSON is automatically parsed
143
+ # and returned as a hash; otherwise it is returned as a string.
123
144
  #
124
- def head(path, *args, &block)
125
- request(:head, path, *args, &block)
126
- end
127
-
145
+ # @raise [Error::HTTPError]
146
+ # if the request is not an HTTP 200 OK
128
147
  #
129
- # The actually HTTPClient agent.
148
+ # @param [Symbol] verb
149
+ # the lowercase symbol of the HTTP verb (e.g. :get, :delete)
150
+ # @param [String] path
151
+ # the absolute or relative path from {Defaults.endpoint} to make the
152
+ # request against
153
+ # @param [#read, Hash, nil] data
154
+ # the data to use (varies based on the +verb+)
155
+ # @param [Hash] headers
156
+ # the list of headers to use
130
157
  #
131
- # @return [HTTPClient]
158
+ # @return [String, Hash]
159
+ # the response body
132
160
  #
133
- def agent
134
- @agent ||= begin
135
- agent = HTTPClient.new(endpoint)
161
+ def request(verb, path, data = {}, headers = {})
162
+ # Build the URI and request object from the given information
163
+ uri = build_uri(verb, path, data)
164
+ request = class_for_request(verb).new(uri.request_uri)
136
165
 
137
- agent.agent_name = user_agent
166
+ # Add headers
167
+ default_headers.merge(headers).each do |key, value|
168
+ request.add_field(key, value)
169
+ end
138
170
 
139
- # Check if authentication was given
140
- if username && password
141
- agent.set_auth(endpoint, username, password)
171
+ # Add basic authentication
172
+ if username && password
173
+ request.basic_auth(username, password)
174
+ end
142
175
 
143
- # https://github.com/nahi/httpclient/issues/63#issuecomment-2377919
144
- agent.www_auth.basic_auth.challenge(endpoint)
176
+ # Setup PATCH/POST/PUT
177
+ if [:patch, :post, :put].include?(verb)
178
+ if data.respond_to?(:read)
179
+ request.body_stream = data
180
+ elsif data.is_a?(Hash)
181
+ request.form_data = data
182
+ else
183
+ request.body = data
145
184
  end
185
+ end
146
186
 
147
- # Check if proxy settings were given
148
- if proxy
149
- agent.proxy = proxy
187
+ # Create the HTTP connection object - since the proxy information defaults
188
+ # to +nil+, we can just pass it to the initializer method instead of doing
189
+ # crazy strange conditionals.
190
+ connection = Net::HTTP.new(uri.host, uri.port,
191
+ proxy_address, proxy_port, proxy_username, proxy_password)
192
+
193
+ # Apply SSL, if applicable
194
+ if uri.scheme == 'https'
195
+ require 'net/https' unless defined?(Net::HTTPS)
196
+
197
+ # Turn on SSL
198
+ connection.use_ssl = true
199
+
200
+ # Custom pem files, no problem!
201
+ if ssl_pem_file
202
+ pem = File.read(ssl_pem_file)
203
+ connection.cert = OpenSSL::X509::Certificate.new(pem)
204
+ connection.key = OpenSSL::PKey::RSA.new(pem)
205
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
150
206
  end
151
207
 
152
- agent
208
+ # Naughty, naughty, naughty! Don't blame when when someone hops in
209
+ # and executes a MITM attack!
210
+ unless ssl_verify
211
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
212
+ end
153
213
  end
214
+
215
+ # Create a connection using the block form, which will ensure the socket
216
+ # is properly closed in the event of an error.
217
+ connection.start do |http|
218
+ response = http.request(request)
219
+
220
+ case response
221
+ when Net::HTTPRedirection
222
+ redirect = URI.parse(response['location'])
223
+ request(verb, redirect, params, headers)
224
+ when Net::HTTPSuccess
225
+ success(response)
226
+ else
227
+ error(response)
228
+ end
229
+ end
230
+ rescue SocketError, Errno::ECONNREFUSED, EOFError
231
+ raise Error::ConnectionError.new(endpoint)
232
+ end
233
+
234
+ #
235
+ # The list of default headers (such as Keep-Alive and User-Agent) for the
236
+ # client object.
237
+ #
238
+ # @return [Hash]
239
+ #
240
+ def default_headers
241
+ {
242
+ 'Connection' => 'keep-alive',
243
+ 'Keep-Alive' => '30',
244
+ 'User-Agent' => user_agent,
245
+ }
154
246
  end
155
247
 
156
248
  #
157
- # Make an HTTP reequest with the given verb and path.
249
+ # Construct a URL from the given verb and path. If the request is a GET or
250
+ # DELETE request, the params are assumed to be query params are are
251
+ # converted as such using {Client#to_query_string}.
158
252
  #
159
- # @param [String, Symbol] verb
160
- # the HTTP verb to use
253
+ # If the path is relative, it is merged with the {Defaults.endpoint}
254
+ # attribute. If the path is absolute, it is converted to a URI object and
255
+ # returned.
256
+ #
257
+ # @param [Symbol] verb
258
+ # the lowercase HTTP verb (e.g. :+get+)
161
259
  # @param [String] path
162
- # the absolute or relative URL to use, expanded relative to {Defaults.endpoint}
260
+ # the absolute or relative HTTP path (url) to get
261
+ # @param [Hash] params
262
+ # the list of params to build the URI with (for GET and DELETE requests)
163
263
  #
164
- # @return [Object]
264
+ # @return [URI]
165
265
  #
166
- def request(verb, path, *args, &block)
167
- url = URI.parse(path)
266
+ def build_uri(verb, path, params = {})
267
+ # Add any query string parameters
268
+ if [:delete, :get].include?(verb)
269
+ path = [path, to_query_string(params)].compact.join('?')
270
+ end
271
+
272
+ # Parse the URI
273
+ uri = URI.parse(path)
168
274
 
169
275
  # Don't merge absolute URLs
170
- unless url.absolute?
171
- url = URI.parse(File.join(endpoint, path)).to_s
172
- end
276
+ uri = URI.parse(File.join(endpoint, path)) unless uri.absolute?
173
277
 
174
- # Covert the URL back into a string
175
- url = url.to_s
176
-
177
- # Make the actual request
178
- response = agent.send(verb, url, *args, &block)
179
-
180
- case response.status.to_i
181
- when 200..399
182
- parse_response(response)
183
- when 400
184
- raise Error::BadRequest.new(url: url, body: response.body)
185
- when 401
186
- raise Error::Unauthorized.new(url: url)
187
- when 403
188
- raise Error::Forbidden.new(url: url)
189
- when 404
190
- raise Error::NotFound.new(url: url)
191
- when 405
192
- raise Error::MethodNotAllowed.new(url: url)
193
- else
194
- raise Error::ConnectionError.new(url: url, body: response.body)
195
- end
196
- rescue SocketError, Errno::ECONNREFUSED, EOFError
197
- raise Error::ConnectionError.new(url: url, body: <<-EOH.gsub(/^ {8}/, ''))
198
- The server is not currently accepting connections.
199
- EOH
278
+ # Return the URI object
279
+ uri
200
280
  end
201
281
 
282
+ #
283
+ # Helper method to get the corresponding {Net::HTTP} class from the given
284
+ # HTTP verb.
285
+ #
286
+ # @param [#to_s] verb
287
+ # the HTTP verb to create a class from
288
+ #
289
+ # @return [Class]
290
+ #
291
+ def class_for_request(verb)
292
+ Net::HTTP.const_get(verb.to_s.capitalize)
293
+ end
294
+
295
+ #
296
+ # Convert the given hash to a list of query string parameters. Each key and
297
+ # value in the hash is URI-escaped for safety.
298
+ #
299
+ # @param [Hash] hash
300
+ # the hash to create the query string from
301
+ #
302
+ # @return [String, nil]
303
+ # the query string as a string, or +nil+ if there are no params
304
+ #
305
+ def to_query_string(hash)
306
+ hash.map do |key, value|
307
+ "#{URI.escape(key.to_s)}=#{URI.escape(value.to_s)}"
308
+ end.join('&')[/.+/]
309
+ end
202
310
 
203
311
  #
204
312
  # Parse the response object and manipulate the result based on the given
@@ -211,14 +319,31 @@ module Artifactory
211
319
  # @return [String, Hash]
212
320
  # the parsed response, as an object
213
321
  #
214
- def parse_response(response)
215
- content_type = response.headers['Content-Type']
216
-
217
- if content_type && content_type.include?('json')
322
+ def success(response)
323
+ if (response.content_type || '').include?('json')
218
324
  JSON.parse(response.body)
219
325
  else
220
326
  response.body
221
327
  end
222
328
  end
329
+
330
+ #
331
+ # Raise a response error, extracting as much information from the server's
332
+ # response as possible.
333
+ #
334
+ # @raise [Error::HTTPError]
335
+ #
336
+ # @param [HTTP::Message] response
337
+ # the response object from the request
338
+ #
339
+ def error(response)
340
+ error = JSON.parse(response.body)['errors'].first
341
+ raise Error::HTTPError.new(error)
342
+ rescue JSON::ParserError
343
+ raise Error::HTTPError.new(
344
+ 'status' => response.code,
345
+ 'message' => response.body,
346
+ )
347
+ end
223
348
  end
224
349
  end