basecamp-sdk 0.13.0 → 0.15.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.
- checksums.yaml +4 -4
- data/README.md +3 -2
- data/lib/basecamp/client.rb +34 -2
- data/lib/basecamp/config.rb +25 -3
- data/lib/basecamp/error.rb +1 -0
- data/lib/basecamp/error_code.rb +1 -0
- data/lib/basecamp/exit_code.rb +1 -0
- data/lib/basecamp/generated/metadata.json +12 -1
- data/lib/basecamp/generated/services/uploads_service.rb +18 -0
- data/lib/basecamp/generated/types.rb +218 -27
- data/lib/basecamp/http.rb +45 -2
- data/lib/basecamp/limit_exceeded_error.rb +22 -0
- data/lib/basecamp/security.rb +8 -2
- data/lib/basecamp/services/authorization_service.rb +13 -2
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +4 -0
- data/scripts/generate-metadata.rb +3 -1
- data/scripts/generate-services.rb +4 -2
- data/scripts/generate-types.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6c1f02712a441b2b7772199b8029904b0c254be6c9b687a94a8d957f7d8a066
|
|
4
|
+
data.tar.gz: f0750c8e75561aaa908630440f0b2c4276a54abb653418361cc4363883497d9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7d6ecde9f18d17b9baadc7b827b6d396c0f6e63ab304d93800ee2e35f3803af9d668965b5d5885a408d926e780700f7fe0320d68375924a6647cce2462eee20
|
|
7
|
+
data.tar.gz: a445a4123c2ebf596026035d347dbe99883b207a650003d2651f0dbc77eed9c0f4634703555e17af674aca87e015fdbe17f38a40e9370deff1b73a42cec6d042
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Official Ruby SDK for the [Basecamp API](https://github.com/basecamp/bc3-api).
|
|
4
4
|
|
|
5
|
-
**Upgrading to v0.
|
|
5
|
+
**Upgrading to v0.14.0?** Read [MIGRATING.md](../MIGRATING.md) before you bump the version — nothing catches either break at load time. `uploads.list_versions` entries now carry the version event's keys, with the file nested under `"upload"` — code reading `version["filename"]` was getting nil and now has a real place to look — and every 507 reports `limit_exceeded` instead of a retryable `api_error`, so a `when` falling through to an else arm reroutes storage, project and webhook limits silently. Coming from v0.12.0 or earlier, read v0.13.0's section too.
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
@@ -316,7 +316,7 @@ aborts before an oversized body is buffered.
|
|
|
316
316
|
|
|
317
317
|
## Services
|
|
318
318
|
|
|
319
|
-
The SDK provides
|
|
319
|
+
The SDK provides the full account-scoped service set documented in SPEC §5. The table below covers the common ones; see `lib/basecamp/generated/services/` for the authoritative, complete set:
|
|
320
320
|
|
|
321
321
|
| Service | Description |
|
|
322
322
|
|---------|-------------|
|
|
@@ -480,6 +480,7 @@ end
|
|
|
480
480
|
| `ValidationError` | Invalid request data (400, 422) |
|
|
481
481
|
| `RateLimitError` | Rate limit exceeded (429) |
|
|
482
482
|
| `NetworkError` | Connection failures |
|
|
483
|
+
| `LimitExceededError` | Account limit reached (507) — file storage, projects, webhooks |
|
|
483
484
|
|
|
484
485
|
### Validation Errors
|
|
485
486
|
|
data/lib/basecamp/client.rb
CHANGED
|
@@ -240,7 +240,9 @@ module Basecamp
|
|
|
240
240
|
#
|
|
241
241
|
# Handles the full download flow: URL rewriting to the configured API host,
|
|
242
242
|
# authenticated first hop (which typically 302s to a signed download URL),
|
|
243
|
-
# and unauthenticated second hop to fetch the actual file content.
|
|
243
|
+
# and unauthenticated second hop to fetch the actual file content. Neither
|
|
244
|
+
# hop follows a redirect on its own: hop 1's is the dispatch to hop 2, and
|
|
245
|
+
# a redirect on hop 2 is an error (SPEC §14 "Hop-2 Redirect Policy").
|
|
244
246
|
#
|
|
245
247
|
# @param raw_url [String] absolute download URL (e.g., from bc-attachment elements)
|
|
246
248
|
# @return [DownloadResult] the download result with body, content_type, content_length, filename
|
|
@@ -616,7 +618,26 @@ module Basecamp
|
|
|
616
618
|
end
|
|
617
619
|
|
|
618
620
|
def fetch_signed_download(url)
|
|
619
|
-
|
|
621
|
+
# The Location target is server-supplied, and this unauthenticated hop
|
|
622
|
+
# has no same_origin? gate to refuse it (a signed URL is legitimately
|
|
623
|
+
# cross-origin), so illegible targets must be refused here — as
|
|
624
|
+
# ApiError, matching the legible refusals Go and TypeScript surface
|
|
625
|
+
# when their dial rejects the scheme. One predicate states the whole
|
|
626
|
+
# invariant — a dialable absolute HTTP(S) URL — because piecemeal
|
|
627
|
+
# guards kept leaking: "it parses" admitted "ftp://", and "scheme is
|
|
628
|
+
# http(s)" admitted hostless "http:foo", which Net::HTTP::Get rejects
|
|
629
|
+
# with a raw ArgumentError before the dial (nothing is ever sent).
|
|
630
|
+
# URI.parse types the scheme (URI::HTTPS < URI::HTTP), and a nil or
|
|
631
|
+
# empty host ("http:foo", "http:///x") is undialable.
|
|
632
|
+
uri = begin
|
|
633
|
+
URI.parse(url)
|
|
634
|
+
rescue URI::Error
|
|
635
|
+
nil
|
|
636
|
+
end
|
|
637
|
+
unless uri.is_a?(URI::HTTP) && uri.host && !uri.host.empty?
|
|
638
|
+
raise ApiError.new("redirect to undialable download URL: #{Security.truncate(url)}")
|
|
639
|
+
end
|
|
640
|
+
|
|
620
641
|
http_client = Net::HTTP.new(uri.host, uri.port)
|
|
621
642
|
http_client.use_ssl = (uri.scheme == "https")
|
|
622
643
|
http_client.open_timeout = config.timeout
|
|
@@ -625,11 +646,22 @@ module Basecamp
|
|
|
625
646
|
request = Net::HTTP::Get.new(uri)
|
|
626
647
|
|
|
627
648
|
begin
|
|
649
|
+
# Net::HTTP#request never follows a redirect, which is the policy:
|
|
650
|
+
# the signed URL is the one destination the API host named, and a
|
|
651
|
+
# redirect from it is refused below, not dialled (SPEC §14 "Hop-2
|
|
652
|
+
# Redirect Policy"). Stated here so a move to a following client
|
|
653
|
+
# (Faraday, Net::HTTP.get_response's callers) has to argue with it.
|
|
628
654
|
response = http_client.request(request)
|
|
629
655
|
rescue StandardError => e
|
|
630
656
|
raise NetworkError.new("Download failed: #{e.message}", cause: e)
|
|
631
657
|
end
|
|
632
658
|
|
|
659
|
+
# The exact set hop 1 dispatches on, not Net::HTTPRedirection — that
|
|
660
|
+
# class also covers 304, which is a cache answer, not a redirect.
|
|
661
|
+
if [ 301, 302, 303, 307, 308 ].include?(response.code.to_i)
|
|
662
|
+
raise ApiError.new("redirect #{response.code} on the signed download hop is not followed", http_status: response.code.to_i)
|
|
663
|
+
end
|
|
664
|
+
|
|
633
665
|
unless response.is_a?(Net::HTTPSuccess)
|
|
634
666
|
raise ApiError.new("download failed with status #{response.code}", http_status: response.code.to_i)
|
|
635
667
|
end
|
data/lib/basecamp/config.rb
CHANGED
|
@@ -35,7 +35,21 @@ module Basecamp
|
|
|
35
35
|
attr_accessor :max_jitter
|
|
36
36
|
|
|
37
37
|
# @return [Integer] maximum pages to fetch in paginated requests
|
|
38
|
-
|
|
38
|
+
attr_reader :max_pages
|
|
39
|
+
|
|
40
|
+
# A validating writer, not attr_accessor: Http reads the config live at
|
|
41
|
+
# every page boundary, so `config.max_pages = -1` after construction
|
|
42
|
+
# replaced the validated cap with a value validate! would have refused —
|
|
43
|
+
# the same second door TypeScript's compile-time-only `readonly` left
|
|
44
|
+
# open. The config deliberately stays mutable (from_file → load_from_env
|
|
45
|
+
# re-validation is builder-style by design); only the cap's invariant
|
|
46
|
+
# must hold at assignment.
|
|
47
|
+
#
|
|
48
|
+
# @param value [Integer] maximum pages to fetch in paginated requests
|
|
49
|
+
def max_pages=(value)
|
|
50
|
+
validate_max_pages!(value)
|
|
51
|
+
@max_pages = value
|
|
52
|
+
end
|
|
39
53
|
|
|
40
54
|
# Default values
|
|
41
55
|
DEFAULT_BASE_URL = "https://3.basecampapi.com"
|
|
@@ -164,7 +178,9 @@ module Basecamp
|
|
|
164
178
|
# @param path [String] path to JSON config file
|
|
165
179
|
# @return [Config]
|
|
166
180
|
def self.from_file(path)
|
|
167
|
-
|
|
181
|
+
# UTF-8 regardless of process locale — JSON is UTF-8 (RFC 8259), and
|
|
182
|
+
# LC_ALL=C would otherwise read as US-ASCII.
|
|
183
|
+
data = JSON.parse(File.read(path, encoding: "UTF-8"))
|
|
168
184
|
config = new(
|
|
169
185
|
base_url: data["base_url"] || DEFAULT_BASE_URL,
|
|
170
186
|
timeout: data["timeout"] || DEFAULT_TIMEOUT,
|
|
@@ -199,7 +215,13 @@ module Basecamp
|
|
|
199
215
|
def validate!
|
|
200
216
|
raise ArgumentError, "timeout must be positive" unless @timeout.is_a?(Numeric) && @timeout > 0
|
|
201
217
|
raise ArgumentError, "max_retries must be non-negative" unless @max_retries.is_a?(Integer) && @max_retries >= 0
|
|
202
|
-
|
|
218
|
+
validate_max_pages!(@max_pages)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Shared by validate! and the max_pages= writer. Integer-only, so a
|
|
222
|
+
# boolean or Float::INFINITY is refused along with zero and negatives.
|
|
223
|
+
def validate_max_pages!(value)
|
|
224
|
+
raise ArgumentError, "max_pages must be positive" unless value.is_a?(Integer) && value > 0
|
|
203
225
|
end
|
|
204
226
|
|
|
205
227
|
def normalize_url(url)
|
data/lib/basecamp/error.rb
CHANGED
|
@@ -79,6 +79,7 @@ module Basecamp
|
|
|
79
79
|
when ErrorCode::API then ExitCode::API
|
|
80
80
|
when ErrorCode::AMBIGUOUS then ExitCode::AMBIGUOUS
|
|
81
81
|
when ErrorCode::VALIDATION then ExitCode::VALIDATION
|
|
82
|
+
when ErrorCode::LIMIT_EXCEEDED then ExitCode::LIMIT_EXCEEDED
|
|
82
83
|
else ExitCode::API
|
|
83
84
|
end
|
|
84
85
|
end
|
data/lib/basecamp/error_code.rb
CHANGED
data/lib/basecamp/exit_code.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://basecamp.com/schemas/sdk-metadata.json",
|
|
3
3
|
"version": "1.0.0",
|
|
4
|
-
"generated": "2026-08-
|
|
4
|
+
"generated": "2026-08-12T19:58:36Z",
|
|
5
5
|
"operations": {
|
|
6
6
|
"GetAccount": {
|
|
7
7
|
"retry": {
|
|
@@ -3148,6 +3148,17 @@
|
|
|
3148
3148
|
"maxPageSize": 50
|
|
3149
3149
|
}
|
|
3150
3150
|
},
|
|
3151
|
+
"CreateUploadVersion": {
|
|
3152
|
+
"retry": {
|
|
3153
|
+
"maxAttempts": 2,
|
|
3154
|
+
"baseDelayMs": 1000,
|
|
3155
|
+
"backoff": "exponential",
|
|
3156
|
+
"retryOn": [
|
|
3157
|
+
429,
|
|
3158
|
+
503
|
|
3159
|
+
]
|
|
3160
|
+
}
|
|
3161
|
+
},
|
|
3151
3162
|
"GetVault": {
|
|
3152
3163
|
"retry": {
|
|
3153
3164
|
"maxAttempts": 3,
|
|
@@ -37,6 +37,24 @@ module Basecamp
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Replace an upload's file with a new version
|
|
41
|
+
# @param upload_id [Integer] upload id ID
|
|
42
|
+
# @param attachable_sgid [String] attachable sgid
|
|
43
|
+
# @param base_name [String, nil] Omit to keep the uploaded file's own name. Sending "" also keeps it.
|
|
44
|
+
# @param description [String, nil] Presence-aware: omit to carry the previous version's description forward,
|
|
45
|
+
# send "" to clear it, send a value to set it.
|
|
46
|
+
# @param notify [String, nil] Who to notify: "default", "everyone", or "custom" (the people in subscriptions).
|
|
47
|
+
#
|
|
48
|
+
# Omit both this and subscriptions to notify nobody. A subscriptions array sent
|
|
49
|
+
# without notify is read as "custom".
|
|
50
|
+
# @param subscriptions [Array, nil] People to notify about the replacement and subscribe to the upload.
|
|
51
|
+
# @return [Hash] response data
|
|
52
|
+
def create_version(upload_id:, attachable_sgid:, base_name: nil, description: nil, notify: nil, subscriptions: nil)
|
|
53
|
+
with_operation(service: "uploads", operation: "create_version", is_mutation: true, resource_id: upload_id) do
|
|
54
|
+
http_post("/uploads/#{upload_id}/versions.json", body: compact_params(attachable_sgid: attachable_sgid, base_name: base_name, description: description, notify: notify, subscriptions: subscriptions)).json
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
40
58
|
# List uploads in a vault
|
|
41
59
|
# @param vault_id [Integer] vault id ID
|
|
42
60
|
# @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Auto-generated from OpenAPI spec. Do not edit manually.
|
|
4
|
-
# Generated: 2026-08-
|
|
4
|
+
# Generated: 2026-08-12T19:58:36Z
|
|
5
5
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "time"
|
|
@@ -2672,23 +2672,23 @@ module Basecamp
|
|
|
2672
2672
|
# MyAssignmentAssignee
|
|
2673
2673
|
class MyAssignmentAssignee
|
|
2674
2674
|
include TypeHelpers
|
|
2675
|
-
attr_accessor :
|
|
2675
|
+
attr_accessor :avatar_url, :id, :name
|
|
2676
2676
|
|
|
2677
2677
|
# @return [Array<Symbol>]
|
|
2678
2678
|
def self.required_fields
|
|
2679
|
-
%i[id].freeze
|
|
2679
|
+
%i[avatar_url id name].freeze
|
|
2680
2680
|
end
|
|
2681
2681
|
|
|
2682
2682
|
def initialize(data = {})
|
|
2683
|
-
@id = parse_integer(data["id"])
|
|
2684
2683
|
@avatar_url = data["avatar_url"]
|
|
2684
|
+
@id = parse_integer(data["id"])
|
|
2685
2685
|
@name = data["name"]
|
|
2686
2686
|
end
|
|
2687
2687
|
|
|
2688
2688
|
def to_h
|
|
2689
2689
|
{
|
|
2690
|
-
"id" => @id,
|
|
2691
2690
|
"avatar_url" => @avatar_url,
|
|
2691
|
+
"id" => @id,
|
|
2692
2692
|
"name" => @name,
|
|
2693
2693
|
}.compact
|
|
2694
2694
|
end
|
|
@@ -2955,23 +2955,23 @@ module Basecamp
|
|
|
2955
2955
|
# OutOfOfficePerson
|
|
2956
2956
|
class OutOfOfficePerson
|
|
2957
2957
|
include TypeHelpers
|
|
2958
|
-
attr_accessor :
|
|
2958
|
+
attr_accessor :avatar_url, :id, :name
|
|
2959
2959
|
|
|
2960
2960
|
# @return [Array<Symbol>]
|
|
2961
2961
|
def self.required_fields
|
|
2962
|
-
%i[id].freeze
|
|
2962
|
+
%i[avatar_url id name].freeze
|
|
2963
2963
|
end
|
|
2964
2964
|
|
|
2965
2965
|
def initialize(data = {})
|
|
2966
|
-
@id = parse_integer(data["id"])
|
|
2967
2966
|
@avatar_url = data["avatar_url"]
|
|
2967
|
+
@id = parse_integer(data["id"])
|
|
2968
2968
|
@name = data["name"]
|
|
2969
2969
|
end
|
|
2970
2970
|
|
|
2971
2971
|
def to_h
|
|
2972
2972
|
{
|
|
2973
|
-
"id" => @id,
|
|
2974
2973
|
"avatar_url" => @avatar_url,
|
|
2974
|
+
"id" => @id,
|
|
2975
2975
|
"name" => @name,
|
|
2976
2976
|
}.compact
|
|
2977
2977
|
end
|
|
@@ -4025,62 +4025,114 @@ module Basecamp
|
|
|
4025
4025
|
# SearchResult
|
|
4026
4026
|
class SearchResult
|
|
4027
4027
|
include TypeHelpers
|
|
4028
|
-
attr_accessor :
|
|
4028
|
+
attr_accessor :content, :description, :app_download_url, :app_url, :attachments, :bookmark_url, :boosts_count, :boosts_url, :bubble_up_url, :bucket, :byte_size, :cards_count, :cards_url, :color, :comment_count, :comments_count, :comments_url, :content_attachments, :content_type, :created_at, :creator, :description_attachments, :download_url, :filename, :height, :id, :image_url, :inherits_status, :language, :on_hold, :parent, :plain_text_content, :plain_text_description, :position, :preview_url, :previewable, :sound_url, :status, :subject, :subscribers, :subscription_url, :thumbnail_url, :title, :type, :updated_at, :url, :visible_to_clients, :width
|
|
4029
4029
|
|
|
4030
4030
|
# @return [Array<Symbol>]
|
|
4031
4031
|
def self.required_fields
|
|
4032
|
-
%i[
|
|
4032
|
+
%i[content description].freeze
|
|
4033
4033
|
end
|
|
4034
4034
|
|
|
4035
4035
|
def initialize(data = {})
|
|
4036
|
-
@app_url = data["app_url"]
|
|
4037
4036
|
@content = data["content"]
|
|
4038
4037
|
@description = data["description"]
|
|
4039
|
-
@
|
|
4040
|
-
@
|
|
4041
|
-
@
|
|
4042
|
-
@url = data["url"]
|
|
4038
|
+
@app_download_url = data["app_download_url"]
|
|
4039
|
+
@app_url = data["app_url"]
|
|
4040
|
+
@attachments = parse_array(data["attachments"], "SearchResultAttachment")
|
|
4043
4041
|
@bookmark_url = data["bookmark_url"]
|
|
4042
|
+
@boosts_count = parse_integer(data["boosts_count"])
|
|
4043
|
+
@boosts_url = data["boosts_url"]
|
|
4044
4044
|
@bubble_up_url = data["bubble_up_url"]
|
|
4045
4045
|
@bucket = parse_type(data["bucket"], "RecordingBucket")
|
|
4046
|
+
@byte_size = parse_integer(data["byte_size"])
|
|
4047
|
+
@cards_count = parse_integer(data["cards_count"])
|
|
4048
|
+
@cards_url = data["cards_url"]
|
|
4049
|
+
@color = data["color"]
|
|
4050
|
+
@comment_count = parse_integer(data["comment_count"])
|
|
4051
|
+
@comments_count = parse_integer(data["comments_count"])
|
|
4052
|
+
@comments_url = data["comments_url"]
|
|
4046
4053
|
@content_attachments = parse_array(data["content_attachments"], "RichTextAttachment")
|
|
4054
|
+
@content_type = data["content_type"]
|
|
4047
4055
|
@created_at = parse_datetime(data["created_at"])
|
|
4048
4056
|
@creator = parse_type(data["creator"], "Person")
|
|
4049
4057
|
@description_attachments = parse_array(data["description_attachments"], "RichTextAttachment")
|
|
4058
|
+
@download_url = data["download_url"]
|
|
4059
|
+
@filename = data["filename"]
|
|
4060
|
+
@height = parse_integer(data["height"])
|
|
4061
|
+
@id = parse_integer(data["id"])
|
|
4062
|
+
@image_url = data["image_url"]
|
|
4050
4063
|
@inherits_status = parse_boolean(data["inherits_status"])
|
|
4064
|
+
@language = data["language"]
|
|
4065
|
+
@on_hold = parse_type(data["on_hold"], "CardColumnOnHold")
|
|
4051
4066
|
@parent = parse_type(data["parent"], "RecordingParent")
|
|
4052
4067
|
@plain_text_content = data["plain_text_content"]
|
|
4053
4068
|
@plain_text_description = data["plain_text_description"]
|
|
4069
|
+
@position = parse_integer(data["position"])
|
|
4070
|
+
@preview_url = data["preview_url"]
|
|
4071
|
+
@previewable = parse_boolean(data["previewable"])
|
|
4072
|
+
@sound_url = data["sound_url"]
|
|
4054
4073
|
@status = data["status"]
|
|
4055
4074
|
@subject = data["subject"]
|
|
4075
|
+
@subscribers = parse_array(data["subscribers"], "Person")
|
|
4076
|
+
@subscription_url = data["subscription_url"]
|
|
4077
|
+
@thumbnail_url = data["thumbnail_url"]
|
|
4078
|
+
@title = data["title"]
|
|
4079
|
+
@type = data["type"]
|
|
4056
4080
|
@updated_at = parse_datetime(data["updated_at"])
|
|
4081
|
+
@url = data["url"]
|
|
4057
4082
|
@visible_to_clients = parse_boolean(data["visible_to_clients"])
|
|
4083
|
+
@width = parse_integer(data["width"])
|
|
4058
4084
|
end
|
|
4059
4085
|
|
|
4060
4086
|
def to_h
|
|
4061
4087
|
{
|
|
4062
|
-
"app_url" => @app_url,
|
|
4063
4088
|
"content" => @content,
|
|
4064
4089
|
"description" => @description,
|
|
4065
|
-
"
|
|
4066
|
-
"
|
|
4067
|
-
"
|
|
4068
|
-
"url" => @url,
|
|
4090
|
+
"app_download_url" => @app_download_url,
|
|
4091
|
+
"app_url" => @app_url,
|
|
4092
|
+
"attachments" => @attachments,
|
|
4069
4093
|
"bookmark_url" => @bookmark_url,
|
|
4094
|
+
"boosts_count" => @boosts_count,
|
|
4095
|
+
"boosts_url" => @boosts_url,
|
|
4070
4096
|
"bubble_up_url" => @bubble_up_url,
|
|
4071
4097
|
"bucket" => @bucket,
|
|
4098
|
+
"byte_size" => @byte_size,
|
|
4099
|
+
"cards_count" => @cards_count,
|
|
4100
|
+
"cards_url" => @cards_url,
|
|
4101
|
+
"color" => @color,
|
|
4102
|
+
"comment_count" => @comment_count,
|
|
4103
|
+
"comments_count" => @comments_count,
|
|
4104
|
+
"comments_url" => @comments_url,
|
|
4072
4105
|
"content_attachments" => @content_attachments,
|
|
4106
|
+
"content_type" => @content_type,
|
|
4073
4107
|
"created_at" => @created_at,
|
|
4074
4108
|
"creator" => @creator,
|
|
4075
4109
|
"description_attachments" => @description_attachments,
|
|
4110
|
+
"download_url" => @download_url,
|
|
4111
|
+
"filename" => @filename,
|
|
4112
|
+
"height" => @height,
|
|
4113
|
+
"id" => @id,
|
|
4114
|
+
"image_url" => @image_url,
|
|
4076
4115
|
"inherits_status" => @inherits_status,
|
|
4116
|
+
"language" => @language,
|
|
4117
|
+
"on_hold" => @on_hold,
|
|
4077
4118
|
"parent" => @parent,
|
|
4078
4119
|
"plain_text_content" => @plain_text_content,
|
|
4079
4120
|
"plain_text_description" => @plain_text_description,
|
|
4121
|
+
"position" => @position,
|
|
4122
|
+
"preview_url" => @preview_url,
|
|
4123
|
+
"previewable" => @previewable,
|
|
4124
|
+
"sound_url" => @sound_url,
|
|
4080
4125
|
"status" => @status,
|
|
4081
4126
|
"subject" => @subject,
|
|
4127
|
+
"subscribers" => @subscribers,
|
|
4128
|
+
"subscription_url" => @subscription_url,
|
|
4129
|
+
"thumbnail_url" => @thumbnail_url,
|
|
4130
|
+
"title" => @title,
|
|
4131
|
+
"type" => @type,
|
|
4082
4132
|
"updated_at" => @updated_at,
|
|
4133
|
+
"url" => @url,
|
|
4083
4134
|
"visible_to_clients" => @visible_to_clients,
|
|
4135
|
+
"width" => @width,
|
|
4084
4136
|
}.reject { |k, v| v.nil? && !["content", "description"].include?(k) }
|
|
4085
4137
|
end
|
|
4086
4138
|
|
|
@@ -4089,6 +4141,55 @@ module Basecamp
|
|
|
4089
4141
|
end
|
|
4090
4142
|
end
|
|
4091
4143
|
|
|
4144
|
+
# SearchResultAttachment
|
|
4145
|
+
class SearchResultAttachment
|
|
4146
|
+
include TypeHelpers
|
|
4147
|
+
attr_accessor :byte_size, :content_type, :download_url, :filename, :height, :id, :preview_url, :previewable, :sgid, :thumbnail_url, :title, :url, :width
|
|
4148
|
+
|
|
4149
|
+
# @return [Array<Symbol>]
|
|
4150
|
+
def self.required_fields
|
|
4151
|
+
%i[byte_size content_type download_url filename].freeze
|
|
4152
|
+
end
|
|
4153
|
+
|
|
4154
|
+
def initialize(data = {})
|
|
4155
|
+
@byte_size = parse_integer(data["byte_size"])
|
|
4156
|
+
@content_type = data["content_type"]
|
|
4157
|
+
@download_url = data["download_url"]
|
|
4158
|
+
@filename = data["filename"]
|
|
4159
|
+
@height = parse_integer(data["height"])
|
|
4160
|
+
@id = parse_integer(data["id"])
|
|
4161
|
+
@preview_url = data["preview_url"]
|
|
4162
|
+
@previewable = parse_boolean(data["previewable"])
|
|
4163
|
+
@sgid = data["sgid"]
|
|
4164
|
+
@thumbnail_url = data["thumbnail_url"]
|
|
4165
|
+
@title = data["title"]
|
|
4166
|
+
@url = data["url"]
|
|
4167
|
+
@width = parse_integer(data["width"])
|
|
4168
|
+
end
|
|
4169
|
+
|
|
4170
|
+
def to_h
|
|
4171
|
+
{
|
|
4172
|
+
"byte_size" => @byte_size,
|
|
4173
|
+
"content_type" => @content_type,
|
|
4174
|
+
"download_url" => @download_url,
|
|
4175
|
+
"filename" => @filename,
|
|
4176
|
+
"height" => @height,
|
|
4177
|
+
"id" => @id,
|
|
4178
|
+
"preview_url" => @preview_url,
|
|
4179
|
+
"previewable" => @previewable,
|
|
4180
|
+
"sgid" => @sgid,
|
|
4181
|
+
"thumbnail_url" => @thumbnail_url,
|
|
4182
|
+
"title" => @title,
|
|
4183
|
+
"url" => @url,
|
|
4184
|
+
"width" => @width,
|
|
4185
|
+
}.compact
|
|
4186
|
+
end
|
|
4187
|
+
|
|
4188
|
+
def to_json(*args)
|
|
4189
|
+
to_h.to_json(*args)
|
|
4190
|
+
end
|
|
4191
|
+
end
|
|
4192
|
+
|
|
4092
4193
|
# SearchType
|
|
4093
4194
|
class SearchType
|
|
4094
4195
|
include TypeHelpers
|
|
@@ -4722,39 +4823,53 @@ module Basecamp
|
|
|
4722
4823
|
# Tool
|
|
4723
4824
|
class Tool
|
|
4724
4825
|
include TypeHelpers
|
|
4725
|
-
attr_accessor :created_at, :
|
|
4826
|
+
attr_accessor :created_at, :creator, :id, :inherits_status, :title, :type, :updated_at, :visible_to_clients, :app_url, :bookmark_url, :bucket, :enabled, :name, :parent, :position, :status, :subscription_url, :url
|
|
4726
4827
|
|
|
4727
4828
|
# @return [Array<Symbol>]
|
|
4728
4829
|
def self.required_fields
|
|
4729
|
-
%i[created_at
|
|
4830
|
+
%i[created_at creator id inherits_status title type updated_at visible_to_clients].freeze
|
|
4730
4831
|
end
|
|
4731
4832
|
|
|
4732
4833
|
def initialize(data = {})
|
|
4733
4834
|
@created_at = parse_datetime(data["created_at"])
|
|
4734
|
-
@
|
|
4835
|
+
@creator = parse_type(data["creator"], "Person")
|
|
4735
4836
|
@id = parse_integer(data["id"])
|
|
4736
|
-
@
|
|
4837
|
+
@inherits_status = parse_boolean(data["inherits_status"])
|
|
4737
4838
|
@title = data["title"]
|
|
4839
|
+
@type = data["type"]
|
|
4738
4840
|
@updated_at = parse_datetime(data["updated_at"])
|
|
4841
|
+
@visible_to_clients = parse_boolean(data["visible_to_clients"])
|
|
4739
4842
|
@app_url = data["app_url"]
|
|
4843
|
+
@bookmark_url = data["bookmark_url"]
|
|
4740
4844
|
@bucket = parse_type(data["bucket"], "RecordingBucket")
|
|
4845
|
+
@enabled = parse_boolean(data["enabled"])
|
|
4846
|
+
@name = data["name"]
|
|
4847
|
+
@parent = parse_type(data["parent"], "RecordingParent")
|
|
4741
4848
|
@position = parse_integer(data["position"])
|
|
4742
4849
|
@status = data["status"]
|
|
4850
|
+
@subscription_url = data["subscription_url"]
|
|
4743
4851
|
@url = data["url"]
|
|
4744
4852
|
end
|
|
4745
4853
|
|
|
4746
4854
|
def to_h
|
|
4747
4855
|
{
|
|
4748
4856
|
"created_at" => @created_at,
|
|
4749
|
-
"
|
|
4857
|
+
"creator" => @creator,
|
|
4750
4858
|
"id" => @id,
|
|
4751
|
-
"
|
|
4859
|
+
"inherits_status" => @inherits_status,
|
|
4752
4860
|
"title" => @title,
|
|
4861
|
+
"type" => @type,
|
|
4753
4862
|
"updated_at" => @updated_at,
|
|
4863
|
+
"visible_to_clients" => @visible_to_clients,
|
|
4754
4864
|
"app_url" => @app_url,
|
|
4865
|
+
"bookmark_url" => @bookmark_url,
|
|
4755
4866
|
"bucket" => @bucket,
|
|
4867
|
+
"enabled" => @enabled,
|
|
4868
|
+
"name" => @name,
|
|
4869
|
+
"parent" => @parent,
|
|
4756
4870
|
"position" => @position,
|
|
4757
4871
|
"status" => @status,
|
|
4872
|
+
"subscription_url" => @subscription_url,
|
|
4758
4873
|
"url" => @url,
|
|
4759
4874
|
}.compact
|
|
4760
4875
|
end
|
|
@@ -5063,6 +5178,82 @@ module Basecamp
|
|
|
5063
5178
|
end
|
|
5064
5179
|
end
|
|
5065
5180
|
|
|
5181
|
+
# UploadVersion
|
|
5182
|
+
class UploadVersion
|
|
5183
|
+
include TypeHelpers
|
|
5184
|
+
attr_accessor :action, :created_at, :creator, :id, :recording_id, :boosts_count, :boosts_url, :details, :upload
|
|
5185
|
+
|
|
5186
|
+
# @return [Array<Symbol>]
|
|
5187
|
+
def self.required_fields
|
|
5188
|
+
%i[action created_at creator id recording_id].freeze
|
|
5189
|
+
end
|
|
5190
|
+
|
|
5191
|
+
def initialize(data = {})
|
|
5192
|
+
@action = data["action"]
|
|
5193
|
+
@created_at = parse_datetime(data["created_at"])
|
|
5194
|
+
@creator = parse_type(data["creator"], "Person")
|
|
5195
|
+
@id = parse_integer(data["id"])
|
|
5196
|
+
@recording_id = parse_integer(data["recording_id"])
|
|
5197
|
+
@boosts_count = parse_integer(data["boosts_count"])
|
|
5198
|
+
@boosts_url = data["boosts_url"]
|
|
5199
|
+
@details = parse_type(data["details"], "EventDetails")
|
|
5200
|
+
@upload = parse_type(data["upload"], "UploadVersionFile")
|
|
5201
|
+
end
|
|
5202
|
+
|
|
5203
|
+
def to_h
|
|
5204
|
+
{
|
|
5205
|
+
"action" => @action,
|
|
5206
|
+
"created_at" => @created_at,
|
|
5207
|
+
"creator" => @creator,
|
|
5208
|
+
"id" => @id,
|
|
5209
|
+
"recording_id" => @recording_id,
|
|
5210
|
+
"boosts_count" => @boosts_count,
|
|
5211
|
+
"boosts_url" => @boosts_url,
|
|
5212
|
+
"details" => @details,
|
|
5213
|
+
"upload" => @upload,
|
|
5214
|
+
}.compact
|
|
5215
|
+
end
|
|
5216
|
+
|
|
5217
|
+
def to_json(*args)
|
|
5218
|
+
to_h.to_json(*args)
|
|
5219
|
+
end
|
|
5220
|
+
end
|
|
5221
|
+
|
|
5222
|
+
# UploadVersionFile
|
|
5223
|
+
class UploadVersionFile
|
|
5224
|
+
include TypeHelpers
|
|
5225
|
+
attr_accessor :app_download_url, :current, :download_url, :filename, :byte_size, :content_type
|
|
5226
|
+
|
|
5227
|
+
# @return [Array<Symbol>]
|
|
5228
|
+
def self.required_fields
|
|
5229
|
+
%i[app_download_url current download_url filename].freeze
|
|
5230
|
+
end
|
|
5231
|
+
|
|
5232
|
+
def initialize(data = {})
|
|
5233
|
+
@app_download_url = data["app_download_url"]
|
|
5234
|
+
@current = parse_boolean(data["current"])
|
|
5235
|
+
@download_url = data["download_url"]
|
|
5236
|
+
@filename = data["filename"]
|
|
5237
|
+
@byte_size = parse_integer(data["byte_size"])
|
|
5238
|
+
@content_type = data["content_type"]
|
|
5239
|
+
end
|
|
5240
|
+
|
|
5241
|
+
def to_h
|
|
5242
|
+
{
|
|
5243
|
+
"app_download_url" => @app_download_url,
|
|
5244
|
+
"current" => @current,
|
|
5245
|
+
"download_url" => @download_url,
|
|
5246
|
+
"filename" => @filename,
|
|
5247
|
+
"byte_size" => @byte_size,
|
|
5248
|
+
"content_type" => @content_type,
|
|
5249
|
+
}.compact
|
|
5250
|
+
end
|
|
5251
|
+
|
|
5252
|
+
def to_json(*args)
|
|
5253
|
+
to_h.to_json(*args)
|
|
5254
|
+
end
|
|
5255
|
+
end
|
|
5256
|
+
|
|
5066
5257
|
# Vault
|
|
5067
5258
|
class Vault
|
|
5068
5259
|
include TypeHelpers
|
data/lib/basecamp/http.rb
CHANGED
|
@@ -342,7 +342,16 @@ module Basecamp
|
|
|
342
342
|
Http.normalize_person_ids(data)
|
|
343
343
|
data
|
|
344
344
|
rescue JSON::ParserError => e
|
|
345
|
-
|
|
345
|
+
# +cause+ carries the parser's own error, not just its message (#750). The
|
|
346
|
+
# message says what happened; the slot is what a caller can act on, and it
|
|
347
|
+
# is the same answer Go reaches through errors.As and Kotlin and Swift
|
|
348
|
+
# through their decodeFailure slot. Passed explicitly rather than left to
|
|
349
|
+
# Ruby's implicit +$!+ chaining because Basecamp::Error defines its own
|
|
350
|
+
# +cause+ reader.
|
|
351
|
+
raise Basecamp::ApiError.new(
|
|
352
|
+
"Failed to parse paginated response (page #{page}): #{Security.truncate(e.message)}",
|
|
353
|
+
cause: e
|
|
354
|
+
)
|
|
346
355
|
end
|
|
347
356
|
|
|
348
357
|
# Extracts the item array from a parsed page body: the body itself for
|
|
@@ -493,8 +502,10 @@ module Basecamp
|
|
|
493
502
|
# Memoized per-operation retry metadata, keyed by canonical operation ID.
|
|
494
503
|
# Benign-race memoization: concurrent first loads compute identical values.
|
|
495
504
|
def self.operation_retry(operation)
|
|
505
|
+
# UTF-8 regardless of process locale — JSON is UTF-8 (RFC 8259), and
|
|
506
|
+
# LC_ALL=C would otherwise read as US-ASCII.
|
|
496
507
|
@operation_metadata ||= JSON.parse(
|
|
497
|
-
File.read(File.join(__dir__, "generated", "metadata.json"))
|
|
508
|
+
File.read(File.join(__dir__, "generated", "metadata.json"), encoding: "UTF-8")
|
|
498
509
|
).fetch("operations").freeze
|
|
499
510
|
@operation_metadata.dig(operation, "retry")
|
|
500
511
|
end
|
|
@@ -632,6 +643,12 @@ module Basecamp
|
|
|
632
643
|
Basecamp.compose_validation_message(Basecamp.parse_error_message(body), field_errors) || "Validation failed"
|
|
633
644
|
)
|
|
634
645
|
Basecamp::ValidationError.new(message, http_status: status, field_errors: field_errors)
|
|
646
|
+
when 507
|
|
647
|
+
# A 5xx status carrying a client fact: the account is out of storage, or
|
|
648
|
+
# at its webhook ceiling. Retrying cannot satisfy it, so this is decided
|
|
649
|
+
# before the 5xx arms below.
|
|
650
|
+
message = Security.truncate(Basecamp.parse_error_message(body) || "Account limit reached")
|
|
651
|
+
Basecamp::LimitExceededError.new(message)
|
|
635
652
|
when 500
|
|
636
653
|
Basecamp::ApiError.new("Server error (500)", http_status: 500, retryable: true)
|
|
637
654
|
when 502, 503, 504
|
|
@@ -779,6 +796,32 @@ module Basecamp
|
|
|
779
796
|
end
|
|
780
797
|
|
|
781
798
|
def parse_next_link(link_header)
|
|
799
|
+
# A non-ASCII-compatible tag (UTF-16/32; only a stub or custom adapter
|
|
800
|
+
# produces one — Faraday's Net::HTTP adapter always yields ASCII-8BIT)
|
|
801
|
+
# defeats the byte scan below: "<" is 3C 00 in UTF-16LE, so the ASCII
|
|
802
|
+
# literals never match and a genuinely UTF-16LE header parsed to nil
|
|
803
|
+
# silently, where the pre-#678 character path at least crashed loudly.
|
|
804
|
+
# Transcoding to UTF-8 up front restores parsing for that case, and
|
|
805
|
+
# rebinding the local means the retag at the bottom hands back UTF-8.
|
|
806
|
+
#
|
|
807
|
+
# ASCII bytes MIStagged UTF-16LE split by bytesize parity, and both
|
|
808
|
+
# outcomes are deliberate:
|
|
809
|
+
# - odd (the natural case): invalid UTF-16LE, encode raises, and the
|
|
810
|
+
# fallthrough keeps today's refusal — the scan extracts the URL
|
|
811
|
+
# still mistagged, and Security.same_origin? refuses the follow
|
|
812
|
+
# (ApiError), so no origin-check bypass opens up.
|
|
813
|
+
# - even: valid UTF-16LE by construction, transcodes to CJK mojibake
|
|
814
|
+
# with no rel="next", parses to nil (was: the ApiError above).
|
|
815
|
+
# Garbage-tagged garbage; nil is as good a refusal as a raise.
|
|
816
|
+
unless link_header.nil? || link_header.encoding.ascii_compatible?
|
|
817
|
+
begin
|
|
818
|
+
link_header = link_header.encode(Encoding::UTF_8)
|
|
819
|
+
rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError,
|
|
820
|
+
Encoding::ConverterNotFoundError
|
|
821
|
+
# Not decodable under its claimed encoding — scan the raw bytes.
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
|
|
782
825
|
next_url = nil
|
|
783
826
|
|
|
784
827
|
unless link_header.nil? || link_header.empty?
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
# Raised when an account limit blocks the request (HTTP 507) — file storage
|
|
5
|
+
# exhausted, or a webhook ceiling reached.
|
|
6
|
+
#
|
|
7
|
+
# Never retryable: no amount of backoff frees storage or raises a plan limit.
|
|
8
|
+
# That is the whole reason this is not an ApiError, which a 507 would
|
|
9
|
+
# otherwise become through the 5xx catch-all.
|
|
10
|
+
class LimitExceededError < Error
|
|
11
|
+
def initialize(message = "Account limit reached", hint: nil, cause: nil)
|
|
12
|
+
super(
|
|
13
|
+
code: ErrorCode::LIMIT_EXCEEDED,
|
|
14
|
+
message: message,
|
|
15
|
+
hint: hint,
|
|
16
|
+
http_status: 507,
|
|
17
|
+
retryable: false,
|
|
18
|
+
cause: cause
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/basecamp/security.rb
CHANGED
|
@@ -39,13 +39,19 @@ module Basecamp
|
|
|
39
39
|
|
|
40
40
|
ua.scheme.downcase == ub.scheme.downcase &&
|
|
41
41
|
normalize_host(ua) == normalize_host(ub)
|
|
42
|
-
rescue URI::
|
|
42
|
+
rescue URI::Error
|
|
43
|
+
# URI::Error, not just InvalidURIError: URI.parse("mailto:") raises
|
|
44
|
+
# URI::InvalidComponentError (URI::MailTo demands an opaque part), and a
|
|
45
|
+
# scheme-only URL must be refused, not a crash.
|
|
43
46
|
false
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
def self.resolve_url(base, target)
|
|
47
50
|
URI.join(base, target).to_s
|
|
48
|
-
rescue URI::
|
|
51
|
+
rescue URI::Error
|
|
52
|
+
# URI::Error, not just InvalidURIError: URI.join raises
|
|
53
|
+
# URI::InvalidComponentError on a scheme-only target ("mailto:"),
|
|
54
|
+
# reachable from a poisoned Link header or Location redirect.
|
|
49
55
|
target
|
|
50
56
|
end
|
|
51
57
|
|
|
@@ -5,11 +5,22 @@ module Basecamp
|
|
|
5
5
|
# Service for authorization operations.
|
|
6
6
|
# This is the only service that doesn't require an account context.
|
|
7
7
|
#
|
|
8
|
+
# The document's shape depends on which issuer served it. Discovery selects a
|
|
9
|
+
# BC5 issuer whenever one is advertised, and a BC5 issuer serves its *own*
|
|
10
|
+
# document (+app/views/api/authorizations/show.json.jbuilder+), which is not
|
|
11
|
+
# Launchpad's: it carries +identity.id+ and nothing else of the identity, no
|
|
12
|
+
# +product+ or +app_href+ on accounts, an RFC 8707 +resource+ indicator
|
|
13
|
+
# instead, and a top-level +scope+ for BC3-issued tokens. Only +identity.id+,
|
|
14
|
+
# +accounts[].id+, +accounts[].name+, +accounts[].href+ and +expires_at+ are
|
|
15
|
+
# common to both; +expires_at+ is an ISO-8601 string from either issuer
|
|
16
|
+
# (integer epoch seconds from bc3 before bc3 #12646 — passed through
|
|
17
|
+
# verbatim either way).
|
|
18
|
+
#
|
|
8
19
|
# @example Get authorization info
|
|
9
20
|
# auth = client.authorization.get
|
|
10
|
-
# puts "Identity: #{auth["identity"]["
|
|
21
|
+
# puts "Identity: #{auth["identity"]["id"]}"
|
|
11
22
|
# auth["accounts"].each do |account|
|
|
12
|
-
# puts "Account: #{account["name"]} (#{account["
|
|
23
|
+
# puts "Account: #{account["name"]} (#{account["href"]})"
|
|
13
24
|
# end
|
|
14
25
|
class AuthorizationService < BaseService
|
|
15
26
|
# Gets authorization information for the current user.
|
data/lib/basecamp/version.rb
CHANGED
data/lib/basecamp.rb
CHANGED
|
@@ -132,6 +132,10 @@ module Basecamp
|
|
|
132
132
|
NotFoundError.new(message: message)
|
|
133
133
|
when 429
|
|
134
134
|
RateLimitError.new(retry_after: retry_after)
|
|
135
|
+
when 507
|
|
136
|
+
# Decided before the 5xx arms: a 507 is an account limit, not a
|
|
137
|
+
# transient server failure, and no retry can satisfy it.
|
|
138
|
+
LimitExceededError.new(Security.truncate(message))
|
|
135
139
|
when 500
|
|
136
140
|
ApiError.new("Server error (500)", http_status: 500, retryable: true)
|
|
137
141
|
when 502, 503, 504
|
|
@@ -14,7 +14,9 @@ class MetadataExtractor
|
|
|
14
14
|
METHODS = %w[get post put patch delete].freeze
|
|
15
15
|
|
|
16
16
|
def initialize(openapi_path)
|
|
17
|
-
|
|
17
|
+
# Read as UTF-8 regardless of process locale (LC_ALL=C would otherwise read
|
|
18
|
+
# as US-ASCII and JSON.parse dies on the spec's multibyte characters)
|
|
19
|
+
@openapi = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def extract
|
|
@@ -65,7 +65,7 @@ class ServiceGenerator
|
|
|
65
65
|
},
|
|
66
66
|
'Files' => {
|
|
67
67
|
'Attachments' => %w[CreateAttachment],
|
|
68
|
-
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions],
|
|
68
|
+
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions CreateUploadVersion],
|
|
69
69
|
'Vaults' => %w[GetVault UpdateVault ListVaults CreateVault],
|
|
70
70
|
'Documents' => %w[GetDocument ReplaceDocument ListDocuments CreateDocument],
|
|
71
71
|
'CloudFiles' => %w[GetCloudFile CreateCloudFile UpdateCloudFile],
|
|
@@ -217,6 +217,7 @@ class ServiceGenerator
|
|
|
217
217
|
'ListUploads' => 'list',
|
|
218
218
|
'CreateUpload' => 'create',
|
|
219
219
|
'ListUploadVersions' => 'list_versions',
|
|
220
|
+
'CreateUploadVersion' => 'create_version',
|
|
220
221
|
'GetMessage' => 'get',
|
|
221
222
|
'UpdateMessage' => 'update',
|
|
222
223
|
'CreateMessage' => 'create',
|
|
@@ -320,7 +321,8 @@ class ServiceGenerator
|
|
|
320
321
|
].freeze
|
|
321
322
|
|
|
322
323
|
def initialize(openapi_path)
|
|
323
|
-
|
|
324
|
+
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
325
|
+
@openapi = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))
|
|
324
326
|
@schemas = @openapi.dig('components', 'schemas') || {}
|
|
325
327
|
end
|
|
326
328
|
|
data/scripts/generate-types.rb
CHANGED
|
@@ -131,7 +131,8 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
131
131
|
puts ' module Types'
|
|
132
132
|
puts ' include TypeHelpers'
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
# UTF-8 regardless of process locale — see generate-metadata.rb
|
|
135
|
+
schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
|
|
135
136
|
sorted = schemas.keys.sort
|
|
136
137
|
|
|
137
138
|
sorted.each do |name|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: basecamp-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Basecamp
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -235,6 +235,7 @@ files:
|
|
|
235
235
|
- lib/basecamp/generated/types.rb
|
|
236
236
|
- lib/basecamp/hooks.rb
|
|
237
237
|
- lib/basecamp/http.rb
|
|
238
|
+
- lib/basecamp/limit_exceeded_error.rb
|
|
238
239
|
- lib/basecamp/list_enumerator.rb
|
|
239
240
|
- lib/basecamp/list_meta.rb
|
|
240
241
|
- lib/basecamp/logger_hooks.rb
|