basecamp-sdk 0.7.3 → 0.9.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 +65 -1
- data/basecamp-sdk.gemspec +2 -3
- data/lib/basecamp/client.rb +5 -0
- data/lib/basecamp/generated/metadata.json +100 -33
- data/lib/basecamp/generated/services/base_service.rb +13 -0
- data/lib/basecamp/generated/services/campfires_service.rb +15 -3
- data/lib/basecamp/generated/services/card_columns_service.rb +26 -23
- data/lib/basecamp/generated/services/checkins_service.rb +5 -4
- data/lib/basecamp/generated/services/client_approvals_service.rb +1 -1
- data/lib/basecamp/generated/services/client_correspondences_service.rb +1 -1
- data/lib/basecamp/generated/services/documents_service.rb +3 -2
- data/lib/basecamp/generated/services/forwards_service.rb +1 -1
- data/lib/basecamp/generated/services/gauges_service.rb +4 -4
- data/lib/basecamp/generated/services/messages_service.rb +4 -3
- data/lib/basecamp/generated/services/my_assignments_service.rb +1 -1
- data/lib/basecamp/generated/services/my_notifications_service.rb +1 -1
- data/lib/basecamp/generated/services/people_service.rb +2 -2
- data/lib/basecamp/generated/services/projects_service.rb +2 -2
- data/lib/basecamp/generated/services/recordings_service.rb +1 -1
- data/lib/basecamp/generated/services/reports_service.rb +3 -3
- data/lib/basecamp/generated/services/schedules_service.rb +5 -4
- data/lib/basecamp/generated/services/search_service.rb +14 -3
- data/lib/basecamp/generated/services/templates_service.rb +4 -5
- data/lib/basecamp/generated/services/timesheets_service.rb +3 -3
- data/lib/basecamp/generated/services/todolists_service.rb +15 -3
- data/lib/basecamp/generated/services/todos_service.rb +5 -5
- data/lib/basecamp/generated/services/tools_service.rb +7 -6
- data/lib/basecamp/generated/services/uploads_service.rb +20 -2
- data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
- data/lib/basecamp/generated/services/wormholes_service.rb +44 -0
- data/lib/basecamp/generated/types.rb +284 -64
- data/lib/basecamp/http.rb +86 -15
- data/lib/basecamp/oauth/config.rb +20 -5
- data/lib/basecamp/oauth/discovery.rb +134 -64
- data/lib/basecamp/oauth/discovery_result.rb +33 -0
- data/lib/basecamp/oauth/discovery_selection_error.rb +30 -0
- data/lib/basecamp/oauth/fetcher.rb +204 -0
- data/lib/basecamp/oauth/protected_resource_metadata.rb +22 -0
- data/lib/basecamp/oauth/resource.rb +95 -0
- data/lib/basecamp/oauth.rb +173 -0
- data/lib/basecamp/security.rb +83 -0
- data/lib/basecamp/services/authorization_service.rb +45 -0
- data/lib/basecamp/services/todos_extensions.rb +121 -0
- data/lib/basecamp/version.rb +2 -2
- data/lib/basecamp.rb +6 -0
- data/scripts/generate-services.rb +61 -13
- data/scripts/generate-types.rb +33 -1
- metadata +14 -21
- data/lib/basecamp/generated/services/authorization_service.rb +0 -47
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Basecamp
|
|
4
|
+
module Services
|
|
5
|
+
# Merge-safe +update+ and read-modify-write +edit+ for todos, prepended
|
|
6
|
+
# onto the generated {TodosService} (see the +on_load+ hook in
|
|
7
|
+
# +basecamp.rb+).
|
|
8
|
+
#
|
|
9
|
+
# Both compose the public +get+ and +replace+ methods, so hooks observe
|
|
10
|
+
# the two wire operations (+get+ then +replace+), not a synthetic
|
|
11
|
+
# composite.
|
|
12
|
+
#
|
|
13
|
+
# Neither is atomic: there is no conditional-update signal on this
|
|
14
|
+
# endpoint, so a concurrent write between the GET and PUT is
|
|
15
|
+
# overwritten — last write wins for the whole representation. The
|
|
16
|
+
# window is one round-trip. Use +replace+ to overwrite deliberately.
|
|
17
|
+
module TodosExtensions
|
|
18
|
+
# A todo's full writable state, yielded to the +edit+ block. The
|
|
19
|
+
# whole struct is PUT back to the server, so clearing a field means
|
|
20
|
+
# setting it empty (+""+ for strings and dates, +[]+ for ID lists) —
|
|
21
|
+
# there is no third state. +notify+ is a send directive, not todo
|
|
22
|
+
# state: never populated from the current todo, sent only when true.
|
|
23
|
+
TodoFields = Struct.new(
|
|
24
|
+
:content, :description, :assignee_ids, :completion_subscriber_ids,
|
|
25
|
+
:due_on, :starts_on, :notify,
|
|
26
|
+
keyword_init: true
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Sets the given fields on a todo and preserves everything else:
|
|
30
|
+
# GETs the current todo, overlays the explicitly-passed keyword
|
|
31
|
+
# arguments, and PUTs the full representation back. An omitted
|
|
32
|
+
# (+nil+) field is untouched, guaranteed; an explicitly-passed empty
|
|
33
|
+
# array clears.
|
|
34
|
+
#
|
|
35
|
+
# Not atomic — see the module docs for the GET→PUT race. Use
|
|
36
|
+
# {#replace} to overwrite deliberately, or {#edit} to clear fields.
|
|
37
|
+
#
|
|
38
|
+
# @param todo_id [Integer] todo id
|
|
39
|
+
# @param content [String, nil] new content (nil = keep current)
|
|
40
|
+
# @param description [String, nil] new description (nil = keep current)
|
|
41
|
+
# @param assignee_ids [Array, nil] complete assignee list ([] clears)
|
|
42
|
+
# @param completion_subscriber_ids [Array, nil] complete subscriber list ([] clears)
|
|
43
|
+
# @param notify [Boolean, nil] notify assignees about this write
|
|
44
|
+
# @param due_on [String, nil] due date YYYY-MM-DD (nil = keep current)
|
|
45
|
+
# @param starts_on [String, nil] start date YYYY-MM-DD (nil = keep current)
|
|
46
|
+
# @return [Hash] the updated todo
|
|
47
|
+
def update(todo_id:, content: nil, description: nil, assignee_ids: nil, completion_subscriber_ids: nil, notify: nil, due_on: nil, starts_on: nil)
|
|
48
|
+
fields = fields_from_todo(get(todo_id: todo_id))
|
|
49
|
+
fields.content = content unless content.nil?
|
|
50
|
+
fields.description = description unless description.nil?
|
|
51
|
+
fields.assignee_ids = assignee_ids unless assignee_ids.nil?
|
|
52
|
+
fields.completion_subscriber_ids = completion_subscriber_ids unless completion_subscriber_ids.nil?
|
|
53
|
+
fields.due_on = due_on unless due_on.nil?
|
|
54
|
+
fields.starts_on = starts_on unless starts_on.nil?
|
|
55
|
+
fields.notify = notify unless notify.nil?
|
|
56
|
+
put_fields(todo_id, fields)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Applies a read-modify-write block to a todo: GETs the current todo,
|
|
60
|
+
# yields its full writable state ({TodoFields}), and PUTs the whole
|
|
61
|
+
# thing back. Clearing a field means setting it empty (+""+ / +[]+) —
|
|
62
|
+
# an untouched field keeps its current value. If the block raises,
|
|
63
|
+
# the edit aborts and nothing is written.
|
|
64
|
+
#
|
|
65
|
+
# Not atomic — see the module docs for the GET→PUT race.
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# account.todos.edit(todo_id: 123) do |t|
|
|
69
|
+
# t.content = "🚨 #{t.content}"
|
|
70
|
+
# t.due_on = "" # clearing = setting empty on a full object
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# @param todo_id [Integer] todo id
|
|
74
|
+
# @yieldparam fields [TodoFields] the todo's writable state, to mutate in place
|
|
75
|
+
# @return [Hash] the updated todo
|
|
76
|
+
# @raise [ArgumentError] if no block is given
|
|
77
|
+
def edit(todo_id:)
|
|
78
|
+
raise ArgumentError, "edit requires a block" unless block_given?
|
|
79
|
+
|
|
80
|
+
fields = fields_from_todo(get(todo_id: todo_id))
|
|
81
|
+
yield fields
|
|
82
|
+
put_fields(todo_id, fields)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# Derives the full writable state from a GET response.
|
|
88
|
+
def fields_from_todo(todo)
|
|
89
|
+
TodoFields.new(
|
|
90
|
+
content: todo["content"] || "",
|
|
91
|
+
description: todo["description"] || "",
|
|
92
|
+
assignee_ids: (todo["assignees"] || []).map { |p| p["id"] },
|
|
93
|
+
completion_subscriber_ids: (todo["completion_subscribers"] || []).map { |p| p["id"] },
|
|
94
|
+
due_on: todo["due_on"] || "",
|
|
95
|
+
starts_on: todo["starts_on"] || "",
|
|
96
|
+
notify: false
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# PUTs the full writable state via +replace+: content, description,
|
|
101
|
+
# and both ID lists are always sent (empties included, so clears
|
|
102
|
+
# survive); dates only when non-empty (the server clears an omitted
|
|
103
|
+
# date, and +""+ is a format error); notify only when true.
|
|
104
|
+
def put_fields(todo_id, fields)
|
|
105
|
+
%i[assignee_ids completion_subscriber_ids].each do |key|
|
|
106
|
+
raise UsageError, "#{key} must be an array of person IDs; use [] to clear — a full write has no nil state" if fields[key].nil?
|
|
107
|
+
end
|
|
108
|
+
replace(
|
|
109
|
+
todo_id: todo_id,
|
|
110
|
+
content: fields.content,
|
|
111
|
+
description: fields.description,
|
|
112
|
+
assignee_ids: fields.assignee_ids,
|
|
113
|
+
completion_subscriber_ids: fields.completion_subscriber_ids,
|
|
114
|
+
due_on: fields.due_on.to_s.empty? ? nil : fields.due_on,
|
|
115
|
+
starts_on: fields.starts_on.to_s.empty? ? nil : fields.starts_on,
|
|
116
|
+
notify: fields.notify ? true : nil
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/basecamp/version.rb
CHANGED
data/lib/basecamp.rb
CHANGED
|
@@ -4,6 +4,12 @@ require "zeitwerk"
|
|
|
4
4
|
|
|
5
5
|
loader = Zeitwerk::Loader.for_gem
|
|
6
6
|
loader.collapse("#{__dir__}/basecamp/generated")
|
|
7
|
+
# The generated class owns the Basecamp::Services::TodosService constant
|
|
8
|
+
# (generated/ is collapsed), so the hand-written merge-safe update/edit
|
|
9
|
+
# surface is prepended onto it as soon as zeitwerk loads it.
|
|
10
|
+
loader.on_load("Basecamp::Services::TodosService") do |klass, _abspath|
|
|
11
|
+
klass.prepend(Basecamp::Services::TodosExtensions)
|
|
12
|
+
end
|
|
7
13
|
loader.setup
|
|
8
14
|
|
|
9
15
|
# Load generated types if available
|
|
@@ -45,7 +45,7 @@ class ServiceGenerator
|
|
|
45
45
|
'Campfires' => %w[
|
|
46
46
|
GetCampfire ListCampfires
|
|
47
47
|
ListChatbots CreateChatbot GetChatbot UpdateChatbot DeleteChatbot
|
|
48
|
-
ListCampfireLines CreateCampfireLine GetCampfireLine DeleteCampfireLine
|
|
48
|
+
ListCampfireLines CreateCampfireLine GetCampfireLine UpdateCampfireLine DeleteCampfireLine
|
|
49
49
|
ListCampfireUploads CreateCampfireUpload
|
|
50
50
|
]
|
|
51
51
|
},
|
|
@@ -60,7 +60,8 @@ class ServiceGenerator
|
|
|
60
60
|
'CardSteps' => %w[
|
|
61
61
|
GetCardStep CreateCardStep UpdateCardStep SetCardStepCompletion
|
|
62
62
|
RepositionCardStep
|
|
63
|
-
]
|
|
63
|
+
],
|
|
64
|
+
'Wormholes' => %w[CreateWormhole UpdateWormhole DeleteWormhole]
|
|
64
65
|
},
|
|
65
66
|
'Files' => {
|
|
66
67
|
'Attachments' => %w[CreateAttachment],
|
|
@@ -69,7 +70,7 @@ class ServiceGenerator
|
|
|
69
70
|
'Documents' => %w[GetDocument UpdateDocument ListDocuments CreateDocument]
|
|
70
71
|
},
|
|
71
72
|
'Automation' => {
|
|
72
|
-
'Tools' => %w[GetTool UpdateTool DeleteTool
|
|
73
|
+
'Tools' => %w[GetTool UpdateTool DeleteTool CreateTool EnableTool DisableTool RepositionTool],
|
|
73
74
|
'Recordings' => %w[GetRecording ArchiveRecording UnarchiveRecording TrashRecording ListRecordings],
|
|
74
75
|
'Webhooks' => %w[ListWebhooks CreateWebhook GetWebhook UpdateWebhook DeleteWebhook],
|
|
75
76
|
'Events' => %w[ListEvents],
|
|
@@ -115,8 +116,8 @@ class ServiceGenerator
|
|
|
115
116
|
'ClientVisibility' => %w[SetClientVisibility]
|
|
116
117
|
},
|
|
117
118
|
'Todos' => {
|
|
118
|
-
'Todos' => %w[ListTodos CreateTodo GetTodo
|
|
119
|
-
'Todolists' => %w[GetTodolistOrGroup UpdateTodolistOrGroup ListTodolists CreateTodolist],
|
|
119
|
+
'Todos' => %w[ListTodos CreateTodo GetTodo ReplaceTodo CompleteTodo UncompleteTodo TrashTodo],
|
|
120
|
+
'Todolists' => %w[GetTodolistOrGroup UpdateTodolistOrGroup ListTodolists CreateTodolist RepositionTodolist],
|
|
120
121
|
'Todosets' => %w[GetTodoset],
|
|
121
122
|
'HillCharts' => %w[GetHillChart UpdateHillChartSettings],
|
|
122
123
|
'TodolistGroups' => %w[ListTodolistGroups CreateTodolistGroup RepositionTodolistGroup]
|
|
@@ -190,6 +191,7 @@ class ServiceGenerator
|
|
|
190
191
|
'ListCampfireLines' => 'list_lines',
|
|
191
192
|
'CreateCampfireLine' => 'create_line',
|
|
192
193
|
'GetCampfireLine' => 'get_line',
|
|
194
|
+
'UpdateCampfireLine' => 'update_line',
|
|
193
195
|
'DeleteCampfireLine' => 'delete_line',
|
|
194
196
|
'ListCampfireUploads' => 'list_uploads',
|
|
195
197
|
'CreateCampfireUpload' => 'create_upload',
|
|
@@ -244,6 +246,7 @@ class ServiceGenerator
|
|
|
244
246
|
{ prefix: 'Get', method: 'get' },
|
|
245
247
|
{ prefix: 'Create', method: 'create' },
|
|
246
248
|
{ prefix: 'Update', method: 'update' },
|
|
249
|
+
{ prefix: 'Replace', method: 'replace' },
|
|
247
250
|
{ prefix: 'Delete', method: 'delete' },
|
|
248
251
|
{ prefix: 'Trash', method: 'trash' },
|
|
249
252
|
{ prefix: 'Archive', method: 'archive' },
|
|
@@ -263,6 +266,31 @@ class ServiceGenerator
|
|
|
263
266
|
{ prefix: 'Search', method: 'search' }
|
|
264
267
|
].freeze
|
|
265
268
|
|
|
269
|
+
# Hand-written methods appended to specific generated services.
|
|
270
|
+
# Keyed by service name; value is an array of method code strings indented to match generated output.
|
|
271
|
+
HAND_WRITTEN_METHODS = {
|
|
272
|
+
'Uploads' => [
|
|
273
|
+
<<~RUBY.chomp
|
|
274
|
+
# Download an upload's file content in one call.
|
|
275
|
+
# Fetches upload metadata, then delegates to the AccountClient download
|
|
276
|
+
# primitive so the auth'd-hop + 302-follow flow lives in one place.
|
|
277
|
+
# @param upload_id [Integer] upload id ID
|
|
278
|
+
# @return [Basecamp::DownloadResult]
|
|
279
|
+
def download(upload_id:)
|
|
280
|
+
with_operation(service: "uploads", operation: "download", is_mutation: false, resource_id: upload_id) do
|
|
281
|
+
upload = get(upload_id: upload_id)
|
|
282
|
+
url = upload["download_url"]
|
|
283
|
+
raise UsageError.new("upload \#{upload_id} has no download_url") if url.nil? || url.empty?
|
|
284
|
+
|
|
285
|
+
result = @client.download_url(url)
|
|
286
|
+
filename = upload["filename"]
|
|
287
|
+
filename.to_s.empty? ? result : result.with(filename: filename)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
RUBY
|
|
291
|
+
]
|
|
292
|
+
}.freeze
|
|
293
|
+
|
|
266
294
|
SIMPLE_RESOURCES = %w[
|
|
267
295
|
todo todos todolist todolists todoset message messages comment comments
|
|
268
296
|
card cards cardtable cardcolumn cardstep column step project projects
|
|
@@ -275,6 +303,7 @@ class ServiceGenerator
|
|
|
275
303
|
clientcorrespondences clientreply clientreplies forwardreply
|
|
276
304
|
forwardreplies campfireline campfirelines todolistgroup todolistgroups
|
|
277
305
|
todolistorgroup uploadversions hillchart hillcharts
|
|
306
|
+
wormhole wormholes
|
|
278
307
|
].freeze
|
|
279
308
|
|
|
280
309
|
def initialize(openapi_path)
|
|
@@ -369,7 +398,12 @@ class ServiceGenerator
|
|
|
369
398
|
.select { |p| p['in'] == 'query' }
|
|
370
399
|
.map do |p|
|
|
371
400
|
{
|
|
372
|
-
|
|
401
|
+
# Strip a trailing `[]` from bracketed array wire names (e.g.
|
|
402
|
+
# `bucket_ids[]`): the kwarg and the params-hash key are both clean
|
|
403
|
+
# `bucket_ids`, and Faraday's NestedParamsEncoder re-adds the `[]` when
|
|
404
|
+
# serializing the array value (a raw `bucket_ids[]` key would double to
|
|
405
|
+
# `bucket_ids[][]=`).
|
|
406
|
+
name: p['name'].sub(/\[\]\z/, ''),
|
|
373
407
|
type: schema_to_ruby_type(p['schema']),
|
|
374
408
|
required: p['required'] || false,
|
|
375
409
|
description: p['description']
|
|
@@ -506,7 +540,13 @@ class ServiceGenerator
|
|
|
506
540
|
def schema_to_ruby_type(schema)
|
|
507
541
|
return 'Object' unless schema
|
|
508
542
|
|
|
509
|
-
|
|
543
|
+
# Object-valued members (e.g. a `project`/`gauge`/`schedule` envelope) are passed
|
|
544
|
+
# as a Hash. Resolve $refs and treat only object-typed schemas as Hash; a string
|
|
545
|
+
# ref such as FirstWeekDay must stay String.
|
|
546
|
+
resolved = schema['$ref'] ? resolve_schema_ref(schema) : schema
|
|
547
|
+
return 'Hash' if resolved && resolved['type'] == 'object'
|
|
548
|
+
|
|
549
|
+
case resolved&.fetch('type', nil)
|
|
510
550
|
when 'integer' then 'Integer'
|
|
511
551
|
when 'boolean' then 'Boolean'
|
|
512
552
|
when 'array' then 'Array'
|
|
@@ -542,6 +582,13 @@ class ServiceGenerator
|
|
|
542
582
|
lines.concat(generate_method(op, service_name: service[:name]))
|
|
543
583
|
end
|
|
544
584
|
|
|
585
|
+
(HAND_WRITTEN_METHODS[service[:name]] || []).each do |method_code|
|
|
586
|
+
lines << ''
|
|
587
|
+
method_code.each_line do |l|
|
|
588
|
+
lines << (l.chomp.empty? ? '' : " #{l.chomp}")
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
|
|
545
592
|
lines << ' end'
|
|
546
593
|
lines << ' end'
|
|
547
594
|
lines << 'end'
|
|
@@ -657,10 +704,11 @@ class ServiceGenerator
|
|
|
657
704
|
kwargs << "operation: \"#{op[:method_name]}\""
|
|
658
705
|
kwargs << "is_mutation: #{op[:is_mutation]}"
|
|
659
706
|
|
|
660
|
-
project_param = op[:path_params].find { |p| p[:name]
|
|
661
|
-
resource_param = op[:path_params].reject { |p| p[:name]
|
|
707
|
+
project_param = op[:path_params].find { |p| %w[projectId bucketId].include?(p[:name]) }
|
|
708
|
+
resource_param = op[:path_params].reject { |p| %w[projectId bucketId].include?(p[:name]) }
|
|
709
|
+
.select { |p| p[:name].end_with?("Id") || p[:name] == "id" }.last
|
|
662
710
|
|
|
663
|
-
kwargs << "project_id:
|
|
711
|
+
kwargs << "project_id: #{to_snake_case(project_param[:name])}" if project_param
|
|
664
712
|
kwargs << "resource_id: #{to_snake_case(resource_param[:name])}" if resource_param
|
|
665
713
|
|
|
666
714
|
kwargs.join(', ')
|
|
@@ -754,7 +802,7 @@ class ServiceGenerator
|
|
|
754
802
|
# Build params hash for query params
|
|
755
803
|
if op[:query_params].any?
|
|
756
804
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
757
|
-
lines << " params =
|
|
805
|
+
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
758
806
|
lines << " paginate(#{path_expr}, params: params)"
|
|
759
807
|
else
|
|
760
808
|
lines << " paginate(#{path_expr})"
|
|
@@ -768,7 +816,7 @@ class ServiceGenerator
|
|
|
768
816
|
|
|
769
817
|
if op[:query_params].any?
|
|
770
818
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
771
|
-
lines << " params =
|
|
819
|
+
lines << " params = compact_query_params(#{param_names.join(', ')})"
|
|
772
820
|
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\", params: params)"
|
|
773
821
|
else
|
|
774
822
|
lines << " paginate_wrapped(#{path_expr}, key: \"#{pagination_key}\")"
|
|
@@ -800,7 +848,7 @@ class ServiceGenerator
|
|
|
800
848
|
lines << " http_#{http_method}(#{path_expr}, body: #{body_expr}).json"
|
|
801
849
|
elsif op[:query_params].any?
|
|
802
850
|
param_names = op[:query_params].map { |q| "#{to_snake_case(q[:name])}: #{to_snake_case(q[:name])}" }
|
|
803
|
-
lines << " http_#{http_method}(#{path_expr}, params:
|
|
851
|
+
lines << " http_#{http_method}(#{path_expr}, params: compact_query_params(#{param_names.join(', ')})).json"
|
|
804
852
|
else
|
|
805
853
|
lines << " http_#{http_method}(#{path_expr}).json"
|
|
806
854
|
end
|
data/scripts/generate-types.rb
CHANGED
|
@@ -124,6 +124,12 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
124
124
|
|
|
125
125
|
puts ''
|
|
126
126
|
puts " # #{name}"
|
|
127
|
+
# Documentation-only deprecation (see #406): YARD marks a whole class/method,
|
|
128
|
+
# not individual params, so a class-level @deprecated tag documents a wholly
|
|
129
|
+
# deprecated type.
|
|
130
|
+
if schema['deprecated']
|
|
131
|
+
puts " # @deprecated #{schema['x-deprecated-reason'] || 'deprecated'}"
|
|
132
|
+
end
|
|
127
133
|
puts " class #{name}"
|
|
128
134
|
puts ' include TypeHelpers'
|
|
129
135
|
|
|
@@ -131,6 +137,19 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
131
137
|
# Add system_label for schemas with flexible integer fields
|
|
132
138
|
has_flexible = ordered_props.any? { |k| properties[k]['x-go-type']&.include?('FlexibleInt64') }
|
|
133
139
|
attr_names << 'system_label' if has_flexible
|
|
140
|
+
|
|
141
|
+
# Per-attribute deprecation. The accessors are declared in one grouped
|
|
142
|
+
# attr_accessor, so a bare comment would wrongly document every attribute;
|
|
143
|
+
# a YARD @!attribute directive scopes the @deprecated tag to just this one.
|
|
144
|
+
ordered_props.each do |k|
|
|
145
|
+
ps = properties[k]
|
|
146
|
+
next unless ps['deprecated']
|
|
147
|
+
|
|
148
|
+
ruby_name = k.gsub(/([A-Z])/, '_\1').downcase.gsub(/^_/, '')
|
|
149
|
+
puts " # @!attribute [rw] #{ruby_name}"
|
|
150
|
+
puts " # @deprecated #{ps['x-deprecated-reason'] || 'deprecated'}"
|
|
151
|
+
end
|
|
152
|
+
|
|
134
153
|
puts " attr_accessor #{attr_names.map { |n| ":#{n}" }.join(", ")}"
|
|
135
154
|
|
|
136
155
|
unless required_props.empty?
|
|
@@ -182,7 +201,20 @@ if __FILE__ == $PROGRAM_NAME
|
|
|
182
201
|
attr_name = prop_name.gsub(/([A-Z])/, '_\1').downcase.gsub(/^_/, '')
|
|
183
202
|
puts " \"#{prop_name}\" => @#{attr_name},"
|
|
184
203
|
end
|
|
185
|
-
|
|
204
|
+
# A required-and-nullable field (OpenAPI 3.1 `type: [..., "null"]`) carries an
|
|
205
|
+
# explicit null that must survive to_h; plain .compact would drop it. Keep
|
|
206
|
+
# nil for exactly those keys — required-but-non-nullable fields still get
|
|
207
|
+
# dropped when nil, like any other nil. Everything else keeps .compact.
|
|
208
|
+
required_nullable = required_fields.select do |k|
|
|
209
|
+
t = properties[k] && properties[k]['type']
|
|
210
|
+
t.is_a?(Array) && t.include?('null')
|
|
211
|
+
end
|
|
212
|
+
if required_nullable.any?
|
|
213
|
+
keep_list = required_nullable.map { |k| "\"#{k}\"" }.join(', ')
|
|
214
|
+
puts " }.reject { |k, v| v.nil? && ![#{keep_list}].include?(k) }"
|
|
215
|
+
else
|
|
216
|
+
puts ' }.compact'
|
|
217
|
+
end
|
|
186
218
|
puts ' end'
|
|
187
219
|
puts ''
|
|
188
220
|
|
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.9.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-
|
|
11
|
+
date: 2026-07-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -86,14 +86,14 @@ dependencies:
|
|
|
86
86
|
requirements:
|
|
87
87
|
- - "~>"
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '0
|
|
89
|
+
version: '1.0'
|
|
90
90
|
type: :development
|
|
91
91
|
prerelease: false
|
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
93
93
|
requirements:
|
|
94
94
|
- - "~>"
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
|
-
version: '0
|
|
96
|
+
version: '1.0'
|
|
97
97
|
- !ruby/object:Gem::Dependency
|
|
98
98
|
name: webmock
|
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -128,28 +128,14 @@ dependencies:
|
|
|
128
128
|
requirements:
|
|
129
129
|
- - "~>"
|
|
130
130
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '
|
|
131
|
+
version: '8.0'
|
|
132
132
|
type: :development
|
|
133
133
|
prerelease: false
|
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
135
|
requirements:
|
|
136
136
|
- - "~>"
|
|
137
137
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '
|
|
139
|
-
- !ruby/object:Gem::Dependency
|
|
140
|
-
name: webrick
|
|
141
|
-
requirement: !ruby/object:Gem::Requirement
|
|
142
|
-
requirements:
|
|
143
|
-
- - "~>"
|
|
144
|
-
- !ruby/object:Gem::Version
|
|
145
|
-
version: '1.9'
|
|
146
|
-
type: :development
|
|
147
|
-
prerelease: false
|
|
148
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
-
requirements:
|
|
150
|
-
- - "~>"
|
|
151
|
-
- !ruby/object:Gem::Version
|
|
152
|
-
version: '1.9'
|
|
138
|
+
version: '8.0'
|
|
153
139
|
- !ruby/object:Gem::Dependency
|
|
154
140
|
name: yard
|
|
155
141
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -194,7 +180,6 @@ files:
|
|
|
194
180
|
- lib/basecamp/generated/metadata.json
|
|
195
181
|
- lib/basecamp/generated/services/account_service.rb
|
|
196
182
|
- lib/basecamp/generated/services/attachments_service.rb
|
|
197
|
-
- lib/basecamp/generated/services/authorization_service.rb
|
|
198
183
|
- lib/basecamp/generated/services/automation_service.rb
|
|
199
184
|
- lib/basecamp/generated/services/base_service.rb
|
|
200
185
|
- lib/basecamp/generated/services/boosts_service.rb
|
|
@@ -238,6 +223,7 @@ files:
|
|
|
238
223
|
- lib/basecamp/generated/services/uploads_service.rb
|
|
239
224
|
- lib/basecamp/generated/services/vaults_service.rb
|
|
240
225
|
- lib/basecamp/generated/services/webhooks_service.rb
|
|
226
|
+
- lib/basecamp/generated/services/wormholes_service.rb
|
|
241
227
|
- lib/basecamp/generated/types.rb
|
|
242
228
|
- lib/basecamp/hooks.rb
|
|
243
229
|
- lib/basecamp/http.rb
|
|
@@ -248,11 +234,16 @@ files:
|
|
|
248
234
|
- lib/basecamp/oauth.rb
|
|
249
235
|
- lib/basecamp/oauth/config.rb
|
|
250
236
|
- lib/basecamp/oauth/discovery.rb
|
|
237
|
+
- lib/basecamp/oauth/discovery_result.rb
|
|
238
|
+
- lib/basecamp/oauth/discovery_selection_error.rb
|
|
251
239
|
- lib/basecamp/oauth/exchange.rb
|
|
252
240
|
- lib/basecamp/oauth/exchange_request.rb
|
|
241
|
+
- lib/basecamp/oauth/fetcher.rb
|
|
253
242
|
- lib/basecamp/oauth/oauth_error.rb
|
|
254
243
|
- lib/basecamp/oauth/pkce.rb
|
|
244
|
+
- lib/basecamp/oauth/protected_resource_metadata.rb
|
|
255
245
|
- lib/basecamp/oauth/refresh_request.rb
|
|
246
|
+
- lib/basecamp/oauth/resource.rb
|
|
256
247
|
- lib/basecamp/oauth/token.rb
|
|
257
248
|
- lib/basecamp/oauth_token_provider.rb
|
|
258
249
|
- lib/basecamp/operation_info.rb
|
|
@@ -261,6 +252,8 @@ files:
|
|
|
261
252
|
- lib/basecamp/request_info.rb
|
|
262
253
|
- lib/basecamp/request_result.rb
|
|
263
254
|
- lib/basecamp/security.rb
|
|
255
|
+
- lib/basecamp/services/authorization_service.rb
|
|
256
|
+
- lib/basecamp/services/todos_extensions.rb
|
|
264
257
|
- lib/basecamp/static_token_provider.rb
|
|
265
258
|
- lib/basecamp/token_provider.rb
|
|
266
259
|
- lib/basecamp/usage_error.rb
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Basecamp
|
|
4
|
-
module Services
|
|
5
|
-
# Service for authorization operations.
|
|
6
|
-
# This is the only service that doesn't require an account context.
|
|
7
|
-
#
|
|
8
|
-
# @example Get authorization info
|
|
9
|
-
# auth = client.authorization.get
|
|
10
|
-
# puts "Identity: #{auth["identity"]["email_address"]}"
|
|
11
|
-
# auth["accounts"].each do |account|
|
|
12
|
-
# puts "Account: #{account["name"]} (#{account["id"]})"
|
|
13
|
-
# end
|
|
14
|
-
class AuthorizationService < BaseService
|
|
15
|
-
# Fallback Launchpad endpoint for authorization
|
|
16
|
-
LAUNCHPAD_AUTHORIZATION_URL = "https://launchpad.37signals.com/authorization.json"
|
|
17
|
-
|
|
18
|
-
# Gets authorization information for the current user.
|
|
19
|
-
#
|
|
20
|
-
# Attempts to use the authorization endpoint discovered via OAuth discovery
|
|
21
|
-
# on the configured base URL. Falls back to Launchpad if discovery fails.
|
|
22
|
-
#
|
|
23
|
-
# Returns the authenticated user's identity and list of accounts
|
|
24
|
-
# they have access to.
|
|
25
|
-
#
|
|
26
|
-
# @return [Hash] authorization info with :identity and :accounts
|
|
27
|
-
# @see https://github.com/basecamp/bc3-api/blob/master/sections/authentication.md
|
|
28
|
-
def get
|
|
29
|
-
url = discover_authorization_url
|
|
30
|
-
response = http.get_absolute(url)
|
|
31
|
-
response.json
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def discover_authorization_url
|
|
37
|
-
# Try OAuth discovery on the configured base URL
|
|
38
|
-
config = Oauth.discover(http.base_url)
|
|
39
|
-
# Use issuer as base for authorization.json
|
|
40
|
-
"#{config.issuer.chomp("/")}/authorization.json"
|
|
41
|
-
rescue Oauth::OauthError
|
|
42
|
-
# Fall back to Launchpad
|
|
43
|
-
LAUNCHPAD_AUTHORIZATION_URL
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|