hclwrite: TokensForValue
This function produces a token stream of a reasonable source representation of the given constant value.
This commit is contained in:
parent
3c0fafde46
commit
966851f309
195
hclwrite/generate.go
Normal file
195
hclwrite/generate.go
Normal file
@ -0,0 +1,195 @@
|
||||
package hclwrite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// TokensForValue returns a sequence of tokens that represents the given
|
||||
// constant value.
|
||||
//
|
||||
// This function only supports types that are used by HCL. In particular, it
|
||||
// does not support capsule types and will panic if given one.
|
||||
//
|
||||
// It is not possible to express an unknown value in source code, so this
|
||||
// function will panic if the given value is unknown or contains any unknown
|
||||
// values. A caller can call the value's IsWhollyKnown method to verify that
|
||||
// no unknown values are present before calling TokensForValue.
|
||||
func TokensForValue(val cty.Value) Tokens {
|
||||
toks := appendTokensForValue(val, nil)
|
||||
format(toks) // fiddle with the SpacesBefore field to get canonical spacing
|
||||
return toks
|
||||
}
|
||||
|
||||
func appendTokensForValue(val cty.Value, toks Tokens) Tokens {
|
||||
switch {
|
||||
|
||||
case !val.IsKnown():
|
||||
panic("cannot produce tokens for unknown value")
|
||||
|
||||
case val.IsNull():
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`null`),
|
||||
})
|
||||
|
||||
case val.Type() == cty.Bool:
|
||||
var src []byte
|
||||
if val.True() {
|
||||
src = []byte(`true`)
|
||||
} else {
|
||||
src = []byte(`false`)
|
||||
}
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: src,
|
||||
})
|
||||
|
||||
case val.Type() == cty.Number:
|
||||
bf := val.AsBigFloat()
|
||||
srcStr := bf.Text('f', -1)
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(srcStr),
|
||||
})
|
||||
|
||||
case val.Type() == cty.String:
|
||||
// TODO: If it's a multi-line string ending in a newline, format
|
||||
// it as a HEREDOC instead.
|
||||
src := escapeQuotedStringLit(val.AsString())
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte{'"'},
|
||||
})
|
||||
if len(src) > 0 {
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: src,
|
||||
})
|
||||
}
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte{'"'},
|
||||
})
|
||||
|
||||
case val.Type().IsListType() || val.Type().IsSetType() || val.Type().IsTupleType():
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte{'['},
|
||||
})
|
||||
|
||||
i := 0
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
if i > 0 {
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenComma,
|
||||
Bytes: []byte{','},
|
||||
})
|
||||
}
|
||||
_, eVal := it.Element()
|
||||
toks = appendTokensForValue(eVal, toks)
|
||||
i++
|
||||
}
|
||||
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte{']'},
|
||||
})
|
||||
|
||||
case val.Type().IsMapType() || val.Type().IsObjectType():
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte{'{'},
|
||||
})
|
||||
|
||||
i := 0
|
||||
for it := val.ElementIterator(); it.Next(); {
|
||||
if i > 0 {
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenComma,
|
||||
Bytes: []byte{','},
|
||||
})
|
||||
}
|
||||
eKey, eVal := it.Element()
|
||||
if hclsyntax.ValidIdentifier(eKey.AsString()) {
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(eKey.AsString()),
|
||||
})
|
||||
} else {
|
||||
toks = appendTokensForValue(eKey, toks)
|
||||
}
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte{'='},
|
||||
})
|
||||
toks = appendTokensForValue(eVal, toks)
|
||||
i++
|
||||
}
|
||||
|
||||
toks = append(toks, &Token{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte{'}'},
|
||||
})
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("cannot produce tokens for %#v", val))
|
||||
}
|
||||
|
||||
return toks
|
||||
}
|
||||
|
||||
func escapeQuotedStringLit(s string) []byte {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
buf := make([]byte, 0, len(s))
|
||||
for i, r := range s {
|
||||
switch r {
|
||||
case '\n':
|
||||
buf = append(buf, '\\', 'n')
|
||||
case '\r':
|
||||
buf = append(buf, '\\', 'r')
|
||||
case '\t':
|
||||
buf = append(buf, '\\', 't')
|
||||
case '"':
|
||||
buf = append(buf, '\\', '"')
|
||||
case '\\':
|
||||
buf = append(buf, '\\', '\\')
|
||||
case '$', '%':
|
||||
buf = appendRune(buf, r)
|
||||
remain := s[i+1:]
|
||||
if len(remain) > 0 && remain[0] == '{' {
|
||||
// Double up our template introducer symbol to escape it.
|
||||
buf = appendRune(buf, r)
|
||||
}
|
||||
default:
|
||||
if !unicode.IsPrint(r) {
|
||||
var fmted string
|
||||
if r < 65536 {
|
||||
fmted = fmt.Sprintf("\\u%04x", r)
|
||||
} else {
|
||||
fmted = fmt.Sprintf("\\U%08x", r)
|
||||
}
|
||||
buf = append(buf, fmted...)
|
||||
} else {
|
||||
buf = appendRune(buf, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func appendRune(b []byte, r rune) []byte {
|
||||
l := utf8.RuneLen(r)
|
||||
for i := 0; i < l; i++ {
|
||||
b = append(b, 0) // make room at the end of our buffer
|
||||
}
|
||||
ch := b[len(b)-l:]
|
||||
utf8.EncodeRune(ch, r)
|
||||
return b
|
||||
}
|
468
hclwrite/generate_test.go
Normal file
468
hclwrite/generate_test.go
Normal file
@ -0,0 +1,468 @@
|
||||
package hclwrite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestTokensForValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
Val cty.Value
|
||||
Want Tokens
|
||||
}{
|
||||
{
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`null`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.True,
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.False,
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`false`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.NumberIntVal(0),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(`0`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.NumberFloatVal(0.5),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(`0.5`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.NumberVal(big.NewFloat(0).SetPrec(512).Mul(big.NewFloat(40000000), big.NewFloat(2000000))),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(`80000000000000`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal(""),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal("foo"),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`foo`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal(`"foo"`),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`\"foo\"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello\nworld\n"),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`hello\nworld\n`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal("hello\r\nworld\r\n"),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`hello\r\nworld\r\n`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal(`what\what`),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`what\\what`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal("𝄞"),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte("𝄞"),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.StringVal("👩🏾"),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`👩🏾`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.EmptyTupleVal,
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.EmptyTupleVal}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.ListValEmpty(cty.String),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.SetValEmpty(cty.Bool),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.True}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.TupleVal([]cty.Value{cty.True, cty.NumberIntVal(0)}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrack,
|
||||
Bytes: []byte(`[`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenComma,
|
||||
Bytes: []byte(`,`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(`0`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrack,
|
||||
Bytes: []byte(`]`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.EmptyObjectVal,
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte(`{`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte(`}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.MapValEmpty(cty.Bool),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte(`{`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte(`}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.True,
|
||||
}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte(`{`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`foo`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte(`=`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte(`}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo": cty.True,
|
||||
"bar": cty.NumberIntVal(0),
|
||||
}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte(`{`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`bar`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte(`=`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte(`0`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenComma,
|
||||
Bytes: []byte(`,`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`foo`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte(`=`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte(`}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"foo bar": cty.True,
|
||||
}),
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenOBrace,
|
||||
Bytes: []byte(`{`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenOQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenQuotedLit,
|
||||
Bytes: []byte(`foo bar`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCQuote,
|
||||
Bytes: []byte(`"`),
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte(`=`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenCBrace,
|
||||
Bytes: []byte(`}`),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Val.GoString(), func(t *testing.T) {
|
||||
got := TokensForValue(test.Val)
|
||||
|
||||
if !cmp.Equal(got, test.Want) {
|
||||
diff := cmp.Diff(got, test.Want, cmp.Comparer(func(a, b []byte) bool {
|
||||
return bytes.Equal(a, b)
|
||||
}))
|
||||
var gotBuf, wantBuf bytes.Buffer
|
||||
got.WriteTo(&gotBuf)
|
||||
test.Want.WriteTo(&wantBuf)
|
||||
t.Errorf(
|
||||
"wrong result\nvalue: %#v\ngot: %s\nwant: %s\ndiff: %s",
|
||||
test.Val, gotBuf.String(), wantBuf.String(), diff,
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user