basecamp-sdk 0.2.1
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 +7 -0
- data/.rubocop.yml +14 -0
- data/.yardopts +6 -0
- data/README.md +293 -0
- data/Rakefile +26 -0
- data/basecamp-sdk.gemspec +46 -0
- data/lib/basecamp/auth_strategy.rb +38 -0
- data/lib/basecamp/chain_hooks.rb +45 -0
- data/lib/basecamp/client.rb +428 -0
- data/lib/basecamp/config.rb +143 -0
- data/lib/basecamp/errors.rb +289 -0
- data/lib/basecamp/generated/metadata.json +2281 -0
- data/lib/basecamp/generated/services/attachments_service.rb +24 -0
- data/lib/basecamp/generated/services/boosts_service.rb +70 -0
- data/lib/basecamp/generated/services/campfires_service.rb +122 -0
- data/lib/basecamp/generated/services/card_columns_service.rb +103 -0
- data/lib/basecamp/generated/services/card_steps_service.rb +57 -0
- data/lib/basecamp/generated/services/card_tables_service.rb +20 -0
- data/lib/basecamp/generated/services/cards_service.rb +66 -0
- data/lib/basecamp/generated/services/checkins_service.rb +157 -0
- data/lib/basecamp/generated/services/client_approvals_service.rb +28 -0
- data/lib/basecamp/generated/services/client_correspondences_service.rb +28 -0
- data/lib/basecamp/generated/services/client_replies_service.rb +30 -0
- data/lib/basecamp/generated/services/client_visibility_service.rb +21 -0
- data/lib/basecamp/generated/services/comments_service.rb +49 -0
- data/lib/basecamp/generated/services/documents_service.rb +52 -0
- data/lib/basecamp/generated/services/events_service.rb +20 -0
- data/lib/basecamp/generated/services/forwards_service.rb +67 -0
- data/lib/basecamp/generated/services/lineup_service.rb +44 -0
- data/lib/basecamp/generated/services/message_boards_service.rb +20 -0
- data/lib/basecamp/generated/services/message_types_service.rb +59 -0
- data/lib/basecamp/generated/services/messages_service.rb +75 -0
- data/lib/basecamp/generated/services/people_service.rb +73 -0
- data/lib/basecamp/generated/services/projects_service.rb +63 -0
- data/lib/basecamp/generated/services/recordings_service.rb +64 -0
- data/lib/basecamp/generated/services/reports_service.rb +56 -0
- data/lib/basecamp/generated/services/schedules_service.rb +92 -0
- data/lib/basecamp/generated/services/search_service.rb +31 -0
- data/lib/basecamp/generated/services/subscriptions_service.rb +50 -0
- data/lib/basecamp/generated/services/templates_service.rb +82 -0
- data/lib/basecamp/generated/services/timeline_service.rb +20 -0
- data/lib/basecamp/generated/services/timesheets_service.rb +81 -0
- data/lib/basecamp/generated/services/todolist_groups_service.rb +41 -0
- data/lib/basecamp/generated/services/todolists_service.rb +53 -0
- data/lib/basecamp/generated/services/todos_service.rb +106 -0
- data/lib/basecamp/generated/services/todosets_service.rb +20 -0
- data/lib/basecamp/generated/services/tools_service.rb +80 -0
- data/lib/basecamp/generated/services/uploads_service.rb +61 -0
- data/lib/basecamp/generated/services/vaults_service.rb +49 -0
- data/lib/basecamp/generated/services/webhooks_service.rb +63 -0
- data/lib/basecamp/generated/types.rb +3196 -0
- data/lib/basecamp/hooks.rb +70 -0
- data/lib/basecamp/http.rb +440 -0
- data/lib/basecamp/logger_hooks.rb +46 -0
- data/lib/basecamp/noop_hooks.rb +9 -0
- data/lib/basecamp/oauth/discovery.rb +123 -0
- data/lib/basecamp/oauth/errors.rb +35 -0
- data/lib/basecamp/oauth/exchange.rb +291 -0
- data/lib/basecamp/oauth/pkce.rb +68 -0
- data/lib/basecamp/oauth/types.rb +133 -0
- data/lib/basecamp/oauth.rb +56 -0
- data/lib/basecamp/oauth_token_provider.rb +108 -0
- data/lib/basecamp/operation_info.rb +17 -0
- data/lib/basecamp/request_info.rb +10 -0
- data/lib/basecamp/request_result.rb +14 -0
- data/lib/basecamp/security.rb +112 -0
- data/lib/basecamp/services/attachments_service.rb +33 -0
- data/lib/basecamp/services/authorization_service.rb +47 -0
- data/lib/basecamp/services/base_service.rb +146 -0
- data/lib/basecamp/services/campfires_service.rb +141 -0
- data/lib/basecamp/services/card_columns_service.rb +106 -0
- data/lib/basecamp/services/card_steps_service.rb +86 -0
- data/lib/basecamp/services/card_tables_service.rb +23 -0
- data/lib/basecamp/services/cards_service.rb +93 -0
- data/lib/basecamp/services/checkins_service.rb +127 -0
- data/lib/basecamp/services/client_approvals_service.rb +33 -0
- data/lib/basecamp/services/client_correspondences_service.rb +33 -0
- data/lib/basecamp/services/client_replies_service.rb +35 -0
- data/lib/basecamp/services/comments_service.rb +63 -0
- data/lib/basecamp/services/documents_service.rb +74 -0
- data/lib/basecamp/services/events_service.rb +27 -0
- data/lib/basecamp/services/forwards_service.rb +80 -0
- data/lib/basecamp/services/lineup_service.rb +67 -0
- data/lib/basecamp/services/message_boards_service.rb +24 -0
- data/lib/basecamp/services/message_types_service.rb +79 -0
- data/lib/basecamp/services/messages_service.rb +133 -0
- data/lib/basecamp/services/people_service.rb +73 -0
- data/lib/basecamp/services/projects_service.rb +67 -0
- data/lib/basecamp/services/recordings_service.rb +127 -0
- data/lib/basecamp/services/reports_service.rb +80 -0
- data/lib/basecamp/services/schedules_service.rb +156 -0
- data/lib/basecamp/services/search_service.rb +36 -0
- data/lib/basecamp/services/subscriptions_service.rb +67 -0
- data/lib/basecamp/services/templates_service.rb +96 -0
- data/lib/basecamp/services/timeline_service.rb +62 -0
- data/lib/basecamp/services/timesheet_service.rb +68 -0
- data/lib/basecamp/services/todolist_groups_service.rb +100 -0
- data/lib/basecamp/services/todolists_service.rb +104 -0
- data/lib/basecamp/services/todos_service.rb +156 -0
- data/lib/basecamp/services/todosets_service.rb +23 -0
- data/lib/basecamp/services/tools_service.rb +89 -0
- data/lib/basecamp/services/uploads_service.rb +84 -0
- data/lib/basecamp/services/vaults_service.rb +84 -0
- data/lib/basecamp/services/webhooks_service.rb +88 -0
- data/lib/basecamp/static_token_provider.rb +24 -0
- data/lib/basecamp/token_provider.rb +42 -0
- data/lib/basecamp/version.rb +6 -0
- data/lib/basecamp/webhooks/event.rb +52 -0
- data/lib/basecamp/webhooks/rack_middleware.rb +49 -0
- data/lib/basecamp/webhooks/receiver.rb +161 -0
- data/lib/basecamp/webhooks/verify.rb +36 -0
- data/lib/basecamp.rb +107 -0
- data/scripts/generate-metadata.rb +106 -0
- data/scripts/generate-services.rb +778 -0
- data/scripts/generate-types.rb +191 -0
- metadata +316 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Generates Ruby service classes from OpenAPI spec.
|
|
5
|
+
#
|
|
6
|
+
# Usage: ruby scripts/generate-services.rb [--openapi ../openapi.json] [--output lib/basecamp/generated/services]
|
|
7
|
+
#
|
|
8
|
+
# This generator:
|
|
9
|
+
# 1. Parses openapi.json
|
|
10
|
+
# 2. Groups operations by tag
|
|
11
|
+
# 3. Maps operationIds to method names
|
|
12
|
+
# 4. Generates Ruby service files
|
|
13
|
+
|
|
14
|
+
require 'json'
|
|
15
|
+
require 'fileutils'
|
|
16
|
+
|
|
17
|
+
# Service generator for Ruby SDK
|
|
18
|
+
class ServiceGenerator
|
|
19
|
+
METHODS = %w[get post put patch delete].freeze
|
|
20
|
+
|
|
21
|
+
# Schema reference cache for resolving $ref
|
|
22
|
+
attr_reader :schemas
|
|
23
|
+
|
|
24
|
+
# Tag to service name mapping overrides
|
|
25
|
+
TAG_TO_SERVICE = {
|
|
26
|
+
'Card Tables' => 'CardTables',
|
|
27
|
+
'Campfire' => 'Campfires',
|
|
28
|
+
'Todos' => 'Todos',
|
|
29
|
+
'Messages' => 'Messages',
|
|
30
|
+
'Files' => 'Files',
|
|
31
|
+
'Forwards' => 'Forwards',
|
|
32
|
+
'Schedule' => 'Schedules',
|
|
33
|
+
'People' => 'People',
|
|
34
|
+
'Projects' => 'Projects',
|
|
35
|
+
'Automation' => 'Automation',
|
|
36
|
+
'ClientFeatures' => 'ClientFeatures',
|
|
37
|
+
'Boosts' => 'Boosts',
|
|
38
|
+
'Untagged' => 'Miscellaneous'
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Service splits - some tags map to multiple services
|
|
42
|
+
SERVICE_SPLITS = {
|
|
43
|
+
'Campfire' => {
|
|
44
|
+
'Campfires' => %w[
|
|
45
|
+
GetCampfire ListCampfires
|
|
46
|
+
ListChatbots CreateChatbot GetChatbot UpdateChatbot DeleteChatbot
|
|
47
|
+
ListCampfireLines CreateCampfireLine GetCampfireLine DeleteCampfireLine
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
'Card Tables' => {
|
|
51
|
+
'CardTables' => %w[GetCardTable],
|
|
52
|
+
'Cards' => %w[GetCard UpdateCard MoveCard CreateCard ListCards],
|
|
53
|
+
'CardColumns' => %w[
|
|
54
|
+
GetCardColumn UpdateCardColumn SetCardColumnColor
|
|
55
|
+
EnableCardColumnOnHold DisableCardColumnOnHold
|
|
56
|
+
CreateCardColumn MoveCardColumn
|
|
57
|
+
],
|
|
58
|
+
'CardSteps' => %w[
|
|
59
|
+
CreateCardStep UpdateCardStep SetCardStepCompletion
|
|
60
|
+
RepositionCardStep
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
'Files' => {
|
|
64
|
+
'Attachments' => %w[CreateAttachment],
|
|
65
|
+
'Uploads' => %w[GetUpload UpdateUpload ListUploads CreateUpload ListUploadVersions],
|
|
66
|
+
'Vaults' => %w[GetVault UpdateVault ListVaults CreateVault],
|
|
67
|
+
'Documents' => %w[GetDocument UpdateDocument ListDocuments CreateDocument]
|
|
68
|
+
},
|
|
69
|
+
'Automation' => {
|
|
70
|
+
'Tools' => %w[GetTool UpdateTool DeleteTool CloneTool EnableTool DisableTool RepositionTool],
|
|
71
|
+
'Recordings' => %w[GetRecording ArchiveRecording UnarchiveRecording TrashRecording ListRecordings],
|
|
72
|
+
'Webhooks' => %w[ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook],
|
|
73
|
+
'Events' => %w[ListEvents],
|
|
74
|
+
'Lineup' => %w[CreateLineupMarker UpdateLineupMarker DeleteLineupMarker],
|
|
75
|
+
'Search' => %w[Search GetSearchMetadata],
|
|
76
|
+
'Templates' => %w[
|
|
77
|
+
ListTemplates CreateTemplate GetTemplate UpdateTemplate
|
|
78
|
+
DeleteTemplate CreateProjectFromTemplate GetProjectConstruction
|
|
79
|
+
],
|
|
80
|
+
'Checkins' => %w[
|
|
81
|
+
GetQuestionnaire ListQuestions CreateQuestion GetQuestion
|
|
82
|
+
UpdateQuestion ListAnswers CreateAnswer GetAnswer UpdateAnswer
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
'Messages' => {
|
|
86
|
+
'Messages' => %w[GetMessage UpdateMessage CreateMessage ListMessages PinMessage UnpinMessage],
|
|
87
|
+
'MessageBoards' => %w[GetMessageBoard],
|
|
88
|
+
'MessageTypes' => %w[
|
|
89
|
+
ListMessageTypes CreateMessageType GetMessageType
|
|
90
|
+
UpdateMessageType DeleteMessageType
|
|
91
|
+
],
|
|
92
|
+
'Comments' => %w[GetComment UpdateComment ListComments CreateComment]
|
|
93
|
+
},
|
|
94
|
+
'People' => {
|
|
95
|
+
'People' => %w[
|
|
96
|
+
GetMyProfile ListPeople GetPerson ListProjectPeople
|
|
97
|
+
UpdateProjectAccess ListPingablePeople
|
|
98
|
+
],
|
|
99
|
+
'Subscriptions' => %w[GetSubscription Subscribe Unsubscribe UpdateSubscription]
|
|
100
|
+
},
|
|
101
|
+
'Schedule' => {
|
|
102
|
+
'Schedules' => %w[
|
|
103
|
+
GetSchedule UpdateScheduleSettings ListScheduleEntries
|
|
104
|
+
CreateScheduleEntry GetScheduleEntry UpdateScheduleEntry
|
|
105
|
+
GetScheduleEntryOccurrence
|
|
106
|
+
],
|
|
107
|
+
'Timesheets' => %w[GetRecordingTimesheet GetProjectTimesheet GetTimesheetReport GetTimesheetEntry CreateTimesheetEntry UpdateTimesheetEntry]
|
|
108
|
+
},
|
|
109
|
+
'ClientFeatures' => {
|
|
110
|
+
'ClientApprovals' => %w[ListClientApprovals GetClientApproval],
|
|
111
|
+
'ClientCorrespondences' => %w[ListClientCorrespondences GetClientCorrespondence],
|
|
112
|
+
'ClientReplies' => %w[ListClientReplies GetClientReply],
|
|
113
|
+
'ClientVisibility' => %w[SetClientVisibility]
|
|
114
|
+
},
|
|
115
|
+
'Todos' => {
|
|
116
|
+
'Todos' => %w[ListTodos CreateTodo GetTodo UpdateTodo CompleteTodo UncompleteTodo TrashTodo],
|
|
117
|
+
'Todolists' => %w[GetTodolistOrGroup UpdateTodolistOrGroup ListTodolists CreateTodolist],
|
|
118
|
+
'Todosets' => %w[GetTodoset],
|
|
119
|
+
'TodolistGroups' => %w[ListTodolistGroups CreateTodolistGroup RepositionTodolistGroup]
|
|
120
|
+
},
|
|
121
|
+
'Untagged' => {
|
|
122
|
+
'Timeline' => %w[GetProjectTimeline],
|
|
123
|
+
'Reports' => %w[GetProgressReport GetUpcomingSchedule GetAssignedTodos GetOverdueTodos GetPersonProgress],
|
|
124
|
+
'Checkins' => %w[
|
|
125
|
+
GetQuestionReminders ListQuestionAnswerers GetAnswersByPerson
|
|
126
|
+
UpdateQuestionNotificationSettings PauseQuestion ResumeQuestion
|
|
127
|
+
],
|
|
128
|
+
'Todos' => %w[RepositionTodo],
|
|
129
|
+
'People' => %w[ListAssignablePeople],
|
|
130
|
+
'CardColumns' => %w[SubscribeToCardColumn UnsubscribeFromCardColumn]
|
|
131
|
+
}
|
|
132
|
+
}.freeze
|
|
133
|
+
|
|
134
|
+
# Method name overrides
|
|
135
|
+
METHOD_NAME_OVERRIDES = {
|
|
136
|
+
'GetMyProfile' => 'my_profile',
|
|
137
|
+
'GetTodolistOrGroup' => 'get',
|
|
138
|
+
'UpdateTodolistOrGroup' => 'update',
|
|
139
|
+
'SetCardColumnColor' => 'set_color',
|
|
140
|
+
'EnableCardColumnOnHold' => 'enable_on_hold',
|
|
141
|
+
'DisableCardColumnOnHold' => 'disable_on_hold',
|
|
142
|
+
'RepositionCardStep' => 'reposition',
|
|
143
|
+
'CreateCardStep' => 'create',
|
|
144
|
+
'UpdateCardStep' => 'update',
|
|
145
|
+
'SetCardStepCompletion' => 'set_completion',
|
|
146
|
+
'GetQuestionnaire' => 'get_questionnaire',
|
|
147
|
+
'GetQuestion' => 'get_question',
|
|
148
|
+
'GetAnswer' => 'get_answer',
|
|
149
|
+
'ListQuestions' => 'list_questions',
|
|
150
|
+
'ListAnswers' => 'list_answers',
|
|
151
|
+
'CreateQuestion' => 'create_question',
|
|
152
|
+
'CreateAnswer' => 'create_answer',
|
|
153
|
+
'UpdateQuestion' => 'update_question',
|
|
154
|
+
'UpdateAnswer' => 'update_answer',
|
|
155
|
+
'GetQuestionReminders' => 'reminders',
|
|
156
|
+
'GetAnswersByPerson' => 'by_person',
|
|
157
|
+
'ListQuestionAnswerers' => 'answerers',
|
|
158
|
+
'UpdateQuestionNotificationSettings' => 'update_notification_settings',
|
|
159
|
+
'PauseQuestion' => 'pause',
|
|
160
|
+
'ResumeQuestion' => 'resume',
|
|
161
|
+
'GetSearchMetadata' => 'metadata',
|
|
162
|
+
'Search' => 'search',
|
|
163
|
+
'CreateProjectFromTemplate' => 'create_project',
|
|
164
|
+
'GetProjectConstruction' => 'get_construction',
|
|
165
|
+
'GetRecordingTimesheet' => 'for_recording',
|
|
166
|
+
'GetProjectTimesheet' => 'for_project',
|
|
167
|
+
'GetTimesheetReport' => 'report',
|
|
168
|
+
'GetTimesheetEntry' => 'get',
|
|
169
|
+
'CreateTimesheetEntry' => 'create',
|
|
170
|
+
'UpdateTimesheetEntry' => 'update',
|
|
171
|
+
'GetProgressReport' => 'progress',
|
|
172
|
+
'GetUpcomingSchedule' => 'upcoming',
|
|
173
|
+
'GetAssignedTodos' => 'assigned',
|
|
174
|
+
'GetOverdueTodos' => 'overdue',
|
|
175
|
+
'GetPersonProgress' => 'person_progress',
|
|
176
|
+
'SubscribeToCardColumn' => 'subscribe_to_column',
|
|
177
|
+
'UnsubscribeFromCardColumn' => 'unsubscribe_from_column',
|
|
178
|
+
'SetClientVisibility' => 'set_visibility',
|
|
179
|
+
# Campfires - use specific names to avoid conflicts between campfire, chatbots, and lines
|
|
180
|
+
'GetCampfire' => 'get',
|
|
181
|
+
'ListCampfires' => 'list',
|
|
182
|
+
'ListChatbots' => 'list_chatbots',
|
|
183
|
+
'CreateChatbot' => 'create_chatbot',
|
|
184
|
+
'GetChatbot' => 'get_chatbot',
|
|
185
|
+
'UpdateChatbot' => 'update_chatbot',
|
|
186
|
+
'DeleteChatbot' => 'delete_chatbot',
|
|
187
|
+
'ListCampfireLines' => 'list_lines',
|
|
188
|
+
'CreateCampfireLine' => 'create_line',
|
|
189
|
+
'GetCampfireLine' => 'get_line',
|
|
190
|
+
'DeleteCampfireLine' => 'delete_line',
|
|
191
|
+
# Forwards - use specific names to avoid conflicts between forwards, replies, and inbox
|
|
192
|
+
'GetForward' => 'get',
|
|
193
|
+
'ListForwards' => 'list',
|
|
194
|
+
'GetForwardReply' => 'get_reply',
|
|
195
|
+
'ListForwardReplies' => 'list_replies',
|
|
196
|
+
'CreateForwardReply' => 'create_reply',
|
|
197
|
+
'GetInbox' => 'get_inbox',
|
|
198
|
+
# Uploads - use specific names to avoid conflicts with versions
|
|
199
|
+
'GetUpload' => 'get',
|
|
200
|
+
'UpdateUpload' => 'update',
|
|
201
|
+
'ListUploads' => 'list',
|
|
202
|
+
'CreateUpload' => 'create',
|
|
203
|
+
'ListUploadVersions' => 'list_versions',
|
|
204
|
+
'GetMessage' => 'get',
|
|
205
|
+
'UpdateMessage' => 'update',
|
|
206
|
+
'CreateMessage' => 'create',
|
|
207
|
+
'ListMessages' => 'list',
|
|
208
|
+
'PinMessage' => 'pin',
|
|
209
|
+
'UnpinMessage' => 'unpin',
|
|
210
|
+
'GetMessageBoard' => 'get',
|
|
211
|
+
'GetMessageType' => 'get',
|
|
212
|
+
'UpdateMessageType' => 'update',
|
|
213
|
+
'CreateMessageType' => 'create',
|
|
214
|
+
'ListMessageTypes' => 'list',
|
|
215
|
+
'DeleteMessageType' => 'delete',
|
|
216
|
+
'GetComment' => 'get',
|
|
217
|
+
'UpdateComment' => 'update',
|
|
218
|
+
'CreateComment' => 'create',
|
|
219
|
+
'ListComments' => 'list',
|
|
220
|
+
'ListProjectPeople' => 'list_for_project',
|
|
221
|
+
'ListPingablePeople' => 'list_pingable',
|
|
222
|
+
'ListAssignablePeople' => 'list_assignable',
|
|
223
|
+
'GetSchedule' => 'get',
|
|
224
|
+
'UpdateScheduleSettings' => 'update_settings',
|
|
225
|
+
'GetScheduleEntry' => 'get_entry',
|
|
226
|
+
'UpdateScheduleEntry' => 'update_entry',
|
|
227
|
+
'CreateScheduleEntry' => 'create_entry',
|
|
228
|
+
'ListScheduleEntries' => 'list_entries',
|
|
229
|
+
'GetScheduleEntryOccurrence' => 'get_entry_occurrence'
|
|
230
|
+
}.freeze
|
|
231
|
+
|
|
232
|
+
# Verb patterns for extracting method names
|
|
233
|
+
VERB_PATTERNS = [
|
|
234
|
+
{ prefix: 'Subscribe', method: 'subscribe' },
|
|
235
|
+
{ prefix: 'Unsubscribe', method: 'unsubscribe' },
|
|
236
|
+
{ prefix: 'List', method: 'list' },
|
|
237
|
+
{ prefix: 'Get', method: 'get' },
|
|
238
|
+
{ prefix: 'Create', method: 'create' },
|
|
239
|
+
{ prefix: 'Update', method: 'update' },
|
|
240
|
+
{ prefix: 'Delete', method: 'delete' },
|
|
241
|
+
{ prefix: 'Trash', method: 'trash' },
|
|
242
|
+
{ prefix: 'Archive', method: 'archive' },
|
|
243
|
+
{ prefix: 'Unarchive', method: 'unarchive' },
|
|
244
|
+
{ prefix: 'Complete', method: 'complete' },
|
|
245
|
+
{ prefix: 'Uncomplete', method: 'uncomplete' },
|
|
246
|
+
{ prefix: 'Enable', method: 'enable' },
|
|
247
|
+
{ prefix: 'Disable', method: 'disable' },
|
|
248
|
+
{ prefix: 'Reposition', method: 'reposition' },
|
|
249
|
+
{ prefix: 'Move', method: 'move' },
|
|
250
|
+
{ prefix: 'Clone', method: 'clone' },
|
|
251
|
+
{ prefix: 'Set', method: 'set' },
|
|
252
|
+
{ prefix: 'Pin', method: 'pin' },
|
|
253
|
+
{ prefix: 'Unpin', method: 'unpin' },
|
|
254
|
+
{ prefix: 'Pause', method: 'pause' },
|
|
255
|
+
{ prefix: 'Resume', method: 'resume' },
|
|
256
|
+
{ prefix: 'Search', method: 'search' }
|
|
257
|
+
].freeze
|
|
258
|
+
|
|
259
|
+
SIMPLE_RESOURCES = %w[
|
|
260
|
+
todo todos todolist todolists todoset message messages comment comments
|
|
261
|
+
card cards cardtable cardcolumn cardstep column step project projects
|
|
262
|
+
person people campfire campfires chatbot chatbots webhook webhooks
|
|
263
|
+
vault vaults document documents upload uploads schedule scheduleentry
|
|
264
|
+
scheduleentries event events recording recordings template templates
|
|
265
|
+
attachment question questions answer answers questionnaire subscription
|
|
266
|
+
forward forwards inbox messageboard messagetype messagetypes tool
|
|
267
|
+
lineupmarker clientapproval clientapprovals clientcorrespondence
|
|
268
|
+
clientcorrespondences clientreply clientreplies forwardreply
|
|
269
|
+
forwardreplies campfireline campfirelines todolistgroup todolistgroups
|
|
270
|
+
todolistorgroup uploadversions
|
|
271
|
+
].freeze
|
|
272
|
+
|
|
273
|
+
def initialize(openapi_path)
|
|
274
|
+
@openapi = JSON.parse(File.read(openapi_path))
|
|
275
|
+
@schemas = @openapi.dig('components', 'schemas') || {}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def generate(output_dir)
|
|
279
|
+
FileUtils.mkdir_p(output_dir)
|
|
280
|
+
|
|
281
|
+
services = group_operations
|
|
282
|
+
generated_files = []
|
|
283
|
+
|
|
284
|
+
services.each do |name, service|
|
|
285
|
+
code = generate_service(service)
|
|
286
|
+
filename = "#{to_snake_case(name)}_service.rb"
|
|
287
|
+
filepath = File.join(output_dir, filename)
|
|
288
|
+
File.write(filepath, code)
|
|
289
|
+
generated_files << filename
|
|
290
|
+
puts "Generated #{filename} (#{service[:operations].length} operations)"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
puts "\nGenerated #{services.length} services with #{services.values.sum { |s| s[:operations].length }} operations total."
|
|
294
|
+
generated_files
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def group_operations
|
|
300
|
+
services = {}
|
|
301
|
+
|
|
302
|
+
@openapi['paths'].each do |path, path_item|
|
|
303
|
+
METHODS.each do |method|
|
|
304
|
+
operation = path_item[method]
|
|
305
|
+
next unless operation
|
|
306
|
+
|
|
307
|
+
tag = operation['tags']&.first || 'Untagged'
|
|
308
|
+
parsed = parse_operation(path, method, operation)
|
|
309
|
+
|
|
310
|
+
# Determine which service this operation belongs to
|
|
311
|
+
service_name = find_service_for_operation(tag, operation['operationId'])
|
|
312
|
+
|
|
313
|
+
services[service_name] ||= {
|
|
314
|
+
name: service_name,
|
|
315
|
+
class_name: "#{service_name}Service",
|
|
316
|
+
description: "Service for #{service_name} operations",
|
|
317
|
+
operations: []
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
services[service_name][:operations] << parsed
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
services
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def find_service_for_operation(tag, operation_id)
|
|
328
|
+
if SERVICE_SPLITS[tag]
|
|
329
|
+
SERVICE_SPLITS[tag].each do |svc, op_ids|
|
|
330
|
+
return svc if op_ids.include?(operation_id)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
TAG_TO_SERVICE[tag] || tag.gsub(/\s+/, '')
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def parse_operation(path, method, operation)
|
|
338
|
+
operation_id = operation['operationId']
|
|
339
|
+
method_name = extract_method_name(operation_id)
|
|
340
|
+
http_method = method.upcase
|
|
341
|
+
description = operation['description']&.lines&.first&.strip || "#{method_name} operation"
|
|
342
|
+
|
|
343
|
+
# Extract path parameters (excluding accountId)
|
|
344
|
+
path_params = (operation['parameters'] || [])
|
|
345
|
+
.select { |p| p['in'] == 'path' && p['name'] != 'accountId' }
|
|
346
|
+
.map { |p| { name: p['name'], type: schema_to_ruby_type(p['schema']), description: p['description'] } }
|
|
347
|
+
|
|
348
|
+
# Extract query parameters
|
|
349
|
+
query_params = (operation['parameters'] || [])
|
|
350
|
+
.select { |p| p['in'] == 'query' }
|
|
351
|
+
.map do |p|
|
|
352
|
+
{
|
|
353
|
+
name: p['name'],
|
|
354
|
+
type: schema_to_ruby_type(p['schema']),
|
|
355
|
+
required: p['required'] || false,
|
|
356
|
+
description: p['description']
|
|
357
|
+
}
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Check for request body (JSON or binary)
|
|
361
|
+
body_schema_ref = operation.dig('requestBody', 'content', 'application/json', 'schema')
|
|
362
|
+
has_binary_body = operation.dig('requestBody', 'content', 'application/octet-stream', 'schema')
|
|
363
|
+
|
|
364
|
+
# Extract body parameters from schema
|
|
365
|
+
body_params = extract_body_params(body_schema_ref)
|
|
366
|
+
|
|
367
|
+
# Check response
|
|
368
|
+
success_response = operation.dig('responses', '200') || operation.dig('responses', '201')
|
|
369
|
+
response_schema = success_response&.dig('content', 'application/json', 'schema')
|
|
370
|
+
returns_void = response_schema.nil?
|
|
371
|
+
returns_array = response_schema&.dig('type') == 'array'
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
operation_id: operation_id,
|
|
375
|
+
method_name: method_name,
|
|
376
|
+
http_method: http_method,
|
|
377
|
+
path: convert_path(path),
|
|
378
|
+
description: description,
|
|
379
|
+
path_params: path_params,
|
|
380
|
+
query_params: query_params,
|
|
381
|
+
body_params: body_params,
|
|
382
|
+
has_body: body_params.any?,
|
|
383
|
+
has_binary_body: !!has_binary_body,
|
|
384
|
+
returns_void: returns_void,
|
|
385
|
+
returns_array: returns_array,
|
|
386
|
+
is_mutation: http_method != 'GET',
|
|
387
|
+
has_pagination: !!operation['x-basecamp-pagination']
|
|
388
|
+
}
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Extract body parameters from a schema reference
|
|
392
|
+
def extract_body_params(schema_ref)
|
|
393
|
+
return [] unless schema_ref
|
|
394
|
+
|
|
395
|
+
# Resolve $ref
|
|
396
|
+
schema = resolve_schema_ref(schema_ref)
|
|
397
|
+
return [] unless schema && schema['properties']
|
|
398
|
+
|
|
399
|
+
required_fields = schema['required'] || []
|
|
400
|
+
|
|
401
|
+
schema['properties'].map do |name, prop|
|
|
402
|
+
type = schema_to_ruby_type(prop)
|
|
403
|
+
format_hint = extract_format_hint(prop)
|
|
404
|
+
{
|
|
405
|
+
name: name,
|
|
406
|
+
type: type,
|
|
407
|
+
required: required_fields.include?(name),
|
|
408
|
+
description: prop['description'],
|
|
409
|
+
format_hint: format_hint
|
|
410
|
+
}
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Resolve a schema reference to its definition
|
|
415
|
+
def resolve_schema_ref(schema_or_ref)
|
|
416
|
+
return schema_or_ref unless schema_or_ref['$ref']
|
|
417
|
+
|
|
418
|
+
ref_path = schema_or_ref['$ref']
|
|
419
|
+
# Handle #/components/schemas/SchemaName format
|
|
420
|
+
if ref_path.start_with?('#/components/schemas/')
|
|
421
|
+
schema_name = ref_path.split('/').last
|
|
422
|
+
@schemas[schema_name]
|
|
423
|
+
else
|
|
424
|
+
nil
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Extract format hint for documentation
|
|
429
|
+
def extract_format_hint(prop)
|
|
430
|
+
return nil unless prop
|
|
431
|
+
|
|
432
|
+
# Check for x-go-type hints (dates)
|
|
433
|
+
case prop['x-go-type']
|
|
434
|
+
when 'types.Date'
|
|
435
|
+
return 'YYYY-MM-DD'
|
|
436
|
+
when 'types.DateTime', 'time.Time'
|
|
437
|
+
return 'RFC3339 (e.g., 2024-12-15T09:00:00Z)'
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Check for format field
|
|
441
|
+
case prop['format']
|
|
442
|
+
when 'date'
|
|
443
|
+
'YYYY-MM-DD'
|
|
444
|
+
when 'date-time'
|
|
445
|
+
'RFC3339 (e.g., 2024-12-15T09:00:00Z)'
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def extract_method_name(operation_id)
|
|
450
|
+
return METHOD_NAME_OVERRIDES[operation_id] if METHOD_NAME_OVERRIDES.key?(operation_id)
|
|
451
|
+
|
|
452
|
+
VERB_PATTERNS.each do |pattern|
|
|
453
|
+
if operation_id.start_with?(pattern[:prefix])
|
|
454
|
+
remainder = operation_id[pattern[:prefix].length..]
|
|
455
|
+
return pattern[:method] if remainder.empty?
|
|
456
|
+
|
|
457
|
+
resource = to_snake_case(remainder)
|
|
458
|
+
return pattern[:method] if simple_resource?(resource)
|
|
459
|
+
|
|
460
|
+
return "#{pattern[:method]}_#{resource}"
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
to_snake_case(operation_id)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def simple_resource?(resource)
|
|
468
|
+
SIMPLE_RESOURCES.include?(resource.downcase.gsub('_', ''))
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def convert_path(path)
|
|
472
|
+
# Remove /{accountId} prefix
|
|
473
|
+
path = path.sub(%r{^/\{accountId\}}, '')
|
|
474
|
+
# Convert {camelCaseParam} to #{snake_case_param}
|
|
475
|
+
path.gsub(/\{(\w+)\}/) do |_match|
|
|
476
|
+
param = ::Regexp.last_match(1)
|
|
477
|
+
snake_param = to_snake_case(param)
|
|
478
|
+
"\#{#{snake_param}}"
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def schema_to_ruby_type(schema)
|
|
483
|
+
return 'Object' unless schema
|
|
484
|
+
|
|
485
|
+
case schema['type']
|
|
486
|
+
when 'integer' then 'Integer'
|
|
487
|
+
when 'boolean' then 'Boolean'
|
|
488
|
+
when 'array' then 'Array'
|
|
489
|
+
else 'String'
|
|
490
|
+
end
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def to_snake_case(str)
|
|
494
|
+
str.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
495
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
496
|
+
.downcase
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def generate_service(service)
|
|
500
|
+
lines = []
|
|
501
|
+
|
|
502
|
+
# Check if any operation uses URI encoding (binary uploads with query params)
|
|
503
|
+
needs_uri = service[:operations].any? { |op| op[:has_binary_body] && op[:query_params].any? }
|
|
504
|
+
|
|
505
|
+
lines << '# frozen_string_literal: true'
|
|
506
|
+
lines << ''
|
|
507
|
+
lines << 'require "uri"' if needs_uri
|
|
508
|
+
lines << '' if needs_uri
|
|
509
|
+
lines << 'module Basecamp'
|
|
510
|
+
lines << ' module Services'
|
|
511
|
+
lines << " # #{service[:description]}"
|
|
512
|
+
lines << ' #'
|
|
513
|
+
lines << ' # @generated from OpenAPI spec'
|
|
514
|
+
lines << " class #{service[:class_name]} < BaseService"
|
|
515
|
+
|
|
516
|
+
service[:operations].each do |op|
|
|
517
|
+
lines << ''
|
|
518
|
+
lines.concat(generate_method(op, service_name: service[:name]))
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
lines << ' end'
|
|
522
|
+
lines << ' end'
|
|
523
|
+
lines << 'end'
|
|
524
|
+
lines << ''
|
|
525
|
+
|
|
526
|
+
lines.join("\n")
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def generate_method(op, service_name:)
|
|
530
|
+
lines = []
|
|
531
|
+
|
|
532
|
+
# Method signature
|
|
533
|
+
params = build_params(op)
|
|
534
|
+
|
|
535
|
+
# YARD documentation
|
|
536
|
+
lines << " # #{op[:description]}"
|
|
537
|
+
|
|
538
|
+
# Add @param tags for path params
|
|
539
|
+
op[:path_params].each do |p|
|
|
540
|
+
ruby_name = to_snake_case(p[:name])
|
|
541
|
+
type = p[:type] || 'Integer'
|
|
542
|
+
desc = p[:description] || "#{ruby_name.gsub('_', ' ')} ID"
|
|
543
|
+
lines << " # @param #{ruby_name} [#{type}] #{desc}"
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Add @param tags for binary upload params
|
|
547
|
+
if op[:has_binary_body]
|
|
548
|
+
lines << ' # @param data [String] Binary file data to upload'
|
|
549
|
+
lines << ' # @param content_type [String] MIME type of the file (e.g., "application/pdf", "image/png")'
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# Add @param tags for body params
|
|
553
|
+
if op[:body_params]&.any?
|
|
554
|
+
op[:body_params].each do |b|
|
|
555
|
+
ruby_name = to_snake_case(b[:name])
|
|
556
|
+
type = b[:type] || 'Object'
|
|
557
|
+
type = "#{type}, nil" unless b[:required]
|
|
558
|
+
desc = b[:description] || ruby_name.gsub('_', ' ')
|
|
559
|
+
format_hint = b[:format_hint] ? " (#{b[:format_hint]})" : ''
|
|
560
|
+
lines << " # @param #{ruby_name} [#{type}] #{desc}#{format_hint}"
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Add @param tags for query params
|
|
565
|
+
op[:query_params].each do |q|
|
|
566
|
+
ruby_name = to_snake_case(q[:name])
|
|
567
|
+
type = q[:type] || 'String'
|
|
568
|
+
type = "#{type}, nil" unless q[:required]
|
|
569
|
+
desc = q[:description] || ruby_name.gsub('_', ' ')
|
|
570
|
+
lines << " # @param #{ruby_name} [#{type}] #{desc}"
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Add @return tag
|
|
574
|
+
if op[:returns_void]
|
|
575
|
+
lines << ' # @return [void]'
|
|
576
|
+
elsif op[:returns_array] || op[:has_pagination]
|
|
577
|
+
lines << ' # @return [Enumerator<Hash>] paginated results'
|
|
578
|
+
else
|
|
579
|
+
lines << ' # @return [Hash] response data'
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
lines << " def #{op[:method_name]}(#{params})"
|
|
583
|
+
|
|
584
|
+
# Build the path
|
|
585
|
+
path_expr = build_path_expression(op)
|
|
586
|
+
|
|
587
|
+
is_paginated = op[:returns_array] || op[:has_pagination]
|
|
588
|
+
hook_kwargs = build_hook_kwargs(op, service_name)
|
|
589
|
+
|
|
590
|
+
if is_paginated
|
|
591
|
+
# wrap_paginated defers hooks to actual iteration time (lazy-safe)
|
|
592
|
+
lines << " wrap_paginated(#{hook_kwargs}) do"
|
|
593
|
+
body_lines = generate_list_method_body(op, path_expr)
|
|
594
|
+
body_lines.each { |l| lines << " #{l}" }
|
|
595
|
+
lines << ' end'
|
|
596
|
+
else
|
|
597
|
+
lines << " with_operation(#{hook_kwargs}) do"
|
|
598
|
+
|
|
599
|
+
body_lines = if op[:returns_void]
|
|
600
|
+
generate_void_method_body(op, path_expr)
|
|
601
|
+
else
|
|
602
|
+
generate_get_method_body(op, path_expr)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
body_lines.each { |l| lines << " #{l}" }
|
|
606
|
+
lines << ' end'
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
lines << ' end'
|
|
610
|
+
lines
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def build_hook_kwargs(op, service_name)
|
|
614
|
+
kwargs = []
|
|
615
|
+
kwargs << "service: \"#{service_name.downcase}\""
|
|
616
|
+
kwargs << "operation: \"#{op[:method_name]}\""
|
|
617
|
+
kwargs << "is_mutation: #{op[:is_mutation]}"
|
|
618
|
+
|
|
619
|
+
project_param = op[:path_params].find { |p| p[:name] == 'projectId' }
|
|
620
|
+
resource_param = op[:path_params].reject { |p| p[:name] == 'projectId' }.last
|
|
621
|
+
|
|
622
|
+
kwargs << "project_id: project_id" if project_param
|
|
623
|
+
kwargs << "resource_id: #{to_snake_case(resource_param[:name])}" if resource_param
|
|
624
|
+
|
|
625
|
+
kwargs.join(', ')
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def build_params(op)
|
|
629
|
+
params = []
|
|
630
|
+
|
|
631
|
+
# Path parameters as keyword args
|
|
632
|
+
op[:path_params].each do |p|
|
|
633
|
+
params << "#{to_snake_case(p[:name])}:"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Binary upload parameters
|
|
637
|
+
if op[:has_binary_body]
|
|
638
|
+
params << 'data:'
|
|
639
|
+
params << 'content_type:'
|
|
640
|
+
elsif op[:has_body]
|
|
641
|
+
# Request body parameters as explicit keyword args (not **body)
|
|
642
|
+
# Required body params first (no default), then optional (with nil default)
|
|
643
|
+
required_body_params = op[:body_params].select { |b| b[:required] }
|
|
644
|
+
optional_body_params = op[:body_params].reject { |b| b[:required] }
|
|
645
|
+
|
|
646
|
+
required_body_params.each do |b|
|
|
647
|
+
params << "#{to_snake_case(b[:name])}:"
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
optional_body_params.each do |b|
|
|
651
|
+
params << "#{to_snake_case(b[:name])}: nil"
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Query parameters - required first (no default), then optional (with nil default)
|
|
656
|
+
required_query_params = op[:query_params].select { |q| q[:required] }
|
|
657
|
+
optional_query_params = op[:query_params].reject { |q| q[:required] }
|
|
658
|
+
|
|
659
|
+
required_query_params.each do |q|
|
|
660
|
+
params << "#{to_snake_case(q[:name])}:"
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
optional_query_params.each do |q|
|
|
664
|
+
params << "#{to_snake_case(q[:name])}: nil"
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
params.join(', ')
|
|
668
|
+
end
|
|
669
|
+
|
|
670
|
+
# Build body hash expression from explicit body params
|
|
671
|
+
def build_body_expression(op)
|
|
672
|
+
return '{}' unless op[:body_params]&.any?
|
|
673
|
+
|
|
674
|
+
# Build compact_params call with all body params
|
|
675
|
+
param_mappings = op[:body_params].map do |b|
|
|
676
|
+
ruby_name = to_snake_case(b[:name])
|
|
677
|
+
# Use original API name as key (snake_case), ruby variable as value
|
|
678
|
+
"#{b[:name]}: #{ruby_name}"
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
"compact_params(#{param_mappings.join(', ')})"
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def build_path_expression(op)
|
|
685
|
+
"\"#{op[:path]}\""
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
def generate_void_method_body(op, path_expr)
|
|
689
|
+
lines = []
|
|
690
|
+
http_method = op[:http_method].downcase
|
|
691
|
+
|
|
692
|
+
if op[:has_body]
|
|
693
|
+
body_expr = build_body_expression(op)
|
|
694
|
+
lines << " http_#{http_method}(#{path_expr}, body: #{body_expr})"
|
|
695
|
+
else
|
|
696
|
+
lines << " http_#{http_method}(#{path_expr})"
|
|
697
|
+
end
|
|
698
|
+
lines << ' nil'
|
|
699
|
+
lines
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def generate_list_method_body(op, path_expr)
|
|
703
|
+
lines = []
|
|
704
|
+
|
|
705
|
+
# Build params hash for query params
|
|
706
|
+
if op[:query_params].any?
|
|
707
|
+
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
708
|
+
lines << " params = compact_params(#{param_names.join(', ')})"
|
|
709
|
+
lines << " paginate(#{path_expr}, params: params)"
|
|
710
|
+
else
|
|
711
|
+
lines << " paginate(#{path_expr})"
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
lines
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def generate_get_method_body(op, path_expr)
|
|
718
|
+
lines = []
|
|
719
|
+
http_method = op[:http_method].downcase
|
|
720
|
+
|
|
721
|
+
if op[:has_binary_body]
|
|
722
|
+
# Binary upload - use raw body and set Content-Type header
|
|
723
|
+
# post_raw accepts (path, body:, content_type:) - no params keyword
|
|
724
|
+
# Query params must be embedded in the URL
|
|
725
|
+
if op[:query_params].any?
|
|
726
|
+
# Build URL with query string
|
|
727
|
+
query_parts = op[:query_params].map { |q| "#{q[:name]}=\#{URI.encode_www_form_component(#{to_snake_case(q[:name])}.to_s)}" }
|
|
728
|
+
query_string = query_parts.join('&')
|
|
729
|
+
# Modify path_expr to include query string
|
|
730
|
+
path_expr_with_query = path_expr.sub(/"$/, "?#{query_string}\"")
|
|
731
|
+
lines << " http_#{http_method}_raw(#{path_expr_with_query}, body: data, content_type: content_type).json"
|
|
732
|
+
else
|
|
733
|
+
lines << " http_#{http_method}_raw(#{path_expr}, body: data, content_type: content_type).json"
|
|
734
|
+
end
|
|
735
|
+
elsif op[:has_body]
|
|
736
|
+
body_expr = build_body_expression(op)
|
|
737
|
+
lines << " http_#{http_method}(#{path_expr}, body: #{body_expr}).json"
|
|
738
|
+
elsif op[:query_params].any?
|
|
739
|
+
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
740
|
+
lines << " http_#{http_method}(#{path_expr}, params: compact_params(#{param_names.join(', ')})).json"
|
|
741
|
+
else
|
|
742
|
+
lines << " http_#{http_method}(#{path_expr}).json"
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
lines
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Main execution
|
|
750
|
+
if __FILE__ == $PROGRAM_NAME
|
|
751
|
+
openapi_path = nil
|
|
752
|
+
output_dir = nil
|
|
753
|
+
|
|
754
|
+
i = 0
|
|
755
|
+
while i < ARGV.length
|
|
756
|
+
case ARGV[i]
|
|
757
|
+
when '--openapi'
|
|
758
|
+
openapi_path = ARGV[i + 1]
|
|
759
|
+
i += 2
|
|
760
|
+
when '--output'
|
|
761
|
+
output_dir = ARGV[i + 1]
|
|
762
|
+
i += 2
|
|
763
|
+
else
|
|
764
|
+
i += 1
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
openapi_path ||= File.expand_path('../../openapi.json', __dir__)
|
|
769
|
+
output_dir ||= File.expand_path('../lib/basecamp/generated/services', __dir__)
|
|
770
|
+
|
|
771
|
+
unless File.exist?(openapi_path)
|
|
772
|
+
warn "Error: OpenAPI file not found: #{openapi_path}"
|
|
773
|
+
exit 1
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
generator = ServiceGenerator.new(openapi_path)
|
|
777
|
+
generator.generate(output_dir)
|
|
778
|
+
end
|