basecamp-sdk 0.12.0 → 0.13.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 +127 -8
- data/lib/basecamp/client.rb +35 -11
- data/lib/basecamp/config.rb +69 -0
- data/lib/basecamp/generated/metadata.json +280 -141
- data/lib/basecamp/generated/services/base_service.rb +37 -16
- data/lib/basecamp/generated/services/bookmarks_service.rb +5 -4
- data/lib/basecamp/generated/services/boosts_service.rb +12 -6
- data/lib/basecamp/generated/services/campfires_service.rb +54 -41
- data/lib/basecamp/generated/services/cards_service.rb +6 -3
- data/lib/basecamp/generated/services/checkins_service.rb +28 -15
- data/lib/basecamp/generated/services/client_approvals_service.rb +8 -5
- data/lib/basecamp/generated/services/client_correspondences_service.rb +8 -5
- data/lib/basecamp/generated/services/client_replies_service.rb +12 -7
- data/lib/basecamp/generated/services/cloud_files_service.rb +57 -0
- data/lib/basecamp/generated/services/comments_service.rb +6 -3
- data/lib/basecamp/generated/services/documents_service.rb +9 -6
- data/lib/basecamp/generated/services/drafts_service.rb +5 -4
- data/lib/basecamp/generated/services/events_service.rb +6 -3
- data/lib/basecamp/generated/services/everything_service.rb +70 -56
- data/lib/basecamp/generated/services/folders_service.rb +62 -0
- data/lib/basecamp/generated/services/forwards_service.rb +12 -17
- data/lib/basecamp/generated/services/gauges_service.rb +12 -7
- data/lib/basecamp/generated/services/google_documents_service.rb +61 -0
- data/lib/basecamp/generated/services/message_types_service.rb +4 -3
- data/lib/basecamp/generated/services/messages_service.rb +6 -4
- data/lib/basecamp/generated/services/my_notes_service.rb +1 -1
- data/lib/basecamp/generated/services/my_notifications_service.rb +8 -5
- data/lib/basecamp/generated/services/people_service.rb +17 -10
- data/lib/basecamp/generated/services/projects_service.rb +26 -4
- data/lib/basecamp/generated/services/recordings_service.rb +6 -13
- data/lib/basecamp/generated/services/reports_service.rb +15 -9
- data/lib/basecamp/generated/services/schedules_service.rb +90 -16
- data/lib/basecamp/generated/services/search_service.rb +6 -4
- data/lib/basecamp/generated/services/templates_service.rb +6 -4
- data/lib/basecamp/generated/services/timeline_service.rb +6 -3
- data/lib/basecamp/generated/services/timesheets_service.rb +22 -8
- data/lib/basecamp/generated/services/todolist_groups_service.rb +7 -4
- data/lib/basecamp/generated/services/todolists_service.rb +11 -9
- data/lib/basecamp/generated/services/todos_service.rb +6 -14
- data/lib/basecamp/generated/services/uploads_service.rb +10 -6
- data/lib/basecamp/generated/services/vaults_service.rb +6 -3
- data/lib/basecamp/generated/services/webhooks_service.rb +4 -3
- data/lib/basecamp/generated/types.rb +527 -139
- data/lib/basecamp/http.rb +346 -163
- data/lib/basecamp/list_enumerator.rb +29 -0
- data/lib/basecamp/list_meta.rb +44 -0
- data/lib/basecamp/services/cards_extensions.rb +35 -27
- data/lib/basecamp/services/documents_extensions.rb +136 -0
- data/lib/basecamp/services/merge_safe.rb +255 -0
- data/lib/basecamp/services/schedules_extensions.rb +354 -0
- data/lib/basecamp/services/todolists_extensions.rb +274 -0
- data/lib/basecamp/services/todos_extensions.rb +22 -6
- data/lib/basecamp/validation_error.rb +11 -1
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +94 -4
- data/scripts/generate-services.rb +74 -25
- data/scripts/generate-types.rb +2 -1
- data/scripts/go_type_spellings.rb +26 -0
- metadata +12 -2
|
@@ -3,13 +3,23 @@
|
|
|
3
3
|
module Basecamp
|
|
4
4
|
# Raised for validation errors (400, 422).
|
|
5
5
|
class ValidationError < Error
|
|
6
|
-
|
|
6
|
+
# @return [Hash{String => Array<String>}, nil] field-keyed validation
|
|
7
|
+
# messages from a 400/422 body — either {"errors" => {"field" => ["msg"]}},
|
|
8
|
+
# the Rails RecordInvalid rendering, or the same map with no wrapper at all
|
|
9
|
+
# ({"field" => ["msg"]}), which some controllers emit. Nil for every other
|
|
10
|
+
# error shape.
|
|
11
|
+
# The flattened form is also folded into the message; this slot preserves
|
|
12
|
+
# the raw, untruncated per-field messages.
|
|
13
|
+
attr_reader :field_errors
|
|
14
|
+
|
|
15
|
+
def initialize(message, hint: nil, http_status: 400, field_errors: nil)
|
|
7
16
|
super(
|
|
8
17
|
code: ErrorCode::VALIDATION,
|
|
9
18
|
message: message,
|
|
10
19
|
hint: hint,
|
|
11
20
|
http_status: http_status
|
|
12
21
|
)
|
|
22
|
+
@field_errors = field_errors
|
|
13
23
|
end
|
|
14
24
|
end
|
|
15
25
|
end
|
data/lib/basecamp/version.rb
CHANGED
data/lib/basecamp.rb
CHANGED
|
@@ -11,10 +11,26 @@ loader.on_load("Basecamp::Services::TodosService") do |klass, _abspath|
|
|
|
11
11
|
klass.prepend(Basecamp::Services::TodosExtensions)
|
|
12
12
|
end
|
|
13
13
|
# Same shape for cards: the generated class owns the constant and the
|
|
14
|
-
#
|
|
14
|
+
# tri-state `due_on` update is prepended over the generated update_verbatim.
|
|
15
15
|
loader.on_load("Basecamp::Services::CardsService") do |klass, _abspath|
|
|
16
16
|
klass.prepend(Basecamp::Services::CardsExtensions)
|
|
17
17
|
end
|
|
18
|
+
# And for todolists: PUT /todolists/{id} is a full replace, so the generated
|
|
19
|
+
# class owns `replace` and the merge-safe update/edit surface is prepended.
|
|
20
|
+
loader.on_load("Basecamp::Services::TodolistsService") do |klass, _abspath|
|
|
21
|
+
klass.prepend(Basecamp::Services::TodolistsExtensions)
|
|
22
|
+
end
|
|
23
|
+
# And for documents: PUT /documents/{id} is a full replace, so the generated
|
|
24
|
+
# class owns `replace` and the merge-safe update/edit surface is prepended.
|
|
25
|
+
loader.on_load("Basecamp::Services::DocumentsService") do |klass, _abspath|
|
|
26
|
+
klass.prepend(Basecamp::Services::DocumentsExtensions)
|
|
27
|
+
end
|
|
28
|
+
# And for schedule entries: PUT /schedule_entries/{id} is a full replace, so
|
|
29
|
+
# the generated class owns `replace_entry` and the merge-safe
|
|
30
|
+
# `update_entry`/`edit_entry` surface is prepended.
|
|
31
|
+
loader.on_load("Basecamp::Services::SchedulesService") do |klass, _abspath|
|
|
32
|
+
klass.prepend(Basecamp::Services::SchedulesExtensions)
|
|
33
|
+
end
|
|
18
34
|
loader.setup
|
|
19
35
|
|
|
20
36
|
# Load generated types if available
|
|
@@ -105,7 +121,9 @@ module Basecamp
|
|
|
105
121
|
|
|
106
122
|
case status
|
|
107
123
|
when 400, 422
|
|
108
|
-
|
|
124
|
+
field_errors = parse_field_errors(body)
|
|
125
|
+
message = Security.truncate(compose_validation_message(parse_error_message(body), field_errors) || "Request failed")
|
|
126
|
+
ValidationError.new(message, http_status: status, field_errors: field_errors)
|
|
109
127
|
when 401
|
|
110
128
|
AuthError.new(message)
|
|
111
129
|
when 403
|
|
@@ -141,7 +159,9 @@ module Basecamp
|
|
|
141
159
|
"download"
|
|
142
160
|
end
|
|
143
161
|
|
|
144
|
-
# Parses error message from response body.
|
|
162
|
+
# Parses error message from response body. A key is used only when its
|
|
163
|
+
# value is a String (SPEC section 6), so a malformed scalar member such as
|
|
164
|
+
# {"error": {}} cannot raise or leak a non-string into the message.
|
|
145
165
|
# @param body [String, nil]
|
|
146
166
|
# @return [String, nil]
|
|
147
167
|
def self.parse_error_message(body)
|
|
@@ -150,9 +170,79 @@ module Basecamp
|
|
|
150
170
|
Security.check_body_size!(body, Security::MAX_ERROR_BODY_BYTES, "Error")
|
|
151
171
|
|
|
152
172
|
data = JSON.parse(body)
|
|
153
|
-
msg = data["error"]
|
|
173
|
+
msg = data.is_a?(Hash) ? [ data["error"], data["message"] ].find { |value| value.is_a?(String) } : nil
|
|
154
174
|
msg ? Security.truncate(msg) : nil
|
|
155
175
|
rescue JSON::ParserError, ApiError
|
|
156
176
|
nil
|
|
157
177
|
end
|
|
178
|
+
|
|
179
|
+
# Extracts the field-keyed validation errors map from a response body — the
|
|
180
|
+
# Rails RecordInvalid rendering {"errors" => {"field" => ["msg", ...]}}.
|
|
181
|
+
# Entries whose value is not an array are skipped, non-string elements are
|
|
182
|
+
# dropped, and a map with no usable entries is treated as absent (nil).
|
|
183
|
+
# @param body [String, nil]
|
|
184
|
+
# @return [Hash{String => Array<String>}, nil]
|
|
185
|
+
def self.parse_field_errors(body)
|
|
186
|
+
return nil if body.nil? || body.empty?
|
|
187
|
+
|
|
188
|
+
Security.check_body_size!(body, Security::MAX_ERROR_BODY_BYTES, "Error")
|
|
189
|
+
|
|
190
|
+
data = JSON.parse(body)
|
|
191
|
+
errors = data.is_a?(Hash) ? data["errors"] : nil
|
|
192
|
+
if errors.is_a?(Hash)
|
|
193
|
+
field_errors = errors.each_with_object({}) do |(field, values), result|
|
|
194
|
+
next unless values.is_a?(Array)
|
|
195
|
+
|
|
196
|
+
messages = values.grep(String)
|
|
197
|
+
result[field.to_s] = messages unless messages.empty?
|
|
198
|
+
end
|
|
199
|
+
field_errors.empty? ? nil : field_errors
|
|
200
|
+
else
|
|
201
|
+
parse_bare_field_errors(data)
|
|
202
|
+
end
|
|
203
|
+
rescue JSON::ParserError, ApiError
|
|
204
|
+
nil
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Extracts an unwrapped field map — the `render json: @webhook.errors`
|
|
208
|
+
# rendering, where the whole body is {"field" => ["msg", ...]}. The gate is
|
|
209
|
+
# all-or-nothing by design (SPEC section 6 step 2): with no "errors" key to
|
|
210
|
+
# declare intent, only shape distinguishes a field map from any other JSON
|
|
211
|
+
# object, so a single non-conforming member means this is not one.
|
|
212
|
+
# @param data [Object] the parsed body
|
|
213
|
+
# @return [Hash{String => Array<String>}, nil]
|
|
214
|
+
def self.parse_bare_field_errors(data)
|
|
215
|
+
return nil unless data.is_a?(Hash) && !data.empty?
|
|
216
|
+
# Only "errors" is structurally reserved (it belongs to the wrapped path).
|
|
217
|
+
# "error" and "message" are not excluded by name: a flat body carries them
|
|
218
|
+
# as strings, which the shape gate below already rejects.
|
|
219
|
+
return nil if data.key?("errors")
|
|
220
|
+
|
|
221
|
+
data.each_with_object({}) do |(field, values), result|
|
|
222
|
+
return nil unless values.is_a?(Array) && !values.empty?
|
|
223
|
+
return nil unless values.all? { |message| message.is_a?(String) && !message.empty? }
|
|
224
|
+
|
|
225
|
+
result[field.to_s] = values
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Merges the top-level error message with the flattened field-keyed errors:
|
|
230
|
+
# appended in parentheses when both are present, standing alone when only the
|
|
231
|
+
# field errors are. The flattened shape — fields sorted lexicographically, a
|
|
232
|
+
# field's messages joined with "; ", fields joined with ", " — is shared by
|
|
233
|
+
# all six SDKs; change it everywhere or nowhere. Callers truncate the
|
|
234
|
+
# composed result so the appended tail is capped too.
|
|
235
|
+
# @param message [String, nil]
|
|
236
|
+
# @param field_errors [Hash{String => Array<String>}, nil]
|
|
237
|
+
# @return [String, nil]
|
|
238
|
+
def self.compose_validation_message(message, field_errors)
|
|
239
|
+
if field_errors.nil?
|
|
240
|
+
message
|
|
241
|
+
else
|
|
242
|
+
flat = field_errors.keys.sort \
|
|
243
|
+
.map { |field| "#{field}: #{field_errors[field].join("; ")}" } \
|
|
244
|
+
.join(", ")
|
|
245
|
+
message ? "#{message} (#{flat})" : flat
|
|
246
|
+
end
|
|
247
|
+
end
|
|
158
248
|
end
|
|
@@ -67,11 +67,13 @@ class ServiceGenerator
|
|
|
67
67
|
'Attachments' => %w[CreateAttachment],
|
|
68
68
|
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions],
|
|
69
69
|
'Vaults' => %w[GetVault UpdateVault ListVaults CreateVault],
|
|
70
|
-
'Documents' => %w[GetDocument
|
|
70
|
+
'Documents' => %w[GetDocument ReplaceDocument ListDocuments CreateDocument],
|
|
71
|
+
'CloudFiles' => %w[GetCloudFile CreateCloudFile UpdateCloudFile],
|
|
72
|
+
'GoogleDocuments' => %w[GetGoogleDocument CreateGoogleDocument UpdateGoogleDocument]
|
|
71
73
|
},
|
|
72
74
|
'Automation' => {
|
|
73
75
|
'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool],
|
|
74
|
-
'Recordings' => %w[
|
|
76
|
+
'Recordings' => %w[ArchiveRecording UnarchiveRecording TrashRecording ListRecordings],
|
|
75
77
|
'Webhooks' => %w[ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook],
|
|
76
78
|
'Events' => %w[ListEvents],
|
|
77
79
|
'Lineup' => %w[CreateLineupMarker UpdateLineupMarker DeleteLineupMarker],
|
|
@@ -104,10 +106,10 @@ class ServiceGenerator
|
|
|
104
106
|
'Schedule' => {
|
|
105
107
|
'Schedules' => %w[
|
|
106
108
|
GetSchedule UpdateScheduleSettings ListScheduleEntries
|
|
107
|
-
CreateScheduleEntry GetScheduleEntry
|
|
109
|
+
CreateScheduleEntry GetScheduleEntry ReplaceScheduleEntry
|
|
108
110
|
GetScheduleEntryOccurrence
|
|
109
111
|
],
|
|
110
|
-
'Timesheets' => %w[GetRecordingTimesheet GetProjectTimesheet GetTimesheetReport GetTimesheetEntry CreateTimesheetEntry UpdateTimesheetEntry]
|
|
112
|
+
'Timesheets' => %w[GetRecordingTimesheet GetProjectTimesheet GetTimesheetReport GetTimesheetEntry CreateTimesheetEntry UpdateTimesheetEntry DestroyTimesheetEntry]
|
|
111
113
|
},
|
|
112
114
|
'ClientFeatures' => {
|
|
113
115
|
'ClientApprovals' => %w[ListClientApprovals GetClientApproval],
|
|
@@ -116,7 +118,7 @@ class ServiceGenerator
|
|
|
116
118
|
'ClientVisibility' => %w[SetClientVisibility]
|
|
117
119
|
},
|
|
118
120
|
'Todos' => {
|
|
119
|
-
'Todos' => %w[ListTodos CreateTodo CreateTodosetTodo GetTodo ReplaceTodo CompleteTodo UncompleteTodo
|
|
121
|
+
'Todos' => %w[ListTodos CreateTodo CreateTodosetTodo GetTodo ReplaceTodo CompleteTodo UncompleteTodo],
|
|
120
122
|
'Todolists' => %w[GetTodolistOrGroup UpdateTodolistOrGroup ListTodolists CreateTodolist RepositionTodolist],
|
|
121
123
|
'Todosets' => %w[GetTodoset],
|
|
122
124
|
'HillCharts' => %w[GetHillChart UpdateHillChartSettings],
|
|
@@ -139,15 +141,19 @@ class ServiceGenerator
|
|
|
139
141
|
METHOD_NAME_OVERRIDES = {
|
|
140
142
|
'GetMyProfile' => 'my_profile',
|
|
141
143
|
'GetTodolistOrGroup' => 'get',
|
|
142
|
-
|
|
144
|
+
# The plain `update` name belongs to the merge-safe composite; the raw
|
|
145
|
+
# single-PUT path keeps a name that says what it does. BC3 rebuilds the
|
|
146
|
+
# todolist from the permitted params, so omission clears. See #374.
|
|
147
|
+
'UpdateTodolistOrGroup' => 'replace',
|
|
143
148
|
'SetCardColumnColor' => 'set_color',
|
|
144
149
|
'EnableCardColumnOnHold' => 'enable_on_hold',
|
|
145
150
|
'DisableCardColumnOnHold' => 'disable_on_hold',
|
|
146
151
|
'RepositionCardStep' => 'reposition',
|
|
147
152
|
'CreateCardStep' => 'create',
|
|
148
153
|
'UpdateCardStep' => 'update',
|
|
149
|
-
# The plain `update` name belongs to the
|
|
150
|
-
#
|
|
154
|
+
# The plain `update` name belongs to the tri-state `due_on` wrapper in
|
|
155
|
+
# CardsExtensions; the unnormalised path keeps a name that says what it
|
|
156
|
+
# does. See #467.
|
|
151
157
|
'UpdateCard' => 'update_verbatim',
|
|
152
158
|
'SetCardStepCompletion' => 'set_completion',
|
|
153
159
|
'GetQuestionnaire' => 'get_questionnaire',
|
|
@@ -175,6 +181,7 @@ class ServiceGenerator
|
|
|
175
181
|
'GetTimesheetEntry' => 'get',
|
|
176
182
|
'CreateTimesheetEntry' => 'create',
|
|
177
183
|
'UpdateTimesheetEntry' => 'update',
|
|
184
|
+
'DestroyTimesheetEntry' => 'destroy',
|
|
178
185
|
'GetProgressReport' => 'progress',
|
|
179
186
|
'GetUpcomingSchedule' => 'upcoming',
|
|
180
187
|
'GetAssignedTodos' => 'assigned',
|
|
@@ -203,7 +210,6 @@ class ServiceGenerator
|
|
|
203
210
|
'ListForwards' => 'list',
|
|
204
211
|
'GetForwardReply' => 'get_reply',
|
|
205
212
|
'ListForwardReplies' => 'list_replies',
|
|
206
|
-
'CreateForwardReply' => 'create_reply',
|
|
207
213
|
'GetInbox' => 'get_inbox',
|
|
208
214
|
# Uploads - use specific names to avoid conflicts with versions
|
|
209
215
|
'GetUpload' => 'get',
|
|
@@ -233,7 +239,11 @@ class ServiceGenerator
|
|
|
233
239
|
'GetSchedule' => 'get',
|
|
234
240
|
'UpdateScheduleSettings' => 'update_settings',
|
|
235
241
|
'GetScheduleEntry' => 'get_entry',
|
|
236
|
-
|
|
242
|
+
# The plain `update_entry` name belongs to the merge-safe composite; the raw
|
|
243
|
+
# single-PUT path keeps a name that says what it does. Without the override
|
|
244
|
+
# the algorithm yields a bare `replace` (scheduleentry is a SIMPLE_RESOURCE),
|
|
245
|
+
# which reads as "replace the schedule". See #547.
|
|
246
|
+
'ReplaceScheduleEntry' => 'replace_entry',
|
|
237
247
|
'CreateScheduleEntry' => 'create_entry',
|
|
238
248
|
'ListScheduleEntries' => 'list_entries',
|
|
239
249
|
'GetScheduleEntryOccurrence' => 'get_entry_occurrence',
|
|
@@ -571,6 +581,34 @@ class ServiceGenerator
|
|
|
571
581
|
.downcase
|
|
572
582
|
end
|
|
573
583
|
|
|
584
|
+
# Folds a multi-line description into the body of a YARD `@param` tag.
|
|
585
|
+
#
|
|
586
|
+
# The first line is returned bare — the caller has already written
|
|
587
|
+
# `# @param name [Type] ` in front of it. Every later line is indented under
|
|
588
|
+
# it, EXCEPT a blank one: a paragraph break must emit a bare `#`, not `#`
|
|
589
|
+
# followed by the continuation padding. Padding an empty line is trailing
|
|
590
|
+
# whitespace, which `git diff --check` fails on, and it stayed fixed only
|
|
591
|
+
# because this lives in the generator — patching the emitted file put it back
|
|
592
|
+
# on the next `make generate`.
|
|
593
|
+
#
|
|
594
|
+
# Any trailing whitespace already in the description is stripped for the same
|
|
595
|
+
# reason: a Smithy doc comment can carry it, and the generator should not
|
|
596
|
+
# launder it into a generated file.
|
|
597
|
+
YARD_CONTINUATION_INDENT = ' # '
|
|
598
|
+
|
|
599
|
+
def yard_param_description(text)
|
|
600
|
+
text.to_s.split("\n", -1).each_with_index.map do |line, index|
|
|
601
|
+
stripped = line.rstrip
|
|
602
|
+
if index.zero?
|
|
603
|
+
stripped
|
|
604
|
+
elsif stripped.empty?
|
|
605
|
+
' #'
|
|
606
|
+
else
|
|
607
|
+
"#{YARD_CONTINUATION_INDENT}#{stripped}"
|
|
608
|
+
end
|
|
609
|
+
end.join("\n")
|
|
610
|
+
end
|
|
611
|
+
|
|
574
612
|
def generate_service(service)
|
|
575
613
|
lines = []
|
|
576
614
|
|
|
@@ -611,8 +649,11 @@ class ServiceGenerator
|
|
|
611
649
|
def generate_method(op, service_name:)
|
|
612
650
|
lines = []
|
|
613
651
|
|
|
614
|
-
|
|
615
|
-
|
|
652
|
+
is_paginated = (op[:returns_array] || op[:has_pagination]) && !op[:pagination_key]
|
|
653
|
+
is_wrapped_paginated = op[:has_pagination] && op[:pagination_key]
|
|
654
|
+
|
|
655
|
+
# Method signature (paginated operations gain a trailing max_items: kwarg)
|
|
656
|
+
params = build_params(op, paginated: is_paginated || is_wrapped_paginated)
|
|
616
657
|
|
|
617
658
|
# YARD documentation
|
|
618
659
|
lines << " # #{op[:description]}"
|
|
@@ -644,7 +685,7 @@ class ServiceGenerator
|
|
|
644
685
|
ruby_name = to_snake_case(b[:name])
|
|
645
686
|
type = b[:type] || 'Object'
|
|
646
687
|
type = "#{type}, nil" unless b[:required]
|
|
647
|
-
desc = (b[:description] || ruby_name.gsub('_', ' '))
|
|
688
|
+
desc = yard_param_description(b[:description] || ruby_name.gsub('_', ' '))
|
|
648
689
|
format_hint = b[:format_hint] ? " (#{b[:format_hint]})" : ''
|
|
649
690
|
lines << " # @param #{ruby_name} [#{type}] #{desc}#{format_hint}"
|
|
650
691
|
end
|
|
@@ -655,20 +696,22 @@ class ServiceGenerator
|
|
|
655
696
|
ruby_name = to_snake_case(q[:name])
|
|
656
697
|
type = q[:type] || 'String'
|
|
657
698
|
type = "#{type}, nil" unless q[:required]
|
|
658
|
-
desc = (q[:description] || ruby_name.gsub('_', ' '))
|
|
699
|
+
desc = yard_param_description(q[:description] || ruby_name.gsub('_', ' '))
|
|
659
700
|
lines << " # @param #{ruby_name} [#{type}] #{desc}"
|
|
660
701
|
end
|
|
661
702
|
|
|
662
|
-
# Add @
|
|
663
|
-
is_paginated
|
|
664
|
-
|
|
703
|
+
# Add @param tag for the pagination cap on paginated operations
|
|
704
|
+
if is_paginated || is_wrapped_paginated
|
|
705
|
+
lines << ' # @param max_items [Integer, nil] cap on items yielded across pages; nil or non-positive means no cap'
|
|
706
|
+
end
|
|
665
707
|
|
|
708
|
+
# Add @return tag
|
|
666
709
|
if op[:returns_void]
|
|
667
710
|
lines << ' # @return [void]'
|
|
668
711
|
elsif is_wrapped_paginated
|
|
669
|
-
lines << ' # @return [Hash]
|
|
712
|
+
lines << ' # @return [Hash] wrapper fields merged with a ListEnumerator of the paginated items'
|
|
670
713
|
elsif is_paginated
|
|
671
|
-
lines << ' # @return [
|
|
714
|
+
lines << ' # @return [ListEnumerator<Hash>] lazily paginated results (#meta carries pagination metadata)'
|
|
672
715
|
elsif op[:returns_bare_array]
|
|
673
716
|
# Unpaginated bare array (single request, no Link-following) — e.g. the
|
|
674
717
|
# overdue todo/card feeds. Returns the parsed JSON array, not a Hash.
|
|
@@ -691,7 +734,8 @@ class ServiceGenerator
|
|
|
691
734
|
body_lines.each { |l| lines << " #{l}" }
|
|
692
735
|
lines << ' end'
|
|
693
736
|
elsif is_paginated
|
|
694
|
-
# wrap_paginated
|
|
737
|
+
# wrap_paginated fires the start hook eagerly (page 1 is fetched inside
|
|
738
|
+
# the block) and the end hook when iteration completes
|
|
695
739
|
lines << " wrap_paginated(#{hook_kwargs}) do"
|
|
696
740
|
body_lines = generate_list_method_body(op, path_expr)
|
|
697
741
|
body_lines.each { |l| lines << " #{l}" }
|
|
@@ -729,7 +773,7 @@ class ServiceGenerator
|
|
|
729
773
|
kwargs.join(', ')
|
|
730
774
|
end
|
|
731
775
|
|
|
732
|
-
def build_params(op)
|
|
776
|
+
def build_params(op, paginated: false)
|
|
733
777
|
params = []
|
|
734
778
|
|
|
735
779
|
# Path parameters as keyword args
|
|
@@ -772,6 +816,9 @@ class ServiceGenerator
|
|
|
772
816
|
params << "#{to_snake_case(q[:name])}: nil"
|
|
773
817
|
end
|
|
774
818
|
|
|
819
|
+
# Client-side pagination cap, threaded into the base paginators
|
|
820
|
+
params << 'max_items: nil' if paginated
|
|
821
|
+
|
|
775
822
|
params.join(', ')
|
|
776
823
|
end
|
|
777
824
|
|
|
@@ -818,9 +865,9 @@ class ServiceGenerator
|
|
|
818
865
|
if op[:query_params].any?
|
|
819
866
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
820
867
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
821
|
-
lines << " paginate(#{path_expr}, params: params, operation: \"#{op[:operation_id]}\")"
|
|
868
|
+
lines << " paginate(#{path_expr}, params: params, operation: \"#{op[:operation_id]}\", max_items: max_items)"
|
|
822
869
|
else
|
|
823
|
-
lines << " paginate(#{path_expr}, operation: \"#{op[:operation_id]}\")"
|
|
870
|
+
lines << " paginate(#{path_expr}, operation: \"#{op[:operation_id]}\", max_items: max_items)"
|
|
824
871
|
end
|
|
825
872
|
|
|
826
873
|
lines
|
|
@@ -832,9 +879,11 @@ class ServiceGenerator
|
|
|
832
879
|
if op[:query_params].any?
|
|
833
880
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
834
881
|
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
835
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params,
|
|
882
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params, " \
|
|
883
|
+
"operation: \"#{op[:operation_id]}\", max_items: max_items)"
|
|
836
884
|
else
|
|
837
|
-
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\",
|
|
885
|
+
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", " \
|
|
886
|
+
"operation: \"#{op[:operation_id]}\", max_items: max_items)"
|
|
838
887
|
end
|
|
839
888
|
|
|
840
889
|
lines
|
data/scripts/generate-types.rb
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
require 'json'
|
|
10
10
|
require 'set'
|
|
11
11
|
require 'time'
|
|
12
|
+
require_relative 'go_type_spellings'
|
|
12
13
|
|
|
13
14
|
# Schemas to skip (internal/generated response wrappers)
|
|
14
15
|
SKIP_PATTERNS = [
|
|
@@ -207,7 +208,7 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
207
208
|
"parse_float(data[\"#{prop_name}\"])"
|
|
208
209
|
elsif prop_schema['type'] == 'boolean'
|
|
209
210
|
"parse_boolean(data[\"#{prop_name}\"])"
|
|
210
|
-
elsif prop_schema['x-go-type']
|
|
211
|
+
elsif GoTypeSpellings.timestamp_go_type?(prop_schema['x-go-type'])
|
|
211
212
|
"parse_datetime(data[\"#{prop_name}\"])"
|
|
212
213
|
else
|
|
213
214
|
"data[\"#{prop_name}\"]"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Go type spellings the Ruby generator needs to reason about, extracted so the
|
|
4
|
+
# generator and its tests can share one definition — the test used to `load`
|
|
5
|
+
# the whole generator script, which defined every one of its helpers on the
|
|
6
|
+
# test process.
|
|
7
|
+
module GoTypeSpellings
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Spellings that mean "full timestamp" and therefore get Time coercion in
|
|
11
|
+
# Ruby. Matched after stripping a leading `*`: the Go optional-pointer policy
|
|
12
|
+
# (SPEC.md §10) means a schema may carry either `time.Time` or `*time.Time`
|
|
13
|
+
# for the same wire contract, and an exact-string match silently degrades the
|
|
14
|
+
# pointer spelling to a raw String (#537).
|
|
15
|
+
#
|
|
16
|
+
# types.FlexibleTime is deliberately NOT here: it also accepts date-only
|
|
17
|
+
# values, and Ruby has passed those through as strings since it was
|
|
18
|
+
# introduced. Adding it is a behavior change, not a spelling fix.
|
|
19
|
+
TIMESTAMP_GO_TYPES = [ 'time.Time' ].freeze
|
|
20
|
+
|
|
21
|
+
def timestamp_go_type?(go_type)
|
|
22
|
+
return false unless go_type.is_a?(String)
|
|
23
|
+
|
|
24
|
+
TIMESTAMP_GO_TYPES.include?(go_type.delete_prefix('*'))
|
|
25
|
+
end
|
|
26
|
+
end
|
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.13.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-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -195,13 +195,16 @@ files:
|
|
|
195
195
|
- lib/basecamp/generated/services/client_correspondences_service.rb
|
|
196
196
|
- lib/basecamp/generated/services/client_replies_service.rb
|
|
197
197
|
- lib/basecamp/generated/services/client_visibility_service.rb
|
|
198
|
+
- lib/basecamp/generated/services/cloud_files_service.rb
|
|
198
199
|
- lib/basecamp/generated/services/comments_service.rb
|
|
199
200
|
- lib/basecamp/generated/services/documents_service.rb
|
|
200
201
|
- lib/basecamp/generated/services/drafts_service.rb
|
|
201
202
|
- lib/basecamp/generated/services/events_service.rb
|
|
202
203
|
- lib/basecamp/generated/services/everything_service.rb
|
|
204
|
+
- lib/basecamp/generated/services/folders_service.rb
|
|
203
205
|
- lib/basecamp/generated/services/forwards_service.rb
|
|
204
206
|
- lib/basecamp/generated/services/gauges_service.rb
|
|
207
|
+
- lib/basecamp/generated/services/google_documents_service.rb
|
|
205
208
|
- lib/basecamp/generated/services/hill_charts_service.rb
|
|
206
209
|
- lib/basecamp/generated/services/lineup_service.rb
|
|
207
210
|
- lib/basecamp/generated/services/message_boards_service.rb
|
|
@@ -232,6 +235,8 @@ files:
|
|
|
232
235
|
- lib/basecamp/generated/types.rb
|
|
233
236
|
- lib/basecamp/hooks.rb
|
|
234
237
|
- lib/basecamp/http.rb
|
|
238
|
+
- lib/basecamp/list_enumerator.rb
|
|
239
|
+
- lib/basecamp/list_meta.rb
|
|
235
240
|
- lib/basecamp/logger_hooks.rb
|
|
236
241
|
- lib/basecamp/network_error.rb
|
|
237
242
|
- lib/basecamp/noop_hooks.rb
|
|
@@ -262,6 +267,10 @@ files:
|
|
|
262
267
|
- lib/basecamp/security.rb
|
|
263
268
|
- lib/basecamp/services/authorization_service.rb
|
|
264
269
|
- lib/basecamp/services/cards_extensions.rb
|
|
270
|
+
- lib/basecamp/services/documents_extensions.rb
|
|
271
|
+
- lib/basecamp/services/merge_safe.rb
|
|
272
|
+
- lib/basecamp/services/schedules_extensions.rb
|
|
273
|
+
- lib/basecamp/services/todolists_extensions.rb
|
|
265
274
|
- lib/basecamp/services/todos_extensions.rb
|
|
266
275
|
- lib/basecamp/static_token_provider.rb
|
|
267
276
|
- lib/basecamp/token_provider.rb
|
|
@@ -276,6 +285,7 @@ files:
|
|
|
276
285
|
- scripts/generate-metadata.rb
|
|
277
286
|
- scripts/generate-services.rb
|
|
278
287
|
- scripts/generate-types.rb
|
|
288
|
+
- scripts/go_type_spellings.rb
|
|
279
289
|
homepage: https://github.com/basecamp/basecamp-sdk
|
|
280
290
|
licenses:
|
|
281
291
|
- MIT
|