basecamp-sdk 0.20.0 → 0.21.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1df4a43587230e1bfbf0403175de3fdf93b50635c42a91a71d3c2e29bb835c8
4
- data.tar.gz: c2a9eb831ecd48967fdb706a808fe13a923570f97edcf7b6ed96b7e2372dfcd2
3
+ metadata.gz: 7b16ed47ffa033dcc0c642c37ebc51a4bc6552fa06a088fd268cb451fa09e2f8
4
+ data.tar.gz: 48c691a48f55cc68f9d3d5208488c8baecb56264a230614a22e6b7746d8a7451
5
5
  SHA512:
6
- metadata.gz: 25fb53f7d538f519dfd7f7091e687dec7a0f9ce59f65df4c612f44fcea5cf2f3d65d8b8257dcccb8a0bc2b535e340efe911b9f435ef54f022ac0f9752953f6d5
7
- data.tar.gz: 6a7f120b233a4d4dc07f5a233c97ee4c8fb3861181143bdd492a7e48998903eb8e0a465b6cfc8816511ef03ecfa0657c0af951b05f3d5a761019130332401a27
6
+ metadata.gz: 20f3979bfb268ce78303e4b10aadb50194a4b20818876a076753560260f54d70a9cbd42851a7a4337cdefc316a1800f6d4088437cd68994a684b2e9f60b2d016
7
+ data.tar.gz: 88bb6d1ec5889506b6f24660ce919befa780057f37587143526d524286d6fcd1d098aed95971f136400cfc1ab65656f28ea9f605c24ac543c5f9f0c02474fd74
data/README.md CHANGED
@@ -347,6 +347,7 @@ The SDK provides the full account-scoped service set documented in SPEC §5. The
347
347
  | `card_tables` | Card tables (kanban) |
348
348
  | `card_columns` | Card table columns |
349
349
  | `card_steps` | Card workflow steps |
350
+ | `subtasks` | Subtasks (checklist items) on to-dos and cards |
350
351
  | `wormholes` | Card table wormholes (cross-project moves) |
351
352
  | `lineup` | Card lineup view |
352
353
  | `tools` | Project dock tools |
@@ -607,6 +607,11 @@ module Basecamp
607
607
  service(:boosts) { Services::BoostsService.new(self) }
608
608
  end
609
609
 
610
+ # @return [Services::SubtasksService]
611
+ def subtasks
612
+ service(:subtasks) { Services::SubtasksService.new(self) }
613
+ end
614
+
610
615
  # @return [Services::AccountService]
611
616
  def account
612
617
  service(:account) { Services::AccountService.new(self) }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://basecamp.com/schemas/sdk-metadata.json",
3
3
  "version": "1.0.0",
4
- "generated": "2026-09-16T14:38:22Z",
4
+ "generated": "2026-09-30T18:52:50Z",
5
5
  "operations": {
6
6
  "GetAccount": {
7
7
  "retry": {
@@ -375,6 +375,28 @@
375
375
  ]
376
376
  }
377
377
  },
378
+ "CreateTemplatification": {
379
+ "retry": {
380
+ "maxAttempts": 2,
381
+ "baseDelayMs": 1000,
382
+ "backoff": "exponential",
383
+ "retryOn": [
384
+ 429,
385
+ 503
386
+ ]
387
+ }
388
+ },
389
+ "GetTemplatification": {
390
+ "retry": {
391
+ "maxAttempts": 3,
392
+ "baseDelayMs": 1000,
393
+ "backoff": "exponential",
394
+ "retryOn": [
395
+ 429,
396
+ 503
397
+ ]
398
+ }
399
+ },
378
400
  "CreateTodosetTodo": {
379
401
  "retry": {
380
402
  "maxAttempts": 3,
@@ -2489,6 +2511,33 @@
2489
2511
  "natural": true
2490
2512
  }
2491
2513
  },
2514
+ "ListSubtasks": {
2515
+ "retry": {
2516
+ "maxAttempts": 3,
2517
+ "baseDelayMs": 1000,
2518
+ "backoff": "exponential",
2519
+ "retryOn": [
2520
+ 429,
2521
+ 503
2522
+ ]
2523
+ },
2524
+ "pagination": {
2525
+ "style": "link",
2526
+ "totalCountHeader": "X-Total-Count",
2527
+ "maxPageSize": 50
2528
+ }
2529
+ },
2530
+ "CreateSubtask": {
2531
+ "retry": {
2532
+ "maxAttempts": 2,
2533
+ "baseDelayMs": 1000,
2534
+ "backoff": "exponential",
2535
+ "retryOn": [
2536
+ 429,
2537
+ 503
2538
+ ]
2539
+ }
2540
+ },
2492
2541
  "GetRecordingTimesheet": {
2493
2542
  "retry": {
2494
2543
  "maxAttempts": 3,
@@ -2834,7 +2883,7 @@
2834
2883
  "natural": true
2835
2884
  }
2836
2885
  },
2837
- "GetTemplateLibrary": {
2886
+ "GetSubtask": {
2838
2887
  "retry": {
2839
2888
  "maxAttempts": 3,
2840
2889
  "baseDelayMs": 1000,
@@ -2845,6 +2894,98 @@
2845
2894
  ]
2846
2895
  }
2847
2896
  },
2897
+ "UpdateSubtask": {
2898
+ "retry": {
2899
+ "maxAttempts": 3,
2900
+ "baseDelayMs": 1000,
2901
+ "backoff": "exponential",
2902
+ "retryOn": [
2903
+ 429,
2904
+ 503
2905
+ ]
2906
+ },
2907
+ "idempotent": {
2908
+ "natural": true
2909
+ }
2910
+ },
2911
+ "DeleteSubtask": {
2912
+ "retry": {
2913
+ "maxAttempts": 3,
2914
+ "baseDelayMs": 1000,
2915
+ "backoff": "exponential",
2916
+ "retryOn": [
2917
+ 429,
2918
+ 503
2919
+ ]
2920
+ },
2921
+ "idempotent": {
2922
+ "natural": true
2923
+ }
2924
+ },
2925
+ "CompleteSubtask": {
2926
+ "retry": {
2927
+ "maxAttempts": 3,
2928
+ "baseDelayMs": 1000,
2929
+ "backoff": "exponential",
2930
+ "retryOn": [
2931
+ 429,
2932
+ 503
2933
+ ]
2934
+ },
2935
+ "idempotent": {
2936
+ "natural": true
2937
+ }
2938
+ },
2939
+ "UncompleteSubtask": {
2940
+ "retry": {
2941
+ "maxAttempts": 3,
2942
+ "baseDelayMs": 1000,
2943
+ "backoff": "exponential",
2944
+ "retryOn": [
2945
+ 429,
2946
+ 503
2947
+ ]
2948
+ },
2949
+ "idempotent": {
2950
+ "natural": true
2951
+ }
2952
+ },
2953
+ "RepositionSubtask": {
2954
+ "retry": {
2955
+ "maxAttempts": 3,
2956
+ "baseDelayMs": 1000,
2957
+ "backoff": "exponential",
2958
+ "retryOn": [
2959
+ 429,
2960
+ 503
2961
+ ]
2962
+ },
2963
+ "idempotent": {
2964
+ "natural": true
2965
+ }
2966
+ },
2967
+ "GetTemplateLibraryCardTables": {
2968
+ "retry": {
2969
+ "maxAttempts": 3,
2970
+ "baseDelayMs": 1000,
2971
+ "backoff": "exponential",
2972
+ "retryOn": [
2973
+ 429,
2974
+ 503
2975
+ ]
2976
+ }
2977
+ },
2978
+ "CreateTemplateLibraryCardTable": {
2979
+ "retry": {
2980
+ "maxAttempts": 2,
2981
+ "baseDelayMs": 1000,
2982
+ "backoff": "exponential",
2983
+ "retryOn": [
2984
+ 429,
2985
+ 503
2986
+ ]
2987
+ }
2988
+ },
2848
2989
  "CreateTemplateLibraryCopy": {
2849
2990
  "retry": {
2850
2991
  "maxAttempts": 2,
@@ -2867,6 +3008,28 @@
2867
3008
  ]
2868
3009
  }
2869
3010
  },
3011
+ "GetTemplateLibraryTodolists": {
3012
+ "retry": {
3013
+ "maxAttempts": 3,
3014
+ "baseDelayMs": 1000,
3015
+ "backoff": "exponential",
3016
+ "retryOn": [
3017
+ 429,
3018
+ 503
3019
+ ]
3020
+ }
3021
+ },
3022
+ "CreateTemplateLibraryTodolist": {
3023
+ "retry": {
3024
+ "maxAttempts": 2,
3025
+ "baseDelayMs": 1000,
3026
+ "backoff": "exponential",
3027
+ "retryOn": [
3028
+ 429,
3029
+ 503
3030
+ ]
3031
+ }
3032
+ },
2870
3033
  "ListTemplates": {
2871
3034
  "retry": {
2872
3035
  "maxAttempts": 3,
@@ -3566,7 +3729,35 @@
3566
3729
  "creator",
3567
3730
  "participants.[]"
3568
3731
  ],
3732
+ "CreateSubtask": [
3733
+ "assignees.[]",
3734
+ "completer",
3735
+ "creator"
3736
+ ],
3737
+ "CreateTemplateLibraryCardTable": [
3738
+ "creator",
3739
+ "lists.[].creator",
3740
+ "lists.[].subscribers.[]",
3741
+ "subscribers.[]",
3742
+ "wormholes.[].creator"
3743
+ ],
3569
3744
  "CreateTemplateLibraryCopy": [
3745
+ "destination_card_table.creator",
3746
+ "destination_card_table.lists.[].creator",
3747
+ "destination_card_table.lists.[].subscribers.[]",
3748
+ "destination_card_table.subscribers.[]",
3749
+ "destination_card_table.wormholes.[].creator",
3750
+ "destination_todolist.creator"
3751
+ ],
3752
+ "CreateTemplateLibraryTodolist": [
3753
+ "creator"
3754
+ ],
3755
+ "CreateTemplatification": [
3756
+ "destination_card_table.creator",
3757
+ "destination_card_table.lists.[].creator",
3758
+ "destination_card_table.lists.[].subscribers.[]",
3759
+ "destination_card_table.subscribers.[]",
3760
+ "destination_card_table.wormholes.[].creator",
3570
3761
  "destination_todolist.creator"
3571
3762
  ],
3572
3763
  "CreateTimesheetEntry": [
@@ -3914,10 +4105,31 @@
3914
4105
  "GetSubscription": [
3915
4106
  "subscribers.[]"
3916
4107
  ],
3917
- "GetTemplateLibrary": [
3918
- "todolists.[].creator"
4108
+ "GetSubtask": [
4109
+ "assignees.[]",
4110
+ "completer",
4111
+ "creator"
4112
+ ],
4113
+ "GetTemplateLibraryCardTables": [
4114
+ "card_tables.[].creator"
3919
4115
  ],
3920
4116
  "GetTemplateLibraryCopy": [
4117
+ "destination_card_table.creator",
4118
+ "destination_card_table.lists.[].creator",
4119
+ "destination_card_table.lists.[].subscribers.[]",
4120
+ "destination_card_table.subscribers.[]",
4121
+ "destination_card_table.wormholes.[].creator",
4122
+ "destination_todolist.creator"
4123
+ ],
4124
+ "GetTemplateLibraryTodolists": [
4125
+ "todolists.[].creator"
4126
+ ],
4127
+ "GetTemplatification": [
4128
+ "destination_card_table.creator",
4129
+ "destination_card_table.lists.[].creator",
4130
+ "destination_card_table.lists.[].subscribers.[]",
4131
+ "destination_card_table.subscribers.[]",
4132
+ "destination_card_table.wormholes.[].creator",
3921
4133
  "destination_todolist.creator"
3922
4134
  ],
3923
4135
  "GetTimesheetEntry": [
@@ -4047,6 +4259,11 @@
4047
4259
  "[].creator",
4048
4260
  "[].participants.[]"
4049
4261
  ],
4262
+ "ListSubtasks": [
4263
+ "[].assignees.[]",
4264
+ "[].completer",
4265
+ "[].creator"
4266
+ ],
4050
4267
  "ListTodolistGroups": [
4051
4268
  "[].creator"
4052
4269
  ],
@@ -4162,6 +4379,11 @@
4162
4379
  "UpdateSubscription": [
4163
4380
  "subscribers.[]"
4164
4381
  ],
4382
+ "UpdateSubtask": [
4383
+ "assignees.[]",
4384
+ "completer",
4385
+ "creator"
4386
+ ],
4165
4387
  "UpdateTimesheetEntry": [
4166
4388
  "creator",
4167
4389
  "person"
@@ -10,7 +10,9 @@ module Basecamp
10
10
  # Reposition a step within a card
11
11
  # @param card_id [Integer] card id ID
12
12
  # @param source_id [Integer] source id
13
- # @param position [Integer] 0-indexed position
13
+ # @param position [Integer] The 1-based position to move it to (1 = top), the same `reposition_to`
14
+ # a to-do uses. bc3's doc said "Zero indexed" until BC3 #12659 corrected
15
+ # it; the server never was.
14
16
  # @return [void]
15
17
  def reposition(card_id:, source_id:, position:)
16
18
  with_operation(service: "cardsteps", operation: "reposition", is_mutation: true, resource_id: card_id) do
@@ -34,7 +34,8 @@ module Basecamp
34
34
  end
35
35
 
36
36
  # Mark specified items as read
37
- # @param readables [Array] Array of readable_sgid values identifying the items to mark as read
37
+ # @param readables [Array] Array of readable_sgid values identifying the items to mark as read.
38
+ # At most 500 per request.
38
39
  # @return [void]
39
40
  def mark_as_read(readables:)
40
41
  with_operation(service: "mynotifications", operation: "mark_as_read", is_mutation: true) do
@@ -52,7 +52,7 @@ module Basecamp
52
52
  end
53
53
  end
54
54
 
55
- # Archive a recording
55
+ # Archive a recording; bc3 answers 403 for types it never lets be archived, timesheet entries among them
56
56
  # @param recording_id [Integer] recording id ID
57
57
  # @return [void]
58
58
  def archive(recording_id:)
@@ -62,7 +62,7 @@ module Basecamp
62
62
  end
63
63
  end
64
64
 
65
- # Trash a recording
65
+ # Trash a recording; bc3 answers 403 for types it never lets be trashed, timesheet entries among them (DestroyTimesheetEntry removes those, permanently)
66
66
  # @param recording_id [Integer] recording id ID
67
67
  # @return [void]
68
68
  def trash(recording_id:)
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Basecamp
4
+ module Services
5
+ # Service for Subtasks operations
6
+ #
7
+ # @generated from OpenAPI spec
8
+ class SubtasksService < BaseService
9
+
10
+ # List a recording's subtasks, in position order
11
+ # @param recording_id [Integer] recording id ID
12
+ # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
13
+ # @param max_items [Integer, nil] cap on items yielded across pages; nil or non-positive means no cap
14
+ # @return [ListEnumerator<Hash>] lazily paginated results (#meta carries pagination metadata)
15
+ def list(recording_id:, page: nil, max_items: nil)
16
+ wrap_paginated(service: "subtasks", operation: "list", is_mutation: false, resource_id: recording_id) do
17
+ params = compact_query_params(page: page)
18
+ paginate("/recordings/#{recording_id}/subtasks.json", params: params, operation: "ListSubtasks", max_items: max_items)
19
+ end
20
+ end
21
+
22
+ # Create a subtask under a to-do or a card
23
+ # @param recording_id [Integer] recording id ID
24
+ # @param title [String] title
25
+ # @param due_on [String, nil] due on (YYYY-MM-DD)
26
+ # @param assignee_ids [Array, nil] assignee ids
27
+ # @return [Hash] response data
28
+ def create(recording_id:, title:, due_on: nil, assignee_ids: nil)
29
+ with_operation(service: "subtasks", operation: "create", is_mutation: true, resource_id: recording_id) do
30
+ http_post("/recordings/#{recording_id}/subtasks.json", body: compact_params(title: title, due_on: due_on, assignee_ids: assignee_ids)).json(operation: "CreateSubtask")
31
+ end
32
+ end
33
+
34
+ # Get a subtask by ID
35
+ # @param subtask_id [Integer] subtask id ID
36
+ # @return [Hash] response data
37
+ def get(subtask_id:)
38
+ with_operation(service: "subtasks", operation: "get", is_mutation: false, resource_id: subtask_id) do
39
+ http_get("/subtasks/#{subtask_id}", operation: "GetSubtask").json(operation: "GetSubtask")
40
+ end
41
+ end
42
+
43
+ # Update a subtask
44
+ # @param subtask_id [Integer] subtask id ID
45
+ # @param title [String, nil] title
46
+ # @param due_on [String, nil] due on (YYYY-MM-DD)
47
+ # @param assignee_ids [Array, nil] assignee ids
48
+ # @return [Hash] response data
49
+ def update(subtask_id:, title: nil, due_on: nil, assignee_ids: nil)
50
+ with_operation(service: "subtasks", operation: "update", is_mutation: true, resource_id: subtask_id) do
51
+ http_put("/subtasks/#{subtask_id}", body: compact_params(title: title, due_on: due_on, assignee_ids: assignee_ids)).json(operation: "UpdateSubtask")
52
+ end
53
+ end
54
+
55
+ # Delete a subtask
56
+ # @param subtask_id [Integer] subtask id ID
57
+ # @return [void]
58
+ def delete(subtask_id:)
59
+ with_operation(service: "subtasks", operation: "delete", is_mutation: true, resource_id: subtask_id) do
60
+ http_delete("/subtasks/#{subtask_id}")
61
+ nil
62
+ end
63
+ end
64
+
65
+ # Mark a subtask as completed
66
+ # @param subtask_id [Integer] subtask id ID
67
+ # @return [void]
68
+ def complete(subtask_id:)
69
+ with_operation(service: "subtasks", operation: "complete", is_mutation: true, resource_id: subtask_id) do
70
+ http_post("/subtasks/#{subtask_id}/completion.json")
71
+ nil
72
+ end
73
+ end
74
+
75
+ # Mark a subtask as not completed
76
+ # @param subtask_id [Integer] subtask id ID
77
+ # @return [void]
78
+ def uncomplete(subtask_id:)
79
+ with_operation(service: "subtasks", operation: "uncomplete", is_mutation: true, resource_id: subtask_id) do
80
+ http_delete("/subtasks/#{subtask_id}/completion.json")
81
+ nil
82
+ end
83
+ end
84
+
85
+ # Move a subtask to a new position among its siblings
86
+ # @param subtask_id [Integer] subtask id ID
87
+ # @param position [Integer] The 1-based position to move it to
88
+ # @return [void]
89
+ def reposition(subtask_id:, position:)
90
+ with_operation(service: "subtasks", operation: "reposition", is_mutation: true, resource_id: subtask_id) do
91
+ http_put("/subtasks/#{subtask_id}/position.json", body: compact_params(position: position))
92
+ nil
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -7,22 +7,63 @@ module Basecamp
7
7
  # @generated from OpenAPI spec
8
8
  class TemplatesService < BaseService
9
9
 
10
- # Get the account's to-do list template library
10
+ # Templatify a to-do list or card table
11
+ # @param bucket_id [Integer] bucket id ID
12
+ # @param recording_id [Integer] The to-do list or card table to templatify. Anything else is a 403.
13
+ # @param template_name [String, nil] What to call the template. Defaults to the source recording's own title.
14
+ # @param copy_comments [Boolean, nil] Carry the comments across.
15
+ # @param copy_assignments [Boolean, nil] Carry assignees and the people involved across, adding them to the library
16
+ # if they are not already there.
17
+ # @param move_cards_to_triage [Boolean, nil] Gather the cards into Triage. Card tables only, and ignored otherwise.
11
18
  # @return [Hash] response data
12
- def get_library()
13
- with_operation(service: "templates", operation: "get_library", is_mutation: false) do
14
- http_get("/template_library.json", operation: "GetTemplateLibrary").json(operation: "GetTemplateLibrary")
19
+ def create_templatification(bucket_id:, recording_id:, template_name: nil, copy_comments: nil, copy_assignments: nil, move_cards_to_triage: nil)
20
+ with_operation(service: "templates", operation: "create_templatification", is_mutation: true, project_id: bucket_id, resource_id: recording_id) do
21
+ http_post("/buckets/#{bucket_id}/recordings/#{recording_id}/templatifications.json", body: compact_params(template_name: template_name, copy_comments: copy_comments, copy_assignments: copy_assignments, move_cards_to_triage: move_cards_to_triage)).json(operation: "CreateTemplatification")
15
22
  end
16
23
  end
17
24
 
18
- # Start copying a to-do list template into a project
19
- # @param template_recording_id [Integer] template recording id
20
- # @param destination_parent_id [Integer] destination parent id
25
+ # Get a templatification
26
+ # @param bucket_id [Integer] bucket id ID
27
+ # @param recording_id [Integer] recording id ID
28
+ # @param templatification_id [Integer] templatification id ID
29
+ # @return [Hash] response data
30
+ def get_templatification(bucket_id:, recording_id:, templatification_id:)
31
+ with_operation(service: "templates", operation: "get_templatification", is_mutation: false, project_id: bucket_id, resource_id: templatification_id) do
32
+ http_get("/buckets/#{bucket_id}/recordings/#{recording_id}/templatifications/#{templatification_id}", operation: "GetTemplatification").json(operation: "GetTemplatification")
33
+ end
34
+ end
35
+
36
+ # Get the account's card table templates
37
+ # @return [Hash] response data
38
+ def get_library_card_tables()
39
+ with_operation(service: "templates", operation: "get_library_card_tables", is_mutation: false) do
40
+ http_get("/template_library/card_tables.json", operation: "GetTemplateLibraryCardTables").json(operation: "GetTemplateLibraryCardTables")
41
+ end
42
+ end
43
+
44
+ # Create a card table template with the default columns
45
+ # @param name [String] The template's name. Write-only: the response carries it as `title`.
46
+ # @return [Hash] response data
47
+ def create_library_card_table(name:)
48
+ with_operation(service: "templates", operation: "create_library_card_table", is_mutation: true) do
49
+ http_post("/template_library/card_tables.json", body: compact_params(name: name)).json(operation: "CreateTemplateLibraryCardTable")
50
+ end
51
+ end
52
+
53
+ # Start copying a to-do list or card table template into a project
54
+ # @param template_recording_id [Integer] The to-do list or card table in the library to copy.
55
+ # @param destination_project_id [Integer, nil] The destination project. Basecamp resolves the container from the
56
+ # template's kind, so a caller naming a project needs to know nothing about
57
+ # docks or to-do sets. Supply this or destination_parent_id; if both are
58
+ # sent, destination_parent_id wins.
59
+ # @param destination_parent_id [Integer, nil] The container to copy into, for a caller that already holds one: the
60
+ # project's to-do set for a to-do list template, or its dock for a card
61
+ # table. A caller who may not edit it gets 404, not 403.
21
62
  # @param adding_people_confirmed [Boolean, nil] Confirm granting destination-project access to people referenced by the template.
22
63
  # @return [Hash] response data
23
- def create_library_copy(template_recording_id:, destination_parent_id:, adding_people_confirmed: nil)
64
+ def create_library_copy(template_recording_id:, destination_project_id: nil, destination_parent_id: nil, adding_people_confirmed: nil)
24
65
  with_operation(service: "templates", operation: "create_library_copy", is_mutation: true) do
25
- http_post("/template_library/copies.json", body: compact_params(template_recording_id: template_recording_id, destination_parent_id: destination_parent_id, adding_people_confirmed: adding_people_confirmed)).json(operation: "CreateTemplateLibraryCopy")
66
+ http_post("/template_library/copies.json", body: compact_params(template_recording_id: template_recording_id, destination_project_id: destination_project_id, destination_parent_id: destination_parent_id, adding_people_confirmed: adding_people_confirmed)).json(operation: "CreateTemplateLibraryCopy")
26
67
  end
27
68
  end
28
69
 
@@ -35,6 +76,24 @@ module Basecamp
35
76
  end
36
77
  end
37
78
 
79
+ # Get the account's to-do list templates
80
+ # @return [Hash] response data
81
+ def get_library_todolists()
82
+ with_operation(service: "templates", operation: "get_library_todolists", is_mutation: false) do
83
+ http_get("/template_library/todolists.json", operation: "GetTemplateLibraryTodolists").json(operation: "GetTemplateLibraryTodolists")
84
+ end
85
+ end
86
+
87
+ # Create an empty to-do list template
88
+ # @param name [String] What to call the template.
89
+ # @param description [String, nil] Rich text describing the template.
90
+ # @return [Hash] response data
91
+ def create_library_todolist(name:, description: nil)
92
+ with_operation(service: "templates", operation: "create_library_todolist", is_mutation: true) do
93
+ http_post("/template_library/todolists.json", body: compact_params(name: name, description: description)).json(operation: "CreateTemplateLibraryTodolist")
94
+ end
95
+ end
96
+
38
97
  # List all templates visible to the current user
39
98
  # @param status [String, nil] active|archived|trashed
40
99
  # @param page [Integer, nil] Page number for paginating through results. Defaults to 1. A positive value selects exactly that page, not a starting offset; see SPEC section 8.
@@ -83,7 +83,7 @@ module Basecamp
83
83
  end
84
84
  end
85
85
 
86
- # Permanently delete a timesheet entry; answers 403 when the caller may not archive or trash it.
86
+ # Permanently delete a timesheet entry, which never appears in the trash and cannot be restored; this is the only way to remove one, since bc3 answers 403 to TrashRecording and ArchiveRecording for every timesheet entry, and it answers 403 when the caller may not remove the entry.
87
87
  # @param entry_id [Integer] entry id ID
88
88
  # @return [void]
89
89
  def destroy(entry_id:)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Auto-generated from OpenAPI spec. Do not edit manually.
4
- # Generated: 2026-09-16T11:13:05Z
4
+ # Generated: 2026-09-30T18:52:50Z
5
5
 
6
6
  require "json"
7
7
  require "time"
@@ -594,7 +594,7 @@ module Basecamp
594
594
  # Card
595
595
  class Card
596
596
  include TypeHelpers
597
- attr_accessor :app_url, :bucket, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completed_at, :completer, :completion_subscribers, :completion_url, :content, :description, :due_on, :position, :steps, :subscription_url
597
+ attr_accessor :app_url, :bucket, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completed_at, :completer, :completion_subscribers, :completion_url, :content, :description, :due_on, :position, :steps, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
598
598
 
599
599
  # @return [Array<Symbol>]
600
600
  def self.required_fields
@@ -631,8 +631,11 @@ module Basecamp
631
631
  @description = data["description"]
632
632
  @due_on = data["due_on"]
633
633
  @position = parse_integer(data["position"])
634
- @steps = parse_array(data["steps"], "CardStep")
634
+ @steps = parse_array(data["steps"], "Subtask")
635
635
  @subscription_url = data["subscription_url"]
636
+ @subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
637
+ @subtasks_count = parse_integer(data["subtasks_count"])
638
+ @subtasks_url = data["subtasks_url"]
636
639
  end
637
640
 
638
641
  def to_h
@@ -668,6 +671,9 @@ module Basecamp
668
671
  "position" => @position,
669
672
  "steps" => @steps,
670
673
  "subscription_url" => @subscription_url,
674
+ "subtasks_completed_count" => @subtasks_completed_count,
675
+ "subtasks_count" => @subtasks_count,
676
+ "subtasks_url" => @subtasks_url,
671
677
  }.compact
672
678
  end
673
679
 
@@ -782,75 +788,10 @@ module Basecamp
782
788
  end
783
789
  end
784
790
 
785
- # CardStep
786
- class CardStep
787
- include TypeHelpers
788
- attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :completed, :completed_at, :completer, :completion_url, :due_on, :position
789
-
790
- # @return [Array<Symbol>]
791
- def self.required_fields
792
- %i[app_url bucket created_at creator id inherits_status parent status title type updated_at url visible_to_clients].freeze
793
- end
794
-
795
- def initialize(data = {})
796
- @app_url = data["app_url"]
797
- @bucket = parse_type(data["bucket"], "TodoBucket")
798
- @created_at = parse_datetime(data["created_at"])
799
- @creator = parse_type(data["creator"], "Person")
800
- @id = parse_integer(data["id"])
801
- @inherits_status = parse_boolean(data["inherits_status"])
802
- @parent = parse_type(data["parent"], "RecordingParent")
803
- @status = data["status"]
804
- @title = data["title"]
805
- @type = data["type"]
806
- @updated_at = parse_datetime(data["updated_at"])
807
- @url = data["url"]
808
- @visible_to_clients = parse_boolean(data["visible_to_clients"])
809
- @assignees = parse_array(data["assignees"], "Person")
810
- @bookmark_url = data["bookmark_url"]
811
- @completed = parse_boolean(data["completed"])
812
- @completed_at = parse_datetime(data["completed_at"])
813
- @completer = parse_type(data["completer"], "Person")
814
- @completion_url = data["completion_url"]
815
- @due_on = data["due_on"]
816
- @position = parse_integer(data["position"])
817
- end
818
-
819
- def to_h
820
- {
821
- "app_url" => @app_url,
822
- "bucket" => @bucket,
823
- "created_at" => @created_at,
824
- "creator" => @creator,
825
- "id" => @id,
826
- "inherits_status" => @inherits_status,
827
- "parent" => @parent,
828
- "status" => @status,
829
- "title" => @title,
830
- "type" => @type,
831
- "updated_at" => @updated_at,
832
- "url" => @url,
833
- "visible_to_clients" => @visible_to_clients,
834
- "assignees" => @assignees,
835
- "bookmark_url" => @bookmark_url,
836
- "completed" => @completed,
837
- "completed_at" => @completed_at,
838
- "completer" => @completer,
839
- "completion_url" => @completion_url,
840
- "due_on" => @due_on,
841
- "position" => @position,
842
- }.compact
843
- end
844
-
845
- def to_json(*args)
846
- to_h.to_json(*args)
847
- end
848
- end
849
-
850
791
  # CardTable
851
792
  class CardTable
852
793
  include TypeHelpers
853
- attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :lists, :subscribers, :subscription_url, :wormholes
794
+ attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :lists, :parent, :position, :public_link_url, :subscribers, :subscription_url, :wormholes
854
795
 
855
796
  # @return [Array<Symbol>]
856
797
  def self.required_fields
@@ -872,6 +813,9 @@ module Basecamp
872
813
  @visible_to_clients = parse_boolean(data["visible_to_clients"])
873
814
  @bookmark_url = data["bookmark_url"]
874
815
  @lists = parse_array(data["lists"], "CardColumn")
816
+ @parent = parse_type(data["parent"], "RecordingParent")
817
+ @position = parse_integer(data["position"])
818
+ @public_link_url = data["public_link_url"]
875
819
  @subscribers = parse_array(data["subscribers"], "Person")
876
820
  @subscription_url = data["subscription_url"]
877
821
  @wormholes = parse_array(data["wormholes"], "Wormhole")
@@ -893,6 +837,9 @@ module Basecamp
893
837
  "visible_to_clients" => @visible_to_clients,
894
838
  "bookmark_url" => @bookmark_url,
895
839
  "lists" => @lists,
840
+ "parent" => @parent,
841
+ "position" => @position,
842
+ "public_link_url" => @public_link_url,
896
843
  "subscribers" => @subscribers,
897
844
  "subscription_url" => @subscription_url,
898
845
  "wormholes" => @wormholes,
@@ -3819,7 +3766,7 @@ module Basecamp
3819
3766
  # Recording
3820
3767
  class Recording
3821
3768
  include TypeHelpers
3822
- attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :boosts_count, :boosts_url, :bubble_up_url, :category, :comments_count, :comments_url, :content, :content_attachments, :description, :description_attachments, :from, :group_on, :parent, :position, :replies_count, :replies_url, :service, :subject, :subscription_url
3769
+ attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :status, :title, :type, :updated_at, :url, :visible_to_clients, :bookmark_url, :boosts_count, :boosts_url, :bubble_up_url, :category, :comments_count, :comments_url, :content, :content_attachments, :description, :description_attachments, :from, :group_on, :parent, :position, :replies_count, :replies_url, :service, :subject, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
3823
3770
 
3824
3771
  # @return [Array<Symbol>]
3825
3772
  def self.required_fields
@@ -3859,6 +3806,9 @@ module Basecamp
3859
3806
  @service = parse_type(data["service"], "DoorService")
3860
3807
  @subject = data["subject"]
3861
3808
  @subscription_url = data["subscription_url"]
3809
+ @subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
3810
+ @subtasks_count = parse_integer(data["subtasks_count"])
3811
+ @subtasks_url = data["subtasks_url"]
3862
3812
  end
3863
3813
 
3864
3814
  def to_h
@@ -3895,6 +3845,9 @@ module Basecamp
3895
3845
  "service" => @service,
3896
3846
  "subject" => @subject,
3897
3847
  "subscription_url" => @subscription_url,
3848
+ "subtasks_completed_count" => @subtasks_completed_count,
3849
+ "subtasks_count" => @subtasks_count,
3850
+ "subtasks_url" => @subtasks_url,
3898
3851
  }.compact
3899
3852
  end
3900
3853
 
@@ -4462,6 +4415,71 @@ module Basecamp
4462
4415
  end
4463
4416
  end
4464
4417
 
4418
+ # Subtask
4419
+ class Subtask
4420
+ include TypeHelpers
4421
+ attr_accessor :app_url, :bucket, :created_at, :creator, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :completed, :completed_at, :completer, :completion_url, :due_on, :position
4422
+
4423
+ # @return [Array<Symbol>]
4424
+ def self.required_fields
4425
+ %i[app_url bucket created_at creator id inherits_status parent status title type updated_at url visible_to_clients].freeze
4426
+ end
4427
+
4428
+ def initialize(data = {})
4429
+ @app_url = data["app_url"]
4430
+ @bucket = parse_type(data["bucket"], "TodoBucket")
4431
+ @created_at = parse_datetime(data["created_at"])
4432
+ @creator = parse_type(data["creator"], "Person")
4433
+ @id = parse_integer(data["id"])
4434
+ @inherits_status = parse_boolean(data["inherits_status"])
4435
+ @parent = parse_type(data["parent"], "RecordingParent")
4436
+ @status = data["status"]
4437
+ @title = data["title"]
4438
+ @type = data["type"]
4439
+ @updated_at = parse_datetime(data["updated_at"])
4440
+ @url = data["url"]
4441
+ @visible_to_clients = parse_boolean(data["visible_to_clients"])
4442
+ @assignees = parse_array(data["assignees"], "Person")
4443
+ @bookmark_url = data["bookmark_url"]
4444
+ @completed = parse_boolean(data["completed"])
4445
+ @completed_at = parse_datetime(data["completed_at"])
4446
+ @completer = parse_type(data["completer"], "Person")
4447
+ @completion_url = data["completion_url"]
4448
+ @due_on = data["due_on"]
4449
+ @position = parse_integer(data["position"])
4450
+ end
4451
+
4452
+ def to_h
4453
+ {
4454
+ "app_url" => @app_url,
4455
+ "bucket" => @bucket,
4456
+ "created_at" => @created_at,
4457
+ "creator" => @creator,
4458
+ "id" => @id,
4459
+ "inherits_status" => @inherits_status,
4460
+ "parent" => @parent,
4461
+ "status" => @status,
4462
+ "title" => @title,
4463
+ "type" => @type,
4464
+ "updated_at" => @updated_at,
4465
+ "url" => @url,
4466
+ "visible_to_clients" => @visible_to_clients,
4467
+ "assignees" => @assignees,
4468
+ "bookmark_url" => @bookmark_url,
4469
+ "completed" => @completed,
4470
+ "completed_at" => @completed_at,
4471
+ "completer" => @completer,
4472
+ "completion_url" => @completion_url,
4473
+ "due_on" => @due_on,
4474
+ "position" => @position,
4475
+ }.compact
4476
+ end
4477
+
4478
+ def to_json(*args)
4479
+ to_h.to_json(*args)
4480
+ end
4481
+ end
4482
+
4465
4483
  # Template
4466
4484
  class Template
4467
4485
  include TypeHelpers
@@ -4503,27 +4521,27 @@ module Basecamp
4503
4521
  end
4504
4522
  end
4505
4523
 
4506
- # TemplateLibrary
4507
- class TemplateLibrary
4524
+ # TemplateLibraryCardTables
4525
+ class TemplateLibraryCardTables
4508
4526
  include TypeHelpers
4509
- attr_accessor :bucket, :todolists, :todoset
4527
+ attr_accessor :bucket, :card_tables, :kanban_boardset
4510
4528
 
4511
4529
  # @return [Array<Symbol>]
4512
4530
  def self.required_fields
4513
- %i[bucket todolists todoset].freeze
4531
+ %i[bucket card_tables kanban_boardset].freeze
4514
4532
  end
4515
4533
 
4516
4534
  def initialize(data = {})
4517
4535
  @bucket = parse_type(data["bucket"], "RecordingBucket")
4518
- @todolists = parse_array(data["todolists"], "Todolist")
4519
- @todoset = parse_type(data["todoset"], "RecordingParent")
4536
+ @card_tables = parse_array(data["card_tables"], "Recording")
4537
+ @kanban_boardset = data["kanban_boardset"]
4520
4538
  end
4521
4539
 
4522
4540
  def to_h
4523
4541
  {
4524
4542
  "bucket" => @bucket,
4525
- "todolists" => @todolists,
4526
- "todoset" => @todoset,
4543
+ "card_tables" => @card_tables,
4544
+ "kanban_boardset" => @kanban_boardset,
4527
4545
  }.compact
4528
4546
  end
4529
4547
 
@@ -4564,7 +4582,7 @@ module Basecamp
4564
4582
  # TemplateLibraryCopy
4565
4583
  class TemplateLibraryCopy
4566
4584
  include TypeHelpers
4567
- attr_accessor :destination_parent_id, :id, :source_recording_id, :status, :url, :destination_todolist
4585
+ attr_accessor :destination_parent_id, :id, :source_recording_id, :status, :url, :destination_card_table, :destination_todolist
4568
4586
 
4569
4587
  # @return [Array<Symbol>]
4570
4588
  def self.required_fields
@@ -4577,6 +4595,7 @@ module Basecamp
4577
4595
  @source_recording_id = parse_integer(data["source_recording_id"])
4578
4596
  @status = data["status"]
4579
4597
  @url = data["url"]
4598
+ @destination_card_table = parse_type(data["destination_card_table"], "CardTable")
4580
4599
  @destination_todolist = parse_type(data["destination_todolist"], "Todolist")
4581
4600
  end
4582
4601
 
@@ -4587,6 +4606,71 @@ module Basecamp
4587
4606
  "source_recording_id" => @source_recording_id,
4588
4607
  "status" => @status,
4589
4608
  "url" => @url,
4609
+ "destination_card_table" => @destination_card_table,
4610
+ "destination_todolist" => @destination_todolist,
4611
+ }.compact
4612
+ end
4613
+
4614
+ def to_json(*args)
4615
+ to_h.to_json(*args)
4616
+ end
4617
+ end
4618
+
4619
+ # TemplateLibraryTodolists
4620
+ class TemplateLibraryTodolists
4621
+ include TypeHelpers
4622
+ attr_accessor :bucket, :todolists, :todoset
4623
+
4624
+ # @return [Array<Symbol>]
4625
+ def self.required_fields
4626
+ %i[bucket todolists todoset].freeze
4627
+ end
4628
+
4629
+ def initialize(data = {})
4630
+ @bucket = parse_type(data["bucket"], "RecordingBucket")
4631
+ @todolists = parse_array(data["todolists"], "Todolist")
4632
+ @todoset = parse_type(data["todoset"], "RecordingParent")
4633
+ end
4634
+
4635
+ def to_h
4636
+ {
4637
+ "bucket" => @bucket,
4638
+ "todolists" => @todolists,
4639
+ "todoset" => @todoset,
4640
+ }.compact
4641
+ end
4642
+
4643
+ def to_json(*args)
4644
+ to_h.to_json(*args)
4645
+ end
4646
+ end
4647
+
4648
+ # Templatification
4649
+ class Templatification
4650
+ include TypeHelpers
4651
+ attr_accessor :id, :source_recording_id, :status, :url, :destination_card_table, :destination_todolist
4652
+
4653
+ # @return [Array<Symbol>]
4654
+ def self.required_fields
4655
+ %i[id source_recording_id status url].freeze
4656
+ end
4657
+
4658
+ def initialize(data = {})
4659
+ @id = parse_integer(data["id"])
4660
+ @source_recording_id = parse_integer(data["source_recording_id"])
4661
+ @status = data["status"]
4662
+ @url = data["url"]
4663
+ @destination_card_table = parse_type(data["destination_card_table"], "CardTable")
4664
+ @destination_todolist = parse_type(data["destination_todolist"], "Todolist")
4665
+ end
4666
+
4667
+ def to_h
4668
+ {
4669
+ "id" => @id,
4670
+ "source_recording_id" => @source_recording_id,
4671
+ "status" => @status,
4672
+ "url" => @url,
4673
+ "destination_card_table" => @destination_card_table,
4590
4674
  "destination_todolist" => @destination_todolist,
4591
4675
  }.compact
4592
4676
  end
@@ -4827,7 +4911,7 @@ module Basecamp
4827
4911
  # Todo
4828
4912
  class Todo
4829
4913
  include TypeHelpers
4830
- attr_accessor :app_url, :bucket, :content, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completion_subscribers, :completion_url, :description, :due_on, :position, :starts_on, :steps, :subscription_url
4914
+ attr_accessor :app_url, :bucket, :content, :created_at, :creator, :description_attachments, :id, :inherits_status, :parent, :status, :title, :type, :updated_at, :url, :visible_to_clients, :assignees, :bookmark_url, :boosts_count, :boosts_url, :comments_count, :comments_url, :completed, :completion_subscribers, :completion_url, :description, :due_on, :position, :starts_on, :steps, :subscription_url, :subtasks_completed_count, :subtasks_count, :subtasks_url
4831
4915
 
4832
4916
  # @return [Array<Symbol>]
4833
4917
  def self.required_fields
@@ -4863,8 +4947,11 @@ module Basecamp
4863
4947
  @due_on = data["due_on"]
4864
4948
  @position = parse_integer(data["position"])
4865
4949
  @starts_on = data["starts_on"]
4866
- @steps = parse_array(data["steps"], "CardStep")
4950
+ @steps = parse_array(data["steps"], "Subtask")
4867
4951
  @subscription_url = data["subscription_url"]
4952
+ @subtasks_completed_count = parse_integer(data["subtasks_completed_count"])
4953
+ @subtasks_count = parse_integer(data["subtasks_count"])
4954
+ @subtasks_url = data["subtasks_url"]
4868
4955
  end
4869
4956
 
4870
4957
  def to_h
@@ -4899,6 +4986,9 @@ module Basecamp
4899
4986
  "starts_on" => @starts_on,
4900
4987
  "steps" => @steps,
4901
4988
  "subscription_url" => @subscription_url,
4989
+ "subtasks_completed_count" => @subtasks_completed_count,
4990
+ "subtasks_count" => @subtasks_count,
4991
+ "subtasks_url" => @subtasks_url,
4902
4992
  }.compact
4903
4993
  end
4904
4994
 
@@ -5875,5 +5965,9 @@ module Basecamp
5875
5965
  to_h.to_json(*args)
5876
5966
  end
5877
5967
  end
5968
+
5969
+ # CardStep
5970
+ # @deprecated renamed to Subtask, the same Kanban::Step record.
5971
+ CardStep = Subtask
5878
5972
  end
5879
5973
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Basecamp
4
- VERSION = "0.20.0"
5
- API_VERSION = "2026-09-02"
4
+ VERSION = "0.21.0"
5
+ API_VERSION = "2026-09-15"
6
6
  end
@@ -82,6 +82,7 @@ class ServiceGenerator
82
82
  'Projects' => 'Projects',
83
83
  'ClientFeatures' => 'ClientFeatures',
84
84
  'Boosts' => 'Boosts',
85
+ 'Subtasks' => 'Subtasks',
85
86
  'Untagged' => 'Miscellaneous'
86
87
  }.freeze
87
88
 
@@ -214,7 +215,10 @@ class ServiceGenerator
214
215
  'Search' => 'search',
215
216
  'CreateProjectFromTemplate' => 'create_project',
216
217
  'GetProjectConstruction' => 'get_construction',
217
- 'GetTemplateLibrary' => 'get_library',
218
+ 'GetTemplateLibraryTodolists' => 'get_library_todolists',
219
+ 'GetTemplateLibraryCardTables' => 'get_library_card_tables',
220
+ 'CreateTemplateLibraryCardTable' => 'create_library_card_table',
221
+ 'CreateTemplateLibraryTodolist' => 'create_library_todolist',
218
222
  'CreateTemplateLibraryCopy' => 'create_library_copy',
219
223
  'GetTemplateLibraryCopy' => 'get_library_copy',
220
224
  'GetRecordingTimesheet' => 'for_recording',
@@ -358,7 +362,7 @@ class ServiceGenerator
358
362
  lineupmarker clientapproval clientapprovals clientcorrespondence
359
363
  clientcorrespondences clientreply clientreplies forwardreply
360
364
  forwardreplies campfireline campfirelines todolistgroup todolistgroups
361
- todolistorgroup uploadversions hillchart hillcharts
365
+ todolistorgroup uploadversions hillchart hillcharts subtask subtasks
362
366
  wormhole wormholes
363
367
  ].freeze
364
368
 
@@ -791,7 +795,7 @@ class ServiceGenerator
791
795
  op[:path_params].each do |p|
792
796
  ruby_name = to_snake_case(p[:name])
793
797
  type = p[:type] || 'Integer'
794
- desc = p[:description] || "#{ruby_name.gsub('_', ' ')} ID"
798
+ desc = yard_param_description(p[:description] || "#{ruby_name.gsub('_', ' ')} ID")
795
799
  lines << " # @param #{ruby_name} [#{type}] #{desc}"
796
800
  end
797
801
 
@@ -132,6 +132,59 @@ def deprecation_doc_lines(reason, indent:, tag_prefix: '')
132
132
  out
133
133
  end
134
134
 
135
+ # Whether the main loop emits a class for this component. The alias pass checks
136
+ # its targets against exactly this predicate, so an alias can never name a
137
+ # class that was not emitted (a *RequestContent target used to produce
138
+ # `Alias = CreateCardStepRequestContent`, a NameError the moment types.rb loads).
139
+ def emits_class?(name, schema)
140
+ return false if SKIP_PATTERNS.any? { |p| name.match?(p) }
141
+
142
+ schema.is_a?(Hash) && schema['type'] == 'object' && !(schema['properties'] || {}).empty?
143
+ end
144
+
145
+ DEPRECATED_ALIAS_KEYS = %w[$ref deprecated description x-deprecated-reason].freeze
146
+
147
+ # A component that is nothing but a deprecated $ref: a rename kept for
148
+ # compatibility, e.g. CardStep -> Subtask. `deprecated` must be the JSON boolean
149
+ # true, as in every other generator; 1 or "false" is not an alias.
150
+ def deprecated_alias?(schema)
151
+ schema.is_a?(Hash) && schema['deprecated'] == true && schema['$ref'].is_a?(String) &&
152
+ (schema.keys - DEPRECATED_ALIAS_KEYS).empty?
153
+ end
154
+
155
+ # Constants Basecamp::Types already holds besides the generated classes.
156
+ RESERVED_TYPE_CONSTANTS = %w[TypeHelpers].freeze
157
+
158
+ # Every deprecated alias as [alias, target], sorted by alias. Aborts on one this
159
+ # generator cannot place, the same cases every SDK generator refuses: a missing
160
+ # target, an alias of an alias (rejected, not resolved), a target that is not an
161
+ # object model, a target this generator does not emit, and a name collision.
162
+ def deprecated_aliases(schemas)
163
+ schemas.keys.sort.filter_map do |name|
164
+ schema = schemas[name]
165
+ next unless deprecated_alias?(schema)
166
+
167
+ target = schema['$ref'].split('/').last
168
+ target_schema = schemas[target]
169
+ problem =
170
+ if !schemas.key?(target)
171
+ 'target schema does not exist'
172
+ elsif deprecated_alias?(target_schema)
173
+ "target is itself a deprecated alias; point #{name} at the model directly"
174
+ elsif !(target_schema.is_a?(Hash) && target_schema['type'] == 'object' &&
175
+ !(target_schema['properties'] || {}).empty?)
176
+ 'target is not an object model'
177
+ elsif !emits_class?(target, target_schema)
178
+ 'target class was not emitted (SKIP_PATTERNS excludes it)'
179
+ elsif RESERVED_TYPE_CONSTANTS.include?(name)
180
+ "#{name} collides with an existing constant in Basecamp::Types"
181
+ end
182
+ abort "Error: deprecated alias #{name} -> #{target}: #{problem}" if problem
183
+
184
+ [ name, target ]
185
+ end
186
+ end
187
+
135
188
  # Main execution
136
189
  if __FILE__ == $PROGRAM_NAME
137
190
  openapi_path = ARGV[0] || File.expand_path('../../openapi.json', __dir__)
@@ -141,6 +194,13 @@ if __FILE__ == $PROGRAM_NAME
141
194
  exit 1
142
195
  end
143
196
 
197
+ # UTF-8 regardless of process locale — see generate-metadata.rb
198
+ schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
199
+ sorted = schemas.keys.sort
200
+ # Validated before the first line is printed, so a refusal leaves no partial
201
+ # types.rb on stdout.
202
+ aliases = deprecated_aliases(schemas)
203
+
144
204
  puts header
145
205
  puts generate_helpers
146
206
  puts ''
@@ -148,18 +208,11 @@ if __FILE__ == $PROGRAM_NAME
148
208
  puts ' module Types'
149
209
  puts ' include TypeHelpers'
150
210
 
151
- # UTF-8 regardless of process locale — see generate-metadata.rb
152
- schemas = JSON.parse(File.read(openapi_path, encoding: 'UTF-8'))['components']['schemas'] || {}
153
- sorted = schemas.keys.sort
154
-
155
211
  sorted.each do |name|
156
- next if SKIP_PATTERNS.any? { |p| name.match?(p) }
157
-
158
212
  schema = schemas[name]
159
- next unless schema['type'] == 'object'
213
+ next unless emits_class?(name, schema)
160
214
 
161
- properties = schema['properties'] || {}
162
- next if properties.empty?
215
+ properties = schema['properties']
163
216
 
164
217
  required_fields = schema['required'] || []
165
218
  required_set = required_fields.to_set
@@ -273,6 +326,17 @@ if __FILE__ == $PROGRAM_NAME
273
326
  puts ' end'
274
327
  end
275
328
 
329
+ # Deprecated former names (see deprecated_aliases), emitted after every class
330
+ # because the alias must reference an already-defined constant. The alias
331
+ # also keeps Basecamp::Types.const_get("CardStep") resolving.
332
+ aliases.each do |name, target|
333
+ schema = schemas[name]
334
+ puts ''
335
+ puts " # #{name}"
336
+ puts deprecation_doc_lines(schema['x-deprecated-reason'] || 'deprecated', indent: ' ')
337
+ puts " #{name} = #{target}"
338
+ end
339
+
276
340
  puts ' end'
277
341
  puts 'end'
278
342
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecamp-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.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-09-20 00:00:00.000000000 Z
11
+ date: 2026-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -224,6 +224,7 @@ files:
224
224
  - lib/basecamp/generated/services/schedules_service.rb
225
225
  - lib/basecamp/generated/services/search_service.rb
226
226
  - lib/basecamp/generated/services/subscriptions_service.rb
227
+ - lib/basecamp/generated/services/subtasks_service.rb
227
228
  - lib/basecamp/generated/services/templates_service.rb
228
229
  - lib/basecamp/generated/services/timeline_service.rb
229
230
  - lib/basecamp/generated/services/timesheets_service.rb