http-client-generator 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 21d65c38e42bcd0404d1eaf2e444adfae16136940b1c162c41f29757cd88531b
4
- data.tar.gz: 869036773a9dbcac9ea6651304a97d48c5fd8d5ee48a6d34dbc052c88bebd9eb
3
+ metadata.gz: edc63e0a164b0694ebe93647137b6b825061c57064cf851f4675c5e2955e6fd6
4
+ data.tar.gz: 68ee612dc6a37f278351736ab53fe8d2a85598314c570b65bb4e9f7daffc9463
5
5
  SHA512:
6
- metadata.gz: bd8c24c756289f12cf0868a39b2954898b05c9eface230e1f2c3c34e2b63fca31fa9654e810e2d39348b31758929a3a96bcded22d3155110f0757e56804a0df5
7
- data.tar.gz: aa75c5897ed9e37c3bea2a432160d0226a6d3b43ec267eac87c7dcd362983848f46f15eba413e3ef65e7adda4d43cab5b5aa5d52e2ca20e401770dab0d40b61a
6
+ metadata.gz: 6b15241122952c07b5560047c505e5bffb0a2f6c2ff80c5626de3a152636766fd9a61e72ff01c9734b06a235aa7aaa33796af544c621e801c5088377f22f50f6
7
+ data.tar.gz: a5663a4c5f8f1c8daeb0e83f3cbefff6f5d6499f5f0ff5011c8324cd9a27780ed0bffce2b9902f37386ad1e91890f532f6e825d536d0e897fbaa2838a7bd4dcc
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavel Egorov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,39 +1,379 @@
1
- # Http::Client::Generator
1
+ # HttpClientGenerator
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ `http-client-generator` is a small Ruby DSL for building API clients on top of the
4
+ [`http`](https://github.com/httprb/http) gem.
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/http/client/generator`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ You declare resources, attach request and response plugs, provide a URL helper,
7
+ and the gem generates class-level request methods such as `get_user`,
8
+ `post_issue`, or `patch_repository`.
6
9
 
7
10
  ## Installation
8
11
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
12
+ Add the gem to your application:
10
13
 
11
- Install the gem and add to the application's Gemfile by executing:
14
+ ```ruby
15
+ gem 'http-client-generator'
16
+ ```
12
17
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+ Then run:
14
19
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
16
-
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
20
+ ```sh
21
+ bundle install
22
+ ```
18
23
 
19
24
  ## Usage
20
25
 
21
- TODO: Write usage instructions here
26
+ Define a client module, include `HttpClientGenerator`, declare resources, and
27
+ provide a URL helper module with methods that match each resource name.
22
28
 
23
- ## Development
29
+ This example sketches a small GitHub API client.
24
30
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
31
+ ```ruby
32
+ require 'http_client_generator'
33
+ require 'zeitwerk'
26
34
 
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
35
+ Zeitwerk::Loader.for_gem.setup
28
36
 
29
- ## Contributing
37
+ module GitHub
38
+ include HttpClientGenerator
30
39
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/http-client-generator. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/http-client-generator/blob/master/CODE_OF_CONDUCT.md).
40
+ Configuration = Struct.new(:base_url, :access_token, keyword_init: true)
32
41
 
33
- ## License
42
+ class InvalidConfigurationError < Error
43
+ ACCESS_TOKEN_ERROR = 'Access token should be a non-empty String.'
44
+ BASE_URL_ERROR = 'Base URL should be a non-empty String.'
45
+ end
46
+
47
+ resources do
48
+ req_plug :set_request_id, :x_request_id
49
+ req_plug :set_header, header: :accept, value: 'application/vnd.github+json'
50
+ req_plug :set_header, header: :x_github_api_version, value: '2022-11-28'
51
+ req_plug :set_header,
52
+ header: :authorization,
53
+ value: -> { "Bearer #{GitHub.config.access_token}" }
54
+
55
+ req_plug :camelize_body
56
+
57
+ resp_plug :enforce_json_response
58
+ resp_plug :underscore_response
59
+
60
+ timeout connect: 2, read: 5, write: 5
61
+
62
+ get :user
63
+ get :repository
64
+ get :repository_issues, timeout: 10
65
+ post :issue
66
+ patch :issue
67
+ end
68
+
69
+ url_helper UrlHelper
70
+
71
+ process_config do |config|
72
+ validation_errors = []
73
+
74
+ unless config.access_token.is_a?(String) && !config.access_token.empty?
75
+ validation_errors << InvalidConfigurationError::ACCESS_TOKEN_ERROR
76
+ end
77
+
78
+ unless config.base_url.is_a?(String) && !config.base_url.empty?
79
+ validation_errors << InvalidConfigurationError::BASE_URL_ERROR
80
+ end
81
+
82
+ raise InvalidConfigurationError, validation_errors.join(' ') if validation_errors.any?
83
+ end
84
+ end
85
+ ```
86
+
87
+ ```ruby
88
+ require 'uri'
89
+
90
+ module GitHub
91
+ module UrlHelper
92
+ include HttpClientGenerator::UrlBuilder
93
+
94
+ def user(_options = {})
95
+ "#{config.base_url}/user"
96
+ end
97
+
98
+ def repository(options)
99
+ "#{config.base_url}/repos/#{options.fetch(:owner)}/#{options.fetch(:repo)}"
100
+ end
101
+
102
+ def repository_issues(options)
103
+ build_url(
104
+ url: "#{config.base_url}/repos/#{options.fetch(:owner)}/#{options.fetch(:repo)}/issues",
105
+ query: options.slice(:state, :labels, :since, :per_page, :page),
106
+ )
107
+ end
108
+
109
+ def issue(options)
110
+ issue_number = options[:issue_number]
111
+ path = "#{config.base_url}/repos/#{options.fetch(:owner)}/#{options.fetch(:repo)}/issues"
112
+
113
+ issue_number ? "#{path}/#{issue_number}" : path
114
+ end
115
+
116
+ private
117
+
118
+ def config = GitHub.config
119
+
120
+ def build_url(url:, query: {})
121
+ uri = URI(url)
122
+ query_values = query.compact.transform_keys { |key| HttpClientGenerator::Inflector.camelize_lower(key) }
123
+
124
+ uri.query = URI.encode_www_form(query_values) if query_values.any?
125
+ uri.to_s
126
+ end
127
+ end
128
+ end
129
+ ```
130
+
131
+ Configure the client before making requests:
132
+
133
+ ```ruby
134
+ GitHub.configure do |config|
135
+ config.base_url = 'https://api.github.com'
136
+ config.access_token = ENV.fetch('GITHUB_TOKEN')
137
+ end
138
+ ```
139
+
140
+ Then call generated methods. Method names are built from the HTTP verb and
141
+ resource name:
142
+
143
+ ```ruby
144
+ GitHub.get_user
145
+
146
+ GitHub.get_user(timeout: 1)
147
+
148
+ GitHub.get_repository(owner: 'rails', repo: 'rails')
149
+
150
+ GitHub.get_repository_issues(
151
+ { owner: 'rails', repo: 'rails', state: 'open', per_page: 10 },
152
+ request_id: SecureRandom.uuid,
153
+ )
154
+
155
+ GitHub.post_issue(
156
+ { owner: 'octo-org', repo: 'octo-repo' },
157
+ body: {
158
+ title: 'Bug report',
159
+ body: 'Steps to reproduce...',
160
+ labels: ['bug'],
161
+ },
162
+ )
163
+
164
+ GitHub.patch_issue(
165
+ { owner: 'octo-org', repo: 'octo-repo', issue_number: 42 },
166
+ body: {
167
+ state: 'closed',
168
+ },
169
+ )
170
+ ```
171
+
172
+ ## Resources
173
+
174
+ Declare resources inside a `resources` block:
175
+
176
+ ```ruby
177
+ resources do
178
+ get :user
179
+ post :issue
180
+ put :repository
181
+ patch :issue
182
+ end
183
+ ```
184
+
185
+ Each resource generates a singleton method on the client module:
186
+
187
+ | Declaration | Generated method |
188
+ | --- | --- |
189
+ | `get :user` | `get_user` |
190
+ | `post :issue` | `post_issue` |
191
+ | `put :repository` | `put_repository` |
192
+ | `patch :issue` | `patch_issue` |
193
+
194
+ Generated methods accept URL options as the first positional argument, optional
195
+ `body:` and `timeout:` keywords, and any extra keyword arguments consumed by
196
+ plugs:
197
+
198
+ ```ruby
199
+ Client.post_resource(
200
+ { id: 1 },
201
+ body: { name: 'Example' },
202
+ timeout: 2,
203
+ request_id: 'abc-123',
204
+ )
205
+ ```
206
+
207
+ JSON is the default content type. Use `content_type: :text` for plain text
208
+ request bodies:
209
+
210
+ ```ruby
211
+ resources do
212
+ post :webhook_test, content_type: :text
213
+ end
214
+ ```
215
+
216
+ ## Timeouts
217
+
218
+ Timeouts mirror the `http` gem API. Use a numeric value for a global timeout,
219
+ per-operation values for connect/read/write limits, or `:null` to explicitly
220
+ disable timeout handling.
34
221
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
222
+ ```ruby
223
+ resources do
224
+ timeout 5
36
225
 
37
- ## Code of Conduct
226
+ get :user
227
+ get :repository_issues, timeout: { read: 10 }
228
+
229
+ namespace do
230
+ timeout connect: 1, read: 3, write: 3
231
+
232
+ post :issue
233
+ end
234
+
235
+ get :stream, timeout: :null
236
+ end
237
+ ```
238
+
239
+ Timeout precedence, from highest to lowest:
240
+
241
+ 1. Per-call `timeout:`
242
+ 2. Resource-level `timeout:`
243
+ 3. Current scoped `timeout`
244
+ 4. No timeout configuration
245
+
246
+ ```ruby
247
+ Client.get_user(timeout: 1)
248
+ Client.post_issue({ id: 1 }, body: { title: 'Bug' }, timeout: :null)
249
+ ```
250
+
251
+ ## URL Helpers
252
+
253
+ Register a URL helper with `url_helper`. The helper must respond to every
254
+ resource name and return a complete URL.
255
+
256
+ ```ruby
257
+ module Client
258
+ module UrlHelper
259
+ include HttpClientGenerator::UrlBuilder
260
+
261
+ def resource(options)
262
+ "#{Client.config.base_url}/resources/#{options.fetch(:id)}"
263
+ end
264
+ end
265
+ end
266
+
267
+ Client.url_helper Client::UrlHelper
268
+ ```
269
+
270
+ `HttpClientGenerator::UrlBuilder` extends the helper module so resource URLs can
271
+ be called by the generated client methods.
272
+
273
+ ## Plugs
274
+
275
+ Plugs are objects that respond to `call(request)`. Request plugs run before the
276
+ HTTP request. Response plugs run after the response body is read.
277
+
278
+ ```ruby
279
+ resources do
280
+ req_plug :set_request_id, :x_request_id
281
+ req_plug :set_bearer_token, from_arg: :access_token
282
+
283
+ resp_plug :enforce_json_response
284
+ resp_plug :underscore_response
285
+
286
+ get :profile
287
+ end
288
+ ```
289
+
290
+ Built-in request plugs:
291
+
292
+ | Plug | Purpose |
293
+ | --- | --- |
294
+ | `:set_request_id` | Sets a generated or provided request id header. |
295
+ | `:set_header` | Sets a static, dynamic, or argument-derived header. |
296
+ | `:set_bearer_token` | Sets `Authorization: Bearer ...` from an extra method argument. |
297
+ | `:camelize_body` | Camelizes JSON request body keys. |
298
+ | `:validate_request` | Validates request bodies with a schema helper. |
299
+
300
+ Built-in response plugs:
301
+
302
+ | Plug | Purpose |
303
+ | --- | --- |
304
+ | `:enforce_json_response` | Parses the response as JSON and raises on invalid JSON. |
305
+ | `:encode_json_response` | Parses JSON responses when possible and leaves invalid JSON unchanged. |
306
+ | `:underscore_response` | Underscores parsed response keys. |
307
+ | `:validate_response` | Validates parsed response bodies with a schema helper. |
308
+
309
+ Limit plugs to specific resources with `only:` or `except:`:
310
+
311
+ ```ruby
312
+ resources do
313
+ req_plug :camelize_body, only: %i[create_issue update_issue]
314
+ resp_plug :enforce_json_response, except: :download_archive
315
+
316
+ post :create_issue
317
+ patch :update_issue
318
+ get :download_archive, content_type: :text
319
+ end
320
+ ```
321
+
322
+ You can also pass a callable plug:
323
+
324
+ ```ruby
325
+ resources do
326
+ req_plug ->(request) {
327
+ request.headers[:user_agent] = 'my-client/1.0'
328
+ request
329
+ }
330
+
331
+ get :user
332
+ end
333
+ ```
334
+
335
+ ## Configuration
336
+
337
+ The client module owns its configuration type. Define `Configuration`, then call
338
+ `configure`:
339
+
340
+ ```ruby
341
+ module Client
342
+ include HttpClientGenerator
343
+
344
+ Configuration = Struct.new(:base_url, :access_token, keyword_init: true)
345
+
346
+ process_config do |config|
347
+ raise Error, 'Missing base URL' unless config.base_url.is_a?(String)
348
+ end
349
+ end
350
+
351
+ Client.configure do |config|
352
+ config.base_url = 'https://api.example.com'
353
+ config.access_token = ENV.fetch('ACCESS_TOKEN')
354
+ end
355
+ ```
356
+
357
+ The final configuration object is frozen after `process_config` runs.
358
+
359
+ ## Development
360
+
361
+ After checking out the repo, run:
362
+
363
+ ```sh
364
+ bin/setup
365
+ bundle exec rake spec
366
+ ```
367
+
368
+ You can also run `bin/console` for an interactive prompt.
369
+
370
+ To install this gem locally:
371
+
372
+ ```sh
373
+ bundle exec rake install
374
+ ```
375
+
376
+ ## License
38
377
 
39
- Everyone interacting in the Http::Client::Generator project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/http-client-generator/blob/master/CODE_OF_CONDUCT.md).
378
+ The gem is available as open source under the terms of the
379
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpClientGenerator
4
+ module Inflector # :nodoc:
5
+ module_function
6
+
7
+ def camelize_lower(value)
8
+ value.to_s.split('_').then do |parts|
9
+ [parts.first, *parts.drop(1).map(&:capitalize)].join
10
+ end
11
+ end
12
+
13
+ def underscore(value)
14
+ value.to_s
15
+ .gsub('::', '/')
16
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
17
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
18
+ .tr('-', '_')
19
+ .downcase
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpClientGenerator
4
+ module KeyTransformer # :nodoc:
5
+ module_function
6
+
7
+ def deep_transform_keys(value, &block)
8
+ case value
9
+ when Hash
10
+ value.each_with_object({}) do |(key, hash_value), transformed|
11
+ transformed[yield(key)] = deep_transform_keys(hash_value, &block)
12
+ end
13
+ when Array
14
+ value.map { |item| deep_transform_keys(item, &block) }
15
+ else
16
+ value
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,13 +2,13 @@
2
2
 
3
3
  module HttpClientGenerator
4
4
  class Plugs
5
- class CamelizeBody
5
+ class CamelizeBody # :nodoc:
6
6
  Plugs.register :camelize_body, self
7
7
 
8
8
  def call(req)
9
9
  body =
10
10
  if req.json? && req.body.is_a?(Hash)
11
- req.body.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym }
11
+ KeyTransformer.deep_transform_keys(req.body) { |key| Inflector.camelize_lower(key).to_sym }
12
12
  else
13
13
  req.body
14
14
  end
@@ -2,13 +2,14 @@
2
2
 
3
3
  module HttpClientGenerator
4
4
  class Plugs
5
- class UnderscoreResponse
5
+ class UnderscoreResponse # :nodoc:
6
6
  Plugs.register :underscore_response, self
7
7
 
8
8
  def call(req)
9
9
  return req unless req.response_body.is_a?(Hash)
10
10
 
11
- req.response_body = req.response_body.deep_transform_keys { |k| k.to_s.underscore.to_sym }
11
+ req.response_body =
12
+ KeyTransformer.deep_transform_keys(req.response_body) { |key| Inflector.underscore(key).to_sym }
12
13
 
13
14
  req
14
15
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpClientGenerator
4
- class Request
4
+ class Request # :nodoc:
5
5
  CONTENT_TYPES = %i[json text].freeze
6
6
 
7
7
  DEFAULT_HEADERS_BY_CONTENT_TYPE = {
8
8
  json: { accept: 'application/json', content_type: 'application/json' },
9
- text: { content_type: 'text/plain' },
9
+ text: { content_type: 'text/plain' }
10
10
  }.freeze
11
11
 
12
12
  CONTENT_TYPES.each do |type|
@@ -15,19 +15,23 @@ module HttpClientGenerator
15
15
  end
16
16
  end
17
17
 
18
- attr_accessor :verb, :name, :content_type, :url, :headers, :body, :rest_args, :response_body, :base, :extra
18
+ attr_accessor :verb, :name, :content_type, :timeout, :url, :headers, :body, :rest_args, :response_body,
19
+ :base, :extra
19
20
 
20
- def initialize(base:, name:, verb:, content_type:, url:, body:, rest_args:)
21
+ # rubocop:disable Metrics/ParameterLists
22
+ def initialize(base:, name:, verb:, content_type:, timeout:, url:, body:, rest_args:)
21
23
  @base = base
22
24
  @name = name
23
25
  @verb = verb
24
26
  @content_type = content_type
27
+ @timeout = timeout
25
28
  @url = url
26
29
  @rest_args = rest_args
27
30
  @body = body
28
31
  @headers = {}
29
32
  @extra = {}
30
33
  end
34
+ # rubocop:enable Metrics/ParameterLists
31
35
 
32
36
  def current_headers
33
37
  default_headers.merge(headers)
@@ -35,15 +39,15 @@ module HttpClientGenerator
35
39
 
36
40
  def raw_body
37
41
  if json?
38
- body && body.to_json
42
+ body&.to_json
39
43
  else
40
44
  body
41
45
  end
42
46
  end
43
47
 
44
- def raise_error(e)
45
- Sentry.capture_exception(e, extra: { request_id: extra[:request_id] }) if Object.const_defined?(:Sentry)
46
- raise base::RequestError, e.message, e.backtrace, cause: nil
48
+ def raise_error(error)
49
+ Sentry.capture_exception(error, extra: { request_id: extra[:request_id] }) if Object.const_defined?(:Sentry)
50
+ raise base::RequestError, error.message, error.backtrace, cause: nil
47
51
  end
48
52
 
49
53
  def raise_message(message)
@@ -3,28 +3,25 @@
3
3
  require 'http'
4
4
 
5
5
  module HttpClientGenerator
6
- class Resource
7
- attr_reader :verb, :content_type, :name, :req_plugs, :resp_plugs, :base
6
+ class Resource # :nodoc:
7
+ attr_reader :verb, :content_type, :name, :req_plugs, :resp_plugs, :base, :timeout
8
8
 
9
- def initialize(verb:, content_type:, name:, base:, req_plugs:, resp_plugs:)
9
+ # rubocop:disable Metrics/ParameterLists
10
+ def initialize(verb:, content_type:, timeout:, name:, base:, req_plugs:, resp_plugs:)
10
11
  @verb = verb
11
12
  @base = base
12
13
  @content_type = content_type
14
+ @timeout = timeout
13
15
  @name = name
14
16
  @req_plugs = select_plugs(req_plugs)
15
17
  @resp_plugs = select_plugs(resp_plugs)
16
18
  end
19
+ # rubocop:enable Metrics/ParameterLists
17
20
 
18
- def perform_request(url_helper, url_options, body, rest_args)
19
- request = prepare_request(url_helper, url_options, body, rest_args)
21
+ def perform_request(url_helper, url_options, body, rest_args, timeout_override)
22
+ request = prepare_request(url_helper, url_options, body, rest_args, timeout_override)
20
23
 
21
- request_attributes = [request.verb, request.url]
22
-
23
- if request.body
24
- request_attributes << { body: request.raw_body }
25
- end
26
-
27
- request.response_body = HTTP[request.current_headers].public_send(*request_attributes).to_s
24
+ request.response_body = perform_http_request(request).to_s
28
25
 
29
26
  process_response(request)
30
27
  rescue HTTP::Error => e
@@ -33,14 +30,37 @@ module HttpClientGenerator
33
30
 
34
31
  private
35
32
 
36
- def prepare_request(url_helper, url_options, body, rest_args)
33
+ def prepare_request(url_helper, url_options, body, rest_args, timeout_override)
37
34
  url = url_helper.public_send(:"#{name}", url_options)
35
+ request = build_request(url, body, rest_args, timeout_override)
36
+
37
+ req_plugs.reduce(request) { |req, plug| plug.call(req) }
38
+ end
38
39
 
39
- request = Request.new(
40
- name: name, verb: verb, url: url, content_type: content_type, body: body, rest_args: rest_args, base: base
40
+ def build_request(url, body, rest_args, timeout_override)
41
+ Request.new(
42
+ name: name,
43
+ verb: verb,
44
+ url: url,
45
+ content_type: content_type,
46
+ timeout: timeout_override.nil? ? timeout : TimeoutNormalizer.call(timeout_override),
47
+ body: body,
48
+ rest_args: rest_args,
49
+ base: base
41
50
  )
51
+ end
42
52
 
43
- req_plugs.reduce(request) { |req, plug| plug.call(req) }
53
+ def perform_http_request(request)
54
+ http_client = HTTP[request.current_headers]
55
+ http_client = http_client.timeout(request.timeout) unless request.timeout.nil?
56
+
57
+ http_client.public_send(*request_attributes(request))
58
+ end
59
+
60
+ def request_attributes(request)
61
+ attributes = [request.verb, request.url]
62
+ attributes << { body: request.raw_body } if request.body
63
+ attributes
44
64
  end
45
65
 
46
66
  def process_response(request)
@@ -51,7 +71,9 @@ module HttpClientGenerator
51
71
 
52
72
  def select_plugs(plug_entries)
53
73
  plug_entries.filter_map do |entry|
54
- entry in only:, except:, plug:
74
+ only = entry[:only]
75
+ except = entry[:except]
76
+ plug = entry[:plug]
55
77
 
56
78
  next if only.any? && !only.include?(name)
57
79
  next if except.include?(name)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpClientGenerator
4
- class ResourcesDefinition
4
+ class ResourcesDefinition # :nodoc:
5
5
  attr_reader :resources
6
6
 
7
7
  DEFAULT_OPTIONS = {
@@ -10,11 +10,12 @@ module HttpClientGenerator
10
10
 
11
11
  HTTP_VERBS = %i[get post put patch].freeze
12
12
 
13
- def initialize(base, req_plugs = [], resp_plugs = [])
13
+ def initialize(base, req_plugs = [], resp_plugs = [], timeout = nil)
14
14
  @base = base
15
15
  @resources = []
16
16
  @req_plugs = req_plugs
17
17
  @resp_plugs = resp_plugs
18
+ @timeout = timeout
18
19
  end
19
20
 
20
21
  HTTP_VERBS.each do |verb|
@@ -31,8 +32,12 @@ module HttpClientGenerator
31
32
  @resp_plugs << build_plug_entry(plug, *args, only: only, except: except, **kwargs)
32
33
  end
33
34
 
34
- def namespace(_name, &block)
35
- namespaced_definition = ResourcesDefinition.new(@base, @req_plugs.dup, @resp_plugs.dup)
35
+ def timeout(value = nil, **options)
36
+ @timeout = TimeoutNormalizer.call(value, **options)
37
+ end
38
+
39
+ def namespace(_name = nil, &block)
40
+ namespaced_definition = ResourcesDefinition.new(@base, @req_plugs.dup, @resp_plugs.dup, @timeout)
36
41
  namespaced_definition.instance_eval(&block)
37
42
  @resources += namespaced_definition.resources
38
43
  end
@@ -58,18 +63,28 @@ module HttpClientGenerator
58
63
  end
59
64
 
60
65
  def build_resource(verb, name, options)
66
+ resource_options = with_defaults(options)
67
+
61
68
  Resource.new(
62
69
  verb: verb,
63
70
  name: name,
64
71
  base: @base,
65
72
  req_plugs: @req_plugs,
66
73
  resp_plugs: @resp_plugs,
67
- **with_defaults(options)
74
+ **resource_options
68
75
  )
69
76
  end
70
77
 
71
78
  def with_defaults(options)
72
- DEFAULT_OPTIONS.merge(options)
79
+ resource_options = DEFAULT_OPTIONS.merge(options)
80
+ resource_options[:timeout] =
81
+ if options.key?(:timeout)
82
+ TimeoutNormalizer.call(options[:timeout])
83
+ else
84
+ @timeout
85
+ end
86
+
87
+ resource_options
73
88
  end
74
89
  end
75
90
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HttpClientGenerator
4
+ module TimeoutNormalizer # :nodoc:
5
+ VALID_KEYS = %i[connect read write].freeze
6
+
7
+ ERROR_MESSAGE = 'Use timeout(global), timeout(:null), or timeout(connect: x, read: y, write: z).'
8
+
9
+ def self.call(value = nil, **options)
10
+ timeout_value = build_timeout_value(value, options)
11
+ case timeout_value
12
+ when Numeric, :null
13
+ timeout_value
14
+ when Hash
15
+ normalize_hash(timeout_value)
16
+ else
17
+ raise ArgumentError, ERROR_MESSAGE
18
+ end
19
+ end
20
+
21
+ def self.build_timeout_value(value, options)
22
+ if options.any?
23
+ raise ArgumentError, ERROR_MESSAGE unless value.nil?
24
+
25
+ options
26
+ else
27
+ value
28
+ end
29
+ end
30
+ private_class_method :build_timeout_value
31
+
32
+ def self.normalize_hash(timeout_value)
33
+ invalid_keys = timeout_value.keys - VALID_KEYS
34
+
35
+ raise ArgumentError, "Unknown timeout option(s): #{invalid_keys.join(', ')}." if invalid_keys.any?
36
+
37
+ timeout_value.dup
38
+ end
39
+ private_class_method :normalize_hash
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HttpClientGenerator
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
@@ -2,14 +2,11 @@
2
2
 
3
3
  require 'zeitwerk'
4
4
 
5
- require 'active_support/core_ext/string/inflections'
6
- require 'active_support/core_ext/hash/keys'
7
-
8
5
  Zeitwerk::Loader.for_gem.setup
9
6
 
10
- Dir["#{__dir__}/http_client_generator/plugs/*.rb"].each { |f| require f }
7
+ Dir["#{__dir__}/http_client_generator/plugs/*.rb"].sort.each { |f| require f }
11
8
 
12
- module HttpClientGenerator
9
+ module HttpClientGenerator # :nodoc:
13
10
  def self.included(base)
14
11
  super
15
12
 
@@ -30,8 +27,8 @@ module HttpClientGenerator
30
27
  resources.each do |resource|
31
28
  method_name = :"#{resource.verb}_#{resource.name}"
32
29
 
33
- define_singleton_method(method_name) do |url_options = {}, body: nil, **rest_args|
34
- resource.perform_request(@url_helper, url_options, body, rest_args)
30
+ define_singleton_method(method_name) do |url_options = {}, body: nil, timeout: nil, **rest_args|
31
+ resource.perform_request(@url_helper, url_options, body, rest_args, timeout)
35
32
  end
36
33
  end
37
34
  end
@@ -40,9 +37,9 @@ module HttpClientGenerator
40
37
 
41
38
  def configure(&configure)
42
39
  @config = self::Configuration.new
43
- .tap(&configure)
44
- .tap { |c| user_process_config(c) if respond_to?(:user_process_config) }
45
- .tap(&:freeze)
40
+ .tap(&configure)
41
+ .tap { |c| user_process_config(c) if respond_to?(:user_process_config) }
42
+ .tap(&:freeze)
46
43
  end
47
44
 
48
45
  def process_config(&block)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: http-client-generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Egorov
@@ -44,8 +44,11 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
+ - LICENSE.txt
47
48
  - README.md
48
49
  - lib/http_client_generator.rb
50
+ - lib/http_client_generator/inflector.rb
51
+ - lib/http_client_generator/key_transformer.rb
49
52
  - lib/http_client_generator/plugs.rb
50
53
  - lib/http_client_generator/plugs/camelize_body.rb
51
54
  - lib/http_client_generator/plugs/encode_json_response.rb
@@ -60,6 +63,7 @@ files:
60
63
  - lib/http_client_generator/resource.rb
61
64
  - lib/http_client_generator/resources_definition.rb
62
65
  - lib/http_client_generator/schema_builder.rb
66
+ - lib/http_client_generator/timeout_normalizer.rb
63
67
  - lib/http_client_generator/url_builder.rb
64
68
  - lib/http_client_generator/version.rb
65
69
  licenses: []