Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 928fd45

Browse files
committed
Add support for multiple formatters to control output format of render
Signed-off-by: Joffrey F <joffrey@docker.com>
1 parent aef230a commit 928fd45

9 files changed

Lines changed: 225 additions & 2 deletions

File tree

cmd/docker-app/render.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"os"
66

77
"github.com/docker/app/internal"
8+
"github.com/docker/app/internal/formatter"
89
"github.com/docker/app/internal/packager"
9-
"github.com/docker/app/internal/yaml"
1010
"github.com/docker/app/render"
1111
"github.com/docker/app/types"
1212
"github.com/docker/cli/cli"
@@ -16,6 +16,7 @@ import (
1616
)
1717

1818
var (
19+
formatDriver string
1920
renderComposeFiles []string
2021
renderSettingsFile []string
2122
renderEnv []string
@@ -42,7 +43,7 @@ func renderCmd(dockerCli command.Cli) *cobra.Command {
4243
if err != nil {
4344
return err
4445
}
45-
res, err := yaml.Marshal(rendered)
46+
res, err := formatter.Format(rendered, formatDriver)
4647
if err != nil {
4748
return err
4849
}
@@ -68,5 +69,6 @@ func renderCmd(dockerCli command.Cli) *cobra.Command {
6869
cmd.Flags().StringArrayVarP(&renderSettingsFile, "settings-files", "f", []string{}, "Override settings files")
6970
cmd.Flags().StringArrayVarP(&renderEnv, "set", "s", []string{}, "Override settings values")
7071
cmd.Flags().StringVarP(&renderOutput, "output", "o", "-", "Output file")
72+
cmd.Flags().StringVarP(&formatDriver, "presenter", "p", "yaml", "Configure the output format")
7173
return cmd
7274
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package driver
2+
3+
import (
4+
composetypes "github.com/docker/cli/cli/compose/types"
5+
)
6+
7+
// Driver is the interface that must be implemented by a formatter driver.
8+
type Driver interface {
9+
// Format executes the formatter on the source config
10+
Format(config *composetypes.Config) (string, error)
11+
}

internal/formatter/formatter.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package formatter
2+
3+
import (
4+
"sort"
5+
"sync"
6+
7+
"github.com/docker/app/internal/formatter/driver"
8+
composetypes "github.com/docker/cli/cli/compose/types"
9+
"github.com/pkg/errors"
10+
)
11+
12+
var (
13+
driversMu sync.RWMutex
14+
drivers = map[string]driver.Driver{}
15+
)
16+
17+
// Register makes a formatter available by the provided name.
18+
// If Register is called twice with the same name or if driver is nil,
19+
// it panics.
20+
func Register(name string, driver driver.Driver) {
21+
driversMu.Lock()
22+
defer driversMu.Unlock()
23+
if driver == nil {
24+
panic("formatter: Register driver is nil")
25+
}
26+
if _, dup := drivers[name]; dup {
27+
panic("formatter: Register called twice for driver " + name)
28+
}
29+
drivers[name] = driver
30+
}
31+
32+
// Format uses the specified formatter to create a printable output.
33+
// If the formatter is not registered, this errors out.
34+
func Format(config *composetypes.Config, formatter string) (string, error) {
35+
driversMu.RLock()
36+
d, present := drivers[formatter]
37+
driversMu.RUnlock()
38+
if !present {
39+
return "", errors.Errorf("unknown formatter %s", formatter)
40+
}
41+
s, err := d.Format(config)
42+
if err != nil {
43+
return "", err
44+
}
45+
return s, nil
46+
}
47+
48+
// Drivers returns a sorted list of the names of the registered drivers.
49+
func Drivers() []string {
50+
list := []string{}
51+
driversMu.RLock()
52+
for name := range drivers {
53+
list = append(list, name)
54+
}
55+
driversMu.RUnlock()
56+
sort.Strings(list)
57+
return list
58+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package formatter
2+
3+
import (
4+
"testing"
5+
6+
"github.com/docker/app/internal/formatter/driver"
7+
composetypes "github.com/docker/cli/cli/compose/types"
8+
"github.com/pkg/errors"
9+
"gotest.tools/assert"
10+
is "gotest.tools/assert/cmp"
11+
)
12+
13+
type fakeDriver struct{}
14+
15+
func (d *fakeDriver) Format(config *composetypes.Config) (string, error) {
16+
return "fake", nil
17+
}
18+
19+
type fakeErrorDriver struct{}
20+
21+
func (d *fakeErrorDriver) Format(config *composetypes.Config) (string, error) {
22+
return "", errors.New("error in driver")
23+
}
24+
25+
func TestRegisterNilPanics(t *testing.T) {
26+
defer func() {
27+
if recover() == nil {
28+
t.Errorf("The code did not panic")
29+
}
30+
resetDrivers()
31+
}()
32+
Register("foo", nil)
33+
}
34+
35+
func TestRegisterDuplicatePanics(t *testing.T) {
36+
defer func() {
37+
if recover() == nil {
38+
t.Errorf("The code did not panic")
39+
}
40+
resetDrivers()
41+
}()
42+
Register("bar", &fakeDriver{})
43+
Register("bar", &fakeDriver{})
44+
}
45+
46+
func TestRegister(t *testing.T) {
47+
d := &fakeDriver{}
48+
Register("baz", d)
49+
defer resetDrivers()
50+
assert.Check(t, is.DeepEqual(drivers, map[string]driver.Driver{"baz": d}))
51+
}
52+
53+
func TestNoDrivers(t *testing.T) {
54+
assert.Check(t, is.DeepEqual(Drivers(), []string{}))
55+
}
56+
57+
func TestRegisteredDrivers(t *testing.T) {
58+
Register("foo", &fakeDriver{})
59+
Register("bar", &fakeDriver{})
60+
defer resetDrivers()
61+
assert.Check(t, is.DeepEqual(Drivers(), []string{"bar", "foo"}))
62+
}
63+
64+
func TestFormatNonExistentDriver(t *testing.T) {
65+
_, err := Format(&composetypes.Config{}, "toto")
66+
assert.Check(t, err != nil)
67+
assert.Check(t, is.ErrorContains(err, "unknown formatter toto"))
68+
}
69+
70+
func TestFormatErrorDriver(t *testing.T) {
71+
Register("err", &fakeErrorDriver{})
72+
defer resetDrivers()
73+
_, err := Format(&composetypes.Config{}, "err")
74+
assert.Check(t, err != nil)
75+
assert.Check(t, is.ErrorContains(err, "error in driver"))
76+
}
77+
78+
func TestFormatNone(t *testing.T) {
79+
Register("fake", &fakeDriver{})
80+
defer resetDrivers()
81+
_, err := Format(&composetypes.Config{}, "none")
82+
assert.Check(t, err != nil)
83+
assert.Check(t, is.ErrorContains(err, "unknown formatter none"))
84+
}
85+
86+
func TestFormat(t *testing.T) {
87+
Register("fake", &fakeDriver{})
88+
defer resetDrivers()
89+
s, err := Format(&composetypes.Config{}, "fake")
90+
assert.NilError(t, err)
91+
assert.Check(t, is.Equal(s, "fake"))
92+
}
93+
94+
func resetDrivers() {
95+
drivers = map[string]driver.Driver{}
96+
}

internal/formatter/json/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package json

internal/formatter/json/driver.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package json
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/docker/app/internal/formatter"
7+
composetypes "github.com/docker/cli/cli/compose/types"
8+
"github.com/pkg/errors"
9+
)
10+
11+
func init() {
12+
formatter.Register("json", &Driver{})
13+
}
14+
15+
// Driver is the json implementation of formatter drivers.
16+
type Driver struct{}
17+
18+
// Format creates a JSON document from the source config.
19+
func (d *Driver) Format(config *composetypes.Config) (string, error) {
20+
result, err := json.MarshalIndent(config, "", " ")
21+
if err != nil {
22+
return "", errors.Wrap(err, "failed to produce json structure")
23+
}
24+
return string(result) + "\n", nil
25+
}

internal/formatter/yaml/doc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package yaml

internal/formatter/yaml/driver.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package yaml
2+
3+
import (
4+
"github.com/docker/app/internal/formatter"
5+
"github.com/docker/app/internal/yaml"
6+
composetypes "github.com/docker/cli/cli/compose/types"
7+
"github.com/pkg/errors"
8+
)
9+
10+
func init() {
11+
formatter.Register("yaml", &Driver{})
12+
}
13+
14+
// Driver is the yaml implementation of formatter drivers.
15+
type Driver struct{}
16+
17+
// Format creates a YAML document from the source config.
18+
func (d *Driver) Format(config *composetypes.Config) (string, error) {
19+
result, err := yaml.Marshal(config)
20+
if err != nil {
21+
return "", errors.Wrap(err, "failed to produce yaml structure")
22+
}
23+
return string(result), nil
24+
}

render/render.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ import (
2222
_ "github.com/docker/app/internal/renderer/mustache"
2323
// Register yatee renderer
2424
_ "github.com/docker/app/internal/renderer/yatee"
25+
26+
// Register json formatter
27+
_ "github.com/docker/app/internal/formatter/json"
28+
// Register yaml formatter
29+
_ "github.com/docker/app/internal/formatter/yaml"
2530
)
2631

2732
var (

0 commit comments

Comments
 (0)