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 +}