superthread 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +4 -0
- data/LICENSE +21 -0
- data/README.md +492 -0
- data/exe/suth +19 -0
- data/lib/superthread/cli/accounts.rb +240 -0
- data/lib/superthread/cli/activity.rb +210 -0
- data/lib/superthread/cli/base.rb +355 -0
- data/lib/superthread/cli/boards.rb +131 -0
- data/lib/superthread/cli/cards.rb +530 -0
- data/lib/superthread/cli/checklists.rb +223 -0
- data/lib/superthread/cli/comments.rb +86 -0
- data/lib/superthread/cli/completion.rb +306 -0
- data/lib/superthread/cli/concerns/board_resolvable.rb +70 -0
- data/lib/superthread/cli/concerns/confirmable.rb +55 -0
- data/lib/superthread/cli/concerns/date_parsable.rb +196 -0
- data/lib/superthread/cli/concerns/list_resolvable.rb +53 -0
- data/lib/superthread/cli/concerns/space_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/sprint_resolvable.rb +55 -0
- data/lib/superthread/cli/concerns/tag_resolvable.rb +49 -0
- data/lib/superthread/cli/concerns/user_resolvable.rb +52 -0
- data/lib/superthread/cli/concerns/workspace_resolvable.rb +83 -0
- data/lib/superthread/cli/config.rb +129 -0
- data/lib/superthread/cli/formatter.rb +388 -0
- data/lib/superthread/cli/lists.rb +85 -0
- data/lib/superthread/cli/main.rb +121 -0
- data/lib/superthread/cli/members.rb +19 -0
- data/lib/superthread/cli/notes.rb +64 -0
- data/lib/superthread/cli/pages.rb +128 -0
- data/lib/superthread/cli/projects.rb +124 -0
- data/lib/superthread/cli/replies.rb +94 -0
- data/lib/superthread/cli/search.rb +34 -0
- data/lib/superthread/cli/setup.rb +253 -0
- data/lib/superthread/cli/spaces.rb +141 -0
- data/lib/superthread/cli/sprints.rb +32 -0
- data/lib/superthread/cli/tags.rb +86 -0
- data/lib/superthread/cli/ui/gum_prompt.rb +58 -0
- data/lib/superthread/cli/ui/plain_prompt.rb +73 -0
- data/lib/superthread/cli/ui.rb +263 -0
- data/lib/superthread/cli/workspaces.rb +105 -0
- data/lib/superthread/cli.rb +12 -0
- data/lib/superthread/client.rb +207 -0
- data/lib/superthread/configuration.rb +354 -0
- data/lib/superthread/connection.rb +57 -0
- data/lib/superthread/error.rb +164 -0
- data/lib/superthread/mention_formatter.rb +96 -0
- data/lib/superthread/model.rb +178 -0
- data/lib/superthread/models/board.rb +59 -0
- data/lib/superthread/models/card.rb +321 -0
- data/lib/superthread/models/checklist.rb +91 -0
- data/lib/superthread/models/checklist_item.rb +69 -0
- data/lib/superthread/models/comment.rb +71 -0
- data/lib/superthread/models/concerns/archivable.rb +32 -0
- data/lib/superthread/models/concerns/presentable.rb +113 -0
- data/lib/superthread/models/concerns/timestampable.rb +91 -0
- data/lib/superthread/models/list.rb +67 -0
- data/lib/superthread/models/member.rb +40 -0
- data/lib/superthread/models/note.rb +56 -0
- data/lib/superthread/models/page.rb +70 -0
- data/lib/superthread/models/project.rb +83 -0
- data/lib/superthread/models/space.rb +71 -0
- data/lib/superthread/models/sprint.rb +53 -0
- data/lib/superthread/models/tag.rb +52 -0
- data/lib/superthread/models/team.rb +68 -0
- data/lib/superthread/models/user.rb +76 -0
- data/lib/superthread/models.rb +12 -0
- data/lib/superthread/object.rb +285 -0
- data/lib/superthread/objects/collection.rb +179 -0
- data/lib/superthread/resources/base.rb +204 -0
- data/lib/superthread/resources/boards.rb +150 -0
- data/lib/superthread/resources/cards.rb +363 -0
- data/lib/superthread/resources/comments.rb +163 -0
- data/lib/superthread/resources/notes.rb +61 -0
- data/lib/superthread/resources/pages.rb +110 -0
- data/lib/superthread/resources/projects.rb +117 -0
- data/lib/superthread/resources/search.rb +46 -0
- data/lib/superthread/resources/spaces.rb +104 -0
- data/lib/superthread/resources/sprints.rb +37 -0
- data/lib/superthread/resources/tags.rb +52 -0
- data/lib/superthread/resources/users.rb +29 -0
- data/lib/superthread/version.rb +6 -0
- data/lib/superthread/version_checker.rb +174 -0
- data/lib/superthread.rb +30 -0
- metadata +259 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread space.
|
|
6
|
+
#
|
|
7
|
+
# Spaces are organizational containers within a workspace that group
|
|
8
|
+
# related projects, pages, and other content. They can have dedicated
|
|
9
|
+
# members with specific access.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# space = client.spaces.find(workspace_id, space_id)
|
|
13
|
+
# space.title # => "Engineering"
|
|
14
|
+
# space.members # => [#<Superthread::Models::Member ...>]
|
|
15
|
+
# space.archived? # => false
|
|
16
|
+
class Space < Superthread::Model
|
|
17
|
+
include Concerns::Archivable
|
|
18
|
+
include Concerns::Presentable
|
|
19
|
+
include Concerns::Timestampable
|
|
20
|
+
|
|
21
|
+
detail_fields :id, :title, :description, :time_created
|
|
22
|
+
list_columns :id, :title
|
|
23
|
+
|
|
24
|
+
# @!attribute [rw] id
|
|
25
|
+
# @return [String] unique space identifier
|
|
26
|
+
attribute :id, Shale::Type::String
|
|
27
|
+
|
|
28
|
+
# @!attribute [rw] type
|
|
29
|
+
# @return [String] space type
|
|
30
|
+
attribute :type, Shale::Type::String
|
|
31
|
+
|
|
32
|
+
# @!attribute [rw] team_id
|
|
33
|
+
# @return [String] ID of the team/workspace this space belongs to
|
|
34
|
+
attribute :team_id, Shale::Type::String
|
|
35
|
+
|
|
36
|
+
# @!attribute [rw] title
|
|
37
|
+
# @return [String] display title of the space
|
|
38
|
+
attribute :title, Shale::Type::String
|
|
39
|
+
|
|
40
|
+
# @!attribute [rw] description
|
|
41
|
+
# @return [String] space description or purpose
|
|
42
|
+
attribute :description, Shale::Type::String
|
|
43
|
+
|
|
44
|
+
# @!attribute [rw] icon
|
|
45
|
+
# @return [Object] emoji or icon data for the space
|
|
46
|
+
attribute :icon, Shale::Type::Value
|
|
47
|
+
|
|
48
|
+
# @!attribute [rw] user_id
|
|
49
|
+
# @return [String] ID of the user who created the space
|
|
50
|
+
attribute :user_id, Shale::Type::String
|
|
51
|
+
|
|
52
|
+
# @!attribute [rw] time_created
|
|
53
|
+
# @return [Integer] Unix timestamp when the space was created
|
|
54
|
+
attribute :time_created, Shale::Type::Integer
|
|
55
|
+
|
|
56
|
+
# @!attribute [rw] time_updated
|
|
57
|
+
# @return [Integer] Unix timestamp when the space was last updated
|
|
58
|
+
attribute :time_updated, Shale::Type::Integer
|
|
59
|
+
|
|
60
|
+
# @!attribute [rw] members
|
|
61
|
+
# @return [Array<Member>] users with access to this space
|
|
62
|
+
attribute :members, Member, collection: true
|
|
63
|
+
|
|
64
|
+
# @!attribute [rw] archived
|
|
65
|
+
# @return [Hash{String => Object}, nil] archive metadata with user_id and time_archived
|
|
66
|
+
attribute :archived, Shale::Type::Value
|
|
67
|
+
|
|
68
|
+
timestamps :time_created, :time_updated
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread sprint.
|
|
6
|
+
#
|
|
7
|
+
# Sprints are time-boxed iterations used for agile planning. Cards
|
|
8
|
+
# can be assigned to sprints to organize work into release cycles.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# sprint = client.sprints.find(workspace_id, sprint_id)
|
|
12
|
+
# sprint.title # => "Sprint 42"
|
|
13
|
+
# sprint.start_time # => 2024-01-15 00:00:00 UTC
|
|
14
|
+
class Sprint < Superthread::Model
|
|
15
|
+
include Concerns::Presentable
|
|
16
|
+
include Concerns::Timestampable
|
|
17
|
+
|
|
18
|
+
detail_fields :id, :title, :start_date, :time_created, :time_updated
|
|
19
|
+
list_columns :id, :title, :status
|
|
20
|
+
|
|
21
|
+
# @!attribute [rw] id
|
|
22
|
+
# @return [String] unique sprint identifier
|
|
23
|
+
attribute :id, Shale::Type::String
|
|
24
|
+
|
|
25
|
+
# @!attribute [rw] team_id
|
|
26
|
+
# @return [String] ID of the team/workspace this sprint belongs to
|
|
27
|
+
attribute :team_id, Shale::Type::String
|
|
28
|
+
|
|
29
|
+
# @!attribute [rw] title
|
|
30
|
+
# @return [String] display title of the sprint
|
|
31
|
+
attribute :title, Shale::Type::String
|
|
32
|
+
|
|
33
|
+
# @!attribute [rw] start_date
|
|
34
|
+
# @return [Integer] Unix timestamp when the sprint starts
|
|
35
|
+
attribute :start_date, Shale::Type::Integer
|
|
36
|
+
|
|
37
|
+
# @!attribute [rw] time_created
|
|
38
|
+
# @return [Integer] Unix timestamp when the sprint was created
|
|
39
|
+
attribute :time_created, Shale::Type::Integer
|
|
40
|
+
|
|
41
|
+
# @!attribute [rw] time_updated
|
|
42
|
+
# @return [Integer] Unix timestamp when the sprint was last updated
|
|
43
|
+
attribute :time_updated, Shale::Type::Integer
|
|
44
|
+
|
|
45
|
+
# @!attribute [rw] lists
|
|
46
|
+
# @return [Array<List>] columns/lists available in this sprint
|
|
47
|
+
attribute :lists, List, collection: true
|
|
48
|
+
|
|
49
|
+
timestamps :time_created, :time_updated
|
|
50
|
+
timestamps start_date: :start_time
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread tag/label.
|
|
6
|
+
#
|
|
7
|
+
# Tags are colored labels that can be applied to cards for categorization
|
|
8
|
+
# and filtering. They track how many cards use each tag.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# tag = card.tags.first
|
|
12
|
+
# tag.name # => "bug"
|
|
13
|
+
# tag.color # => "red"
|
|
14
|
+
# tag.total_cards # => 17
|
|
15
|
+
class Tag < Superthread::Model
|
|
16
|
+
include Concerns::Presentable
|
|
17
|
+
|
|
18
|
+
presents_as :name
|
|
19
|
+
|
|
20
|
+
detail_fields :id, :name, :color, :total_cards
|
|
21
|
+
list_columns :id, :name, :color
|
|
22
|
+
|
|
23
|
+
# @!attribute [rw] id
|
|
24
|
+
# @return [String] unique tag identifier
|
|
25
|
+
attribute :id, Shale::Type::String
|
|
26
|
+
|
|
27
|
+
# @!attribute [rw] team_id
|
|
28
|
+
# @return [String] ID of the team/workspace this tag belongs to
|
|
29
|
+
attribute :team_id, Shale::Type::String
|
|
30
|
+
|
|
31
|
+
# @!attribute [rw] project_id
|
|
32
|
+
# @return [String] ID of the project this tag is scoped to, if any
|
|
33
|
+
attribute :project_id, Shale::Type::String
|
|
34
|
+
|
|
35
|
+
# @!attribute [rw] name
|
|
36
|
+
# @return [String] display name of the tag
|
|
37
|
+
attribute :name, Shale::Type::String
|
|
38
|
+
|
|
39
|
+
# @!attribute [rw] slug
|
|
40
|
+
# @return [String] URL-safe version of the tag name
|
|
41
|
+
attribute :slug, Shale::Type::String
|
|
42
|
+
|
|
43
|
+
# @!attribute [rw] color
|
|
44
|
+
# @return [String] color code for visual styling
|
|
45
|
+
attribute :color, Shale::Type::String
|
|
46
|
+
|
|
47
|
+
# @!attribute [rw] total_cards
|
|
48
|
+
# @return [Integer] number of cards using this tag
|
|
49
|
+
attribute :total_cards, Shale::Type::Integer
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread team/workspace.
|
|
6
|
+
#
|
|
7
|
+
# Teams are the top-level organizational unit in Superthread. All boards,
|
|
8
|
+
# projects, spaces, and users belong to a team. Users can be members of
|
|
9
|
+
# multiple teams.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# user = client.users.me
|
|
13
|
+
# user.teams.each do |team|
|
|
14
|
+
# puts "#{team.team_name} (#{team.id})"
|
|
15
|
+
# end
|
|
16
|
+
class Team < Superthread::Model
|
|
17
|
+
include Concerns::Presentable
|
|
18
|
+
include Concerns::Timestampable
|
|
19
|
+
|
|
20
|
+
presents_as :team_name
|
|
21
|
+
|
|
22
|
+
# @!attribute [rw] id
|
|
23
|
+
# @return [String] unique team identifier (workspace ID)
|
|
24
|
+
attribute :id, Shale::Type::String
|
|
25
|
+
|
|
26
|
+
# @!attribute [rw] team_name
|
|
27
|
+
# @return [String] display name of the team
|
|
28
|
+
attribute :team_name, Shale::Type::String
|
|
29
|
+
|
|
30
|
+
# @!attribute [rw] sub_domain
|
|
31
|
+
# @return [String] subdomain for the team's Superthread URL
|
|
32
|
+
attribute :sub_domain, Shale::Type::String
|
|
33
|
+
|
|
34
|
+
# @!attribute [rw] role
|
|
35
|
+
# @return [String] current user's role in this team
|
|
36
|
+
attribute :role, Shale::Type::String
|
|
37
|
+
|
|
38
|
+
# @!attribute [rw] status
|
|
39
|
+
# @return [String] team status (e.g., "active")
|
|
40
|
+
attribute :status, Shale::Type::String
|
|
41
|
+
|
|
42
|
+
# @!attribute [rw] creator_user_id
|
|
43
|
+
# @return [String] ID of the user who created the team
|
|
44
|
+
attribute :creator_user_id, Shale::Type::String
|
|
45
|
+
|
|
46
|
+
# @!attribute [rw] subscription_plan_id
|
|
47
|
+
# @return [String] ID of the team's subscription plan
|
|
48
|
+
attribute :subscription_plan_id, Shale::Type::String
|
|
49
|
+
|
|
50
|
+
# @!attribute [rw] time_created
|
|
51
|
+
# @return [Integer] Unix timestamp when the team was created
|
|
52
|
+
attribute :time_created, Shale::Type::Integer
|
|
53
|
+
|
|
54
|
+
# @!attribute [rw] time_updated
|
|
55
|
+
# @return [Integer] Unix timestamp when the team was last updated
|
|
56
|
+
attribute :time_updated, Shale::Type::Integer
|
|
57
|
+
|
|
58
|
+
timestamps :time_created
|
|
59
|
+
|
|
60
|
+
# Alias for team_name for consistency.
|
|
61
|
+
#
|
|
62
|
+
# @return [String] Team/workspace name
|
|
63
|
+
def name
|
|
64
|
+
team_name
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
module Models
|
|
5
|
+
# Represents a Superthread user.
|
|
6
|
+
#
|
|
7
|
+
# Users are people who can access Superthread workspaces. They have
|
|
8
|
+
# profiles with display names, emails, and avatars, and can be members
|
|
9
|
+
# of multiple teams.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# user = client.users.me
|
|
13
|
+
# user.display_name # => "John Doe"
|
|
14
|
+
# user.email # => "john@example.com"
|
|
15
|
+
# user.id # => "u123"
|
|
16
|
+
class User < Superthread::Model
|
|
17
|
+
include Concerns::Presentable
|
|
18
|
+
include Concerns::Timestampable
|
|
19
|
+
|
|
20
|
+
presents_as :display_name
|
|
21
|
+
|
|
22
|
+
detail_fields :user_id, :display_name, :email, :role
|
|
23
|
+
list_columns :user_id, :display_name, :email
|
|
24
|
+
|
|
25
|
+
# @!attribute [rw] id
|
|
26
|
+
# @return [String] unique user identifier
|
|
27
|
+
attribute :id, Shale::Type::String
|
|
28
|
+
|
|
29
|
+
# @!attribute [rw] user_id
|
|
30
|
+
# @return [String] alternate user identifier (API returns both in different contexts)
|
|
31
|
+
attribute :user_id, Shale::Type::String
|
|
32
|
+
|
|
33
|
+
# @!attribute [rw] type
|
|
34
|
+
# @return [String] user type
|
|
35
|
+
attribute :type, Shale::Type::String
|
|
36
|
+
|
|
37
|
+
# @!attribute [rw] display_name
|
|
38
|
+
# @return [String] user's display name
|
|
39
|
+
attribute :display_name, Shale::Type::String
|
|
40
|
+
|
|
41
|
+
# @!attribute [rw] email
|
|
42
|
+
# @return [String] user's email address
|
|
43
|
+
attribute :email, Shale::Type::String
|
|
44
|
+
|
|
45
|
+
# @!attribute [rw] avatar
|
|
46
|
+
# @return [String] URL to the user's avatar image
|
|
47
|
+
attribute :avatar, Shale::Type::String
|
|
48
|
+
|
|
49
|
+
# @!attribute [rw] role
|
|
50
|
+
# @return [String] user's role within the current context
|
|
51
|
+
attribute :role, Shale::Type::String
|
|
52
|
+
|
|
53
|
+
# @!attribute [rw] time_created
|
|
54
|
+
# @return [Integer] Unix timestamp when the user account was created
|
|
55
|
+
attribute :time_created, Shale::Type::Integer
|
|
56
|
+
|
|
57
|
+
# @!attribute [rw] time_updated
|
|
58
|
+
# @return [Integer] Unix timestamp when the user was last updated
|
|
59
|
+
attribute :time_updated, Shale::Type::Integer
|
|
60
|
+
|
|
61
|
+
# @!attribute [rw] teams
|
|
62
|
+
# @return [Array<Team>] teams/workspaces this user belongs to
|
|
63
|
+
attribute :teams, Team, collection: true
|
|
64
|
+
|
|
65
|
+
timestamps :time_created, :time_updated
|
|
66
|
+
|
|
67
|
+
# Returns the user ID, checking both 'id' and 'user_id' fields
|
|
68
|
+
# (API returns different fields in different contexts)
|
|
69
|
+
#
|
|
70
|
+
# @return [String] User ID
|
|
71
|
+
def user_identifier
|
|
72
|
+
id || user_id
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
# Namespace for Shale-based API response models.
|
|
5
|
+
#
|
|
6
|
+
# These typed models are replacing the generic Superthread::Object
|
|
7
|
+
# classes, providing better type safety and validation.
|
|
8
|
+
#
|
|
9
|
+
# @see Superthread::Model Base class for all models
|
|
10
|
+
module Models
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Superthread
|
|
4
|
+
# Generic wrapper for API response objects (legacy pattern).
|
|
5
|
+
#
|
|
6
|
+
# Prefer {Superthread::Model} (Shale-based) for new resource types.
|
|
7
|
+
# This class is retained for backward compatibility with endpoints
|
|
8
|
+
# that haven't been migrated to typed models yet.
|
|
9
|
+
#
|
|
10
|
+
# Provides hash-like access, dot notation via method_missing, and conversion to Hash.
|
|
11
|
+
#
|
|
12
|
+
# Inspired by Stripe's StripeObject pattern - allows both:
|
|
13
|
+
# card.title # dot notation
|
|
14
|
+
# card[:title] # symbol key access
|
|
15
|
+
# card["title"] # string key access
|
|
16
|
+
# card.to_h # convert to plain hash
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# card = client.cards.find(workspace_id, card_id)
|
|
20
|
+
# card.title # => "My Card"
|
|
21
|
+
# card[:title] # => "My Card"
|
|
22
|
+
# card.members.first.role # => "admin"
|
|
23
|
+
# card.to_h # => { id: "123", title: "My Card", ... }
|
|
24
|
+
class Object
|
|
25
|
+
include Enumerable
|
|
26
|
+
|
|
27
|
+
# @return [Hash] The raw data from the API response
|
|
28
|
+
attr_reader :data
|
|
29
|
+
|
|
30
|
+
# Creates a new object from a Hash or another SuperthreadObject.
|
|
31
|
+
#
|
|
32
|
+
# @param data [Hash, Superthread::Object] The data to wrap
|
|
33
|
+
def initialize(data = {})
|
|
34
|
+
@data = case data
|
|
35
|
+
when Hash
|
|
36
|
+
data.transform_keys(&:to_sym)
|
|
37
|
+
when Superthread::Object
|
|
38
|
+
data.data.dup
|
|
39
|
+
else
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Factory method to construct the appropriate typed object from API response data.
|
|
45
|
+
# Uses the "type" field in the response to determine the class.
|
|
46
|
+
#
|
|
47
|
+
# @param data [Hash, Array, nil] The API response data
|
|
48
|
+
# @return [Superthread::Object, Array, nil] The constructed object(s)
|
|
49
|
+
def self.construct_from(data)
|
|
50
|
+
case data
|
|
51
|
+
when Array
|
|
52
|
+
data.map { |item| construct_from(item) }
|
|
53
|
+
when Hash
|
|
54
|
+
klass = object_class_for(data)
|
|
55
|
+
klass.new(data)
|
|
56
|
+
else
|
|
57
|
+
data
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Determines the appropriate class for the given data based on the "type" field.
|
|
62
|
+
#
|
|
63
|
+
# @param data [Hash] The data hash
|
|
64
|
+
# @return [Class] The class to use for construction
|
|
65
|
+
def self.object_class_for(data)
|
|
66
|
+
type = data[:type] || data["type"]
|
|
67
|
+
@object_types.fetch(type, Superthread::Object)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Registry mapping API "type" values to Ruby classes.
|
|
71
|
+
#
|
|
72
|
+
# Subclasses register themselves here.
|
|
73
|
+
#
|
|
74
|
+
# @note This hash is intentionally mutable for dynamic registration.
|
|
75
|
+
@object_types = {}
|
|
76
|
+
|
|
77
|
+
class << self
|
|
78
|
+
attr_reader :object_types
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Registers a subclass for a given type name.
|
|
82
|
+
# Called automatically when OBJECT_NAME is defined on a subclass.
|
|
83
|
+
#
|
|
84
|
+
# @param type_name [String] The API type name
|
|
85
|
+
# @param klass [Class] The class to register
|
|
86
|
+
def self.register_type(type_name, klass)
|
|
87
|
+
@object_types[type_name] = klass
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Hook called when a subclass is defined.
|
|
91
|
+
#
|
|
92
|
+
# Automatically registers the subclass if it defines OBJECT_NAME.
|
|
93
|
+
#
|
|
94
|
+
# @param subclass [Class] the subclass being defined
|
|
95
|
+
# @return [void]
|
|
96
|
+
def self.inherited(subclass)
|
|
97
|
+
super
|
|
98
|
+
subclass.instance_eval do
|
|
99
|
+
def self.object_name
|
|
100
|
+
const_defined?(:OBJECT_NAME, false) ? const_get(:OBJECT_NAME) : nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Access a value by key (symbol or string).
|
|
106
|
+
#
|
|
107
|
+
# @param key [Symbol, String] The key to access
|
|
108
|
+
# @return [Object] The value, wrapped in a SuperthreadObject if it's a Hash
|
|
109
|
+
def [](key)
|
|
110
|
+
value = @data[key.to_sym]
|
|
111
|
+
wrap_value(value)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Set a value by key.
|
|
115
|
+
#
|
|
116
|
+
# @param key [Symbol, String] The key to set
|
|
117
|
+
# @param value [Object] The value to set
|
|
118
|
+
def []=(key, value)
|
|
119
|
+
@data[key.to_sym] = value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Iterate over key-value pairs.
|
|
123
|
+
#
|
|
124
|
+
# @param block [Proc] receives key-value pairs for each attribute
|
|
125
|
+
# @yieldparam key [Symbol] the attribute key
|
|
126
|
+
# @yieldparam value [Object] the attribute value (wrapped if Hash)
|
|
127
|
+
# @return [void]
|
|
128
|
+
def each(&block)
|
|
129
|
+
@data.each do |key, value|
|
|
130
|
+
block.call(key, wrap_value(value))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns all keys.
|
|
135
|
+
#
|
|
136
|
+
# @return [Array<Symbol>] The keys
|
|
137
|
+
def keys
|
|
138
|
+
@data.keys
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns all values (wrapped).
|
|
142
|
+
#
|
|
143
|
+
# @return [Array] The values
|
|
144
|
+
def values
|
|
145
|
+
@data.values.map { |v| wrap_value(v) }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if a key exists.
|
|
149
|
+
#
|
|
150
|
+
# @param key [Symbol, String] The key to check
|
|
151
|
+
# @return [Boolean] True if the key exists
|
|
152
|
+
def key?(key)
|
|
153
|
+
@data.key?(key.to_sym)
|
|
154
|
+
end
|
|
155
|
+
alias_method :has_key?, :key?
|
|
156
|
+
|
|
157
|
+
# Convert to a plain Hash (deep conversion).
|
|
158
|
+
#
|
|
159
|
+
# @return [Hash] A plain hash representation
|
|
160
|
+
def to_h
|
|
161
|
+
@data.transform_values do |value|
|
|
162
|
+
case value
|
|
163
|
+
when Superthread::Object
|
|
164
|
+
value.to_h
|
|
165
|
+
when Array
|
|
166
|
+
value.map { |v| v.respond_to?(:to_h) ? v.to_h : v }
|
|
167
|
+
when Hash
|
|
168
|
+
value.transform_values { |v| v.respond_to?(:to_h) ? v.to_h : v }
|
|
169
|
+
else
|
|
170
|
+
value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
alias_method :to_hash, :to_h
|
|
175
|
+
|
|
176
|
+
# Convert to JSON string.
|
|
177
|
+
#
|
|
178
|
+
# @param args [Array] Arguments passed to JSON.generate
|
|
179
|
+
# @return [String] JSON representation
|
|
180
|
+
def to_json(*args)
|
|
181
|
+
to_h.to_json(*args)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Check equality based on data.
|
|
185
|
+
#
|
|
186
|
+
# @param other [Object] The object to compare
|
|
187
|
+
# @return [Boolean] True if equal
|
|
188
|
+
def ==(other)
|
|
189
|
+
case other
|
|
190
|
+
when Superthread::Object
|
|
191
|
+
@data == other.data
|
|
192
|
+
when Hash
|
|
193
|
+
@data == other.transform_keys(&:to_sym)
|
|
194
|
+
else
|
|
195
|
+
false
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
alias_method :eql?, :==
|
|
199
|
+
|
|
200
|
+
# Hash code for use in Hash keys.
|
|
201
|
+
#
|
|
202
|
+
# @return [Integer] Hash code
|
|
203
|
+
def hash
|
|
204
|
+
@data.hash
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# String representation for debugging.
|
|
208
|
+
#
|
|
209
|
+
# @return [String] Debug representation
|
|
210
|
+
def inspect
|
|
211
|
+
"#<#{self.class.name} #{@data.inspect}>"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Pretty string representation.
|
|
215
|
+
#
|
|
216
|
+
# @return [String] String representation
|
|
217
|
+
def to_s
|
|
218
|
+
inspect
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Check if the object has a given attribute.
|
|
222
|
+
# Supports predicate methods like `archived?`.
|
|
223
|
+
#
|
|
224
|
+
# @param method_name [Symbol] The method name
|
|
225
|
+
# @param include_private [Boolean] Whether to include private methods
|
|
226
|
+
# @return [Boolean] True if the method exists
|
|
227
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
228
|
+
name = method_name.to_s
|
|
229
|
+
if name.end_with?("=")
|
|
230
|
+
true
|
|
231
|
+
elsif name.end_with?("?")
|
|
232
|
+
@data.key?(name.chomp("?").to_sym)
|
|
233
|
+
else
|
|
234
|
+
@data.key?(name.to_sym) || super
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
# Dynamic attribute access via method_missing.
|
|
241
|
+
#
|
|
242
|
+
# Supports:
|
|
243
|
+
# - Getters: card.title
|
|
244
|
+
# - Setters: card.title = "New Title"
|
|
245
|
+
# - Predicates: card.archived?
|
|
246
|
+
#
|
|
247
|
+
# @param method_name [Symbol] the method being called
|
|
248
|
+
# @param args [Array] arguments passed to the method
|
|
249
|
+
# @return [Object] the attribute value or result of assignment
|
|
250
|
+
def method_missing(method_name, *args)
|
|
251
|
+
name = method_name.to_s
|
|
252
|
+
|
|
253
|
+
if name.end_with?("=")
|
|
254
|
+
# Setter: card.title = "New Title"
|
|
255
|
+
key = name.chomp("=").to_sym
|
|
256
|
+
@data[key] = args.first
|
|
257
|
+
elsif name.end_with?("?")
|
|
258
|
+
# Predicate: card.archived?
|
|
259
|
+
key = name.chomp("?").to_sym
|
|
260
|
+
!!@data[key]
|
|
261
|
+
elsif @data.key?(name.to_sym)
|
|
262
|
+
# Getter: card.title
|
|
263
|
+
wrap_value(@data[name.to_sym])
|
|
264
|
+
else
|
|
265
|
+
super
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Wraps values in SuperthreadObject instances for nested access.
|
|
270
|
+
# Arrays of Hashes become Arrays of SuperthreadObjects.
|
|
271
|
+
#
|
|
272
|
+
# @param value [Object] The value to wrap
|
|
273
|
+
# @return [Object] The wrapped value
|
|
274
|
+
def wrap_value(value)
|
|
275
|
+
case value
|
|
276
|
+
when Hash
|
|
277
|
+
Superthread::Object.construct_from(value)
|
|
278
|
+
when Array
|
|
279
|
+
value.map { |v| wrap_value(v) }
|
|
280
|
+
else
|
|
281
|
+
value
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|