Module: Basecamp
- Defined in:
- lib/basecamp.rb,
lib/basecamp/http.rb,
lib/basecamp/error.rb,
lib/basecamp/hooks.rb,
lib/basecamp/oauth.rb,
lib/basecamp/client.rb,
lib/basecamp/config.rb,
lib/basecamp/version.rb,
lib/basecamp/security.rb,
lib/basecamp/api_error.rb,
lib/basecamp/exit_code.rb,
lib/basecamp/list_meta.rb,
lib/basecamp/auth_error.rb,
lib/basecamp/error_code.rb,
lib/basecamp/noop_hooks.rb,
lib/basecamp/oauth/pkce.rb,
lib/basecamp/bearer_auth.rb,
lib/basecamp/chain_hooks.rb,
lib/basecamp/oauth/token.rb,
lib/basecamp/usage_error.rb,
lib/basecamp/logger_hooks.rb,
lib/basecamp/oauth/config.rb,
lib/basecamp/request_info.rb,
lib/basecamp/auth_strategy.rb,
lib/basecamp/network_error.rb,
lib/basecamp/oauth/fetcher.rb,
lib/basecamp/oauth/exchange.rb,
lib/basecamp/oauth/resource.rb,
lib/basecamp/operation_info.rb,
lib/basecamp/request_result.rb,
lib/basecamp/token_provider.rb,
lib/basecamp/webhooks/event.rb,
lib/basecamp/ambiguous_error.rb,
lib/basecamp/download_result.rb,
lib/basecamp/forbidden_error.rb,
lib/basecamp/generated/types.rb,
lib/basecamp/list_enumerator.rb,
lib/basecamp/not_found_error.rb,
lib/basecamp/oauth/discovery.rb,
lib/basecamp/webhooks/verify.rb,
lib/basecamp/operation_result.rb,
lib/basecamp/rate_limit_error.rb,
lib/basecamp/validation_error.rb,
lib/basecamp/oauth/device_flow.rb,
lib/basecamp/oauth/oauth_error.rb,
lib/basecamp/webhooks/receiver.rb,
lib/basecamp/services/merge_safe.rb,
lib/basecamp/oauth_token_provider.rb,
lib/basecamp/oauth/refresh_request.rb,
lib/basecamp/static_token_provider.rb,
lib/basecamp/oauth/discovery_result.rb,
lib/basecamp/oauth/exchange_request.rb,
lib/basecamp/oauth/device_flow_error.rb,
lib/basecamp/webhooks/rack_middleware.rb,
lib/basecamp/services/cards_extensions.rb,
lib/basecamp/services/todos_extensions.rb,
lib/basecamp/oauth/device_authorization.rb,
lib/basecamp/webhooks/verification_error.rb,
lib/basecamp/services/documents_extensions.rb,
lib/basecamp/services/schedules_extensions.rb,
lib/basecamp/services/todolists_extensions.rb,
lib/basecamp/services/authorization_service.rb,
lib/basecamp/generated/services/base_service.rb,
lib/basecamp/oauth/discovery_selection_error.rb,
lib/basecamp/generated/services/cards_service.rb,
lib/basecamp/generated/services/todos_service.rb,
lib/basecamp/generated/services/tools_service.rb,
lib/basecamp/generated/services/boosts_service.rb,
lib/basecamp/generated/services/drafts_service.rb,
lib/basecamp/generated/services/events_service.rb,
lib/basecamp/generated/services/gauges_service.rb,
lib/basecamp/generated/services/lineup_service.rb,
lib/basecamp/generated/services/people_service.rb,
lib/basecamp/generated/services/search_service.rb,
lib/basecamp/generated/services/vaults_service.rb,
lib/basecamp/oauth/protected_resource_metadata.rb,
lib/basecamp/generated/services/account_service.rb,
lib/basecamp/generated/services/folders_service.rb,
lib/basecamp/generated/services/reports_service.rb,
lib/basecamp/generated/services/uploads_service.rb,
lib/basecamp/generated/services/checkins_service.rb,
lib/basecamp/generated/services/comments_service.rb,
lib/basecamp/generated/services/forwards_service.rb,
lib/basecamp/generated/services/messages_service.rb,
lib/basecamp/generated/services/my_notes_service.rb,
lib/basecamp/generated/services/projects_service.rb,
lib/basecamp/generated/services/timeline_service.rb,
lib/basecamp/generated/services/todosets_service.rb,
lib/basecamp/generated/services/webhooks_service.rb,
lib/basecamp/generated/services/bookmarks_service.rb,
lib/basecamp/generated/services/calendars_service.rb,
lib/basecamp/generated/services/campfires_service.rb,
lib/basecamp/generated/services/documents_service.rb,
lib/basecamp/generated/services/schedules_service.rb,
lib/basecamp/generated/services/templates_service.rb,
lib/basecamp/generated/services/todolists_service.rb,
lib/basecamp/generated/services/wormholes_service.rb,
lib/basecamp/generated/services/automation_service.rb,
lib/basecamp/generated/services/card_steps_service.rb,
lib/basecamp/generated/services/everything_service.rb,
lib/basecamp/generated/services/recordings_service.rb,
lib/basecamp/generated/services/timesheets_service.rb,
lib/basecamp/generated/services/attachments_service.rb,
lib/basecamp/generated/services/card_tables_service.rb,
lib/basecamp/generated/services/cloud_files_service.rb,
lib/basecamp/generated/services/hill_charts_service.rb,
lib/basecamp/generated/services/card_columns_service.rb,
lib/basecamp/generated/services/message_types_service.rb,
lib/basecamp/generated/services/subscriptions_service.rb,
lib/basecamp/generated/services/client_replies_service.rb,
lib/basecamp/generated/services/message_boards_service.rb,
lib/basecamp/generated/services/my_assignments_service.rb,
lib/basecamp/generated/services/todolist_groups_service.rb,
lib/basecamp/generated/services/client_approvals_service.rb,
lib/basecamp/generated/services/google_documents_service.rb,
lib/basecamp/generated/services/my_notifications_service.rb,
lib/basecamp/generated/services/client_visibility_service.rb,
lib/basecamp/generated/services/client_correspondences_service.rb
Overview
Main entry point for the Basecamp SDK.
The SDK follows a Client -> AccountClient pattern:
- Client: Holds shared resources (HTTP client, token provider, hooks)
- AccountClient: Bound to a specific account ID, provides service accessors
Defined Under Namespace
Modules: AuthStrategy, ErrorCode, ExitCode, Hooks, Oauth, Security, Services, TokenProvider, Types, Webhooks Classes: AccountClient, AmbiguousError, ApiError, AuthError, BearerAuth, ChainHooks, Client, Config, DownloadResult, Error, ForbiddenError, Http, ListEnumerator, ListMeta, LoggerHooks, NetworkError, NoopHooks, NotFoundError, OauthTokenProvider, OperationInfo, OperationResult, RateLimitError, RequestInfo, RequestResult, Response, StaticTokenProvider, UsageError, ValidationError
Constant Summary collapse
- VERSION =
"0.13.0"- API_VERSION =
"2026-08-05"
Class Method Summary collapse
-
.client(access_token: nil, auth: nil, account_id: nil, base_url: Config::DEFAULT_BASE_URL, hooks: nil) ⇒ Client, AccountClient
Creates a new Basecamp client.
-
.compose_validation_message(message, field_errors) ⇒ String?
Merges the top-level error message with the flattened field-keyed errors: appended in parentheses when both are present, standing alone when only the field errors are.
-
.error_from_response(status, body = nil, retry_after: nil) ⇒ Error
Maps an HTTP response to the appropriate error class.
-
.filename_from_url(raw_url) ⇒ Object
Extracts a filename from the last path segment of a URL.
-
.parse_bare_field_errors(data) ⇒ Hash{String => Array<String>}?
Extracts an unwrapped field map — the
render json: @webhook.errorsrendering, where the whole body is => ["msg", ...]. -
.parse_error_message(body) ⇒ String?
Parses error message from response body.
-
.parse_field_errors(body) ⇒ Hash{String => Array<String>}?
Extracts the field-keyed validation errors map from a response body — the Rails RecordInvalid rendering => {"field" => ["msg", ...]}.
Class Method Details
.client(access_token: nil, auth: nil, account_id: nil, base_url: Config::DEFAULT_BASE_URL, hooks: nil) ⇒ Client, AccountClient
Creates a new Basecamp client.
This is a convenience method that creates a Client with the given options.
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/basecamp.rb', line 91 def self.client( access_token: nil, auth: nil, account_id: nil, base_url: Config::DEFAULT_BASE_URL, hooks: nil ) raise ArgumentError, "provide either access_token or auth, not both" if access_token && auth raise ArgumentError, "provide access_token or auth" if !access_token && !auth config = Config.new(base_url: base_url) client = if auth Client.new(config: config, auth_strategy: auth, hooks: hooks) else token_provider = StaticTokenProvider.new(access_token) Client.new(config: config, token_provider: token_provider, hooks: hooks) end account_id ? client.for_account(account_id) : client end |
.compose_validation_message(message, field_errors) ⇒ String?
Merges the top-level error message with the flattened field-keyed errors: appended in parentheses when both are present, standing alone when only the field errors are. The flattened shape — fields sorted lexicographically, a field's messages joined with "; ", fields joined with ", " — is shared by all six SDKs; change it everywhere or nowhere. Callers truncate the composed result so the appended tail is capped too.
238 239 240 241 242 243 244 245 246 247 |
# File 'lib/basecamp.rb', line 238 def self.(, field_errors) if field_errors.nil? else flat = field_errors.keys.sort \ .map { |field| "#{field}: #{field_errors[field].join("; ")}" } \ .join(", ") ? "#{} (#{flat})" : flat end end |
.error_from_response(status, body = nil, retry_after: nil) ⇒ Error
Maps an HTTP response to the appropriate error class.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/basecamp.rb', line 119 def self.error_from_response(status, body = nil, retry_after: nil) = (body) || "Request failed" case status when 400, 422 field_errors = parse_field_errors(body) = Security.truncate(((body), field_errors) || "Request failed") ValidationError.new(, http_status: status, field_errors: field_errors) when 401 AuthError.new() when 403 ForbiddenError.new() when 404 NotFoundError.new(message: ) when 429 RateLimitError.new(retry_after: retry_after) when 500 ApiError.new("Server error (500)", http_status: 500, retryable: true) when 502, 503, 504 ApiError.new("Gateway error (#{status})", http_status: status, retryable: true) else ApiError.from_status(status, ) end end |
.filename_from_url(raw_url) ⇒ Object
Extracts a filename from the last path segment of a URL. Falls back to "download" if the URL is unparseable or has no path segments.
146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
# File 'lib/basecamp.rb', line 146 def self.filename_from_url(raw_url) uri = URI.parse(raw_url) path = uri.path return "download" if path.nil? || path.empty? || path == "/" || path.end_with?("/") segments = path.split("/").reject(&:empty?) return "download" if segments.empty? last = segments.last return "download" if last.nil? || last.empty? || last == "." || last == "/" URI::RFC2396_PARSER.unescape(last) rescue URI::InvalidURIError "download" end |
.parse_bare_field_errors(data) ⇒ Hash{String => Array<String>}?
Extracts an unwrapped field map — the render json: @webhook.errors
rendering, where the whole body is => ["msg", ...]. The gate is
all-or-nothing by design (SPEC section 6 step 2): with no "errors" key to
declare intent, only shape distinguishes a field map from any other JSON
object, so a single non-conforming member means this is not one.
214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/basecamp.rb', line 214 def self.(data) return nil unless data.is_a?(Hash) && !data.empty? # Only "errors" is structurally reserved (it belongs to the wrapped path). # "error" and "message" are not excluded by name: a flat body carries them # as strings, which the shape gate below already rejects. return nil if data.key?("errors") data.each_with_object({}) do |(field, values), result| return nil unless values.is_a?(Array) && !values.empty? return nil unless values.all? { || .is_a?(String) && !.empty? } result[field.to_s] = values end end |
.parse_error_message(body) ⇒ String?
Parses error message from response body. A key is used only when its value is a String (SPEC section 6), so a malformed scalar member such as {} cannot raise or leak a non-string into the message.
167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/basecamp.rb', line 167 def self.(body) return nil if body.nil? || body.empty? Security.check_body_size!(body, Security::MAX_ERROR_BODY_BYTES, "Error") data = JSON.parse(body) msg = data.is_a?(Hash) ? [ data["error"], data["message"] ].find { |value| value.is_a?(String) } : nil msg ? Security.truncate(msg) : nil rescue JSON::ParserError, ApiError nil end |
.parse_field_errors(body) ⇒ Hash{String => Array<String>}?
Extracts the field-keyed validation errors map from a response body — the Rails RecordInvalid rendering => {"field" => ["msg", ...]}. Entries whose value is not an array are skipped, non-string elements are dropped, and a map with no usable entries is treated as absent (nil).
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/basecamp.rb', line 185 def self.parse_field_errors(body) return nil if body.nil? || body.empty? Security.check_body_size!(body, Security::MAX_ERROR_BODY_BYTES, "Error") data = JSON.parse(body) errors = data.is_a?(Hash) ? data["errors"] : nil if errors.is_a?(Hash) field_errors = errors.each_with_object({}) do |(field, values), result| next unless values.is_a?(Array) = values.grep(String) result[field.to_s] = unless .empty? end field_errors.empty? ? nil : field_errors else (data) end rescue JSON::ParserError, ApiError nil end |