89 lines
6.4 KiB
Markdown
89 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.
|