From fffca3d205974ed0288312ab6781b0349a256000 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Thu, 27 Jul 2017 16:23:20 -0700 Subject: [PATCH] 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. --- ext/transform/doc.go | 7 +++ ext/transform/error.go | 108 ++++++++++++++++++++++++++++++++ ext/transform/transform.go | 83 ++++++++++++++++++++++++ ext/transform/transform_test.go | 102 ++++++++++++++++++++++++++++++ ext/transform/transformer.go | 40 ++++++++++++ 5 files changed, 340 insertions(+) create mode 100644 ext/transform/doc.go create mode 100644 ext/transform/error.go create mode 100644 ext/transform/transform.go create mode 100644 ext/transform/transform_test.go create mode 100644 ext/transform/transformer.go diff --git a/ext/transform/doc.go b/ext/transform/doc.go new file mode 100644 index 0000000..ac46669 --- /dev/null +++ b/ext/transform/doc.go @@ -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 diff --git a/ext/transform/error.go b/ext/transform/error.go new file mode 100644 index 0000000..40b1015 --- /dev/null +++ b/ext/transform/error.go @@ -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: "", + } +} + +func (b diagBody) emptyContent() *zcl.BodyContent { + return &zcl.BodyContent{ + MissingItemRange: b.MissingItemRange(), + } +} diff --git a/ext/transform/transform.go b/ext/transform/transform.go new file mode 100644 index 0000000..fe35bbd --- /dev/null +++ b/ext/transform/transform.go @@ -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() +} diff --git a/ext/transform/transform_test.go b/ext/transform/transform_test.go new file mode 100644 index 0000000..7e3c264 --- /dev/null +++ b/ext/transform/transform_test.go @@ -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") + } +} diff --git a/ext/transform/transformer.go b/ext/transform/transformer.go new file mode 100644 index 0000000..1fcb5ea --- /dev/null +++ b/ext/transform/transformer.go @@ -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 +}