From 966851f3097fdb3cbaab0f8869ab3dbbdb779331 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 14 Jul 2018 15:02:26 -0700 Subject: [PATCH] hclwrite: TokensForValue This function produces a token stream of a reasonable source representation of the given constant value. --- hclwrite/generate.go | 195 ++++++++++++++++ hclwrite/generate_test.go | 468 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 663 insertions(+) create mode 100644 hclwrite/generate.go create mode 100644 hclwrite/generate_test.go diff --git a/hclwrite/generate.go b/hclwrite/generate.go new file mode 100644 index 0000000..320ee6c --- /dev/null +++ b/hclwrite/generate.go @@ -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 +} diff --git a/hclwrite/generate_test.go b/hclwrite/generate_test.go new file mode 100644 index 0000000..e6046db --- /dev/null +++ b/hclwrite/generate_test.go @@ -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, + ) + } + }) + } +}