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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ # Main API client - this is what users interact with
5
+ class ApiClient
6
+ attr_reader :configuration
7
+
8
+ def initialize(config = nil)
9
+ @configuration = config || Configuration.new
10
+ @configuration.validate!
11
+ end
12
+
13
+ def configure
14
+ yield(configuration) if block_given?
15
+ # Reset clients when configuration changes
16
+ @pet_client = nil
17
+ @store_client = nil
18
+ self
19
+ end
20
+
21
+ # Access to Pet endpoints
22
+ def pets
23
+ @pets ||= Clients::PetClient.new(configuration)
24
+ end
25
+
26
+ # Access to Store endpoints
27
+ def store
28
+ @store ||= Clients::StoreClient.new(configuration)
29
+ end
30
+
31
+ # Convenience methods so you can do client.create_pet instead of client.pets.create_pet
32
+ def create_pet(pet_data)
33
+ pets.create_pet(pet_data)
34
+ end
35
+
36
+ def get_pet(pet_id)
37
+ pets.get_pet(pet_id)
38
+ end
39
+
40
+ def update_pet(pet_data)
41
+ pets.update_pet(pet_data)
42
+ end
43
+
44
+ def delete_pet(pet_id)
45
+ pets.delete_pet(pet_id)
46
+ end
47
+
48
+ def create_order(order_data)
49
+ store.create_order(order_data)
50
+ end
51
+
52
+ def get_order(order_id)
53
+ store.get_order(order_id)
54
+ end
55
+
56
+ def delete_order(order_id)
57
+ store.delete_order(order_id)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module PetstoreApiClient
6
+ module Authentication
7
+ # API Key authentication strategy
8
+ # Adds api_key header to requests
9
+ #
10
+ # The Swagger Petstore API accepts api_key in the request header.
11
+ # According to official docs, the special key is "special-key"
12
+ #
13
+ # Security best practices:
14
+ # - API keys are validated before use
15
+ # - Warnings issued for insecure (HTTP) connections
16
+ # - Supports loading from environment variables
17
+ #
18
+ # @example Direct instantiation
19
+ # auth = ApiKey.new("special-key")
20
+ #
21
+ # @example From environment variable
22
+ # ENV['PETSTORE_API_KEY'] = "special-key"
23
+ # auth = ApiKey.from_env
24
+ class ApiKey < Base
25
+ # Header name for API key
26
+ # According to Swagger Petstore spec: https://petstore.swagger.io
27
+ HEADER_NAME = "api_key"
28
+
29
+ # Environment variable name for API key
30
+ ENV_VAR_NAME = "PETSTORE_API_KEY"
31
+
32
+ # Minimum length for API key (security validation)
33
+ MIN_KEY_LENGTH = 3
34
+
35
+ attr_reader :api_key
36
+
37
+ # Initialize API key authenticator
38
+ #
39
+ # @param api_key [String, nil] The API key to use
40
+ # @raise [ValidationError] if API key is invalid
41
+ # rubocop:disable Lint/MissingSuper
42
+ def initialize(api_key = nil)
43
+ @api_key = api_key&.to_s&.strip
44
+ validate! if configured?
45
+ end
46
+ # rubocop:enable Lint/MissingSuper
47
+
48
+ # Create authenticator from environment variable
49
+ #
50
+ # @return [ApiKey] New authenticator instance
51
+ def self.from_env
52
+ new(ENV.fetch(ENV_VAR_NAME, nil))
53
+ end
54
+
55
+ # Apply API key authentication to request
56
+ # Adds api_key header to the request
57
+ #
58
+ # @param env [Faraday::Env] The request environment
59
+ # @return [void]
60
+ def apply(env)
61
+ return unless configured?
62
+
63
+ # Warn if sending API key over insecure connection (HTTP)
64
+ warn_if_insecure!(env)
65
+
66
+ # Add API key header
67
+ env.request_headers[HEADER_NAME] = @api_key
68
+ end
69
+
70
+ # Check if API key is configured
71
+ #
72
+ # @return [Boolean]
73
+ def configured?
74
+ !@api_key.nil? && !@api_key.empty?
75
+ end
76
+
77
+ # String representation (masks API key for security)
78
+ #
79
+ # @return [String]
80
+ def inspect
81
+ return unconfigured_inspect unless configured?
82
+
83
+ # Use base class method to mask credential (show first 4 chars)
84
+ masked = mask_credential(@api_key, 4)
85
+
86
+ "#<#{self.class.name} api_key=#{masked}>"
87
+ end
88
+ alias to_s inspect
89
+
90
+ private
91
+
92
+ # Validate API key format and length
93
+ #
94
+ # @raise [ValidationError] if API key is invalid
95
+ def validate!
96
+ return unless configured?
97
+
98
+ # Use base class validation methods for DRY principle
99
+ validate_credential_length(@api_key, "API key", MIN_KEY_LENGTH)
100
+ validate_no_newlines(@api_key, "API key")
101
+ validate_no_whitespace(@api_key, "API key")
102
+ end
103
+
104
+ # Note: warn_if_insecure! method inherited from Base class
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetstoreApiClient
4
+ module Authentication
5
+ # Base class for authentication strategies
6
+ # Implements Strategy Pattern - allows different authentication methods
7
+ # to be swapped without changing client code
8
+ #
9
+ # This follows the same pattern as battle-tested gems like:
10
+ # - Octokit (GitHub API client)
11
+ # - Slack-ruby-client
12
+ # - Stripe Ruby library
13
+ class Base
14
+ # Apply authentication to a Faraday request environment
15
+ #
16
+ # @param env [Faraday::Env] The request environment
17
+ # @return [void]
18
+ def apply(env)
19
+ raise NotImplementedError, "#{self.class.name} must implement #apply"
20
+ end
21
+
22
+ # Check if this authentication strategy is configured
23
+ # Used to determine if auth should be applied
24
+ #
25
+ # @return [Boolean]
26
+ def configured?
27
+ raise NotImplementedError, "#{self.class.name} must implement #configured?"
28
+ end
29
+
30
+ # Human-readable description of authentication type
31
+ # Useful for logging and debugging
32
+ #
33
+ # @return [String]
34
+ def type
35
+ self.class.name.split("::").last
36
+ end
37
+
38
+ protected
39
+
40
+ # Validates that a credential meets minimum length requirements
41
+ #
42
+ # @param value [String] The credential value to validate
43
+ # @param field_name [String] Name of the field for error messages
44
+ # @param min_length [Integer] Minimum required length
45
+ # @raise [ValidationError] if credential is too short
46
+ # @return [void]
47
+ def validate_credential_length(value, field_name, min_length)
48
+ return if value.length >= min_length
49
+
50
+ raise ValidationError,
51
+ "#{field_name} must be at least #{min_length} characters (got #{value.length})"
52
+ end
53
+
54
+ # Validates that a credential doesn't contain newline characters
55
+ # Newlines can cause security issues and are never valid in credentials
56
+ #
57
+ # @param value [String] The credential value to validate
58
+ # @param field_name [String] Name of the field for error messages
59
+ # @raise [ValidationError] if credential contains newlines
60
+ # @return [void]
61
+ def validate_no_newlines(value, field_name)
62
+ return unless value.include?("\n") || value.include?("\r")
63
+
64
+ raise ValidationError, "#{field_name} contains newline characters"
65
+ end
66
+
67
+ # Validates that a credential doesn't have leading/trailing whitespace
68
+ # Whitespace usually indicates copy-paste errors
69
+ #
70
+ # @param value [String] The credential value to validate
71
+ # @param field_name [String] Name of the field for error messages
72
+ # @raise [ValidationError] if credential has whitespace
73
+ # @return [void]
74
+ def validate_no_whitespace(value, field_name)
75
+ return if value == value.strip
76
+
77
+ raise ValidationError,
78
+ "#{field_name} has leading/trailing whitespace (did you copy-paste incorrectly?)"
79
+ end
80
+
81
+ # Warns if authentication is being sent over insecure HTTP connection
82
+ # This is a security risk as credentials can be intercepted
83
+ #
84
+ # @param env [Faraday::Env] The request environment
85
+ # @return [void]
86
+ def warn_if_insecure!(env)
87
+ return if env.url.scheme == "https"
88
+
89
+ warn "[PetstoreApiClient] WARNING: Sending credentials over insecure HTTP connection! " \
90
+ "Use HTTPS in production to protect credentials."
91
+ end
92
+
93
+ # Masks a credential for safe display in logs and inspect output
94
+ # Shows first few characters and masks the rest
95
+ #
96
+ # @param value [String] The credential to mask
97
+ # @param visible_chars [Integer] Number of characters to show (default: 3)
98
+ # @return [String] Masked credential string
99
+ def mask_credential(value, visible_chars = 3)
100
+ return "***" if value.length <= visible_chars
101
+
102
+ "#{value[0...visible_chars]}#{"*" * (value.length - visible_chars)}"
103
+ end
104
+
105
+ # Standard inspect message for unconfigured authenticators
106
+ #
107
+ # @return [String] Inspect message
108
+ def unconfigured_inspect
109
+ "#<#{self.class.name} (not configured)>"
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module PetstoreApiClient
6
+ module Authentication
7
+ # Composite authentication strategy for applying multiple auth methods simultaneously
8
+ #
9
+ # This authenticator implements the Composite pattern, allowing multiple authentication
10
+ # strategies to be applied to a single request. This is particularly useful during
11
+ # migration periods when transitioning from one auth method to another, or when an
12
+ # API accepts multiple authentication methods.
13
+ #
14
+ # The Petstore API accepts both API Key and OAuth2 authentication. Using this composite
15
+ # authenticator, you can send both headers simultaneously, allowing the server to
16
+ # accept either authentication method.
17
+ #
18
+ # All configured strategies are applied in the order they were added. Each strategy's
19
+ # apply() method is called, allowing it to add its own headers to the request.
20
+ #
21
+ # @example Using both API Key and OAuth2
22
+ # api_key_auth = ApiKey.new("special-key")
23
+ # oauth2_auth = OAuth2.new(client_id: "id", client_secret: "secret")
24
+ # composite = Composite.new([api_key_auth, oauth2_auth])
25
+ #
26
+ # composite.apply(env)
27
+ # # Request now has both:
28
+ # # - api_key: special-key
29
+ # # - Authorization: Bearer <token>
30
+ #
31
+ # @example Building from configuration
32
+ # strategies = []
33
+ # strategies << ApiKey.new(config.api_key) if config.api_key
34
+ # strategies << OAuth2.new(...) if config.oauth2_client_id
35
+ # composite = Composite.new(strategies)
36
+ #
37
+ # @see https://refactoring.guru/design-patterns/composite Composite Pattern
38
+ # @since 0.2.0
39
+ class Composite < Base
40
+ # @!attribute [r] strategies
41
+ # @return [Array<Base>] List of authentication strategies to apply
42
+ attr_reader :strategies
43
+
44
+ # Initialize composite authenticator with multiple strategies
45
+ #
46
+ # @param strategies [Array<Base>] List of authentication strategies
47
+ # Each strategy must respond to #apply(env) and #configured?
48
+ #
49
+ # @raise [ArgumentError] if strategies is not an array
50
+ # @raise [ArgumentError] if any strategy doesn't inherit from Base
51
+ #
52
+ # @example
53
+ # api_key = ApiKey.new("my-key")
54
+ # oauth2 = OAuth2.new(client_id: "id", client_secret: "secret")
55
+ # composite = Composite.new([api_key, oauth2])
56
+ #
57
+ # rubocop:disable Lint/MissingSuper
58
+ def initialize(strategies = [])
59
+ raise ArgumentError, "strategies must be an Array (got #{strategies.class})" unless strategies.is_a?(Array)
60
+
61
+ validate_strategies!(strategies)
62
+ @strategies = strategies
63
+ # Performance optimization: cache configured strategies to avoid repeated checks
64
+ @configured_strategies = @strategies.select(&:configured?)
65
+ end
66
+ # rubocop:enable Lint/MissingSuper
67
+
68
+ # Apply all configured authentication strategies to the request
69
+ #
70
+ # Iterates through all strategies and calls apply() on each one if it's configured.
71
+ # This allows multiple authentication headers to be added to the same request.
72
+ #
73
+ # @param env [Faraday::Env] The Faraday request environment
74
+ # @return [void]
75
+ #
76
+ # @example
77
+ # composite = Composite.new([api_key_auth, oauth2_auth])
78
+ # composite.apply(env)
79
+ # # Both authentication methods are now applied
80
+ #
81
+ def apply(env)
82
+ # Performance optimization: use cached configured strategies
83
+ # Avoids checking configured? on every request
84
+ @configured_strategies.each { |strategy| strategy.apply(env) }
85
+ end
86
+
87
+ # Check if any authentication strategy is configured
88
+ #
89
+ # Returns true if at least one strategy in the composite is configured.
90
+ # Returns false if no strategies are configured or if strategies array is empty.
91
+ #
92
+ # @return [Boolean] true if at least one strategy is configured
93
+ #
94
+ # @example
95
+ # # No strategies configured
96
+ # composite = Composite.new([])
97
+ # composite.configured? # => false
98
+ #
99
+ # # One strategy configured
100
+ # api_key = ApiKey.new("my-key")
101
+ # composite = Composite.new([api_key])
102
+ # composite.configured? # => true
103
+ #
104
+ # # Mixed (one configured, one not)
105
+ # oauth2 = OAuth2.new # Not configured (no credentials)
106
+ # composite = Composite.new([api_key, oauth2])
107
+ # composite.configured? # => true (at least one is configured)
108
+ #
109
+ def configured?
110
+ # Performance optimization: use cached configured strategies
111
+ !@configured_strategies.empty?
112
+ end
113
+
114
+ # Get list of configured strategy types
115
+ #
116
+ # Returns array of type names for strategies that are actually configured.
117
+ # Useful for debugging and logging.
118
+ #
119
+ # @return [Array<String>] List of configured strategy type names
120
+ #
121
+ # @example
122
+ # api_key = ApiKey.new("key")
123
+ # oauth2 = OAuth2.new # Not configured
124
+ # composite = Composite.new([api_key, oauth2])
125
+ # composite.configured_types # => ["ApiKey"]
126
+ #
127
+ def configured_types
128
+ @strategies.select(&:configured?).map(&:type)
129
+ end
130
+
131
+ # String representation showing all configured strategies
132
+ #
133
+ # @return [String] Human-readable representation
134
+ #
135
+ # @example
136
+ # composite = Composite.new([api_key, oauth2])
137
+ # composite.inspect
138
+ # # => "#<Composite strategies=[ApiKey, OAuth2] configured=[ApiKey, OAuth2]>"
139
+ #
140
+ def inspect
141
+ all_types = @strategies.map(&:type).join(", ")
142
+ configured = configured_types.join(", ")
143
+ configured = "none" if configured.empty?
144
+
145
+ "#<#{self.class.name} strategies=[#{all_types}] configured=[#{configured}]>"
146
+ end
147
+ alias to_s inspect
148
+
149
+ private
150
+
151
+ # Validate that all strategies are valid authentication strategy objects
152
+ #
153
+ # @param strategies [Array] List of strategies to validate
154
+ # @return [void]
155
+ # @raise [ArgumentError] if any strategy is invalid
156
+ #
157
+ def validate_strategies!(strategies)
158
+ strategies.each_with_index do |strategy, index|
159
+ unless strategy.is_a?(Base)
160
+ raise ArgumentError,
161
+ "Strategy at index #{index} must inherit from Authentication::Base " \
162
+ "(got #{strategy.class})"
163
+ end
164
+
165
+ unless strategy.respond_to?(:apply)
166
+ raise ArgumentError,
167
+ "Strategy at index #{index} (#{strategy.class}) must respond to #apply"
168
+ end
169
+
170
+ unless strategy.respond_to?(:configured?)
171
+ raise ArgumentError,
172
+ "Strategy at index #{index} (#{strategy.class}) must respond to #configured?"
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module PetstoreApiClient
6
+ module Authentication
7
+ # No authentication strategy (Null Object pattern)
8
+ # Used when no authentication is configured
9
+ #
10
+ # This allows the authentication system to always have an authenticator
11
+ # without needing nil checks everywhere
12
+ #
13
+ # @example
14
+ # auth = None.new
15
+ # auth.configured? # => false
16
+ # auth.apply(env) # Does nothing
17
+ class None < Base
18
+ # Apply no authentication (does nothing)
19
+ #
20
+ # @param _env [Faraday::Env] The request environment (unused)
21
+ # @return [void]
22
+ def apply(_env)
23
+ # Intentionally empty - no authentication to apply
24
+ end
25
+
26
+ # Check if authentication is configured
27
+ #
28
+ # @return [Boolean] Always false
29
+ def configured?
30
+ false
31
+ end
32
+
33
+ # String representation
34
+ #
35
+ # @return [String]
36
+ def inspect
37
+ "#<#{self.class.name} (no authentication)>"
38
+ end
39
+ alias to_s inspect
40
+ end
41
+ end
42
+ end