Mercurial Hosting > d2o
changeset 6:54ab94198677
abstract mixins + building sctipt + documentation
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Tue, 10 Mar 2026 10:22:02 +0500 |
| parents | 07b6f06899e0 |
| children | 8e4813b4e509 |
| files | icf/icf.go justfile man/d2o.1.md man/d2obase.5.md |
| diffstat | 4 files changed, 225 insertions(+), 7 deletions(-) [+] |
line wrap: on
line diff
--- a/icf/icf.go Mon Mar 09 03:35:47 2026 +0500 +++ b/icf/icf.go Tue Mar 10 10:22:02 2026 +0500 @@ -17,9 +17,10 @@ } type Config struct { - vars map[string]string - abstract map[string][]Directive - Blocks []ParsedBlock + vars map[string]string + abstract map[string][]Directive + abstractMixin map[string]string // mixin name for each abstract block + Blocks []ParsedBlock } type ParsedBlock struct { @@ -103,8 +104,9 @@ // --- Pass 3: separate abstract from concrete, apply var substitution --- c := &Config{ - vars: vars, - abstract: make(map[string][]Directive), + vars: vars, + abstract: make(map[string][]Directive), + abstractMixin: make(map[string]string), } for _, rb := range raws { @@ -118,7 +120,11 @@ } if strings.HasPrefix(rb.id, "@") { - c.abstract[rb.id[1:]] = dirs + name := rb.id[1:] + c.abstract[name] = dirs + if rb.mixin != "" { + c.abstractMixin[name] = rb.mixin + } } else { c.Blocks = append(c.Blocks, ParsedBlock{ ID: rb.id, @@ -185,7 +191,7 @@ func (c *Config) ResolveBlock(b ParsedBlock, caps map[string]string) []Directive { var merged []Directive if b.Mixin != "" { - merged = append(merged, c.abstract[b.Mixin]...) + merged = append(merged, c.resolveAbstract(b.Mixin, make(map[string]bool))...) } merged = append(merged, b.Directives...) @@ -206,6 +212,22 @@ return out } +// resolveAbstract recursively expands an abstract block and its mixin chain. +// visited guards against circular references. +func (c *Config) resolveAbstract(name string, visited map[string]bool) []Directive { + if visited[name] { + return nil + } + visited[name] = true + + var out []Directive + if parent, ok := c.abstractMixin[name]; ok { + out = append(out, c.resolveAbstract(parent, visited)...) + } + out = append(out, c.abstract[name]...) + return out +} + // makeSubst returns a function that substitutes $VAR in s, // checking caps first, then vars. func makeSubst(vars map[string]string) func(s string, caps map[string]string) string {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/justfile Tue Mar 10 10:22:02 2026 +0500 @@ -0,0 +1,7 @@ +build: d2o man + +d2o: + go build . +man: + lowdown -s -tman man/d2o.1.md -o d2o.1 + lowdown -s -tman man/d2obase.5.md -o d2obase.5 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/man/d2o.1.md Tue Mar 10 10:22:02 2026 +0500 @@ -0,0 +1,24 @@ +% d2o(1) +% 2026-03-10 + +# NAME +d2o (heavy h2o) - an experimental web server inspired by h2o project + +# SYNOPSIS +**d2o** \[*config*\] + +# DESCRIPTION +d2o is a webserver, **the main feature** of which is its **unique ICF configuration format**, which uses inheritance and string substitution as the primary means of managing websites. + +Instead of spawning endless blocks and drowning in `location/`, It just uses pipelines. The entire server, with all subdomains, TLS, and proxying, fits into 12 lines that read from top to bottom. If a folder exists in $WWW, it automatically becomes a site through named wildcards. It’s not magic, it’s just the absence of unnecessary noise in a system that doesn’t try to appear more complex than it is. + +# OPTIONS +*config* +: The path to the configuration file in ICF format. If not specified, `/etc/d2obase' is used. + +# FILES +*/etc/d2obase* +: The default configuration file. + +# SEE ALSO +d2obase(5) \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/man/d2obase.5.md Tue Mar 10 10:22:02 2026 +0500 @@ -0,0 +1,165 @@ +% D2OBASE(5) +% March 2026 + +# NAME +d2obase - syntax and configuration guide for the d2o web server + +# DESCRIPTION +The **ICF** (Inheritance Config Format) uses a stream-oriented approach to web serving. It focuses on reducing "boilerplate" by using variables, reusable mixins, and execution pipelines. + +# SYNTAX +A configuration file consists of variable assignments, mixin definitions, and site blocks containing pipelines. + +## Variables +Variables are defined as `KEY=VALUE`. They are global and can be referenced later using the `$` prefix (e.g., `$DOM`). + +Variable names may only contain characters `[A-Za-z0-9_]`. Values are substituted at the point of declaration, so a variable can reference only those variables defined above it in the file. + +## Mixins (Templates) +A mixin is defined with the `@` prefix. It stores a set of pipeline directives that can be "splatted" into a site block to avoid repetition. + + @name + |> directive argument + +Mixins can inherit from other mixins: + + @child @parent + |> directive argument + +The parent's directives are inserted before the child's, so the child can override them. Circular references are silently ignored. + +## Pipelines and Directives +The core of the configuration is the pipeline. Each line starting with `|>` is a directive executed in top-down order. + +Directives follow the format: `|> directive [arguments...]`. + +### Argument Expansion +Directives support curly brace expansion `{}`. For example: +`file.{crt,key}` expands to two separate arguments: `file.crt` and `file.key`. + +Only one pair of braces per argument is expanded. Nested braces are not supported. + +## Site Blocks +A site block starts with a domain name (or a variable containing one), followed by optional mixins, and a pipeline. + + example.org @setup + |> directive argument + +### Pattern Matching +The domain part of a site block is matched exactly. The path part (if present, separated by `/`) is matched by prefix. + +Named capture groups can be used in patterns with `<name>` syntax. The captured value is available as `$name` in the block's directives. + + <sub>.example.org + |> root /srv/www/$sub + +#### Block Specificity + +When multiple blocks match a request, the most specific one wins. Specificity is the number of literal characters matched in the pattern - capture groups `<n>` do not contribute to the score. Host and path are scored separately; host score is weighted heavier (`score = host_score × 1000 + path_score`). + +For example, given these two blocks: + + app.example.org + |> rprx http://127.0.0.1:8080 + + <sub>.example.org + |> root /srv/www/$sub + +A request for `app.example.org` matches both, but `app.example.org` wins - it matched 15 literal characters against 11 for `<sub>.example.org` (`.example.org` only). Order of declaration in the file does not matter. + +## Comments +Lines beginning with `;` are ignored. Inline comments start with ` ;` (space before semicolon). + + ; full line comment + KEY=value ; inline comment + +# DIRECTIVES + +## Listening + +**port** *number* +: Listen for plain HTTP on the given port. + +**port+tls** *number* [*cert* *key*] +: Listen for HTTPS on the given port. Certificate and key paths are optional if a **tls** directive is present in the same block. + +**tls** *cert* *key* +: Set the default certificate and key for **port+tls** directives in this block that do not specify their own paths. + +## Serving + +**root** *path* [**show**] +: Serve static files from *path*. Without **show**, directory listing is forbidden (403). With **show**, an HTML directory listing is returned. + +**ndex** *file...* +: List of index filenames to try when a directory is requested, checked left to right. If a matching file passes the **fcgi** pattern, it is handled by FastCGI instead of served directly. + +**fcgi** *address* [*pattern*] +: Forward matching requests to a FastCGI server. *address* is either `unix:///path/to/socket` or `host:port`. *pattern* is a glob matched against the request path (default `*`). When used together with **root**, only requests matching the pattern are forwarded; everything else is served as static. + +**rprx** *address* +: Reverse-proxy all requests to *address*. The `http://` scheme is assumed if not specified. + +**rdir** *code* *url* +: Redirect to *url* with the given HTTP status code. If *code* is omitted or zero, 302 is used. + +## Global (in `@d2o` block only) + +**threads** *n* +: Set `GOMAXPROCS` to *n*. + +# EXAMPLES +Below is a complex setup using global variables, a shared mixin for PHP sites, and subdomain handling: + + DOM=qwaderton.org + ACME=/etc/acme + WWW=/srv/www/$DOM + + @ports + |> port 80 + |> port+tls 443 $ACME/$DOM.{crt,key} + |> ndex index.php index.html + + @ports+fcgi @ports + |> fcgi unix:///run/php-fpm.sock *.php + + $DOM @ports+fcgi + |> root $WWW show + + <sub>.$DOM @ports+fcgi + |> root $WWW/$sub + + app.$DOM @ports + |> rprx http://127.0.0.1:8080 + + qwa.su + |> port 80 + |> port+tls 443 $ACME/qwa.su.{crt,key} + |> rdir 307 https://qwaderton.org/ + +We use this configuration on our web server (as it is at the time of writing this manual). + +If you need something simplier to start quickly, use this config: + + DOM=mydomain + TLS=/etc/acme/$DOM + WWW=/srv/www/$DOM + + $DOM + |> port 80 + |> port+tls 443 $TLS.{crt,key} + |> root $WWW show + +# CAVEATS +1. ICF does not support strings with spaces - there are **no quotes or escapes**. Every character except space is treated as part of a token, including `!`, `*`, `/`, and so on. Glob patterns passed to **fcgi** are forwarded as-is to the server. + +2. Variables are evaluated top-down at the point of declaration. Forward references do not work: if `B=$A` appears before `A=value`, `B` will contain the literal `$A`. + +3. Only one pair of curly braces is expanded per argument. `file.{a,b}.{x,y}` does not produce four arguments - only the first `{}` pair is expanded. + +4. A directive outside of any block (a `|>` line before the first block header) is a parse error and will prevent the server from starting. + +5. Duplicate ports across blocks are silently deduplicated - the first declaration wins. TLS certificate paths from a later block with the same port are ignored. + +# SEE ALSO +**d2o**(1) \ No newline at end of file
