Browse code

Refactor formatter.

Signed-off-by: Daniel Nephin <dnephin@docker.com>

Daniel Nephin authored on 2016/09/13 05:59:18
Showing 14 changed files
... ...
@@ -106,27 +106,19 @@ func runPs(dockerCli *command.DockerCli, opts *psOptions) error {
106 106
 		return err
107 107
 	}
108 108
 
109
-	f := opts.format
110
-	if len(f) == 0 {
109
+	format := opts.format
110
+	if len(format) == 0 {
111 111
 		if len(dockerCli.ConfigFile().PsFormat) > 0 && !opts.quiet {
112
-			f = dockerCli.ConfigFile().PsFormat
112
+			format = dockerCli.ConfigFile().PsFormat
113 113
 		} else {
114
-			f = "table"
114
+			format = "table"
115 115
 		}
116 116
 	}
117 117
 
118
-	psCtx := formatter.ContainerContext{
119
-		Context: formatter.Context{
120
-			Output: dockerCli.Out(),
121
-			Format: f,
122
-			Quiet:  opts.quiet,
123
-			Trunc:  !opts.noTrunc,
124
-		},
125
-		Size:       listOptions.Size,
126
-		Containers: containers,
118
+	containerCtx := formatter.Context{
119
+		Output: dockerCli.Out(),
120
+		Format: formatter.NewContainerFormat(format, opts.quiet, opts.size),
121
+		Trunc:  !opts.noTrunc,
127 122
 	}
128
-
129
-	psCtx.Write()
130
-
131
-	return nil
123
+	return formatter.ContainerWrite(containerCtx, containers)
132 124
 }
... ...
@@ -1,7 +1,6 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
-	"bytes"
5 4
 	"fmt"
6 5
 	"strconv"
7 6
 	"strings"
... ...
@@ -11,7 +10,7 @@ import (
11 11
 	"github.com/docker/docker/api/types"
12 12
 	"github.com/docker/docker/pkg/stringid"
13 13
 	"github.com/docker/docker/pkg/stringutils"
14
-	"github.com/docker/go-units"
14
+	units "github.com/docker/go-units"
15 15
 )
16 16
 
17 17
 const (
... ...
@@ -26,67 +25,53 @@ const (
26 26
 	mountsHeader      = "MOUNTS"
27 27
 )
28 28
 
29
-// ContainerContext contains container specific information required by the formater, encapsulate a Context struct.
30
-type ContainerContext struct {
31
-	Context
32
-	// Size when set to true will display the size of the output.
33
-	Size bool
34
-	// Containers
35
-	Containers []types.Container
36
-}
37
-
38
-func (ctx ContainerContext) Write() {
39
-	switch ctx.Format {
40
-	case tableFormatKey:
41
-		if ctx.Quiet {
42
-			ctx.Format = defaultQuietFormat
43
-		} else {
44
-			ctx.Format = defaultContainerTableFormat
45
-			if ctx.Size {
46
-				ctx.Format += `\t{{.Size}}`
47
-			}
29
+// NewContainerFormat returns a Format for rendering using a Context
30
+func NewContainerFormat(source string, quiet bool, size bool) Format {
31
+	switch source {
32
+	case TableFormatKey:
33
+		if quiet {
34
+			return defaultQuietFormat
48 35
 		}
49
-	case rawFormatKey:
50
-		if ctx.Quiet {
51
-			ctx.Format = `container_id: {{.ID}}`
52
-		} else {
53
-			ctx.Format = `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
54
-			if ctx.Size {
55
-				ctx.Format += `size: {{.Size}}\n`
56
-			}
36
+		format := defaultContainerTableFormat
37
+		if size {
38
+			format += `\t{{.Size}}`
57 39
 		}
58
-	}
59
-
60
-	ctx.buffer = bytes.NewBufferString("")
61
-	ctx.preformat()
62
-
63
-	tmpl, err := ctx.parseFormat()
64
-	if err != nil {
65
-		return
66
-	}
67
-
68
-	for _, container := range ctx.Containers {
69
-		containerCtx := &containerContext{
70
-			trunc: ctx.Trunc,
71
-			c:     container,
40
+		return Format(format)
41
+	case RawFormatKey:
42
+		if quiet {
43
+			return `container_id: {{.ID}}`
72 44
 		}
73
-		err = ctx.contextFormat(tmpl, containerCtx)
74
-		if err != nil {
75
-			return
45
+		format := `container_id: {{.ID}}\nimage: {{.Image}}\ncommand: {{.Command}}\ncreated_at: {{.CreatedAt}}\nstatus: {{.Status}}\nnames: {{.Names}}\nlabels: {{.Labels}}\nports: {{.Ports}}\n`
46
+		if size {
47
+			format += `size: {{.Size}}\n`
76 48
 		}
49
+		return Format(format)
77 50
 	}
51
+	return Format(source)
52
+}
78 53
 
79
-	ctx.postformat(tmpl, &containerContext{})
54
+// ContainerWrite renders the context for a list of containers
55
+func ContainerWrite(ctx Context, containers []types.Container) error {
56
+	render := func(format func(subContext subContext) error) error {
57
+		for _, container := range containers {
58
+			err := format(&containerContext{trunc: ctx.Trunc, c: container})
59
+			if err != nil {
60
+				return err
61
+			}
62
+		}
63
+		return nil
64
+	}
65
+	return ctx.Write(&containerContext{}, render)
80 66
 }
81 67
 
82 68
 type containerContext struct {
83
-	baseSubContext
69
+	HeaderContext
84 70
 	trunc bool
85 71
 	c     types.Container
86 72
 }
87 73
 
88 74
 func (c *containerContext) ID() string {
89
-	c.addHeader(containerIDHeader)
75
+	c.AddHeader(containerIDHeader)
90 76
 	if c.trunc {
91 77
 		return stringid.TruncateID(c.c.ID)
92 78
 	}
... ...
@@ -94,7 +79,7 @@ func (c *containerContext) ID() string {
94 94
 }
95 95
 
96 96
 func (c *containerContext) Names() string {
97
-	c.addHeader(namesHeader)
97
+	c.AddHeader(namesHeader)
98 98
 	names := stripNamePrefix(c.c.Names)
99 99
 	if c.trunc {
100 100
 		for _, name := range names {
... ...
@@ -108,7 +93,7 @@ func (c *containerContext) Names() string {
108 108
 }
109 109
 
110 110
 func (c *containerContext) Image() string {
111
-	c.addHeader(imageHeader)
111
+	c.AddHeader(imageHeader)
112 112
 	if c.c.Image == "" {
113 113
 		return "<no image>"
114 114
 	}
... ...
@@ -121,7 +106,7 @@ func (c *containerContext) Image() string {
121 121
 }
122 122
 
123 123
 func (c *containerContext) Command() string {
124
-	c.addHeader(commandHeader)
124
+	c.AddHeader(commandHeader)
125 125
 	command := c.c.Command
126 126
 	if c.trunc {
127 127
 		command = stringutils.Ellipsis(command, 20)
... ...
@@ -130,28 +115,28 @@ func (c *containerContext) Command() string {
130 130
 }
131 131
 
132 132
 func (c *containerContext) CreatedAt() string {
133
-	c.addHeader(createdAtHeader)
133
+	c.AddHeader(createdAtHeader)
134 134
 	return time.Unix(int64(c.c.Created), 0).String()
135 135
 }
136 136
 
137 137
 func (c *containerContext) RunningFor() string {
138
-	c.addHeader(runningForHeader)
138
+	c.AddHeader(runningForHeader)
139 139
 	createdAt := time.Unix(int64(c.c.Created), 0)
140 140
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
141 141
 }
142 142
 
143 143
 func (c *containerContext) Ports() string {
144
-	c.addHeader(portsHeader)
144
+	c.AddHeader(portsHeader)
145 145
 	return api.DisplayablePorts(c.c.Ports)
146 146
 }
147 147
 
148 148
 func (c *containerContext) Status() string {
149
-	c.addHeader(statusHeader)
149
+	c.AddHeader(statusHeader)
150 150
 	return c.c.Status
151 151
 }
152 152
 
153 153
 func (c *containerContext) Size() string {
154
-	c.addHeader(sizeHeader)
154
+	c.AddHeader(sizeHeader)
155 155
 	srw := units.HumanSizeWithPrecision(float64(c.c.SizeRw), 3)
156 156
 	sv := units.HumanSizeWithPrecision(float64(c.c.SizeRootFs), 3)
157 157
 
... ...
@@ -163,7 +148,7 @@ func (c *containerContext) Size() string {
163 163
 }
164 164
 
165 165
 func (c *containerContext) Labels() string {
166
-	c.addHeader(labelsHeader)
166
+	c.AddHeader(labelsHeader)
167 167
 	if c.c.Labels == nil {
168 168
 		return ""
169 169
 	}
... ...
@@ -180,7 +165,7 @@ func (c *containerContext) Label(name string) string {
180 180
 	r := strings.NewReplacer("-", " ", "_", " ")
181 181
 	h := r.Replace(n[len(n)-1])
182 182
 
183
-	c.addHeader(h)
183
+	c.AddHeader(h)
184 184
 
185 185
 	if c.c.Labels == nil {
186 186
 		return ""
... ...
@@ -189,7 +174,7 @@ func (c *containerContext) Label(name string) string {
189 189
 }
190 190
 
191 191
 func (c *containerContext) Mounts() string {
192
-	c.addHeader(mountsHeader)
192
+	c.AddHeader(mountsHeader)
193 193
 
194 194
 	var name string
195 195
 	var mounts []string
... ...
@@ -95,7 +95,7 @@ func TestContainerPsContext(t *testing.T) {
95 95
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
96 96
 		}
97 97
 
98
-		h := ctx.fullHeader()
98
+		h := ctx.FullHeader()
99 99
 		if h != c.expHeader {
100 100
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
101 101
 		}
... ...
@@ -114,7 +114,7 @@ func TestContainerPsContext(t *testing.T) {
114 114
 		t.Fatalf("Expected ubuntu, was %s\n", node)
115 115
 	}
116 116
 
117
-	h := ctx.fullHeader()
117
+	h := ctx.FullHeader()
118 118
 	if h != "SWARM ID\tNODE NAME" {
119 119
 		t.Fatalf("Expected %s, was %s\n", "SWARM ID\tNODE NAME", h)
120 120
 
... ...
@@ -129,9 +129,9 @@ func TestContainerPsContext(t *testing.T) {
129 129
 	}
130 130
 
131 131
 	ctx = containerContext{c: c2, trunc: true}
132
-	fullHeader := ctx.fullHeader()
133
-	if fullHeader != "" {
134
-		t.Fatalf("Expected fullHeader to be empty, was %s", fullHeader)
132
+	FullHeader := ctx.FullHeader()
133
+	if FullHeader != "" {
134
+		t.Fatalf("Expected FullHeader to be empty, was %s", FullHeader)
135 135
 	}
136 136
 
137 137
 }
... ...
@@ -140,186 +140,127 @@ func TestContainerContextWrite(t *testing.T) {
140 140
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
141 141
 	expectedTime := time.Unix(unixTime, 0).String()
142 142
 
143
-	contexts := []struct {
144
-		context  ContainerContext
143
+	cases := []struct {
144
+		context  Context
145 145
 		expected string
146 146
 	}{
147 147
 		// Errors
148 148
 		{
149
-			ContainerContext{
150
-				Context: Context{
151
-					Format: "{{InvalidFunction}}",
152
-				},
153
-			},
149
+			Context{Format: "{{InvalidFunction}}"},
154 150
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
155 151
 `,
156 152
 		},
157 153
 		{
158
-			ContainerContext{
159
-				Context: Context{
160
-					Format: "{{nil}}",
161
-				},
162
-			},
154
+			Context{Format: "{{nil}}"},
163 155
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
164 156
 `,
165 157
 		},
166 158
 		// Table Format
167 159
 		{
168
-			ContainerContext{
169
-				Context: Context{
170
-					Format: "table",
171
-				},
172
-				Size: true,
173
-			},
160
+			Context{Format: NewContainerFormat("table", false, true)},
174 161
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES               SIZE
175 162
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz          0 B
176 163
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar          0 B
177 164
 `,
178 165
 		},
179 166
 		{
180
-			ContainerContext{
181
-				Context: Context{
182
-					Format: "table",
183
-				},
184
-			},
167
+			Context{Format: NewContainerFormat("table", false, false)},
185 168
 			`CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
186 169
 containerID1        ubuntu              ""                  24 hours ago                                                foobar_baz
187 170
 containerID2        ubuntu              ""                  24 hours ago                                                foobar_bar
188 171
 `,
189 172
 		},
190 173
 		{
191
-			ContainerContext{
192
-				Context: Context{
193
-					Format: "table {{.Image}}",
194
-				},
195
-			},
174
+			Context{Format: NewContainerFormat("table {{.Image}}", false, false)},
196 175
 			"IMAGE\nubuntu\nubuntu\n",
197 176
 		},
198 177
 		{
199
-			ContainerContext{
200
-				Context: Context{
201
-					Format: "table {{.Image}}",
202
-				},
203
-				Size: true,
204
-			},
178
+			Context{Format: NewContainerFormat("table {{.Image}}", false, true)},
205 179
 			"IMAGE\nubuntu\nubuntu\n",
206 180
 		},
207 181
 		{
208
-			ContainerContext{
209
-				Context: Context{
210
-					Format: "table {{.Image}}",
211
-					Quiet:  true,
212
-				},
213
-			},
182
+			Context{Format: NewContainerFormat("table {{.Image}}", true, false)},
214 183
 			"IMAGE\nubuntu\nubuntu\n",
215 184
 		},
216 185
 		{
217
-			ContainerContext{
218
-				Context: Context{
219
-					Format: "table",
220
-					Quiet:  true,
221
-				},
222
-			},
186
+			Context{Format: NewContainerFormat("table", true, false)},
223 187
 			"containerID1\ncontainerID2\n",
224 188
 		},
225 189
 		// Raw Format
226 190
 		{
227
-			ContainerContext{
228
-				Context: Context{
229
-					Format: "raw",
230
-				},
231
-			},
191
+			Context{Format: NewContainerFormat("raw", false, false)},
232 192
 			fmt.Sprintf(`container_id: containerID1
233 193
 image: ubuntu
234 194
 command: ""
235 195
 created_at: %s
236
-status: 
196
+status:
237 197
 names: foobar_baz
238
-labels: 
239
-ports: 
198
+labels:
199
+ports:
240 200
 
241 201
 container_id: containerID2
242 202
 image: ubuntu
243 203
 command: ""
244 204
 created_at: %s
245
-status: 
205
+status:
246 206
 names: foobar_bar
247
-labels: 
248
-ports: 
207
+labels:
208
+ports:
249 209
 
250 210
 `, expectedTime, expectedTime),
251 211
 		},
252 212
 		{
253
-			ContainerContext{
254
-				Context: Context{
255
-					Format: "raw",
256
-				},
257
-				Size: true,
258
-			},
213
+			Context{Format: NewContainerFormat("raw", false, true)},
259 214
 			fmt.Sprintf(`container_id: containerID1
260 215
 image: ubuntu
261 216
 command: ""
262 217
 created_at: %s
263
-status: 
218
+status:
264 219
 names: foobar_baz
265
-labels: 
266
-ports: 
220
+labels:
221
+ports:
267 222
 size: 0 B
268 223
 
269 224
 container_id: containerID2
270 225
 image: ubuntu
271 226
 command: ""
272 227
 created_at: %s
273
-status: 
228
+status:
274 229
 names: foobar_bar
275
-labels: 
276
-ports: 
230
+labels:
231
+ports:
277 232
 size: 0 B
278 233
 
279 234
 `, expectedTime, expectedTime),
280 235
 		},
281 236
 		{
282
-			ContainerContext{
283
-				Context: Context{
284
-					Format: "raw",
285
-					Quiet:  true,
286
-				},
287
-			},
237
+			Context{Format: NewContainerFormat("raw", true, false)},
288 238
 			"container_id: containerID1\ncontainer_id: containerID2\n",
289 239
 		},
290 240
 		// Custom Format
291 241
 		{
292
-			ContainerContext{
293
-				Context: Context{
294
-					Format: "{{.Image}}",
295
-				},
296
-			},
242
+			Context{Format: "{{.Image}}"},
297 243
 			"ubuntu\nubuntu\n",
298 244
 		},
299 245
 		{
300
-			ContainerContext{
301
-				Context: Context{
302
-					Format: "{{.Image}}",
303
-				},
304
-				Size: true,
305
-			},
246
+			Context{Format: NewContainerFormat("{{.Image}}", false, true)},
306 247
 			"ubuntu\nubuntu\n",
307 248
 		},
308 249
 	}
309 250
 
310
-	for _, context := range contexts {
251
+	for _, testcase := range cases {
311 252
 		containers := []types.Container{
312 253
 			{ID: "containerID1", Names: []string{"/foobar_baz"}, Image: "ubuntu", Created: unixTime},
313 254
 			{ID: "containerID2", Names: []string{"/foobar_bar"}, Image: "ubuntu", Created: unixTime},
314 255
 		}
315 256
 		out := bytes.NewBufferString("")
316
-		context.context.Output = out
317
-		context.context.Containers = containers
318
-		context.context.Write()
319
-		actual := out.String()
320
-		assert.Equal(t, actual, context.expected)
321
-		// Clean buffer
322
-		out.Reset()
257
+		testcase.context.Output = out
258
+		err := ContainerWrite(testcase.context, containers)
259
+		if err != nil {
260
+			assert.Error(t, err, testcase.expected)
261
+		} else {
262
+			assert.Equal(t, out.String(), testcase.expected)
263
+		}
323 264
 	}
324 265
 }
325 266
 
... ...
@@ -328,75 +269,56 @@ func TestContainerContextWriteWithNoContainers(t *testing.T) {
328 328
 	containers := []types.Container{}
329 329
 
330 330
 	contexts := []struct {
331
-		context  ContainerContext
331
+		context  Context
332 332
 		expected string
333 333
 	}{
334 334
 		{
335
-			ContainerContext{
336
-				Context: Context{
337
-					Format: "{{.Image}}",
338
-					Output: out,
339
-				},
335
+			Context{
336
+				Format: "{{.Image}}",
337
+				Output: out,
340 338
 			},
341 339
 			"",
342 340
 		},
343 341
 		{
344
-			ContainerContext{
345
-				Context: Context{
346
-					Format: "table {{.Image}}",
347
-					Output: out,
348
-				},
342
+			Context{
343
+				Format: "table {{.Image}}",
344
+				Output: out,
349 345
 			},
350 346
 			"IMAGE\n",
351 347
 		},
352 348
 		{
353
-			ContainerContext{
354
-				Context: Context{
355
-					Format: "{{.Image}}",
356
-					Output: out,
357
-				},
358
-				Size: true,
349
+			Context{
350
+				Format: NewContainerFormat("{{.Image}}", false, true),
351
+				Output: out,
359 352
 			},
360 353
 			"",
361 354
 		},
362 355
 		{
363
-			ContainerContext{
364
-				Context: Context{
365
-					Format: "table {{.Image}}",
366
-					Output: out,
367
-				},
368
-				Size: true,
356
+			Context{
357
+				Format: NewContainerFormat("table {{.Image}}", false, true),
358
+				Output: out,
369 359
 			},
370 360
 			"IMAGE\n",
371 361
 		},
372 362
 		{
373
-			ContainerContext{
374
-				Context: Context{
375
-					Format: "table {{.Image}}\t{{.Size}}",
376
-					Output: out,
377
-				},
363
+			Context{
364
+				Format: "table {{.Image}}\t{{.Size}}",
365
+				Output: out,
378 366
 			},
379 367
 			"IMAGE               SIZE\n",
380 368
 		},
381 369
 		{
382
-			ContainerContext{
383
-				Context: Context{
384
-					Format: "table {{.Image}}\t{{.Size}}",
385
-					Output: out,
386
-				},
387
-				Size: true,
370
+			Context{
371
+				Format: NewContainerFormat("table {{.Image}}\t{{.Size}}", false, true),
372
+				Output: out,
388 373
 			},
389 374
 			"IMAGE               SIZE\n",
390 375
 		},
391 376
 	}
392 377
 
393 378
 	for _, context := range contexts {
394
-		context.context.Containers = containers
395
-		context.context.Write()
396
-		actual := out.String()
397
-		if actual != context.expected {
398
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
399
-		}
379
+		ContainerWrite(context.context, containers)
380
+		assert.Equal(t, context.expected, out.String())
400 381
 		// Clean buffer
401 382
 		out.Reset()
402 383
 	}
... ...
@@ -5,8 +5,6 @@ import (
5 5
 )
6 6
 
7 7
 const (
8
-	tableKey = "table"
9
-
10 8
 	imageHeader        = "IMAGE"
11 9
 	createdSinceHeader = "CREATED"
12 10
 	createdAtHeader    = "CREATED AT"
... ...
@@ -18,22 +16,25 @@ const (
18 18
 )
19 19
 
20 20
 type subContext interface {
21
-	fullHeader() string
22
-	addHeader(header string)
21
+	FullHeader() string
22
+	AddHeader(header string)
23 23
 }
24 24
 
25
-type baseSubContext struct {
25
+// HeaderContext provides the subContext interface for managing headers
26
+type HeaderContext struct {
26 27
 	header []string
27 28
 }
28 29
 
29
-func (c *baseSubContext) fullHeader() string {
30
+// FullHeader returns the header as a string
31
+func (c *HeaderContext) FullHeader() string {
30 32
 	if c.header == nil {
31 33
 		return ""
32 34
 	}
33 35
 	return strings.Join(c.header, "\t")
34 36
 }
35 37
 
36
-func (c *baseSubContext) addHeader(header string) {
38
+// AddHeader adds another column to the header
39
+func (c *HeaderContext) AddHeader(header string) {
37 40
 	if c.header == nil {
38 41
 		c.header = []string{}
39 42
 	}
... ...
@@ -12,36 +12,48 @@ import (
12 12
 )
13 13
 
14 14
 const (
15
-	tableFormatKey = "table"
16
-	rawFormatKey   = "raw"
15
+	// TableFormatKey is the key used to format as a table
16
+	TableFormatKey = "table"
17
+	// RawFormatKey is the key used to format as raw JSON
18
+	RawFormatKey = "raw"
17 19
 
18 20
 	defaultQuietFormat = "{{.ID}}"
19 21
 )
20 22
 
23
+// Format is the format string rendered using the Context
24
+type Format string
25
+
26
+// IsTable returns true if the format is a table-type format
27
+func (f Format) IsTable() bool {
28
+	return strings.HasPrefix(string(f), TableFormatKey)
29
+}
30
+
31
+// Contains returns true if the format contains the substring
32
+func (f Format) Contains(sub string) bool {
33
+	return strings.Contains(string(f), sub)
34
+}
35
+
21 36
 // Context contains information required by the formatter to print the output as desired.
22 37
 type Context struct {
23 38
 	// Output is the output stream to which the formatted string is written.
24 39
 	Output io.Writer
25 40
 	// Format is used to choose raw, table or custom format for the output.
26
-	Format string
27
-	// Quiet when set to true will simply print minimal information.
28
-	Quiet bool
41
+	Format Format
29 42
 	// Trunc when set to true will truncate the output of certain fields such as Container ID.
30 43
 	Trunc bool
31 44
 
32 45
 	// internal element
33
-	table       bool
34 46
 	finalFormat string
35 47
 	header      string
36 48
 	buffer      *bytes.Buffer
37 49
 }
38 50
 
39
-func (c *Context) preformat() {
40
-	c.finalFormat = c.Format
51
+func (c *Context) preFormat() {
52
+	c.finalFormat = string(c.Format)
41 53
 
42
-	if strings.HasPrefix(c.Format, tableKey) {
43
-		c.table = true
44
-		c.finalFormat = c.finalFormat[len(tableKey):]
54
+	// TODO: handle this in the Format type
55
+	if c.Format.IsTable() {
56
+		c.finalFormat = c.finalFormat[len(TableFormatKey):]
45 57
 	}
46 58
 
47 59
 	c.finalFormat = strings.Trim(c.finalFormat, " ")
... ...
@@ -52,18 +64,17 @@ func (c *Context) preformat() {
52 52
 func (c *Context) parseFormat() (*template.Template, error) {
53 53
 	tmpl, err := templates.Parse(c.finalFormat)
54 54
 	if err != nil {
55
-		c.buffer.WriteString(fmt.Sprintf("Template parsing error: %v\n", err))
56
-		c.buffer.WriteTo(c.Output)
55
+		return tmpl, fmt.Errorf("Template parsing error: %v\n", err)
57 56
 	}
58 57
 	return tmpl, err
59 58
 }
60 59
 
61
-func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
62
-	if c.table {
60
+func (c *Context) postFormat(tmpl *template.Template, subContext subContext) {
61
+	if c.Format.IsTable() {
63 62
 		if len(c.header) == 0 {
64 63
 			// if we still don't have a header, we didn't have any containers so we need to fake it to get the right headers from the template
65 64
 			tmpl.Execute(bytes.NewBufferString(""), subContext)
66
-			c.header = subContext.fullHeader()
65
+			c.header = subContext.FullHeader()
67 66
 		}
68 67
 
69 68
 		t := tabwriter.NewWriter(c.Output, 20, 1, 3, ' ', 0)
... ...
@@ -78,13 +89,35 @@ func (c *Context) postformat(tmpl *template.Template, subContext subContext) {
78 78
 
79 79
 func (c *Context) contextFormat(tmpl *template.Template, subContext subContext) error {
80 80
 	if err := tmpl.Execute(c.buffer, subContext); err != nil {
81
-		c.buffer = bytes.NewBufferString(fmt.Sprintf("Template parsing error: %v\n", err))
82
-		c.buffer.WriteTo(c.Output)
83
-		return err
81
+		return fmt.Errorf("Template parsing error: %v\n", err)
84 82
 	}
85
-	if c.table && len(c.header) == 0 {
86
-		c.header = subContext.fullHeader()
83
+	if c.Format.IsTable() && len(c.header) == 0 {
84
+		c.header = subContext.FullHeader()
87 85
 	}
88 86
 	c.buffer.WriteString("\n")
89 87
 	return nil
90 88
 }
89
+
90
+// SubFormat is a function type accepted by Write()
91
+type SubFormat func(func(subContext) error) error
92
+
93
+// Write the template to the buffer using this Context
94
+func (c *Context) Write(sub subContext, f SubFormat) error {
95
+	c.buffer = bytes.NewBufferString("")
96
+	c.preFormat()
97
+
98
+	tmpl, err := c.parseFormat()
99
+	if err != nil {
100
+		return err
101
+	}
102
+
103
+	subFormat := func(subContext subContext) error {
104
+		return c.contextFormat(tmpl, subContext)
105
+	}
106
+	if err := f(subFormat); err != nil {
107
+		return err
108
+	}
109
+
110
+	c.postFormat(tmpl, sub)
111
+	return nil
112
+}
... ...
@@ -1,14 +1,12 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
-	"bytes"
5
-	"strings"
6 4
 	"time"
7 5
 
8 6
 	"github.com/docker/docker/api/types"
9 7
 	"github.com/docker/docker/pkg/stringid"
10 8
 	"github.com/docker/docker/reference"
11
-	"github.com/docker/go-units"
9
+	units "github.com/docker/go-units"
12 10
 )
13 11
 
14 12
 const (
... ...
@@ -25,59 +23,63 @@ const (
25 25
 type ImageContext struct {
26 26
 	Context
27 27
 	Digest bool
28
-	// Images
29
-	Images []types.Image
30 28
 }
31 29
 
32 30
 func isDangling(image types.Image) bool {
33 31
 	return len(image.RepoTags) == 1 && image.RepoTags[0] == "<none>:<none>" && len(image.RepoDigests) == 1 && image.RepoDigests[0] == "<none>@<none>"
34 32
 }
35 33
 
36
-func (ctx ImageContext) Write() {
37
-	switch ctx.Format {
38
-	case tableFormatKey:
39
-		ctx.Format = defaultImageTableFormat
40
-		if ctx.Digest {
41
-			ctx.Format = defaultImageTableFormatWithDigest
34
+// NewImageFormat returns a format for rendering an ImageContext
35
+func NewImageFormat(source string, quiet bool, digest bool) Format {
36
+	switch source {
37
+	case TableFormatKey:
38
+		switch {
39
+		case quiet:
40
+			return defaultQuietFormat
41
+		case digest:
42
+			return defaultImageTableFormatWithDigest
43
+		default:
44
+			return defaultImageTableFormat
42 45
 		}
43
-		if ctx.Quiet {
44
-			ctx.Format = defaultQuietFormat
45
-		}
46
-	case rawFormatKey:
47
-		if ctx.Quiet {
48
-			ctx.Format = `image_id: {{.ID}}`
49
-		} else {
50
-			if ctx.Digest {
51
-				ctx.Format = `repository: {{ .Repository }}
46
+	case RawFormatKey:
47
+		switch {
48
+		case quiet:
49
+			return `image_id: {{.ID}}`
50
+		case digest:
51
+			return `repository: {{ .Repository }}
52 52
 tag: {{.Tag}}
53 53
 digest: {{.Digest}}
54 54
 image_id: {{.ID}}
55 55
 created_at: {{.CreatedAt}}
56 56
 virtual_size: {{.Size}}
57 57
 `
58
-			} else {
59
-				ctx.Format = `repository: {{ .Repository }}
58
+		default:
59
+			return `repository: {{ .Repository }}
60 60
 tag: {{.Tag}}
61 61
 image_id: {{.ID}}
62 62
 created_at: {{.CreatedAt}}
63 63
 virtual_size: {{.Size}}
64 64
 `
65
-			}
66 65
 		}
67 66
 	}
68 67
 
69
-	ctx.buffer = bytes.NewBufferString("")
70
-	ctx.preformat()
71
-	if ctx.table && ctx.Digest && !strings.Contains(ctx.Format, "{{.Digest}}") {
72
-		ctx.finalFormat += "\t{{.Digest}}"
68
+	format := Format(source)
69
+	if format.IsTable() && digest && !format.Contains("{{.Digest}}") {
70
+		format += "\t{{.Digest}}"
73 71
 	}
72
+	return format
73
+}
74 74
 
75
-	tmpl, err := ctx.parseFormat()
76
-	if err != nil {
77
-		return
75
+// ImageWrite writes the formatter images using the ImageContext
76
+func ImageWrite(ctx ImageContext, images []types.Image) error {
77
+	render := func(format func(subContext subContext) error) error {
78
+		return imageFormat(ctx, images, format)
78 79
 	}
80
+	return ctx.Write(&imageContext{}, render)
81
+}
79 82
 
80
-	for _, image := range ctx.Images {
83
+func imageFormat(ctx ImageContext, images []types.Image, format func(subContext subContext) error) error {
84
+	for _, image := range images {
81 85
 		images := []*imageContext{}
82 86
 		if isDangling(image) {
83 87
 			images = append(images, &imageContext{
... ...
@@ -170,18 +172,16 @@ virtual_size: {{.Size}}
170 170
 			}
171 171
 		}
172 172
 		for _, imageCtx := range images {
173
-			err = ctx.contextFormat(tmpl, imageCtx)
174
-			if err != nil {
175
-				return
173
+			if err := format(imageCtx); err != nil {
174
+				return err
176 175
 			}
177 176
 		}
178 177
 	}
179
-
180
-	ctx.postformat(tmpl, &imageContext{})
178
+	return nil
181 179
 }
182 180
 
183 181
 type imageContext struct {
184
-	baseSubContext
182
+	HeaderContext
185 183
 	trunc  bool
186 184
 	i      types.Image
187 185
 	repo   string
... ...
@@ -190,7 +190,7 @@ type imageContext struct {
190 190
 }
191 191
 
192 192
 func (c *imageContext) ID() string {
193
-	c.addHeader(imageIDHeader)
193
+	c.AddHeader(imageIDHeader)
194 194
 	if c.trunc {
195 195
 		return stringid.TruncateID(c.i.ID)
196 196
 	}
... ...
@@ -198,32 +198,32 @@ func (c *imageContext) ID() string {
198 198
 }
199 199
 
200 200
 func (c *imageContext) Repository() string {
201
-	c.addHeader(repositoryHeader)
201
+	c.AddHeader(repositoryHeader)
202 202
 	return c.repo
203 203
 }
204 204
 
205 205
 func (c *imageContext) Tag() string {
206
-	c.addHeader(tagHeader)
206
+	c.AddHeader(tagHeader)
207 207
 	return c.tag
208 208
 }
209 209
 
210 210
 func (c *imageContext) Digest() string {
211
-	c.addHeader(digestHeader)
211
+	c.AddHeader(digestHeader)
212 212
 	return c.digest
213 213
 }
214 214
 
215 215
 func (c *imageContext) CreatedSince() string {
216
-	c.addHeader(createdSinceHeader)
216
+	c.AddHeader(createdSinceHeader)
217 217
 	createdAt := time.Unix(int64(c.i.Created), 0)
218 218
 	return units.HumanDuration(time.Now().UTC().Sub(createdAt))
219 219
 }
220 220
 
221 221
 func (c *imageContext) CreatedAt() string {
222
-	c.addHeader(createdAtHeader)
222
+	c.AddHeader(createdAtHeader)
223 223
 	return time.Unix(int64(c.i.Created), 0).String()
224 224
 }
225 225
 
226 226
 func (c *imageContext) Size() string {
227
-	c.addHeader(sizeHeader)
227
+	c.AddHeader(sizeHeader)
228 228
 	return units.HumanSizeWithPrecision(float64(c.i.Size), 3)
229 229
 }
... ...
@@ -9,6 +9,7 @@ import (
9 9
 
10 10
 	"github.com/docker/docker/api/types"
11 11
 	"github.com/docker/docker/pkg/stringid"
12
+	"github.com/docker/docker/pkg/testutil/assert"
12 13
 )
13 14
 
14 15
 func TestImageContext(t *testing.T) {
... ...
@@ -66,7 +67,7 @@ func TestImageContext(t *testing.T) {
66 66
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
67 67
 		}
68 68
 
69
-		h := ctx.fullHeader()
69
+		h := ctx.FullHeader()
70 70
 		if h != c.expHeader {
71 71
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
72 72
 		}
... ...
@@ -77,7 +78,7 @@ func TestImageContextWrite(t *testing.T) {
77 77
 	unixTime := time.Now().AddDate(0, 0, -1).Unix()
78 78
 	expectedTime := time.Unix(unixTime, 0).String()
79 79
 
80
-	contexts := []struct {
80
+	cases := []struct {
81 81
 		context  ImageContext
82 82
 		expected string
83 83
 	}{
... ...
@@ -104,7 +105,7 @@ func TestImageContextWrite(t *testing.T) {
104 104
 		{
105 105
 			ImageContext{
106 106
 				Context: Context{
107
-					Format: "table",
107
+					Format: NewImageFormat("table", false, false),
108 108
 				},
109 109
 			},
110 110
 			`REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
... ...
@@ -116,7 +117,7 @@ image               tag2                imageID2            24 hours ago
116 116
 		{
117 117
 			ImageContext{
118 118
 				Context: Context{
119
-					Format: "table {{.Repository}}",
119
+					Format: NewImageFormat("table {{.Repository}}", false, false),
120 120
 				},
121 121
 			},
122 122
 			"REPOSITORY\nimage\nimage\n<none>\n",
... ...
@@ -124,7 +125,7 @@ image               tag2                imageID2            24 hours ago
124 124
 		{
125 125
 			ImageContext{
126 126
 				Context: Context{
127
-					Format: "table {{.Repository}}",
127
+					Format: NewImageFormat("table {{.Repository}}", false, true),
128 128
 				},
129 129
 				Digest: true,
130 130
 			},
... ...
@@ -137,8 +138,7 @@ image               <none>
137 137
 		{
138 138
 			ImageContext{
139 139
 				Context: Context{
140
-					Format: "table {{.Repository}}",
141
-					Quiet:  true,
140
+					Format: NewImageFormat("table {{.Repository}}", true, false),
142 141
 				},
143 142
 			},
144 143
 			"REPOSITORY\nimage\nimage\n<none>\n",
... ...
@@ -146,8 +146,7 @@ image               <none>
146 146
 		{
147 147
 			ImageContext{
148 148
 				Context: Context{
149
-					Format: "table",
150
-					Quiet:  true,
149
+					Format: NewImageFormat("table", true, false),
151 150
 				},
152 151
 			},
153 152
 			"imageID1\nimageID2\nimageID3\n",
... ...
@@ -155,8 +154,7 @@ image               <none>
155 155
 		{
156 156
 			ImageContext{
157 157
 				Context: Context{
158
-					Format: "table",
159
-					Quiet:  false,
158
+					Format: NewImageFormat("table", false, true),
160 159
 				},
161 160
 				Digest: true,
162 161
 			},
... ...
@@ -169,8 +167,7 @@ image               tag2                <none>
169 169
 		{
170 170
 			ImageContext{
171 171
 				Context: Context{
172
-					Format: "table",
173
-					Quiet:  true,
172
+					Format: NewImageFormat("table", true, true),
174 173
 				},
175 174
 				Digest: true,
176 175
 			},
... ...
@@ -180,7 +177,7 @@ image               tag2                <none>
180 180
 		{
181 181
 			ImageContext{
182 182
 				Context: Context{
183
-					Format: "raw",
183
+					Format: NewImageFormat("raw", false, false),
184 184
 				},
185 185
 			},
186 186
 			fmt.Sprintf(`repository: image
... ...
@@ -206,7 +203,7 @@ virtual_size: 0 B
206 206
 		{
207 207
 			ImageContext{
208 208
 				Context: Context{
209
-					Format: "raw",
209
+					Format: NewImageFormat("raw", false, true),
210 210
 				},
211 211
 				Digest: true,
212 212
 			},
... ...
@@ -236,8 +233,7 @@ virtual_size: 0 B
236 236
 		{
237 237
 			ImageContext{
238 238
 				Context: Context{
239
-					Format: "raw",
240
-					Quiet:  true,
239
+					Format: NewImageFormat("raw", true, false),
241 240
 				},
242 241
 			},
243 242
 			`image_id: imageID1
... ...
@@ -249,7 +245,7 @@ image_id: imageID3
249 249
 		{
250 250
 			ImageContext{
251 251
 				Context: Context{
252
-					Format: "{{.Repository}}",
252
+					Format: NewImageFormat("{{.Repository}}", false, false),
253 253
 				},
254 254
 			},
255 255
 			"image\nimage\n<none>\n",
... ...
@@ -257,7 +253,7 @@ image_id: imageID3
257 257
 		{
258 258
 			ImageContext{
259 259
 				Context: Context{
260
-					Format: "{{.Repository}}",
260
+					Format: NewImageFormat("{{.Repository}}", false, true),
261 261
 				},
262 262
 				Digest: true,
263 263
 			},
... ...
@@ -265,22 +261,20 @@ image_id: imageID3
265 265
 		},
266 266
 	}
267 267
 
268
-	for _, context := range contexts {
268
+	for _, testcase := range cases {
269 269
 		images := []types.Image{
270 270
 			{ID: "imageID1", RepoTags: []string{"image:tag1"}, RepoDigests: []string{"image@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf"}, Created: unixTime},
271 271
 			{ID: "imageID2", RepoTags: []string{"image:tag2"}, Created: unixTime},
272 272
 			{ID: "imageID3", RepoTags: []string{"<none>:<none>"}, RepoDigests: []string{"<none>@<none>"}, Created: unixTime},
273 273
 		}
274 274
 		out := bytes.NewBufferString("")
275
-		context.context.Output = out
276
-		context.context.Images = images
277
-		context.context.Write()
278
-		actual := out.String()
279
-		if actual != context.expected {
280
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
275
+		testcase.context.Output = out
276
+		err := ImageWrite(testcase.context, images)
277
+		if err != nil {
278
+			assert.Error(t, err, testcase.expected)
279
+		} else {
280
+			assert.Equal(t, out.String(), testcase.expected)
281 281
 		}
282
-		// Clean buffer
283
-		out.Reset()
284 282
 	}
285 283
 }
286 284
 
... ...
@@ -295,7 +289,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
295 295
 		{
296 296
 			ImageContext{
297 297
 				Context: Context{
298
-					Format: "{{.Repository}}",
298
+					Format: NewImageFormat("{{.Repository}}", false, false),
299 299
 					Output: out,
300 300
 				},
301 301
 			},
... ...
@@ -304,7 +298,7 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
304 304
 		{
305 305
 			ImageContext{
306 306
 				Context: Context{
307
-					Format: "table {{.Repository}}",
307
+					Format: NewImageFormat("table {{.Repository}}", false, false),
308 308
 					Output: out,
309 309
 				},
310 310
 			},
... ...
@@ -313,32 +307,26 @@ func TestImageContextWriteWithNoImage(t *testing.T) {
313 313
 		{
314 314
 			ImageContext{
315 315
 				Context: Context{
316
-					Format: "{{.Repository}}",
316
+					Format: NewImageFormat("{{.Repository}}", false, true),
317 317
 					Output: out,
318 318
 				},
319
-				Digest: true,
320 319
 			},
321 320
 			"",
322 321
 		},
323 322
 		{
324 323
 			ImageContext{
325 324
 				Context: Context{
326
-					Format: "table {{.Repository}}",
325
+					Format: NewImageFormat("table {{.Repository}}", false, true),
327 326
 					Output: out,
328 327
 				},
329
-				Digest: true,
330 328
 			},
331 329
 			"REPOSITORY          DIGEST\n",
332 330
 		},
333 331
 	}
334 332
 
335 333
 	for _, context := range contexts {
336
-		context.context.Images = images
337
-		context.context.Write()
338
-		actual := out.String()
339
-		if actual != context.expected {
340
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
341
-		}
334
+		ImageWrite(context.context, images)
335
+		assert.Equal(t, out.String(), context.expected)
342 336
 		// Clean buffer
343 337
 		out.Reset()
344 338
 	}
... ...
@@ -1,7 +1,6 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
-	"bytes"
5 4
 	"fmt"
6 5
 	"strings"
7 6
 
... ...
@@ -17,60 +16,45 @@ const (
17 17
 	internalHeader  = "INTERNAL"
18 18
 )
19 19
 
20
-// NetworkContext contains network specific information required by the formatter,
21
-// encapsulate a Context struct.
22
-type NetworkContext struct {
23
-	Context
24
-	// Networks
25
-	Networks []types.NetworkResource
26
-}
27
-
28
-func (ctx NetworkContext) Write() {
29
-	switch ctx.Format {
30
-	case tableFormatKey:
31
-		if ctx.Quiet {
32
-			ctx.Format = defaultQuietFormat
33
-		} else {
34
-			ctx.Format = defaultNetworkTableFormat
20
+// NewNetworkFormat returns a Format for rendering using a network Context
21
+func NewNetworkFormat(source string, quiet bool) Format {
22
+	switch source {
23
+	case TableFormatKey:
24
+		if quiet {
25
+			return defaultQuietFormat
35 26
 		}
36
-	case rawFormatKey:
37
-		if ctx.Quiet {
38
-			ctx.Format = `network_id: {{.ID}}`
39
-		} else {
40
-			ctx.Format = `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
27
+		return defaultNetworkTableFormat
28
+	case RawFormatKey:
29
+		if quiet {
30
+			return `network_id: {{.ID}}`
41 31
 		}
32
+		return `network_id: {{.ID}}\nname: {{.Name}}\ndriver: {{.Driver}}\nscope: {{.Scope}}\n`
42 33
 	}
34
+	return Format(source)
35
+}
43 36
 
44
-	ctx.buffer = bytes.NewBufferString("")
45
-	ctx.preformat()
46
-
47
-	tmpl, err := ctx.parseFormat()
48
-	if err != nil {
49
-		return
50
-	}
51
-
52
-	for _, network := range ctx.Networks {
53
-		networkCtx := &networkContext{
54
-			trunc: ctx.Trunc,
55
-			n:     network,
56
-		}
57
-		err = ctx.contextFormat(tmpl, networkCtx)
58
-		if err != nil {
59
-			return
37
+// NetworkWrite writes the context
38
+func NetworkWrite(ctx Context, networks []types.NetworkResource) error {
39
+	render := func(format func(subContext subContext) error) error {
40
+		for _, network := range networks {
41
+			networkCtx := &networkContext{trunc: ctx.Trunc, n: network}
42
+			if err := format(networkCtx); err != nil {
43
+				return err
44
+			}
60 45
 		}
46
+		return nil
61 47
 	}
62
-
63
-	ctx.postformat(tmpl, &networkContext{})
48
+	return ctx.Write(&networkContext{}, render)
64 49
 }
65 50
 
66 51
 type networkContext struct {
67
-	baseSubContext
52
+	HeaderContext
68 53
 	trunc bool
69 54
 	n     types.NetworkResource
70 55
 }
71 56
 
72 57
 func (c *networkContext) ID() string {
73
-	c.addHeader(networkIDHeader)
58
+	c.AddHeader(networkIDHeader)
74 59
 	if c.trunc {
75 60
 		return stringid.TruncateID(c.n.ID)
76 61
 	}
... ...
@@ -78,32 +62,32 @@ func (c *networkContext) ID() string {
78 78
 }
79 79
 
80 80
 func (c *networkContext) Name() string {
81
-	c.addHeader(nameHeader)
81
+	c.AddHeader(nameHeader)
82 82
 	return c.n.Name
83 83
 }
84 84
 
85 85
 func (c *networkContext) Driver() string {
86
-	c.addHeader(driverHeader)
86
+	c.AddHeader(driverHeader)
87 87
 	return c.n.Driver
88 88
 }
89 89
 
90 90
 func (c *networkContext) Scope() string {
91
-	c.addHeader(scopeHeader)
91
+	c.AddHeader(scopeHeader)
92 92
 	return c.n.Scope
93 93
 }
94 94
 
95 95
 func (c *networkContext) IPv6() string {
96
-	c.addHeader(ipv6Header)
96
+	c.AddHeader(ipv6Header)
97 97
 	return fmt.Sprintf("%v", c.n.EnableIPv6)
98 98
 }
99 99
 
100 100
 func (c *networkContext) Internal() string {
101
-	c.addHeader(internalHeader)
101
+	c.AddHeader(internalHeader)
102 102
 	return fmt.Sprintf("%v", c.n.Internal)
103 103
 }
104 104
 
105 105
 func (c *networkContext) Labels() string {
106
-	c.addHeader(labelsHeader)
106
+	c.AddHeader(labelsHeader)
107 107
 	if c.n.Labels == nil {
108 108
 		return ""
109 109
 	}
... ...
@@ -120,7 +104,7 @@ func (c *networkContext) Label(name string) string {
120 120
 	r := strings.NewReplacer("-", " ", "_", " ")
121 121
 	h := r.Replace(n[len(n)-1])
122 122
 
123
-	c.addHeader(h)
123
+	c.AddHeader(h)
124 124
 
125 125
 	if c.n.Labels == nil {
126 126
 		return ""
... ...
@@ -7,6 +7,7 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/api/types"
9 9
 	"github.com/docker/docker/pkg/stringid"
10
+	"github.com/docker/docker/pkg/testutil/assert"
10 11
 )
11 12
 
12 13
 func TestNetworkContext(t *testing.T) {
... ...
@@ -62,7 +63,7 @@ func TestNetworkContext(t *testing.T) {
62 62
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
63 63
 		}
64 64
 
65
-		h := ctx.fullHeader()
65
+		h := ctx.FullHeader()
66 66
 		if h != c.expHeader {
67 67
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
68 68
 		}
... ...
@@ -70,71 +71,45 @@ func TestNetworkContext(t *testing.T) {
70 70
 }
71 71
 
72 72
 func TestNetworkContextWrite(t *testing.T) {
73
-	contexts := []struct {
74
-		context  NetworkContext
73
+	cases := []struct {
74
+		context  Context
75 75
 		expected string
76 76
 	}{
77 77
 
78 78
 		// Errors
79 79
 		{
80
-			NetworkContext{
81
-				Context: Context{
82
-					Format: "{{InvalidFunction}}",
83
-				},
84
-			},
80
+			Context{Format: "{{InvalidFunction}}"},
85 81
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
86 82
 `,
87 83
 		},
88 84
 		{
89
-			NetworkContext{
90
-				Context: Context{
91
-					Format: "{{nil}}",
92
-				},
93
-			},
85
+			Context{Format: "{{nil}}"},
94 86
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
95 87
 `,
96 88
 		},
97 89
 		// Table format
98 90
 		{
99
-			NetworkContext{
100
-				Context: Context{
101
-					Format: "table",
102
-				},
103
-			},
91
+			Context{Format: NewNetworkFormat("table", false)},
104 92
 			`NETWORK ID          NAME                DRIVER              SCOPE
105 93
 networkID1          foobar_baz          foo                 local
106 94
 networkID2          foobar_bar          bar                 local
107 95
 `,
108 96
 		},
109 97
 		{
110
-			NetworkContext{
111
-				Context: Context{
112
-					Format: "table",
113
-					Quiet:  true,
114
-				},
115
-			},
98
+			Context{Format: NewNetworkFormat("table", true)},
116 99
 			`networkID1
117 100
 networkID2
118 101
 `,
119 102
 		},
120 103
 		{
121
-			NetworkContext{
122
-				Context: Context{
123
-					Format: "table {{.Name}}",
124
-				},
125
-			},
104
+			Context{Format: NewNetworkFormat("table {{.Name}}", false)},
126 105
 			`NAME
127 106
 foobar_baz
128 107
 foobar_bar
129 108
 `,
130 109
 		},
131 110
 		{
132
-			NetworkContext{
133
-				Context: Context{
134
-					Format: "table {{.Name}}",
135
-					Quiet:  true,
136
-				},
137
-			},
111
+			Context{Format: NewNetworkFormat("table {{.Name}}", true)},
138 112
 			`NAME
139 113
 foobar_baz
140 114
 foobar_bar
... ...
@@ -142,11 +117,8 @@ foobar_bar
142 142
 		},
143 143
 		// Raw Format
144 144
 		{
145
-			NetworkContext{
146
-				Context: Context{
147
-					Format: "raw",
148
-				},
149
-			}, `network_id: networkID1
145
+			Context{Format: NewNetworkFormat("raw", false)},
146
+			`network_id: networkID1
150 147
 name: foobar_baz
151 148
 driver: foo
152 149
 scope: local
... ...
@@ -159,43 +131,32 @@ scope: local
159 159
 `,
160 160
 		},
161 161
 		{
162
-			NetworkContext{
163
-				Context: Context{
164
-					Format: "raw",
165
-					Quiet:  true,
166
-				},
167
-			},
162
+			Context{Format: NewNetworkFormat("raw", true)},
168 163
 			`network_id: networkID1
169 164
 network_id: networkID2
170 165
 `,
171 166
 		},
172 167
 		// Custom Format
173 168
 		{
174
-			NetworkContext{
175
-				Context: Context{
176
-					Format: "{{.Name}}",
177
-				},
178
-			},
169
+			Context{Format: NewNetworkFormat("{{.Name}}", false)},
179 170
 			`foobar_baz
180 171
 foobar_bar
181 172
 `,
182 173
 		},
183 174
 	}
184 175
 
185
-	for _, context := range contexts {
176
+	for _, testcase := range cases {
186 177
 		networks := []types.NetworkResource{
187 178
 			{ID: "networkID1", Name: "foobar_baz", Driver: "foo", Scope: "local"},
188 179
 			{ID: "networkID2", Name: "foobar_bar", Driver: "bar", Scope: "local"},
189 180
 		}
190 181
 		out := bytes.NewBufferString("")
191
-		context.context.Output = out
192
-		context.context.Networks = networks
193
-		context.context.Write()
194
-		actual := out.String()
195
-		if actual != context.expected {
196
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
182
+		testcase.context.Output = out
183
+		err := NetworkWrite(testcase.context, networks)
184
+		if err != nil {
185
+			assert.Error(t, err, testcase.expected)
186
+		} else {
187
+			assert.Equal(t, out.String(), testcase.expected)
197 188
 		}
198
-		// Clean buffer
199
-		out.Reset()
200 189
 	}
201 190
 }
... ...
@@ -1,7 +1,6 @@
1 1
 package formatter
2 2
 
3 3
 import (
4
-	"bytes"
5 4
 	"fmt"
6 5
 	"strings"
7 6
 
... ...
@@ -16,78 +15,63 @@ const (
16 16
 	// Status header ?
17 17
 )
18 18
 
19
-// VolumeContext contains volume specific information required by the formatter,
20
-// encapsulate a Context struct.
21
-type VolumeContext struct {
22
-	Context
23
-	// Volumes
24
-	Volumes []*types.Volume
25
-}
26
-
27
-func (ctx VolumeContext) Write() {
28
-	switch ctx.Format {
29
-	case tableFormatKey:
30
-		if ctx.Quiet {
31
-			ctx.Format = defaultVolumeQuietFormat
32
-		} else {
33
-			ctx.Format = defaultVolumeTableFormat
19
+// NewVolumeFormat returns a format for use with a volume Context
20
+func NewVolumeFormat(source string, quiet bool) Format {
21
+	switch source {
22
+	case TableFormatKey:
23
+		if quiet {
24
+			return defaultVolumeQuietFormat
34 25
 		}
35
-	case rawFormatKey:
36
-		if ctx.Quiet {
37
-			ctx.Format = `name: {{.Name}}`
38
-		} else {
39
-			ctx.Format = `name: {{.Name}}\ndriver: {{.Driver}}\n`
26
+		return defaultVolumeTableFormat
27
+	case RawFormatKey:
28
+		if quiet {
29
+			return `name: {{.Name}}`
40 30
 		}
31
+		return `name: {{.Name}}\ndriver: {{.Driver}}\n`
41 32
 	}
33
+	return Format(source)
34
+}
42 35
 
43
-	ctx.buffer = bytes.NewBufferString("")
44
-	ctx.preformat()
45
-
46
-	tmpl, err := ctx.parseFormat()
47
-	if err != nil {
48
-		return
49
-	}
50
-
51
-	for _, volume := range ctx.Volumes {
52
-		volumeCtx := &volumeContext{
53
-			v: volume,
54
-		}
55
-		err = ctx.contextFormat(tmpl, volumeCtx)
56
-		if err != nil {
57
-			return
36
+// VolumeWrite writes formatted volumes using the Context
37
+func VolumeWrite(ctx Context, volumes []*types.Volume) error {
38
+	render := func(format func(subContext subContext) error) error {
39
+		for _, volume := range volumes {
40
+			if err := format(&volumeContext{v: volume}); err != nil {
41
+				return err
42
+			}
58 43
 		}
44
+		return nil
59 45
 	}
60
-
61
-	ctx.postformat(tmpl, &networkContext{})
46
+	return ctx.Write(&volumeContext{}, render)
62 47
 }
63 48
 
64 49
 type volumeContext struct {
65
-	baseSubContext
50
+	HeaderContext
66 51
 	v *types.Volume
67 52
 }
68 53
 
69 54
 func (c *volumeContext) Name() string {
70
-	c.addHeader(nameHeader)
55
+	c.AddHeader(nameHeader)
71 56
 	return c.v.Name
72 57
 }
73 58
 
74 59
 func (c *volumeContext) Driver() string {
75
-	c.addHeader(driverHeader)
60
+	c.AddHeader(driverHeader)
76 61
 	return c.v.Driver
77 62
 }
78 63
 
79 64
 func (c *volumeContext) Scope() string {
80
-	c.addHeader(scopeHeader)
65
+	c.AddHeader(scopeHeader)
81 66
 	return c.v.Scope
82 67
 }
83 68
 
84 69
 func (c *volumeContext) Mountpoint() string {
85
-	c.addHeader(mountpointHeader)
70
+	c.AddHeader(mountpointHeader)
86 71
 	return c.v.Mountpoint
87 72
 }
88 73
 
89 74
 func (c *volumeContext) Labels() string {
90
-	c.addHeader(labelsHeader)
75
+	c.AddHeader(labelsHeader)
91 76
 	if c.v.Labels == nil {
92 77
 		return ""
93 78
 	}
... ...
@@ -105,7 +89,7 @@ func (c *volumeContext) Label(name string) string {
105 105
 	r := strings.NewReplacer("-", " ", "_", " ")
106 106
 	h := r.Replace(n[len(n)-1])
107 107
 
108
-	c.addHeader(h)
108
+	c.AddHeader(h)
109 109
 
110 110
 	if c.v.Labels == nil {
111 111
 		return ""
... ...
@@ -7,6 +7,7 @@ import (
7 7
 
8 8
 	"github.com/docker/docker/api/types"
9 9
 	"github.com/docker/docker/pkg/stringid"
10
+	"github.com/docker/docker/pkg/testutil/assert"
10 11
 )
11 12
 
12 13
 func TestVolumeContext(t *testing.T) {
... ...
@@ -48,7 +49,7 @@ func TestVolumeContext(t *testing.T) {
48 48
 			t.Fatalf("Expected %s, was %s\n", c.expValue, v)
49 49
 		}
50 50
 
51
-		h := ctx.fullHeader()
51
+		h := ctx.FullHeader()
52 52
 		if h != c.expHeader {
53 53
 			t.Fatalf("Expected %s, was %s\n", c.expHeader, h)
54 54
 		}
... ...
@@ -56,71 +57,45 @@ func TestVolumeContext(t *testing.T) {
56 56
 }
57 57
 
58 58
 func TestVolumeContextWrite(t *testing.T) {
59
-	contexts := []struct {
60
-		context  VolumeContext
59
+	cases := []struct {
60
+		context  Context
61 61
 		expected string
62 62
 	}{
63 63
 
64 64
 		// Errors
65 65
 		{
66
-			VolumeContext{
67
-				Context: Context{
68
-					Format: "{{InvalidFunction}}",
69
-				},
70
-			},
66
+			Context{Format: "{{InvalidFunction}}"},
71 67
 			`Template parsing error: template: :1: function "InvalidFunction" not defined
72 68
 `,
73 69
 		},
74 70
 		{
75
-			VolumeContext{
76
-				Context: Context{
77
-					Format: "{{nil}}",
78
-				},
79
-			},
71
+			Context{Format: "{{nil}}"},
80 72
 			`Template parsing error: template: :1:2: executing "" at <nil>: nil is not a command
81 73
 `,
82 74
 		},
83 75
 		// Table format
84 76
 		{
85
-			VolumeContext{
86
-				Context: Context{
87
-					Format: "table",
88
-				},
89
-			},
77
+			Context{Format: NewVolumeFormat("table", false)},
90 78
 			`DRIVER              NAME
91 79
 foo                 foobar_baz
92 80
 bar                 foobar_bar
93 81
 `,
94 82
 		},
95 83
 		{
96
-			VolumeContext{
97
-				Context: Context{
98
-					Format: "table",
99
-					Quiet:  true,
100
-				},
101
-			},
84
+			Context{Format: NewVolumeFormat("table", true)},
102 85
 			`foobar_baz
103 86
 foobar_bar
104 87
 `,
105 88
 		},
106 89
 		{
107
-			VolumeContext{
108
-				Context: Context{
109
-					Format: "table {{.Name}}",
110
-				},
111
-			},
90
+			Context{Format: NewVolumeFormat("table {{.Name}}", false)},
112 91
 			`NAME
113 92
 foobar_baz
114 93
 foobar_bar
115 94
 `,
116 95
 		},
117 96
 		{
118
-			VolumeContext{
119
-				Context: Context{
120
-					Format: "table {{.Name}}",
121
-					Quiet:  true,
122
-				},
123
-			},
97
+			Context{Format: NewVolumeFormat("table {{.Name}}", true)},
124 98
 			`NAME
125 99
 foobar_baz
126 100
 foobar_bar
... ...
@@ -128,11 +103,8 @@ foobar_bar
128 128
 		},
129 129
 		// Raw Format
130 130
 		{
131
-			VolumeContext{
132
-				Context: Context{
133
-					Format: "raw",
134
-				},
135
-			}, `name: foobar_baz
131
+			Context{Format: NewVolumeFormat("raw", false)},
132
+			`name: foobar_baz
136 133
 driver: foo
137 134
 
138 135
 name: foobar_bar
... ...
@@ -141,43 +113,32 @@ driver: bar
141 141
 `,
142 142
 		},
143 143
 		{
144
-			VolumeContext{
145
-				Context: Context{
146
-					Format: "raw",
147
-					Quiet:  true,
148
-				},
149
-			},
144
+			Context{Format: NewVolumeFormat("raw", true)},
150 145
 			`name: foobar_baz
151 146
 name: foobar_bar
152 147
 `,
153 148
 		},
154 149
 		// Custom Format
155 150
 		{
156
-			VolumeContext{
157
-				Context: Context{
158
-					Format: "{{.Name}}",
159
-				},
160
-			},
151
+			Context{Format: NewVolumeFormat("{{.Name}}", false)},
161 152
 			`foobar_baz
162 153
 foobar_bar
163 154
 `,
164 155
 		},
165 156
 	}
166 157
 
167
-	for _, context := range contexts {
158
+	for _, testcase := range cases {
168 159
 		volumes := []*types.Volume{
169 160
 			{Name: "foobar_baz", Driver: "foo"},
170 161
 			{Name: "foobar_bar", Driver: "bar"},
171 162
 		}
172 163
 		out := bytes.NewBufferString("")
173
-		context.context.Output = out
174
-		context.context.Volumes = volumes
175
-		context.context.Write()
176
-		actual := out.String()
177
-		if actual != context.expected {
178
-			t.Fatalf("Expected \n%s, got \n%s", context.expected, actual)
164
+		testcase.context.Output = out
165
+		err := VolumeWrite(testcase.context, volumes)
166
+		if err != nil {
167
+			assert.Error(t, err, testcase.expected)
168
+		} else {
169
+			assert.Equal(t, out.String(), testcase.expected)
179 170
 		}
180
-		// Clean buffer
181
-		out.Reset()
182 171
 	}
183 172
 }
... ...
@@ -64,27 +64,22 @@ func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
64 64
 		return err
65 65
 	}
66 66
 
67
-	f := opts.format
68
-	if len(f) == 0 {
67
+	format := opts.format
68
+	if len(format) == 0 {
69 69
 		if len(dockerCli.ConfigFile().ImagesFormat) > 0 && !opts.quiet {
70
-			f = dockerCli.ConfigFile().ImagesFormat
70
+			format = dockerCli.ConfigFile().ImagesFormat
71 71
 		} else {
72
-			f = "table"
72
+			format = "table"
73 73
 		}
74 74
 	}
75 75
 
76
-	imagesCtx := formatter.ImageContext{
76
+	imageCtx := formatter.ImageContext{
77 77
 		Context: formatter.Context{
78 78
 			Output: dockerCli.Out(),
79
-			Format: f,
80
-			Quiet:  opts.quiet,
79
+			Format: formatter.NewImageFormat(format, opts.quiet, opts.showDigests),
81 80
 			Trunc:  !opts.noTrunc,
82 81
 		},
83 82
 		Digest: opts.showDigests,
84
-		Images: images,
85 83
 	}
86
-
87
-	imagesCtx.Write()
88
-
89
-	return nil
84
+	return formatter.ImageWrite(imageCtx, images)
90 85
 }
... ...
@@ -50,35 +50,27 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
50 50
 
51 51
 func runList(dockerCli *command.DockerCli, opts listOptions) error {
52 52
 	client := dockerCli.Client()
53
-
54 53
 	options := types.NetworkListOptions{Filters: opts.filter.Value()}
55 54
 	networkResources, err := client.NetworkList(context.Background(), options)
56 55
 	if err != nil {
57 56
 		return err
58 57
 	}
59 58
 
60
-	f := opts.format
61
-	if len(f) == 0 {
59
+	format := opts.format
60
+	if len(format) == 0 {
62 61
 		if len(dockerCli.ConfigFile().NetworksFormat) > 0 && !opts.quiet {
63
-			f = dockerCli.ConfigFile().NetworksFormat
62
+			format = dockerCli.ConfigFile().NetworksFormat
64 63
 		} else {
65
-			f = "table"
64
+			format = "table"
66 65
 		}
67 66
 	}
68 67
 
69 68
 	sort.Sort(byNetworkName(networkResources))
70 69
 
71
-	networksCtx := formatter.NetworkContext{
72
-		Context: formatter.Context{
73
-			Output: dockerCli.Out(),
74
-			Format: f,
75
-			Quiet:  opts.quiet,
76
-			Trunc:  !opts.noTrunc,
77
-		},
78
-		Networks: networkResources,
70
+	networksCtx := formatter.Context{
71
+		Output: dockerCli.Out(),
72
+		Format: formatter.NewNetworkFormat(format, opts.quiet),
73
+		Trunc:  !opts.noTrunc,
79 74
 	}
80
-
81
-	networksCtx.Write()
82
-
83
-	return nil
75
+	return formatter.NetworkWrite(networksCtx, networkResources)
84 76
 }
... ...
@@ -56,29 +56,22 @@ func runList(dockerCli *command.DockerCli, opts listOptions) error {
56 56
 		return err
57 57
 	}
58 58
 
59
-	f := opts.format
60
-	if len(f) == 0 {
59
+	format := opts.format
60
+	if len(format) == 0 {
61 61
 		if len(dockerCli.ConfigFile().VolumesFormat) > 0 && !opts.quiet {
62
-			f = dockerCli.ConfigFile().VolumesFormat
62
+			format = dockerCli.ConfigFile().VolumesFormat
63 63
 		} else {
64
-			f = "table"
64
+			format = "table"
65 65
 		}
66 66
 	}
67 67
 
68 68
 	sort.Sort(byVolumeName(volumes.Volumes))
69 69
 
70
-	volumeCtx := formatter.VolumeContext{
71
-		Context: formatter.Context{
72
-			Output: dockerCli.Out(),
73
-			Format: f,
74
-			Quiet:  opts.quiet,
75
-		},
76
-		Volumes: volumes.Volumes,
70
+	volumeCtx := formatter.Context{
71
+		Output: dockerCli.Out(),
72
+		Format: formatter.NewVolumeFormat(format, opts.quiet),
77 73
 	}
78
-
79
-	volumeCtx.Write()
80
-
81
-	return nil
74
+	return formatter.VolumeWrite(volumeCtx, volumes.Volumes)
82 75
 }
83 76
 
84 77
 var listDescription = `