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/. 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."}, ), ) }