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:
parent
a414468aac
commit
fffca3d205
7
ext/transform/doc.go
Normal file
7
ext/transform/doc.go
Normal 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
108
ext/transform/error.go
Normal 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(),
|
||||
}
|
||||
}
|
83
ext/transform/transform.go
Normal file
83
ext/transform/transform.go
Normal 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()
|
||||
}
|
102
ext/transform/transform_test.go
Normal file
102
ext/transform/transform_test.go
Normal 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")
|
||||
}
|
||||
}
|
40
ext/transform/transformer.go
Normal file
40
ext/transform/transformer.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user