From ed7453e277ae09578339a8be60b7ac67d9176b02 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sun, 11 Nov 2018 00:00:53 +0000 Subject: [PATCH] hclpack: PackNativeFile to get a packed version of a native syntax file This is a straightforward way to get a hclpack.Body in the common case where the input is already native syntax source code. Since the native syntax is unambiguous about structure, the whole structure can be packed in a single pass with no further information. --- hclpack/pack_native.go | 58 +++++++++++++ hclpack/pack_native_test.go | 166 ++++++++++++++++++++++++++++++++++++ hclpack/structure.go | 9 ++ 3 files changed, 233 insertions(+) create mode 100644 hclpack/pack_native.go create mode 100644 hclpack/pack_native_test.go diff --git a/hclpack/pack_native.go b/hclpack/pack_native.go new file mode 100644 index 0000000..05f67a6 --- /dev/null +++ b/hclpack/pack_native.go @@ -0,0 +1,58 @@ +package hclpack + +import ( + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" +) + +// PackNativeFile parses the given source code as HCL native syntax and packs +// it into a hclpack Body ready to be marshalled. +// +// If the given source code contains syntax errors then error diagnostics will +// be returned. A non-nil body might still be returned in this case, which +// allows a cautious caller to still do certain analyses on the result. +func PackNativeFile(src []byte, filename string, start hcl.Pos) (*Body, hcl.Diagnostics) { + f, diags := hclsyntax.ParseConfig(src, filename, start) + rootBody := f.Body.(*hclsyntax.Body) + return packNativeBody(rootBody, src), diags +} + +func packNativeBody(body *hclsyntax.Body, src []byte) *Body { + ret := &Body{} + for name, attr := range body.Attributes { + exprRng := attr.Expr.Range() + exprStartRng := attr.Expr.StartRange() + exprSrc := exprRng.SliceBytes(src) + ret.setAttribute(name, Attribute{ + Expr: Expression{ + Source: exprSrc, + SourceType: ExprNative, + + Range_: exprRng, + StartRange_: exprStartRng, + }, + Range: attr.Range(), + NameRange: attr.NameRange, + }) + } + + for _, block := range body.Blocks { + childBody := packNativeBody(block.Body, src) + defRange := block.TypeRange + if len(block.LabelRanges) > 0 { + defRange = hcl.RangeBetween(defRange, block.LabelRanges[len(block.LabelRanges)-1]) + } + ret.appendBlock(Block{ + Type: block.Type, + Labels: block.Labels, + Body: *childBody, + TypeRange: block.TypeRange, + DefRange: defRange, + LabelRanges: block.LabelRanges, + }) + } + + ret.MissingItemRange_ = body.EndRange + + return ret +} diff --git a/hclpack/pack_native_test.go b/hclpack/pack_native_test.go new file mode 100644 index 0000000..651b98f --- /dev/null +++ b/hclpack/pack_native_test.go @@ -0,0 +1,166 @@ +package hclpack + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/hcl2/hcl" +) + +func TestPackNativeFile(t *testing.T) { + src := ` +foo = "bar" +baz = "boz" + +child { + a = b + c +} + +another_child "foo" "bar" {} +` + + got, diags := PackNativeFile([]byte(src), "", hcl.Pos{Line: 1, Column: 1}) + for _, diag := range diags { + t.Errorf("unexpected diagnostic: %s", diag.Error()) + } + + want := &Body{ + Attributes: map[string]Attribute{ + "baz": { + Expr: Expression{ + Source: []byte(`"boz"`), + SourceType: ExprNative, + Range_: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 7, Byte: 19}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 24}, + }, + StartRange_: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 8, Byte: 20}, + End: hcl.Pos{Line: 3, Column: 11, Byte: 23}, + }, + }, + Range: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 1, Byte: 13}, + End: hcl.Pos{Line: 3, Column: 12, Byte: 24}, + }, + NameRange: hcl.Range{ + Start: hcl.Pos{Line: 3, Column: 1, Byte: 13}, + End: hcl.Pos{Line: 3, Column: 4, Byte: 16}, + }, + }, + "foo": { + Expr: Expression{ + Source: []byte(`"bar"`), + SourceType: ExprNative, + Range_: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 7, Byte: 7}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 12}, + }, + StartRange_: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 8, Byte: 8}, + End: hcl.Pos{Line: 2, Column: 11, Byte: 11}, + }, + }, + Range: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 2, Column: 12, Byte: 12}, + }, + NameRange: hcl.Range{ + Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 2, Column: 4, Byte: 4}, + }, + }, + }, + ChildBlocks: []Block{ + { + Type: "child", + Body: Body{ + Attributes: map[string]Attribute{ + "a": { + Expr: Expression{ + Source: []byte(`b + c`), + SourceType: ExprNative, + Range_: hcl.Range{ + Start: hcl.Pos{Line: 6, Column: 7, Byte: 40}, + End: hcl.Pos{Line: 6, Column: 12, Byte: 45}, + }, + StartRange_: hcl.Range{ + Start: hcl.Pos{Line: 6, Column: 7, Byte: 40}, + End: hcl.Pos{Line: 6, Column: 8, Byte: 41}, + }, + }, + Range: hcl.Range{ + Start: hcl.Pos{Line: 6, Column: 3, Byte: 36}, + End: hcl.Pos{Line: 6, Column: 12, Byte: 45}, + }, + NameRange: hcl.Range{ + Start: hcl.Pos{Line: 6, Column: 3, Byte: 36}, + End: hcl.Pos{Line: 6, Column: 4, Byte: 37}, + }, + }, + }, + MissingItemRange_: hcl.Range{ + Start: hcl.Pos{Line: 7, Column: 2, Byte: 47}, + End: hcl.Pos{Line: 7, Column: 2, Byte: 47}, + }, + }, + DefRange: hcl.Range{ + Start: hcl.Pos{Line: 5, Column: 1, Byte: 26}, + End: hcl.Pos{Line: 5, Column: 6, Byte: 31}, + }, + TypeRange: hcl.Range{ + Start: hcl.Pos{Line: 5, Column: 1, Byte: 26}, + End: hcl.Pos{Line: 5, Column: 6, Byte: 31}, + }, + }, + { + Type: "another_child", + Labels: []string{"foo", "bar"}, + Body: Body{ + MissingItemRange_: hcl.Range{ + Start: hcl.Pos{Line: 9, Column: 29, Byte: 77}, + End: hcl.Pos{Line: 9, Column: 29, Byte: 77}, + }, + }, + DefRange: hcl.Range{ + Start: hcl.Pos{Line: 9, Column: 1, Byte: 49}, + End: hcl.Pos{Line: 9, Column: 26, Byte: 74}, + }, + TypeRange: hcl.Range{ + Start: hcl.Pos{Line: 9, Column: 1, Byte: 49}, + End: hcl.Pos{Line: 9, Column: 14, Byte: 62}, + }, + LabelRanges: []hcl.Range{ + hcl.Range{ + Start: hcl.Pos{Line: 9, Column: 15, Byte: 63}, + End: hcl.Pos{Line: 9, Column: 20, Byte: 68}, + }, + hcl.Range{ + Start: hcl.Pos{Line: 9, Column: 21, Byte: 69}, + End: hcl.Pos{Line: 9, Column: 26, Byte: 74}, + }, + }, + }, + }, + MissingItemRange_: hcl.Range{ + Start: hcl.Pos{Line: 10, Column: 1, Byte: 78}, + End: hcl.Pos{Line: 10, Column: 1, Byte: 78}, + }, + } + + if !cmp.Equal(want, got) { + bytesAsString := func(s []byte) string { + return string(s) + } + posAsString := func(pos hcl.Pos) string { + return fmt.Sprintf("%#v", pos) + } + t.Errorf("wrong result\n%s", cmp.Diff( + want, got, + cmp.Transformer("bytesAsString", bytesAsString), + cmp.Transformer("posAsString", posAsString), + )) + } +} diff --git a/hclpack/structure.go b/hclpack/structure.go index a9038bd..aeff4ed 100644 --- a/hclpack/structure.go +++ b/hclpack/structure.go @@ -17,6 +17,10 @@ type Body struct { var _ hcl.Body = (*Body)(nil) // Content is an implementation of the method of the same name on hcl.Body. +// +// When Content is called directly on a hclpack.Body, all child block bodies +// are guaranteed to be of type *hclpack.Body, so callers can type-assert +// to obtain a child Body in order to serialize it separately if needed. func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { return b.content(schema, nil) } @@ -26,6 +30,11 @@ func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostic // The returned "remain" body may share some backing objects with the receiver, // so neither the receiver nor the returned remain body, or any descendent // objects within them, may be mutated after this method is used. +// +// When Content is called directly on a hclpack.Body, all child block bodies +// and the returned "remain" body are guaranteed to be of type *hclpack.Body, +// so callers can type-assert to obtain a child Body in order to serialize it +// separately if needed. func (b *Body) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { remain := &Body{} content, diags := b.content(schema, remain)