-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #440 from ripcurld0/search_format
Add --format to docker-search
- Loading branch information
Showing
4 changed files
with
452 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
package formatter | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
|
||
registry "github.com/docker/docker/api/types/registry" | ||
"github.com/docker/docker/pkg/stringutils" | ||
) | ||
|
||
const ( | ||
defaultSearchTableFormat = "table {{.Name}}\t{{.Description}}\t{{.StarCount}}\t{{.IsOfficial}}\t{{.IsAutomated}}" | ||
|
||
starsHeader = "STARS" | ||
officialHeader = "OFFICIAL" | ||
automatedHeader = "AUTOMATED" | ||
) | ||
|
||
// NewSearchFormat returns a Format for rendering using a network Context | ||
func NewSearchFormat(source string) Format { | ||
switch source { | ||
case "": | ||
return defaultSearchTableFormat | ||
case TableFormatKey: | ||
return defaultSearchTableFormat | ||
} | ||
return Format(source) | ||
} | ||
|
||
// SearchWrite writes the context | ||
func SearchWrite(ctx Context, results []registry.SearchResult, auto bool, stars int) error { | ||
render := func(format func(subContext subContext) error) error { | ||
for _, result := range results { | ||
// --automated and -s, --stars are deprecated since Docker 1.12 | ||
if (auto && !result.IsAutomated) || (stars > result.StarCount) { | ||
continue | ||
} | ||
searchCtx := &searchContext{trunc: ctx.Trunc, s: result} | ||
if err := format(searchCtx); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
searchCtx := searchContext{} | ||
searchCtx.header = map[string]string{ | ||
"Name": nameHeader, | ||
"Description": descriptionHeader, | ||
"StarCount": starsHeader, | ||
"IsOfficial": officialHeader, | ||
"IsAutomated": automatedHeader, | ||
} | ||
return ctx.Write(&searchCtx, render) | ||
} | ||
|
||
type searchContext struct { | ||
HeaderContext | ||
trunc bool | ||
json bool | ||
s registry.SearchResult | ||
} | ||
|
||
func (c *searchContext) MarshalJSON() ([]byte, error) { | ||
c.json = true | ||
return marshalJSON(c) | ||
} | ||
|
||
func (c *searchContext) Name() string { | ||
return c.s.Name | ||
} | ||
|
||
func (c *searchContext) Description() string { | ||
desc := strings.Replace(c.s.Description, "\n", " ", -1) | ||
desc = strings.Replace(desc, "\r", " ", -1) | ||
if c.trunc { | ||
desc = stringutils.Ellipsis(desc, 45) | ||
} | ||
return desc | ||
} | ||
|
||
func (c *searchContext) StarCount() string { | ||
return strconv.Itoa(c.s.StarCount) | ||
} | ||
|
||
func (c *searchContext) formatBool(value bool) string { | ||
switch { | ||
case value && c.json: | ||
return "true" | ||
case value: | ||
return "[OK]" | ||
case c.json: | ||
return "false" | ||
default: | ||
return "" | ||
} | ||
} | ||
|
||
func (c *searchContext) IsOfficial() string { | ||
return c.formatBool(c.s.IsOfficial) | ||
} | ||
|
||
func (c *searchContext) IsAutomated() string { | ||
return c.formatBool(c.s.IsAutomated) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,284 @@ | ||
package formatter | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"strings" | ||
"testing" | ||
|
||
registrytypes "github.com/docker/docker/api/types/registry" | ||
"github.com/docker/docker/pkg/stringutils" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestSearchContext(t *testing.T) { | ||
name := "nginx" | ||
starCount := 5000 | ||
|
||
var ctx searchContext | ||
cases := []struct { | ||
searchCtx searchContext | ||
expValue string | ||
call func() string | ||
}{ | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Name: name}, | ||
}, name, ctx.Name}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{StarCount: starCount}, | ||
}, "5000", ctx.StarCount}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{IsOfficial: true}, | ||
}, "[OK]", ctx.IsOfficial}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{IsOfficial: false}, | ||
}, "", ctx.IsOfficial}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{IsAutomated: true}, | ||
}, "[OK]", ctx.IsAutomated}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{IsAutomated: false}, | ||
}, "", ctx.IsAutomated}, | ||
} | ||
|
||
for _, c := range cases { | ||
ctx = c.searchCtx | ||
v := c.call() | ||
if strings.Contains(v, ",") { | ||
compareMultipleValues(t, v, c.expValue) | ||
} else if v != c.expValue { | ||
t.Fatalf("Expected %s, was %s\n", c.expValue, v) | ||
} | ||
} | ||
} | ||
|
||
func TestSearchContextDescription(t *testing.T) { | ||
shortDescription := "Official build of Nginx." | ||
longDescription := "Automated Nginx reverse proxy for docker containers" | ||
descriptionWReturns := "Automated\nNginx reverse\rproxy\rfor docker\ncontainers" | ||
|
||
var ctx searchContext | ||
cases := []struct { | ||
searchCtx searchContext | ||
expValue string | ||
call func() string | ||
}{ | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: shortDescription}, | ||
trunc: true, | ||
}, shortDescription, ctx.Description}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: shortDescription}, | ||
trunc: false, | ||
}, shortDescription, ctx.Description}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: longDescription}, | ||
trunc: false, | ||
}, longDescription, ctx.Description}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: longDescription}, | ||
trunc: true, | ||
}, stringutils.Ellipsis(longDescription, 45), ctx.Description}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: descriptionWReturns}, | ||
trunc: false, | ||
}, longDescription, ctx.Description}, | ||
{searchContext{ | ||
s: registrytypes.SearchResult{Description: descriptionWReturns}, | ||
trunc: true, | ||
}, stringutils.Ellipsis(longDescription, 45), ctx.Description}, | ||
} | ||
|
||
for _, c := range cases { | ||
ctx = c.searchCtx | ||
v := c.call() | ||
if strings.Contains(v, ",") { | ||
compareMultipleValues(t, v, c.expValue) | ||
} else if v != c.expValue { | ||
t.Fatalf("Expected %s, was %s\n", c.expValue, v) | ||
} | ||
} | ||
} | ||
|
||
func TestSearchContextWrite(t *testing.T) { | ||
cases := []struct { | ||
context Context | ||
expected string | ||
}{ | ||
|
||
// Errors | ||
{ | ||
Context{Format: "{{InvalidFunction}}"}, | ||
`Template parsing error: template: :1: function "InvalidFunction" not defined | ||
`, | ||
}, | ||
{ | ||
Context{Format: "{{nil}}"}, | ||
`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command | ||
`, | ||
}, | ||
// Table format | ||
{ | ||
Context{Format: NewSearchFormat("table")}, | ||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED | ||
result1 Official build 5000 [OK] | ||
result2 Not official 5 [OK] | ||
`, | ||
}, | ||
{ | ||
Context{Format: NewSearchFormat("table {{.Name}}")}, | ||
`NAME | ||
result1 | ||
result2 | ||
`, | ||
}, | ||
// Custom Format | ||
{ | ||
Context{Format: NewSearchFormat("{{.Name}}")}, | ||
`result1 | ||
result2 | ||
`, | ||
}, | ||
// Custom Format with CreatedAt | ||
{ | ||
Context{Format: NewSearchFormat("{{.Name}} {{.StarCount}}")}, | ||
`result1 5000 | ||
result2 5 | ||
`, | ||
}, | ||
} | ||
|
||
for _, testcase := range cases { | ||
results := []registrytypes.SearchResult{ | ||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false}, | ||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, | ||
} | ||
out := bytes.NewBufferString("") | ||
testcase.context.Output = out | ||
err := SearchWrite(testcase.context, results, false, 0) | ||
if err != nil { | ||
assert.Error(t, err, testcase.expected) | ||
} else { | ||
assert.Equal(t, out.String(), testcase.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestSearchContextWriteAutomated(t *testing.T) { | ||
cases := []struct { | ||
context Context | ||
expected string | ||
}{ | ||
|
||
// Table format | ||
{ | ||
Context{Format: NewSearchFormat("table")}, | ||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED | ||
result2 Not official 5 [OK] | ||
`, | ||
}, | ||
{ | ||
Context{Format: NewSearchFormat("table {{.Name}}")}, | ||
`NAME | ||
result2 | ||
`, | ||
}, | ||
} | ||
|
||
for _, testcase := range cases { | ||
results := []registrytypes.SearchResult{ | ||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false}, | ||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, | ||
} | ||
out := bytes.NewBufferString("") | ||
testcase.context.Output = out | ||
err := SearchWrite(testcase.context, results, true, 0) | ||
if err != nil { | ||
assert.Error(t, err, testcase.expected) | ||
} else { | ||
assert.Equal(t, out.String(), testcase.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestSearchContextWriteStars(t *testing.T) { | ||
cases := []struct { | ||
context Context | ||
expected string | ||
}{ | ||
|
||
// Table format | ||
{ | ||
Context{Format: NewSearchFormat("table")}, | ||
`NAME DESCRIPTION STARS OFFICIAL AUTOMATED | ||
result1 Official build 5000 [OK] | ||
`, | ||
}, | ||
{ | ||
Context{Format: NewSearchFormat("table {{.Name}}")}, | ||
`NAME | ||
result1 | ||
`, | ||
}, | ||
} | ||
|
||
for _, testcase := range cases { | ||
results := []registrytypes.SearchResult{ | ||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false}, | ||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, | ||
} | ||
out := bytes.NewBufferString("") | ||
testcase.context.Output = out | ||
err := SearchWrite(testcase.context, results, false, 6) | ||
if err != nil { | ||
assert.Error(t, err, testcase.expected) | ||
} else { | ||
assert.Equal(t, out.String(), testcase.expected) | ||
} | ||
} | ||
} | ||
|
||
func TestSearchContextWriteJSON(t *testing.T) { | ||
results := []registrytypes.SearchResult{ | ||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false}, | ||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, | ||
} | ||
expectedJSONs := []map[string]interface{}{ | ||
{"Name": "result1", "Description": "Official build", "StarCount": "5000", "IsOfficial": "true", "IsAutomated": "false"}, | ||
{"Name": "result2", "Description": "Not official", "StarCount": "5", "IsOfficial": "false", "IsAutomated": "true"}, | ||
} | ||
|
||
out := bytes.NewBufferString("") | ||
err := SearchWrite(Context{Format: "{{json .}}", Output: out}, results, false, 0) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { | ||
t.Logf("Output: line %d: %s", i, line) | ||
var m map[string]interface{} | ||
if err := json.Unmarshal([]byte(line), &m); err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.Equal(t, m, expectedJSONs[i]) | ||
} | ||
} | ||
|
||
func TestSearchContextWriteJSONField(t *testing.T) { | ||
results := []registrytypes.SearchResult{ | ||
{Name: "result1", Description: "Official build", StarCount: 5000, IsOfficial: true, IsAutomated: false}, | ||
{Name: "result2", Description: "Not official", StarCount: 5, IsOfficial: false, IsAutomated: true}, | ||
} | ||
out := bytes.NewBufferString("") | ||
err := SearchWrite(Context{Format: "{{json .Name}}", Output: out}, results, false, 0) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
for i, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { | ||
t.Logf("Output: line %d: %s", i, line) | ||
var s string | ||
if err := json.Unmarshal([]byte(line), &s); err != nil { | ||
t.Fatal(err) | ||
} | ||
assert.Equal(t, s, results[i].Name) | ||
} | ||
} |
Oops, something went wrong.