aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/zclconf/go-cty/cty/function/stdlib/format.go
blob: fb24f204694ad00bf69f6851e94358708790a314 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
package stdlib

import (
	"bytes"
	"fmt"
	"math/big"
	"strings"

	"github.com/apparentlymart/go-textseg/textseg"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
	"github.com/zclconf/go-cty/cty/function"
	"github.com/zclconf/go-cty/cty/json"
)

//go:generate ragel -Z format_fsm.rl
//go:generate gofmt -w format_fsm.go

var FormatFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{
			Name: "format",
			Type: cty.String,
		},
	},
	VarParam: &function.Parameter{
		Name:      "args",
		Type:      cty.DynamicPseudoType,
		AllowNull: true,
	},
	Type: function.StaticReturnType(cty.String),
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
		for _, arg := range args[1:] {
			if !arg.IsWhollyKnown() {
				// We require all nested values to be known because the only
				// thing we can do for a collection/structural type is print
				// it as JSON and that requires it to be wholly known.
				return cty.UnknownVal(cty.String), nil
			}
		}
		str, err := formatFSM(args[0].AsString(), args[1:])
		return cty.StringVal(str), err
	},
})

var FormatListFunc = function.New(&function.Spec{
	Params: []function.Parameter{
		{
			Name: "format",
			Type: cty.String,
		},
	},
	VarParam: &function.Parameter{
		Name:         "args",
		Type:         cty.DynamicPseudoType,
		AllowNull:    true,
		AllowUnknown: true,
	},
	Type: function.StaticReturnType(cty.List(cty.String)),
	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
		fmtVal := args[0]
		args = args[1:]

		if len(args) == 0 {
			// With no arguments, this function is equivalent to Format, but
			// returning a single-element list result.
			result, err := Format(fmtVal, args...)
			return cty.ListVal([]cty.Value{result}), err
		}

		fmtStr := fmtVal.AsString()

		// Each of our arguments will be dealt with either as an iterator
		// or as a single value. Iterators are used for sequence-type values
		// (lists, sets, tuples) while everything else is treated as a
		// single value. The sequences we iterate over are required to be
		// all the same length.
		iterLen := -1
		lenChooser := -1
		iterators := make([]cty.ElementIterator, len(args))
		singleVals := make([]cty.Value, len(args))
		for i, arg := range args {
			argTy := arg.Type()
			switch {
			case (argTy.IsListType() || argTy.IsSetType() || argTy.IsTupleType()) && !arg.IsNull():
				thisLen := arg.LengthInt()
				if iterLen == -1 {
					iterLen = thisLen
					lenChooser = i
				} else {
					if thisLen != iterLen {
						return cty.NullVal(cty.List(cty.String)), function.NewArgErrorf(
							i+1,
							"argument %d has length %d, which is inconsistent with argument %d of length %d",
							i+1, thisLen,
							lenChooser+1, iterLen,
						)
					}
				}
				iterators[i] = arg.ElementIterator()
			default:
				singleVals[i] = arg
			}
		}

		if iterLen == 0 {
			// If our sequences are all empty then our result must be empty.
			return cty.ListValEmpty(cty.String), nil
		}

		if iterLen == -1 {
			// If we didn't encounter any iterables at all then we're going
			// to just do one iteration with items from singleVals.
			iterLen = 1
		}

		ret := make([]cty.Value, 0, iterLen)
		fmtArgs := make([]cty.Value, len(iterators))
	Results:
		for iterIdx := 0; iterIdx < iterLen; iterIdx++ {

			// Construct our arguments for a single format call
			for i := range fmtArgs {
				switch {
				case iterators[i] != nil:
					iterator := iterators[i]
					iterator.Next()
					_, val := iterator.Element()
					fmtArgs[i] = val
				default:
					fmtArgs[i] = singleVals[i]
				}

				// If any of the arguments to this call would be unknown then
				// this particular result is unknown, but we'll keep going
				// to see if any other iterations can produce known values.
				if !fmtArgs[i].IsWhollyKnown() {
					// We require all nested values to be known because the only
					// thing we can do for a collection/structural type is print
					// it as JSON and that requires it to be wholly known.
					ret = append(ret, cty.UnknownVal(cty.String))
					continue Results
				}
			}

			str, err := formatFSM(fmtStr, fmtArgs)
			if err != nil {
				return cty.NullVal(cty.List(cty.String)), fmt.Errorf(
					"error on format iteration %d: %s", iterIdx, err,
				)
			}

			ret = append(ret, cty.StringVal(str))
		}

		return cty.ListVal(ret), nil
	},
})

// Format produces a string representation of zero or more values using a
// format string similar to the "printf" function in C.
//
// It supports the following "verbs":
//
//     %%      Literal percent sign, consuming no value
//     %v      A default formatting of the value based on type, as described below.
//     %#v     JSON serialization of the value
//     %t      Converts to boolean and then produces "true" or "false"
//     %b      Converts to number, requires integer, produces binary representation
//     %d      Converts to number, requires integer, produces decimal representation
//     %o      Converts to number, requires integer, produces octal representation
//     %x      Converts to number, requires integer, produces hexadecimal representation
//             with lowercase letters
//     %X      Like %x but with uppercase letters
//     %e      Converts to number, produces scientific notation like -1.234456e+78
//     %E      Like %e but with an uppercase "E" representing the exponent
//     %f      Converts to number, produces decimal representation with fractional
//             part but no exponent, like 123.456
//     %g      %e for large exponents or %f otherwise
//     %G      %E for large exponents or %f otherwise
//     %s      Converts to string and produces the string's characters
//     %q      Converts to string and produces JSON-quoted string representation,
//             like %v.
//
// The default format selections made by %v are:
//
//     string  %s
//     number  %g
//     bool    %t
//     other   %#v
//
// Null values produce the literal keyword "null" for %v and %#v, and produce
// an error otherwise.
//
// Width is specified by an optional decimal number immediately preceding the
// verb letter. If absent, the width is whatever is necessary to represent the
// value. Precision is specified after the (optional) width by a period
// followed by a decimal number. If no period is present, a default precision
// is used. A period with no following number is invalid.
// For examples:
//
//     %f     default width, default precision
//     %9f    width 9, default precision
//     %.2f   default width, precision 2
//     %9.2f  width 9, precision 2
//
// Width and precision are measured in unicode characters (grapheme clusters).
//
// For most values, width is the minimum number of characters to output,
// padding the formatted form with spaces if necessary.
//
// For strings, precision limits the length of the input to be formatted (not
// the size of the output), truncating if necessary.
//
// For numbers, width sets the minimum width of the field and precision sets
// the number of places after the decimal, if appropriate, except that for
// %g/%G precision sets the total number of significant digits.
//
// The following additional symbols can be used immediately after the percent
// introducer as flags:
//
//           (a space) leave a space where the sign would be if number is positive
//     +     Include a sign for a number even if it is positive (numeric only)
//     -     Pad with spaces on the left rather than the right
//     0     Pad with zeros rather than spaces.
//
// Flag characters are ignored for verbs that do not support them.
//
// By default, % sequences consume successive arguments starting with the first.
// Introducing a [n] sequence immediately before the verb letter, where n is a
// decimal integer, explicitly chooses a particular value argument by its
// one-based index. Subsequent calls without an explicit index will then
// proceed with n+1, n+2, etc.
//
// An error is produced if the format string calls for an impossible conversion
// or accesses more values than are given. An error is produced also for
// an unsupported format verb.
func Format(format cty.Value, vals ...cty.Value) (cty.Value, error) {
	args := make([]cty.Value, 0, len(vals)+1)
	args = append(args, format)
	args = append(args, vals...)
	return FormatFunc.Call(args)
}

// FormatList applies the same formatting behavior as Format, but accepts
// a mixture of list and non-list values as arguments. Any list arguments
// passed must have the same length, which dictates the length of the
// resulting list.
//
// Any non-list arguments are used repeatedly for each iteration over the
// list arguments. The list arguments are iterated in order by key, so
// corresponding items are formatted together.
func FormatList(format cty.Value, vals ...cty.Value) (cty.Value, error) {
	args := make([]cty.Value, 0, len(vals)+1)
	args = append(args, format)
	args = append(args, vals...)
	return FormatListFunc.Call(args)
}

type formatVerb struct {
	Raw    string
	Offset int

	ArgNum int
	Mode   rune

	Zero  bool
	Sharp bool
	Plus  bool
	Minus bool
	Space bool

	HasPrec bool
	Prec    int

	HasWidth bool
	Width    int
}

// formatAppend is called by formatFSM (generated by format_fsm.rl) for each
// formatting sequence that is encountered.
func formatAppend(verb *formatVerb, buf *bytes.Buffer, args []cty.Value) error {
	argIdx := verb.ArgNum - 1
	if argIdx >= len(args) {
		return fmt.Errorf(
			"not enough arguments for %q at %d: need index %d but have %d total",
			verb.Raw, verb.Offset,
			verb.ArgNum, len(args),
		)
	}
	arg := args[argIdx]

	if verb.Mode != 'v' && arg.IsNull() {
		return fmt.Errorf("unsupported value for %q at %d: null value cannot be formatted", verb.Raw, verb.Offset)
	}

	// Normalize to make some things easier for downstream formatters
	if !verb.HasWidth {
		verb.Width = -1
	}
	if !verb.HasPrec {
		verb.Prec = -1
	}

	// For our first pass we'll ensure the verb is supported and then fan
	// out to other functions based on what conversion is needed.
	switch verb.Mode {

	case 'v':
		return formatAppendAsIs(verb, buf, arg)

	case 't':
		return formatAppendBool(verb, buf, arg)

	case 'b', 'd', 'o', 'x', 'X', 'e', 'E', 'f', 'g', 'G':
		return formatAppendNumber(verb, buf, arg)

	case 's', 'q':
		return formatAppendString(verb, buf, arg)

	default:
		return fmt.Errorf("unsupported format verb %q in %q at offset %d", verb.Mode, verb.Raw, verb.Offset)
	}
}

func formatAppendAsIs(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {

	if !verb.Sharp && !arg.IsNull() {
		// Unless the caller overrode it with the sharp flag, we'll try some
		// specialized formats before we fall back on JSON.
		switch arg.Type() {
		case cty.String:
			fmted := arg.AsString()
			fmted = formatPadWidth(verb, fmted)
			buf.WriteString(fmted)
			return nil
		case cty.Number:
			bf := arg.AsBigFloat()
			fmted := bf.Text('g', -1)
			fmted = formatPadWidth(verb, fmted)
			buf.WriteString(fmted)
			return nil
		}
	}

	jb, err := json.Marshal(arg, arg.Type())
	if err != nil {
		return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
	}
	fmted := formatPadWidth(verb, string(jb))
	buf.WriteString(fmted)

	return nil
}

func formatAppendBool(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
	var err error
	arg, err = convert.Convert(arg, cty.Bool)
	if err != nil {
		return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
	}

	if arg.True() {
		buf.WriteString("true")
	} else {
		buf.WriteString("false")
	}
	return nil
}

func formatAppendNumber(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
	var err error
	arg, err = convert.Convert(arg, cty.Number)
	if err != nil {
		return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
	}

	switch verb.Mode {
	case 'b', 'd', 'o', 'x', 'X':
		return formatAppendInteger(verb, buf, arg)
	default:
		bf := arg.AsBigFloat()

		// For floats our format syntax is a subset of Go's, so it's
		// safe for us to just lean on the existing Go implementation.
		fmtstr := formatStripIndexSegment(verb.Raw)
		fmted := fmt.Sprintf(fmtstr, bf)
		buf.WriteString(fmted)
		return nil
	}
}

func formatAppendInteger(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
	bf := arg.AsBigFloat()
	bi, acc := bf.Int(nil)
	if acc != big.Exact {
		return fmt.Errorf("unsupported value for %q at %d: an integer is required", verb.Raw, verb.Offset)
	}

	// For integers our format syntax is a subset of Go's, so it's
	// safe for us to just lean on the existing Go implementation.
	fmtstr := formatStripIndexSegment(verb.Raw)
	fmted := fmt.Sprintf(fmtstr, bi)
	buf.WriteString(fmted)
	return nil
}

func formatAppendString(verb *formatVerb, buf *bytes.Buffer, arg cty.Value) error {
	var err error
	arg, err = convert.Convert(arg, cty.String)
	if err != nil {
		return fmt.Errorf("unsupported value for %q at %d: %s", verb.Raw, verb.Offset, err)
	}

	// We _cannot_ directly use the Go fmt.Sprintf implementation for strings
	// because it measures widths and precisions in runes rather than grapheme
	// clusters.

	str := arg.AsString()
	if verb.Prec > 0 {
		strB := []byte(str)
		pos := 0
		wanted := verb.Prec
		for i := 0; i < wanted; i++ {
			next := strB[pos:]
			if len(next) == 0 {
				// ran out of characters before we hit our max width
				break
			}
			d, _, _ := textseg.ScanGraphemeClusters(strB[pos:], true)
			pos += d
		}
		str = str[:pos]
	}

	switch verb.Mode {
	case 's':
		fmted := formatPadWidth(verb, str)
		buf.WriteString(fmted)
	case 'q':
		jb, err := json.Marshal(cty.StringVal(str), cty.String)
		if err != nil {
			// Should never happen, since we know this is a known, non-null string
			panic(fmt.Errorf("failed to marshal %#v as JSON: %s", arg, err))
		}
		fmted := formatPadWidth(verb, string(jb))
		buf.WriteString(fmted)
	default:
		// Should never happen because formatAppend should've already validated
		panic(fmt.Errorf("invalid string formatting mode %q", verb.Mode))
	}
	return nil
}

func formatPadWidth(verb *formatVerb, fmted string) string {
	if verb.Width < 0 {
		return fmted
	}

	// Safe to ignore errors because ScanGraphemeClusters cannot produce errors
	givenLen, _ := textseg.TokenCount([]byte(fmted), textseg.ScanGraphemeClusters)
	wantLen := verb.Width
	if givenLen >= wantLen {
		return fmted
	}

	padLen := wantLen - givenLen
	padChar := " "
	if verb.Zero {
		padChar = "0"
	}
	pads := strings.Repeat(padChar, padLen)

	if verb.Minus {
		return fmted + pads
	}
	return pads + fmted
}

// formatStripIndexSegment strips out any [nnn] segment present in a verb
// string so that we can pass it through to Go's fmt.Sprintf with a single
// argument. This is used in cases where we're just leaning on Go's formatter
// because it's a superset of ours.
func formatStripIndexSegment(rawVerb string) string {
	// We assume the string has already been validated here, since we should
	// only be using this function with strings that were accepted by our
	// scanner in formatFSM.
	start := strings.Index(rawVerb, "[")
	end := strings.Index(rawVerb, "]")
	if start == -1 || end == -1 {
		return rawVerb
	}

	return rawVerb[:start] + rawVerb[end+1:]
}