Skip to content

Commit c863348

Browse files
committed
feat: add JSON output for the apps command
1 parent 4624e40 commit c863348

File tree

8 files changed

+150
-27
lines changed

8 files changed

+150
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* fix(databases): `parseScheduleAtFlag` returns a validation error
1313
* fix(privatenetworks): `PrivateNetworksDomainsList` must take a `pagination.Request` in argument
1414
* build(deps): update `github.com/Scalingo/go-scalingo` from v10 to v11
15+
* feat: add JSON output for the `apps` command
1516

1617
## 1.43.3
1718

apps/list.go

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,14 @@ package apps
22

33
import (
44
"context"
5-
"fmt"
6-
"os"
7-
8-
"github.com/olekukonko/tablewriter"
95

106
"github.com/Scalingo/cli/config"
11-
"github.com/Scalingo/cli/io"
12-
"github.com/Scalingo/cli/utils"
7+
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
8+
"github.com/Scalingo/go-scalingo/v11"
139
"github.com/Scalingo/go-utils/errors/v3"
1410
)
1511

16-
func List(ctx context.Context, projectSlug string) error {
12+
func List(ctx context.Context, renderer renderer.Renderer[[]*scalingo.App], projectSlug string) error {
1713
c, err := config.ScalingoClient(ctx)
1814
if err != nil {
1915
return errors.Wrap(ctx, err, "get Scalingo client")
@@ -24,30 +20,28 @@ func List(ctx context.Context, projectSlug string) error {
2420
return errors.Wrap(ctx, err, "list apps")
2521
}
2622

27-
if len(apps) == 0 {
28-
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
29-
return nil
23+
filteredApps := filterAppsByProject(apps, projectSlug)
24+
renderer.SetData(ctx, filteredApps)
25+
26+
err = renderer.Render(ctx)
27+
if err != nil {
28+
return errors.Wrap(ctx, err, "render apps list")
3029
}
3130

32-
t := tablewriter.NewWriter(os.Stdout)
33-
t.Header([]string{"Name", "Role", "Status", "Project"})
31+
return nil
32+
}
3433

35-
currentUser, err := config.C.CurrentUser(ctx)
36-
if err != nil {
37-
return errors.Wrap(ctx, err, "fail to get current user")
34+
func filterAppsByProject(apps []*scalingo.App, projectSlug string) []*scalingo.App {
35+
if projectSlug == "" {
36+
return apps
3837
}
3938

39+
filteredApps := make([]*scalingo.App, 0, len(apps))
4040
for _, app := range apps {
41-
// If a filter was set but the app is not in the project, skip to the next one.
42-
if projectSlug != "" && projectSlug != app.ProjectSlug() {
43-
continue
41+
if app.ProjectSlug() == projectSlug {
42+
filteredApps = append(filteredApps, app)
4443
}
45-
46-
role := utils.AppRole(currentUser, app)
47-
48-
_ = t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
4944
}
50-
_ = t.Render()
5145

52-
return nil
46+
return filteredApps
5347
}

cmd/apps.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import (
88

99
"github.com/Scalingo/cli/apps"
1010
"github.com/Scalingo/cli/cmd/autocomplete"
11+
"github.com/Scalingo/cli/config"
1112
"github.com/Scalingo/cli/detect"
13+
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
14+
rendererjson "github.com/Scalingo/cli/internal/boundaries/out/renderer/json"
15+
renderertable "github.com/Scalingo/cli/internal/boundaries/out/renderer/table"
1216
"github.com/Scalingo/cli/io"
1317
"github.com/Scalingo/cli/utils"
18+
"github.com/Scalingo/go-scalingo/v11"
1419
"github.com/Scalingo/go-utils/errors/v3"
1520
)
1621

@@ -19,17 +24,37 @@ var (
1924
Name: "apps",
2025
Category: "Global",
2126
Description: "List your apps and give some details about them",
22-
Flags: []cli.Flag{&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"}},
23-
Usage: "List your apps",
27+
Flags: []cli.Flag{
28+
&cli.StringFlag{Name: "project", Usage: "Filter apps by project. The filter uses the format <ownerUsername>/<projectName>"},
29+
},
30+
Usage: "List your apps",
2431
Action: func(ctx context.Context, c *cli.Command) error {
2532
projectSlug := c.String("project")
33+
format := renderer.Format(c.String("format"))
34+
2635
if projectSlug != "" {
2736
projectSlugSplit := strings.Split(projectSlug, "/")
2837
if len(projectSlugSplit) != 2 || (len(projectSlugSplit) == 2 && (projectSlugSplit[0] == "" || projectSlugSplit[1] == "")) {
2938
errorQuitWithHelpMessage(ctx, errors.New(ctx, "project filter doesn't respect the expected format"), c, "apps")
3039
}
3140
}
32-
err := apps.List(ctx, projectSlug)
41+
42+
var appsRenderer renderer.Renderer[[]*scalingo.App]
43+
switch format {
44+
case renderer.FormatTable:
45+
currentUser, err := config.C.CurrentUser(ctx)
46+
if err != nil {
47+
errorQuit(ctx, errors.Wrap(ctx, err, "get current user"))
48+
}
49+
50+
appsRenderer = renderertable.NewAppsList(currentUser)
51+
case renderer.FormatJSON:
52+
appsRenderer = rendererjson.NewAppsList()
53+
default:
54+
errorQuitWithHelpMessage(ctx, errors.Newf(ctx, "invalid format '%v'", format), c, "apps")
55+
}
56+
57+
err := apps.List(ctx, appsRenderer, projectSlug)
3358
if err != nil {
3459
errorQuit(ctx, err)
3560
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package renderer
2+
3+
type Format string
4+
5+
const (
6+
FormatJSON Format = "json"
7+
FormatTable Format = "table"
8+
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package json
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"os"
7+
8+
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
9+
"github.com/Scalingo/go-scalingo/v11"
10+
"github.com/Scalingo/go-utils/errors/v3"
11+
)
12+
13+
type appsListRenderer struct {
14+
apps []*scalingo.App
15+
}
16+
17+
type appsListResponse struct {
18+
Apps []*scalingo.App `json:"apps"`
19+
}
20+
21+
func NewAppsList() renderer.Renderer[[]*scalingo.App] {
22+
return &appsListRenderer{}
23+
}
24+
25+
func (r *appsListRenderer) Render(ctx context.Context) error {
26+
err := json.NewEncoder(os.Stdout).Encode(appsListResponse{Apps: r.apps})
27+
if err != nil {
28+
return errors.Wrap(ctx, err, "encode apps list to JSON")
29+
}
30+
return nil
31+
}
32+
33+
func (r *appsListRenderer) SetData(ctx context.Context, apps []*scalingo.App) {
34+
r.apps = apps
35+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package renderer
2+
3+
import "context"
4+
5+
type Renderer[D any] interface {
6+
Render(ctx context.Context) error
7+
SetData(ctx context.Context, data D)
8+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package table
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"github.com/olekukonko/tablewriter"
9+
10+
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
11+
"github.com/Scalingo/cli/io"
12+
"github.com/Scalingo/cli/utils"
13+
"github.com/Scalingo/go-scalingo/v11"
14+
"github.com/Scalingo/go-utils/errors/v3"
15+
)
16+
17+
type appsListRenderer struct {
18+
currentUser *scalingo.User
19+
apps []*scalingo.App
20+
}
21+
22+
func NewAppsList(currentUser *scalingo.User) renderer.Renderer[[]*scalingo.App] {
23+
return &appsListRenderer{
24+
currentUser: currentUser,
25+
}
26+
}
27+
28+
func (r *appsListRenderer) Render(ctx context.Context) error {
29+
if len(r.apps) == 0 {
30+
fmt.Println(io.Indent("\nYou haven't created any app yet, create your first application using:\n→ scalingo create <app_name>\n", 2))
31+
return nil
32+
}
33+
34+
t := tablewriter.NewWriter(os.Stdout)
35+
t.Header([]string{"Name", "Role", "Status", "Project"})
36+
37+
for _, app := range r.apps {
38+
role := utils.AppRole(r.currentUser, app)
39+
err := t.Append([]string{app.Name, string(role), string(app.Status), app.ProjectSlug()})
40+
if err != nil {
41+
return errors.Wrap(ctx, err, "append app to table")
42+
}
43+
}
44+
45+
return t.Render()
46+
}
47+
48+
func (r *appsListRenderer) SetData(ctx context.Context, apps []*scalingo.App) {
49+
r.apps = apps
50+
}

scalingo/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/Scalingo/cli/cmd"
1515
"github.com/Scalingo/cli/cmd/autocomplete"
1616
"github.com/Scalingo/cli/config"
17+
"github.com/Scalingo/cli/internal/boundaries/out/renderer"
1718
"github.com/Scalingo/cli/signals"
1819
"github.com/Scalingo/cli/update"
1920
"github.com/Scalingo/go-scalingo/v11/debug"
@@ -88,6 +89,7 @@ func main() {
8889
app.Flags = []cli.Flag{
8990
&cli.StringFlag{Name: "addon", Value: "<addon_id>", Usage: "ID of the current addon", Sources: cli.EnvVars("SCALINGO_ADDON")},
9091
&cli.StringFlag{Name: "app", Aliases: []string{"a"}, Value: "<name>", Usage: "Name of the app", Sources: cli.EnvVars("SCALINGO_APP")},
92+
&cli.StringFlag{Name: "format", Value: string(renderer.FormatTable), Usage: "[" + string(renderer.FormatJSON) + "|" + string(renderer.FormatTable) + "]"},
9193
&cli.StringFlag{Name: "remote", Aliases: []string{"r"}, Value: "scalingo", Usage: "Name of the remote"},
9294
&cli.StringFlag{Name: "region", Value: "", Usage: "Name of the region to use"},
9395
}

0 commit comments

Comments
 (0)