Skip to content

Commit e08c6c0

Browse files
Merge pull request #281 from dropbox/json-normalize-account-du
Normalize account and du to shared JSON operation output
2 parents c334e34 + 3338abc commit e08c6c0

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)