booqable 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.
@@ -0,0 +1,114 @@
1
+ module Booqable
2
+ # Authentication methods for {Booqable::Client}
3
+ #
4
+ # Provides authentication support for multiple methods including OAuth2,
5
+ # API keys, and single-use tokens. The module automatically detects which
6
+ # authentication method to use based on the configured credentials.
7
+ #
8
+ # @example OAuth authentication
9
+ # client = Booqable::Client.new(
10
+ # client_id: "your_client_id",
11
+ # client_secret: "your_client_secret"
12
+ # )
13
+ # client.authenticate_with_code("auth_code_from_callback")
14
+ #
15
+ # @example API key authentication
16
+ # client = Booqable::Client.new(api_key: "your_api_key")
17
+ # # Authentication is automatic with API requests
18
+ #
19
+ # @example Single-use token authentication
20
+ # client = Booqable::Client.new(
21
+ # single_use_token: "your_token",
22
+ # single_use_token_algorithm: "HS256",
23
+ # single_use_token_secret: "your_secret"
24
+ # )
25
+ module Auth
26
+ # Complete OAuth authentication flow with authorization code
27
+ #
28
+ # Exchanges an authorization code for an access token and stores it
29
+ # using the configured write_token proc. This method should be called
30
+ # after the user has been redirected back from the OAuth provider.
31
+ #
32
+ # @param code [String] Authorization code from OAuth callback
33
+ # @return [void]
34
+ # @raise [OAuth2::Error] If the authorization code is invalid or expired
35
+ #
36
+ # @example
37
+ # # In your OAuth callback handler
38
+ # client.authenticate_with_code(params[:code])
39
+ def authenticate_with_code(code)
40
+ token = oauth_client.get_token_from_code(code)
41
+ @write_token.call(token.to_hash)
42
+ end
43
+
44
+ # Inject appropriate authentication middleware into the request stack
45
+ #
46
+ # Automatically detects which authentication method is configured and
47
+ # injects the corresponding middleware. Multiple authentication methods
48
+ # can be configured, with OAuth taking precedence over API keys.
49
+ #
50
+ # @param builder [Faraday::Builder] The middleware builder to configure
51
+ # @return [void]
52
+ # @api private
53
+ def inject_auth_middleware(builder)
54
+ builder.use Booqable::Middleware::Auth::OAuth, {
55
+ client_id: client_id,
56
+ client_secret: client_secret,
57
+ api_endpoint: api_endpoint,
58
+ redirect_uri: redirect_uri,
59
+ read_token: read_token,
60
+ write_token: write_token
61
+ } if oauth_authenticated?
62
+
63
+ builder.use Booqable::Middleware::Auth::ApiKey, {
64
+ api_key: api_key
65
+ } if api_key_authenticated?
66
+
67
+ builder.use Booqable::Middleware::Auth::SingleUse, {
68
+ single_use_token: single_use_token,
69
+ single_use_token_algorithm: single_use_token_algorithm,
70
+ single_use_token_private_key: single_use_token_private_key || single_use_token_secret,
71
+ single_use_token_expiration_period: single_use_token_expiration_period,
72
+ single_use_token_company_id: single_use_token_company_id,
73
+ single_use_token_user_id: single_use_token_user_id,
74
+ api_endpoint: api_endpoint
75
+ } if single_use_token_authenticated?
76
+ end
77
+
78
+ # Get or create the OAuth client
79
+ #
80
+ # Returns a memoized OAuth client instance configured with the current
81
+ # client credentials. Returns nil if OAuth is not configured.
82
+ #
83
+ # @return [OAuthClient, nil] OAuth client instance or nil if not configured
84
+ def oauth_client
85
+ @oauth_client ||= OAuthClient.new(
86
+ api_endpoint: api_endpoint,
87
+ client_id: @client_id,
88
+ client_secret: @client_secret,
89
+ redirect_uri: @redirect_uri
90
+ ) if oauth_authenticated?
91
+ end
92
+
93
+ # Check if OAuth authentication is configured
94
+ #
95
+ # @return [Boolean] true if both client_id and client_secret are present
96
+ def oauth_authenticated?
97
+ !!@client_id && !!@client_secret
98
+ end
99
+
100
+ # Check if API key authentication is configured
101
+ #
102
+ # @return [Boolean] true if api_key is present
103
+ def api_key_authenticated?
104
+ !!@api_key
105
+ end
106
+
107
+ # Check if single-use token authentication is configured
108
+ #
109
+ # @return [Boolean] true if single_use_token is present
110
+ def single_use_token_authenticated?
111
+ !!@single_use_token
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booqable
4
+ # Client for the Booqable API
5
+ #
6
+ # Provides a Ruby interface to interact with the Booqable rental management API.
7
+ # The client can be configured with various authentication methods including
8
+ # API keys, OAuth, and single-use tokens.
9
+ #
10
+ # @example Initialize with API key
11
+ # client = Booqable::Client.new(
12
+ # api_key: "your_api_key",
13
+ # company_id: "your_company_id"
14
+ # )
15
+ #
16
+ # @example Initialize with OAuth
17
+ # client = Booqable::Client.new(
18
+ # client_id: "your_client_id",
19
+ # client_secret: "your_client_secret",
20
+ # company_id: "your_company_id"
21
+ # )
22
+ #
23
+ # @see https://developers.booqable.com/
24
+ class Client
25
+ include Booqable::Configurable
26
+ include Booqable::Resources
27
+ include Booqable::Auth
28
+ include Booqable::HTTP
29
+
30
+ # List of configuration keys that contain sensitive information
31
+ # and should be masked in inspect output
32
+ SECRETS = %w[
33
+ client_secret
34
+ client_id
35
+ api_key
36
+ single_use_token
37
+ single_use_token_private_key
38
+ single_use_token_secret
39
+ refresh_token
40
+ access_token
41
+ ]
42
+
43
+ # Initialize a new Client
44
+ #
45
+ # @param options [Hash] Configuration options for the client
46
+ # @see Booqable::Configurable For a complete list of supported configuration options
47
+ def initialize(options = {})
48
+ # Use options passed in, but fall back to module defaults
49
+ #
50
+ # This may look like a `.keys.each` which should be replaced with `#each_key`, but
51
+ # this doesn't actually work, since `#keys` is just a method we've defined ourselves.
52
+ # The class doesn't fulfill the whole `Enumerable` contract.
53
+ Booqable::Configurable.keys.each do |key|
54
+ value = options[key].nil? ? Booqable.instance_variable_get(:"@#{key}") : options[key]
55
+ instance_variable_set(:"@#{key}", value)
56
+ end
57
+ end
58
+
59
+ # String representation of the client with sensitive information masked
60
+ #
61
+ # Overrides the default inspect method to hide sensitive configuration
62
+ # values like API keys, client secrets, and tokens by replacing them
63
+ # with asterisks.
64
+ #
65
+ # @return [String] String representation with secrets masked
66
+ # @example
67
+ # client = Booqable::Client.new(api_key: "secret123")
68
+ # client.inspect #=> "#<Booqable::Client:0x... @api_key=\"*********\">"
69
+ def inspect
70
+ inspected = super
71
+
72
+ secrets = SECRETS.map { |secret| instance_variable_get("@#{secret}") }
73
+
74
+ inspected.gsub!(/"(#{secrets.join("|")})"/) do |match|
75
+ match.gsub!(/./, "*")
76
+ end
77
+
78
+ inspected
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Booqable
4
+ # Configuration options for {Client}, defaulting to values
5
+ # in {Default}
6
+ module Configurable
7
+ # @!attribute api_endpoint
8
+ # @return [String] Base URL for API requests. default: https://company.booqable.com/api/4
9
+ # @!attribute auto_paginate
10
+ # @return [Boolean] Auto fetch next page of results until rate limit reached
11
+ # @!attribute client_id
12
+ # @return [String] Configure OAuth app key
13
+ # @!attribute [w] client_secret
14
+ # @return [String] Configure OAuth app secret
15
+ # @!attribute default_media_type
16
+ # @return [String] Configure preferred media type (for API versioning, for example)
17
+ # @!attribute connection_options
18
+ # @see https://github.com/lostisland/faraday
19
+ # @return [Hash] Configure connection options for Faraday
20
+ # @!attribute middleware
21
+ # @see https://github.com/lostisland/faraday
22
+ # @return [Faraday::Builder or Faraday::RackBuilder] Configure middleware for Faraday
23
+ # @!attribute per_page
24
+ # @return [String] Configure page size for paginated results. API default: 25
25
+ # @!attribute proxy
26
+ # @see https://github.com/lostisland/faraday
27
+ # @return [String] URI for proxy server
28
+ # @!attribute ssl_verify_mode
29
+ # @see https://github.com/lostisland/faraday
30
+ # @return [String] SSL verify mode for ssl connections
31
+ # @!attribute user_agent
32
+ # @return [String] Configure User-Agent header for requests.
33
+
34
+ attr_accessor :api_domain,
35
+ :api_endpoint,
36
+ :api_key,
37
+ :api_version,
38
+ :auto_paginate,
39
+ :client_id,
40
+ :client_secret,
41
+ :company_id,
42
+ :connection_options,
43
+ :debug,
44
+ :default_media_type,
45
+ :middleware,
46
+ :no_retries,
47
+ :per_page,
48
+ :proxy,
49
+ :read_token,
50
+ :redirect_uri,
51
+ :single_use_token,
52
+ :single_use_token_algorithm,
53
+ :single_use_token_company_id,
54
+ :single_use_token_expiration_period,
55
+ :single_use_token_private_key,
56
+ :single_use_token_secret,
57
+ :single_use_token_user_id,
58
+ :ssl_verify_mode,
59
+ :user_agent,
60
+ :write_token
61
+
62
+ class << self
63
+ # List of configurable keys for {Booqable::Client}
64
+ # @return [Array] of option keys
65
+ def keys
66
+ @keys ||= %i[
67
+ api_domain
68
+ api_endpoint
69
+ api_key
70
+ api_version
71
+ auto_paginate
72
+ client_id
73
+ client_secret
74
+ company_id
75
+ connection_options
76
+ debug
77
+ default_media_type
78
+ middleware
79
+ no_retries
80
+ per_page
81
+ proxy
82
+ read_token
83
+ redirect_uri
84
+ single_use_token
85
+ single_use_token_algorithm
86
+ single_use_token_company_id
87
+ single_use_token_expiration_period
88
+ single_use_token_private_key
89
+ single_use_token_secret
90
+ single_use_token_user_id
91
+ ssl_verify_mode
92
+ user_agent
93
+ write_token
94
+ ]
95
+ end
96
+ end
97
+
98
+ # Set configuration options using a block
99
+ def configure
100
+ yield self
101
+ end
102
+
103
+ # Reset configuration options to default values
104
+ def reset!
105
+ # rubocop:disable Style/HashEachMethods
106
+ #
107
+ # This may look like a `.keys.each` which should be replaced with `#each_key`, but
108
+ # this doesn't actually work, since `#keys` is just a method we've defined ourselves.
109
+ # The class doesn't fulfill the whole `Enumerable` contract.
110
+ Booqable::Configurable.keys.each do |key|
111
+ # rubocop:enable Style/HashEachMethods
112
+ instance_variable_set(:"@#{key}", Booqable::Default.options[key])
113
+ end
114
+ self
115
+ end
116
+ alias setup reset!
117
+
118
+ # Compares client options to a Hash of requested options
119
+ #
120
+ # @param opts [Hash] Options to compare with current client options
121
+ # @return [Boolean]
122
+ def same_options?(opts)
123
+ opts.hash == options.hash
124
+ end
125
+
126
+ # Whether to print debug information
127
+ #
128
+ # @return [Boolean] true if debug mode is enabled, false otherwise
129
+ def debug?
130
+ @debug || false
131
+ end
132
+
133
+ private
134
+
135
+ def api_protocol
136
+ @api_domain == "booqable.com" ? "https" : "http"
137
+ end
138
+
139
+ def options
140
+ Booqable::Configurable.keys.to_h { |key| [ key, instance_variable_get(:"@#{key}") ] }
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,215 @@
1
+
2
+ module Booqable
3
+ # Default configuration options for {Client}
4
+ #
5
+ # Provides default values for all configuration options, with support for
6
+ # environment variable overrides. All defaults can be overridden by setting
7
+ # the appropriate environment variables.
8
+ #
9
+ # @example Environment variable configuration
10
+ # ENV["BOOQABLE_API_KEY"] = "your_api_key"
11
+ # ENV["BOOQABLE_COMPANY_ID"] = "your_company_id"
12
+ # ENV["BOOQABLE_PER_PAGE"] = "50"
13
+ module Default
14
+ # Default User Agent header string
15
+ USER_AGENT = "Booqable Ruby Gem #{Booqable::VERSION}"
16
+
17
+ # Default media type (json:api) for requests
18
+ MEDIA_TYPE = "application/vnd.api+json"
19
+
20
+ # Default retry options for Faraday::Retry middleware
21
+ RETRY_OPTIONS = {
22
+ exceptions: Faraday::Retry::Middleware::DEFAULT_EXCEPTIONS + [ Booqable::ServerError ],
23
+ max: 2, # maximum number of retries (total of 3 attempts including the first)
24
+ interval: 2, # seconds to wait before retrying
25
+ interval_randomness: 0.5, # randomize the interval by this amount
26
+ backoff_factor: 2 # multiply the interval by this factor on each retry
27
+ }
28
+
29
+ # Basic middleware stack for Faraday::Connection (without authentication middleware)
30
+ MIDDLEWARE = Faraday::RackBuilder.new do |builder|
31
+ # Retry middleware
32
+ builder.use Faraday::Retry::Middleware, RETRY_OPTIONS
33
+ # Error handling middleware
34
+ builder.use Booqable::Middleware::RaiseError
35
+
36
+ builder.adapter Faraday.default_adapter
37
+ end
38
+
39
+ class << self
40
+ # Configuration options
41
+ # @return [Hash]
42
+ def options
43
+ Booqable::Configurable.keys.to_h { |key| [ key, send(key) ] }
44
+ end
45
+
46
+ # Default API endpoint from ENV
47
+ # @return [String]
48
+ def api_domain
49
+ ENV.fetch("BOOQABLE_API_DOMAIN", "booqable.com")
50
+ end
51
+
52
+ # Default API version from ENV
53
+ # @return [Integer] API version number
54
+ def api_version
55
+ ENV.fetch("BOOQABLE_API_VERSION", 4)
56
+ end
57
+
58
+ # Default API endpoint from ENV
59
+ # @return [String, nil] Full API endpoint URL or nil to construct from domain
60
+ def api_endpoint
61
+ ENV.fetch("BOOQABLE_API_ENDPOINT", nil)
62
+ end
63
+
64
+ # Default pagination preference from ENV
65
+ # @return [String]
66
+ def auto_paginate
67
+ ENV.fetch("BOOQABLE_AUTO_PAGINATE", nil)
68
+ end
69
+
70
+ # Default OAuth app key from ENV
71
+ # @return [String]
72
+ def client_id
73
+ ENV.fetch("BOOQABLE_CLIENT_ID", nil)
74
+ end
75
+
76
+ # Default OAuth app secret from ENV
77
+ # @return [String]
78
+ def client_secret
79
+ ENV.fetch("BOOQABLE_CLIENT_SECRET", nil)
80
+ end
81
+
82
+ # Default company ID from ENV
83
+ # @return [String, nil] Company identifier
84
+ def company_id
85
+ ENV.fetch("BOOQABLE_COMPANY_ID", nil)
86
+ end
87
+
88
+ # Default redirect URI for OAuth from ENV
89
+ # @return [String]
90
+ def redirect_uri
91
+ ENV.fetch("BOOQABLE_REDIRECT_URI", nil)
92
+ end
93
+
94
+ # Default options for Faraday::Connection
95
+ # @return [Hash]
96
+ def connection_options
97
+ nil
98
+ end
99
+
100
+ # Default media type from ENV or {MEDIA_TYPE}
101
+ # @return [String]
102
+ def default_media_type
103
+ ENV.fetch("BOOQABLE_DEFAULT_MEDIA_TYPE") { MEDIA_TYPE }
104
+ end
105
+
106
+ # Default middleware stack for Faraday::Connection
107
+ # from {MIDDLEWARE}
108
+ # @return [Faraday::RackBuilder or Faraday::Builder]
109
+ def middleware
110
+ MIDDLEWARE
111
+ end
112
+
113
+ # Default pagination page size from ENV
114
+ # @return [Integer] Page size
115
+ def per_page
116
+ page_size = ENV.fetch("BOOQABLE_PER_PAGE", 25)
117
+
118
+ page_size&.to_i
119
+ end
120
+
121
+ # Default proxy server URI for Faraday connection from ENV
122
+ # @return [String]
123
+ def proxy
124
+ ENV.fetch("BOOQABLE_PROXY", nil)
125
+ end
126
+
127
+ # Default SSL verify mode from ENV
128
+ # @return [Integer]
129
+ def ssl_verify_mode
130
+ # 0 is OpenSSL::SSL::VERIFY_NONE
131
+ # 1 is OpenSSL::SSL::SSL_VERIFY_PEER
132
+ # the standard default for SSL is SSL_VERIFY_PEER which requires a server certificate check on the client
133
+ ENV.fetch("BOOQABLE_SSL_VERIFY_MODE", 1).to_i
134
+ end
135
+
136
+ # Default User-Agent header string from ENV or {USER_AGENT}
137
+ # @return [String]
138
+ def user_agent
139
+ ENV.fetch("BOOQABLE_USER_AGENT") { USER_AGENT }
140
+ end
141
+
142
+ # Default OAuth token reader
143
+ # @return [Proc] Empty proc that returns nothing
144
+ def read_token
145
+ Proc.new { }
146
+ end
147
+
148
+ # Default OAuth token writer
149
+ # @return [Proc] Empty proc that does nothing
150
+ def write_token
151
+ Proc.new { }
152
+ end
153
+
154
+ # Default API key from ENV
155
+ # @return [String, nil] API key for authentication
156
+ def api_key
157
+ ENV.fetch("BOOQABLE_API_KEY", nil)
158
+ end
159
+
160
+ # Default single use token from ENV
161
+ # @return [String, nil] Single use token for authentication
162
+ def single_use_token
163
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN", nil)
164
+ end
165
+
166
+ # Default single use token algorithm from ENV
167
+ # @return [String, nil] Algorithm for single use token (e.g., "HS256", "RS256")
168
+ def single_use_token_algorithm
169
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_ALGORITHM", nil)
170
+ end
171
+
172
+ # Default single use token secret from ENV
173
+ # @return [String, nil] Secret for HMAC single use token signing
174
+ def single_use_token_secret
175
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_SECRET", nil)
176
+ end
177
+
178
+ # Default single use token private key from ENV
179
+ # @return [String, nil] Private key for RSA/ECDSA single use token signing
180
+ def single_use_token_private_key
181
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_PRIVATE_KEY", nil)
182
+ end
183
+
184
+ # Default single use token expiration period from ENV
185
+ # @return [Integer] Token expiration period in seconds
186
+ def single_use_token_expiration_period
187
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_EXPIRATION_PERIOD") { 10 * 60 }.to_i # default to 10 minutes
188
+ end
189
+
190
+ # Default single use token company ID from ENV
191
+ # @return [String, nil] Company ID for single use token
192
+ def single_use_token_company_id
193
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_COMPANY_ID", nil)
194
+ end
195
+
196
+ # Default single use token user ID from ENV
197
+ # @return [String, nil] User ID for single use token
198
+ def single_use_token_user_id
199
+ ENV.fetch("BOOQABLE_SINGLE_USE_TOKEN_USER_ID", nil)
200
+ end
201
+
202
+ # Default debug mode setting
203
+ # @return [Boolean] Whether debug mode is enabled
204
+ def debug
205
+ false
206
+ end
207
+
208
+ # Default retry setting
209
+ # @return [Boolean] Whether to disable retries
210
+ def no_retries
211
+ false
212
+ end
213
+ end
214
+ end
215
+ end