Documentation
¶
Overview ¶
Package demoapi is a fake API to build TUIs against: applications that can be listed, filtered, paged and sorted, and jobs that run against them and finish after real time has passed.
It is a fixture. It has no authentication, it will fail or stall a request on request, and the binary binds loopback on purpose. Do not deploy it.
Why it is a handler ¶
New returns an http.Handler rather than running a server, which is what lets one fixture serve four uses:
srv := httptest.NewServer(demoapi.New(demoapi.Options{Seed: 7})) // tests
client := demoapi.Client(demoapi.New(demoapi.Options{})) // examples, no socket
go run ./demoapi/cmd/demoapi // a terminal
import "github.com/jsdrews/tuilib/demoapi" // another project
A main cannot be shared. A handler can be shared four ways.
Why it has no dependencies ¶
Standard library only, plus pkg/query from this module. The cost of a package here is not compile time — Go builds what is imported — it is go.mod: anything demoapi required, every consumer of tuilib would require. The day this needs a third-party import it should become a nested module.
Latency and failure are per-request ¶
Every endpoint honours ?latency=, ?fail= and ?flaky=. Per-request rather than server-wide because the orderings worth testing are the asymmetric ones: a slow reply to an abandoned filter arriving after a fast reply to the current one is what source.Deliver's generation check exists for, and one knob for the whole server cannot express it.
GET /apps?q=eu&latency=900ms // the abandoned query GET /apps?q=euro // what the user actually typed
The world advances on the clock ¶
A job started at T finishes at T+4s because four seconds passed, not because four requests arrived — so a curl in another terminal sees the same world a TUI does. Nothing runs in the background; each read applies whatever became due. Options.Now pins the clock for a test, and the same code path serves both.
Index ¶
Constants ¶
const ( SyncSynced = "Synced" SyncOutOfSync = "OutOfSync" SyncSyncing = "Syncing" SyncRefresh = "Refreshing" )
Sync states. The two the examples treat as in-flight are the two a server reports while it is working.
const ( JobRunning = "running" JobSucceeded = "succeeded" JobFailed = "failed" )
Job statuses.
const DefaultApps = 5000
DefaultApps is enough rows that a windowed table is actually windowed.
const DefaultSchedule = 6 * time.Second
DefaultSchedule is how often the world starts work of its own.
const EnvBase = "TUILIB_DEMO_API"
EnvBase names the environment variable that points a client at a running demoapi instead of an in-process one.
Variables ¶
var ( ErrNoSuchApp = errors.New("no such application") ErrBusy = errors.New("an operation is already running") )
Errors launch reports. They exist so the handler can answer 404 and 409 distinctly, which is the whole of the conflict story: a client cannot avoid every race, so the server has to be the one that says no.
Functions ¶
func Client ¶
Client returns an *http.Client that dispatches straight into h.
No port, no bind, no cleanup, and no flake when twenty examples start at once. A screen still builds an *http.Request and still parses JSON out of an *http.Response, so nothing about how it is written changes — which is the point: `task examples` needs no sidecar running, and the same screen works unaltered against a real server.
Streaming works. See inProcess.RoundTrip for why that took a pipe.
Types ¶
type App ¶
type App struct {
ID string `json:"id"`
Name string `json:"name"`
Region string `json:"region"`
Sync string `json:"sync"`
Health string `json:"health"`
// Rev changes whenever a job against this app completes. It is on the
// wire rather than derived because it is what activity.ActivityRevision
// watches: without it a fixture cannot demonstrate work that began and
// ended between two polls, which is the case that needs it most.
Rev int64 `json:"rev"`
}
App is one application.
type Job ¶
type Job struct {
ID string `json:"id"`
App string `json:"app"`
Kind string `json:"kind"`
Status string `json:"status"`
Started time.Time `json:"started"`
Finished time.Time `json:"finished,omitzero"`
// contains filtered or unexported fields
}
Job is work running against an app.
type Options ¶
type Options struct {
// Seed makes the generated world and its scheduled events reproducible.
// Zero seeds from the clock.
Seed int64
// Now is the clock the world advances on. Nil means time.Now. A test
// passes a controllable one and drives transitions exactly.
Now func() time.Time
// Apps is how many applications to generate. Zero means DefaultApps.
Apps int
// Latency is a floor applied to every request, before any per-request
// ?latency=. Zero means none.
Latency time.Duration
// Schedule is how often the world starts work nobody asked for. Zero means
// DefaultSchedule.
//
// A demo wants this short: background work is the thing a screen with
// ActivityWhen exists to show, and a viewer who has to wait out two
// intervals to see one concludes the feature does not work.
Schedule time.Duration
}
Options configures the fixture.
type Target ¶
type Target struct {
Client *http.Client
// Base has no trailing slash. For an in-process target it is a hostname
// the transport never resolves, and exists only so request URLs are valid.
Base string
// Live reports whether this talks to a separate process. A screen that
// wants to say so on its border can read it; nothing else behaves
// differently.
Live bool
}
Target is where a client should talk: a client, and the base URL to build request paths onto.
func From ¶
From returns a Target chosen by the environment.
With TUILIB_DEMO_API set, requests go over the network to that base URL and opts is ignored — the server already has a world, and a second one generated here would be a different set of applications answering none of the requests. Unset, it builds an in-process handler from opts and dispatches into it with no socket, which is what keeps `task examples` free of a sidecar.
The point of the live path is the workflow the wall-clock rule exists for: one world, a TUI watching it, and a curl poking it from another terminal.