X Tutup
Skip to content

Commit ca0f89d

Browse files
mislavjosebalius
andcommitted
Introduce an App struct that executes core business logic
The Cobra commands are now a light wrapper around the App struct. Co-authored-by: Jose Garcia <josebalius@github.com>
1 parent 8807b3a commit ca0f89d

File tree

15 files changed

+557
-175
lines changed

15 files changed

+557
-175
lines changed

cmd/ghcs/code.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@ import (
55
"fmt"
66
"net/url"
77

8-
"github.com/github/ghcs/internal/api"
98
"github.com/skratchdot/open-golang/open"
109
"github.com/spf13/cobra"
1110
)
1211

13-
func newCodeCmd() *cobra.Command {
12+
func newCodeCmd(app *App) *cobra.Command {
1413
var (
1514
codespace string
1615
useInsiders bool
@@ -21,7 +20,7 @@ func newCodeCmd() *cobra.Command {
2120
Short: "Open a codespace in VS Code",
2221
Args: noArgsConstraint,
2322
RunE: func(cmd *cobra.Command, args []string) error {
24-
return code(codespace, useInsiders)
23+
return app.VSCode(cmd.Context(), codespace, useInsiders)
2524
},
2625
}
2726

@@ -31,17 +30,15 @@ func newCodeCmd() *cobra.Command {
3130
return codeCmd
3231
}
3332

34-
func code(codespaceName string, useInsiders bool) error {
35-
apiClient := api.New(GithubToken)
36-
ctx := context.Background()
37-
38-
user, err := apiClient.GetUser(ctx)
33+
// VSCode opens a codespace in the local VS VSCode application.
34+
func (a *App) VSCode(ctx context.Context, codespaceName string, useInsiders bool) error {
35+
user, err := a.apiClient.GetUser(ctx)
3936
if err != nil {
4037
return fmt.Errorf("error getting user: %w", err)
4138
}
4239

4340
if codespaceName == "" {
44-
codespace, err := chooseCodespace(ctx, apiClient, user)
41+
codespace, err := chooseCodespace(ctx, a.apiClient, user)
4542
if err != nil {
4643
if err == errNoCodespaces {
4744
return err

cmd/ghcs/common.go

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,43 @@ import (
1212

1313
"github.com/AlecAivazis/survey/v2"
1414
"github.com/AlecAivazis/survey/v2/terminal"
15+
"github.com/github/ghcs/cmd/ghcs/output"
1516
"github.com/github/ghcs/internal/api"
1617
"github.com/spf13/cobra"
1718
"golang.org/x/term"
1819
)
1920

21+
type App struct {
22+
apiClient apiClient
23+
logger *output.Logger
24+
}
25+
26+
func NewApp(logger *output.Logger, apiClient apiClient) *App {
27+
return &App{
28+
apiClient: apiClient,
29+
logger: logger,
30+
}
31+
}
32+
33+
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
34+
type apiClient interface {
35+
GetUser(ctx context.Context) (*api.User, error)
36+
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
37+
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
38+
ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error)
39+
DeleteCodespace(ctx context.Context, user, name string) error
40+
StartCodespace(ctx context.Context, token string, codespace *api.Codespace) error
41+
CreateCodespace(ctx context.Context, logger api.Logger, params *api.CreateCodespaceParams) (*api.Codespace, error)
42+
GetRepository(ctx context.Context, nwo string) (*api.Repository, error)
43+
AuthorizedKeys(ctx context.Context, user string) ([]byte, error)
44+
GetCodespaceRegionLocation(ctx context.Context) (string, error)
45+
GetCodespacesSKUs(ctx context.Context, user *api.User, repository *api.Repository, branch, location string) ([]*api.SKU, error)
46+
GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
47+
}
48+
2049
var errNoCodespaces = errors.New("you have no codespaces")
2150

22-
func chooseCodespace(ctx context.Context, apiClient *api.API, user *api.User) (*api.Codespace, error) {
51+
func chooseCodespace(ctx context.Context, apiClient apiClient, user *api.User) (*api.Codespace, error) {
2352
codespaces, err := apiClient.ListCodespaces(ctx, user.Login)
2453
if err != nil {
2554
return nil, fmt.Errorf("error getting codespaces: %w", err)
@@ -68,7 +97,7 @@ func chooseCodespaceFromList(ctx context.Context, codespaces []*api.Codespace) (
6897

6998
// getOrChooseCodespace prompts the user to choose a codespace if the codespaceName is empty.
7099
// It then fetches the codespace token and the codespace record.
71-
func getOrChooseCodespace(ctx context.Context, apiClient *api.API, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) {
100+
func getOrChooseCodespace(ctx context.Context, apiClient apiClient, user *api.User, codespaceName string) (codespace *api.Codespace, token string, err error) {
72101
if codespaceName == "" {
73102
codespace, err = chooseCodespace(ctx, apiClient, user)
74103
if err != nil {
@@ -135,7 +164,7 @@ func ask(qs []*survey.Question, response interface{}) error {
135164
// checkAuthorizedKeys reports an error if the user has not registered any SSH keys;
136165
// see https://github.com/github/ghcs/issues/166#issuecomment-921769703.
137166
// The check is not required for security but it improves the error message.
138-
func checkAuthorizedKeys(ctx context.Context, client *api.API, user string) error {
167+
func checkAuthorizedKeys(ctx context.Context, client apiClient, user string) error {
139168
keys, err := client.AuthorizedKeys(ctx, user)
140169
if err != nil {
141170
return fmt.Errorf("failed to read GitHub-authorized SSH keys for %s: %w", user, err)

cmd/ghcs/create.go

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ type createOptions struct {
2222
showStatus bool
2323
}
2424

25-
func newCreateCmd() *cobra.Command {
25+
func newCreateCmd(app *App) *cobra.Command {
2626
opts := &createOptions{}
2727

2828
createCmd := &cobra.Command{
2929
Use: "create",
3030
Short: "Create a codespace",
3131
Args: noArgsConstraint,
3232
RunE: func(cmd *cobra.Command, args []string) error {
33-
return create(opts)
33+
return app.Create(cmd.Context(), opts)
3434
},
3535
}
3636

@@ -42,12 +42,10 @@ func newCreateCmd() *cobra.Command {
4242
return createCmd
4343
}
4444

45-
func create(opts *createOptions) error {
46-
ctx := context.Background()
47-
apiClient := api.New(GithubToken)
48-
locationCh := getLocation(ctx, apiClient)
49-
userCh := getUser(ctx, apiClient)
50-
log := output.NewLogger(os.Stdout, os.Stderr, false)
45+
// Create creates a new Codespace
46+
func (a *App) Create(ctx context.Context, opts *createOptions) error {
47+
locationCh := getLocation(ctx, a.apiClient)
48+
userCh := getUser(ctx, a.apiClient)
5149

5250
repo, err := getRepoName(opts.repo)
5351
if err != nil {
@@ -58,7 +56,7 @@ func create(opts *createOptions) error {
5856
return fmt.Errorf("error getting branch name: %w", err)
5957
}
6058

61-
repository, err := apiClient.GetRepository(ctx, repo)
59+
repository, err := a.apiClient.GetRepository(ctx, repo)
6260
if err != nil {
6361
return fmt.Errorf("error getting repository: %w", err)
6462
}
@@ -73,34 +71,34 @@ func create(opts *createOptions) error {
7371
return fmt.Errorf("error getting codespace user: %w", userResult.Err)
7472
}
7573

76-
machine, err := getMachineName(ctx, opts.machine, userResult.User, repository, branch, locationResult.Location, apiClient)
74+
machine, err := getMachineName(ctx, opts.machine, userResult.User, repository, branch, locationResult.Location, a.apiClient)
7775
if err != nil {
7876
return fmt.Errorf("error getting machine type: %w", err)
7977
}
8078
if machine == "" {
8179
return errors.New("there are no available machine types for this repository")
8280
}
8381

84-
log.Print("Creating your codespace...")
85-
codespace, err := apiClient.CreateCodespace(ctx, log, &api.CreateCodespaceParams{
82+
a.logger.Print("Creating your codespace...")
83+
codespace, err := a.apiClient.CreateCodespace(ctx, a.logger, &api.CreateCodespaceParams{
8684
User: userResult.User.Login,
8785
RepositoryID: repository.ID,
8886
Branch: branch,
8987
Machine: machine,
9088
Location: locationResult.Location,
9189
})
92-
log.Print("\n")
90+
a.logger.Print("\n")
9391
if err != nil {
9492
return fmt.Errorf("error creating codespace: %w", err)
9593
}
9694

9795
if opts.showStatus {
98-
if err := showStatus(ctx, log, apiClient, userResult.User, codespace); err != nil {
96+
if err := showStatus(ctx, a.logger, a.apiClient, userResult.User, codespace); err != nil {
9997
return fmt.Errorf("show status: %w", err)
10098
}
10199
}
102100

103-
log.Printf("Codespace created: ")
101+
a.logger.Printf("Codespace created: ")
104102

105103
fmt.Fprintln(os.Stdout, codespace.Name)
106104

@@ -110,7 +108,7 @@ func create(opts *createOptions) error {
110108
// showStatus polls the codespace for a list of post create states and their status. It will keep polling
111109
// until all states have finished. Once all states have finished, we poll once more to check if any new
112110
// states have been introduced and stop polling otherwise.
113-
func showStatus(ctx context.Context, log *output.Logger, apiClient *api.API, user *api.User, codespace *api.Codespace) error {
111+
func showStatus(ctx context.Context, log *output.Logger, apiClient apiClient, user *api.User, codespace *api.Codespace) error {
114112
var lastState codespaces.PostCreateState
115113
var breakNextState bool
116114

@@ -177,7 +175,7 @@ type getUserResult struct {
177175
}
178176

179177
// getUser fetches the user record associated with the GITHUB_TOKEN
180-
func getUser(ctx context.Context, apiClient *api.API) <-chan getUserResult {
178+
func getUser(ctx context.Context, apiClient apiClient) <-chan getUserResult {
181179
ch := make(chan getUserResult, 1)
182180
go func() {
183181
user, err := apiClient.GetUser(ctx)
@@ -192,7 +190,7 @@ type locationResult struct {
192190
}
193191

194192
// getLocation fetches the closest Codespace datacenter region/location to the user.
195-
func getLocation(ctx context.Context, apiClient *api.API) <-chan locationResult {
193+
func getLocation(ctx context.Context, apiClient apiClient) <-chan locationResult {
196194
ch := make(chan locationResult, 1)
197195
go func() {
198196
location, err := apiClient.GetCodespaceRegionLocation(ctx)
@@ -236,7 +234,7 @@ func getBranchName(branch string) (string, error) {
236234
}
237235

238236
// getMachineName prompts the user to select the machine type, or validates the machine if non-empty.
239-
func getMachineName(ctx context.Context, machine string, user *api.User, repo *api.Repository, branch, location string, apiClient *api.API) (string, error) {
237+
func getMachineName(ctx context.Context, machine string, user *api.User, repo *api.Repository, branch, location string, apiClient apiClient) (string, error) {
240238
skus, err := apiClient.GetCodespacesSKUs(ctx, user, repo, branch, location)
241239
if err != nil {
242240
return "", fmt.Errorf("error requesting machine instance types: %w", err)

cmd/ghcs/delete.go

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"os"
87
"strings"
98
"time"
109

1110
"github.com/AlecAivazis/survey/v2"
12-
"github.com/github/ghcs/cmd/ghcs/output"
1311
"github.com/github/ghcs/internal/api"
1412
"github.com/spf13/cobra"
1513
"golang.org/x/sync/errgroup"
@@ -24,7 +22,6 @@ type deleteOptions struct {
2422

2523
isInteractive bool
2624
now func() time.Time
27-
apiClient apiClient
2825
prompter prompter
2926
}
3027

@@ -33,20 +30,10 @@ type prompter interface {
3330
Confirm(message string) (bool, error)
3431
}
3532

36-
//go:generate moq -fmt goimports -rm -skip-ensure -out mock_api.go . apiClient
37-
type apiClient interface {
38-
GetUser(ctx context.Context) (*api.User, error)
39-
GetCodespaceToken(ctx context.Context, user, name string) (string, error)
40-
GetCodespace(ctx context.Context, token, user, name string) (*api.Codespace, error)
41-
ListCodespaces(ctx context.Context, user string) ([]*api.Codespace, error)
42-
DeleteCodespace(ctx context.Context, user, name string) error
43-
}
44-
45-
func newDeleteCmd() *cobra.Command {
33+
func newDeleteCmd(app *App) *cobra.Command {
4634
opts := deleteOptions{
4735
isInteractive: hasTTY,
4836
now: time.Now,
49-
apiClient: api.New(os.Getenv("GITHUB_TOKEN")),
5037
prompter: &surveyPrompter{},
5138
}
5239

@@ -58,8 +45,7 @@ func newDeleteCmd() *cobra.Command {
5845
if opts.deleteAll && opts.repoFilter != "" {
5946
return errors.New("both --all and --repo is not supported")
6047
}
61-
log := output.NewLogger(cmd.OutOrStdout(), cmd.ErrOrStderr(), !opts.isInteractive)
62-
return delete(context.Background(), log, opts)
48+
return app.Delete(cmd.Context(), opts)
6349
},
6450
}
6551

@@ -72,20 +58,16 @@ func newDeleteCmd() *cobra.Command {
7258
return deleteCmd
7359
}
7460

75-
type logger interface {
76-
Errorf(format string, v ...interface{}) (int, error)
77-
}
78-
79-
func delete(ctx context.Context, log logger, opts deleteOptions) error {
80-
user, err := opts.apiClient.GetUser(ctx)
61+
func (a *App) Delete(ctx context.Context, opts deleteOptions) error {
62+
user, err := a.apiClient.GetUser(ctx)
8163
if err != nil {
8264
return fmt.Errorf("error getting user: %w", err)
8365
}
8466

8567
var codespaces []*api.Codespace
8668
nameFilter := opts.codespaceName
8769
if nameFilter == "" {
88-
codespaces, err = opts.apiClient.ListCodespaces(ctx, user.Login)
70+
codespaces, err = a.apiClient.ListCodespaces(ctx, user.Login)
8971
if err != nil {
9072
return fmt.Errorf("error getting codespaces: %w", err)
9173
}
@@ -99,12 +81,12 @@ func delete(ctx context.Context, log logger, opts deleteOptions) error {
9981
}
10082
} else {
10183
// TODO: this token is discarded and then re-requested later in DeleteCodespace
102-
token, err := opts.apiClient.GetCodespaceToken(ctx, user.Login, nameFilter)
84+
token, err := a.apiClient.GetCodespaceToken(ctx, user.Login, nameFilter)
10385
if err != nil {
10486
return fmt.Errorf("error getting codespace token: %w", err)
10587
}
10688

107-
codespace, err := opts.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
89+
codespace, err := a.apiClient.GetCodespace(ctx, token, user.Login, nameFilter)
10890
if err != nil {
10991
return fmt.Errorf("error fetching codespace information: %w", err)
11092
}
@@ -150,8 +132,8 @@ func delete(ctx context.Context, log logger, opts deleteOptions) error {
150132
for _, c := range codespacesToDelete {
151133
codespaceName := c.Name
152134
g.Go(func() error {
153-
if err := opts.apiClient.DeleteCodespace(ctx, user.Login, codespaceName); err != nil {
154-
_, _ = log.Errorf("error deleting codespace %q: %v\n", codespaceName, err)
135+
if err := a.apiClient.DeleteCodespace(ctx, user.Login, codespaceName); err != nil {
136+
_, _ = a.logger.Errorf("error deleting codespace %q: %v\n", codespaceName, err)
155137
return err
156138
}
157139
return nil

cmd/ghcs/delete_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ func TestDelete(t *testing.T) {
186186
}
187187
}
188188
opts := tt.opts
189-
opts.apiClient = apiMock
190189
opts.now = func() time.Time { return now }
191190
opts.prompter = &prompterMock{
192191
ConfirmFunc: func(msg string) (bool, error) {
@@ -200,8 +199,11 @@ func TestDelete(t *testing.T) {
200199

201200
stdout := &bytes.Buffer{}
202201
stderr := &bytes.Buffer{}
203-
log := output.NewLogger(stdout, stderr, false)
204-
err := delete(context.Background(), log, opts)
202+
app := &App{
203+
apiClient: apiMock,
204+
logger: output.NewLogger(stdout, stderr, false),
205+
}
206+
err := app.Delete(context.Background(), opts)
205207
if (err != nil) != tt.wantErr {
206208
t.Errorf("delete() error = %v, wantErr %v", err, tt.wantErr)
207209
}

cmd/ghcs/list.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ type listOptions struct {
1414
asJSON bool
1515
}
1616

17-
func newListCmd() *cobra.Command {
17+
func newListCmd(app *App) *cobra.Command {
1818
opts := &listOptions{}
1919

2020
listCmd := &cobra.Command{
2121
Use: "list",
2222
Short: "List your codespaces",
2323
Args: noArgsConstraint,
2424
RunE: func(cmd *cobra.Command, args []string) error {
25-
return list(opts)
25+
return app.List(cmd.Context(), opts)
2626
},
2727
}
2828

@@ -31,16 +31,13 @@ func newListCmd() *cobra.Command {
3131
return listCmd
3232
}
3333

34-
func list(opts *listOptions) error {
35-
apiClient := api.New(GithubToken)
36-
ctx := context.Background()
37-
38-
user, err := apiClient.GetUser(ctx)
34+
func (a *App) List(ctx context.Context, opts *listOptions) error {
35+
user, err := a.apiClient.GetUser(ctx)
3936
if err != nil {
4037
return fmt.Errorf("error getting user: %w", err)
4138
}
4239

43-
codespaces, err := apiClient.ListCodespaces(ctx, user.Login)
40+
codespaces, err := a.apiClient.ListCodespaces(ctx, user.Login)
4441
if err != nil {
4542
return fmt.Errorf("error getting codespaces: %w", err)
4643
}

0 commit comments

Comments
 (0)
X Tutup