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,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ # HTTP request methods for the Petstore API client
5
+ #
6
+ # Provides high-level abstraction over HTTP operations using Faraday.
7
+ # This module implements the Dependency Inversion Principle by abstracting
8
+ # HTTP operations behind a clean interface.
9
+ #
10
+ # All HTTP methods return a Response object that wraps the Faraday response.
11
+ # Errors are automatically detected and raised as appropriate exception types.
12
+ #
13
+ # This module is included by Client and provides:
14
+ # - GET requests for retrieving resources
15
+ # - POST requests for creating resources
16
+ # - PUT requests for updating resources
17
+ # - DELETE requests for removing resources
18
+ #
19
+ # @example Making a GET request
20
+ # client = Client.new
21
+ # response = client.get("/pet/123")
22
+ # pet = response.body
23
+ #
24
+ # @example Making a POST request with body
25
+ # response = client.post("/pet", body: { name: "Fluffy", status: "available" })
26
+ #
27
+ # @see Connection
28
+ # @see Response
29
+ # @since 0.1.0
30
+ module Request
31
+ # Perform HTTP GET request
32
+ #
33
+ # Retrieves a resource from the API. Query parameters can be provided
34
+ # via the params hash.
35
+ #
36
+ # @param path [String] The API endpoint path (e.g., "/pet/123")
37
+ # @param params [Hash] Optional query parameters
38
+ #
39
+ # @return [Response] Wrapped HTTP response
40
+ #
41
+ # @raise [NotFoundError] if resource not found (404)
42
+ # @raise [InvalidInputError] if request is invalid (400, 405)
43
+ # @raise [RateLimitError] if rate limit exceeded (429)
44
+ # @raise [ConnectionError] if connection fails or times out
45
+ # @raise [ApiError] for other API errors
46
+ #
47
+ # @example Get a pet by ID
48
+ # response = client.get("/pet/123")
49
+ # pet = response.body
50
+ #
51
+ # @example Get pets with query parameters
52
+ # response = client.get("/pet/findByStatus", params: { status: "available" })
53
+ # pets = response.body
54
+ #
55
+ def get(path, params: {})
56
+ request(:get, path, params: params)
57
+ end
58
+
59
+ # Perform HTTP POST request
60
+ #
61
+ # Creates a new resource on the API. The request body should contain
62
+ # the resource data as a hash, which will be automatically serialized to JSON.
63
+ #
64
+ # @param path [String] The API endpoint path
65
+ # @param body [Hash] Request body data
66
+ #
67
+ # @return [Response] Wrapped HTTP response
68
+ #
69
+ # @raise [InvalidInputError] if request body is invalid
70
+ # @raise [ValidationError] if data fails validation
71
+ # @raise [RateLimitError] if rate limit exceeded
72
+ # @raise [ConnectionError] if connection fails or times out
73
+ # @raise [ApiError] for other API errors
74
+ #
75
+ # @example Create a new pet
76
+ # response = client.post("/pet", body: {
77
+ # name: "Fluffy",
78
+ # status: "available",
79
+ # category: { id: 1, name: "Dogs" }
80
+ # })
81
+ #
82
+ def post(path, body: {})
83
+ request(:post, path, body: body)
84
+ end
85
+
86
+ # Perform HTTP PUT request
87
+ #
88
+ # Updates an existing resource on the API. The request body should contain
89
+ # the updated resource data as a hash.
90
+ #
91
+ # @param path [String] The API endpoint path
92
+ # @param body [Hash] Request body data with updates
93
+ #
94
+ # @return [Response] Wrapped HTTP response
95
+ #
96
+ # @raise [NotFoundError] if resource not found (404)
97
+ # @raise [InvalidInputError] if request body is invalid
98
+ # @raise [ValidationError] if data fails validation
99
+ # @raise [ConnectionError] if connection fails or times out
100
+ # @raise [ApiError] for other API errors
101
+ #
102
+ # @example Update an existing pet
103
+ # response = client.put("/pet", body: {
104
+ # id: 123,
105
+ # name: "Fluffy Updated",
106
+ # status: "sold"
107
+ # })
108
+ #
109
+ def put(path, body: {})
110
+ request(:put, path, body: body)
111
+ end
112
+
113
+ # Perform HTTP DELETE request
114
+ #
115
+ # Deletes a resource from the API. Query parameters can be provided
116
+ # via the params hash if needed.
117
+ #
118
+ # @param path [String] The API endpoint path
119
+ # @param params [Hash] Optional query parameters
120
+ #
121
+ # @return [Response] Wrapped HTTP response
122
+ #
123
+ # @raise [NotFoundError] if resource not found (404)
124
+ # @raise [InvalidInputError] if request is invalid
125
+ # @raise [ConnectionError] if connection fails or times out
126
+ # @raise [ApiError] for other API errors
127
+ #
128
+ # @example Delete a pet
129
+ # response = client.delete("/pet/123")
130
+ #
131
+ def delete(path, params: {})
132
+ request(:delete, path, params: params)
133
+ end
134
+
135
+ private
136
+
137
+ # Core request method that handles all HTTP operations
138
+ #
139
+ # This is the central method that all public HTTP methods (get, post, put, delete)
140
+ # delegate to. It handles:
141
+ # - Executing the HTTP request via Faraday
142
+ # - Wrapping the response in a Response object
143
+ # - Error detection and exception raising
144
+ # - Connection error handling
145
+ #
146
+ # @param method [Symbol] HTTP method (:get, :post, :put, :delete)
147
+ # @param path [String] API endpoint path
148
+ # @param params [Hash] Query parameters for GET/DELETE requests
149
+ # @param body [Hash] Request body for POST/PUT requests
150
+ #
151
+ # @return [Response] Wrapped HTTP response
152
+ #
153
+ # @raise [ConnectionError] if connection fails or times out
154
+ # @raise [ApiError] for unexpected errors
155
+ # @raise [NotFoundError, InvalidInputError, RateLimitError, etc.] for API errors
156
+ #
157
+ # @api private
158
+ def request(method, path, params: {}, body: {})
159
+ # puts "DEBUG: #{method.upcase} #{path}" if ENV['DEBUG']
160
+ resp = connection.public_send(method) do |req|
161
+ req.url path
162
+ req.params = params if params.any?
163
+ req.body = body if body.any?
164
+ end
165
+
166
+ wrapped_resp = Response.new(resp)
167
+ handle_error_response(wrapped_resp) if wrapped_resp.error?
168
+
169
+ wrapped_resp
170
+ rescue Faraday::ConnectionFailed => e
171
+ raise ConnectionError, "Connection failed: #{e.message}"
172
+ rescue Faraday::TimeoutError => e
173
+ raise ConnectionError, "Request timeout: #{e.message}"
174
+ rescue Error
175
+ # Don't double-wrap our own errors
176
+ raise
177
+ rescue StandardError => e
178
+ # Catch-all for unexpected errors
179
+ raise ApiError, "Request failed: #{e.message}"
180
+ end
181
+
182
+ # Handle error responses and raise appropriate exceptions
183
+ #
184
+ # Examines the HTTP status code and error message from the API response
185
+ # and raises the appropriate exception type. This provides a clean
186
+ # abstraction where callers can rescue specific error types.
187
+ #
188
+ # Status code mapping:
189
+ # - 404: NotFoundError
190
+ # - 400: InvalidOrderError (if error_type is "InvalidOrder") or InvalidInputError
191
+ # - 405: InvalidInputError
192
+ # - 429: RateLimitError (includes retry-after header)
193
+ # - Other: ApiError
194
+ #
195
+ # @param response [Response] The wrapped HTTP response
196
+ # @return [void]
197
+ #
198
+ # @raise [NotFoundError] for 404 responses
199
+ # @raise [InvalidInputError] for 400/405 responses
200
+ # @raise [InvalidOrderError] for 400 responses with InvalidOrder error type
201
+ # @raise [RateLimitError] for 429 responses
202
+ # @raise [ApiError] for other error responses
203
+ #
204
+ # @api private
205
+ def handle_error_response(response)
206
+ case response.status
207
+ when 404
208
+ raise NotFoundError, response.error_message
209
+ when 400
210
+ # Check if it's an order-related error
211
+ raise InvalidOrderError, response.error_message if response.error_type == "InvalidOrder"
212
+
213
+ raise InvalidInputError, response.error_message
214
+ when 405
215
+ raise InvalidInputError, response.error_message
216
+ when 429
217
+ # Rate limiting - extract retry-after header if present
218
+ retry_after = response.headers["retry-after"] || response.headers["Retry-After"]
219
+ raise RateLimitError.new(response.error_message, retry_after: retry_after)
220
+ else
221
+ raise ApiError, response.error_message
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ # HTTP response wrapper for Petstore API responses
5
+ #
6
+ # Wraps Faraday HTTP responses and provides a clean, consistent interface
7
+ # for accessing response data. This class implements the Single Responsibility
8
+ # Principle by encapsulating all response handling logic in one place.
9
+ #
10
+ # The Response object provides:
11
+ # - Parsed JSON body (automatically converted from JSON)
12
+ # - HTTP status code
13
+ # - Response headers
14
+ # - Success/error detection
15
+ # - Error message extraction
16
+ # - Access to raw Faraday response
17
+ #
18
+ # Response bodies are automatically parsed from JSON. If the API returns
19
+ # non-JSON content (like HTML error pages), the body is returned as a string.
20
+ #
21
+ # @example Accessing a successful response
22
+ # response = client.get("/pet/123")
23
+ # if response.success?
24
+ # pet = response.body
25
+ # puts pet["name"]
26
+ # end
27
+ #
28
+ # @example Handling an error response
29
+ # response = client.get("/pet/invalid")
30
+ # if response.error?
31
+ # puts response.error_message
32
+ # puts response.error_code
33
+ # end
34
+ #
35
+ # @see Request
36
+ # @since 0.1.0
37
+ class Response
38
+ # @!attribute [r] status
39
+ # @return [Integer] HTTP status code (e.g., 200, 404, 500)
40
+ # @!attribute [r] body
41
+ # @return [Hash, Array, String] Parsed response body
42
+ # @!attribute [r] headers
43
+ # @return [Hash] HTTP response headers
44
+ # @!attribute [r] raw_response
45
+ # @return [Faraday::Response] Original Faraday response object
46
+ attr_reader :status, :body, :headers, :raw_response
47
+
48
+ # Initialize a new Response wrapper
49
+ #
50
+ # Wraps a Faraday response object and extracts relevant data.
51
+ # The response body is automatically parsed from JSON to Ruby objects.
52
+ #
53
+ # @param faraday_response [Faraday::Response] The Faraday response object
54
+ #
55
+ # @example
56
+ # faraday_response = connection.get("/pet/123")
57
+ # response = Response.new(faraday_response)
58
+ #
59
+ def initialize(faraday_response)
60
+ @raw_response = faraday_response
61
+ @status = faraday_response.status
62
+ @body = parse_body(faraday_response.body)
63
+ @headers = faraday_response.headers
64
+ end
65
+
66
+ # Check if the response was successful
67
+ #
68
+ # A response is considered successful if the HTTP status code is
69
+ # in the 2xx range (200-299).
70
+ #
71
+ # @return [Boolean] true if status is 200-299, false otherwise
72
+ #
73
+ # @example
74
+ # response = client.get("/pet/123")
75
+ # if response.success?
76
+ # # Handle successful response
77
+ # end
78
+ #
79
+ def success?
80
+ (200..299).cover?(status)
81
+ end
82
+
83
+ # Check if response indicates an error
84
+ #
85
+ # A response is considered an error if the HTTP status code is
86
+ # outside the 2xx range.
87
+ #
88
+ # @return [Boolean] true if status is not 200-299, false otherwise
89
+ #
90
+ # @example
91
+ # response = client.get("/pet/invalid")
92
+ # if response.error?
93
+ # puts response.error_message
94
+ # end
95
+ #
96
+ def error?
97
+ !success?
98
+ end
99
+
100
+ # Extract error message from response body
101
+ #
102
+ # Attempts to extract a human-readable error message from the response.
103
+ # Handles various response formats:
104
+ # - JSON with "message" key
105
+ # - Plain text error messages
106
+ # - HTML error pages
107
+ # - Empty/nil responses
108
+ #
109
+ # @return [String, nil] Error message if response is an error, nil otherwise
110
+ #
111
+ # @example JSON error response
112
+ # # API returns: { "code": 404, "type": "NotFound", "message": "Pet not found" }
113
+ # response.error_message # => "Pet not found"
114
+ #
115
+ # @example HTML error response
116
+ # # API returns HTML error page
117
+ # response.error_message # => "Request failed with status 500"
118
+ #
119
+ def error_message
120
+ return nil unless error?
121
+
122
+ # Extract message from different response formats
123
+ case body
124
+ when Hash
125
+ body["message"] || body[:message] || "Unknown error"
126
+ when String
127
+ # Sometimes the API returns HTML instead of JSON (sigh...)
128
+ body.include?("<html>") ? "Request failed with status #{status}" : body
129
+ else
130
+ "Request failed with status #{status}"
131
+ end
132
+ end
133
+
134
+ # Extract error code from response body
135
+ #
136
+ # Returns the error code from the response if available.
137
+ # Falls back to HTTP status code if no error code is present in body.
138
+ #
139
+ # @return [Integer, nil] Error code if response is an error, nil otherwise
140
+ #
141
+ # @example
142
+ # # API returns: { "code": 1, "type": "NotFound", "message": "Pet not found" }
143
+ # response.error_code # => 1
144
+ #
145
+ def error_code
146
+ return nil unless error?
147
+
148
+ body.is_a?(Hash) ? (body["code"] || body[:code]) : status
149
+ end
150
+
151
+ # Extract error type from response body
152
+ #
153
+ # Returns the error type/category from the response if available.
154
+ # This is useful for programmatic error handling.
155
+ #
156
+ # @return [String, nil] Error type if available, nil otherwise
157
+ #
158
+ # @example
159
+ # # API returns: { "code": 1, "type": "NotFound", "message": "Pet not found" }
160
+ # response.error_type # => "NotFound"
161
+ #
162
+ def error_type
163
+ return nil unless error?
164
+
165
+ body.is_a?(Hash) ? (body["type"] || body[:type]) : nil
166
+ end
167
+
168
+ private
169
+
170
+ # Parse response body from JSON
171
+ #
172
+ # Handles JSON parsing and various edge cases:
173
+ # - Already-parsed JSON (Hash/Array)
174
+ # - Empty/nil responses
175
+ # - Non-JSON responses (returns as-is)
176
+ #
177
+ # Faraday's JSON middleware typically handles parsing, but this method
178
+ # provides additional safety for edge cases.
179
+ #
180
+ # @param body [Object] Raw response body
181
+ # @return [Hash, Array, String, Object] Parsed body
182
+ #
183
+ # @api private
184
+ def parse_body(body)
185
+ # Faraday's JSON middleware already parses the response,
186
+ # but we handle edge cases here
187
+ return body if body.is_a?(Hash) || body.is_a?(Array)
188
+ return {} if body.nil? || body.empty?
189
+
190
+ body
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Custom validator for required array fields
4
+ # ActiveModel's presence validator doesn't handle empty arrays correctly
5
+ class ArrayPresenceValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ if value.nil?
8
+ record.errors.add(attribute, "must be present")
9
+ elsif !value.is_a?(Array)
10
+ record.errors.add(attribute, "must be an array")
11
+ elsif value.empty?
12
+ record.errors.add(attribute, "cannot be empty")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Custom enum validator - ActiveModel's inclusion validator doesn't quite work the way we want
4
+ # for status enums, so rolling our own
5
+ class EnumValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ return if value.nil? && options[:allow_nil]
8
+
9
+ allowed_values = options[:in] || options[:within]
10
+ return if allowed_values.include?(value)
11
+
12
+ record.errors.add(
13
+ attribute,
14
+ "must be one of: #{allowed_values.join(", ")}, but got '#{value}'"
15
+ )
16
+ end
17
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "active_support/all"
5
+
6
+ require_relative "petstore_api_client/version"
7
+ require_relative "petstore_api_client/errors"
8
+ require_relative "petstore_api_client/configuration"
9
+ require_relative "petstore_api_client/response"
10
+ require_relative "petstore_api_client/paginated_collection"
11
+ require_relative "petstore_api_client/connection"
12
+ require_relative "petstore_api_client/request"
13
+ require_relative "petstore_api_client/client"
14
+
15
+ # Load validators
16
+ require_relative "petstore_api_client/validators/array_presence_validator"
17
+ require_relative "petstore_api_client/validators/enum_validator"
18
+
19
+ # Load models
20
+ require_relative "petstore_api_client/models/category"
21
+ require_relative "petstore_api_client/models/tag"
22
+ require_relative "petstore_api_client/models/api_response"
23
+ require_relative "petstore_api_client/models/pet"
24
+ require_relative "petstore_api_client/models/order"
25
+
26
+ # Load clients
27
+ require_relative "petstore_api_client/clients/pet_client"
28
+ require_relative "petstore_api_client/clients/store_client"
29
+ require_relative "petstore_api_client/api_client"
30
+
31
+ # Module for the Petstore API Client library
32
+ module PetstoreApiClient
33
+ class << self
34
+ attr_writer :configuration
35
+
36
+ # Global configuration accessor
37
+ def configuration
38
+ @configuration ||= Configuration.new
39
+ end
40
+
41
+ # Configure the library globally
42
+ # Example:
43
+ # PetstoreApiClient.configure do |config|
44
+ # config.api_key = "special-key"
45
+ # end
46
+ def configure
47
+ yield(configuration) if block_given?
48
+ end
49
+
50
+ # Reset the global configuration
51
+ def reset_configuration!
52
+ @configuration = Configuration.new
53
+ end
54
+ end
55
+ end