basecamp-sdk 0.19.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 +4 -4
- data/README.md +1 -0
- data/lib/basecamp/client.rb +5 -5
- data/lib/basecamp/generated/metadata.json +226 -4
- data/lib/basecamp/generated/services/card_steps_service.rb +3 -1
- data/lib/basecamp/generated/services/lineup_service.rb +8 -0
- data/lib/basecamp/generated/services/my_notifications_service.rb +2 -1
- data/lib/basecamp/generated/services/recordings_service.rb +2 -2
- data/lib/basecamp/generated/services/subtasks_service.rb +97 -0
- data/lib/basecamp/generated/services/templates_service.rb +68 -9
- data/lib/basecamp/generated/services/timesheets_service.rb +1 -1
- data/lib/basecamp/generated/types.rb +175 -81
- data/lib/basecamp/version.rb +2 -2
- data/scripts/generate-metadata.rb +95 -12
- data/scripts/generate-services.rb +128 -25
- data/scripts/generate-types.rb +73 -9
- metadata +3 -3
- data/lib/basecamp/generated/services/automation_service.rb +0 -19
|
@@ -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-
|
|
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"], "
|
|
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
|
-
#
|
|
4507
|
-
class
|
|
4524
|
+
# TemplateLibraryCardTables
|
|
4525
|
+
class TemplateLibraryCardTables
|
|
4508
4526
|
include TypeHelpers
|
|
4509
|
-
attr_accessor :bucket, :
|
|
4527
|
+
attr_accessor :bucket, :card_tables, :kanban_boardset
|
|
4510
4528
|
|
|
4511
4529
|
# @return [Array<Symbol>]
|
|
4512
4530
|
def self.required_fields
|
|
4513
|
-
%i[bucket
|
|
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
|
-
@
|
|
4519
|
-
@
|
|
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
|
-
"
|
|
4526
|
-
"
|
|
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"], "
|
|
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
|
data/lib/basecamp/version.rb
CHANGED
|
@@ -13,7 +13,46 @@ require 'time'
|
|
|
13
13
|
|
|
14
14
|
# Extract metadata from OpenAPI spec
|
|
15
15
|
class MetadataExtractor
|
|
16
|
-
|
|
16
|
+
# A Path Item Object is read by EXCLUSION: its non-operation fields are a
|
|
17
|
+
# closed, spec-defined set, its extensions are `x-` prefixed, and every other
|
|
18
|
+
# field is an operation. A verb list here dropped an operation's metadata in
|
|
19
|
+
# silence for any method outside the five it knew (#925). Nothing in this
|
|
20
|
+
# file is verb-specific — metadata is keyed by operationId — so the walk is
|
|
21
|
+
# total and no verb bound belongs here.
|
|
22
|
+
NON_OPERATION_FIELDS = %w[summary description servers parameters].freeze
|
|
23
|
+
|
|
24
|
+
# Membership is decided above, by exclusion. The shared declaration every
|
|
25
|
+
# generator reads (spec/generated-verbs.json) is ORDERED, and that order is
|
|
26
|
+
# reused here to keep metadata.json byte-stable. Order is not membership: a
|
|
27
|
+
# verb absent from the declaration sorts deterministically to the end by name
|
|
28
|
+
# and is still extracted, because nothing in this file is verb-specific.
|
|
29
|
+
# The self-test points this at a crafted declaration to prove the bound is
|
|
30
|
+
# sourced from the shared file rather than a private literal; production runs
|
|
31
|
+
# never set it.
|
|
32
|
+
GENERATED_VERBS_FILE = ENV.fetch(
|
|
33
|
+
'BASECAMP_GENERATED_VERBS', File.expand_path('../../spec/generated-verbs.json', __dir__)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
METHOD_ORDER = begin
|
|
37
|
+
verbs = JSON.parse(File.read(GENERATED_VERBS_FILE, encoding: 'UTF-8'))['verbs']
|
|
38
|
+
# An HTTP method is a TOKEN, so the rule is a positive character class rather
|
|
39
|
+
# than a blankness predicate: `[a-z]+`, identical in all six loaders. Two
|
|
40
|
+
# review rounds chased "blank" across languages — Kotlin and Rust rejected a
|
|
41
|
+
# space while the others accepted it, then Ruby's ASCII `strip` accepted a
|
|
42
|
+
# non-breaking space the Unicode-aware ones rejected — and the next
|
|
43
|
+
# disagreement was guaranteed, because every language defines whitespace
|
|
44
|
+
# differently (Java's Character.isWhitespace excludes U+00A0; Rust's
|
|
45
|
+
# char::is_whitespace includes it; Ruby's String#strip is ASCII-only). A
|
|
46
|
+
# closed positive rule has no such seam: "", " ", "\u00a0", "GET" and 1 are
|
|
47
|
+
# all rejected the same way everywhere.
|
|
48
|
+
unless verbs.is_a?(Array) && !verbs.empty? && verbs.all? { |v| v.is_a?(String) && v.match?(/\A[a-z]+\z/) }
|
|
49
|
+
abort "Error: #{GENERATED_VERBS_FILE} must declare a non-empty `verbs` array of lowercase " \
|
|
50
|
+
'ASCII method names (/\A[a-z]+\z/).'
|
|
51
|
+
end
|
|
52
|
+
verbs.freeze
|
|
53
|
+
rescue Errno::ENOENT, JSON::ParserError => e
|
|
54
|
+
abort "Error: cannot read the generated-verb declaration #{GENERATED_VERBS_FILE}: #{e.message}"
|
|
55
|
+
end
|
|
17
56
|
|
|
18
57
|
def initialize(openapi_path)
|
|
19
58
|
# Read as UTF-8 regardless of process locale (LC_ALL=C would otherwise read
|
|
@@ -24,13 +63,9 @@ class MetadataExtractor
|
|
|
24
63
|
def extract
|
|
25
64
|
operations = {}
|
|
26
65
|
|
|
27
|
-
(@openapi['paths'] || {}).
|
|
28
|
-
|
|
29
|
-
operation = path_item[method]
|
|
30
|
-
next unless operation
|
|
31
|
-
|
|
66
|
+
(@openapi['paths'] || {}).each do |path, path_item|
|
|
67
|
+
each_operation(path, path_item) do |_method, operation|
|
|
32
68
|
operation_id = operation['operationId']
|
|
33
|
-
next unless operation_id
|
|
34
69
|
|
|
35
70
|
metadata = extract_operation_metadata(operation)
|
|
36
71
|
operations[operation_id] = metadata if metadata.any?
|
|
@@ -61,11 +96,8 @@ class MetadataExtractor
|
|
|
61
96
|
# in "[]" names an array of people. "$" is the body itself.
|
|
62
97
|
def person_id_sites
|
|
63
98
|
sites = {}
|
|
64
|
-
(@openapi['paths'] || {}).
|
|
65
|
-
|
|
66
|
-
operation = path_item[method]
|
|
67
|
-
next unless operation && operation['operationId']
|
|
68
|
-
|
|
99
|
+
(@openapi['paths'] || {}).each do |path, path_item|
|
|
100
|
+
each_operation(path, path_item) do |_method, operation|
|
|
69
101
|
paths = (operation['responses'] || {}).flat_map do |code, response|
|
|
70
102
|
next [] unless code.to_s.start_with?('2')
|
|
71
103
|
|
|
@@ -117,6 +149,57 @@ class MetadataExtractor
|
|
|
117
149
|
raise "person id site under additionalProperties at #{nested.first} has no runtime path syntax" if nested.any?
|
|
118
150
|
end
|
|
119
151
|
|
|
152
|
+
# Yields [verb, operation] for every operation in a path item. A field that is
|
|
153
|
+
# neither a known non-operation field nor a readable operation object stops
|
|
154
|
+
# the run by name rather than being stepped over.
|
|
155
|
+
def each_operation(path, path_item)
|
|
156
|
+
unless path_item.is_a?(Hash)
|
|
157
|
+
abort "Error: openapi.json path #{path} is a #{path_item.class}, not a path item object."
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if path_item.key?('$ref')
|
|
161
|
+
abort "Error: openapi.json path #{path} is a $ref to #{path_item['$ref'].inspect}. " \
|
|
162
|
+
'This extractor cannot resolve a path-item reference, and skipping it would leave ' \
|
|
163
|
+
'every operation behind it without runtime metadata.'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# OpenAPI 3.2's `additionalOperations` is a MAP of method to Operation, not an
|
|
167
|
+
# operation. Read as one it carries no operationId, so it would drop every
|
|
168
|
+
# operation inside it without saying so. Refuse by name until the walk learns
|
|
169
|
+
# the map shape.
|
|
170
|
+
if path_item.key?('additionalOperations')
|
|
171
|
+
abort "Error: openapi.json path #{path} declares `additionalOperations`, which OpenAPI 3.2 " \
|
|
172
|
+
'defines as a map of method to Operation. This walk reads a path-item field as a ' \
|
|
173
|
+
'single operation, so it would drop every operation inside it. Teach the walk the map ' \
|
|
174
|
+
'shape, or take the field out of the spec.'
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
fields = path_item.keys.reject { |f| NON_OPERATION_FIELDS.include?(f) || f.start_with?('x-') }
|
|
178
|
+
fields.sort_by! { |f| [ METHOD_ORDER.index(f) || METHOD_ORDER.length, f ] }
|
|
179
|
+
|
|
180
|
+
fields.each do |field|
|
|
181
|
+
operation = path_item[field]
|
|
182
|
+
|
|
183
|
+
unless operation.is_a?(Hash)
|
|
184
|
+
abort "Error: openapi.json path #{path} field #{field.inspect} is a #{operation.class}, " \
|
|
185
|
+
'which is neither a known non-operation field nor an operation object. If a later ' \
|
|
186
|
+
'OpenAPI version added it, add it to NON_OPERATION_FIELDS with a reason.'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# An operation has to be IDENTIFIABLE. OpenAPI lets operationId be omitted,
|
|
190
|
+
# and every walker here used to step over one that was — a silent drop of a
|
|
191
|
+
# real operation, which is #925 wearing a different field.
|
|
192
|
+
op_id = operation['operationId']
|
|
193
|
+
unless op_id.is_a?(String) && !op_id.empty?
|
|
194
|
+
abort "Error: openapi.json declares #{field.upcase} #{path} with no operationId. " \
|
|
195
|
+
'Everything downstream is keyed by it, and skipping the operation would drop it ' \
|
|
196
|
+
'from the SDK in silence.'
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
yield field, operation
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
120
203
|
private
|
|
121
204
|
|
|
122
205
|
def extract_operation_metadata(operation)
|