hclwrite: Allow selecting blocks for updating
This commit is contained in:
parent
ed70541558
commit
9d1235a5b4
@ -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
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
105
hclwrite/ast_block_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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.
|
||||
//
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user