safsaf/CLAUDE.md
Christopher Baines 5b0e6397dc
All checks were successful
/ test (push) Successful in 9s
Initial commit
Safsaf is a Guile web framework, written using Claude Code running
Claude Opus 4.6, based off of the Guix Data Service, Nar Herder and
Guix Build Coordinator codebases.
2026-04-13 14:24:19 +03:00

88 lines
6.4 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Safsaf is a web framework for Guile Scheme, built on Guile Fibers
using the Guile Knots web server.
## Environment Setup
The project uses `direnv` with Guix. The `.envrc` runs `use guix -D -f guix-dev.scm`, which pulls in all dependencies: `guile-knots`, `guile-webutils`, `guile-lib`, `guile-json-4`, `guile-squee`, `guile-sqlite3`, `guile-gcrypt`.
Run `direnv allow` to activate the environment. All Guile dependencies are on `GUILE_LOAD_PATH` via the Guix profile.
## Key Dependencies
### Guile Knots
- `(knots web-server)``run-knots-web-server`: the HTTP server. Handler signature is `(request body-port) → (values response body)`. `body-port` is the port for reading the request body lazily.
- `(knots resource-pool)``with-resource-from-pool`: DB/resource connection pooling.
- `(knots parallelism)``fibers-let`, `fibers-parallel`, `fibers-map`: concurrent work in handlers.
- `(knots thread-pool)``call-with-thread`: offload blocking/CPU-bound work.
- `(knots timeout)``with-fibers-timeout`, `with-port-timeouts`: request and I/O timeouts.
- `(knots web)``call-with-connection-cache`: outbound HTTP with connection pooling.
- `(knots)``call-with-sigint`, `format/knots`, `spawn-fiber/knots`.
- `(knots web-server)` also exports `make-chunked-output-port/knots`, `sanitize-response`, `request-body-port/knots`, `read-request-body/knots`.
### Guile Webutils
- `(webutils multipart)``parse-request-body`, `<part>` record, `parts-ref`, `parts-ref-string`.
- `(webutils cookie)``set-cookie`, `delete-cookie`. Registers `Cookie`/`Set-Cookie` header parsers with `(web http)`.
- `(webutils sessions)``<session-manager>`, HMAC-signed cookie sessions. Format: `signature$expires$base64-data`.
- `(webutils date)` — RFC3339 and HTTP date conversions.
### Guile JSON (v4.7.3)
- `(json)` — Re-exports everything from parser, builder, and record modules.
- `(json parser)``json->scm` (from port), `json-string->scm` (from string). Options: `#:null` (default `'null`), `#:ordered` (preserve key order).
- `(json builder)``scm->json` (to port), `scm->json-string` (to string). Options: `#:pretty`, `#:unicode`, `#:validate`.
- `(json record)``define-json-mapping`: bidirectional SRFI-9 record ↔ JSON conversion.
- Data mapping: objects ↔ alists, arrays ↔ vectors, strings ↔ strings, numbers ↔ numbers, `true`/`false``#t`/`#f`, `null``'null`.
### Guile Gcrypt
- `(gcrypt random)` — cryptographic random bytes (used for CSRF token generation).
### Guile Lib
- `(htmlprag)` — HTML/SHTML parsing and generation. `html->shtml`: parse HTML to SXML. `shtml->html`: render SXML to HTML string. `write-shtml-as-html`: write SXML to port.
- `(logging logger)` — Logging framework. `(logging port-log)` — log to ports. `(logging rotating-log)` — rotating file logs.
- `(md5)` — MD5 hashing.
- `(container async-queue)``make-async-queue`, `async-enqueue!`, `async-dequeue!`.
- `(string transform)`, `(string wrap)`, `(string completion)` — String utilities.
### Guile Standard Library
- `(web request)`, `(web response)`, `(web uri)`, `(web http)` — Guile's built-in HTTP types.
- `(srfi srfi-9)` — Record types. `(srfi srfi-64)` — Test framework. `(srfi srfi-71)` — Extended `let` with multiple values; prefer over `(srfi srfi-11)` `let-values`.
## Architecture
Handler signature throughout is `(request body-port) → (values response body)`, using Guile's `<request>` directly. `body-port` is the port for reading the request body lazily. Context is threaded via Guile parameters, not a wrapper record.
Safsaf wraps `run-knots-web-server` with:
1. **Parameters for context**`current-route-params` (alist of matched route bindings), `current-reverse-routes` (for `path-for`). Handler wrappers add their own parameters (e.g. `current-csrf-token`, `current-session`).
2. **Router** — data-driven route table using `(route method pattern handler)`. Patterns are lists of segments: strings (literal match), symbols (capture), or `(predicate name)` pairs. Dotted-tail patterns (e.g. `'("api" . rest)`) capture remaining segments. Routes can be organized with `(route-group prefix ...)`. Named routes support reverse routing via `path-for`.
3. **Handler wrappers** — convention: `(foo-handler-wrapper handler) → handler'`. A handler wrapper transforms the request on the way in and the response/body on the way out. Wrappers that need configuration provide a `(make-foo-handler-wrapper ...)` constructor. Applied to route trees via `wrap-routes`, which accepts one or more wrappers.
4. **Entry point**`(run-safsaf routes #:key host port method-not-allowed? method-not-allowed-handler connection-buffer-size)` compiles the route table, builds the dispatch handler, and starts the HTTP server via `run-knots-web-server`. When called outside a Fibers scheduler, it wraps everything in `run-fibers` and blocks until Ctrl-C. When called inside an existing scheduler (e.g. within `run-fibers`), it just starts the server and returns immediately. `method-not-allowed?` defaults to `#t`, enabling automatic 405 responses. Handler wrappers are applied to routes via `wrap-routes` before passing to `run-safsaf`.
## Finding Guile Library Sources
To read source code for Guile dependencies, look them up via `GUILE_LOAD_PATH`. The first entry is the project's Guix profile directory containing all dependencies. Do **not** search `/gnu/store` directly — it is slow and noisy.
A Guile module path like `(knots web-server)` maps to the file `knots/web-server.scm` under a load path directory. To find it:
```
ls "$GUILE_LOAD_PATH" | head # see what's available
cat "$(echo $GUILE_LOAD_PATH | cut -d: -f1)/knots/web-server.scm" # read a specific module
```
Or use the Read/Glob tools directly against the first `GUILE_LOAD_PATH` entry (e.g. `/gnu/store/...-profile/share/guile/site/3.0/`). Module path segments map to directories, with the final segment as `<name>.scm`. For example:
- `(json parser)``json/parser.scm`
- `(webutils multipart)``webutils/multipart.scm`
- `(srfi srfi-9)``srfi/srfi-9.scm`
## Guile Conventions
- Predicates end with `?`. Setters use `set-` prefix. Constructors use `make-`.
- Records defined with `define-record-type` from `(srfi srfi-9)`.
- Modules use `define-module` with `#:use-module` and `#:export`.
- Use `values` for multiple return values, `call-with-values` or `receive` to consume them.