Begin go documentation (ai slop for now)
This commit is contained in:
303
go/cmd/kjol-web/app/golayer.go
Normal file
303
go/cmd/kjol-web/app/golayer.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
. "kjol/vdom"
|
||||
ui "kjol/webui"
|
||||
)
|
||||
|
||||
// The Go layer's documentation.
|
||||
//
|
||||
// One page, like /c and the component pages. The Go module is the biggest layer by far —
|
||||
// a dozen small packages plus both web engines — so the job here is a MAP, not a manual:
|
||||
// say what each subsystem is for and name the handful of identifiers you would reach for,
|
||||
// and leave the exhaustive reference to go doc.
|
||||
//
|
||||
// The web engines get a deliberately short section. They are the two compositions, and
|
||||
// each already has a whole documentation section of its own (/wasm, /js) — repeating it
|
||||
// here would be two maps of the same ground, kept in sync by hand. So this page points at
|
||||
// them and moves on.
|
||||
//
|
||||
// The organising idea is the same as the C page: every section is a SUBSYSTEM, and the
|
||||
// sidebar lists them. A subsystem here is a small group of packages that answer one
|
||||
// question — "how does it talk to a database", "how does it not trust its input" — rather
|
||||
// than one package per section, which for thirteen packages would be a wall.
|
||||
|
||||
// goSubsystems is the single source for the page's sections AND the sidebar that jumps to
|
||||
// them, so the sidebar cannot offer a jump to a section that does not exist.
|
||||
func goSubsystems() []subsystem {
|
||||
return []subsystem{
|
||||
{ID: "config", Label: "Configuration", Icon: "bolt",
|
||||
Blurb: "Where the binary learns its world: the environment baked in at compile time, and the config struct read at startup."},
|
||||
{ID: "data", Label: "Data", Icon: "table",
|
||||
Blurb: "A PostgreSQL query builder and row-to-struct automapper, and CSV in and out."},
|
||||
{ID: "http", Label: "HTTP & email", Icon: "globe",
|
||||
Blurb: "Dependency-free HTTP glue — CORS, responses, a typed client — and pluggable mail."},
|
||||
{ID: "values", Label: "Values", Icon: "calculator",
|
||||
Blurb: "The small stuff done once: generic helpers, UTC-first time, and money as integer cents."},
|
||||
{ID: "trust", Label: "Trust & logging", Icon: "shield-check",
|
||||
Blurb: "Crypto, input validation, and logging — the three that decide what the program will believe and remember."},
|
||||
{ID: "text", Label: "Text", Icon: "code",
|
||||
Blurb: "Syntax highlighting: source in, coloured HTML out. A lexer, not a parser."},
|
||||
{ID: "engines", Label: "Web engines", Icon: "layers",
|
||||
Blurb: "The two front-end frameworks are Go too — but documented on their own pages. This is only the pointer."},
|
||||
{ID: "tooling", Label: "Tooling", Icon: "cube",
|
||||
Blurb: "The command-line tools, most of them run by the build rather than by hand."},
|
||||
}
|
||||
}
|
||||
|
||||
// goNav is the sidebar while you are reading /go. The group is called Subsystems, as on /c.
|
||||
func goNav() []docsGroup {
|
||||
items := make([]docsItem, 0, len(goSubsystems()))
|
||||
for _, s := range goSubsystems() {
|
||||
items = append(items, docsItem{
|
||||
Path: "/go#" + s.ID,
|
||||
Label: s.Label,
|
||||
Icon: s.Icon,
|
||||
Blurb: s.Blurb,
|
||||
})
|
||||
}
|
||||
|
||||
return []docsGroup{
|
||||
{
|
||||
Title: "Introduction",
|
||||
Items: []docsItem{
|
||||
{Path: "/go", Label: "Overview", Icon: "book-open",
|
||||
Blurb: "What the Go layer is, and the one rule that shapes all of it."},
|
||||
},
|
||||
},
|
||||
{Title: "Subsystems", Items: items},
|
||||
}
|
||||
}
|
||||
|
||||
//gowasm:page /go static layout=app
|
||||
func GoPage(d Deps) func() *VNode {
|
||||
return func() *VNode {
|
||||
return docPage("Layers", "Kjøl Go",
|
||||
"The oldest and largest layer: configuration, a database toolkit, logging, HTTP helpers, "+
|
||||
"mail, money math, validation — the parts an application needs that are not the application. "+
|
||||
"Both web engines live here too, but those have their own pages; this is the base beneath them.",
|
||||
|
||||
docSection("what-this-is", "What this is",
|
||||
prose("A single Go module, imported package by package as kjol/<name>. Each package is small, "+
|
||||
"stdlib-first, and does one thing — there is no framework object to construct and no "+
|
||||
"lifecycle to learn. You import config, or dbutil, or chrono, and call it."),
|
||||
prose("One rule shapes the whole layer, and it is worth stating before the parts: the framework "+
|
||||
"NEVER imports application code. Where a package needs something only the app knows — the "+
|
||||
"names of its tables, where to write a log, its mail credentials, the shape of its config — "+
|
||||
"the app hands that in, and the package is written against the gap. That is why dbutil has a "+
|
||||
"Register, l4g a SetDatabaseWriter, snailmail a Configure, and config a generic Load[T]. The "+
|
||||
"inversions are not decoration; they are the reason one base layer can sit under several "+
|
||||
"different applications without knowing anything about any of them."),
|
||||
|
||||
note("This layer is consumed in place, not published",
|
||||
"kjøl is a git submodule inside each app, wired up with a go.work file — so editing a file "+
|
||||
"here takes effect in the consuming app immediately, with no version to bump and no go get. "+
|
||||
"There is no ABI to keep stable because there is nothing to keep stable between: the layer "+
|
||||
"and its consumer are built together."),
|
||||
),
|
||||
|
||||
goConfig(),
|
||||
goData(),
|
||||
goHTTP(),
|
||||
goValues(),
|
||||
goTrust(),
|
||||
goText(),
|
||||
goEngines(),
|
||||
goTooling(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- configuration -------------------------------------------------------
|
||||
|
||||
func goConfig() *VNode {
|
||||
return docSection("config", "Configuration",
|
||||
prose("Two packages, and they answer the same question — what world is this binary running in — at "+
|
||||
"two different times. appenv answers it at COMPILE time: Environment is a const chosen by a build "+
|
||||
"tag, so a production binary cannot be talked into thinking it is staging by a stray environment "+
|
||||
"variable. The value is fixed the moment go build runs, and the bundler reads it to define the "+
|
||||
"same constant for the JavaScript side."),
|
||||
prose("config answers it at RUNTIME. Load[T] fills the app's own config struct from environment "+
|
||||
"variables and an optional .env file, with real environment variables always winning over the "+
|
||||
"file. The app defines the struct — its fields, its env tags, its defaults — and the package only "+
|
||||
"provides the generic loading, so no two apps have to agree on what configuration means."),
|
||||
apiTable(
|
||||
apiRow{"appenv.Environment", "The deployment environment, a const fixed by build tag (-tags staging / production). No runtime path can change it."},
|
||||
apiRow{"config.Load[T](file, *T)", "Fill your config struct from env + .env. Generic over the struct; environment variables beat the file."},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- data ----------------------------------------------------------------
|
||||
|
||||
func goData() *VNode {
|
||||
return docSection("data", "Data",
|
||||
prose("dbutil is the largest package in the module, and it is two things: a PostgreSQL query builder "+
|
||||
"and a reflection-based row-to-struct automapper. It is deliberately NOT an ORM — there are no "+
|
||||
"migrations here (that is a separate tool) and no magic persistence. You bind a model type to a "+
|
||||
"table, build parameterized SQL through chainable Select / InsertInto / Update / DeleteFrom, and "+
|
||||
"scan the result straight into your structs by their db tags — including LEFT JOINs, which map a "+
|
||||
"missing joined row to a nil pointer rather than a lie."),
|
||||
prose("The table names come from a registry the app fills at startup (RegisterAll), which is the "+
|
||||
"inversion at work: the builder resolves a Go type to a table without ever importing the app's "+
|
||||
"models. Field references are type-safe — you pass a pointer to a struct field and the builder "+
|
||||
"turns it into a column — so a renamed field is a compile error, not a wrong query at runtime."),
|
||||
prose("csv is the small sibling: build CSV text from headers and rows, or straight from a slice of "+
|
||||
"structs, and stream it to the browser as a download."),
|
||||
apiTable(
|
||||
apiRow{"dbutil.Init / ConnConfig", "Open the pooled *sql.DB and pin the session to UTC. Credentials are injected, never read from app config."},
|
||||
apiRow{"dbutil.Register / RegisterAll", "Map a model type to its table name. The inversion: dbutil never imports your models."},
|
||||
apiRow{"dbutil.Select / InsertInto / Update / DeleteFrom", "Chainable builders — Where, joins, order, paging — that emit $1,$2 parameterized SQL and its args."},
|
||||
apiRow{"dbutil.ScanAll / QueryOne / QueryScalar[T]", "Result rows into your structs by db tag; joined rows that are all-NULL become nil pointers."},
|
||||
apiRow{"dbutil.ParseFilterFromRequest / ApplyPagination", "Turn a request's query-string filters and paging into WHERE and LIMIT."},
|
||||
apiRow{"csv.MakeCSV / StructToCSV / WriteCSVtoHTTP", "CSV from rows or from a slice of structs, and the headers to send it as an attachment."},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- http & mail ---------------------------------------------------------
|
||||
|
||||
func goHTTP() *VNode {
|
||||
return docSection("http", "HTTP & email",
|
||||
prose("httputil is the HTTP glue, and it imports nothing of the app's. CorsMiddleware is "+
|
||||
"constructor-style — you hand it the allowed domains and a function that reports the current "+
|
||||
"bundle version, and it never reads those from config itself. Alongside it are the response "+
|
||||
"writers (JSON, gob, error) and their client-side mirrors: FetchGob[T] and FetchJSON[T] do a typed "+
|
||||
"GET-and-decode, and are a deliberate no-op during server rendering, so an SSR pass keeps its "+
|
||||
"loading state instead of blocking on a network call."),
|
||||
prose("snailmail sends mail through a provider chosen at startup — SMTP or Cloudflare — with the "+
|
||||
"credentials injected via Configure and the actual branded message composed on the app side. The "+
|
||||
"package takes an already-rendered Email and a type (text or HTML) and sends it; it does not know "+
|
||||
"or care what the mail says."),
|
||||
apiTable(
|
||||
apiRow{"httputil.CorsMiddleware(CorsConfig)", "CORS as constructor-style middleware; allowed domains and the bundle-version source are injected."},
|
||||
apiRow{"httputil.RespondJSON / RespondGob / RespondError", "Encode and write a response."},
|
||||
apiRow{"httputil.FetchGob[T] / FetchJSON[T]", "Client-side typed GET+decode. A no-op during SSR, so the server keeps a loading state."},
|
||||
apiRow{"snailmail.Configure(Settings)", "Pick SMTP or Cloudflare and hand it credentials, once at startup."},
|
||||
apiRow{"snailmail.SendMail(Email, type)", "Send an already-rendered message. TYPE_TEXT or TYPE_HTML."},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- values --------------------------------------------------------------
|
||||
|
||||
func goValues() *VNode {
|
||||
return docSection("values", "Values",
|
||||
prose("Three packages of the small stuff, done once so the apps do not each do it slightly "+
|
||||
"differently. basic is the personal standard library: generic slice, map and pointer helpers, "+
|
||||
"name capitalization that knows about Mc and O', number-to-string with commas, a reflection-based "+
|
||||
"struct diff. chrono treats every stored time as UTC and only localizes at the edge — format a "+
|
||||
"time in a timezone for display, parse an HTML date input back to UTC, render \"3 days ago\". "+
|
||||
"finance keeps money as integer cents, never a float, and formats it back out with a symbol and "+
|
||||
"grouping."),
|
||||
apiTable(
|
||||
apiRow{"basic.Reverse / IndexOf / RemoveDuplicates / MapMerge", "Generic slice and map helpers, stdlib-only."},
|
||||
apiRow{"basic.NormalizeName / Int64ToStringWithCommas / CompareStructs", "Name casing, grouped numbers, and a field-by-field struct diff."},
|
||||
apiRow{"chrono.FormatWithTz / DateToHTMLString", "A UTC time localized for display, or fed into an HTML date field."},
|
||||
apiRow{"chrono.HTMLDateToTime / TimeSinceToString", "An HTML input parsed back to UTC; and \"Just now\" / \"3 days ago\"."},
|
||||
apiRow{"finance.Int64ToMoneyWithCommas / MoneyToInt64", "Cents-as-int64 to a dollar string and back — no float ever touches the money."},
|
||||
apiRow{"finance.MultiplyByPercentage / DaysToRateTerm", "Percentage math on cents, and a day count as a best-fit term string."},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- trust & logging -----------------------------------------------------
|
||||
|
||||
func goTrust() *VNode {
|
||||
return docSection("trust", "Trust & logging",
|
||||
prose("Three packages that decide what the program will believe and what it will remember. security "+
|
||||
"is the crypto: bcrypt for passwords, AES-256-GCM for secrets (with generic EncryptData[T] that "+
|
||||
"gob-serializes then encrypts), hashing, base58/64, random keys, and a bluemonday HTML "+
|
||||
"sanitization policy you initialize at startup. validation cleans and checks input — email, "+
|
||||
"phone, US state, tax id, ZIP — and its validators return descriptive errors rather than a bare "+
|
||||
"false, so the caller can say what was wrong."),
|
||||
prose("l4g is logging, and it carries the same inversion as dbutil. It owns the Entry type — the "+
|
||||
"framework's mirror of the app's log-entry model — and persists to the database through a function "+
|
||||
"the app registers with SetDatabaseWriter. If none is registered it falls back to the terminal, so "+
|
||||
"a line is never silently dropped. The main logger is terminal, file, or database, chosen by an "+
|
||||
"environment variable."),
|
||||
apiTable(
|
||||
apiRow{"security.HashPassword / ComparePasswords", "bcrypt."},
|
||||
apiRow{"security.EncryptData[T] / DecryptData[T]", "gob-serialize then AES-256-GCM, generic over the value."},
|
||||
apiRow{"security.Init / SanitizationPolicy", "The bluemonday UGC policy (extended to allow svg/path). Initialize it before use."},
|
||||
apiRow{"validation.SanitizeEmail / ValidatePhoneNumber / ValidateStateCode", "Clean and check US-centric input; validators return an error, not a bool."},
|
||||
apiRow{"l4g.Init / Write / Fatal", "Terminal, file, or database logging, selected by LOGGER_TYPE."},
|
||||
apiRow{"l4g.SetDatabaseWriter(func(Entry) error)", "The inversion: l4g owns Entry, the app owns the table it lands in."},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- text ----------------------------------------------------------------
|
||||
|
||||
func goText() *VNode {
|
||||
return docSection("text", "Text",
|
||||
prose("lexer is syntax highlighting: source code in, HTML with coloured spans out. It is a lexer and "+
|
||||
"not a parser on purpose — it degrades to escaped plain text on anything it does not understand "+
|
||||
"rather than failing, so an unknown language is not an error and a half-written snippet still "+
|
||||
"renders. It lives beside webui rather than inside it because it touches no DOM; it is a string in "+
|
||||
"and a string out. It is what colours the C snippets over on the C page."),
|
||||
apiTable(
|
||||
apiRow{"lexer.Highlight(lang, src)", "Dispatch by language name. An unknown language comes back escaped and unpainted, not wrong."},
|
||||
apiRow{"lexer.HighlightGo / HighlightC", "The two languages implemented so far."},
|
||||
),
|
||||
note("Its output is class names, so the stylesheet has to know it exists",
|
||||
"The spans lexer emits carry Tailwind classes (text-emerald-300 and the like), so any stylesheet "+
|
||||
"that renders a code block has to scan lexer/**/*.go for them. A build that forgets still "+
|
||||
"compiles and just renders the snippet unstyled — which is exactly how it is wired into this "+
|
||||
"site's Tailwind step."),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- web engines ---------------------------------------------------------
|
||||
|
||||
// The short section, on purpose: these are the two compositions, and each is documented in
|
||||
// full elsewhere. All this page owes them is a sentence and a door.
|
||||
func goEngines() *VNode {
|
||||
return docSection("engines", "Web engines",
|
||||
prose("Both of kjøl's web frameworks are assembled out of this layer — and both have their own "+
|
||||
"documentation, so this is only the map. The gowasm engine (the packages vdom, wasmruntime, rsc "+
|
||||
"and wasmdevserver, plus the webui component kit) lets you write user interfaces as ordinary Go "+
|
||||
"compiled to WebAssembly, server-rendered and then hydrated, with no JavaScript build at all. "+
|
||||
"jsbundler and tw are the other road: the JavaScript build — TSX to Solid to esbuild — and a "+
|
||||
"Tailwind v4 compiler written in Go, which is what styles both engines."),
|
||||
P(Attr("class", "mt-4 flex flex-wrap gap-3"),
|
||||
engineLink("/wasm", "code", "Kjøl Wasm Web"),
|
||||
engineLink("/js", "table", "Kjøl JS Web"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// engineLink is a pill that crosses into a composition's documentation. A plain href, not a
|
||||
// client-side route: /js is a different binary's SPA, and even /wasm is reached most simply
|
||||
// by letting the browser navigate rather than asking this page to swap itself out. No Deps,
|
||||
// therefore — there is no navigate() to intercept.
|
||||
func engineLink(href, icon, label string) *VNode {
|
||||
return A(Attr("class", "inline-flex items-center gap-2 rounded-default border border-line px-3 py-1.5 text-sm font-medium text-ink-soft no-underline hover:border-primary-border hover:bg-primary-subtle hover:text-accent"),
|
||||
Attr("href", href),
|
||||
ui.IconInline(icon, 14, "text-ink-faint"),
|
||||
Text(label),
|
||||
ui.IconInline("arrow-right", 12, "text-ink-faint"),
|
||||
)
|
||||
}
|
||||
|
||||
// ---- tooling -------------------------------------------------------------
|
||||
|
||||
func goTooling() *VNode {
|
||||
return docSection("tooling", "Tooling",
|
||||
prose("The module ships a handful of command-line programs under cmd/. Most of them are run by the "+
|
||||
"build rather than typed by hand: wasmgen reads the //gowasm: directives and writes the route and "+
|
||||
"layout glue, twcss compiles the Tailwind stylesheet, bundle drives the JavaScript build, and "+
|
||||
"typecheck runs the TypeScript checker. The rest are operational: migrate applies database "+
|
||||
"migrations, loc reports the lines of code across the repository, and passgen bcrypt-hashes a "+
|
||||
"password from the command line."),
|
||||
apiTable(
|
||||
apiRow{"cmd/wasmgen", "Preprocesses the //gowasm: directives into glue: the route map, the layouts, the server-component calls."},
|
||||
apiRow{"cmd/twcss", "The Tailwind v4 compiler as a CLI — scan the sources, write the stylesheet."},
|
||||
apiRow{"cmd/bundle", "A thin CLI over jsbundler: TSX → Solid → esbuild, plus the SSR bake."},
|
||||
apiRow{"cmd/typecheck", "Runs the frontend TypeScript checker (tsgo, the native-Go TypeScript compiler)."},
|
||||
apiRow{"cmd/migrate", "PostgreSQL migrations — up and down, behind an advisory lock."},
|
||||
apiRow{"cmd/loc", "A lines-of-code report over git-tracked files (gocloc), vendored code excluded."},
|
||||
apiRow{"cmd/passgen", "bcrypt-hash a password given on the command line."},
|
||||
),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user