Skip to content

Commit 3338abc

Browse files
Normalize account and du commands to shared JSON operation output
Extends jsonOperationOutput normalization to account and du commands. Both now output the consistent input/results/warnings structure. Adds completion command registration for shell completions.
1 parent 63abf16 commit 3338abc

7 files changed

Lines changed: 373 additions & 54 deletions

File tree

README.md

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Structured success output is rolling out command by command. Currently migrated
158158

159159
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. New operation-style JSON payloads should use the same `warnings` field.
160160

161-
Successful JSON responses are command-specific. Operation commands return an `input` object, a `results` array, and a `warnings` array. For commands such as `mkdir`, each result reports what happened to the requested path:
161+
Successful JSON responses are command-specific. Operation-style commands return an `input` object, a `results` array, and a `warnings` array. For commands such as `mkdir`, each result reports what happened to the requested path:
162162

163163
```json
164164
{
@@ -299,28 +299,45 @@ Commands that return entry lists, such as `ls`, `search`, and `revs`, return an
299299
}
300300
```
301301

302-
Account and usage commands return command-specific objects:
302+
Account and usage commands use the operation-style wrapper with a single result:
303303

304304
```json
305305
{
306306
"input": {},
307-
"account": {
308-
"type": "full",
309-
"account_id": "dbid:...",
310-
"email": "user@example.com",
311-
"email_verified": true,
312-
"disabled": false
313-
}
307+
"results": [
308+
{
309+
"kind": "account",
310+
"input": {},
311+
"result": {
312+
"type": "full",
313+
"account_id": "dbid:...",
314+
"email": "user@example.com",
315+
"email_verified": true,
316+
"disabled": false
317+
}
318+
}
319+
],
320+
"warnings": []
314321
}
315322
```
316323

317324
```json
318325
{
319-
"used": 123,
320-
"allocation": {
321-
"type": "individual",
322-
"allocated": 100000
323-
}
326+
"input": {},
327+
"results": [
328+
{
329+
"kind": "space_usage",
330+
"input": {},
331+
"result": {
332+
"used": 123,
333+
"allocation": {
334+
"type": "individual",
335+
"allocated": 100000
336+
}
337+
}
338+
}
339+
],
340+
"warnings": []
324341
}
325342
```
326343

cmd/account.go

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@ type accountInput struct {
2828
AccountID string `json:"account_id,omitempty"`
2929
}
3030

31-
type accountOutput struct {
32-
Input accountInput `json:"input"`
33-
Account jsonAccount `json:"account"`
34-
}
35-
3631
type jsonAccount struct {
3732
Type string `json:"type"`
3833
AccountID string `json:"account_id"`
@@ -64,6 +59,8 @@ type jsonAccountTeam struct {
6459
MemberID string `json:"member_id,omitempty"`
6560
}
6661

62+
const accountKindAccount = "account"
63+
6764
// renderFullAccount prints the account details returned by GetCurrentAccount.
6865
func renderFullAccount(out io.Writer, fa *users.FullAccount) error {
6966
w := new(tabwriter.Writer)
@@ -117,12 +114,10 @@ func account(cmd *cobra.Command, args []string) error {
117114
if err != nil {
118115
return err
119116
}
117+
input := accountInput{}
120118
return out.Render(func(w io.Writer) error {
121119
return renderFullAccount(w, res)
122-
}, accountOutput{
123-
Input: accountInput{},
124-
Account: jsonFullAccount(res),
125-
})
120+
}, newAccountOperationOutput(input, jsonFullAccount(res)))
126121
}
127122

128123
// Otherwise look up an account with the provided ID
@@ -131,14 +126,18 @@ func account(cmd *cobra.Command, args []string) error {
131126
if err != nil {
132127
return err
133128
}
129+
input := accountInput{
130+
AccountID: args[0],
131+
}
134132
return out.Render(func(w io.Writer) error {
135133
return renderBasicAccount(w, res)
136-
}, accountOutput{
137-
Input: accountInput{
138-
AccountID: args[0],
139-
},
140-
Account: jsonBasicAccount(res),
141-
})
134+
}, newAccountOperationOutput(input, jsonBasicAccount(res)))
135+
}
136+
137+
func newAccountOperationOutput(input accountInput, account jsonAccount) jsonOperationOutput {
138+
return newJSONOperationOutput(input, []jsonOperationResult{
139+
newJSONOperationResult("", accountKindAccount, input, account),
140+
}, nil)
142141
}
143142

144143
func jsonFullAccount(fa *users.FullAccount) jsonAccount {

cmd/account_test.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ func TestAccountCurrentJSONOutputsAccount(t *testing.T) {
5050
if got.Input.AccountID != "" {
5151
t.Fatalf("input.account_id = %q, want empty for current account", got.Input.AccountID)
5252
}
53-
account := got.Account
53+
result := got.Results[0]
54+
if result.Kind != accountKindAccount {
55+
t.Fatalf("kind = %q, want account", result.Kind)
56+
}
57+
if result.Input.AccountID != "" {
58+
t.Fatalf("result input.account_id = %q, want empty for current account", result.Input.AccountID)
59+
}
60+
account := result.Result
5461
if account.Type != "full" || account.AccountID != "dbid:current" || account.Email != "test@example.com" {
5562
t.Fatalf("account = %#v, want current full account", account)
5663
}
@@ -90,7 +97,14 @@ func TestAccountLookupJSONUsesAccountID(t *testing.T) {
9097
if got.Input.AccountID != "dbid:lookup" {
9198
t.Fatalf("input.account_id = %q, want dbid:lookup", got.Input.AccountID)
9299
}
93-
account := got.Account
100+
result := got.Results[0]
101+
if result.Kind != accountKindAccount {
102+
t.Fatalf("kind = %q, want account", result.Kind)
103+
}
104+
if result.Input.AccountID != "dbid:lookup" {
105+
t.Fatalf("result input.account_id = %q, want dbid:lookup", result.Input.AccountID)
106+
}
107+
account := result.Result
94108
if account.Type != "basic" || account.AccountID != "dbid:lookup" || account.Email != "lookup@example.com" {
95109
t.Fatalf("account = %#v, want lookup basic account", account)
96110
}
@@ -137,13 +151,34 @@ func setAccountOutputJSON(t *testing.T, cmd *cobra.Command) {
137151
}
138152
}
139153

140-
func decodeAccountOutput(t *testing.T, out *bytes.Buffer) accountOutput {
154+
type accountJSONOutput struct {
155+
Input accountInput `json:"input"`
156+
Results []accountJSONResult `json:"results"`
157+
Warnings []jsonWarning `json:"warnings"`
158+
}
159+
160+
type accountJSONResult struct {
161+
Kind string `json:"kind"`
162+
Input accountInput `json:"input"`
163+
Result jsonAccount `json:"result"`
164+
}
165+
166+
func decodeAccountOutput(t *testing.T, out *bytes.Buffer) accountJSONOutput {
141167
t.Helper()
142168

143-
var got accountOutput
144-
if err := json.NewDecoder(out).Decode(&got); err != nil {
169+
var got accountJSONOutput
170+
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
145171
t.Fatalf("decode JSON output: %v\noutput: %s", err, out.String())
146172
}
173+
if got.Warnings == nil {
174+
t.Fatalf("warnings = nil, want empty array")
175+
}
176+
if len(got.Warnings) != 0 {
177+
t.Fatalf("warnings = %+v, want empty", got.Warnings)
178+
}
179+
if len(got.Results) != 1 {
180+
t.Fatalf("results len = %d, want 1", len(got.Results))
181+
}
147182
return got
148183
}
149184

cmd/completion.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright © 2016 Dropbox, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/spf13/cobra"
21+
)
22+
23+
func newCompletionCmd() *cobra.Command {
24+
noDesc := false
25+
programName := RootCmd.Name()
26+
27+
completionCmd := &cobra.Command{
28+
Use: "completion [bash|zsh|fish|powershell]",
29+
Short: "Generate the autocompletion script for the specified shell",
30+
Long: completionLong(programName),
31+
Args: cobra.NoArgs,
32+
ValidArgsFunction: cobra.NoFileCompletions,
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
return cmd.Help()
35+
},
36+
}
37+
38+
addCompletionNoDescFlag := func(cmd *cobra.Command) {
39+
cmd.Flags().BoolVar(&noDesc, "no-descriptions", false, "disable completion descriptions")
40+
}
41+
42+
bash := &cobra.Command{
43+
Use: "bash",
44+
Short: "Generate the autocompletion script for bash",
45+
Long: bashCompletionLong(programName),
46+
Args: cobra.NoArgs,
47+
DisableFlagsInUseLine: true,
48+
ValidArgsFunction: cobra.NoFileCompletions,
49+
RunE: func(cmd *cobra.Command, args []string) error {
50+
return cmd.Root().GenBashCompletionV2(cmd.Root().OutOrStdout(), !noDesc)
51+
},
52+
}
53+
addCompletionNoDescFlag(bash)
54+
55+
zsh := &cobra.Command{
56+
Use: "zsh",
57+
Short: "Generate the autocompletion script for zsh",
58+
Long: zshCompletionLong(programName),
59+
Args: cobra.NoArgs,
60+
ValidArgsFunction: cobra.NoFileCompletions,
61+
RunE: func(cmd *cobra.Command, args []string) error {
62+
if noDesc {
63+
return cmd.Root().GenZshCompletionNoDesc(cmd.Root().OutOrStdout())
64+
}
65+
return cmd.Root().GenZshCompletion(cmd.Root().OutOrStdout())
66+
},
67+
}
68+
addCompletionNoDescFlag(zsh)
69+
70+
fish := &cobra.Command{
71+
Use: "fish",
72+
Short: "Generate the autocompletion script for fish",
73+
Long: fishCompletionLong(programName),
74+
Args: cobra.NoArgs,
75+
ValidArgsFunction: cobra.NoFileCompletions,
76+
RunE: func(cmd *cobra.Command, args []string) error {
77+
return cmd.Root().GenFishCompletion(cmd.Root().OutOrStdout(), !noDesc)
78+
},
79+
}
80+
addCompletionNoDescFlag(fish)
81+
82+
powershell := &cobra.Command{
83+
Use: "powershell",
84+
Short: "Generate the autocompletion script for powershell",
85+
Long: powershellCompletionLong(programName),
86+
Args: cobra.NoArgs,
87+
ValidArgsFunction: cobra.NoFileCompletions,
88+
RunE: func(cmd *cobra.Command, args []string) error {
89+
if noDesc {
90+
return cmd.Root().GenPowerShellCompletion(cmd.Root().OutOrStdout())
91+
}
92+
return cmd.Root().GenPowerShellCompletionWithDesc(cmd.Root().OutOrStdout())
93+
},
94+
}
95+
addCompletionNoDescFlag(powershell)
96+
97+
completionCmd.AddCommand(bash, zsh, fish, powershell)
98+
return completionCmd
99+
}
100+
101+
func completionLong(programName string) string {
102+
return fmt.Sprintf(`Generate the autocompletion script for %s for the specified shell.
103+
See each sub-command's help for details on how to use the generated script.
104+
`, programName)
105+
}
106+
107+
func bashCompletionLong(programName string) string {
108+
return fmt.Sprintf(`Generate the autocompletion script for the bash shell.
109+
110+
This script depends on the 'bash-completion' package.
111+
If it is not installed already, you can install it via your OS's package manager.
112+
113+
To load completions in your current shell session:
114+
115+
source <(%[1]s completion bash)
116+
117+
To load completions for every new session, execute once:
118+
119+
#### Linux:
120+
121+
%[1]s completion bash > /etc/bash_completion.d/%[1]s
122+
123+
#### macOS:
124+
125+
%[1]s completion bash > $(brew --prefix)/etc/bash_completion.d/%[1]s
126+
127+
You will need to start a new shell for this setup to take effect.
128+
`, programName)
129+
}
130+
131+
func zshCompletionLong(programName string) string {
132+
return fmt.Sprintf(`Generate the autocompletion script for the zsh shell.
133+
134+
If shell completion is not already enabled in your environment you will need
135+
to enable it. You can execute the following once:
136+
137+
echo "autoload -U compinit; compinit" >> ~/.zshrc
138+
139+
To load completions in your current shell session:
140+
141+
source <(%[1]s completion zsh)
142+
143+
To load completions for every new session, execute once:
144+
145+
#### Linux:
146+
147+
%[1]s completion zsh > "${fpath[1]}/_%[1]s"
148+
149+
#### macOS:
150+
151+
%[1]s completion zsh > $(brew --prefix)/share/zsh/site-functions/_%[1]s
152+
153+
You will need to start a new shell for this setup to take effect.
154+
`, programName)
155+
}
156+
157+
func fishCompletionLong(programName string) string {
158+
return fmt.Sprintf(`Generate the autocompletion script for the fish shell.
159+
160+
To load completions in your current shell session:
161+
162+
%[1]s completion fish | source
163+
164+
To load completions for every new session, execute once:
165+
166+
%[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
167+
168+
You will need to start a new shell for this setup to take effect.
169+
`, programName)
170+
}
171+
172+
func powershellCompletionLong(programName string) string {
173+
return fmt.Sprintf(`Generate the autocompletion script for powershell.
174+
175+
To load completions in your current shell session:
176+
177+
%[1]s completion powershell | Out-String | Invoke-Expression
178+
179+
To load completions for every new session, add the output of the above command
180+
to your powershell profile.
181+
`, programName)
182+
}
183+
184+
func init() {
185+
RootCmd.AddCommand(newCompletionCmd())
186+
}

0 commit comments

Comments
 (0)