Skip to content

Commit 8285450

Browse files
Merge pull request #286 from dropbox/json-team-commands
Add JSON output to team commands with pagination support
2 parents 3cdff04 + 5fc9ce6 commit 8285450

10 files changed

Lines changed: 856 additions & 27 deletions

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,17 @@ $ dbxcli share-link update --output=json https://www.dropbox.com/s/example/old.p
155155
$ dbxcli share-link revoke --output=json https://www.dropbox.com/s/example/old.pdf
156156
$ dbxcli share-link download --output=json https://www.dropbox.com/s/example/old.pdf ./old.pdf
157157
$ dbxcli share list folder --output=json
158+
$ dbxcli team info --output=json
159+
$ dbxcli team list-members --output=json
160+
$ dbxcli team list-groups --output=json
161+
$ dbxcli team add-member --output=json user@example.com User Name
162+
$ dbxcli team remove-member --output=json user@example.com
158163
$ dbxcli mkdir --output=json /new-folder
159164
$ dbxcli rm --output=json /old-file.txt
160165
$ dbxcli restore --output=json /Reports/old.pdf 015f...
161166
```
162167

163-
Structured success output is rolling out command by command. Currently migrated commands are `version`, `account`, `du`, `ls`, `search`, `revs`, `cp`, `mv`, `put`, `get`, `share-link create`, `share-link list`, `share-link info`, `share-link update`, `share-link revoke`, `share-link download`, `share list folder`, `mkdir`, `rm`, and `restore`. Commands that have not been migrated return a JSON error whose `error.message` is `structured output is not supported for this command yet` when used with `--output=json`.
168+
Structured success output is rolling out command by command. Currently migrated commands are `version`, `account`, `du`, `ls`, `search`, `revs`, `cp`, `mv`, `put`, `get`, `share-link create`, `share-link list`, `share-link info`, `share-link update`, `share-link revoke`, `share-link download`, `share list folder`, `team info`, `team list-members`, `team list-groups`, `team add-member`, `team remove-member`, `mkdir`, `rm`, and `restore`. Commands that have not been migrated return a JSON error whose `error.message` is `structured output is not supported for this command yet` when used with `--output=json`.
164169

165170
Command results and JSON errors are written to stdout. Status, progress, human-facing warnings, diagnostics, and verbose logs are written to stderr. JSON errors include a `warnings` array for machine-actionable warnings; it is `[]` when no warnings are present. Successful JSON payloads use the same `warnings` field.
166171

@@ -422,6 +427,29 @@ The legacy `share list folder` command also supports operation-style JSON. It us
422427
}
423428
```
424429

430+
Team commands use the same operation-style wrapper. `team info` returns a single `team` result, `team list-members` and `team list-groups` return `listed` results, and mutating member commands return the Dropbox launch status:
431+
432+
```json
433+
{
434+
"input": {},
435+
"results": [
436+
{
437+
"status": "listed",
438+
"kind": "team_member",
439+
"result": {
440+
"type": "team_member",
441+
"team_member_id": "dbmid:...",
442+
"email": "user@example.com",
443+
"email_verified": true,
444+
"status": "active",
445+
"role": "member_only"
446+
}
447+
}
448+
],
449+
"warnings": []
450+
}
451+
```
452+
425453
`get --output=json <source> -` and `share-link download --output=json <url> -` are not supported because stdout is reserved for downloaded file bytes when the target is `-`.
426454

427455
In JSON mode, command errors are written to stdout as JSON, including errors from commands that do not yet support structured success output. The process still exits with a non-zero status. Detailed diagnostics may also be written to stderr:

cmd/add-member.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package cmd
1717
import (
1818
"errors"
1919
"fmt"
20+
"io"
2021

2122
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team"
2223
"github.com/spf13/cobra"
@@ -26,7 +27,7 @@ func addMember(cmd *cobra.Command, args []string) (err error) {
2627
if len(args) != 3 {
2728
return errors.New("`add-member` requires `email`, `first`, and `last` arguments")
2829
}
29-
dbx := team.New(config)
30+
dbx := teamNewFunc(config)
3031

3132
email := args[0]
3233
firstName := args[1]
@@ -39,10 +40,22 @@ func addMember(cmd *cobra.Command, args []string) (err error) {
3940
if err != nil {
4041
return err
4142
}
42-
if res.Tag == "complete" {
43-
fmt.Printf("User successfully added to the team.\n")
43+
input := teamMemberAddInput{
44+
Email: email,
45+
FirstName: firstName,
46+
LastName: lastName,
4447
}
45-
return
48+
return commandOutput(cmd).Render(func(w io.Writer) error {
49+
return renderTeamMemberAdd(w, res)
50+
}, teamMemberAddOperationOutput(input, res))
51+
}
52+
53+
func renderTeamMemberAdd(out io.Writer, res *team.MembersAddLaunch) error {
54+
if res != nil && res.Tag == "complete" {
55+
_, err := fmt.Fprintln(out, "User successfully added to the team.")
56+
return err
57+
}
58+
return nil
4659
}
4760

4861
// addMemberCmd represents the add-member command
@@ -54,4 +67,5 @@ var addMemberCmd = &cobra.Command{
5467

5568
func init() {
5669
teamCmd.AddCommand(addMemberCmd)
70+
enableStructuredOutput(addMemberCmd)
5771
}

cmd/info.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,28 @@ package cmd
1616

1717
import (
1818
"fmt"
19-
"os"
19+
"io"
2020
"text/tabwriter"
2121

2222
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team"
2323
"github.com/spf13/cobra"
2424
)
2525

2626
func info(cmd *cobra.Command, args []string) (err error) {
27-
dbx := team.New(config)
27+
dbx := teamNewFunc(config)
2828
res, err := dbx.GetInfo()
2929
if err != nil {
3030
return err
3131
}
3232

33+
return commandOutput(cmd).Render(func(w io.Writer) error {
34+
return renderTeamInfo(w, res)
35+
}, teamInfoOperationOutput(res))
36+
}
37+
38+
func renderTeamInfo(out io.Writer, res *team.TeamGetInfoResult) error {
3339
w := new(tabwriter.Writer)
34-
w.Init(os.Stdout, 4, 8, 1, ' ', 0)
40+
w.Init(out, 4, 8, 1, ' ', 0)
3541
fmt.Fprintf(w, "Name:\t%s\n", res.Name)
3642
fmt.Fprintf(w, "Team Id:\t%s\n", res.TeamId)
3743
fmt.Fprintf(w, "Licensed Users:\t%d\n", res.NumLicensedUsers)
@@ -48,4 +54,5 @@ var infoCmd = &cobra.Command{
4854

4955
func init() {
5056
teamCmd.AddCommand(infoCmd)
57+
enableStructuredOutput(infoCmd)
5158
}

cmd/list-groups.go

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,61 @@
1515
package cmd
1616

1717
import (
18+
"errors"
1819
"fmt"
19-
"os"
20+
"io"
2021
"text/tabwriter"
2122

2223
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team"
24+
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team_common"
2325
"github.com/spf13/cobra"
2426
)
2527

2628
func listGroups(cmd *cobra.Command, args []string) (err error) {
27-
dbx := team.New(config)
29+
dbx := teamNewFunc(config)
2830
arg := team.NewGroupsListArg()
29-
res, err := dbx.GroupsList(arg)
31+
groups, err := listTeamGroups(dbx, arg)
3032
if err != nil {
3133
return err
3234
}
3335

34-
if len(res.Groups) == 0 {
35-
return
36+
commandVerboseStatus(cmd, "Listed %d team groups", len(groups))
37+
38+
return commandOutput(cmd).Render(func(w io.Writer) error {
39+
return renderTeamGroups(w, groups)
40+
}, newJSONOperationOutput(teamInfoInput{}, teamGroupOperationResults(groups), nil))
41+
}
42+
43+
func listTeamGroups(dbx teamClient, arg *team.GroupsListArg) ([]*team_common.GroupSummary, error) {
44+
var groups []*team_common.GroupSummary
45+
res, err := dbx.GroupsList(arg)
46+
if err != nil {
47+
return nil, err
48+
}
49+
groups = append(groups, res.Groups...)
50+
51+
for res.HasMore {
52+
if res.Cursor == "" {
53+
return nil, errors.New("team group list has more results but no cursor")
54+
}
55+
res, err = dbx.GroupsListContinue(team.NewGroupsListContinueArg(res.Cursor))
56+
if err != nil {
57+
return nil, err
58+
}
59+
groups = append(groups, res.Groups...)
60+
}
61+
return groups, nil
62+
}
63+
64+
func renderTeamGroups(out io.Writer, groups []*team_common.GroupSummary) error {
65+
if len(groups) == 0 {
66+
return nil
3667
}
3768

3869
w := new(tabwriter.Writer)
39-
w.Init(os.Stdout, 4, 8, 1, ' ', 0)
70+
w.Init(out, 4, 8, 1, ' ', 0)
4071
fmt.Fprintf(w, "Name\tId\t# Members\tExternal Id\n")
41-
for _, group := range res.Groups {
72+
for _, group := range groups {
4273
fmt.Fprintf(w, "%s\t%s\t%d\t%s\n", group.GroupName, group.GroupId, group.MemberCount, group.GroupExternalId)
4374
}
4475
return w.Flush()
@@ -53,4 +84,5 @@ var listGroupsCmd = &cobra.Command{
5384

5485
func init() {
5586
teamCmd.AddCommand(listGroupsCmd)
87+
enableStructuredOutput(listGroupsCmd)
5688
}

cmd/list-members.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,61 @@
1515
package cmd
1616

1717
import (
18+
"errors"
1819
"fmt"
19-
"os"
20+
"io"
2021
"text/tabwriter"
2122

2223
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team"
2324
"github.com/spf13/cobra"
2425
)
2526

2627
func listMembers(cmd *cobra.Command, args []string) (err error) {
27-
dbx := team.New(config)
28+
dbx := teamNewFunc(config)
2829
arg := team.NewMembersListArg()
29-
res, err := dbx.MembersList(arg)
30+
members, err := listTeamMembers(dbx, arg)
3031
if err != nil {
3132
return err
3233
}
3334

34-
if len(res.Members) == 0 {
35-
return
35+
commandVerboseStatus(cmd, "Listed %d team members", len(members))
36+
37+
return commandOutput(cmd).Render(func(w io.Writer) error {
38+
return renderTeamMembers(w, members)
39+
}, newJSONOperationOutput(teamInfoInput{}, teamMemberOperationResults(members), nil))
40+
}
41+
42+
func listTeamMembers(dbx teamClient, arg *team.MembersListArg) ([]*team.TeamMemberInfo, error) {
43+
var members []*team.TeamMemberInfo
44+
res, err := dbx.MembersList(arg)
45+
if err != nil {
46+
return nil, err
47+
}
48+
members = append(members, res.Members...)
49+
50+
for res.HasMore {
51+
if res.Cursor == "" {
52+
return nil, errors.New("team member list has more results but no cursor")
53+
}
54+
res, err = dbx.MembersListContinue(team.NewMembersListContinueArg(res.Cursor))
55+
if err != nil {
56+
return nil, err
57+
}
58+
members = append(members, res.Members...)
59+
}
60+
return members, nil
61+
}
62+
63+
func renderTeamMembers(out io.Writer, members []*team.TeamMemberInfo) error {
64+
if len(members) == 0 {
65+
return nil
3666
}
3767

3868
w := new(tabwriter.Writer)
39-
w.Init(os.Stdout, 4, 8, 1, ' ', 0)
69+
w.Init(out, 4, 8, 1, ' ', 0)
4070
fmtStr := "%s\t%s\t%s\t%s\t%s\n"
4171
fmt.Fprintf(w, fmtStr, "Name", "Id", "Status", "Email", "Role")
42-
for _, member := range res.Members {
72+
for _, member := range members {
4373
fmt.Fprintf(w, fmtStr,
4474
member.Profile.Name.DisplayName,
4575
member.Profile.TeamMemberId,
@@ -59,4 +89,5 @@ var listMembersCmd = &cobra.Command{
5989

6090
func init() {
6191
teamCmd.AddCommand(listMembersCmd)
92+
enableStructuredOutput(listMembersCmd)
6293
}

cmd/output.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ func jsonErrorCode(err error) string {
180180
return "unknown_flag"
181181
case strings.Contains(message, "path exists and is not a folder"):
182182
return "path_conflict"
183-
case strings.Contains(message, "requires a"):
183+
case strings.Contains(message, "requires "):
184184
return "invalid_arguments"
185185
case strings.Contains(message, "accepts an optional"):
186186
return "invalid_arguments"

cmd/output_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,13 @@ func TestJSONErrorCodeOptionalArgumentValidation(t *testing.T) {
608608
}
609609
}
610610

611+
func TestJSONErrorCodeRequiredArgumentValidation(t *testing.T) {
612+
err := errors.New("`add-member` requires `email`, `first`, and `last` arguments")
613+
if got, want := jsonErrorCode(err), "invalid_arguments"; got != want {
614+
t.Fatalf("jsonErrorCode = %q, want %q", got, want)
615+
}
616+
}
617+
611618
func decodeJSONErrorResponse(t *testing.T, value string) jsonErrorResponse {
612619
t.Helper()
613620

cmd/remove-member.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ package cmd
1717
import (
1818
"errors"
1919
"fmt"
20+
"io"
2021

22+
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/async"
2123
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team"
2224
"github.com/spf13/cobra"
2325
)
@@ -27,7 +29,7 @@ func removeMember(cmd *cobra.Command, args []string) (err error) {
2729
return errors.New("`remove-member` requires an `email` argument")
2830
}
2931

30-
dbx := team.New(config)
32+
dbx := teamNewFunc(config)
3133
email := args[0]
3234
selector := &team.UserSelectorArg{Email: email}
3335
selector.Tag = "email"
@@ -36,10 +38,18 @@ func removeMember(cmd *cobra.Command, args []string) (err error) {
3638
if err != nil {
3739
return err
3840
}
39-
if res.Tag == "complete" {
40-
fmt.Printf("User successfully removed from team.\n")
41+
input := teamMemberRemoveInput{Email: email}
42+
return commandOutput(cmd).Render(func(w io.Writer) error {
43+
return renderTeamMemberRemove(w, res)
44+
}, teamMemberRemoveOperationOutput(input, res))
45+
}
46+
47+
func renderTeamMemberRemove(out io.Writer, res *async.LaunchEmptyResult) error {
48+
if res != nil && res.Tag == "complete" {
49+
_, err := fmt.Fprintln(out, "User successfully removed from team.")
50+
return err
4151
}
42-
return
52+
return nil
4353
}
4454

4555
// removeMemberCmd represents the remove-member command
@@ -51,4 +61,5 @@ var removeMemberCmd = &cobra.Command{
5161

5262
func init() {
5363
teamCmd.AddCommand(removeMemberCmd)
64+
enableStructuredOutput(removeMemberCmd)
5465
}

0 commit comments

Comments
 (0)