Automatic website update
This commit is contained in:
parent
4ca60e7de3
commit
a1176f2df1
1 changed files with 780 additions and 0 deletions
780
index.html
Normal file
780
index.html
Normal file
|
|
@ -0,0 +1,780 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<html>
|
||||
<!-- Created by GNU Texinfo 6.8, https://www.gnu.org/software/texinfo/ -->
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Safsaf</title>
|
||||
|
||||
<meta name="description" content="Safsaf">
|
||||
<meta name="keywords" content="Safsaf">
|
||||
<meta name="resource-type" content="document">
|
||||
<meta name="distribution" content="global">
|
||||
<meta name="Generator" content="makeinfo">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<link href="#Top" rel="start" title="Top">
|
||||
<link href="#Concept-Index" rel="index" title="Concept Index">
|
||||
<link href="#SEC_Contents" rel="contents" title="Table of Contents">
|
||||
<link href="#Guidance" rel="next" title="Guidance">
|
||||
<style type="text/css">
|
||||
<!--
|
||||
a.copiable-anchor {visibility: hidden; text-decoration: none; line-height: 0em}
|
||||
a.summary-letter {text-decoration: none}
|
||||
blockquote.indentedblock {margin-right: 0em}
|
||||
div.display {margin-left: 3.2em}
|
||||
div.example {margin-left: 3.2em}
|
||||
kbd {font-style: oblique}
|
||||
pre.display {font-family: inherit}
|
||||
pre.format {font-family: inherit}
|
||||
pre.menu-comment {font-family: serif}
|
||||
pre.menu-preformatted {font-family: serif}
|
||||
span.nolinebreak {white-space: nowrap}
|
||||
span.roman {font-family: initial; font-weight: normal}
|
||||
span.sansserif {font-family: sans-serif; font-weight: normal}
|
||||
span:hover a.copiable-anchor {visibility: visible}
|
||||
ul.no-bullet {list-style: none}
|
||||
-->
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="https://luis-felipe.gitlab.io/texinfo-css/static/css/texinfo-7.css">
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body lang="en">
|
||||
<h1 class="settitle" align="center">Safsaf</h1>
|
||||
|
||||
|
||||
<div style="text-align: center;">
|
||||
<img src="logo.svg" alt="Safsaf" width="200" height="200">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="top" id="Top">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Guidance" accesskey="n" rel="next">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Overview"></span><h1 class="top">Overview</h1>
|
||||
|
||||
<p>Safsaf is a web framework for Guile Scheme, built on
|
||||
<a href="https://codeberg.org/guile/fibers">Guile Fibers</a> using the
|
||||
<a href="https://cbaines.codeberg.page/guile-knots/">Guile Knots</a> web
|
||||
server.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="Contents_element" id="SEC_Contents">
|
||||
<h2 class="contents-heading">Table of Contents</h2>
|
||||
|
||||
<div class="contents">
|
||||
|
||||
<ul class="no-bullet">
|
||||
<li><a id="toc-Guidance-1" href="#Guidance">1 Guidance</a>
|
||||
<ul class="no-bullet">
|
||||
<li><a id="toc-Getting-Started-1" href="#Getting-Started">1.1 Getting Started</a></li>
|
||||
<li><a id="toc-Routing-1" href="#Routing">1.2 Routing</a></li>
|
||||
<li><a id="toc-Handler-Wrappers-1" href="#Handler-Wrappers">1.3 Handler Wrappers</a></li>
|
||||
<li><a id="toc-Responses-1" href="#Responses">1.4 Responses</a></li>
|
||||
<li><a id="toc-Request-Parsing-1" href="#Request-Parsing">1.5 Request Parsing</a></li>
|
||||
<li><a id="toc-Parameter-Parsing-1" href="#Parameter-Parsing">1.6 Parameter Parsing</a></li>
|
||||
<li><a id="toc-Sessions-1" href="#Sessions">1.7 Sessions</a></li>
|
||||
<li><a id="toc-Templating-1" href="#Templating">1.8 Templating</a></li>
|
||||
<li><a id="toc-Static-Files-1" href="#Static-Files">1.9 Static Files</a></li>
|
||||
</ul></li>
|
||||
<li><a id="toc-API-1" href="#API">2 API</a>
|
||||
<ul class="no-bullet">
|
||||
<li><a id="toc-_0028safsaf_0029" href="#safsaf">2.1 (safsaf)</a>
|
||||
<ul class="no-bullet">
|
||||
<li><a id="toc-Procedures" href="#Procedures">2.1.1 Procedures</a></li>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
<li><a id="toc-Version-History-1" href="#Version-History">Appendix A Version History</a></li>
|
||||
<li><a id="toc-Copying-Information-1" href="#Copying-Information">Appendix B Copying Information</a></li>
|
||||
<li><a id="toc-Concept-Index-1" href="#Concept-Index" rel="index">Concept Index</a></li>
|
||||
<li><a id="toc-Data-Type-Index-1" href="#Data-Type-Index" rel="index">Data Type Index</a></li>
|
||||
<li><a id="toc-Procedure-Index-1" href="#Procedure-Index" rel="index">Procedure Index</a></li>
|
||||
<li><a id="toc-Variable-Index-1" href="#Variable-Index" rel="index">Variable Index</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="chapter" id="Guidance">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#API" accesskey="n" rel="next">API</a>, Previous: <a href="#Top" accesskey="p" rel="prev">Overview</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Guidance-1"></span><h2 class="chapter">1 Guidance</h2>
|
||||
|
||||
<p>This chapter explains how the pieces of Safsaf fit together. Each
|
||||
section covers one concept with a short code example. For the full
|
||||
list of parameters and options, see <a href="#API">API</a>.
|
||||
</p>
|
||||
|
||||
|
||||
<ul class="section-toc">
|
||||
<li><a href="#Getting-Started" accesskey="1">Getting Started</a></li>
|
||||
<li><a href="#Routing" accesskey="2">Routing</a></li>
|
||||
<li><a href="#Handler-Wrappers" accesskey="3">Handler Wrappers</a></li>
|
||||
<li><a href="#Responses" accesskey="4">Responses</a></li>
|
||||
<li><a href="#Request-Parsing" accesskey="5">Request Parsing</a></li>
|
||||
<li><a href="#Parameter-Parsing" accesskey="6">Parameter Parsing</a></li>
|
||||
<li><a href="#Sessions" accesskey="7">Sessions</a></li>
|
||||
<li><a href="#Templating" accesskey="8">Templating</a></li>
|
||||
<li><a href="#Static-Files" accesskey="9">Static Files</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="section" id="Getting-Started">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Routing" accesskey="n" rel="next">Routing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Getting-Started-1"></span><h3 class="section">1.1 Getting Started</h3>
|
||||
|
||||
<p>A Safsaf application is a list of routes passed to <code>run-safsaf</code>.
|
||||
Each route binds an HTTP method and URL pattern to a handler procedure.
|
||||
The handler receives a Guile <code><request></code> and a body port, and
|
||||
returns two values: a response and a body.
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(use-modules (safsaf)
|
||||
(safsaf router)
|
||||
(safsaf response-helpers))
|
||||
|
||||
(define routes
|
||||
(list
|
||||
(route 'GET '() index-page
|
||||
#:name 'index)
|
||||
(route 'GET '("hello" name) hello-page)
|
||||
(route '* '* (lambda (request body-port)
|
||||
(not-found-response)))))
|
||||
|
||||
(define (index-page request body-port)
|
||||
(html-response '(h1 "Welcome")))
|
||||
|
||||
(define (hello-page request body-port)
|
||||
(let ((name (assoc-ref (current-route-params) 'name)))
|
||||
(text-response (string-append "Hello, " name "!"))))
|
||||
|
||||
(run-safsaf routes #:port 8080)
|
||||
</pre></div>
|
||||
|
||||
<p>The last route should be a catch-all (<code>'*</code> method, <code>'*</code>
|
||||
pattern) so that every request is handled. <code>run-safsaf</code> sets up
|
||||
a Fibers scheduler, starts the HTTP server, and blocks until Ctrl-C.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Routing">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Handler-Wrappers" accesskey="n" rel="next">Handler Wrappers</a>, Previous: <a href="#Getting-Started" accesskey="p" rel="prev">Getting Started</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Routing-1"></span><h3 class="section">1.2 Routing</h3>
|
||||
|
||||
<span id="Patterns"></span><h4 class="subheading">Patterns</h4>
|
||||
|
||||
<p>Route patterns are lists of segments. A string matches literally, a
|
||||
symbol captures that segment into <code>current-route-params</code>, and a
|
||||
two-element list <code>(predicate name)</code> captures only when
|
||||
<var>predicate</var> returns true. A dotted tail captures the remaining
|
||||
path.
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">;; Literal: /about
|
||||
(route 'GET '("about") about-handler)
|
||||
|
||||
;; Capture: /users/:id
|
||||
(route 'GET '("users" id) show-user)
|
||||
|
||||
;; Predicate: /posts/:id where id is numeric
|
||||
(route 'GET '("posts" (,string->number id)) show-post)
|
||||
|
||||
;; Wildcard (rest): /files/* — captures remaining segments
|
||||
(route 'GET '("files" . path) serve-file)
|
||||
</pre></div>
|
||||
|
||||
<span id="Route-groups"></span><h4 class="subheading">Route groups</h4>
|
||||
|
||||
<p><code>route-group</code> nests routes under a shared prefix:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(route-group '("api" "v1")
|
||||
(route 'GET '("users") api-list-users)
|
||||
(route 'GET '("users" id) api-show-user))
|
||||
</pre></div>
|
||||
|
||||
<p>This matches <code>/api/v1/users</code> and <code>/api/v1/users/:id</code>.
|
||||
</p>
|
||||
<span id="Named-routes-and-path_002dfor"></span><h4 class="subheading">Named routes and path-for</h4>
|
||||
|
||||
<p>Give a route a <code>#:name</code> and use <code>path-for</code> to generate its
|
||||
URL, so paths are never hard-coded. The first argument is always a
|
||||
route group:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define my-routes
|
||||
(route-group '()
|
||||
(route 'GET '("posts" id) show-post #:name 'show-post)))
|
||||
|
||||
(define all-routes
|
||||
(list my-routes
|
||||
(route '* '* (lambda (r b) (not-found-response)))))
|
||||
|
||||
;; In a handler or template:
|
||||
(path-for my-routes 'show-post '((id . "42")))
|
||||
;; => "/posts/42"
|
||||
</pre></div>
|
||||
|
||||
<p><code>path-for</code> also accepts <code>#:query</code> and <code>#:fragment</code>
|
||||
keyword arguments.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Handler-Wrappers">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Responses" accesskey="n" rel="next">Responses</a>, Previous: <a href="#Routing" accesskey="p" rel="prev">Routing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Handler-Wrappers-1"></span><h3 class="section">1.3 Handler Wrappers</h3>
|
||||
|
||||
<p>A handler wrapper is a procedure that takes a handler and returns a
|
||||
new handler. It can transform the request on the way in and the
|
||||
response on the way out. Apply wrappers to a route tree with
|
||||
<code>wrap-routes</code>.
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(wrap-routes routes
|
||||
(make-exceptions-handler-wrapper #:dev? #t)
|
||||
logging-handler-wrapper)
|
||||
</pre></div>
|
||||
|
||||
<p>When multiple wrappers are given, the first wraps outermost — it runs
|
||||
first on the request and last on the response. In the example above,
|
||||
exceptions catches errors from the logging wrapper and the inner
|
||||
handler.
|
||||
</p>
|
||||
<span id="Per_002dgroup-wrappers"></span><h4 class="subheading">Per-group wrappers</h4>
|
||||
|
||||
<p>Apply wrappers to part of the route tree by wrapping a group
|
||||
separately:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define api-routes
|
||||
(wrap-routes
|
||||
(route-group '("api")
|
||||
(route 'GET '("items") api-list-items))
|
||||
cors-handler-wrapper))
|
||||
|
||||
(define all-routes
|
||||
(wrap-routes
|
||||
(list api-routes
|
||||
(route 'GET '() index-page)
|
||||
(route '* '* (lambda (r b) (not-found-response))))
|
||||
logging-handler-wrapper))
|
||||
</pre></div>
|
||||
|
||||
<p>Here CORS headers are added only to <code>/api/*</code> routes, while
|
||||
logging applies to everything.
|
||||
</p>
|
||||
<span id="Security-headers"></span><h4 class="subheading">Security headers</h4>
|
||||
|
||||
<p><code>security-headers-handler-wrapper</code> <em>appends</em> its headers to
|
||||
the response rather than replacing existing ones. If a handler sets
|
||||
<code>X-Frame-Options</code> itself, both values will appear in the response.
|
||||
To avoid duplication, either omit the header from the wrapper (pass
|
||||
<code>#:frame-options #f</code>) or do not set it in the handler.
|
||||
</p>
|
||||
<span id="Max-body-size"></span><h4 class="subheading">Max body size</h4>
|
||||
|
||||
<p><code>make-max-body-size-handler-wrapper</code> checks the
|
||||
<code>Content-Length</code> header and rejects requests that exceed the
|
||||
limit with a 413 response. However, it does <em>not</em> limit chunked
|
||||
transfer-encoded requests that lack <code>Content-Length</code>. For
|
||||
untrusted networks, use a reverse proxy (e.g. Nginx’s
|
||||
<code>client_max_body_size</code>) to enforce size limits at the transport
|
||||
level.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Responses">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Request-Parsing" accesskey="n" rel="next">Request Parsing</a>, Previous: <a href="#Handler-Wrappers" accesskey="p" rel="prev">Handler Wrappers</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Responses-1"></span><h3 class="section">1.4 Responses</h3>
|
||||
|
||||
<p>Safsaf provides helpers that return <code>(values response body)</code>
|
||||
directly:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">;; HTML — streams an SXML tree
|
||||
(html-response '(div (h1 "Hello") (p "world")))
|
||||
|
||||
;; JSON — takes a JSON string
|
||||
(json-response (scm->json-string '(("ok" . #t))))
|
||||
|
||||
;; Plain text
|
||||
(text-response "pong")
|
||||
|
||||
;; Redirect (default 303 See Other)
|
||||
(redirect-response "/login")
|
||||
(redirect-response "/new-item" #:code 302)
|
||||
|
||||
;; Error responses
|
||||
(not-found-response)
|
||||
(bad-request-response "Missing field")
|
||||
(forbidden-response)
|
||||
</pre></div>
|
||||
|
||||
<p><code>html-response</code>, <code>json-response</code>, <code>text-response</code>, and
|
||||
<code>redirect-response</code> accept <code>#:code</code> and <code>#:headers</code> for
|
||||
overrides. The error helpers (<code>not-found-response</code>, etc.) accept
|
||||
<code>#:headers</code> but have a fixed status code.
|
||||
</p>
|
||||
<p>For content negotiation, use <code>negotiate-content-type</code>:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define (show-item request body-port)
|
||||
(let ((item (fetch-item (assoc-ref (current-route-params) 'id))))
|
||||
(case (negotiate-content-type request
|
||||
'(text/html application/json))
|
||||
((application/json)
|
||||
(json-response (scm->json-string (item->alist item))))
|
||||
(else
|
||||
(html-response `(div (h1 ,(item-title item))))))))
|
||||
</pre></div>
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Request-Parsing">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Parameter-Parsing" accesskey="n" rel="next">Parameter Parsing</a>, Previous: <a href="#Responses" accesskey="p" rel="prev">Responses</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Request-Parsing-1"></span><h3 class="section">1.5 Request Parsing</h3>
|
||||
|
||||
<span id="Form-bodies"></span><h4 class="subheading">Form bodies</h4>
|
||||
|
||||
<p><code>parse-form-body</code> reads a URL-encoded POST body and returns an
|
||||
alist of string pairs:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define (handle-login request body-port)
|
||||
(let* ((form (parse-form-body request body-port))
|
||||
(username (assoc-ref form "username"))
|
||||
(password (assoc-ref form "password")))
|
||||
(if (valid-credentials? username password)
|
||||
(redirect-response "/dashboard")
|
||||
(text-response "Invalid login" #:code 401))))
|
||||
</pre></div>
|
||||
|
||||
<span id="Query-strings"></span><h4 class="subheading">Query strings</h4>
|
||||
|
||||
<p><code>parse-query-string</code> extracts query parameters from the request
|
||||
URL:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(let ((qs (parse-query-string request)))
|
||||
(assoc-ref qs "page")) ;; => "2" or #f
|
||||
</pre></div>
|
||||
|
||||
<span id="Multipart"></span><h4 class="subheading">Multipart</h4>
|
||||
|
||||
<p>For file uploads, use <code>parse-multipart-body</code>:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(let* ((parts (parse-multipart-body request body-port))
|
||||
(form (multipart-text-fields parts))
|
||||
(file (parts-ref parts "avatar")))
|
||||
;; form is an alist of text fields
|
||||
;; file is a <part> record — read its body with (part-body file)
|
||||
...)
|
||||
</pre></div>
|
||||
|
||||
<span id="Cookies"></span><h4 class="subheading">Cookies</h4>
|
||||
|
||||
<p>Read cookies with <code>request-cookie-ref</code> or
|
||||
<code>request-cookies</code>. Set them via response headers with
|
||||
<code>set-cookie-header</code> and <code>delete-cookie-header</code>:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(request-cookie-ref request "theme") ;; => "dark" or #f
|
||||
|
||||
(text-response "ok"
|
||||
#:headers (list (set-cookie-header "theme" "dark"
|
||||
#:path "/"
|
||||
#:http-only #t)))
|
||||
</pre></div>
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Parameter-Parsing">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Sessions" accesskey="n" rel="next">Sessions</a>, Previous: <a href="#Request-Parsing" accesskey="p" rel="prev">Request Parsing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Parameter-Parsing-1"></span><h3 class="section">1.6 Parameter Parsing</h3>
|
||||
|
||||
<p><code>parse-params</code> validates and transforms raw form or query data
|
||||
according to a declarative spec. Each spec entry names a parameter,
|
||||
a processor (a procedure that converts a string or returns an
|
||||
<code><invalid-param></code>), and options like <code>#:required</code> or
|
||||
<code>#:default</code>.
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(let ((params (parse-params
|
||||
`((page ,as-integer #:default 1)
|
||||
(per-page ,as-integer #:default 20)
|
||||
(q ,as-string))
|
||||
(parse-query-string request))))
|
||||
(assq-ref params 'page)) ;; => 1 (integer, not string)
|
||||
</pre></div>
|
||||
|
||||
<p>Built-in processors: <code>as-string</code>, <code>as-integer</code>,
|
||||
<code>as-number</code>, <code>as-checkbox</code>, <code>as-one-of</code>,
|
||||
<code>as-matching</code>, <code>as-predicate</code>.
|
||||
</p>
|
||||
<span id="Form-params-with-CSRF"></span><h4 class="subheading">Form params with CSRF</h4>
|
||||
|
||||
<p>For POST forms, use <code>parse-form-params</code> instead — it
|
||||
automatically checks the CSRF token (from
|
||||
<code>csrf-handler-wrapper</code>) before parsing:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(let* ((form (parse-form-body request body-port))
|
||||
(params (parse-form-params
|
||||
`((title ,as-string #:required)
|
||||
(body ,as-string #:required))
|
||||
form)))
|
||||
(if (any-invalid-params? params)
|
||||
;; Re-render the form with errors
|
||||
(render-form (field-errors params 'title)
|
||||
(field-errors params 'body))
|
||||
;; Proceed
|
||||
(create-item! (assq-ref params 'title)
|
||||
(assq-ref params 'body))))
|
||||
</pre></div>
|
||||
|
||||
<p><code>any-invalid-params?</code> returns <code>#t</code> if any value failed
|
||||
validation. <code>field-errors</code> returns a list of error message
|
||||
strings for a given field, suitable for rendering next to form inputs.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Sessions">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Templating" accesskey="n" rel="next">Templating</a>, Previous: <a href="#Parameter-Parsing" accesskey="p" rel="prev">Parameter Parsing</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Sessions-1"></span><h3 class="section">1.7 Sessions</h3>
|
||||
|
||||
<p>Sessions use HMAC-signed cookies via <code>(webutils sessions)</code>.
|
||||
Set up a session config and apply the wrapper:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define session-mgr
|
||||
(make-session-config "my-secret-key"
|
||||
#:cookie-name "my-session"))
|
||||
|
||||
(define routes
|
||||
(wrap-routes my-routes
|
||||
(make-session-handler-wrapper session-mgr)))
|
||||
</pre></div>
|
||||
|
||||
<p>Inside a handler, <code>(current-session)</code> returns the session data
|
||||
(an alist) or <code>#f</code> if no valid session exists.
|
||||
</p>
|
||||
<p>To set session data, include a <code>session-set</code> header in the
|
||||
response. To delete, use <code>session-delete</code>:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">;; Set session
|
||||
(redirect-response "/"
|
||||
#:headers (list (session-set session-mgr
|
||||
'((user-id . 42)))))
|
||||
|
||||
;; Read session
|
||||
(let ((user-id (and (current-session)
|
||||
(assoc-ref (current-session) 'user-id))))
|
||||
...)
|
||||
|
||||
;; Delete session
|
||||
(redirect-response "/"
|
||||
#:headers (list (session-delete session-mgr)))
|
||||
</pre></div>
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Templating">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Static-Files" accesskey="n" rel="next">Static Files</a>, Previous: <a href="#Sessions" accesskey="p" rel="prev">Sessions</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Templating-1"></span><h3 class="section">1.8 Templating</h3>
|
||||
|
||||
<p><code>write-shtml-as-html/streaming</code> works like htmlprag’s
|
||||
<code>write-shtml-as-html</code>, but any procedure in the SHTML tree is
|
||||
called as <code>(proc port)</code> and can write dynamic content directly.
|
||||
</p>
|
||||
<p><code>streaming-html-response</code> wraps this into a response: give it an
|
||||
SHTML tree (with optional procedure slots) and it returns
|
||||
<code>(values response body)</code> ready for a handler.
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define (base-layout title content-proc)
|
||||
`(*TOP*
|
||||
(*DECL* DOCTYPE html)
|
||||
(html
|
||||
(head (title ,title))
|
||||
(body
|
||||
(nav (a (@ (href "/")) "Home"))
|
||||
(main ,content-proc)
|
||||
(footer (p "Footer"))))))
|
||||
</pre></div>
|
||||
|
||||
<p>The layout is plain SHTML with a procedure in the <var>content-proc</var>
|
||||
position. Use <code>streaming-html-response</code> to send it:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(define (index-page request body-port)
|
||||
(streaming-html-response
|
||||
(base-layout "Home"
|
||||
(lambda (port)
|
||||
(write-shtml-as-html
|
||||
`(div (h1 "Welcome")
|
||||
(p "Content goes here."))
|
||||
port)))))
|
||||
</pre></div>
|
||||
|
||||
<p>You can also call <code>write-shtml-as-html/streaming</code> directly when
|
||||
you need to write SHTML with procedure slots to an arbitrary port.
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="section" id="Static-Files">
|
||||
<div class="header">
|
||||
<p>
|
||||
Previous: <a href="#Templating" accesskey="p" rel="prev">Templating</a>, Up: <a href="#Guidance" accesskey="u" rel="up">Guidance</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Static-Files-1"></span><h3 class="section">1.9 Static Files</h3>
|
||||
|
||||
<p><code>make-static-handler</code> returns a handler that serves files from a
|
||||
directory. Pair it with a wildcard route:
|
||||
</p>
|
||||
<div class="example lisp">
|
||||
<pre class="lisp">(route-group '("static")
|
||||
(route 'GET '(. path)
|
||||
(make-static-handler "./public"
|
||||
#:cache-control '((max-age . 3600)))))
|
||||
</pre></div>
|
||||
|
||||
<p>This serves <code>/static/css/style.css</code> from
|
||||
<code>./public/css/style.css</code>. The handler supports
|
||||
<code>If-Modified-Since</code> for 304 responses.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chapter" id="API">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Version-History" accesskey="n" rel="next">Version History</a>, Previous: <a href="#Guidance" accesskey="p" rel="prev">Guidance</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="API-1"></span><h2 class="chapter">2 API</h2>
|
||||
<p>The following is the list of modules provided by this library.
|
||||
</p>
|
||||
|
||||
|
||||
<ul class="section-toc">
|
||||
<li><a href="#safsaf" accesskey="1">(safsaf)</a></li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="section" id="safsaf">
|
||||
<div class="header">
|
||||
<p>
|
||||
Up: <a href="#API" accesskey="u" rel="up">API</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="g_t_0028safsaf_0029"></span><h3 class="section">2.1 (safsaf)</h3>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<ul class="section-toc">
|
||||
<li><a href="#Procedures" accesskey="1">Procedures</a></li>
|
||||
</ul>
|
||||
<div class="subsection" id="Procedures">
|
||||
<h4 class="subsection">2.1.1 Procedures</h4>
|
||||
|
||||
|
||||
<dl class="def">
|
||||
<dt id="index-default_002dmethod_002dnot_002dallowed_002dhandler"><span class="category">Procedure: </span><span><strong>default-method-not-allowed-handler</strong> <em>a b</em><a href='#index-default_002dmethod_002dnot_002dallowed_002dhandler' class='copiable-anchor'> ¶</a></span></dt>
|
||||
<dd><p>Return a 405 Method Not Allowed response with an Allow header listing
|
||||
ALLOWED-METHODS.
|
||||
</p>
|
||||
</dd></dl>
|
||||
|
||||
|
||||
|
||||
|
||||
<dl class="def">
|
||||
<dt id="index-run_002dsafsaf"><span class="category">Procedure: </span><span><strong>run-safsaf</strong> <em>_ KEY: #:host #:port #:method-not-allowed? #:method-not-allowed-handler #:connection-buffer-size</em><a href='#index-run_002dsafsaf' class='copiable-anchor'> ¶</a></span></dt>
|
||||
<dd><p>Start a Safsaf web server.
|
||||
</p>
|
||||
<p>ROUTES is a list of routes and route-groups (as returned by component
|
||||
constructors). The last route must be a catch-all so that every request
|
||||
is handled.
|
||||
</p>
|
||||
<p>HEAD requests are handled automatically: when no explicit HEAD route
|
||||
matches, the matching GET handler runs and its response body is
|
||||
discarded. Explicit HEAD routes always take precedence.
|
||||
</p>
|
||||
<p>When METHOD-NOT-ALLOWED? is #t (the default), requests that match a
|
||||
route’s path but not its method receive a 405 response with an Allow
|
||||
header. METHOD-NOT-ALLOWED-HANDLER is a procedure (request
|
||||
allowed-methods) -> (values response body) that produces the 405
|
||||
response; the default returns plain text.
|
||||
</p>
|
||||
<p>When called outside a Fibers scheduler, sets up a scheduler, starts the
|
||||
HTTP server, and blocks until Ctrl-C. When called inside an existing
|
||||
scheduler (e.g. within run-fibers), just starts the HTTP server and
|
||||
returns immediately — the caller manages the lifecycle.
|
||||
</p>
|
||||
</dd></dl>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="appendix" id="Version-History">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Copying-Information" accesskey="n" rel="next">Copying Information</a>, Previous: <a href="#API" accesskey="p" rel="prev">API</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Version-History-1"></span><h2 class="appendix">Appendix A Version History</h2>
|
||||
|
||||
<dl compact="compact">
|
||||
<dt><span><em>Version 0.1</em></span></dt>
|
||||
<dd><ul>
|
||||
<li> Initial release.
|
||||
</li><li> Built on the code of the Guix Data Serivce, plus other web
|
||||
services like the Guix Build Coordinator and Nar Herder.
|
||||
</li><li> Written using Claude Opus 4.6 using Claude Code.
|
||||
</li></ul>
|
||||
|
||||
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="appendix" id="Copying-Information">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Concept-Index" accesskey="n" rel="next">Concept Index</a>, Previous: <a href="#Version-History" accesskey="p" rel="prev">Version History</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Copying-Information-1"></span><h2 class="appendix">Appendix B Copying Information</h2>
|
||||
|
||||
<p>Copyright © 2026 Christopher Baines <mail@cbaines.net>
|
||||
</p>
|
||||
<p>This library is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU Lesser General Public License as
|
||||
published by the Free Software Foundation; either version 3 of the
|
||||
License, or (at your option) any later version.
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="unnumbered" id="Concept-Index">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Data-Type-Index" accesskey="n" rel="next">Data Type Index</a>, Previous: <a href="#Copying-Information" accesskey="p" rel="prev">Copying Information</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Concept-Index-1"></span><h2 class="unnumbered">Concept Index</h2>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="unnumbered" id="Data-Type-Index">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Procedure-Index" accesskey="n" rel="next">Procedure Index</a>, Previous: <a href="#Concept-Index" accesskey="p" rel="prev">Concept Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Data-Type-Index-1"></span><h2 class="unnumbered">Data Type Index</h2>
|
||||
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="unnumbered" id="Procedure-Index">
|
||||
<div class="header">
|
||||
<p>
|
||||
Next: <a href="#Variable-Index" accesskey="n" rel="next">Variable Index</a>, Previous: <a href="#Data-Type-Index" accesskey="p" rel="prev">Data Type Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Procedure-Index-1"></span><h2 class="unnumbered">Procedure Index</h2>
|
||||
|
||||
<table><tr><th valign="top">Jump to: </th><td><a class="summary-letter" href="#Procedure-Index_fn_letter-D"><b>D</b></a>
|
||||
|
||||
<a class="summary-letter" href="#Procedure-Index_fn_letter-R"><b>R</b></a>
|
||||
|
||||
</td></tr></table>
|
||||
<table class="index-fn" border="0">
|
||||
<tr><td></td><th align="left">Index Entry</th><td> </td><th align="left"> Section</th></tr>
|
||||
<tr><td colspan="4"> <hr></td></tr>
|
||||
<tr><th id="Procedure-Index_fn_letter-D">D</th><td></td><td></td></tr>
|
||||
<tr><td></td><td valign="top"><a href="#index-default_002dmethod_002dnot_002dallowed_002dhandler"><code>default-method-not-allowed-handler</code></a>:</td><td> </td><td valign="top"><a href="#safsaf">safsaf</a></td></tr>
|
||||
<tr><td colspan="4"> <hr></td></tr>
|
||||
<tr><th id="Procedure-Index_fn_letter-R">R</th><td></td><td></td></tr>
|
||||
<tr><td></td><td valign="top"><a href="#index-run_002dsafsaf"><code>run-safsaf</code></a>:</td><td> </td><td valign="top"><a href="#safsaf">safsaf</a></td></tr>
|
||||
<tr><td colspan="4"> <hr></td></tr>
|
||||
</table>
|
||||
<table><tr><th valign="top">Jump to: </th><td><a class="summary-letter" href="#Procedure-Index_fn_letter-D"><b>D</b></a>
|
||||
|
||||
<a class="summary-letter" href="#Procedure-Index_fn_letter-R"><b>R</b></a>
|
||||
|
||||
</td></tr></table>
|
||||
|
||||
|
||||
<hr>
|
||||
</div>
|
||||
<div class="unnumbered" id="Variable-Index">
|
||||
<div class="header">
|
||||
<p>
|
||||
Previous: <a href="#Procedure-Index" accesskey="p" rel="prev">Procedure Index</a>, Up: <a href="#Top" accesskey="u" rel="up">Overview</a> [<a href="#SEC_Contents" title="Table of contents" rel="contents">Contents</a>][<a href="#Concept-Index" title="Index" rel="index">Index</a>]</p>
|
||||
</div>
|
||||
<span id="Variable-Index-1"></span><h2 class="unnumbered">Variable Index</h2>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue