demoapi

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Sep 14, 2026 License: MIT Imports: 13 Imported by: 0

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

View Source
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.

View Source
const (
	JobRunning   = "running"
	JobSucceeded = "succeeded"
	JobFailed    = "failed"
)

Job statuses.

View Source
const DefaultApps = 5000

DefaultApps is enough rows that a windowed table is actually windowed.

View Source
const DefaultSchedule = 6 * time.Second

DefaultSchedule is how often the world starts work of its own.

View Source
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

View Source
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

func Client(h http.Handler) *http.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.

func New

func New(opts Options) http.Handler

New returns the fixture as an http.Handler.

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

func From(opts Options) Target

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.

func (Target) URL

func (t Target) URL(path string) string

URL joins a path onto the target's base.

Directories

Path Synopsis
cmd
demoapi command
Command demoapi runs the demo API on loopback.
Command demoapi runs the demo API on loopback.

Jump to

Keyboard shortcuts

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