jeckle 0.6.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4761ead36774dc1acab1c41b0365ff229c412677a75292aa1473acd381fd06cd
4
- data.tar.gz: 64f4f522c235470e25be1660a414bebf5aabe81e4b60e934b1fcbd13ed08056b
3
+ metadata.gz: acac117a0dc0c961029fb70e87d6c2f412fe108989fdfec04d3d911274ceb460
4
+ data.tar.gz: 9ed85573a4512a3915071479500fd22b1901e8b852c23c3490ed9f35c4dd9c66
5
5
  SHA512:
6
- metadata.gz: 94176cba14e8fa3ad4aac3006191f34dffc050ca6739a05e991715108f8a9a8b4e6abb9e937028e3230357b6a39146f015a8484fc29a8a987e5dab8d43ae0508
7
- data.tar.gz: 719608cb95326167d652fd67fa76ceac3929ba4610a1705e4ad948c769d97177f3eaa161907e7396ade56cc32368fe0b625e9be043ae6884529a15adc200b9b3
6
+ metadata.gz: ce75de0db73e82f2b9f832eba3df78c9b862d812c85e98e1034d50a6b6aeb83b4db5070615fdb3c8a50239bbd9f9b8d8c1d09064c87a8ea81dda832680375eec
7
+ data.tar.gz: ea854a9bdf95b332be7feebe9fb6215c78a2bf3b120ad8707983c86d18e0971919220af8086ec8b14828a8341d0d76230f3bebb2ed7f9f1c74f052e75569b067
data/README.md CHANGED
@@ -28,28 +28,133 @@ And then execute:
28
28
  $ bundle
29
29
  ```
30
30
 
31
- ## Usage
32
-
33
- ### Configuring an API
34
-
35
- Let's say you'd like to connect your app to Dribbble.com - a community of designers sharing screenshots of their work, process, and projects.
36
-
37
- First, you would need to configure the API:
31
+ ## Quick Start
38
32
 
39
33
  ```ruby
34
+ # 1. Configure the API
40
35
  Jeckle.configure do |config|
41
36
  config.register :dribbble do |api|
42
37
  api.base_uri = 'http://api.dribbble.com'
38
+ api.bearer_token = ENV['DRIBBBLE_TOKEN']
43
39
  api.middlewares do
44
40
  response :json
41
+ response :jeckle_raise_error
45
42
  end
46
43
  end
47
44
  end
45
+
46
+ # 2. Define a resource
47
+ class Shot < Jeckle::Resource
48
+ api :dribbble
49
+
50
+ attribute :id, Jeckle::Types::Integer
51
+ attribute :name, Jeckle::Types::String
52
+ attribute :url, Jeckle::Types::String
53
+ end
54
+
55
+ # 3. Use it
56
+ shot = Shot.find(1600459)
57
+ shots = Shot.list(name: 'avengers')
48
58
  ```
49
59
 
50
- ### Mapping resources
60
+ ## API Configuration
51
61
 
52
- Following the previous example, Dribbble.com consists of pieces of web designers work called "Shots". Each shot has the attributes `id`, `name`, `url` and `image_url`. A Jeckle resource representing Dribbble's shots would be something like this:
62
+ ### Basic Auth
63
+
64
+ ```ruby
65
+ Jeckle.configure do |config|
66
+ config.register :my_api do |api|
67
+ api.base_uri = 'https://api.example.com'
68
+ api.basic_auth = { username: 'user', password: 'pass' }
69
+ end
70
+ end
71
+ ```
72
+
73
+ ### Bearer Token
74
+
75
+ ```ruby
76
+ config.register :my_api do |api|
77
+ api.base_uri = 'https://api.example.com'
78
+ api.bearer_token = 'my-oauth-token'
79
+ end
80
+ ```
81
+
82
+ ### API Key (Header)
83
+
84
+ ```ruby
85
+ config.register :my_api do |api|
86
+ api.base_uri = 'https://api.example.com'
87
+ api.api_key = { value: 'secret', header: 'X-Api-Key' }
88
+ end
89
+ ```
90
+
91
+ ### API Key (Query Param)
92
+
93
+ ```ruby
94
+ config.register :my_api do |api|
95
+ api.base_uri = 'https://api.example.com'
96
+ api.api_key = { value: 'secret', param: 'api_key' }
97
+ end
98
+ ```
99
+
100
+ ### OAuth 2.0
101
+
102
+ ```ruby
103
+ config.register :my_api do |api|
104
+ api.base_uri = 'https://api.example.com'
105
+ api.oauth2 = {
106
+ client_id: 'id',
107
+ client_secret: 'secret',
108
+ token_url: 'https://auth.example.com/oauth/token'
109
+ }
110
+ end
111
+ ```
112
+
113
+ ### Environment-Based Configuration
114
+
115
+ ```ruby
116
+ # Reads MY_API_BASE_URI, MY_API_BEARER_TOKEN from ENV
117
+ Jeckle::Setup.register_from_env(:my_api)
118
+ ```
119
+
120
+ ### Automatic Retries
121
+
122
+ ```ruby
123
+ config.register :my_api do |api|
124
+ api.base_uri = 'https://api.example.com'
125
+ api.retry = { max: 3, interval: 1, retry_statuses: [429, 503] }
126
+ end
127
+ ```
128
+
129
+ ### Other Options
130
+
131
+ ```ruby
132
+ config.register :my_api do |api|
133
+ api.base_uri = 'https://api.example.com'
134
+ api.namespaces = { prefix: 'api', version: 'v2' }
135
+ api.headers = { 'Content-Type' => 'application/json' }
136
+ api.params = { locale: 'en' }
137
+ api.open_timeout = 2
138
+ api.read_timeout = 5
139
+ api.logger = Rails.logger
140
+
141
+ api.middlewares do
142
+ request :json
143
+ response :json
144
+ response :jeckle_raise_error
145
+ end
146
+
147
+ # Extend Faraday defaults
148
+ api.configure_connection do |conn|
149
+ conn.use MyCustomMiddleware
150
+ conn.adapter :typhoeus
151
+ end
152
+ end
153
+ ```
154
+
155
+ ## Defining Resources
156
+
157
+ Resources inherit from `Jeckle::Resource` and use `Jeckle::Types` for attribute definitions:
53
158
 
54
159
  ```ruby
55
160
  class Shot < Jeckle::Resource
@@ -58,101 +163,252 @@ class Shot < Jeckle::Resource
58
163
  attribute :id, Jeckle::Types::Integer
59
164
  attribute :name, Jeckle::Types::String
60
165
  attribute :url, Jeckle::Types::String
61
- attribute :image_url, Jeckle::Types::String
166
+ attribute :score, Jeckle::Types::Float
62
167
  end
63
168
  ```
64
169
 
65
- ### Fetching data
170
+ Available types: `Jeckle::Types::Integer`, `String`, `Float`, `Bool`, `Array`, `Hash`, `DateTime`, `Time`, `Decimal`, `UUID`, `URI`, `SymbolizedHash`, `StringArray`, and any [dry-types](https://dry-rb.org/gems/dry-types/) type.
171
+
172
+ ## CRUD Operations
173
+
174
+ ### Find
175
+
176
+ ```ruby
177
+ # GET /shots/1600459
178
+ shot = Shot.find(1600459)
179
+ shot.name #=> "Daryl Heckle And Jeckle Oates"
180
+ ```
181
+
182
+ ### List
66
183
 
67
- The resource class allows us to list shots through HTTP requests to the API, based on the provided information. For example, we can find a specific shot by providing its id to the `find` method:
184
+ ```ruby
185
+ # GET /shots?name=avengers
186
+ shots = Shot.list(name: 'avengers')
187
+ ```
188
+
189
+ ### Create
68
190
 
69
191
  ```ruby
70
- # GET http://api.dribbble.com/shots/1600459
71
- shot = Shot.find 1600459
192
+ # POST /shots
193
+ shot = Shot.create(name: 'New Shot', url: 'http://example.com')
72
194
  ```
73
195
 
74
- That will return a `Shot` instance, containing the shot info:
196
+ ### Update
75
197
 
76
198
  ```ruby
77
- shot.id
78
- => 1600459
199
+ # PATCH /shots/123
200
+ shot = Shot.update(123, name: 'Updated Name')
201
+ ```
79
202
 
80
- shot.name
81
- => "Daryl Heckle And Jeckle Oates"
203
+ ### Destroy
82
204
 
83
- shot.image_url
84
- => "https://d13yacurqjgara.cloudfront.net/users/85699/screenshots/1600459/daryl_heckle_and_jeckle_oates-dribble.jpg"
205
+ ```ruby
206
+ # DELETE /shots/123
207
+ Shot.destroy(123) #=> true
85
208
  ```
86
209
 
87
- You can also look for many shots matching one or more attributes, by using the `list` method:
210
+ ### Instance-Level Operations
88
211
 
89
212
  ```ruby
90
- # GET http://api.dribbble.com/shots?name=avengers
91
- shots = Shot.list name: 'avengers'
213
+ shot = Shot.find(123)
214
+ updated = shot.save # PATCH /shots/123
215
+ fresh = shot.reload # GET /shots/123
216
+ shot.delete # DELETE /shots/123
92
217
  ```
93
218
 
94
- ### Attribute Aliasing
219
+ ## Composable Operations
95
220
 
96
- Sometimes you want to call the API's attributes something else, either because their names aren't very concise or because they're out of you app's convention. If that's the case, you can add an `as` option:
221
+ By default, resources get all CRUD operations. For fine-grained control, extend individual modules:
97
222
 
98
223
  ```ruby
99
- attribute :thumbnailSize, Jeckle::Types::String, as: :thumbnail_size
224
+ class ReadOnlyShot < Jeckle::Resource
225
+ api :dribbble
226
+ extend Jeckle::Operations::Find
227
+ extend Jeckle::Operations::List
228
+ # No Create, Update, or Delete
229
+ end
100
230
  ```
101
231
 
102
- Both mapping will work:
232
+ Available modules: `Jeckle::Operations::Find`, `List`, `Create`, `Update`, `Delete`.
233
+
234
+ ## Nested Resources
103
235
 
104
236
  ```ruby
105
- shot.thumbnailSize
106
- => "50x50"
237
+ class Comment < Jeckle::Resource
238
+ api :my_api
239
+ belongs_to :post
107
240
 
108
- shot.thumbnail_size
109
- => "50x50"
241
+ attribute :id, Jeckle::Types::Integer
242
+ attribute :body, Jeckle::Types::String
243
+ end
244
+
245
+ Comment.find(456, post_id: 123) # GET /posts/123/comments/456
246
+ Comment.list(post_id: 123) # GET /posts/123/comments
247
+ Comment.create(post_id: 123, body: 'Nice') # POST /posts/123/comments
110
248
  ```
111
249
 
112
- ### Error Handling
250
+ ## Attribute Aliasing
113
251
 
114
- Jeckle provides a built-in Faraday middleware that automatically raises typed errors for HTTP error responses. Enable it in your API configuration:
252
+ Map API attribute names to Ruby-friendly names:
115
253
 
116
254
  ```ruby
117
- Jeckle.configure do |config|
118
- config.register :dribbble do |api|
119
- api.base_uri = 'http://api.dribbble.com'
120
- api.middlewares do
121
- response :json
122
- response :jeckle_raise_error
123
- end
124
- end
255
+ class Shot < Jeckle::Resource
256
+ api :dribbble
257
+ attribute :thumbnailSize, Jeckle::Types::String, as: :thumbnail_size
258
+ end
259
+
260
+ shot.thumbnailSize #=> "50x50"
261
+ shot.thumbnail_size #=> "50x50"
262
+ ```
263
+
264
+ ## Error Handling
265
+
266
+ Enable the error middleware to get typed exceptions:
267
+
268
+ ```ruby
269
+ api.middlewares do
270
+ response :json
271
+ response :jeckle_raise_error
125
272
  end
126
273
  ```
127
274
 
128
- Then rescue specific errors in your code:
275
+ Then rescue specific errors:
129
276
 
130
277
  ```ruby
131
278
  begin
132
- Shot.find 999
279
+ Shot.find(999)
133
280
  rescue Jeckle::NotFoundError => e
134
281
  puts "Not found: #{e.message} (status: #{e.status})"
282
+ puts "Request ID: #{e.request_id}" if e.request_id
283
+ rescue Jeckle::TooManyRequestsError => e
284
+ puts "Rate limited! Remaining: #{e.rate_limit&.remaining}"
135
285
  rescue Jeckle::ClientError => e
136
286
  puts "Client error: #{e.status}"
137
287
  rescue Jeckle::ServerError => e
138
288
  puts "Server error: #{e.status}"
139
- rescue Jeckle::HTTPError => e
140
- puts "HTTP error: #{e.status}"
141
289
  end
142
290
  ```
143
291
 
144
- The error hierarchy:
292
+ Error hierarchy:
145
293
 
146
- - `Jeckle::Error` base error
147
- - `Jeckle::ConnectionError` network connectivity errors
148
- - `Jeckle::TimeoutError` request timeout errors
149
- - `Jeckle::HTTPError` HTTP errors (has `status` and `body` attributes)
150
- - `Jeckle::ClientError` 4xx errors
294
+ - `Jeckle::Error` -- base error
295
+ - `Jeckle::ConnectionError` -- network errors
296
+ - `Jeckle::TimeoutError` -- timeout errors
297
+ - `Jeckle::HTTPError` -- HTTP errors (`status`, `body`, `request_id`)
298
+ - `Jeckle::ClientError` -- 4xx
151
299
  - `BadRequestError` (400), `UnauthorizedError` (401), `ForbiddenError` (403), `NotFoundError` (404), `UnprocessableEntityError` (422), `TooManyRequestsError` (429)
152
- - `Jeckle::ServerError` 5xx errors
300
+ - `Jeckle::ServerError` -- 5xx
153
301
  - `InternalServerError` (500), `ServiceUnavailableError` (503)
154
302
 
155
- We're all set! Now we can expand the mapping of our API, e.g to add ability to search Dribbble Designer directory by adding Designer class, or we can expand the original mapping of Shot class to include more attributes, such as tags or comments.
303
+ ## Pagination
304
+
305
+ ### Offset-Based (Default)
306
+
307
+ ```ruby
308
+ Shot.list_each(per_page: 10).each do |shot|
309
+ puts shot.name
310
+ end
311
+
312
+ # Works with Enumerable methods
313
+ Shot.list_each(per_page: 50).first(5)
314
+ ```
315
+
316
+ ### Cursor-Based
317
+
318
+ ```ruby
319
+ config.register :stripe do |api|
320
+ api.base_uri = 'https://api.stripe.com/v1'
321
+ api.pagination :cursor, cursor_param: :starting_after, limit_param: :limit
322
+ end
323
+ ```
324
+
325
+ ### Link Header (GitHub-Style)
326
+
327
+ ```ruby
328
+ config.register :github do |api|
329
+ api.base_uri = 'https://api.github.com'
330
+ api.pagination :link_header, per_page_param: :per_page
331
+ end
332
+ ```
333
+
334
+ ## Instance-Based Client
335
+
336
+ Use `Jeckle::Client` for requests with different credentials:
337
+
338
+ ```ruby
339
+ client = Jeckle::Client.new(:my_api, bearer_token: 'other-token')
340
+ client.find(Shot, 123)
341
+ client.list(Shot, name: 'avengers')
342
+ client.create(Shot, name: 'New Shot')
343
+ ```
344
+
345
+ ## Response Inspection
346
+
347
+ Access the raw HTTP response after API calls:
348
+
349
+ ```ruby
350
+ shot = Shot.find(123)
351
+ shot._response.status #=> 200
352
+ shot._response.headers #=> { 'X-Request-Id' => '...' }
353
+ ```
354
+
355
+ ## Observability
356
+
357
+ ### Instrumentation
358
+
359
+ Enable `ActiveSupport::Notifications` events:
360
+
361
+ ```ruby
362
+ api.middlewares do
363
+ request :jeckle_instrumentation
364
+ end
365
+
366
+ ActiveSupport::Notifications.subscribe('request.jeckle') do |*args|
367
+ event = ActiveSupport::Notifications::Event.new(*args)
368
+ puts "#{event.payload[:method]} #{event.payload[:url]} => #{event.payload[:status]}"
369
+ end
370
+ ```
371
+
372
+ ### Log Redaction
373
+
374
+ Redact sensitive headers in log output:
375
+
376
+ ```ruby
377
+ redactor = Jeckle::Middleware::LogRedactor.new(
378
+ headers: %w[Authorization X-Api-Key],
379
+ patterns: [/password/i]
380
+ )
381
+ safe_headers = redactor.redact_headers(response.headers)
382
+ ```
383
+
384
+ ## Testing
385
+
386
+ Use test mode to stub HTTP requests without real network calls:
387
+
388
+ ```ruby
389
+ # In your test setup
390
+ Jeckle.test_mode!
391
+
392
+ Jeckle.stub_request(:my_api, :get, 'shots/1', body: { id: 1, name: 'Test' })
393
+
394
+ shot = Shot.find(1) #=> uses stubbed response
395
+
396
+ # Cleanup
397
+ Jeckle.reset_test_stubs!
398
+ ```
399
+
400
+ ## Credential Providers
401
+
402
+ Chain multiple credential sources (AWS SDK pattern):
403
+
404
+ ```ruby
405
+ chain = Jeckle::Auth::CredentialChain.new(
406
+ -> { ENV['MY_API_TOKEN'] },
407
+ -> { File.read(File.expand_path('~/.my_api/token')).strip rescue nil },
408
+ -> { 'fallback-token' }
409
+ )
410
+ api.bearer_token = chain.resolve
411
+ ```
156
412
 
157
413
  ## Migration from 0.4.x
158
414
 
@@ -167,7 +423,7 @@ class Shot
167
423
  attribute :id, Integer
168
424
  end
169
425
 
170
- # After (0.6.0+)
426
+ # After (1.0.0)
171
427
  class Shot < Jeckle::Resource
172
428
  attribute :id, Jeckle::Types::Integer
173
429
  end
data/lib/jeckle/api.rb CHANGED
@@ -1,11 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jeckle
4
+ # Holds configuration for a single API endpoint including base URI,
5
+ # authentication, headers, params, timeouts, and Faraday middlewares.
4
6
  class API
7
+ # @return [Logger, nil] logger for request/response logging
5
8
  attr_accessor :logger
9
+
10
+ # @!attribute [w] base_uri
11
+ # @param value [String] the base URL for the API
12
+ # @!attribute [w] namespaces
13
+ # @param value [Hash] URL path segments appended to base_uri
14
+ # @!attribute [w] params
15
+ # @param value [Hash] default query parameters for all requests
16
+ # @!attribute [w] headers
17
+ # @param value [Hash] default headers for all requests
18
+ # @!attribute [w] open_timeout
19
+ # @param value [Integer] connection open timeout in seconds
20
+ # @!attribute [w] read_timeout
21
+ # @param value [Integer] read timeout in seconds
6
22
  attr_writer :base_uri, :namespaces, :params, :headers, :open_timeout, :read_timeout
7
- attr_reader :basic_auth, :request_timeout
8
23
 
24
+ # @return [Hash, nil] basic auth credentials
25
+ # @return [Integer, nil] request timeout
26
+ # @return [String, nil] bearer token for Authorization header
27
+ # @return [Hash, nil] API key configuration
28
+ # @return [Hash, nil] retry configuration for faraday-retry
29
+ # @return [#paginate, #next_context, nil] pagination strategy for collections
30
+ # @return [Jeckle::Auth::OAuth2, nil] OAuth 2.0 configuration
31
+ attr_reader :basic_auth, :request_timeout, :bearer_token, :api_key, :retry_options,
32
+ :pagination_strategy, :oauth2
33
+
34
+ # Returns or builds a configured Faraday connection.
35
+ #
36
+ # @return [Faraday::Connection]
9
37
  def connection
10
38
  @connection ||= Faraday.new(url: base_uri, request: timeout).tap do |conn|
11
39
  conn.headers = headers
@@ -13,10 +41,28 @@ module Jeckle
13
41
  conn.response :logger, logger
14
42
 
15
43
  conn.request :authorization, :basic, basic_auth[:username], basic_auth[:password] if basic_auth
44
+ conn.request :authorization, :Bearer, bearer_token if bearer_token
45
+ conn.request :authorization, :Bearer, -> { oauth2.token } if oauth2
46
+ conn.request :retry, retry_options if retry_options
47
+
48
+ if api_key
49
+ if api_key[:header]
50
+ conn.headers[api_key[:header]] = api_key[:value]
51
+ elsif api_key[:param]
52
+ conn.params[api_key[:param]] = api_key[:value]
53
+ end
54
+ end
55
+
16
56
  conn.instance_exec(&@middlewares_block) if @middlewares_block
57
+ @connection_customizer&.call(conn)
17
58
  end
18
59
  end
19
60
 
61
+ # Set basic authentication credentials.
62
+ #
63
+ # @param credential_params [Hash] must contain :username and :password
64
+ # @raise [Jeckle::NoUsernameOrPasswordError] if keys are missing
65
+ # @return [Hash]
20
66
  def basic_auth=(credential_params)
21
67
  %i[username password].all? do |key|
22
68
  credential_params.key? key
@@ -25,28 +71,161 @@ module Jeckle
25
71
  @basic_auth = credential_params
26
72
  end
27
73
 
74
+ # Set bearer token authentication. Resets cached connection.
75
+ #
76
+ # @param token [String] the bearer token
77
+ # @return [String]
78
+ def bearer_token=(token)
79
+ @connection = nil
80
+ @bearer_token = token
81
+ end
82
+
83
+ # Configure automatic retries using faraday-retry. Resets cached connection.
84
+ #
85
+ # @param options [Hash] retry options passed to Faraday::Retry::Middleware
86
+ # @option options [Integer] :max (2) maximum number of retries
87
+ # @option options [Float] :interval (0.5) initial interval between retries in seconds
88
+ # @option options [Float] :interval_randomness (0.5) randomness factor for retry interval
89
+ # @option options [Integer] :backoff_factor (2) exponential backoff multiplier
90
+ # @option options [Array<Integer>] :retry_statuses ([429, 500, 502, 503]) HTTP status codes to retry
91
+ # @return [Hash]
92
+ #
93
+ # @example
94
+ # api.retry = { max: 3, interval: 1, retry_statuses: [429, 503] }
95
+ def retry=(options)
96
+ @connection = nil
97
+ @retry_options = DEFAULT_RETRY_OPTIONS.merge(options)
98
+ end
99
+
100
+ # Default retry configuration.
101
+ DEFAULT_RETRY_OPTIONS = {
102
+ max: 2,
103
+ interval: 0.5,
104
+ interval_randomness: 0.5,
105
+ backoff_factor: 2,
106
+ retry_statuses: [429, 500, 502, 503]
107
+ }.freeze
108
+
109
+ # Set API key authentication. Resets cached connection.
110
+ #
111
+ # @param config [Hash] must contain :value and either :header or :param
112
+ # @raise [Jeckle::ArgumentError] if config is invalid
113
+ # @return [Hash]
114
+ #
115
+ # @example Header-based API key
116
+ # api.api_key = { value: 'secret', header: 'X-Api-Key' }
117
+ #
118
+ # @example Query param-based API key
119
+ # api.api_key = { value: 'secret', param: 'api_key' }
120
+ def api_key=(config)
121
+ unless config.is_a?(Hash) && config[:value] && (config[:header] || config[:param])
122
+ raise Jeckle::ArgumentError, 'api_key requires :value and either :header or :param'
123
+ end
124
+
125
+ @connection = nil
126
+ @api_key = config
127
+ end
128
+
129
+ # Returns the full base URI including namespace segments.
130
+ #
131
+ # @return [String]
28
132
  def base_uri
29
133
  [@base_uri, *namespaces.values].join '/'
30
134
  end
31
135
 
136
+ # @return [Hash] default query parameters
32
137
  def params
33
138
  @params || {}
34
139
  end
35
140
 
141
+ # @return [Hash] default headers
36
142
  def headers
37
143
  @headers || {}
38
144
  end
39
145
 
146
+ # @return [Hash] URL namespace segments
40
147
  def namespaces
41
148
  @namespaces || {}
42
149
  end
43
150
 
151
+ # Set OAuth 2.0 client credentials authentication. Resets cached connection.
152
+ # The token is fetched lazily on the first request.
153
+ #
154
+ # @param config [Hash] OAuth 2.0 configuration
155
+ # @option config [String] :client_id OAuth client ID
156
+ # @option config [String] :client_secret OAuth client secret
157
+ # @option config [String] :token_url token endpoint URL
158
+ # @option config [String] :scope (nil) requested scope
159
+ # @return [Jeckle::Auth::OAuth2]
160
+ #
161
+ # @example
162
+ # api.oauth2 = {
163
+ # client_id: 'id', client_secret: 'secret',
164
+ # token_url: 'https://auth.example.com/oauth/token'
165
+ # }
166
+ def oauth2=(config)
167
+ @connection = nil
168
+ @oauth2 = Jeckle::Auth::OAuth2.new(**config)
169
+ end
170
+
171
+ # Configure the pagination strategy for this API.
172
+ #
173
+ # @param strategy [Symbol, #paginate] :offset, :cursor, :link_header, or a strategy instance
174
+ # @param options [Hash] options passed to the built-in strategy constructor
175
+ # @return [#paginate, #next_context]
176
+ #
177
+ # @example Cursor-based pagination (Stripe-style)
178
+ # api.pagination :cursor, cursor_param: :starting_after, limit_param: :limit
179
+ #
180
+ # @example Link header pagination (GitHub-style)
181
+ # api.pagination :link_header
182
+ #
183
+ # @example Custom strategy instance
184
+ # api.pagination MyCustomStrategy.new
185
+ def pagination(strategy, **options)
186
+ @pagination_strategy = case strategy
187
+ when :offset then Jeckle::Pagination::Offset.new(**options)
188
+ when :cursor then Jeckle::Pagination::Cursor.new(**options)
189
+ when :link_header then Jeckle::Pagination::LinkHeader.new(**options)
190
+ else strategy
191
+ end
192
+ end
193
+
194
+ # Configure Faraday middlewares for this API.
195
+ #
196
+ # @yield block evaluated in the context of the Faraday connection builder
197
+ # @raise [Jeckle::ArgumentError] if no block is given
198
+ #
199
+ # @example
200
+ # api.middlewares do
201
+ # request :json
202
+ # response :json
203
+ # response :jeckle_raise_error
204
+ # end
44
205
  def middlewares(&block)
45
206
  raise Jeckle::ArgumentError, 'A block is required when configuring API middlewares' unless block_given?
46
207
 
47
208
  @middlewares_block = block
48
209
  end
49
210
 
211
+ # Customize the Faraday connection after Jeckle's defaults are applied.
212
+ # The block receives the Faraday connection and can add middleware,
213
+ # override the adapter, etc.
214
+ #
215
+ # @yield [Faraday::Connection] the connection after default setup
216
+ #
217
+ # @example
218
+ # api.configure_connection do |conn|
219
+ # conn.use MyCustomMiddleware
220
+ # conn.adapter :typhoeus
221
+ # end
222
+ def configure_connection(&block)
223
+ @connection_customizer = block
224
+ end
225
+
226
+ # Returns timeout configuration hash for Faraday.
227
+ #
228
+ # @return [Hash]
50
229
  def timeout
51
230
  {}.tap do |t|
52
231
  t[:open_timeout] = @open_timeout if @open_timeout