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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +65 -1
  3. data/basecamp-sdk.gemspec +2 -3
  4. data/lib/basecamp/client.rb +5 -0
  5. data/lib/basecamp/generated/metadata.json +100 -33
  6. data/lib/basecamp/generated/services/base_service.rb +13 -0
  7. data/lib/basecamp/generated/services/campfires_service.rb +15 -3
  8. data/lib/basecamp/generated/services/card_columns_service.rb +26 -23
  9. data/lib/basecamp/generated/services/checkins_service.rb +5 -4
  10. data/lib/basecamp/generated/services/client_approvals_service.rb +1 -1
  11. data/lib/basecamp/generated/services/client_correspondences_service.rb +1 -1
  12. data/lib/basecamp/generated/services/documents_service.rb +3 -2
  13. data/lib/basecamp/generated/services/forwards_service.rb +1 -1
  14. data/lib/basecamp/generated/services/gauges_service.rb +4 -4
  15. data/lib/basecamp/generated/services/messages_service.rb +4 -3
  16. data/lib/basecamp/generated/services/my_assignments_service.rb +1 -1
  17. data/lib/basecamp/generated/services/my_notifications_service.rb +1 -1
  18. data/lib/basecamp/generated/services/people_service.rb +2 -2
  19. data/lib/basecamp/generated/services/projects_service.rb +2 -2
  20. data/lib/basecamp/generated/services/recordings_service.rb +1 -1
  21. data/lib/basecamp/generated/services/reports_service.rb +3 -3
  22. data/lib/basecamp/generated/services/schedules_service.rb +5 -4
  23. data/lib/basecamp/generated/services/search_service.rb +14 -3
  24. data/lib/basecamp/generated/services/templates_service.rb +4 -5
  25. data/lib/basecamp/generated/services/timesheets_service.rb +3 -3
  26. data/lib/basecamp/generated/services/todolists_service.rb +15 -3
  27. data/lib/basecamp/generated/services/todos_service.rb +5 -5
  28. data/lib/basecamp/generated/services/tools_service.rb +7 -6
  29. data/lib/basecamp/generated/services/uploads_service.rb +20 -2
  30. data/lib/basecamp/generated/services/webhooks_service.rb +2 -2
  31. data/lib/basecamp/generated/services/wormholes_service.rb +44 -0
  32. data/lib/basecamp/generated/types.rb +284 -64
  33. data/lib/basecamp/http.rb +86 -15
  34. data/lib/basecamp/oauth/config.rb +20 -5
  35. data/lib/basecamp/oauth/discovery.rb +134 -64
  36. data/lib/basecamp/oauth/discovery_result.rb +33 -0
  37. data/lib/basecamp/oauth/discovery_selection_error.rb +30 -0
  38. data/lib/basecamp/oauth/fetcher.rb +204 -0
  39. data/lib/basecamp/oauth/protected_resource_metadata.rb +22 -0
  40. data/lib/basecamp/oauth/resource.rb +95 -0
  41. data/lib/basecamp/oauth.rb +173 -0
  42. data/lib/basecamp/security.rb +83 -0
  43. data/lib/basecamp/services/authorization_service.rb +45 -0
  44. data/lib/basecamp/services/todos_extensions.rb +121 -0
  45. data/lib/basecamp/version.rb +2 -2
  46. data/lib/basecamp.rb +6 -0
  47. data/scripts/generate-services.rb +61 -13
  48. data/scripts/generate-types.rb +33 -1
  49. metadata +14 -21
  50. 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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.7.3"
5
- API_VERSION = "2026-03-23"
4
+ VERSION = "0.9.0"
5
+ API_VERSION = "2026-07-24"
6
6
  end
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 CloneTool EnableTool DisableTool RepositionTool],
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 UpdateTodo CompleteTodo UncompleteTodo TrashTodo],
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
- name: p['name'],
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
- case schema['type']
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] == 'projectId' }
661
- resource_param = op[:path_params].reject { |p| p[:name] == 'projectId' }.last
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: project_id" if project_param
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 = compact_params(#{param_names.join(', ')})"
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 = compact_params(#{param_names.join(', ')})"
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: compact_params(#{param_names.join(', ')})).json"
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
@@ -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
- puts ' }.compact'
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.7.3
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-03-30 00:00:00.000000000 Z
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.22'
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.22'
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: '7.1'
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: '7.1'
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