hclwrite: Allow selecting blocks for updating

This commit is contained in:
Masayuki Morita 2019-07-21 00:23:02 +09:00 committed by Martin Atkins
parent ed70541558
commit 9d1235a5b4
6 changed files with 402 additions and 5 deletions

View File

@ -1661,7 +1661,7 @@ Token:
break Token
case TokenQuotedLit:
s, sDiags := p.decodeStringLit(tok)
s, sDiags := ParseStringLiteralToken(tok)
diags = append(diags, sDiags...)
ret.WriteString(s)
@ -1721,13 +1721,13 @@ Token:
return ret.String(), hcl.RangeBetween(oQuote.Range, cQuote.Range), diags
}
// decodeStringLit processes the given token, which must be either a
// ParseStringLiteralToken processes the given token, which must be either a
// TokenQuotedLit or a TokenStringLit, returning the string resulting from
// resolving any escape sequences.
//
// If any error diagnostics are returned, the returned string may be incomplete
// or otherwise invalid.
func (p *parser) decodeStringLit(tok Token) (string, hcl.Diagnostics) {
func ParseStringLiteralToken(tok Token) (string, hcl.Diagnostics) {
var quoted bool
switch tok.Type {
case TokenQuotedLit:
@ -1735,7 +1735,7 @@ func (p *parser) decodeStringLit(tok Token) (string, hcl.Diagnostics) {
case TokenStringLit:
quoted = false
default:
panic("decodeQuotedLit can only be used with TokenStringLit and TokenQuotedLit tokens")
panic("ParseStringLiteralToken can only be used with TokenStringLit and TokenQuotedLit tokens")
}
var diags hcl.Diagnostics

View File

@ -383,7 +383,7 @@ Token:
switch next.Type {
case TokenStringLit, TokenQuotedLit:
str, strDiags := p.decodeStringLit(next)
str, strDiags := ParseStringLiteralToken(next)
diags = append(diags, strDiags...)
if ltrim {

View File

@ -72,3 +72,47 @@ func (b *Block) init(typeName string, labels []string) {
func (b *Block) Body() *Body {
return b.body.content.(*Body)
}
// Type returns the type name of the block.
func (b *Block) Type() string {
typeNameObj := b.typeName.content.(*identifier)
return string(typeNameObj.token.Bytes)
}
// Labels returns the labels of the block.
func (b *Block) Labels() []string {
labelNames := make([]string, 0, len(b.labels))
list := b.labels.List()
for _, label := range list {
switch labelObj := label.content.(type) {
case *identifier:
if labelObj.token.Type == hclsyntax.TokenIdent {
labelString := string(labelObj.token.Bytes)
labelNames = append(labelNames, labelString)
}
case *quoted:
tokens := labelObj.tokens
if len(tokens) == 3 &&
tokens[0].Type == hclsyntax.TokenOQuote &&
tokens[1].Type == hclsyntax.TokenQuotedLit &&
tokens[2].Type == hclsyntax.TokenCQuote {
// Note that TokenQuotedLit may contain escape sequences.
labelString, diags := hclsyntax.ParseStringLiteralToken(tokens[1].asHCLSyntax())
// If parsing the string literal returns error diagnostics
// then we can just assume the label doesn't match, because it's invalid in some way.
if !diags.HasErrors() {
labelNames = append(labelNames, labelString)
}
}
default:
// If neither of the previous cases are true (should be impossible)
// then we can just ignore it, because it's invalid too.
}
}
return labelNames
}

105
hclwrite/ast_block_test.go Normal file
View File

@ -0,0 +1,105 @@
package hclwrite
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/hashicorp/hcl/v2"
)
func TestBlockType(t *testing.T) {
tests := []struct {
src string
want string
}{
{
`
service {
attr0 = "val0"
}
`,
"service",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s", test.want), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
block := f.Body().Blocks()[0]
got := string(block.Type())
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want)
}
})
}
}
func TestBlockLabels(t *testing.T) {
tests := []struct {
src string
want []string
}{
{
`
nolabel {
}
`,
[]string{},
},
{
`
quoted "label1" {
}
`,
[]string{"label1"},
},
{
`
quoted "label1" "label2" {
}
`,
[]string{"label1", "label2"},
},
{
`
unquoted label1 {
}
`,
[]string{"label1"},
},
{
`
escape "\u0041" {
}
`,
[]string{"\u0041"},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s", strings.Join(test.want, " ")), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
block := f.Body().Blocks()[0]
got := block.Labels()
if !reflect.DeepEqual(got, test.want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}

View File

@ -1,6 +1,8 @@
package hclwrite
import (
"reflect"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
@ -82,6 +84,23 @@ func (b *Body) GetAttribute(name string) *Attribute {
return nil
}
// FirstMatchingBlock returns a first matching block from the body that has the
// given name and labels or returns nil if there is currently no matching
// block.
func (b *Body) FirstMatchingBlock(typeName string, labels []string) *Block {
for _, block := range b.Blocks() {
if typeName == block.Type() {
labelNames := block.Labels()
if reflect.DeepEqual(labels, labelNames) {
// We've found it!
return block
}
}
}
return nil
}
// SetAttributeValue either replaces the expression of an existing attribute
// of the given name or adds a new attribute definition to the end of the block.
//

View File

@ -3,6 +3,7 @@ package hclwrite
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
@ -216,6 +217,131 @@ func TestBodyGetAttribute(t *testing.T) {
}
}
func TestBodyFirstMatchingBlock(t *testing.T) {
src := `a = "b"
service {
attr0 = "val0"
}
service "label1" {
attr1 = "val1"
}
service "label1" "label2" {
attr2 = "val2"
}
parent {
attr3 = "val3"
child {
attr4 = "val4"
}
}
`
tests := []struct {
src string
typeName string
labels []string
want string
}{
{
src,
"service",
[]string{},
`service {
attr0 = "val0"
}
`,
},
{
src,
"service",
[]string{"label1"},
`service "label1" {
attr1 = "val1"
}
`,
},
{
src,
"service",
[]string{"label1", "label2"},
`service "label1" "label2" {
attr2 = "val2"
}
`,
},
{
src,
"parent",
[]string{},
`parent {
attr3 = "val3"
child {
attr4 = "val4"
}
}
`,
},
{
src,
"hoge",
[]string{},
"",
},
{
src,
"hoge",
[]string{"label1"},
"",
},
{
src,
"service",
[]string{"label2"},
"",
},
{
src,
"service",
[]string{"label2", "label1"},
"",
},
{
src,
"child",
[]string{},
"",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s %s", test.typeName, strings.Join(test.labels, " ")), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
block := f.Body().FirstMatchingBlock(test.typeName, test.labels)
if block == nil {
if test.want != "" {
t.Fatal("block not found, but want it to exist")
}
} else {
if test.want == "" {
t.Fatal("block found, but expecting not found")
}
got := string(block.BuildTokens(nil).Bytes())
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s", got, test.want)
}
}
})
}
}
func TestBodySetAttributeValue(t *testing.T) {
tests := []struct {
src string
@ -640,6 +766,109 @@ func TestBodySetAttributeTraversal(t *testing.T) {
}
}
func TestBodySetAttributeValueInBlock(t *testing.T) {
src := `service "label1" {
attr1 = "val1"
}
`
tests := []struct {
src string
typeName string
labels []string
attr string
val cty.Value
want string
}{
{
src,
"service",
[]string{"label1"},
"attr1",
cty.StringVal("updated1"),
`service "label1" {
attr1 = "updated1"
}
`,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s = %#v in %s %s", test.attr, test.val, test.typeName, strings.Join(test.labels, " ")), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
b := f.Body().FirstMatchingBlock(test.typeName, test.labels)
b.Body().SetAttributeValue(test.attr, test.val)
tokens := f.BuildTokens(nil)
format(tokens)
got := string(tokens.Bytes())
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s\n", got, test.want)
}
})
}
}
func TestBodySetAttributeValueInNestedBlock(t *testing.T) {
src := `parent {
attr1 = "val1"
child {
attr2 = "val2"
}
}
`
tests := []struct {
src string
parentTypeName string
childTypeName string
attr string
val cty.Value
want string
}{
{
src,
"parent",
"child",
"attr2",
cty.StringVal("updated2"),
`parent {
attr1 = "val1"
child {
attr2 = "updated2"
}
}
`,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s = %#v in %s in %s", test.attr, test.val, test.childTypeName, test.parentTypeName), func(t *testing.T) {
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
if len(diags) != 0 {
for _, diag := range diags {
t.Logf("- %s", diag.Error())
}
t.Fatalf("unexpected diagnostics")
}
parent := f.Body().FirstMatchingBlock(test.parentTypeName, []string{})
child := parent.Body().FirstMatchingBlock(test.childTypeName, []string{})
child.Body().SetAttributeValue(test.attr, test.val)
tokens := f.BuildTokens(nil)
format(tokens)
got := string(tokens.Bytes())
if got != test.want {
t.Errorf("wrong result\ngot: %s\nwant: %s\n", got, test.want)
}
})
}
}
func TestBodyAppendBlock(t *testing.T) {
tests := []struct {
src string