ext/transform: helper package for applying transforms to bodies

This utility is intended to support the extension packages that are
siblings of this package, along with third-party extensions, by providing
a way to transform bodies in arbitrary ways.

The "Deep" function then provides a means to apply a particular transform
recursively to a nested block tree, allowing a particular extension to
be supported at arbitrary nesting levels.

This functionality is provided in terms of the standard zcl.Body
interface, so that transform results can be used with any code that
operates generically on bodies. This includes the zcldec and gozcl
packages, so files with extensions can still be decoded in the usual
way.
This commit is contained in:
Martin Atkins 2017-07-27 16:23:20 -07:00
parent a414468aac
commit fffca3d205
5 changed files with 340 additions and 0 deletions

7
ext/transform/doc.go Normal file
View File

@ -0,0 +1,7 @@
// Package transform is a helper package for writing extensions that work
// by applying transforms to bodies.
//
// It defines a type for body transformers, and then provides utilities in
// terms of that type for working with transformers, including recursively
// applying such transforms as heirarchical block structures are extracted.
package transform

108
ext/transform/error.go Normal file
View File

@ -0,0 +1,108 @@
package transform
import (
"github.com/zclconf/go-zcl/zcl"
)
// NewErrorBody returns a zcl.Body that returns the given diagnostics whenever
// any of its content-access methods are called.
//
// The given diagnostics must have at least one diagnostic of severity
// zcl.DiagError, or this function will panic.
//
// This can be used to prepare a return value for a Transformer that
// can't complete due to an error. While the transform itself will succeed,
// the error will be returned as soon as a caller attempts to extract content
// from the resulting body.
func NewErrorBody(diags zcl.Diagnostics) zcl.Body {
if !diags.HasErrors() {
panic("NewErrorBody called without any error diagnostics")
}
return diagBody{
Diags: diags,
}
}
// BodyWithDiagnostics returns a zcl.Body that wraps another zcl.Body
// and emits the given diagnostics for any content-extraction method.
//
// Unlike the result of NewErrorBody, a body with diagnostics still runs
// the extraction actions on the underlying body if (and only if) the given
// diagnostics do not contain errors, but prepends the given diagnostics with
// any diagnostics produced by the action.
//
// If the given diagnostics is empty, the given body is returned verbatim.
//
// This function is intended for conveniently reporting errors and/or warnings
// produced during a transform, ensuring that they will be seen when the
// caller eventually extracts content from the returned body.
func BodyWithDiagnostics(body zcl.Body, diags zcl.Diagnostics) zcl.Body {
if len(diags) == 0 {
// nothing to do!
return body
}
return diagBody{
Diags: diags,
Wrapped: body,
}
}
type diagBody struct {
Diags zcl.Diagnostics
Wrapped zcl.Body
}
func (b diagBody) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
if b.Diags.HasErrors() {
return b.emptyContent(), b.Diags
}
content, wrappedDiags := b.Wrapped.Content(schema)
diags := make(zcl.Diagnostics, 0, len(b.Diags)+len(wrappedDiags))
diags = append(diags, b.Diags...)
diags = append(diags, wrappedDiags...)
return content, diags
}
func (b diagBody) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
if b.Diags.HasErrors() {
return b.emptyContent(), b.Wrapped, b.Diags
}
content, remain, wrappedDiags := b.Wrapped.PartialContent(schema)
diags := make(zcl.Diagnostics, 0, len(b.Diags)+len(wrappedDiags))
diags = append(diags, b.Diags...)
diags = append(diags, wrappedDiags...)
return content, remain, diags
}
func (b diagBody) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
if b.Diags.HasErrors() {
return nil, b.Diags
}
attributes, wrappedDiags := b.Wrapped.JustAttributes()
diags := make(zcl.Diagnostics, 0, len(b.Diags)+len(wrappedDiags))
diags = append(diags, b.Diags...)
diags = append(diags, wrappedDiags...)
return attributes, diags
}
func (b diagBody) MissingItemRange() zcl.Range {
if b.Wrapped != nil {
return b.Wrapped.MissingItemRange()
}
// Placeholder. This should never be seen in practice because decoding
// a diagBody without a wrapped body should always produce an error.
return zcl.Range{
Filename: "<empty>",
}
}
func (b diagBody) emptyContent() *zcl.BodyContent {
return &zcl.BodyContent{
MissingItemRange: b.MissingItemRange(),
}
}

View File

@ -0,0 +1,83 @@
package transform
import (
"github.com/zclconf/go-zcl/zcl"
)
// Shallow is equivalent to calling transformer.TransformBody(body), and
// is provided only for completeness of the top-level API.
func Shallow(body zcl.Body, transformer Transformer) zcl.Body {
return transformer.TransformBody(body)
}
// Deep applies the given transform to the given body and then
// wraps the result such that any descendent blocks that are decoded will
// also have the transform applied to their bodies.
//
// This allows for language extensions that define a particular block type
// for a particular body and all nested blocks within it.
//
// Due to the wrapping behavior, the body resulting from this function
// will not be of the type returned by the transformer. Callers may call
// only the methods defined for interface zcl.Body, and may not type-assert
// to access other methods.
func Deep(body zcl.Body, transformer Transformer) zcl.Body {
return deepWrapper{
Transformed: transformer.TransformBody(body),
Transformer: transformer,
}
}
// deepWrapper is a zcl.Body implementation that ensures that a given
// transformer is applied to another given body when content is extracted,
// and that it recursively applies to any child blocks that are extracted.
type deepWrapper struct {
Transformed zcl.Body
Transformer Transformer
}
func (w deepWrapper) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
content, diags := w.Transformed.Content(schema)
content = w.transformContent(content)
return content, diags
}
func (w deepWrapper) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Body, zcl.Diagnostics) {
content, remain, diags := w.Transformed.PartialContent(schema)
content = w.transformContent(content)
return content, remain, diags
}
func (w deepWrapper) transformContent(content *zcl.BodyContent) *zcl.BodyContent {
if len(content.Blocks) == 0 {
// Easy path: if there are no blocks then there are no child bodies to wrap
return content
}
// Since we're going to change things here, we'll be polite and clone the
// structure so that we don't risk impacting any internal state of the
// original body.
ret := &zcl.BodyContent{
Attributes: content.Attributes,
MissingItemRange: content.MissingItemRange,
Blocks: make(zcl.Blocks, len(content.Blocks)),
}
for i, givenBlock := range content.Blocks {
// Shallow-copy the block so we can mutate it
newBlock := *givenBlock
newBlock.Body = Deep(newBlock.Body, w.Transformer)
ret.Blocks[i] = &newBlock
}
return ret
}
func (w deepWrapper) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
// Attributes can't have bodies or nested blocks, so this is just a thin wrapper.
return w.Transformed.JustAttributes()
}
func (w deepWrapper) MissingItemRange() zcl.Range {
return w.Transformed.MissingItemRange()
}

View File

@ -0,0 +1,102 @@
package transform
import (
"testing"
"reflect"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-zcl/zcl"
"github.com/zclconf/go-zcl/zcltest"
)
// Assert that deepWrapper implements Body
var deepWrapperIsBody zcl.Body = deepWrapper{}
func TestDeep(t *testing.T) {
testTransform := TransformerFunc(func(body zcl.Body) zcl.Body {
_, remain, diags := body.PartialContent(&zcl.BodySchema{
Blocks: []zcl.BlockHeaderSchema{
{
Type: "remove",
},
},
})
return BodyWithDiagnostics(remain, diags)
})
src := zcltest.MockBody(&zcl.BodyContent{
Attributes: zcltest.MockAttrs(map[string]zcl.Expression{
"true": zcltest.MockExprLiteral(cty.True),
}),
Blocks: []*zcl.Block{
{
Type: "remove",
Body: zcl.EmptyBody(),
},
{
Type: "child",
Body: zcltest.MockBody(&zcl.BodyContent{
Blocks: []*zcl.Block{
{
Type: "remove",
},
},
}),
},
},
})
wrapped := Deep(src, testTransform)
rootContent, diags := wrapped.Content(&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "true",
},
},
Blocks: []zcl.BlockHeaderSchema{
{
Type: "child",
},
},
})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics for root content")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}
wantAttrs := zcltest.MockAttrs(map[string]zcl.Expression{
"true": zcltest.MockExprLiteral(cty.True),
})
if !reflect.DeepEqual(rootContent.Attributes, wantAttrs) {
t.Errorf("wrong root attributes\ngot: %#v\nwant: %#v", rootContent.Attributes, wantAttrs)
}
if got, want := len(rootContent.Blocks), 1; got != want {
t.Fatalf("wrong number of root blocks %d; want %d", got, want)
}
if got, want := rootContent.Blocks[0].Type, "child"; got != want {
t.Errorf("wrong block type %s; want %s", got, want)
}
childBlock := rootContent.Blocks[0]
childContent, diags := childBlock.Body.Content(&zcl.BodySchema{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics for child content")
for _, diag := range diags {
t.Logf("- %s", diag)
}
}
if len(childContent.Attributes) != 0 {
t.Errorf("unexpected attributes in child content; want empty content")
}
if len(childContent.Blocks) != 0 {
t.Errorf("unexpected blocks in child content; want empty content")
}
}

View File

@ -0,0 +1,40 @@
package transform
import (
"github.com/zclconf/go-zcl/zcl"
)
// A Transformer takes a given body, applies some (possibly no-op)
// transform to it, and returns the new body.
//
// It must _not_ mutate the given body in-place.
//
// The transform call cannot fail, but it _can_ return a body that immediately
// returns diagnostics when its methods are called. NewErrorBody is a utility
// to help with this.
type Transformer interface {
TransformBody(zcl.Body) zcl.Body
}
// TransformerFunc is a function type that implements Transformer.
type TransformerFunc func(zcl.Body) zcl.Body
// TransformBody is an implementation of Transformer.TransformBody.
func (f TransformerFunc) TransformBody(in zcl.Body) zcl.Body {
return f(in)
}
type chain []Transformer
// Chain takes a slice of transformers and returns a single new
// Transformer that applies each of the given transformers in sequence.
func Chain(c []Transformer) Transformer {
return chain(c)
}
func (c chain) TransformBody(body zcl.Body) zcl.Body {
for _, t := range c {
body = t.TransformBody(body)
}
return body
}