Documentation
¶
Overview ¶
Package plugin is the contract between a Pacenote server and a plugin.
It holds the interface and nothing else: no host, no implementation, no database, no HTTP. A plugin author depends on this module and never on a server, which is the point — the same plugin runs on the open community edition and on the closed enterprise one, because both are only hosts implementing what is here.
The module is licensed Apache-2.0 while the community server is GPL-3, and that is deliberate rather than an oversight. A plugin is a separate process talking to the server over a local channel, so it is not linked into the server and is not a derivative work of it; depending on an Apache-2.0 interface module leaves a plugin author free to pick any licence, including a closed commercial one.
What a plugin can do ¶
Five things, which are the five a demanding plugin — a coach that turns telemetry into a sentence a driver hears mid-corner — actually needs:
- Be told something happened. Event, fire and forget.
- Be asked for something, with the host waiting. Request and Response.
- Be lent one of the operator's credentials for a call. Secret.
- Report what a call cost. Usage. The daily cap belongs to the core.
- Declare what the operator must configure. Setting.
Nothing else is here yet. Serving an endpoint, adding a page to the panel and enriching data on the way in are in the plan and are not in version 1, because an extension point with no user is a guess.
The governing rule ¶
The plugin receives facts, never traces. Every number in LapFacts and StintFacts was calculated exactly before the plugin saw it — apex speeds, corner deficits, consistency, fuel per lap. A plugin narrates; it does not compute. That buys accuracy, because something that cannot do arithmetic cannot get it wrong, and it buys latency and cost, because a lap trace is three hundred samples and the facts are a dozen numbers.
Writing one ¶
A plugin is a program with two files: a binary and a Manifest beside it, in a directory named after the plugin, under the server's plugin directory.
pacenote-data/plugins/loudmouth/plugin.json pacenote-data/plugins/loudmouth/loudmouth
The manifest says what it is and what it was built against:
{
"name": "loudmouth",
"version": "1.0.0",
"author": "Someone",
"description": "Says something in the team channel when a driver sets a personal best.",
"interface_version": 1,
"capabilities": {
"events": ["lap.completed"],
"network": true,
"reads_driver_data": true
}
}
The program implements Plugin and calls Serve:
package main
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/pacenote-sim/plugin"
)
type loudmouth struct{}
// Settings is what the operator fills in, rendered by the panel.
func (loudmouth) Settings(context.Context) ([]plugin.Setting, error) {
return []plugin.Setting{{
Name: "webhook",
Label: "Channel webhook",
Help: "The address the message is posted to. Create it in your chat service's channel settings.",
Kind: plugin.KindSecret,
Required: true,
}, {
Name: "only_personal_bests",
Label: "Only personal bests",
Help: "Off means every clean lap, which is a lot of messages on a full grid.",
Kind: plugin.KindBool,
Default: "true",
}}, nil
}
// Notify is told a lap was completed. It does not have to be quick: the
// server dispatched it and carried on.
func (loudmouth) Notify(ctx context.Context, e plugin.Event) (plugin.Usage, error) {
if e.Kind != plugin.EventLapCompleted {
return plugin.Usage{}, nil
}
if e.Settings.Bool("only_personal_bests") && !e.Lap.PersonalBest {
return plugin.Usage{}, nil
}
hook, ok := e.Secrets.Get("webhook")
if !ok {
return plugin.Usage{}, plugin.ErrNotConfigured
}
line := fmt.Sprintf("%s: %s at %s", e.Driver.Name, e.Lap.SpokenLap, e.Session.Track)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, hook.Value(), strings.NewReader(line))
if err != nil {
return plugin.Usage{}, err
}
res, err := http.DefaultClient.Do(req)
if err != nil {
return plugin.Usage{}, err
}
defer res.Body.Close()
// Nothing was bought from a vendor by the token, so nothing is metered.
return plugin.Usage{}, nil
}
// Answer is not something this plugin does, and its manifest says so.
func (loudmouth) Answer(context.Context, plugin.Request) (plugin.Response, error) {
return plugin.Response{}, plugin.ErrUnsupported
}
func main() { plugin.Serve(loudmouth{}) }
Note what is not in that program: no configuration file, no place a credential is stored, no decision about how much of the operator's money to spend. The core owns all three.
Rules a plugin author has to know ¶
Standard output belongs to the handshake until Serve has taken it over. Write one line to it before that and the host cannot talk to the plugin, and the error will not say why. Once serving, both streams are yours: the host captures the last of what a plugin prints and shows it to the operator when something goes wrong. Standard error is still the right place to write.
Deadlines are real. The host stops waiting when the deadline passes, whatever the plugin is doing, and reports the plugin as having missed it. Work that continues afterwards is work nobody reads.
Credentials are lent, not given. Secret renders as "[redacted]" when printed, logged or encoded, so the usual accidents cannot leak one. Reading the value with Secret.Value and then storing it somewhere is not an accident, and it is outside what this contract permits.
Every method may be called concurrently. Guard your own state.
Versioning ¶
InterfaceVersion is this contract's version. A host declares it, a plugin declares in its manifest the one it was built against, and they must match exactly. A mismatch refuses to start with a message naming both versions and what to do: there is no degraded mode, because a plugin quietly missing a fact is a coach telling a driver something wrong hours after anyone would connect it to an upgrade.
A worked example you can run ¶
examples/testplugin is a plugin that exercises all five capabilities and does nothing useful, which is exactly what a host needs to test against. It is worth reading before writing the first real one.
Index ¶
- Constants
- Variables
- func CheckVersion(name string, built, host int) error
- func ClientSet() goplugin.PluginSet
- func DatabaseURL() (string, error)
- func Serve(impl Plugin)
- func ValidName(s string) bool
- func ValidateSettings(settings []Setting) error
- type Access
- type Caller
- type Capabilities
- type CarSetup
- type Choice
- type Conditions
- type Corner
- type CornerPattern
- type Direction
- type Driver
- type Event
- type EventKind
- type FuelSummary
- type HTTPCapability
- type HTTPRequest
- type HTTPResponse
- type Kind
- type LapFacts
- type LapKind
- type Manifest
- type Plugin
- type Position
- type Request
- type RequestKind
- type Response
- type Route
- type Secret
- type Secrets
- type Server
- type Session
- type SessionType
- type Setting
- type SetupChange
- type SetupTyre
- type SetupValue
- type StintFacts
- type TyreSummary
- type Usage
- type Values
- type VersionError
- type Wheel
Constants ¶
const ( MagicCookieKey = "PACENOTE_PLUGIN" MagicCookieValue = "pacenote-plugin-v1-handshake" )
MagicCookieKey and MagicCookieValue are the handshake go-plugin performs before either side speaks. They are not security — anything that can run the plugin can read them out of this file — they are the check that stops a program started by mistake from being talked to as though it were a plugin.
const DispenseKey = "pacenote"
DispenseKey is the name the single plugin implementation is registered and dispensed under. There is one implementation per process on purpose: a process that serves two plugins cannot crash for one of them.
const EnvDatabaseURL = "PACENOTE_DATABASE_URL"
EnvDatabaseURL names the variable a plugin's own database arrives in.
A plugin that declared Capabilities.Database is started with this set and with nothing else from the server's environment. The connection string it carries is not the server's: it names a role created for this plugin alone, which owns one schema and may read the core_read views and nothing else. A plugin cannot read another plugin's tables, and cannot write the server's.
const InterfaceVersion = 1
InterfaceVersion is the version of this contract. A host declares it, a plugin declares the one it was built against in its manifest, and the two must match exactly.
Exactly, not "at least": a plugin built against a newer contract may expect facts an older host does not send, and one built against an older contract may ignore a field that now carries the meaning. Both are a coach saying something wrong to a driver at speed, hours after anyone would connect it to an upgrade. There is no degraded mode here on purpose.
const ManifestName = "plugin.json"
ManifestName is the file that declares a plugin, beside its binary. A directory under the host's plugin directory with one of these in it is a plugin; a directory without one is ignored and reported, never guessed at.
const MaxRoutes = 32
MaxRoutes is how many a plugin may declare. It is generous for anything with a reviewable surface and small enough that the list stays something a person reads rather than scrolls.
const MigrationsDir = "migrations"
MigrationsDir is the directory a plugin keeps its schema in, beside its manifest. The host applies every .sql file in it, in the order their names sort, once each.
There are no down migrations. Uninstalling a plugin drops its schema whole, which is the only rollback that is ever actually correct — a down migration that has to guess how to un-split a column is a way to lose data slowly.
const Redacted = "[redacted]"
Redacted is what a Secret renders as anywhere it can escape: a log line, an error, a formatted string, a JSON document. It is a fixed word rather than a length or a prefix, because "the first four characters of the key" is still four characters of the key.
Variables ¶
var ( // ErrInvalid is a malformed manifest, an unusable setting declaration, or a // value that does not fit the setting it was given for. It is the plugin // author's mistake or the operator's, never a transient failure, so a // caller must not retry it. ErrInvalid = errors.New("plugin: invalid") // ErrUnsupported is a plugin being asked for something it never said it // does — an event it did not declare, a request kind it does not answer. // The host avoids this by reading the manifest first; a plugin returns it // when the host asks anyway. ErrUnsupported = errors.New("plugin: not supported by this plugin") // ErrNoAnswer is a plugin declining to say anything. It is a normal // outcome and not a failure: a coach with nothing worth saying says // nothing, and the caller uses its own fallback. Returning an empty // response would be a lie the caller might speak aloud. ErrNoAnswer = errors.New("plugin: no answer") // ErrNotConfigured is a plugin that cannot work until the operator fills // something in — most often a credential. The caller falls back; the panel // shows the plugin as needing attention rather than as broken. ErrNotConfigured = errors.New("plugin: not configured") // ErrNoDatabase is [DatabaseURL] on a plugin that declared none. It is a // programming mistake rather than a runtime condition — a plugin that asks // for a database it did not declare — so a plugin should treat it as fatal // at startup rather than carrying on without one. ErrNoDatabase = errors.New("plugin: no database") )
The failures a host and a plugin both need to recognise. They are values rather than strings so that a caller can branch on them across the process boundary: the transport carries the sentinel in the error's text and rebuilds it on the other side.
var Handshake = goplugin.HandshakeConfig{ ProtocolVersion: InterfaceVersion, MagicCookieKey: MagicCookieKey, MagicCookieValue: MagicCookieValue, }
Handshake is what the host and the plugin exchange before either speaks. The magic cookie is not a secret and is not security; it is what makes a program started by mistake fail immediately and legibly instead of hanging.
ProtocolVersion is InterfaceVersion, so go-plugin refuses a mismatch as a backstop. The host checks the manifest first, because that check can name both versions and say what to do, and a handshake failure cannot.
Functions ¶
func CheckVersion ¶
CheckVersion compares what a plugin was built against with what a host speaks. A nil result is a plugin that may start.
func ClientSet ¶
ClientSet is the plugin set a host hands go-plugin. It is here rather than in the host so that both sides register the same thing from the same file.
func DatabaseURL ¶
DatabaseURL is the connection string for this plugin's own database, or ErrNoDatabase if it declared none.
It returns a string rather than an open pool so that a plugin may use whatever it likes — pgx, database/sql, sqlc, an ORM — and so that this module, which every plugin depends on including closed commercial ones, needs no database driver of its own.
Call it once at startup and keep the pool. It is stable for the life of the process; a host that changes it restarts the plugin.
func Serve ¶
func Serve(impl Plugin)
Serve runs impl as a plugin and blocks until the host closes the connection or the process is killed. It is the whole of a plugin's main:
func main() { plugin.Serve(&myPlugin{}) }
Nothing else may be written to standard output. The handshake is on it, and a stray fmt.Println breaks the connection before the host can say why. Standard error is free, and the host captures it: it is what the panel shows when a plugin fails to start.
func ValidName ¶
ValidName reports whether s is a name this contract accepts — for a plugin, for a setting, and for anything else the host has to put in a URL, a column or a role. Lowercase letters, digits, underscores and hyphens, starting with a letter.
It is exported because the host has to apply the same rule in places the plugin never sees: the address a plugin is mounted at, and the name of the PostgreSQL role it owns. Two spellings of one rule is one spelling too many.
func ValidateSettings ¶
ValidateSettings checks a whole declaration: every setting on its own, and no two of them sharing a name.
Types ¶
type Access ¶
type Access string
Access is what the host requires of somebody before it forwards a request. It is declared in the manifest, per plugin rather than per route: a plugin that needs two different answers serves two different things, and the one that is public is the one worth being explicit about.
const ( // AccessPublic forwards anything, to anybody. It is for a plugin that // publishes something a league wants public — a leaderboard, a results // page somebody links to from a forum. AccessPublic Access = "public" // AccessDriver forwards only where the host holds a driver session, and // answers everybody else itself. The plugin is told which driver. AccessDriver Access = "driver" // AccessAdmin forwards only where the host holds an administrator session. // It is for a plugin that adds something to the operator's own tools. AccessAdmin Access = "admin" // AccessCustom forwards everything and leaves the decision to the plugin. // // It is what a plugin that signs drivers in needs, because its own sign-in // page has to be reachable by somebody who is not signed in yet. It is also // the one an operator should read twice before installing: the host is // checking nothing, and what the plugin publishes is what the internet // gets. AccessCustom Access = "custom" )
type Caller ¶
type Caller struct {
// DriverSlug and DriverName are the signed-in driver, or empty for nobody.
// A driver is signed in only where the host holds a session for them, and
// the host is the only thing that can mint one.
DriverSlug string `json:"driver_slug,omitempty"`
DriverName string `json:"driver_name,omitempty"`
// AdminEmail is the signed-in operator, or empty.
AdminEmail string `json:"admin_email,omitempty"`
// Remote is where the request came from, as the host resolved it behind
// whatever proxy the operator runs.
Remote string `json:"remote,omitempty"`
}
Caller is what the host knows about whoever made a request. Every field is the host's own word: none of it comes from a header the caller can set, so a plugin can act on it.
type Capabilities ¶
type Capabilities struct {
// Events are the kinds this plugin wants delivered. The host sends these
// and no others.
Events []EventKind `json:"events,omitempty"`
// Requests are the kinds it will answer. The host refuses to ask for
// anything else rather than waiting out a deadline to find out.
Requests []RequestKind `json:"requests,omitempty"`
// Network reports that it calls something outside this machine. It is the
// declaration that matters most, because it is the one that turns the
// operator's data into somebody else's.
Network bool `json:"network"`
// WritesFiles reports that it writes to disk.
WritesFiles bool `json:"writes_files"`
// ReadsDriverData reports that it uses the driver's name and record rather
// than anonymous numbers.
ReadsDriverData bool `json:"reads_driver_data"`
// Database asks for tables of its own. The host creates a PostgreSQL role
// and a schema for this plugin alone, applies whatever is in its
// [MigrationsDir], and hands it the connection string in [EnvDatabaseURL].
//
// Unlike everything else here, this one is enforced. A plugin that does not
// declare it is given no database and no role exists for it; a plugin that
// does gets one it owns entirely and cannot reach out of. Uninstalling
// drops both, so a plugin's data leaves with the plugin.
Database bool `json:"database,omitempty"`
// HTTP asks for a route of this plugin's own, at /plugin/<name>/. A plugin
// that declares it must implement [Server]; one that does not is refused at
// install rather than left as a link in the panel that answers 500.
//
// Like [Capabilities.Database] this one is enforced. A plugin that does not
// declare it has no route and is never handed a request, so the surface a
// plugin adds to the operator's server is the surface it asked for in
// writing.
HTTP *HTTPCapability `json:"http,omitempty"`
}
Capabilities are what a plugin says it does, shown to the operator before they install it and again on its page afterwards.
Nothing here is enforced, and that is worth saying plainly rather than implying a declaration is a sandbox. A plugin is a process running with the server's privileges, and an operator who installs one is trusting its author. What the declaration buys is an informed decision and an honest page, which is not nothing: an integration that says it makes no network calls and then does is a plugin nobody will list again.
func (Capabilities) Answers ¶
func (c Capabilities) Answers(k RequestKind) bool
Answers reports whether this plugin says it answers the request.
func (Capabilities) Describe ¶
func (c Capabilities) Describe() []string
Describe is the capability list as an operator reads it, one sentence each. It is here rather than in the panel so that every host words it the same way.
func (Capabilities) Serves ¶
func (c Capabilities) Serves() bool
Serves reports whether this plugin asked for a route.
func (Capabilities) Validate ¶
func (c Capabilities) Validate() error
Validate refuses a capability list that names something this interface version does not carry, which is nearly always a plugin built against another version whose manifest was edited by hand.
func (Capabilities) Wants ¶
func (c Capabilities) Wants(k EventKind) bool
Wants reports whether this plugin asked for the event.
type CarSetup ¶
type CarSetup struct {
// UpdateCount is the simulator's own revision counter for the sheet, when
// it publishes one.
UpdateCount int `json:"update_count,omitempty"`
// Tyres is one entry per wheel the simulator published, in the order LF,
// RF, LR, RR.
Tyres []SetupTyre `json:"tyres,omitempty"`
// RearWing is the rear wing setting, for the cars that have one. It is
// lifted out of Values and does not appear there as well.
RearWing *SetupValue `json:"rear_wing,omitempty"`
// Values is every other setting of the sheet — aero, chassis, brakes,
// drivetrain, fuel — in the order the simulator published them.
//
// A setup sheet is car-specific: a GT3 car has a dive plane count and a
// stock car has a track bar. Those cannot be a fixed Go struct without
// this interface releasing a version per car, so they arrive named rather
// than typed. It is still not a document to parse: every entry carries its
// group, its name and its number.
Values []SetupValue `json:"values,omitempty"`
}
CarSetup is the car the driver actually drove, as the simulator published it.
It is what turns setup advice from "does it understeer?" into "your inner fronts ran twelve degrees hotter than the outers". A plugin answering RequestSetup is expected to reach for the measurements first and for the driver's own description second.
It is absent more often than not, and absent is normal: a simulator that publishes no setup and a series that locks the setup away look the same from here, and neither is a failure. A plugin must say nothing about the setup rather than guess at one.
func (CarSetup) TyreAt ¶
TyreAt returns the setup of one wheel and whether the simulator published it.
func (CarSetup) Value ¶
func (s CarSetup) Value(name string) (SetupValue, bool)
Value returns the named setting and whether the sheet carries it. The name is the simulator's own spelling and the match is exact, because a setup sheet names two different things "ToeIn" in two different groups and guessing which was meant is worse than answering nothing.
type Choice ¶
type Choice struct {
// Value is what is stored. It is stable: renaming a label is cosmetic,
// renaming a value orphans what the operator already chose.
Value string `json:"value"`
// Label is what the operator reads.
Label string `json:"label"`
// Note is the half-line under the label, for the difference between two
// options that the labels alone do not make obvious. It may be empty.
Note string `json:"note,omitempty"`
}
Choice is one option of a KindChoice setting.
type Conditions ¶
type Conditions struct {
// Skies is the simulator's enum, 0 clear to 3 overcast, and Wetness its
// own, 0 unknown to 7 very wet.
Skies int `json:"skies"`
Wetness int `json:"wetness"`
// WindKmh is km/h, Humidity a percentage, and the temperatures Celsius.
WindKmh float64 `json:"wind_kmh"`
Humidity float64 `json:"humidity"`
TrackTempC float64 `json:"track_temp_c"`
AirTempC float64 `json:"air_temp_c"`
}
Conditions are the session-mean weather values.
type Corner ¶
type Corner struct {
// Turn is the corner's number on this lap, from 1, in the order driven. A
// cue names it as "Turn 4" and never invents one that is not in this list.
Turn int `json:"turn"`
// ApexPct is where the apex is, in ‰ of the lap (0…1000). It is what tells
// two corners of the same number on two laps apart, and what orders a list
// of them along the track.
ApexPct int `json:"apex_pct"`
// ApexKmh is the minimum speed through the corner, and ReferenceApexKmh
// the same number on the reference lap. Zero for the reference means there
// was none to compare with.
ApexKmh int `json:"apex_kmh"`
ReferenceApexKmh int `json:"ref_apex_kmh,omitempty"`
// DeficitKmh is how much apex speed this corner lost against the
// reference, in whole km/h, and it is positive: the corners that gained
// time are not sent.
//
// It is a speed and not a time. Apex speed is what the client measures; a
// time lost per corner would be an integration over a piece of track the
// two laps did not cover at the same points, and a plugin told
// "milliseconds" would repeat an estimate as a measurement.
DeficitKmh int `json:"deficit_kmh"`
// BrakeAtApex is the brake still applied at the apex, in percent, and
// ThrottleLag the distance from the apex to the throttle pickup in ‰ of
// the lap. They are the two numbers Pattern is read from, and they are
// here so that a cue can carry the evidence rather than only the verdict.
BrakeAtApex int `json:"brake_at_apex,omitempty"`
ThrottleLag int `json:"throttle_lag,omitempty"`
// Pattern is the shape of it, when the detector could name one.
Pattern CornerPattern `json:"pattern,omitempty"`
}
Corner is where a lap was lost, one turn at a time.
Turn is the detector's own numbering and not the circuit's. A lap's corners are numbered from 1 in the order they are driven, counting only the ones the detector found: a turn taken flat and one whose speed drop is below the detector's threshold are not in the list, and every later corner shifts down by one when they are dropped. It is what the client already speaks aloud ("Turn 4"), so it is what a written cue must say too — and a plugin that maps it onto a circuit's published turn table is inventing a number nobody can check.
Every speed here is whole km/h, because the channel it is measured from is.
type CornerPattern ¶
type CornerPattern string
CornerPattern is the shape of a mistake, named rather than described so that a plugin can branch on it and a prompt can be written against a fixed vocabulary.
const ( // PatternEarlyApex is a corner still being slowed at its apex that the // driver then waits to get back on the power in: the car was turned in // before the corner arrived. PatternEarlyApex CornerPattern = "early_apex" // PatternLateBraking is a corner whose apex still carries brake pressure, // with the throttle picked up normally afterwards. PatternLateBraking CornerPattern = "late_braking" // PatternSlowExit is a corner the driver is off the brakes in but waits a // long way past the apex before picking the throttle up. PatternSlowExit CornerPattern = "slow_exit" )
The patterns the corner detector names.
There are three, and there are three because three is what a detector can measure from the driver's own pedals at the apex of a corner. A pattern that would have to be guessed at — a lift the trace cannot tell from a short-shift, a late apex nothing in the data distinguishes from a good one — is not here, because a fact set is not a place to put an inference and call it measured.
An empty pattern is a corner that was slower with nothing conclusive about why. It is a real answer, it is the commonest one, and it usually means the right thing to say is the deficit and no diagnosis.
type Direction ¶
type Direction string
Direction is which way a setup change goes.
const ( DirectionMore Direction = "more" DirectionLess Direction = "less" DirectionStiffer Direction = "stiffer" DirectionSofter Direction = "softer" DirectionHigher Direction = "higher" DirectionLower Direction = "lower" DirectionForward Direction = "forward" DirectionRearward Direction = "rearward" )
The directions a change can take. They are named rather than signed numbers because "stiffer" and "more" are what an operator reads in the panel, and a plugin that returns +1 is asking a human to remember a convention.
type Driver ¶
type Driver struct {
// ID is this server's own identifier for the driver. It is stable and it
// is the right key for a plugin's own cache.
ID int64 `json:"id"`
// Slug is the short, URL-safe name.
Slug string `json:"slug"`
// Name is what the driver calls themselves, and what a cue should use.
Name string `json:"name,omitempty"`
// Team is the team name, empty when they are not in one.
Team string `json:"team,omitempty"`
}
Driver is who was at the wheel.
A plugin that does not declare Capabilities.ReadsDriverData still receives this, because a coach with no idea who it is coaching cannot say "you". What the declaration is for is the operator's judgement about where that name is about to be sent.
type Event ¶
type Event struct {
// ID is unique to this delivery. It is the key to deduplicate on: a host
// that restarts mid-dispatch may send the same event twice, and a plugin
// that posts to a chat room should not post twice.
ID string `json:"id"`
// Kind is what happened.
Kind EventKind `json:"kind"`
// At is when the host dispatched it, not when the lap was driven — that is
// in the facts.
At time.Time `json:"at"`
// Driver and Session are who and where.
Driver Driver `json:"driver"`
Session Session `json:"session"`
// Lap is set when Kind is [EventLapCompleted], and nil otherwise.
Lap *LapFacts `json:"lap,omitempty"`
// Stint is set when Kind is [EventStintFinished], and nil otherwise.
Stint *StintFacts `json:"stint,omitempty"`
// Settings are the operator's answers for this plugin, already validated
// against what it declared and with the defaults filled in. Credentials are
// not here; they are in [Event.Secrets].
Settings Values `json:"-"`
// Secrets are the credentials this plugin declared, lent for this call.
Secrets Secrets `json:"-"`
// TokenCeiling is the most this call may spend, or zero for no ceiling
// beyond the operator's daily cap. It is the core rationing its own money:
// a plugin refuses before it sends rather than truncating afterwards.
TokenCeiling int `json:"token_ceiling,omitempty"`
}
Event is the host telling a plugin that something happened.
It is fire and forget from the server's side: the host dispatches it, does not block on it, and carries on. A plugin that is slow, wedged or dead delays nothing and fails nothing — an upload must never wait on an integration. The plugin still answers, because the answer carries what the event cost, and a plugin that spends the operator's tokens on an event has to be metered like anything else.
Handle an event that is not yours by returning the zero Usage and no error. The host only sends the kinds a plugin declared, but a manifest can be edited.
type EventKind ¶
type EventKind string
EventKind names something that happened.
const ( // EventLapCompleted is one lap, stored and reduced to facts. It arrives // once per lap per driver, which on a full grid is the highest rate // anything here runs at. EventLapCompleted EventKind = "lap.completed" // EventStintFinished is a stint closed by its final summary. It carries // the whole-stint facts a debrief is written from. EventStintFinished EventKind = "stint.finished" )
The events version 1 carries. Two, because two is what the first demanding plugin needed, and an event nothing consumes is a payload to maintain for nobody. More will be added when a plugin asks.
type FuelSummary ¶
type FuelSummary struct {
UsedL float64 `json:"used_l"`
RemainingL float64 `json:"remaining_l"`
// PerLapL is the average over the counted laps, which is the number a
// strategy is built on.
PerLapL float64 `json:"per_lap_l,omitempty"`
}
FuelSummary is what the car drank over a stint, in litres.
type HTTPCapability ¶
type HTTPCapability struct {
// Title is what the panel calls the link to this plugin's pages. Empty
// means the plugin's own name.
Title string `json:"title,omitempty"`
// Routes is every address this plugin serves. A plugin that declares none
// is refused: asking for a URL without saying which is asking for all of
// them.
Routes []Route `json:"routes"`
}
HTTPCapability is what a plugin declares in its manifest to be given routes.
func (HTTPCapability) Describe ¶
func (h HTTPCapability) Describe() []string
Describe is the route table in words, for the page an operator reads before they enable a plugin. Most plugins are never reviewed by anybody: this is what stands in for it.
func (HTTPCapability) For ¶
func (h HTTPCapability) For(path string) (Access, bool)
For is the access required at a path, and whether this plugin serves it at all. The longest route that covers the path wins.
func (HTTPCapability) Validate ¶
func (h HTTPCapability) Validate() error
Validate reports what is wrong with a declaration.
type HTTPRequest ¶
type HTTPRequest struct {
// Method is the HTTP method.
Method string `json:"method"`
// Path is what follows the plugin's own prefix, always beginning with a
// slash. A plugin routes on this.
Path string `json:"path"`
// Query is the raw query string, without the question mark.
Query string `json:"query,omitempty"`
// Header is the request's headers, minus the host's own: no cookie of the
// host's reaches a plugin, and a plugin sees only cookies it set itself.
Header http.Header `json:"header,omitempty"`
// Body is the request body, already capped by the host.
Body []byte `json:"body,omitempty"`
// Caller is who made it.
Caller Caller `json:"caller"`
// Prefix is where this plugin is mounted — "/plugin/results" — so that it
// can build links and form actions that come back to itself.
Prefix string `json:"prefix"`
// BaseURL is the operator's public address, for the links a plugin sends
// somewhere else and expects a browser back from.
BaseURL string `json:"base_url"`
// Settings and Secrets are this plugin's configuration, on the same terms
// as every other call.
Settings Values `json:"settings,omitempty"`
Secrets Secrets `json:"-"`
}
HTTPRequest is one request, forwarded whole.
func (HTTPRequest) URL ¶
func (r HTTPRequest) URL(path string) string
URL is the address this request arrived at, for a plugin building an absolute link back to one of its own pages.
type HTTPResponse ¶
type HTTPResponse struct {
// Status is the HTTP status code. Zero is 200.
Status int `json:"status,omitempty"`
// Header is what to write. The host refuses the headers that are its own
// to decide, and lets a plugin set only its own cookies.
Header http.Header `json:"header,omitempty"`
// Body is the response body.
Body []byte `json:"body,omitempty"`
// SignIn asks the host to establish a driver session for this browser, by
// the driver's slug.
//
// It is how a plugin that authenticates drivers hands the result back. The
// plugin says who; the host checks the driver exists, mints the session and
// sets the cookie. A plugin never sees a session token and cannot make one,
// so the worst a broken one can do is name the wrong driver — which is bad,
// and is not the same as forging sessions for a server it is not installed
// on.
SignIn string `json:"sign_in,omitempty"`
// SignOut ends whatever driver session this browser has.
SignOut bool `json:"sign_out,omitempty"`
// Usage is what serving the request cost, if anything.
Usage Usage `json:"usage,omitzero"`
}
HTTPResponse is what to answer with.
func Redirect ¶
func Redirect(status int, to string) HTTPResponse
Redirect sends the browser somewhere else.
func Text ¶
func Text(status int, body string) HTTPResponse
Text is a plain-text answer, for the plugin that wants one line rather than a template.
type Kind ¶
type Kind string
Kind is what a setting holds, which is what the panel renders it as.
const ( // KindText is one line of text. KindText Kind = "text" // KindSecret is a credential. It is written once and never read back: the // core seals it with the data key, the panel shows that there is one, and // the value reaches the plugin only in [Request.Secrets] or // [Event.Secrets], at call time. KindSecret Kind = "secret" // KindNumber is a whole number. KindNumber Kind = "number" // KindBool is a switch. Its value is "true" or "false". KindBool Kind = "bool" // KindChoice is one of [Setting.Choices]. KindChoice Kind = "choice" )
The five kinds. There are five rather than fifteen because every one of them is a control an operator has to understand without documentation, and because a kind the panel cannot draw is a setting nobody can fill in.
type LapFacts ¶
type LapFacts struct {
// Number is the simulator's lap counter and LapMs the lap time.
Number int `json:"number"`
LapMs int `json:"lap_ms"`
// Kind is how the lap counts.
Kind LapKind `json:"kind"`
// DeltaMs is the lap time against [LapFacts.Reference], in milliseconds:
// positive is slower. It means nothing when Reference is empty.
DeltaMs int `json:"delta_ms,omitempty"`
// Reference names what the delta is against, in words a driver would use —
// "your best lap", "the class best". Empty means there was nothing to
// compare against, which happens on a circuit nobody has driven here yet
// and is not a failure.
Reference string `json:"reference,omitempty"`
// PersonalBest reports that this is the driver's best lap here.
PersonalBest bool `json:"personal_best,omitempty"`
// Corners are where the time went, worst first. It is empty when the
// client sent no corner detection, and a plugin must cope with that rather
// than inventing a turn number.
Corners []Corner `json:"corners,omitempty"`
// Position is the race picture, absent outside a race.
Position *Position `json:"position,omitempty"`
// SpokenLap is the lap time already rendered into speakable words — "one
// minute 31.2 seconds". It exists because a speech engine reading "91240"
// produces something nobody wants in their ear, and the client can render
// it correctly while a model cannot be trusted to.
SpokenLap string `json:"spoken_lap,omitempty"`
// StartedAt is when the lap began.
StartedAt time.Time `json:"started_at"`
}
LapFacts is one completed lap, reduced to what can be said about it.
type Manifest ¶
type Manifest struct {
// Name identifies the plugin everywhere — the directory, the settings, the
// metering rows, the panel. Lowercase letters, digits, underscores and
// hyphens, and permanent: renaming one is installing a different plugin.
Name string `json:"name"`
// Version is the plugin's own version, for the operator and the
// marketplace. Its spelling is the author's business.
Version string `json:"version"`
// Author is who to blame, and who the operator is trusting.
Author string `json:"author"`
// Description is one line of what it does, shown in the panel.
Description string `json:"description"`
// InterfaceVersion is the version of this contract the plugin was built
// against. It must equal the host's [InterfaceVersion] exactly; see
// [CheckVersion].
InterfaceVersion int `json:"interface_version"`
// Binary is the executable to run, relative to the manifest and with no
// directory separators in it. Empty means [Manifest.Name]; on Windows a
// ".exe" is added when it is not already there.
Binary string `json:"binary,omitempty"`
// Capabilities are what it does.
Capabilities Capabilities `json:"capabilities"`
}
Manifest is the file beside the binary: what this plugin is, what it was built against, and what it does.
func LoadManifest ¶
LoadManifest reads the manifest in dir.
func ParseManifest ¶
ParseManifest reads a manifest and checks it. An unknown field is refused rather than ignored: a manifest with "capabilitys" in it is a plugin that will start and then quietly receive nothing, and the author needs to be told at the point they can still fix it.
func (Manifest) Executable ¶
Executable is the file to run, given the directory the manifest was read from. It is the manifest's binary, or the plugin's name, with the platform's extension.
type Plugin ¶
type Plugin interface {
// Settings declares what the operator has to configure. The host asks once
// when the plugin starts, stores the answer, and renders the form from it.
//
// It must not depend on configuration — a fresh installation has none — and
// it must be stable: a setting that appears and disappears between calls is
// a form that loses what the operator typed.
Settings(ctx context.Context) ([]Setting, error)
// Notify is told that something happened. The host does not wait for this
// before carrying on with its own work, so taking a while here costs
// nothing but the plugin's own timeliness.
//
// The returned [Usage] is what the event cost. Return the zero value when
// it cost nothing. An error is logged against the plugin and nothing else:
// there is no retry, because an event that matters enough to retry is a
// request and should be one.
Notify(ctx context.Context, e Event) (Usage, error)
// Answer is asked for something, with the host waiting. Return [ErrNoAnswer]
// when there is nothing worth saying — that is a normal outcome and the
// caller has its own fallback — and [ErrNotConfigured] when the operator
// has not filled something in.
//
// Missing the deadline is the one unforgivable failure: the caller is a
// driver at speed, and an answer that arrives after the corner is worse
// than no answer. Watch ctx.Done and give up.
Answer(ctx context.Context, r Request) (Response, error)
}
Plugin is the whole contract. Implement these three methods, call Serve in main, put a Manifest beside the binary, and the host can run it.
Every method is called on its own goroutine and several may be in flight at once, so an implementation has to be safe for concurrent use. Every method is given a context with the host's deadline on it, and is expected to respect it: the host stops waiting when the deadline passes whatever the plugin does, so work that carries on afterwards is work nobody will read.
type Position ¶
type Position struct {
// ClassPos is the position in the driver's own class.
ClassPos int `json:"class_pos"`
// GapAheadMs and GapBehindMs are the gaps either side, in milliseconds.
// Zero means there is nobody there.
GapAheadMs int `json:"gap_ahead_ms,omitempty"`
GapBehindMs int `json:"gap_behind_ms,omitempty"`
}
Position is where the driver is in the race. It is absent outside a race, because a gap to the car ahead in a practice session is not a fact about anything.
type Request ¶
type Request struct {
// ID is unique to this call, for the plugin's own logging and cache.
ID string `json:"id"`
// Kind is what is wanted.
Kind RequestKind `json:"kind"`
// Deadline is when the host stops waiting. It is also the deadline on the
// context, and it is repeated here so that a plugin deciding between a
// fast model and a good one has the number without having to ask the
// context for it.
Deadline time.Time `json:"deadline"`
// Driver and Session are who and where.
Driver Driver `json:"driver"`
Session Session `json:"session"`
// Lap is the lap in question, for the two cue jobs.
Lap *LapFacts `json:"lap,omitempty"`
// Stint is the stint in question, for a debrief and for setup advice.
Stint *StintFacts `json:"stint,omitempty"`
// Symptom is what the car was doing, for [RequestSetup]. It is derived
// from the telemetry rather than asked of the driver — steering angle
// against lateral acceleration says whether the car turns as much as it is
// asked to — but a driver may override it, because they felt it and we did
// not. Empty means the plugin should derive its own from the facts.
Symptom string `json:"symptom,omitempty"`
// Text is what to say, for [RequestSpeak]. It is the line a coach already
// wrote and a validator already passed, so a plugin speaking it neither
// edits it nor decides whether it should be said.
//
// Voice is which voice to use, when the caller has a preference. It is the
// plugin's own identifier for one — a service's voice id — and an empty
// string is the operator's configured default.
Text string `json:"text,omitempty"`
Voice string `json:"voice,omitempty"`
// Settings, Secrets and TokenCeiling are as on [Event].
Settings Values `json:"-"`
Secrets Secrets `json:"-"`
TokenCeiling int `json:"token_ceiling,omitempty"`
}
Request is the host asking a plugin for something and waiting.
It is the other half of Event and the reason this interface is not just a notification bus: coaching returns a line the server stores and the client speaks, and setup advice returns changes the server files against a car and a circuit.
The deadline is real. It is on the context the plugin is called with, and a plugin that misses it is skipped, reported, and the caller told there was no answer so it can use its own fallback. Nothing is queued for a retry: a cue that arrives after the corner is worse than no cue.
type RequestKind ¶
type RequestKind string
RequestKind names something the host wants back.
const ( // RequestCueRace is one spoken line about position and the gaps either // side, under twelve words, wanted inside two seconds. RequestCueRace RequestKind = "cue.race" // RequestCueTraining is one spoken line about the corner the driver lost // the most in and one fix, under eighteen words, wanted inside two seconds. RequestCueTraining RequestKind = "cue.training" // RequestDebrief is a few hundred written words after a session, wanted // inside thirty. RequestDebrief RequestKind = "debrief" // RequestSetup is setup advice: a list of changes with a reason each. RequestSetup RequestKind = "setup" // RequestSpeak turns one line into audio a driver hears. It is the only // request whose answer is bytes rather than language, and it is the one // with the least room: the line was written because a corner is coming. // // The text is in [Request.Text] and the answer in [Response.Audio]. A // plugin answering this holds the credential for whatever service it uses; // the server relays what comes back and knows nothing about the vendor. RequestSpeak RequestKind = "speak" )
The requests version 1 carries: the four jobs a coaching plugin does, and one a voice plugin does.
func (RequestKind) Valid ¶
func (k RequestKind) Valid() bool
Valid reports whether k is a request this version defines.
type Response ¶
type Response struct {
// Kind echoes the request, so a caller holding several in flight can tell
// them apart without keeping the map.
Kind RequestKind `json:"kind"`
// Text is the answer for the jobs whose answer is language: a cue, a
// debrief. It is checked by the caller against the rules for that job
// before anything is spoken, because a prompt is a request and a validator
// is a guarantee.
Text string `json:"text,omitempty"`
// Changes are the answer for [RequestSetup].
Changes []SetupChange `json:"changes,omitempty"`
// Audio is the answer for [RequestSpeak], and AudioType its media type —
// "audio/wav" or "audio/mpeg". The server relays both to the client without
// decoding either, so a plugin may answer in whatever its service produces.
Audio []byte `json:"audio,omitempty"`
AudioType string `json:"audio_type,omitempty"`
// Usage is what the call cost. A plugin that leaves this zero is telling
// the core it spent nothing, and the core will believe it.
Usage Usage `json:"usage"`
// PromptVersion is the version of the prompt asset that produced this,
// recorded so a change in output can be traced to a change in prompt. It
// is free text and it may be empty.
PromptVersion string `json:"prompt_version,omitempty"`
}
Response is what a plugin gives back.
func (Response) Validate ¶
Validate refuses a response the caller cannot use. An answer with no content at all is ErrNoAnswer rather than an empty success: a caller that speaks what it is given must be able to tell "nothing to say" from "here is nothing".
type Route ¶
type Route struct {
// Path is the address, relative to the plugin's own prefix and beginning
// with a slash. "/" covers everything the plugin serves.
Path string `json:"path"`
// Access is what the host requires of a caller before it forwards.
Access Access `json:"access"`
// Reason is why this route decides for itself. It is required for
// [AccessCustom] and meaningless otherwise: that is the one mode where the
// host checks nothing, so it is the one an operator has to be told about
// in words before they enable the plugin.
Reason string `json:"reason,omitempty"`
}
Route is one address a plugin serves, and who may reach it.
A route covers itself and everything under it: "/webhook" covers "/webhook" and "/webhook/stripe", and does not cover "/webhooks". The longest route that covers a path decides it, and a path no route covers is not served at all.
That last rule is the one that makes a plugin reviewable. The declaration is not a promise the author makes about what their code does; it is a wall the host puts up in front of it. A path nobody thought about is refused rather than exposed, and somebody reading the manifest has read the whole of what this plugin puts on the operator's server.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret is one of the operator's credentials, lent to a plugin for the length of one call.
The value is unexported and there is exactly one way to read it, Secret.Value. Every other way a string usually escapes a program is closed: printing it gives Redacted, logging it gives Redacted, encoding it as JSON gives Redacted. A plugin author who logs the whole request they were handed — which is the first thing anybody does when a call misbehaves — cannot leak the operator's key by accident, and that is the entire reason this is a type and not a string.
The core holds the credential. A plugin is given it, uses it, and forgets it: storing one on disk or in a package variable is outside what this contract allows, and the operator's key is not the plugin's to keep.
func NewSecret ¶
NewSecret wraps a credential. Plugin authors do not usually call this — the host fills Request.Secrets and Event.Secrets — but a test that fakes a call needs it.
func (Secret) Empty ¶
Empty reports whether there is no credential here. It is the check to make before a call the operator has not configured a key for.
func (Secret) GoString ¶
GoString is Redacted, which covers the %#v that a debugging session reaches for when %v did not say enough.
func (Secret) LogValue ¶
LogValue is Redacted, so slog renders it that way whether it is logged on its own or reached through a struct.
func (Secret) MarshalJSON ¶
MarshalJSON is Redacted. Secrets are not carried inside any JSON document this contract defines — they travel in their own field — so this exists for the plugin author who encodes a request into their own log or cache.
func (*Secret) UnmarshalJSON ¶
UnmarshalJSON always fails. A Secret that came out of a JSON document would be one that was written into a JSON document, which is the thing this type exists to prevent.
type Secrets ¶
Secrets are the credentials for one call, keyed by the name of the setting that holds them.
A plugin receives the secret settings it declared itself and nothing else. There is no key here belonging to another plugin, and no way to ask for one.
type Server ¶
type Server interface {
// ServeHTTP answers one request. The deadline is on the context and the
// host answers for a plugin that misses it, so a slow page is a slow page
// and not a connection somebody else is waiting behind.
//
// It is named for [net/http.Handler] because that is the model — a request
// in, a response out, and nothing held between them — and to keep it apart
// from [Serve], which starts the process.
ServeHTTP(ctx context.Context, r HTTPRequest) (HTTPResponse, error)
}
Server is the optional half of the contract. A plugin implements it as well as Plugin to put pages or endpoints on the operator's server; one that does not simply does not implement it, and is never asked.
A plugin that declares http in its manifest and does not implement this is refused at install, because the alternative is an operator following a link from their own panel to a 500.
type Session ¶
type Session struct {
// StintID is the capture this belongs to, a UUID.
StintID string `json:"stint_id"`
// Sim is the simulator's own identifier.
Sim string `json:"sim"`
// Track is the display name and TrackID the simulator's stable identifier.
Track string `json:"track"`
TrackID string `json:"track_id"`
// Car is the car's display name and CarClass the class it runs in.
Car string `json:"car"`
CarClass string `json:"car_class,omitempty"`
// Type is what the session was.
Type SessionType `json:"type"`
// StartedAt is when the stint began.
StartedAt time.Time `json:"started_at"`
}
Session is the context every fact in an event belongs to: one car, one circuit, one simulator, one sitting.
The simulator is part of the identity and not decoration. Two simulators agree on "spa" and on "Ferrari 296 GT3" and disagree about the tyre model, the fuel burn and therefore the lap time, so a plugin that compares across them is comparing nothing.
type SessionType ¶
type SessionType string
SessionType is what the driver was doing. The spellings match the ones the client and the server already use on the wire.
const ( SessionPractice SessionType = "practice" SessionQualifying SessionType = "qualifying" SessionRace SessionType = "race" SessionTesting SessionType = "testing" )
The four session types.
type Setting ¶
type Setting struct {
// Name is the key the value is stored under and the key the plugin reads
// it back by. It is lowercase letters, digits and underscores, and it is
// permanent: changing it loses whatever the operator had set.
Name string `json:"name"`
// Label is the field's name in the panel, in sentence case, without a
// trailing colon.
Label string `json:"label"`
// Help is the line under the field. It is what an operator who has never
// seen this plugin needs in order to answer, and it is worth writing
// properly: it is the only documentation most of them will read.
Help string `json:"help,omitempty"`
// Kind is what it holds.
Kind Kind `json:"kind"`
// Required refuses an empty value. A plugin with an unfilled required
// setting is shown as needing attention and is not called.
Required bool `json:"required,omitempty"`
// Default is the value used when the operator has set none. It must be
// empty for [KindSecret]: a default credential is not a thing.
Default string `json:"default,omitempty"`
// Choices are the options of a [KindChoice] setting, in the order the
// panel shows them. Ignored for every other kind.
Choices []Choice `json:"choices,omitempty"`
// Placeholder is the grey text in an empty field, for the shape of the
// answer rather than an example that someone will paste verbatim.
Placeholder string `json:"placeholder,omitempty"`
}
Setting is one thing the operator has to fill in, declared by the plugin and rendered by the panel.
A plugin declares these rather than reading a configuration file of its own, so that an operator configures every plugin in the same place as everything else, and so that a credential is sealed by the core rather than left in a file beside a binary.
type SetupChange ¶
type SetupChange struct {
// Area is the part of the car — "front suspension", "differential".
Area string `json:"area"`
// Setting is the control on the setup sheet, spelled the way the
// simulator spells it, so a driver can find it.
Setting string `json:"setting"`
// Direction is which way to move it.
Direction Direction `json:"direction"`
// Amount is how far, in the units the sheet uses — "one click", "2 mm".
// It may be empty when the direction is the whole advice.
Amount string `json:"amount,omitempty"`
// Why is one line of reason, tied to a fact the plugin was given. A change
// with no why is a guess with a confident tone.
Why string `json:"why"`
}
SetupChange is one thing to change on the car.
type SetupTyre ¶
type SetupTyre struct {
// Wheel is which corner of the car this is.
Wheel Wheel `json:"wheel"`
// ColdKpa is the starting pressure on the setup sheet and HotKpa the
// pressure the tyre was last read at, both in kPa.
ColdKpa float64 `json:"cold_kpa,omitempty"`
HotKpa float64 `json:"hot_kpa,omitempty"`
// The last tread temperatures across the tyre, in °C, inner to outer.
TempInnerC float64 `json:"temp_inner_c,omitempty"`
TempMiddleC float64 `json:"temp_middle_c,omitempty"`
TempOuterC float64 `json:"temp_outer_c,omitempty"`
// The tread remaining across the tyre, in percent, inner to outer.
TreadInnerPct float64 `json:"tread_inner_pct,omitempty"`
TreadMiddlePct float64 `json:"tread_middle_pct,omitempty"`
TreadOuterPct float64 `json:"tread_outer_pct,omitempty"`
}
SetupTyre is one wheel of a CarSetup: what it was set to and what it came back at.
The three temperatures and the three tread depths run inner, middle, outer as the car stands, on both sides of the car, whichever order the simulator published them in. That is the point of them: the spread across a tread is the canonical camber measurement, and an inner that ran twelve degrees hotter than the outer is a car leaning on the wrong part of the contact patch — a measurement, not an opinion, and not something a driver has to be surveyed about. HotKpa against ColdKpa is the other one: it says exactly how far the cold setting has to move.
A field left at zero was not published. No real tyre is at zero kPa, zero degrees or zero tread.
type SetupValue ¶
type SetupValue struct {
// Group is the path the simulator published this under, slash-separated
// ("Chassis/LeftFront"), or empty.
Group string `json:"group,omitempty"`
// Name is the key, spelled the way the simulator spells it, so a driver
// can find the control on their setup screen. A plugin proposing a
// [SetupChange] should spell Setting the same way.
Name string `json:"name"`
// Text is the value as published.
Text string `json:"text"`
// Number is the leading number of Text, and Unit what followed it.
Number float64 `json:"number,omitempty"`
Unit string `json:"unit,omitempty"`
}
SetupValue is one named setting of a setup sheet.
Text is what the simulator printed and is authoritative: "-2.5 deg", "Soft", "9 hole". Number and Unit are that text read as a measurement where it is one. A setting whose text carries no number has Number zero and Unit empty, which is indistinguishable from a value that really is zero — Text is what separates the two, which is why it is always present.
type StintFacts ¶
type StintFacts struct {
// Laps is every lap driven and CleanLaps the ones that counted.
Laps int `json:"laps"`
CleanLaps int `json:"clean_laps"`
// Incidents is the simulator's own count.
Incidents int `json:"incidents"`
// BestLapMs and AvgLapMs are over the counted laps.
BestLapMs int `json:"best_lap_ms"`
AvgLapMs int `json:"avg_lap_ms"`
// ConsistencyPct is 0 to 100: how tightly the counted laps cluster. It is
// the single most useful number about a stint and the one a driver cannot
// feel.
ConsistencyPct int `json:"consistency_pct"`
// TopSpeedKmh is the fastest the car went.
TopSpeedKmh int `json:"top_speed_kmh"`
// Fuel and Tyres are what the car had left and how hard it was working.
Fuel FuelSummary `json:"fuel"`
Tyres TyreSummary `json:"tyres"`
// Setup is the car the driver drove, or nil when the simulator published
// none. It is on the stint and not on the lap because it is one per
// sitting. See [CarSetup] — nil is normal and a plugin must cope with it.
Setup *CarSetup `json:"setup,omitempty"`
// Conditions is the weather it was driven in.
Conditions Conditions `json:"conditions"`
// FinishedAt is when the stint ended.
FinishedAt time.Time `json:"finished_at"`
}
StintFacts is a finished stint, reduced the same way a lap is.
type TyreSummary ¶
type TyreSummary struct {
LF float64 `json:"lf"`
RF float64 `json:"rf"`
LR float64 `json:"lr"`
RR float64 `json:"rr"`
// SpreadC is the hottest minus the coldest.
SpreadC float64 `json:"spread_c"`
}
TyreSummary is the four corners at the end of the stint, in degrees Celsius, with the spread that says whether one corner of the car is working alone.
type Usage ¶
type Usage struct {
// Job is what the call was for. For a request it is the request kind; for
// an event a plugin names its own work. It is what the operator sees when
// they ask where the money went, so "cue.training" is useful and "call" is
// not.
Job string `json:"job,omitempty"`
// Model is the model the tokens were spent on, when there was one. It is
// free text because the plugin knows the vendor and the core does not.
Model string `json:"model,omitempty"`
// InputTokens and OutputTokens are counted the way the vendor counts them,
// so that the figure in the panel matches the figure on the bill.
InputTokens int64 `json:"input_tokens,omitempty"`
OutputTokens int64 `json:"output_tokens,omitempty"`
// Cached reports that the answer came from the plugin's own cache. It is
// recorded because a cache that is not measured is a cache nobody can tell
// is working.
Cached bool `json:"cached,omitempty"`
}
Usage is what one call cost the operator.
The plugin reports it; the core records it and enforces the daily cap. That direction is deliberate and it is the one thing in this contract worth being blunt about: **the cap belongs to the core**. A plugin deciding how much of somebody else's money to spend is backwards, so a plugin is told its ceiling for a call (Request.TokenCeiling), it reports what it actually used, and the decision to call it again is not its own.
A plugin that spends nothing — one that posts to a chat room, one that answered from its own cache — returns the zero value, and the core records nothing.
type Values ¶
Values are the operator's answers, keyed by Setting.Name.
Secrets are not here. A KindSecret setting's value reaches the plugin as a Secret in Request.Secrets or Event.Secrets, and what appears here for such a setting is nothing at all — which is what lets a plugin log its whole settings map while debugging.
func ValidateValues ¶
ValidateValues checks the operator's answers against the declaration and fills in the defaults. The result is what a plugin is given: every declared non-secret setting that has a value, and nothing that was not declared.
A value for a setting the plugin does not declare is dropped rather than refused. That is what makes an upgrade that removes a setting survivable: the row stays in the database until the operator saves the form again, and nothing breaks in the meantime.
func (Values) Bool ¶
Bool is the value of a KindBool setting. Anything that is not "true" is false, because a switch has no third position.
func (Values) Int ¶
Int is the value of a KindNumber setting. The second result is false when there is no value or it is not a number, which for a validated setting means the operator left it empty and the plugin declared no default.
type VersionError ¶
type VersionError struct {
// Plugin is the name from the manifest, for the message.
Plugin string
// Built is the interface version the plugin declared.
Built int
// Host is the interface version this host implements.
Host int
}
VersionError is a plugin and a host that were built against different contracts. It names both versions and what to do, because the operator reading it did not write either side.
func (*VersionError) Error ¶
func (e *VersionError) Error() string
Error is the sentence an operator sees.
func (*VersionError) Is ¶
func (e *VersionError) Is(target error) bool
Is reports that this is an ErrInvalid, so a caller that only wants to know "is this the operator's problem or ours" does not have to unwrap it.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
payments
command
payments is an example plugin that serves two addresses and needs a different answer at each: a webhook a payment provider posts to, and pages only the operator may open.
|
payments is an example plugin that serves two addresses and needs a different answer at each: a webhook a payment provider posts to, and pages only the operator may open. |
|
testplugin
command
Command testplugin is a plugin that exercises every part of the contract and does nothing useful.
|
Command testplugin is a plugin that exercises every part of the contract and does nothing useful. |
|
internal
|
|