All checks were successful
/ test (push) Successful in 9s
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.
88 lines
6.4 KiB
Markdown
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.
|