basecamp-sdk 0.3.0 → 0.4.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/lib/basecamp/client.rb +9 -0
- data/lib/basecamp/generated/metadata.json +16 -1
- data/lib/basecamp/generated/services/reports_service.rb +2 -2
- data/lib/basecamp/generated/services/timesheets_service.rb +8 -6
- data/lib/basecamp/generated/types.rb +1 -1
- data/lib/basecamp/http.rb +66 -0
- data/lib/basecamp/services/base_service.rb +42 -0
- data/lib/basecamp/version.rb +1 -1
- data/scripts/generate-services.rb +29 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77ea8d483dab96eeb6721a83f11100e5b46ffd1901d4607f4c1dc1b882fb34c9
|
|
4
|
+
data.tar.gz: f2e1946f8a21e6620f051cb2ea6c74f6374cd8d6ba1074ab7264960acd8528c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 919b6403677541f344409a96208f1e4e3a20a7cee39ba078b72847d7fb8359af418a3959901cbe6e09835a51b361a5c0d1f48aaa0c18aa4e3e68cec7e2b7051b
|
|
7
|
+
data.tar.gz: 504c56f9a01cdcc29876969e485f78fbc19d8d6b74849f871ca01406ad1078d58f52caac079ef5778f52f39a6694b63a47bd1264968212c4c21bfd9ad6f1743c
|
data/lib/basecamp/client.rb
CHANGED
|
@@ -208,6 +208,15 @@ module Basecamp
|
|
|
208
208
|
@parent.http.paginate_key(account_path(path), key: key, params: params, &)
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
# Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items.
|
|
212
|
+
# @param path [String] URL path (without account prefix)
|
|
213
|
+
# @param key [String] the key containing the array of paginated items
|
|
214
|
+
# @param params [Hash] query parameters
|
|
215
|
+
# @return [Hash] wrapper fields merged with key => Enumerator of all items
|
|
216
|
+
def paginate_wrapped(path, key:, params: {})
|
|
217
|
+
@parent.http.paginate_wrapped(account_path(path), key: key, params: params)
|
|
218
|
+
end
|
|
219
|
+
|
|
211
220
|
# @!group Services
|
|
212
221
|
|
|
213
222
|
# @return [Services::ProjectsService]
|
|
@@ -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-03-
|
|
4
|
+
"generated": "2026-03-11T02:58:58Z",
|
|
5
5
|
"operations": {
|
|
6
6
|
"CreateAttachment": {
|
|
7
7
|
"retry": {
|
|
@@ -1107,6 +1107,11 @@
|
|
|
1107
1107
|
429,
|
|
1108
1108
|
503
|
|
1109
1109
|
]
|
|
1110
|
+
},
|
|
1111
|
+
"pagination": {
|
|
1112
|
+
"style": "link",
|
|
1113
|
+
"totalCountHeader": "X-Total-Count",
|
|
1114
|
+
"maxPageSize": 50
|
|
1110
1115
|
}
|
|
1111
1116
|
},
|
|
1112
1117
|
"GetAnswer": {
|
|
@@ -1544,6 +1549,11 @@
|
|
|
1544
1549
|
429,
|
|
1545
1550
|
503
|
|
1546
1551
|
]
|
|
1552
|
+
},
|
|
1553
|
+
"pagination": {
|
|
1554
|
+
"style": "link",
|
|
1555
|
+
"totalCountHeader": "X-Total-Count",
|
|
1556
|
+
"maxPageSize": 50
|
|
1547
1557
|
}
|
|
1548
1558
|
},
|
|
1549
1559
|
"CreateTimesheetEntry": {
|
|
@@ -1676,6 +1686,11 @@
|
|
|
1676
1686
|
429,
|
|
1677
1687
|
503
|
|
1678
1688
|
]
|
|
1689
|
+
},
|
|
1690
|
+
"pagination": {
|
|
1691
|
+
"style": "link",
|
|
1692
|
+
"totalCountHeader": "X-Total-Count",
|
|
1693
|
+
"maxPageSize": 50
|
|
1679
1694
|
}
|
|
1680
1695
|
},
|
|
1681
1696
|
"GetScheduleEntry": {
|
|
@@ -47,8 +47,8 @@ module Basecamp
|
|
|
47
47
|
# @param person_id [Integer] person id ID
|
|
48
48
|
# @return [Hash] response data
|
|
49
49
|
def person_progress(person_id:)
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
wrap_paginated_wrapped(key: "events", service: "reports", operation: "person_progress", is_mutation: false, resource_id: person_id) do
|
|
51
|
+
paginate_wrapped("/reports/users/progress/#{person_id}.json", key: "events")
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
|
@@ -12,10 +12,11 @@ module Basecamp
|
|
|
12
12
|
# @param from [String, nil] from
|
|
13
13
|
# @param to [String, nil] to
|
|
14
14
|
# @param person_id [Integer, nil] person id
|
|
15
|
-
# @return [Hash]
|
|
15
|
+
# @return [Enumerator<Hash>] paginated results
|
|
16
16
|
def for_project(project_id:, from: nil, to: nil, person_id: nil)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
wrap_paginated(service: "timesheets", operation: "for_project", is_mutation: false, project_id: project_id) do
|
|
18
|
+
params = compact_params(from: from, to: to, person_id: person_id)
|
|
19
|
+
paginate("/projects/#{project_id}/timesheet.json", params: params)
|
|
19
20
|
end
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -24,10 +25,11 @@ module Basecamp
|
|
|
24
25
|
# @param from [String, nil] from
|
|
25
26
|
# @param to [String, nil] to
|
|
26
27
|
# @param person_id [Integer, nil] person id
|
|
27
|
-
# @return [Hash]
|
|
28
|
+
# @return [Enumerator<Hash>] paginated results
|
|
28
29
|
def for_recording(recording_id:, from: nil, to: nil, person_id: nil)
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
wrap_paginated(service: "timesheets", operation: "for_recording", is_mutation: false, resource_id: recording_id) do
|
|
31
|
+
params = compact_params(from: from, to: to, person_id: person_id)
|
|
32
|
+
paginate("/recordings/#{recording_id}/timesheet.json", params: params)
|
|
31
33
|
end
|
|
32
34
|
end
|
|
33
35
|
|
data/lib/basecamp/http.rb
CHANGED
|
@@ -172,6 +172,72 @@ module Basecamp
|
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# Fetches a wrapped paginated resource, returning wrapper fields + lazy paginated items.
|
|
176
|
+
# Use this for endpoints that return {wrapper_field: ..., key: [items]} on every page.
|
|
177
|
+
# @param path [String] initial URL path
|
|
178
|
+
# @param key [String] the key containing the array of paginated items
|
|
179
|
+
# @param params [Hash] query parameters
|
|
180
|
+
# @return [Hash] wrapper fields merged with key => Enumerator of all items
|
|
181
|
+
def paginate_wrapped(path, key:, params: {})
|
|
182
|
+
base_url = build_url(path)
|
|
183
|
+
|
|
184
|
+
@hooks.on_paginate(base_url, 1)
|
|
185
|
+
first_response = get(base_url, params: params)
|
|
186
|
+
Security.check_body_size!(first_response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
first_data = JSON.parse(first_response.body)
|
|
190
|
+
rescue JSON::ParserError => e
|
|
191
|
+
raise Basecamp::APIError.new(
|
|
192
|
+
"Failed to parse paginated response (page 1): #{Security.truncate(e.message)}"
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
wrapper = first_data.reject { |k, _| k == key }
|
|
197
|
+
first_items = first_data[key] || []
|
|
198
|
+
|
|
199
|
+
events = Enumerator.new do |yielder|
|
|
200
|
+
first_items.each { |item| yielder << item }
|
|
201
|
+
|
|
202
|
+
next_link = parse_next_link(first_response.headers["Link"])
|
|
203
|
+
url = base_url
|
|
204
|
+
page = 1
|
|
205
|
+
|
|
206
|
+
while next_link && page < @config.max_pages
|
|
207
|
+
page += 1
|
|
208
|
+
next_url = Security.resolve_url(url, next_link)
|
|
209
|
+
|
|
210
|
+
unless Security.same_origin?(next_url, base_url)
|
|
211
|
+
raise Basecamp::APIError.new(
|
|
212
|
+
"Pagination Link header points to different origin: " \
|
|
213
|
+
"#{Security.truncate(next_url)}"
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
@hooks.on_paginate(next_url, page)
|
|
218
|
+
response = get(next_url)
|
|
219
|
+
Security.check_body_size!(response.body, Security::MAX_RESPONSE_BODY_BYTES)
|
|
220
|
+
|
|
221
|
+
begin
|
|
222
|
+
data = JSON.parse(response.body)
|
|
223
|
+
rescue JSON::ParserError => e
|
|
224
|
+
raise Basecamp::APIError.new(
|
|
225
|
+
"Failed to parse paginated response (page #{page}): " \
|
|
226
|
+
"#{Security.truncate(e.message)}"
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
items = data[key] || []
|
|
231
|
+
items.each { |item| yielder << item }
|
|
232
|
+
|
|
233
|
+
next_link = parse_next_link(response.headers["Link"])
|
|
234
|
+
url = next_url
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
wrapper.merge(key => events)
|
|
239
|
+
end
|
|
240
|
+
|
|
175
241
|
private
|
|
176
242
|
|
|
177
243
|
def build_faraday_client
|
|
@@ -85,6 +85,43 @@ module Basecamp
|
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
# Wraps a wrapped-paginated operation with hooks.
|
|
89
|
+
# Fires on_operation_start eagerly (before page 1 fetch),
|
|
90
|
+
# on_operation_end when the events Enumerator completes/errors/breaks.
|
|
91
|
+
def wrap_paginated_wrapped(key:, service:, operation:, is_mutation: false, project_id: nil, resource_id: nil)
|
|
92
|
+
info = OperationInfo.new(
|
|
93
|
+
service: service, operation: operation,
|
|
94
|
+
is_mutation: is_mutation, project_id: project_id, resource_id: resource_id
|
|
95
|
+
)
|
|
96
|
+
hooks = @hooks
|
|
97
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
98
|
+
safe_hook { hooks.on_operation_start(info) }
|
|
99
|
+
|
|
100
|
+
begin
|
|
101
|
+
result = yield # paginate_wrapped fetches page 1 here
|
|
102
|
+
rescue => e
|
|
103
|
+
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
104
|
+
safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: e)) }
|
|
105
|
+
raise
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
inner_enum = result[key]
|
|
109
|
+
wrapped_enum = Enumerator.new do |yielder|
|
|
110
|
+
error = nil
|
|
111
|
+
begin
|
|
112
|
+
inner_enum.each { |item| yielder.yield(item) }
|
|
113
|
+
rescue => e
|
|
114
|
+
error = e
|
|
115
|
+
raise
|
|
116
|
+
ensure
|
|
117
|
+
duration = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
|
|
118
|
+
safe_hook { hooks.on_operation_end(info, OperationResult.new(duration_ms: duration, error: error)) }
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
result.merge(key => wrapped_enum)
|
|
123
|
+
end
|
|
124
|
+
|
|
88
125
|
# Invoke a hook callback, swallowing exceptions so hooks never break SDK behavior.
|
|
89
126
|
def safe_hook
|
|
90
127
|
yield
|
|
@@ -141,6 +178,11 @@ module Basecamp
|
|
|
141
178
|
def paginate_key(...)
|
|
142
179
|
@client.paginate_key(...)
|
|
143
180
|
end
|
|
181
|
+
|
|
182
|
+
# Paginate a wrapped response extracting items from a specific key
|
|
183
|
+
def paginate_wrapped(...)
|
|
184
|
+
@client.paginate_wrapped(...)
|
|
185
|
+
end
|
|
144
186
|
end
|
|
145
187
|
end
|
|
146
188
|
end
|
data/lib/basecamp/version.rb
CHANGED
|
@@ -387,7 +387,8 @@ class ServiceGenerator
|
|
|
387
387
|
returns_void: returns_void,
|
|
388
388
|
returns_array: returns_array,
|
|
389
389
|
is_mutation: http_method != 'GET',
|
|
390
|
-
has_pagination: !!operation['x-basecamp-pagination']
|
|
390
|
+
has_pagination: !!operation['x-basecamp-pagination'],
|
|
391
|
+
pagination_key: operation.dig('x-basecamp-pagination', 'key')
|
|
391
392
|
}
|
|
392
393
|
end
|
|
393
394
|
|
|
@@ -574,9 +575,14 @@ class ServiceGenerator
|
|
|
574
575
|
end
|
|
575
576
|
|
|
576
577
|
# Add @return tag
|
|
578
|
+
is_paginated = (op[:returns_array] || op[:has_pagination]) && !op[:pagination_key]
|
|
579
|
+
is_wrapped_paginated = op[:has_pagination] && op[:pagination_key]
|
|
580
|
+
|
|
577
581
|
if op[:returns_void]
|
|
578
582
|
lines << ' # @return [void]'
|
|
579
|
-
elsif
|
|
583
|
+
elsif is_wrapped_paginated
|
|
584
|
+
lines << ' # @return [Hash] response data'
|
|
585
|
+
elsif is_paginated
|
|
580
586
|
lines << ' # @return [Enumerator<Hash>] paginated results'
|
|
581
587
|
else
|
|
582
588
|
lines << ' # @return [Hash] response data'
|
|
@@ -587,10 +593,15 @@ class ServiceGenerator
|
|
|
587
593
|
# Build the path
|
|
588
594
|
path_expr = build_path_expression(op)
|
|
589
595
|
|
|
590
|
-
is_paginated = op[:returns_array] || op[:has_pagination]
|
|
591
596
|
hook_kwargs = build_hook_kwargs(op, service_name)
|
|
592
597
|
|
|
593
|
-
if
|
|
598
|
+
if is_wrapped_paginated
|
|
599
|
+
pagination_key = op[:pagination_key]
|
|
600
|
+
lines << " wrap_paginated_wrapped(key: \"#{pagination_key}\", #{hook_kwargs}) do"
|
|
601
|
+
body_lines = generate_wrapped_paginated_method_body(op, path_expr, pagination_key)
|
|
602
|
+
body_lines.each { |l| lines << " #{l}" }
|
|
603
|
+
lines << ' end'
|
|
604
|
+
elsif is_paginated
|
|
594
605
|
# wrap_paginated defers hooks to actual iteration time (lazy-safe)
|
|
595
606
|
lines << " wrap_paginated(#{hook_kwargs}) do"
|
|
596
607
|
body_lines = generate_list_method_body(op, path_expr)
|
|
@@ -717,6 +728,20 @@ class ServiceGenerator
|
|
|
717
728
|
lines
|
|
718
729
|
end
|
|
719
730
|
|
|
731
|
+
def generate_wrapped_paginated_method_body(op, path_expr, pagination_key)
|
|
732
|
+
lines = []
|
|
733
|
+
|
|
734
|
+
if op[:query_params].any?
|
|
735
|
+
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
736
|
+
lines << " params = compact_params(#{param_names.join(', ')})"
|
|
737
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params)"
|
|
738
|
+
else
|
|
739
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\")"
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
lines
|
|
743
|
+
end
|
|
744
|
+
|
|
720
745
|
def generate_get_method_body(op, path_expr)
|
|
721
746
|
lines = []
|
|
722
747
|
http_method = op[:http_method].downcase
|
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.4.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-03-
|
|
11
|
+
date: 2026-03-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|