plugin

package
v0.2.11 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: GPL-3.0 Imports: 15 Imported by: 0

Documentation

Overview

Package plugin is the wire protocol a TDL backend speaks, and the SDK for writing one.

Writing a backend

A backend implements Backend: Backend.Describe says what it is and what directives it understands, and Backend.Generate turns a request into files. A plugin's main is Serve and nothing else:

func main() {
	if err := plugin.Serve(myBackend{}); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Build it as tdl-gen-<name> and put it on PATH. A target block naming that backend will find it.

What a request contains

Three things about the model surprise people, and all three come from how the compiler resolves rather than from this protocol.

The prelude is in it. A model whose source declares two things arrives with twenty-one declarations, nineteen of them string, List, Option, and the rest of the standard prelude, merged in untagged. That is what lets a replacement prelude change what a collection is without any backend learning about it, and it means a backend emitting one file per declaration will emit nineteen nobody asked for. Filter by the filename in each declaration's position.

Directives are tagged, not filtered. A node carries the directives of every target block in the model, each naming the block it came from, so a backend keeps its own with Directives. Reading them unfiltered means acting on another backend's instructions.

A directive expanded from a class names the class it came from, so a backend can say why a rule applies rather than only that it does.

Run `tdl ir --format json` over a model to see all of this before writing code against it.

Returning files

A backend returns contents, and tdl writes them. That is what lets tdl enforce path confinement, --verify, and --clean rather than asking every backend to honour them. Paths are relative to Request.Out; an absolute one, or one climbing out with "..", is refused and nothing is written.

A problem with the model belongs in Response diagnostics, where it reaches the user with a position attached. Returning an error from Backend.Generate means the backend could not produce a response at all.

What a plugin will not see

A dependency's target blocks, because merging them needs the dependency lowered and nothing does that yet. And class-scoped directives on types that satisfy a class only through a conditional instance: a directive on Auditable reaches Audited and not the Page[Audited] that satisfies Auditable through an instance.

The wire

Messages are protobuf, framed with a varint length prefix, in both directions over one connection. tdl sends a Handshake first and a plugin answers with a HandshakeReply, accepting or refusing with the version it needed. Refusing is the point: a plugin that silently ignored fields it was compiled before would emit subtly wrong code with no diagnostic anywhere.

Conn is the codec, for anyone implementing the protocol in another language or embedding it somewhere Serve does not fit.

Example (Backend)

A plugin's main is Serve and nothing else.

package main

import (
	"context"
	"fmt"
	"strings"

	"github.com/unstoppablemango/tdl/ir"
	"github.com/unstoppablemango/tdl/plugin"
)

// shout is a backend in full: it declares what it understands, and it
// turns a model into files. A real one differs only in what it writes.
type shout struct{}

func (shout) Describe() plugin.Description {
	return plugin.Description{
		Name:    "shout",
		Version: "1.0.0",
		Directives: []*plugin.DirectiveSpec{{
			Name:     "as",
			MinArgs:  1,
			MaxArgs:  1,
			ArgKinds: []ir.LiteralKind{ir.LiteralKind_LITERAL_KIND_STRING},
		}},
	}
}

func (shout) Generate(_ context.Context, req *plugin.Request) (*plugin.Response, error) {
	var b strings.Builder

	for _, decl := range req.GetModel().GetDecls() {
		name := decl.GetMeta().GetName()

		// A model carries directives for every target block in it. Filter,
		// or you will act on another backend's instructions.
		for _, d := range plugin.Directives(req.GetTarget(), decl.GetDirectives()) {
			if d.GetName() == "as" && len(d.GetArgs()) == 1 {
				name = d.GetArgs()[0].GetText()
			}
		}

		fmt.Fprintln(&b, strings.ToUpper(name))
	}

	return &plugin.Response{
		Files: []*plugin.File{{Path: "shouted.txt", Content: []byte(b.String())}},
	}, nil
}

// A plugin's main is Serve and nothing else.
func main() {
	model := &ir.Model{
		Decls: []*ir.Decl{
			{Meta: &ir.Meta{Name: "Order"}},
			{
				Meta:       &ir.Meta{Name: "Customer"},
				Directives: []*ir.Directive{{Name: "as", Target: "shout", Args: []*ir.Literal{{Text: "buyer"}}}},
			},
		},
	}

	resp, err := shout{}.Generate(context.Background(), &plugin.Request{
		Target: "shout",
		Model:  model,
	})
	if err != nil {
		panic(err)
	}
	fmt.Print(string(resp.GetFiles()[0].GetContent()))

}
Output:
ORDER
BUYER

Index

Examples

Constants

View Source
const FramingVersion = 1

FramingVersion is the wire framing this package implements: a varint byte count followed by the encoded message, in both directions, over one connection that may carry several messages.

It is not protoc's model of one request in, one response out and a closed stdin. A stream lets the handshake and the request be ordinary messages, lets a plugin be reused across regenerations, and leaves room to offer something else later without a second mechanism.

View Source
const IRVersion = "tdl.ir.v1"

IRVersion is the protobuf package the model is encoded with.

View Source
const MaxMessageSize = 64 << 20

MaxMessageSize bounds a single message.

A length prefix is the first thing read from a stream that may be anything at all, so it is checked before it is trusted: without a bound, a corrupt or hostile prefix asks for an allocation of whatever it says.

Variables

View Source
var (
	Severity_name = map[int32]string{
		0: "SEVERITY_UNSPECIFIED",
		1: "SEVERITY_ERROR",
		2: "SEVERITY_WARNING",
	}
	Severity_value = map[string]int32{
		"SEVERITY_UNSPECIFIED": 0,
		"SEVERITY_ERROR":       1,
		"SEVERITY_WARNING":     2,
	}
)

Enum value maps for Severity.

View Source
var ErrTooLarge = errors.New("plugin: message exceeds the maximum size")

ErrTooLarge is returned when a length prefix exceeds MaxMessageSize.

View Source
var File_tdl_plugin_v1_plugin_proto protoreflect.FileDescriptor

Functions

func Directives

func Directives(target string, all []*ir.Directive) []*ir.Directive

Directives returns the directives on a node that belong to the target being served.

A model carries directives for every target block in it, each tagged with the block it came from, so a backend filters rather than assuming what it is handed is its own.

func Serve

func Serve(b Backend) error

Serve runs a backend over a connection, reading requests until the stream ends.

A plugin's main is this and nothing else. Without it, "anyone can ship a backend" would mean "anyone can reimplement the framing", and the protocol would have no second implementation keeping it honest.

func ServeConn

func ServeConn(ctx context.Context, b Backend, conn *Conn) error

ServeConn is Serve over a connection the caller supplies.

Types

type Backend

type Backend interface {
	// Describe reports what this backend is and what it understands. tdl
	// sends it in the handshake reply, and checks a target block against
	// the directives before generating anything.
	Describe() Description

	// Generate returns the files for one request. It returns an error only
	// when it cannot produce a response at all; a problem with the model
	// belongs in Response.diagnostics, where it reaches the user with a
	// position attached.
	Generate(ctx context.Context, req *Request) (*Response, error)
}

Backend turns a resolved model into files.

This is the whole surface. A backend compiled into tdl implements it and is called in process; a backend shipped as tdl-gen-<name> implements it and is served over a connection by Serve. There is no richer interface available to the first kind, because a surface only one of the two can reach is one nothing keeps honest.

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is one end of a plugin connection.

It is not safe for concurrent use. The protocol is a sequence of exchanges on one connection, so there is nothing to interleave.

func NewConn

func NewConn(r io.Reader, w io.Writer) *Conn

NewConn returns a Conn reading from r and writing to w.

func (*Conn) Recv

func (c *Conn) Recv(m proto.Message) error

Recv reads one message into m.

A stream that ends between messages returns io.EOF, which is how a reader knows the other end is done. A stream that ends part way through one returns io.ErrUnexpectedEOF, because that is a truncated message rather than an ending.

func (*Conn) Send

func (c *Conn) Send(m proto.Message) error

Send encodes m and writes it with its length prefix.

func (*Conn) Writer

func (c *Conn) Writer() io.Writer

Writer returns the underlying writer, so a caller holding a pipe can close it to tell the other end there is nothing more coming.

type Description

type Description struct {
	Name       string
	Version    string
	Directives []*DirectiveSpec
	Reuse      bool
}

Description is what a backend says about itself.

func (Description) Reply

func (d Description) Reply() *HandshakeReply

Reply turns a description into the handshake reply that carries it.

type Diagnostic

type Diagnostic struct {
	Severity Severity     `protobuf:"varint,1,opt,name=severity,enum=tdl.plugin.v1.Severity" json:"severity,omitempty"`
	Message  string       `protobuf:"bytes,2,opt,name=message" json:"message,omitempty"`
	Position *ir.Position `protobuf:"bytes,3,opt,name=position" json:"position,omitempty"`
	// contains filtered or unexported fields
}

Diagnostic is a problem a backend found.

It carries a position rather than a node ID: ir has three ID spaces, into declarations, types, and externs, and an ID alone does not say which. Every ir node carries its own position, so a backend copies the one it is complaining about.

func (*Diagnostic) Descriptor deprecated

func (*Diagnostic) Descriptor() ([]byte, []int)

Deprecated: Use Diagnostic.ProtoReflect.Descriptor instead.

func (*Diagnostic) GetMessage

func (x *Diagnostic) GetMessage() string

func (*Diagnostic) GetPosition

func (x *Diagnostic) GetPosition() *ir.Position

func (*Diagnostic) GetSeverity

func (x *Diagnostic) GetSeverity() Severity

func (*Diagnostic) ProtoMessage

func (*Diagnostic) ProtoMessage()

func (*Diagnostic) ProtoReflect

func (x *Diagnostic) ProtoReflect() protoreflect.Message

func (*Diagnostic) Reset

func (x *Diagnostic) Reset()

func (*Diagnostic) String

func (x *Diagnostic) String() string

type DirectiveSpec

type DirectiveSpec struct {
	Name    string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
	MinArgs int32  `protobuf:"varint,2,opt,name=min_args,json=minArgs" json:"min_args,omitempty"`
	// max_args is the largest number of arguments accepted, or -1 for any
	// number.
	MaxArgs int32 `protobuf:"varint,3,opt,name=max_args,json=maxArgs" json:"max_args,omitempty"`
	// arg_kinds constrains each argument by position. An empty list accepts
	// any kind, and a list shorter than the argument count constrains only
	// the arguments it covers.
	ArgKinds []ir.LiteralKind `protobuf:"varint,4,rep,packed,name=arg_kinds,json=argKinds,enum=tdl.ir.v1.LiteralKind" json:"arg_kinds,omitempty"`
	// repeatable says several entries of this directive at the same
	// specificity all reach the backend, in source order, rather than being
	// an error.
	Repeatable bool `protobuf:"varint,5,opt,name=repeatable" json:"repeatable,omitempty"`
	// contains filtered or unexported fields
}

DirectiveSpec is the shape of one directive a backend accepts.

A directive tdl finds in a target block and does not find here is a warning and is passed through anyway: under-declaring is a plugin bug that should not break a working project.

func (*DirectiveSpec) Descriptor deprecated

func (*DirectiveSpec) Descriptor() ([]byte, []int)

Deprecated: Use DirectiveSpec.ProtoReflect.Descriptor instead.

func (*DirectiveSpec) GetArgKinds

func (x *DirectiveSpec) GetArgKinds() []ir.LiteralKind

func (*DirectiveSpec) GetMaxArgs

func (x *DirectiveSpec) GetMaxArgs() int32

func (*DirectiveSpec) GetMinArgs

func (x *DirectiveSpec) GetMinArgs() int32

func (*DirectiveSpec) GetName

func (x *DirectiveSpec) GetName() string

func (*DirectiveSpec) GetRepeatable added in v0.2.10

func (x *DirectiveSpec) GetRepeatable() bool

func (*DirectiveSpec) ProtoMessage

func (*DirectiveSpec) ProtoMessage()

func (*DirectiveSpec) ProtoReflect

func (x *DirectiveSpec) ProtoReflect() protoreflect.Message

func (*DirectiveSpec) Reset

func (x *DirectiveSpec) Reset()

func (*DirectiveSpec) String

func (x *DirectiveSpec) String() string

type Features

type Features struct {

	// reuse says the plugin can serve more than one request on a
	// connection. It must treat each as independent.
	Reuse bool `protobuf:"varint,1,opt,name=reuse" json:"reuse,omitempty"`
	// contains filtered or unexported fields
}

Features are the optional parts of the protocol a backend supports.

func (*Features) Descriptor deprecated

func (*Features) Descriptor() ([]byte, []int)

Deprecated: Use Features.ProtoReflect.Descriptor instead.

func (*Features) GetReuse

func (x *Features) GetReuse() bool

func (*Features) ProtoMessage

func (*Features) ProtoMessage()

func (*Features) ProtoReflect

func (x *Features) ProtoReflect() protoreflect.Message

func (*Features) Reset

func (x *Features) Reset()

func (*Features) String

func (x *Features) String() string

type File

type File struct {
	Path    string `protobuf:"bytes,1,opt,name=path" json:"path,omitempty"`
	Content []byte `protobuf:"bytes,2,opt,name=content" json:"content,omitempty"`
	// contains filtered or unexported fields
}

File is one generated file, with a path relative to Request.out. An absolute path, or one containing "..", is an error and nothing is written.

func (*File) Descriptor deprecated

func (*File) Descriptor() ([]byte, []int)

Deprecated: Use File.ProtoReflect.Descriptor instead.

func (*File) GetContent

func (x *File) GetContent() []byte

func (*File) GetPath

func (x *File) GetPath() string

func (*File) ProtoMessage

func (*File) ProtoMessage()

func (*File) ProtoReflect

func (x *File) ProtoReflect() protoreflect.Message

func (*File) Reset

func (x *File) Reset()

func (*File) String

func (x *File) String() string

type Handshake

type Handshake struct {

	// framing_version is the wire framing this connection uses. Version 1 is
	// a varint length prefix followed by an encoded message, in both
	// directions. It exists so a later version can offer something else and
	// an older plugin can decline.
	FramingVersion int32 `protobuf:"varint,1,opt,name=framing_version,json=framingVersion" json:"framing_version,omitempty"`
	// ir_version is the protobuf package the model is encoded with, such as
	// "tdl.ir.v1". Within a version, field numbers are never reused and new
	// fields are additive.
	IrVersion string `protobuf:"bytes,2,opt,name=ir_version,json=irVersion" json:"ir_version,omitempty"`
	// watch says this connection may serve more than one request, which a
	// plugin declaring reuse should expect.
	Watch bool `protobuf:"varint,3,opt,name=watch" json:"watch,omitempty"`
	// contains filtered or unexported fields
}

Handshake is what tdl sends first, before any request.

A plugin that cannot read what is coming refuses here rather than silently ignoring fields it was compiled before and emitting subtly wrong code with no diagnostic anywhere.

func (*Handshake) Descriptor deprecated

func (*Handshake) Descriptor() ([]byte, []int)

Deprecated: Use Handshake.ProtoReflect.Descriptor instead.

func (*Handshake) GetFramingVersion

func (x *Handshake) GetFramingVersion() int32

func (*Handshake) GetIrVersion

func (x *Handshake) GetIrVersion() string

func (*Handshake) GetWatch

func (x *Handshake) GetWatch() bool

func (*Handshake) ProtoMessage

func (*Handshake) ProtoMessage()

func (*Handshake) ProtoReflect

func (x *Handshake) ProtoReflect() protoreflect.Message

func (*Handshake) Reset

func (x *Handshake) Reset()

func (*Handshake) String

func (x *Handshake) String() string

type HandshakeReply

type HandshakeReply struct {
	Accepted bool `protobuf:"varint,1,opt,name=accepted" json:"accepted,omitempty"`
	// refusal says what the plugin needed, when accepted is false. A
	// refusal naming both versions is the point of the handshake.
	Refusal string `protobuf:"bytes,2,opt,name=refusal" json:"refusal,omitempty"`
	Name    string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"`
	Version string `protobuf:"bytes,4,opt,name=version" json:"version,omitempty"`
	// directives are what this backend understands. tdl checks the target
	// block against them before generating; see DirectiveSpec.
	Directives []*DirectiveSpec `protobuf:"bytes,5,rep,name=directives" json:"directives,omitempty"`
	Features   *Features        `protobuf:"bytes,6,opt,name=features" json:"features,omitempty"`
	// contains filtered or unexported fields
}

HandshakeReply is the plugin's answer.

func (*HandshakeReply) Descriptor deprecated

func (*HandshakeReply) Descriptor() ([]byte, []int)

Deprecated: Use HandshakeReply.ProtoReflect.Descriptor instead.

func (*HandshakeReply) GetAccepted

func (x *HandshakeReply) GetAccepted() bool

func (*HandshakeReply) GetDirectives

func (x *HandshakeReply) GetDirectives() []*DirectiveSpec

func (*HandshakeReply) GetFeatures

func (x *HandshakeReply) GetFeatures() *Features

func (*HandshakeReply) GetName

func (x *HandshakeReply) GetName() string

func (*HandshakeReply) GetRefusal

func (x *HandshakeReply) GetRefusal() string

func (*HandshakeReply) GetVersion

func (x *HandshakeReply) GetVersion() string

func (*HandshakeReply) ProtoMessage

func (*HandshakeReply) ProtoMessage()

func (*HandshakeReply) ProtoReflect

func (x *HandshakeReply) ProtoReflect() protoreflect.Message

func (*HandshakeReply) Reset

func (x *HandshakeReply) Reset()

func (*HandshakeReply) String

func (x *HandshakeReply) String() string

type Request

type Request struct {

	// target names the block being served. The model carries directives for
	// every target block in it, each tagged with the block it came from, so
	// a backend keeps the ones matching this name.
	Target string `protobuf:"bytes,1,opt,name=target" json:"target,omitempty"`
	// model is one package, with the prelude's declarations merged into it.
	// See docs/design/plugins.md for what that means for a backend.
	Model *ir.Model `protobuf:"bytes,2,opt,name=model" json:"model,omitempty"`
	// out is the directory the response's paths are relative to.
	Out string `protobuf:"bytes,3,opt,name=out" json:"out,omitempty"`
	// dry_run says tdl will diff the response against disk rather than
	// write it. A backend that would do expensive work it knows cannot
	// affect the answer may skip it; one that ignores the flag is correct,
	// only slower.
	DryRun bool `protobuf:"varint,4,opt,name=dry_run,json=dryRun" json:"dry_run,omitempty"`
	// contains filtered or unexported fields
}

Request is everything a backend needs for one generation. Nothing travels in argv or environment variables.

func (*Request) Descriptor deprecated

func (*Request) Descriptor() ([]byte, []int)

Deprecated: Use Request.ProtoReflect.Descriptor instead.

func (*Request) GetDryRun

func (x *Request) GetDryRun() bool

func (*Request) GetModel

func (x *Request) GetModel() *ir.Model

func (*Request) GetOut

func (x *Request) GetOut() string

func (*Request) GetTarget

func (x *Request) GetTarget() string

func (*Request) ProtoMessage

func (*Request) ProtoMessage()

func (*Request) ProtoReflect

func (x *Request) ProtoReflect() protoreflect.Message

func (*Request) Reset

func (x *Request) Reset()

func (*Request) String

func (x *Request) String() string

type Response

type Response struct {
	Files []*File `protobuf:"bytes,1,rep,name=files" json:"files,omitempty"`
	// post names commands to run over the written files, by name only. The
	// project decides what a name maps to; a name it has not declared is
	// skipped with a warning.
	Post        []string      `protobuf:"bytes,2,rep,name=post" json:"post,omitempty"`
	Diagnostics []*Diagnostic `protobuf:"bytes,3,rep,name=diagnostics" json:"diagnostics,omitempty"`
	// contains filtered or unexported fields
}

Response is what a backend returns. It returns file contents rather than writing files, which is what lets tdl enforce path confinement, --verify, and --clean rather than asking every backend to honour them.

func (*Response) Descriptor deprecated

func (*Response) Descriptor() ([]byte, []int)

Deprecated: Use Response.ProtoReflect.Descriptor instead.

func (*Response) GetDiagnostics

func (x *Response) GetDiagnostics() []*Diagnostic

func (*Response) GetFiles

func (x *Response) GetFiles() []*File

func (*Response) GetPost

func (x *Response) GetPost() []string

func (*Response) ProtoMessage

func (*Response) ProtoMessage()

func (*Response) ProtoReflect

func (x *Response) ProtoReflect() protoreflect.Message

func (*Response) Reset

func (x *Response) Reset()

func (*Response) String

func (x *Response) String() string

type Severity

type Severity int32

Severity is how much a diagnostic matters.

const (
	Severity_SEVERITY_UNSPECIFIED Severity = 0
	Severity_SEVERITY_ERROR       Severity = 1
	Severity_SEVERITY_WARNING     Severity = 2
)

func (Severity) Descriptor

func (Severity) Descriptor() protoreflect.EnumDescriptor

func (Severity) Enum

func (x Severity) Enum() *Severity

func (Severity) EnumDescriptor deprecated

func (Severity) EnumDescriptor() ([]byte, []int)

Deprecated: Use Severity.Descriptor instead.

func (Severity) Number

func (x Severity) Number() protoreflect.EnumNumber

func (Severity) String

func (x Severity) String() string

func (Severity) Type

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL