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 ¶
Units, because ir defers them and a model using one does not lower. 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 ¶
- Constants
- Variables
- func Directives(target string, all []*ir.Directive) []*ir.Directive
- func Serve(b Backend) error
- func ServeConn(ctx context.Context, b Backend, conn *Conn) error
- type Backend
- type Conn
- type Description
- type Diagnostic
- func (*Diagnostic) Descriptor() ([]byte, []int)deprecated
- func (x *Diagnostic) GetMessage() string
- func (x *Diagnostic) GetPosition() *ir.Position
- func (x *Diagnostic) GetSeverity() Severity
- func (*Diagnostic) ProtoMessage()
- func (x *Diagnostic) ProtoReflect() protoreflect.Message
- func (x *Diagnostic) Reset()
- func (x *Diagnostic) String() string
- type DirectiveSpec
- func (*DirectiveSpec) Descriptor() ([]byte, []int)deprecated
- func (x *DirectiveSpec) GetArgKinds() []ir.LiteralKind
- func (x *DirectiveSpec) GetMaxArgs() int32
- func (x *DirectiveSpec) GetMinArgs() int32
- func (x *DirectiveSpec) GetName() string
- func (*DirectiveSpec) ProtoMessage()
- func (x *DirectiveSpec) ProtoReflect() protoreflect.Message
- func (x *DirectiveSpec) Reset()
- func (x *DirectiveSpec) String() string
- type Features
- type File
- type Handshake
- func (*Handshake) Descriptor() ([]byte, []int)deprecated
- func (x *Handshake) GetFramingVersion() int32
- func (x *Handshake) GetIrVersion() string
- func (x *Handshake) GetWatch() bool
- func (*Handshake) ProtoMessage()
- func (x *Handshake) ProtoReflect() protoreflect.Message
- func (x *Handshake) Reset()
- func (x *Handshake) String() string
- type HandshakeReply
- func (*HandshakeReply) Descriptor() ([]byte, []int)deprecated
- func (x *HandshakeReply) GetAccepted() bool
- func (x *HandshakeReply) GetDirectives() []*DirectiveSpec
- func (x *HandshakeReply) GetFeatures() *Features
- func (x *HandshakeReply) GetName() string
- func (x *HandshakeReply) GetRefusal() string
- func (x *HandshakeReply) GetVersion() string
- func (*HandshakeReply) ProtoMessage()
- func (x *HandshakeReply) ProtoReflect() protoreflect.Message
- func (x *HandshakeReply) Reset()
- func (x *HandshakeReply) String() string
- type Request
- func (*Request) Descriptor() ([]byte, []int)deprecated
- func (x *Request) GetDryRun() bool
- func (x *Request) GetModel() *ir.Model
- func (x *Request) GetOut() string
- func (x *Request) GetTarget() string
- func (*Request) ProtoMessage()
- func (x *Request) ProtoReflect() protoreflect.Message
- func (x *Request) Reset()
- func (x *Request) String() string
- type Response
- func (*Response) Descriptor() ([]byte, []int)deprecated
- func (x *Response) GetDiagnostics() []*Diagnostic
- func (x *Response) GetFiles() []*File
- func (x *Response) GetPost() []string
- func (*Response) ProtoMessage()
- func (x *Response) ProtoReflect() protoreflect.Message
- func (x *Response) Reset()
- func (x *Response) String() string
- type Severity
Examples ¶
Constants ¶
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.
const IRVersion = "tdl.ir.v1"
IRVersion is the protobuf package the model is encoded with.
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 ¶
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.
var ErrTooLarge = errors.New("plugin: message exceeds the maximum size")
ErrTooLarge is returned when a length prefix exceeds MaxMessageSize.
var File_tdl_plugin_v1_plugin_proto protoreflect.FileDescriptor
Functions ¶
func Directives ¶
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.
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 (*Conn) Recv ¶
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.
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"`
// 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) 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) ProtoMessage ¶
func (*Features) ProtoMessage()
func (*Features) ProtoReflect ¶
func (x *Features) ProtoReflect() protoreflect.Message
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) GetContent ¶
func (*File) ProtoMessage ¶
func (*File) ProtoMessage()
func (*File) ProtoReflect ¶
func (x *File) ProtoReflect() protoreflect.Message
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) GetFramingVersion ¶
func (*Handshake) GetIrVersion ¶
func (*Handshake) ProtoMessage ¶
func (*Handshake) ProtoMessage()
func (*Handshake) ProtoReflect ¶
func (x *Handshake) ProtoReflect() protoreflect.Message
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) ProtoMessage ¶
func (*Request) ProtoMessage()
func (*Request) ProtoReflect ¶
func (x *Request) ProtoReflect() protoreflect.Message
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) GetDiagnostics ¶
func (x *Response) GetDiagnostics() []*Diagnostic
func (*Response) ProtoMessage ¶
func (*Response) ProtoMessage()
func (*Response) ProtoReflect ¶
func (x *Response) ProtoReflect() protoreflect.Message
type Severity ¶
type Severity int32
Severity is how much a diagnostic matters.
func (Severity) Descriptor ¶
func (Severity) Descriptor() protoreflect.EnumDescriptor
func (Severity) EnumDescriptor
deprecated
func (Severity) Number ¶
func (x Severity) Number() protoreflect.EnumNumber
func (Severity) Type ¶
func (Severity) Type() protoreflect.EnumType