plugin

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Oct 6, 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. 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.

What a request contains

The prelude's declarations (string, List, Option, and the rest) are in the model untagged; filter by the filename in a declaration's position.

A node carries the directives of every target block, each naming its target; keep your own with Directives. A directive expanded from a class names that class.

`tdl ir --format json` shows what a model contains.

Returning files

A backend returns contents, and tdl writes 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, not in an error from Backend.Generate.

What a plugin will not see

The declaration-level directives of a dependency's target blocks; only their block-scope directives arrive, on the dependency's import. 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, refusing a version it does not support. Conn is the codec.

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 complete backend: it declares what it understands and turns
// a model into files.
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()

		// Keep only this target's directives.
		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.

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 over it is refused before anything is allocated.

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 in all that belong to target. A node carries the directives of every target block in the model.

func Serve

func Serve(b Backend) error

Serve runs a backend over stdin and stdout, reading requests until the stream ends. A plugin's main is this and nothing else.

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 directives it
	// understands. tdl checks a target block against them before
	// generating.
	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.
	Generate(ctx context.Context, req *Request) (*Response, error)
}

Backend turns a resolved model into files, in process or served as a plugin by Serve.

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.

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; one that ends part way through a message returns io.ErrUnexpectedEOF.

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.

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, because an ID alone does not say which table it indexes.

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. Otherwise they are
	// 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 in a target block that no spec declares is a warning and is passed through anyway.

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.
	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.
	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 instead of ignoring fields it does not know and emitting wrong code.

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. It should
	// name both versions.
	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.
	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. A backend keeps the directives
	// tagged with 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.
	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 may skip work that cannot affect the files; ignoring
	// the flag is correct.
	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: file contents, which tdl writes, so tdl enforces path confinement, --verify, and --clean.

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