petstore_api_client 0.1.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +33 -0
  3. data/.env.example +50 -0
  4. data/.github/CODEOWNERS +36 -0
  5. data/.github/workflows/ci.yml +157 -0
  6. data/.ruby-version +1 -0
  7. data/CONTRIBUTORS.md +39 -0
  8. data/LICENSE +21 -0
  9. data/README.md +684 -0
  10. data/Rakefile +12 -0
  11. data/lib/petstore_api_client/api_client.rb +60 -0
  12. data/lib/petstore_api_client/authentication/api_key.rb +107 -0
  13. data/lib/petstore_api_client/authentication/base.rb +113 -0
  14. data/lib/petstore_api_client/authentication/composite.rb +178 -0
  15. data/lib/petstore_api_client/authentication/none.rb +42 -0
  16. data/lib/petstore_api_client/authentication/oauth2.rb +305 -0
  17. data/lib/petstore_api_client/client.rb +87 -0
  18. data/lib/petstore_api_client/clients/concerns/pagination.rb +124 -0
  19. data/lib/petstore_api_client/clients/concerns/resource_operations.rb +121 -0
  20. data/lib/petstore_api_client/clients/pet_client.rb +119 -0
  21. data/lib/petstore_api_client/clients/store_client.rb +37 -0
  22. data/lib/petstore_api_client/configuration.rb +318 -0
  23. data/lib/petstore_api_client/connection.rb +55 -0
  24. data/lib/petstore_api_client/errors.rb +70 -0
  25. data/lib/petstore_api_client/middleware/authentication.rb +44 -0
  26. data/lib/petstore_api_client/models/api_response.rb +31 -0
  27. data/lib/petstore_api_client/models/base.rb +60 -0
  28. data/lib/petstore_api_client/models/category.rb +17 -0
  29. data/lib/petstore_api_client/models/named_entity.rb +36 -0
  30. data/lib/petstore_api_client/models/order.rb +55 -0
  31. data/lib/petstore_api_client/models/pet.rb +225 -0
  32. data/lib/petstore_api_client/models/tag.rb +20 -0
  33. data/lib/petstore_api_client/paginated_collection.rb +133 -0
  34. data/lib/petstore_api_client/request.rb +225 -0
  35. data/lib/petstore_api_client/response.rb +193 -0
  36. data/lib/petstore_api_client/validators/array_presence_validator.rb +15 -0
  37. data/lib/petstore_api_client/validators/enum_validator.rb +17 -0
  38. data/lib/petstore_api_client/version.rb +5 -0
  39. data/lib/petstore_api_client.rb +55 -0
  40. metadata +252 -0
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "concerns/resource_operations"
4
+ require_relative "concerns/pagination"
5
+
6
+ module PetstoreApiClient
7
+ module Clients
8
+ # Client for Pet-related API endpoints
9
+ # Handles creation, retrieval, updating, and deletion of pets
10
+ #
11
+ # Refactored to use ResourceOperations concern (Template Method pattern)
12
+ # which eliminates ~50 lines of duplication with StoreClient
13
+ class PetClient < Client
14
+ include Concerns::ResourceOperations
15
+ include Concerns::Pagination
16
+
17
+ # Public API methods - delegate to generic resource operations
18
+ alias create_pet create_resource
19
+ alias update_pet update_resource
20
+ alias get_pet get_resource
21
+ alias delete_pet delete_resource
22
+
23
+ # Find pets by status with optional pagination
24
+ #
25
+ # @param status [String, Array<String>] Status value(s) to filter by
26
+ # Valid values: "available", "pending", "sold"
27
+ # @param options [Hash] Optional pagination and filter options
28
+ # @option options [Integer] :page Page number (default: 1)
29
+ # @option options [Integer] :per_page Items per page (default: 25)
30
+ # @option options [Integer] :offset Alternative: offset for results
31
+ # @option options [Integer] :limit Alternative: limit for results
32
+ # @return [PaginatedCollection<Pet>] Paginated collection of pets
33
+ #
34
+ # @example
35
+ # # Get first page of available pets
36
+ # pets = client.pets.find_by_status("available")
37
+ #
38
+ # # Get second page with custom page size
39
+ # pets = client.pets.find_by_status("available", page: 2, per_page: 50)
40
+ #
41
+ # # Search multiple statuses
42
+ # pets = client.pets.find_by_status(["available", "pending"])
43
+ def find_by_status(status, options = {})
44
+ # Validate status values
45
+ statuses = Array(status)
46
+ validate_statuses!(statuses)
47
+
48
+ # Make API request (returns all matching pets)
49
+ params = { status: statuses.join(",") }
50
+ resp = get("pet/findByStatus", params: params)
51
+
52
+ # Parse response into Pet objects
53
+ pets = Array(resp.body).map { |pet_data| Models::Pet.from_response(pet_data) }
54
+
55
+ # Apply client-side pagination (API doesn't support server-side pagination)
56
+ apply_client_side_pagination(pets, options)
57
+ end
58
+
59
+ # Find pets by tags with optional pagination
60
+ #
61
+ # @param tags [String, Array<String>] Tag value(s) to filter by
62
+ # @param options [Hash] Optional pagination options
63
+ # @option options [Integer] :page Page number (default: 1)
64
+ # @option options [Integer] :per_page Items per page (default: 25)
65
+ # @return [PaginatedCollection<Pet>] Paginated collection of pets
66
+ #
67
+ # @example
68
+ # # Find pets with specific tag
69
+ # pets = client.pets.find_by_tags("friendly")
70
+ #
71
+ # # Find pets with multiple tags
72
+ # pets = client.pets.find_by_tags(["friendly", "vaccinated"], page: 1, per_page: 10)
73
+ def find_by_tags(tags, options = {})
74
+ # Ensure tags is an array
75
+ tag_array = Array(tags)
76
+ raise ValidationError, "Tags cannot be empty" if tag_array.empty?
77
+
78
+ # Make API request
79
+ params = { tags: tag_array.join(",") }
80
+ resp = get("pet/findByTags", params: params)
81
+
82
+ # Parse response into Pet objects
83
+ pets = Array(resp.body).map { |pet_data| Models::Pet.from_response(pet_data) }
84
+
85
+ # Apply client-side pagination
86
+ apply_client_side_pagination(pets, options)
87
+ end
88
+
89
+ private
90
+
91
+ # Template method implementation: Define the model class
92
+ def model_class
93
+ Models::Pet
94
+ end
95
+
96
+ # Template method implementation: Define the resource name for API paths
97
+ def resource_name
98
+ "pet"
99
+ end
100
+
101
+ # Template method implementation: Human-readable name for validation errors
102
+ def id_field_name
103
+ "Pet ID"
104
+ end
105
+
106
+ # Validate status values against allowed values
107
+ def validate_statuses!(statuses)
108
+ # Performance optimization: use constant instead of creating new array
109
+ invalid = statuses.reject { |s| Models::Pet::VALID_STATUSES.include?(s.to_s) }
110
+
111
+ return if invalid.empty?
112
+
113
+ raise ValidationError,
114
+ "Invalid status value(s): #{invalid.join(", ")}. " \
115
+ "Must be one of: #{Models::Pet::VALID_STATUSES.join(", ")}"
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "concerns/resource_operations"
4
+
5
+ module PetstoreApiClient
6
+ module Clients
7
+ # Store client - handles order operations
8
+ #
9
+ # Refactored to use ResourceOperations concern (Template Method pattern)
10
+ # which eliminates ~40 lines of duplication with PetClient
11
+ class StoreClient < Client
12
+ include Concerns::ResourceOperations
13
+
14
+ # Public API methods - delegate to generic resource operations
15
+ alias create_order create_resource
16
+ alias get_order get_resource
17
+ alias delete_order delete_resource
18
+
19
+ private
20
+
21
+ # Template method implementation: Define the model class
22
+ def model_class
23
+ Models::Order
24
+ end
25
+
26
+ # Template method implementation: Define the resource name for API paths
27
+ def resource_name
28
+ "store/order"
29
+ end
30
+
31
+ # Template method implementation: Human-readable name for validation errors
32
+ def id_field_name
33
+ "Order ID"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "authentication/api_key"
4
+ require_relative "authentication/oauth2"
5
+ require_relative "authentication/composite"
6
+ require_relative "authentication/none"
7
+
8
+ module PetstoreApiClient
9
+ # Configuration management for Petstore API client
10
+ #
11
+ # Centralized configuration for all client settings including authentication,
12
+ # timeouts, retries, and pagination. Supports multiple authentication strategies
13
+ # via feature flags.
14
+ #
15
+ # @example Basic configuration
16
+ # config = Configuration.new
17
+ # config.api_key = "special-key"
18
+ # config.timeout = 60
19
+ #
20
+ # @example Block-based configuration
21
+ # config = Configuration.new
22
+ # config.configure do |c|
23
+ # c.auth_strategy = :oauth2
24
+ # c.oauth2_client_id = "my-id"
25
+ # c.oauth2_client_secret = "my-secret"
26
+ # end
27
+ #
28
+ # @since 0.1.0
29
+ # rubocop:disable Metrics/ClassLength
30
+ class Configuration
31
+ # Default API base URL for Petstore API
32
+ DEFAULT_BASE_URL = "https://petstore.swagger.io/v2"
33
+
34
+ # Default number of items per page for paginated endpoints
35
+ DEFAULT_PAGE_SIZE = 25
36
+
37
+ # Maximum allowed page size to prevent abuse
38
+ MAX_PAGE_SIZE = 100
39
+
40
+ # Valid authentication strategy options
41
+ VALID_AUTH_STRATEGIES = %i[none api_key oauth2 both].freeze
42
+
43
+ # @!attribute [rw] base_url
44
+ # @return [String] Base URL for API endpoints
45
+ # @!attribute [rw] timeout
46
+ # @return [Integer] Request timeout in seconds
47
+ # @!attribute [rw] open_timeout
48
+ # @return [Integer] Connection open timeout in seconds
49
+ # @!attribute [rw] retry_enabled
50
+ # @return [Boolean] Enable automatic retry for transient failures
51
+ # @!attribute [rw] max_retries
52
+ # @return [Integer] Maximum number of retry attempts
53
+ # @!attribute [rw] default_page_size
54
+ # @return [Integer] Default items per page for pagination
55
+ # @!attribute [rw] max_page_size
56
+ # @return [Integer] Maximum items per page for pagination
57
+ attr_accessor :base_url, :timeout, :open_timeout, :retry_enabled, :max_retries,
58
+ :default_page_size, :max_page_size
59
+
60
+ # @!attribute [r] api_key
61
+ # @return [String, nil] API key for authentication (read-only, use setter)
62
+ attr_reader :api_key
63
+
64
+ # @!attribute [rw] auth_strategy
65
+ # @return [Symbol] Authentication strategy (:none, :api_key, :oauth2, :both)
66
+ attr_accessor :auth_strategy
67
+
68
+ # @!attribute [rw] oauth2_client_id
69
+ # @return [String, nil] OAuth2 client ID
70
+ # @!attribute [rw] oauth2_client_secret
71
+ # @return [String, nil] OAuth2 client secret
72
+ # @!attribute [rw] oauth2_token_url
73
+ # @return [String, nil] OAuth2 token endpoint URL
74
+ # @!attribute [rw] oauth2_scope
75
+ # @return [String, nil] OAuth2 scope (space-separated permissions)
76
+ attr_accessor :oauth2_client_id, :oauth2_client_secret, :oauth2_token_url, :oauth2_scope
77
+
78
+ # Initialize configuration with default values
79
+ #
80
+ # Sets sensible defaults for all configuration options:
81
+ # - base_url: Petstore API endpoint
82
+ # - timeout: 30 seconds
83
+ # - retry_enabled: true
84
+ # - auth_strategy: :api_key (backward compatible)
85
+ #
86
+ # @example
87
+ # config = Configuration.new
88
+ # config.base_url # => "https://petstore.swagger.io/v2"
89
+ # config.timeout # => 30
90
+ #
91
+ def initialize
92
+ @base_url = DEFAULT_BASE_URL
93
+ @api_key = nil
94
+ @timeout = 30 # seconds
95
+ @open_timeout = 10 # seconds
96
+ @retry_enabled = true # Auto-retry transient failures
97
+ @max_retries = 2 # Number of retries for failed requests
98
+ @default_page_size = DEFAULT_PAGE_SIZE # Default items per page for pagination
99
+ @max_page_size = MAX_PAGE_SIZE # Maximum items per page (prevents abuse)
100
+ @auth_strategy = :api_key # Default strategy for backward compatibility
101
+
102
+ # OAuth2 credentials
103
+ @oauth2_client_id = nil
104
+ @oauth2_client_secret = nil
105
+ @oauth2_token_url = nil
106
+ @oauth2_scope = nil
107
+ end
108
+
109
+ # Set API key for authentication
110
+ #
111
+ # Supports loading from environment variable using :from_env symbol.
112
+ #
113
+ # @param value [String, Symbol, nil] The API key or :from_env
114
+ #
115
+ # @example Direct setting
116
+ # config.api_key = "special-key"
117
+ #
118
+ # @example From environment variable
119
+ # ENV['PETSTORE_API_KEY'] = "special-key"
120
+ # config.api_key = :from_env
121
+ #
122
+ def api_key=(value)
123
+ @api_key = if value == :from_env
124
+ ENV.fetch(Authentication::ApiKey::ENV_VAR_NAME, nil)
125
+ else
126
+ value
127
+ end
128
+ end
129
+
130
+ # Build authenticator instance based on auth_strategy
131
+ #
132
+ # Returns appropriate authentication strategy based on auth_strategy setting:
133
+ # - :none - No authentication
134
+ # - :api_key - API Key authentication only
135
+ # - :oauth2 - OAuth2 authentication only
136
+ # - :both - Both API Key AND OAuth2 (composite)
137
+ #
138
+ # The authenticator is memoized and reused until reset_authenticator! is called.
139
+ #
140
+ # @return [Authentication::Base] Authentication strategy instance
141
+ #
142
+ # @raise [ConfigurationError] if auth_strategy is invalid
143
+ #
144
+ # @example
145
+ # config.auth_strategy = :oauth2
146
+ # config.authenticator # => #<OAuth2 client_id=...>
147
+ #
148
+ def authenticator
149
+ @authenticator ||= build_authenticator
150
+ end
151
+
152
+ # Reset memoized authenticator
153
+ #
154
+ # Call this when configuration changes to rebuild the authenticator
155
+ # with new settings.
156
+ #
157
+ # @return [void]
158
+ #
159
+ # @example
160
+ # config.auth_strategy = :api_key
161
+ # config.authenticator # => ApiKey instance
162
+ # config.auth_strategy = :oauth2
163
+ # config.reset_authenticator!
164
+ # config.authenticator # => OAuth2 instance
165
+ #
166
+ def reset_authenticator!
167
+ @authenticator = nil
168
+ end
169
+
170
+ # Configure settings via block
171
+ #
172
+ # Yields self to the block for convenient configuration.
173
+ # Automatically resets authenticator after configuration.
174
+ #
175
+ # @yieldparam [Configuration] self
176
+ # @return [Configuration] self for method chaining
177
+ #
178
+ # @example
179
+ # config.configure do |c|
180
+ # c.api_key = "special-key"
181
+ # c.timeout = 60
182
+ # c.retry_enabled = false
183
+ # end
184
+ #
185
+ def configure
186
+ yield(self) if block_given?
187
+ reset_authenticator! # Rebuild authenticator when config changes
188
+ self
189
+ end
190
+
191
+ # Validate configuration settings
192
+ #
193
+ # Checks that all required configuration is valid.
194
+ # Currently validates:
195
+ # - base_url is present
196
+ # - authenticator is properly configured (if auth is enabled)
197
+ #
198
+ # @return [Boolean] true if valid
199
+ # @raise [ValidationError] if configuration is invalid
200
+ #
201
+ # @example
202
+ # config = Configuration.new
203
+ # config.validate! # => true
204
+ #
205
+ # config.base_url = nil
206
+ # config.validate! # raises ValidationError
207
+ #
208
+ def validate!
209
+ raise ValidationError, "base_url can't be nil" if base_url.nil? || base_url.empty?
210
+
211
+ # Validate authenticator if configured
212
+ authenticator.is_a?(Authentication::ApiKey) && authenticator.configured?
213
+
214
+ true
215
+ end
216
+
217
+ private
218
+
219
+ # Build authentication strategy based on auth_strategy setting
220
+ #
221
+ # @return [Authentication::Base] Authentication strategy instance
222
+ # @raise [ConfigurationError] if auth_strategy is invalid
223
+ #
224
+ def build_authenticator
225
+ case @auth_strategy
226
+ when :none
227
+ build_none_authenticator
228
+ when :api_key
229
+ build_api_key_authenticator
230
+ when :oauth2
231
+ build_oauth2_authenticator
232
+ when :both
233
+ build_composite_authenticator
234
+ else
235
+ raise ConfigurationError,
236
+ "Invalid auth_strategy: #{@auth_strategy.inspect}. " \
237
+ "Must be one of: #{VALID_AUTH_STRATEGIES.map(&:inspect).join(", ")}"
238
+ end
239
+ end
240
+
241
+ # Build None authenticator (no authentication)
242
+ #
243
+ # @return [Authentication::None]
244
+ #
245
+ def build_none_authenticator
246
+ Authentication::None.new
247
+ end
248
+
249
+ # Build API Key authenticator
250
+ #
251
+ # Returns None authenticator if api_key is not configured.
252
+ #
253
+ # @return [Authentication::ApiKey, Authentication::None]
254
+ #
255
+ def build_api_key_authenticator
256
+ if api_key.nil? || api_key.to_s.strip.empty?
257
+ Authentication::None.new
258
+ else
259
+ Authentication::ApiKey.new(api_key)
260
+ end
261
+ end
262
+
263
+ # Build OAuth2 authenticator
264
+ #
265
+ # Returns None authenticator if OAuth2 credentials are not configured.
266
+ #
267
+ # @return [Authentication::OAuth2, Authentication::None]
268
+ #
269
+ def build_oauth2_authenticator
270
+ if oauth2_configured?
271
+ Authentication::OAuth2.new(
272
+ client_id: @oauth2_client_id,
273
+ client_secret: @oauth2_client_secret,
274
+ token_url: @oauth2_token_url || Authentication::OAuth2::DEFAULT_TOKEN_URL,
275
+ scope: @oauth2_scope
276
+ )
277
+ else
278
+ Authentication::None.new
279
+ end
280
+ end
281
+
282
+ # Build Composite authenticator (both API Key and OAuth2)
283
+ #
284
+ # Creates a composite that applies both authentication methods simultaneously.
285
+ # Only includes strategies that are actually configured.
286
+ #
287
+ # @return [Authentication::Composite]
288
+ #
289
+ def build_composite_authenticator
290
+ strategies = []
291
+
292
+ # Add API Key if configured
293
+ strategies << Authentication::ApiKey.new(api_key) unless api_key.nil? || api_key.to_s.strip.empty?
294
+
295
+ # Add OAuth2 if configured
296
+ if oauth2_configured?
297
+ strategies << Authentication::OAuth2.new(
298
+ client_id: @oauth2_client_id,
299
+ client_secret: @oauth2_client_secret,
300
+ token_url: @oauth2_token_url || Authentication::OAuth2::DEFAULT_TOKEN_URL,
301
+ scope: @oauth2_scope
302
+ )
303
+ end
304
+
305
+ Authentication::Composite.new(strategies)
306
+ end
307
+
308
+ # Check if OAuth2 credentials are configured
309
+ #
310
+ # @return [Boolean] true if client_id and client_secret are present
311
+ #
312
+ def oauth2_configured?
313
+ !@oauth2_client_id.nil? && !@oauth2_client_id.to_s.strip.empty? &&
314
+ !@oauth2_client_secret.nil? && !@oauth2_client_secret.to_s.strip.empty?
315
+ end
316
+ end
317
+ # rubocop:enable Metrics/ClassLength
318
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/retry"
5
+ require_relative "middleware/authentication"
6
+
7
+ module PetstoreApiClient
8
+ # Connection setup for HTTP requests
9
+ # Tried HTTParty first but Faraday's middleware is cleaner for this use case
10
+ module Connection
11
+ private
12
+
13
+ # Creates and memoizes a Faraday connection (reuses same connection for better performance)
14
+ def connection
15
+ @connection ||= Faraday.new(url: configuration.base_url) do |conn|
16
+ conn.request :json
17
+ conn.response :json, content_type: /\bjson$/
18
+
19
+ # Add authentication middleware (Strategy pattern + Interceptor pattern)
20
+ # This uses the authentication strategy from configuration
21
+ conn.use Middleware::Authentication, authenticator: configuration.authenticator
22
+
23
+ # Add retry middleware for transient failures (finally got around to implementing this!)
24
+ setup_retry_middleware(conn) if configuration.retry_enabled
25
+
26
+ # Standard headers for JSON API
27
+ conn.headers["Content-Type"] = "application/json"
28
+ conn.headers["Accept"] = "application/json"
29
+
30
+ # Timeouts to prevent hanging requests
31
+ conn.options.timeout = configuration.timeout
32
+ conn.options.open_timeout = configuration.open_timeout
33
+
34
+ conn.adapter Faraday.default_adapter
35
+ end
36
+ end
37
+
38
+ # Configure retry middleware with sensible defaults
39
+ def setup_retry_middleware(conn)
40
+ conn.request :retry,
41
+ max: configuration.max_retries,
42
+ interval: 0.5,
43
+ interval_randomness: 0.5,
44
+ backoff_factor: 2,
45
+ retry_statuses: [429, 500, 502, 503, 504], # Retry on these HTTP codes
46
+ methods: %i[get post put delete], # Retry all methods
47
+ exceptions: [Faraday::ConnectionFailed, Faraday::TimeoutError]
48
+ end
49
+
50
+ # Reset connection when config changes
51
+ def reset_connection!
52
+ @connection = nil
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ # Base error class - all our exceptions inherit from this
5
+ class Error < StandardError
6
+ attr_reader :status_code, :error_type
7
+
8
+ def initialize(message = nil, status_code: nil, error_type: nil)
9
+ @status_code = status_code
10
+ @error_type = error_type
11
+ super(message)
12
+ end
13
+ end
14
+
15
+ # Validation errors - thrown before we even hit the API
16
+ class ValidationError < Error; end
17
+
18
+ # Configuration errors - thrown when configuration is invalid
19
+ # @since 0.2.0
20
+ class ConfigurationError < Error; end
21
+
22
+ # Authentication errors - thrown when authentication fails
23
+ # @since 0.2.0
24
+ class AuthenticationError < Error
25
+ def initialize(message = "Authentication failed")
26
+ super(message, status_code: 401, error_type: "Authentication")
27
+ end
28
+ end
29
+
30
+ # 404 errors
31
+ class NotFoundError < Error
32
+ def initialize(message = "Resource not found")
33
+ super(message, status_code: 404, error_type: "NotFound")
34
+ end
35
+ end
36
+
37
+ # 405 or 400 errors for bad input
38
+ class InvalidInputError < Error
39
+ def initialize(message = "Invalid input provided")
40
+ super(message, status_code: 405, error_type: "InvalidInput")
41
+ end
42
+ end
43
+
44
+ # 400 errors specific to orders
45
+ class InvalidOrderError < Error
46
+ def initialize(message = "Invalid order supplied")
47
+ super(message, status_code: 400, error_type: "InvalidOrder")
48
+ end
49
+ end
50
+
51
+ # Network/connection issues
52
+ class ConnectionError < Error
53
+ def initialize(message = "Failed to connect to the API")
54
+ super(message, status_code: nil, error_type: "Connection")
55
+ end
56
+ end
57
+
58
+ # Rate limiting errors - too many requests
59
+ class RateLimitError < Error
60
+ attr_reader :retry_after
61
+
62
+ def initialize(message = "Rate limit exceeded", retry_after: nil)
63
+ @retry_after = retry_after
64
+ super(message, status_code: 429, error_type: "RateLimit")
65
+ end
66
+ end
67
+
68
+ # Catch-all for other API errors
69
+ class ApiError < Error; end
70
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ module Middleware
5
+ # Faraday middleware for authentication
6
+ # Applies authentication strategy to each request
7
+ #
8
+ # This middleware follows the Interceptor pattern - it intercepts
9
+ # outgoing requests and adds authentication headers before they're sent
10
+ #
11
+ # This is the same pattern used by industry-standard gems:
12
+ # - Octokit (GitHub API)
13
+ # - Slack-ruby-client
14
+ # - Stripe Ruby
15
+ #
16
+ # @example
17
+ # # In Faraday connection setup
18
+ # conn.use PetstoreApiClient::Middleware::Authentication,
19
+ # authenticator: ApiKey.new("special-key")
20
+ class Authentication < Faraday::Middleware
21
+ # Initialize middleware with authentication strategy
22
+ #
23
+ # @param app [#call] The next middleware in the stack
24
+ # @param options [Hash] Middleware options
25
+ # @option options [Authentication::Base] :authenticator The auth strategy
26
+ def initialize(app, options = {})
27
+ super(app)
28
+ @authenticator = options[:authenticator]
29
+ end
30
+
31
+ # Process request - apply authentication before sending
32
+ #
33
+ # @param env [Faraday::Env] The request environment
34
+ # @return [Faraday::Response] The response from the next middleware
35
+ def call(env)
36
+ # Apply authentication if configured
37
+ @authenticator&.apply(env) if @authenticator&.configured?
38
+
39
+ # Continue to next middleware
40
+ @app.call(env)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ module Models
5
+ # ApiResponse model for structured error responses
6
+ # Not used much since we raise exceptions instead of returning error objects
7
+ class ApiResponse < Base
8
+ attribute :code, :integer
9
+ attribute :type, :string
10
+ attribute :message, :string
11
+
12
+ def to_h
13
+ {
14
+ code: code,
15
+ type: type,
16
+ message: message
17
+ }.compact
18
+ end
19
+
20
+ def self.from_response(data)
21
+ return nil if data.nil?
22
+
23
+ new(
24
+ code: data["code"] || data[:code],
25
+ type: data["type"] || data[:type],
26
+ message: data["message"] || data[:message]
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end