hclwrite: Allow selecting blocks for updating
This commit is contained in:
parent
ed70541558
commit
9d1235a5b4
@ -1661,7 +1661,7 @@ Token:
|
|||||||
break Token
|
break Token
|
||||||
|
|
||||||
case TokenQuotedLit:
|
case TokenQuotedLit:
|
||||||
s, sDiags := p.decodeStringLit(tok)
|
s, sDiags := ParseStringLiteralToken(tok)
|
||||||
diags = append(diags, sDiags...)
|
diags = append(diags, sDiags...)
|
||||||
ret.WriteString(s)
|
ret.WriteString(s)
|
||||||
|
|
||||||
@ -1721,13 +1721,13 @@ Token:
|
|||||||
return ret.String(), hcl.RangeBetween(oQuote.Range, cQuote.Range), diags
|
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
|
// TokenQuotedLit or a TokenStringLit, returning the string resulting from
|
||||||
// resolving any escape sequences.
|
// resolving any escape sequences.
|
||||||
//
|
//
|
||||||
// If any error diagnostics are returned, the returned string may be incomplete
|
// If any error diagnostics are returned, the returned string may be incomplete
|
||||||
// or otherwise invalid.
|
// or otherwise invalid.
|
||||||
func (p *parser) decodeStringLit(tok Token) (string, hcl.Diagnostics) {
|
func ParseStringLiteralToken(tok Token) (string, hcl.Diagnostics) {
|
||||||
var quoted bool
|
var quoted bool
|
||||||
switch tok.Type {
|
switch tok.Type {
|
||||||
case TokenQuotedLit:
|
case TokenQuotedLit:
|
||||||
@ -1735,7 +1735,7 @@ func (p *parser) decodeStringLit(tok Token) (string, hcl.Diagnostics) {
|
|||||||
case TokenStringLit:
|
case TokenStringLit:
|
||||||
quoted = false
|
quoted = false
|
||||||
default:
|
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
|
var diags hcl.Diagnostics
|
||||||
|
|
||||||
|
@ -383,7 +383,7 @@ Token:
|
|||||||
|
|
||||||
switch next.Type {
|
switch next.Type {
|
||||||
case TokenStringLit, TokenQuotedLit:
|
case TokenStringLit, TokenQuotedLit:
|
||||||
str, strDiags := p.decodeStringLit(next)
|
str, strDiags := ParseStringLiteralToken(next)
|
||||||
diags = append(diags, strDiags...)
|
diags = append(diags, strDiags...)
|
||||||
|
|
||||||
if ltrim {
|
if ltrim {
|
||||||
|
@ -72,3 +72,47 @@ func (b *Block) init(typeName string, labels []string) {
|
|||||||
func (b *Block) Body() *Body {
|
func (b *Block) Body() *Body {
|
||||||
return b.body.content.(*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
|
package hclwrite
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"github.com/hashicorp/hcl/v2"
|
"github.com/hashicorp/hcl/v2"
|
||||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
@ -82,6 +84,23 @@ func (b *Body) GetAttribute(name string) *Attribute {
|
|||||||
return nil
|
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
|
// 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.
|
// of the given name or adds a new attribute definition to the end of the block.
|
||||||
//
|
//
|
||||||
|
@ -3,6 +3,7 @@ package hclwrite
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"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) {
|
func TestBodySetAttributeValue(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
src string
|
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) {
|
func TestBodyAppendBlock(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
src string
|
src string
|
||||||
|
Loading…
Reference in New Issue
Block a user