basecamp-sdk 0.9.0 → 0.11.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 +106 -10
- data/lib/basecamp/client.rb +13 -8
- data/lib/basecamp/config.rb +4 -3
- data/lib/basecamp/generated/metadata.json +280 -12
- data/lib/basecamp/generated/services/account_service.rb +1 -1
- data/lib/basecamp/generated/services/automation_service.rb +2 -2
- data/lib/basecamp/generated/services/boosts_service.rb +3 -3
- data/lib/basecamp/generated/services/campfires_service.rb +7 -7
- data/lib/basecamp/generated/services/card_columns_service.rb +1 -1
- data/lib/basecamp/generated/services/card_steps_service.rb +1 -1
- data/lib/basecamp/generated/services/card_tables_service.rb +1 -1
- data/lib/basecamp/generated/services/cards_service.rb +4 -4
- data/lib/basecamp/generated/services/checkins_service.rb +8 -8
- data/lib/basecamp/generated/services/client_approvals_service.rb +2 -2
- data/lib/basecamp/generated/services/client_correspondences_service.rb +2 -2
- data/lib/basecamp/generated/services/client_replies_service.rb +2 -2
- data/lib/basecamp/generated/services/comments_service.rb +2 -2
- data/lib/basecamp/generated/services/documents_service.rb +2 -2
- data/lib/basecamp/generated/services/events_service.rb +1 -1
- data/lib/basecamp/generated/services/everything_service.rb +169 -0
- data/lib/basecamp/generated/services/forwards_service.rb +5 -5
- data/lib/basecamp/generated/services/gauges_service.rb +3 -3
- data/lib/basecamp/generated/services/hill_charts_service.rb +1 -1
- data/lib/basecamp/generated/services/message_boards_service.rb +1 -1
- data/lib/basecamp/generated/services/message_types_service.rb +20 -15
- data/lib/basecamp/generated/services/messages_service.rb +2 -2
- data/lib/basecamp/generated/services/my_assignments_service.rb +5 -5
- data/lib/basecamp/generated/services/my_notifications_service.rb +16 -2
- data/lib/basecamp/generated/services/people_service.rb +9 -9
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +3 -3
- data/lib/basecamp/generated/services/reports_service.rb +5 -5
- data/lib/basecamp/generated/services/schedules_service.rb +13 -5
- data/lib/basecamp/generated/services/search_service.rb +2 -2
- data/lib/basecamp/generated/services/subscriptions_service.rb +1 -1
- data/lib/basecamp/generated/services/templates_service.rb +3 -3
- data/lib/basecamp/generated/services/timeline_service.rb +1 -1
- data/lib/basecamp/generated/services/timesheets_service.rb +5 -5
- data/lib/basecamp/generated/services/todolist_groups_service.rb +1 -1
- data/lib/basecamp/generated/services/todolists_service.rb +2 -2
- data/lib/basecamp/generated/services/todos_service.rb +2 -2
- data/lib/basecamp/generated/services/todosets_service.rb +1 -1
- data/lib/basecamp/generated/services/tools_service.rb +4 -3
- data/lib/basecamp/generated/services/uploads_service.rb +3 -3
- data/lib/basecamp/generated/services/vaults_service.rb +2 -2
- data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
- data/lib/basecamp/generated/types.rb +374 -18
- data/lib/basecamp/http.rb +47 -18
- data/lib/basecamp/oauth/device_authorization.rb +37 -0
- data/lib/basecamp/oauth/device_flow.rb +873 -0
- data/lib/basecamp/oauth/device_flow_error.rb +46 -0
- data/lib/basecamp/oauth/discovery.rb +13 -11
- data/lib/basecamp/oauth/exchange.rb +44 -5
- data/lib/basecamp/oauth/fetcher.rb +626 -56
- data/lib/basecamp/oauth/oauth_error.rb +3 -1
- data/lib/basecamp/oauth/refresh_request.rb +8 -2
- data/lib/basecamp/oauth/resource.rb +13 -10
- data/lib/basecamp/oauth/token.rb +10 -3
- data/lib/basecamp/oauth.rb +47 -2
- data/lib/basecamp/services/cards_extensions.rb +67 -0
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +5 -0
- data/scripts/generate-services.rb +24 -6
- data/scripts/generate-types.rb +29 -2
- metadata +7 -2
|
@@ -4,7 +4,9 @@ module Basecamp
|
|
|
4
4
|
module Oauth
|
|
5
5
|
# OAuth-specific error class.
|
|
6
6
|
#
|
|
7
|
-
# @attr type [String] Error type ("validation", "auth", "network",
|
|
7
|
+
# @attr type [String] Error type ("validation", "auth", "network",
|
|
8
|
+
# "api_error", or "usage" — the parent type {DeviceFlowError} derives for
|
|
9
|
+
# a cancelled device flow)
|
|
8
10
|
# @attr http_status [Integer, nil] HTTP status code if applicable
|
|
9
11
|
# @attr hint [String, nil] Helpful hint for resolving the error
|
|
10
12
|
# @attr retryable [Boolean] Whether the request can be retried
|
|
@@ -9,19 +9,25 @@ module Basecamp
|
|
|
9
9
|
# @attr client_id [String, nil] The client identifier (optional)
|
|
10
10
|
# @attr client_secret [String, nil] The client secret (optional)
|
|
11
11
|
# @attr use_legacy_format [Boolean] Use Launchpad's non-standard token format
|
|
12
|
+
# @attr resource [String, nil] RFC 8707 resource indicator to bind the
|
|
13
|
+
# refreshed token to, sent only when set. Echo the stored token's
|
|
14
|
+
# resource: BC5 multi-account refresh tokens hard-require it (SPEC §16).
|
|
15
|
+
# Appended last so earlier members keep their positions.
|
|
12
16
|
RefreshRequest = Data.define(
|
|
13
17
|
:token_endpoint,
|
|
14
18
|
:refresh_token,
|
|
15
19
|
:client_id,
|
|
16
20
|
:client_secret,
|
|
17
|
-
:use_legacy_format
|
|
21
|
+
:use_legacy_format,
|
|
22
|
+
:resource
|
|
18
23
|
) do
|
|
19
24
|
def initialize(
|
|
20
25
|
token_endpoint:,
|
|
21
26
|
refresh_token:,
|
|
22
27
|
client_id: nil,
|
|
23
28
|
client_secret: nil,
|
|
24
|
-
use_legacy_format: false
|
|
29
|
+
use_legacy_format: false,
|
|
30
|
+
resource: nil
|
|
25
31
|
)
|
|
26
32
|
super
|
|
27
33
|
end
|
|
@@ -5,25 +5,28 @@ module Basecamp
|
|
|
5
5
|
# Fetches RFC 9728 protected-resource metadata (hop 1 of resource-first
|
|
6
6
|
# discovery) and binds the returned +resource+ to the requested origin.
|
|
7
7
|
class Resource
|
|
8
|
-
# @param http_client [Faraday::Connection, nil]
|
|
9
|
-
#
|
|
8
|
+
# @param http_client [Faraday::Connection, nil] injected Faraday client
|
|
9
|
+
# (kept, verified redirect-free); nil selects the default headers-first
|
|
10
|
+
# Net::HTTP transport ({Fetcher.stream_http})
|
|
11
|
+
# @param timeout [Numeric] request timeout in seconds (default: 10); an
|
|
12
|
+
# invalid value falls back via {Fetcher.normalize_timeout}
|
|
10
13
|
# @param max_body_bytes [Integer] bounded read cap in bytes
|
|
11
14
|
def initialize(http_client: nil, timeout: 10, max_body_bytes: Fetcher::DEFAULT_MAX_BODY_BYTES)
|
|
12
15
|
Fetcher.ensure_redirects_suppressed!(http_client) if http_client
|
|
13
|
-
# Normalize before
|
|
16
|
+
# Normalize before storing (nil selects the headers-first Net::HTTP
|
|
17
|
+
# transport; an injected client is kept) and before the fetch computes its
|
|
14
18
|
# wall-clock deadline: a non-finite/non-positive timeout must not disable
|
|
15
19
|
# either bound (see Fetcher.normalize_timeout).
|
|
16
20
|
@timeout = Fetcher.normalize_timeout(timeout)
|
|
17
|
-
|
|
21
|
+
# nil selects the headers-first {Fetcher.stream_http} transport (total
|
|
22
|
+
# wall-clock bound incl. the header phase); an injected connection keeps
|
|
23
|
+
# the Faraday path, verified redirect-free above.
|
|
24
|
+
@http_client = http_client
|
|
18
25
|
# Normalize the public cap to a finite non-negative Integer: a nil, float,
|
|
19
26
|
# or Float::INFINITY would otherwise disable the streaming memory bound
|
|
20
27
|
# (an infinite/undefined cap never trips), reintroducing an SSRF/OOM risk.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
max_body_bytes
|
|
24
|
-
else
|
|
25
|
-
Fetcher::DEFAULT_MAX_BODY_BYTES
|
|
26
|
-
end
|
|
28
|
+
# Shared with Discovery and the device flow.
|
|
29
|
+
@max_body_bytes = Fetcher.normalize_body_cap(max_body_bytes)
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
# Discovers protected-resource metadata from
|
data/lib/basecamp/oauth/token.rb
CHANGED
|
@@ -10,13 +10,18 @@ module Basecamp
|
|
|
10
10
|
# @attr expires_in [Integer, nil] Lifetime of the access token in seconds
|
|
11
11
|
# @attr expires_at [Time, nil] Calculated expiration time
|
|
12
12
|
# @attr scope [String, nil] OAuth scope granted
|
|
13
|
+
# @attr resource [String, nil] RFC 8707 resource indicator the token is
|
|
14
|
+
# bound to (BC5: +urn:bc:account:<id>+). Echo it when refreshing —
|
|
15
|
+
# BC5 multi-account refresh tokens reject a refresh without it
|
|
16
|
+
# (SPEC §16). Appended last so earlier members keep their positions.
|
|
13
17
|
Token = Data.define(
|
|
14
18
|
:access_token,
|
|
15
19
|
:token_type,
|
|
16
20
|
:refresh_token,
|
|
17
21
|
:expires_in,
|
|
18
22
|
:expires_at,
|
|
19
|
-
:scope
|
|
23
|
+
:scope,
|
|
24
|
+
:resource
|
|
20
25
|
) do
|
|
21
26
|
def initialize(
|
|
22
27
|
access_token:,
|
|
@@ -24,7 +29,8 @@ module Basecamp
|
|
|
24
29
|
refresh_token: nil,
|
|
25
30
|
expires_in: nil,
|
|
26
31
|
expires_at: nil,
|
|
27
|
-
scope: nil
|
|
32
|
+
scope: nil,
|
|
33
|
+
resource: nil
|
|
28
34
|
)
|
|
29
35
|
# Calculate expires_at from expires_in if not provided
|
|
30
36
|
calculated_expires_at = expires_at || (expires_in ? Time.now + expires_in : nil)
|
|
@@ -34,7 +40,8 @@ module Basecamp
|
|
|
34
40
|
refresh_token: refresh_token,
|
|
35
41
|
expires_in: expires_in,
|
|
36
42
|
expires_at: calculated_expires_at,
|
|
37
|
-
scope: scope
|
|
43
|
+
scope: scope,
|
|
44
|
+
resource: resource
|
|
38
45
|
)
|
|
39
46
|
end
|
|
40
47
|
|
data/lib/basecamp/oauth.rb
CHANGED
|
@@ -133,12 +133,12 @@ module Basecamp
|
|
|
133
133
|
def self.refresh_token(
|
|
134
134
|
token_endpoint:, refresh_token:,
|
|
135
135
|
client_id: nil, client_secret: nil,
|
|
136
|
-
use_legacy_format: false, timeout: 30
|
|
136
|
+
use_legacy_format: false, timeout: 30, resource: nil
|
|
137
137
|
)
|
|
138
138
|
request = RefreshRequest.new(
|
|
139
139
|
token_endpoint: token_endpoint, refresh_token: refresh_token,
|
|
140
140
|
client_id: client_id, client_secret: client_secret,
|
|
141
|
-
use_legacy_format: use_legacy_format
|
|
141
|
+
use_legacy_format: use_legacy_format, resource: resource
|
|
142
142
|
)
|
|
143
143
|
Exchange.new(timeout: timeout).refresh(request)
|
|
144
144
|
end
|
|
@@ -147,6 +147,51 @@ module Basecamp
|
|
|
147
147
|
token.expired?(buffer_seconds)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
# Requests an RFC 8628 device/user code pair. Thin delegator to
|
|
151
|
+
# {DeviceFlow.request_device_authorization}.
|
|
152
|
+
#
|
|
153
|
+
# @return [DeviceAuthorization]
|
|
154
|
+
def self.request_device_authorization(device_authorization_endpoint:, client_id:, scope: nil, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT)
|
|
155
|
+
DeviceFlow.request_device_authorization(
|
|
156
|
+
device_authorization_endpoint: device_authorization_endpoint,
|
|
157
|
+
client_id: client_id, scope: scope, timeout: timeout
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Polls the token endpoint for a device grant (RFC 8628 §3.5). Thin delegator
|
|
162
|
+
# to {DeviceFlow.poll_device_token}; +clock+, +sleeper+, and +cancelled+ are
|
|
163
|
+
# injectable for tests.
|
|
164
|
+
#
|
|
165
|
+
# @return [Token]
|
|
166
|
+
def self.poll_device_token(
|
|
167
|
+
token_endpoint:, client_id:, device_code:, interval:, expires_in:,
|
|
168
|
+
clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER,
|
|
169
|
+
cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT,
|
|
170
|
+
deadline_at: nil
|
|
171
|
+
)
|
|
172
|
+
DeviceFlow.poll_device_token(
|
|
173
|
+
token_endpoint: token_endpoint, client_id: client_id,
|
|
174
|
+
device_code: device_code, interval: interval, expires_in: expires_in,
|
|
175
|
+
clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout,
|
|
176
|
+
deadline_at: deadline_at
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Runs the full device authorization grant against an already-selected
|
|
181
|
+
# {Config} (RFC 8628). Thin delegator to {DeviceFlow.perform_device_login}.
|
|
182
|
+
#
|
|
183
|
+
# @return [Token]
|
|
184
|
+
def self.perform_device_login(
|
|
185
|
+
config:, client_id:, display:, scope: nil,
|
|
186
|
+
clock: DeviceFlow::DEFAULT_CLOCK, sleeper: DeviceFlow::DEFAULT_SLEEPER,
|
|
187
|
+
cancelled: DeviceFlow::DEFAULT_CANCELLED, timeout: DeviceFlow::DEVICE_REQUEST_TIMEOUT
|
|
188
|
+
)
|
|
189
|
+
DeviceFlow.perform_device_login(
|
|
190
|
+
config: config, client_id: client_id, display: display, scope: scope,
|
|
191
|
+
clock: clock, sleeper: sleeper, cancelled: cancelled, timeout: timeout
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
150
195
|
# Selects an issuer from the advertised set and — once a BC5 issuer is
|
|
151
196
|
# committed — binds its AS metadata. From this point on, every failure is
|
|
152
197
|
# fatal: no Launchpad request may be issued.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Services
|
|
5
|
+
# Merge-safe +update+ for cards, prepended onto the generated
|
|
6
|
+
# {CardsService} (see the +on_load+ hook in +basecamp.rb+).
|
|
7
|
+
#
|
|
8
|
+
# BC3 builds the card's update params as
|
|
9
|
+
# <tt>{ due_on: nil }.merge(card_params)</tt>
|
|
10
|
+
# (+kanban/cards_controller.rb+), so *any* update whose body omits +due_on+
|
|
11
|
+
# erases the card's due date. A sparse PUT — the natural thing to write —
|
|
12
|
+
# is therefore destructive on the raw endpoint, which remains available as
|
|
13
|
+
# {#update_verbatim}.
|
|
14
|
+
#
|
|
15
|
+
# +update+ composes the public +get+ and +update_verbatim+ methods, so
|
|
16
|
+
# hooks observe the two wire operations, not a synthetic composite.
|
|
17
|
+
#
|
|
18
|
+
# Not atomic: a concurrent due-date change landing between the GET and the
|
|
19
|
+
# PUT is overwritten with the value this call read. The window is one
|
|
20
|
+
# round-trip.
|
|
21
|
+
module CardsExtensions
|
|
22
|
+
# Updates a card without disturbing fields the caller did not mention.
|
|
23
|
+
#
|
|
24
|
+
# +due_on+ is tri-state, which is what makes this safe:
|
|
25
|
+
#
|
|
26
|
+
# * +nil+ (omitted) — the current due date is fetched and resent
|
|
27
|
+
# * <tt>""</tt> — the due date is cleared
|
|
28
|
+
# * a date — the due date is set
|
|
29
|
+
#
|
|
30
|
+
# The extra GET is only paid for in the +nil+ case, the one where the
|
|
31
|
+
# API would otherwise destroy something.
|
|
32
|
+
#
|
|
33
|
+
# Assignees are never resent on the caller's behalf: BC3 filters incoming
|
|
34
|
+
# IDs through +reachable_people+, so echoing back an id belonging to
|
|
35
|
+
# someone who has since lost board access would silently unassign them.
|
|
36
|
+
#
|
|
37
|
+
# @param card_id [Integer] card id
|
|
38
|
+
# @param title [String, nil] new title (nil = keep current)
|
|
39
|
+
# @param content [String, nil] new content (nil = keep current, "" clears)
|
|
40
|
+
# @param due_on [String, nil] new due date (nil = keep current, "" clears)
|
|
41
|
+
# @param assignee_ids [Array, nil] new assignees (nil = keep current, [] clears)
|
|
42
|
+
# @return [Hash] the updated card
|
|
43
|
+
def update(card_id:, title: nil, content: nil, due_on: nil, assignee_ids: nil)
|
|
44
|
+
resolved_due_on =
|
|
45
|
+
if due_on.nil?
|
|
46
|
+
get(card_id: card_id)["due_on"]
|
|
47
|
+
elsif due_on.to_s.empty?
|
|
48
|
+
# Clearing is encoded by OMITTING due_on — compact_params strips the
|
|
49
|
+
# nil below, and BC3 nils an omitted due date. Sending an explicit
|
|
50
|
+
# null would violate body compaction (SPEC §18), and sending ""
|
|
51
|
+
# risks a date-format error.
|
|
52
|
+
nil
|
|
53
|
+
else
|
|
54
|
+
due_on
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
update_verbatim(
|
|
58
|
+
card_id: card_id,
|
|
59
|
+
title: title,
|
|
60
|
+
content: content,
|
|
61
|
+
due_on: resolved_due_on,
|
|
62
|
+
assignee_ids: assignee_ids
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/basecamp/version.rb
CHANGED
data/lib/basecamp.rb
CHANGED
|
@@ -10,6 +10,11 @@ loader.collapse("#{__dir__}/basecamp/generated")
|
|
|
10
10
|
loader.on_load("Basecamp::Services::TodosService") do |klass, _abspath|
|
|
11
11
|
klass.prepend(Basecamp::Services::TodosExtensions)
|
|
12
12
|
end
|
|
13
|
+
# Same shape for cards: the generated class owns the constant and the
|
|
14
|
+
# merge-safe update is prepended over the generated update_verbatim.
|
|
15
|
+
loader.on_load("Basecamp::Services::CardsService") do |klass, _abspath|
|
|
16
|
+
klass.prepend(Basecamp::Services::CardsExtensions)
|
|
17
|
+
end
|
|
13
18
|
loader.setup
|
|
14
19
|
|
|
15
20
|
# Load generated types if available
|
|
@@ -146,6 +146,9 @@ class ServiceGenerator
|
|
|
146
146
|
'RepositionCardStep' => 'reposition',
|
|
147
147
|
'CreateCardStep' => 'create',
|
|
148
148
|
'UpdateCardStep' => 'update',
|
|
149
|
+
# The plain `update` name belongs to the merge-safe composite; the raw
|
|
150
|
+
# single-PUT path keeps a name that says what it does. See #467.
|
|
151
|
+
'UpdateCard' => 'update_verbatim',
|
|
149
152
|
'SetCardStepCompletion' => 'set_completion',
|
|
150
153
|
'GetQuestionnaire' => 'get_questionnaire',
|
|
151
154
|
'GetQuestion' => 'get_question',
|
|
@@ -424,6 +427,13 @@ class ServiceGenerator
|
|
|
424
427
|
response_schema = success_response&.dig('content', 'application/json', 'schema')
|
|
425
428
|
returns_void = response_schema.nil?
|
|
426
429
|
returns_array = response_schema&.dig('type') == 'array'
|
|
430
|
+
# Bare-array responses are usually a $ref to a single-member output alias, so
|
|
431
|
+
# the inline `type` is absent. Resolve the ref to detect them — used only for
|
|
432
|
+
# the YARD @return doc (an unpaginated bare array returns an Array, not a
|
|
433
|
+
# Hash); the pagination/body logic deliberately keys off the raw `type` so a
|
|
434
|
+
# bare array without x-basecamp-pagination stays a single request.
|
|
435
|
+
resolved_response = response_schema && response_schema['$ref'] ? resolve_schema_ref(response_schema) : response_schema
|
|
436
|
+
returns_bare_array = resolved_response&.dig('type') == 'array'
|
|
427
437
|
|
|
428
438
|
{
|
|
429
439
|
operation_id: operation_id,
|
|
@@ -440,6 +450,7 @@ class ServiceGenerator
|
|
|
440
450
|
multipart_field: multipart_field,
|
|
441
451
|
returns_void: returns_void,
|
|
442
452
|
returns_array: returns_array,
|
|
453
|
+
returns_bare_array: returns_bare_array,
|
|
443
454
|
is_mutation: http_method != 'GET',
|
|
444
455
|
has_pagination: !!operation['x-basecamp-pagination'],
|
|
445
456
|
pagination_key: operation.dig('x-basecamp-pagination', 'key')
|
|
@@ -658,6 +669,10 @@ class ServiceGenerator
|
|
|
658
669
|
lines << ' # @return [Hash] response data'
|
|
659
670
|
elsif is_paginated
|
|
660
671
|
lines << ' # @return [Enumerator<Hash>] paginated results'
|
|
672
|
+
elsif op[:returns_bare_array]
|
|
673
|
+
# Unpaginated bare array (single request, no Link-following) — e.g. the
|
|
674
|
+
# overdue todo/card feeds. Returns the parsed JSON array, not a Hash.
|
|
675
|
+
lines << ' # @return [Array<Hash>] response data'
|
|
661
676
|
else
|
|
662
677
|
lines << ' # @return [Hash] response data'
|
|
663
678
|
end
|
|
@@ -803,9 +818,9 @@ class ServiceGenerator
|
|
|
803
818
|
if op[:query_params].any?
|
|
804
819
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
805
820
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
806
|
-
lines << " paginate(#{path_expr}, params: params)"
|
|
821
|
+
lines << " paginate(#{path_expr}, params: params, operation: \"#{op[:operation_id]}\")"
|
|
807
822
|
else
|
|
808
|
-
lines << " paginate(#{path_expr})"
|
|
823
|
+
lines << " paginate(#{path_expr}, operation: \"#{op[:operation_id]}\")"
|
|
809
824
|
end
|
|
810
825
|
|
|
811
826
|
lines
|
|
@@ -817,9 +832,9 @@ class ServiceGenerator
|
|
|
817
832
|
if op[:query_params].any?
|
|
818
833
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
819
834
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
820
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params)"
|
|
835
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params, operation: \"#{op[:operation_id]}\")"
|
|
821
836
|
else
|
|
822
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\")"
|
|
837
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", operation: \"#{op[:operation_id]}\")"
|
|
823
838
|
end
|
|
824
839
|
|
|
825
840
|
lines
|
|
@@ -828,6 +843,9 @@ class ServiceGenerator
|
|
|
828
843
|
def generate_get_method_body(op, path_expr)
|
|
829
844
|
lines = []
|
|
830
845
|
http_method = op[:http_method].downcase
|
|
846
|
+
# Only GETs are retry-governed in Ruby, and only Http#get accepts the
|
|
847
|
+
# operation keyword — mutations go through single_request with no retry.
|
|
848
|
+
operation_arg = http_method == 'get' ? ", operation: \"#{op[:operation_id]}\"" : ''
|
|
831
849
|
|
|
832
850
|
if op[:has_binary_body]
|
|
833
851
|
# Binary upload - use raw body and set Content-Type header
|
|
@@ -848,9 +866,9 @@ class ServiceGenerator
|
|
|
848
866
|
lines << " http_#{http_method}(#{path_expr}, body: #{body_expr}).json"
|
|
849
867
|
elsif op[:query_params].any?
|
|
850
868
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
851
|
-
lines << " http_#{http_method}(#{path_expr}, params: compact_query_params(#{param_names.join(', ')})).json"
|
|
869
|
+
lines << " http_#{http_method}(#{path_expr}, params: compact_query_params(#{param_names.join(', ')})#{operation_arg}).json"
|
|
852
870
|
else
|
|
853
|
-
lines << " http_#{http_method}(#{path_expr}).json"
|
|
871
|
+
lines << " http_#{http_method}(#{path_expr}#{operation_arg}).json"
|
|
854
872
|
end
|
|
855
873
|
|
|
856
874
|
lines
|
data/scripts/generate-types.rb
CHANGED
|
@@ -87,6 +87,33 @@ def generate_helpers
|
|
|
87
87
|
HELPERS
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
# Renders a deprecation +reason+ as one or more Ruby comment lines (#406).
|
|
91
|
+
#
|
|
92
|
+
# A reason can be sourced from a multi-line OpenAPI description. Interpolating
|
|
93
|
+
# one into a single "# @deprecated ..." line would leave every continuation on a
|
|
94
|
+
# bare, un-commented source line, which is a syntax error in the generated
|
|
95
|
+
# types.rb — Ruby is the only owned generator exposed this way (Python escapes
|
|
96
|
+
# the reason, Kotlin/TypeScript emit block comments). Splitting on line
|
|
97
|
+
# boundaries and prefixing each line with the comment leader keeps the output
|
|
98
|
+
# valid for any reason.
|
|
99
|
+
#
|
|
100
|
+
# +indent+ is the leading source indentation; +tag_prefix+ is inserted between
|
|
101
|
+
# the "# " leader and the YARD tag (the per-attribute site nests its tag under
|
|
102
|
+
# an @!attribute directive). Continuation lines are indented two columns past
|
|
103
|
+
# the tag so YARD folds them into the same tag's text.
|
|
104
|
+
#
|
|
105
|
+
# Single-line reasons render byte-identically to the previous single-line
|
|
106
|
+
# interpolation, so this is output-neutral for the current spec.
|
|
107
|
+
def deprecation_doc_lines(reason, indent:, tag_prefix: '')
|
|
108
|
+
lines = reason.to_s.split(/\r?\n/, -1)
|
|
109
|
+
out = [ "#{indent}# #{tag_prefix}@deprecated #{lines.first}" ]
|
|
110
|
+
continuation = "#{indent}# #{tag_prefix} "
|
|
111
|
+
lines.drop(1).each do |line|
|
|
112
|
+
out << (line.empty? ? "#{indent}#" : "#{continuation}#{line}")
|
|
113
|
+
end
|
|
114
|
+
out
|
|
115
|
+
end
|
|
116
|
+
|
|
90
117
|
# Main execution
|
|
91
118
|
if __FILE__ == $PROGRAM_NAME
|
|
92
119
|
openapi_path = ARGV[0] || File.expand_path('../../openapi.json', __dir__)
|
|
@@ -128,7 +155,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
128
155
|
# not individual params, so a class-level @deprecated tag documents a wholly
|
|
129
156
|
# deprecated type.
|
|
130
157
|
if schema['deprecated']
|
|
131
|
-
puts
|
|
158
|
+
puts deprecation_doc_lines(schema['x-deprecated-reason'] || 'deprecated', indent: ' ')
|
|
132
159
|
end
|
|
133
160
|
puts " class #{name}"
|
|
134
161
|
puts ' include TypeHelpers'
|
|
@@ -147,7 +174,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
147
174
|
|
|
148
175
|
ruby_name = k.gsub(/([A-Z])/, '_\1').downcase.gsub(/^_/, '')
|
|
149
176
|
puts " # @!attribute [rw] #{ruby_name}"
|
|
150
|
-
puts
|
|
177
|
+
puts deprecation_doc_lines(ps['x-deprecated-reason'] || 'deprecated', indent: ' ', tag_prefix: ' ')
|
|
151
178
|
end
|
|
152
179
|
|
|
153
180
|
puts " attr_accessor #{attr_names.map { |n| ":#{n}" }.join(", ")}"
|
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.11.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-07-
|
|
11
|
+
date: 2026-07-31 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -196,6 +196,7 @@ files:
|
|
|
196
196
|
- lib/basecamp/generated/services/comments_service.rb
|
|
197
197
|
- lib/basecamp/generated/services/documents_service.rb
|
|
198
198
|
- lib/basecamp/generated/services/events_service.rb
|
|
199
|
+
- lib/basecamp/generated/services/everything_service.rb
|
|
199
200
|
- lib/basecamp/generated/services/forwards_service.rb
|
|
200
201
|
- lib/basecamp/generated/services/gauges_service.rb
|
|
201
202
|
- lib/basecamp/generated/services/hill_charts_service.rb
|
|
@@ -233,6 +234,9 @@ files:
|
|
|
233
234
|
- lib/basecamp/not_found_error.rb
|
|
234
235
|
- lib/basecamp/oauth.rb
|
|
235
236
|
- lib/basecamp/oauth/config.rb
|
|
237
|
+
- lib/basecamp/oauth/device_authorization.rb
|
|
238
|
+
- lib/basecamp/oauth/device_flow.rb
|
|
239
|
+
- lib/basecamp/oauth/device_flow_error.rb
|
|
236
240
|
- lib/basecamp/oauth/discovery.rb
|
|
237
241
|
- lib/basecamp/oauth/discovery_result.rb
|
|
238
242
|
- lib/basecamp/oauth/discovery_selection_error.rb
|
|
@@ -253,6 +257,7 @@ files:
|
|
|
253
257
|
- lib/basecamp/request_result.rb
|
|
254
258
|
- lib/basecamp/security.rb
|
|
255
259
|
- lib/basecamp/services/authorization_service.rb
|
|
260
|
+
- lib/basecamp/services/cards_extensions.rb
|
|
256
261
|
- lib/basecamp/services/todos_extensions.rb
|
|
257
262
|
- lib/basecamp/static_token_provider.rb
|
|
258
263
|
- lib/basecamp/token_provider.rb
|