Compare commits

..
1 Commits
Author SHA1 Message Date
nightshade 0155e6ddf0 nerak repo 2026-07-31 15:48:58 -05:00
26 changed files with 64 additions and 34 deletions
+64 -34
View File
@@ -70,7 +70,7 @@ Four principles:
* **(A)utonomous:** modules are self-contained: own schemas, migrations, seeds, routes, UI, and logic. The compiler enforces boundaries. * **(A)utonomous:** modules are self-contained: own schemas, migrations, seeds, routes, UI, and logic. The compiler enforces boundaries.
* **(D)omain Based:** each module owns one slice of the app. A `todos` module defines everything related to todos and nothing else. * **(D)omain Based:** each module owns one slice of the app. A `todos` module defines everything related to todos and nothing else.
Influenced by: Inspired by:
* [Data Oriented Design](https://youtu.be/rX0ItVEVjHc) * [Data Oriented Design](https://youtu.be/rX0ItVEVjHc)
* [A Philosophy of Software Design](https://youtu.be/bmSAYlu0NcY) * [A Philosophy of Software Design](https://youtu.be/bmSAYlu0NcY)
@@ -98,7 +98,7 @@ Each `resource(...)` declares a named URL endpoint; each verb pipeline is a list
Both pages share a layout, so `home` doubles as the layout: it declares the nav and a `{{$body}}` block whose default is the welcome page. The `todos` page extends it with `{{< home}}...{{/home}}`, overriding that block. Any template that declares a `{{$block}}` can be a parent; there is no special layout type. Both pages share a layout, so `home` doubles as the layout: it declares the nav and a `{{$body}}` block whose default is the welcome page. The `todos` page extends it with `{{< home}}...{{/home}}`, overriding that block. Any template that declares a `{{$block}}` can be a parent; there is no special layout type.
**`home.html`** **`home.mustache.html`**
```html ```html
<html> <html>
<body> <body>
@@ -112,7 +112,7 @@ Both pages share a layout, so `home` doubles as the layout: it declares the nav
</html> </html>
``` ```
**`todos.html`** **`todos.mustache.html`**
```html ```html
{{< home}} {{< home}}
{{$body}} {{$body}}
@@ -170,7 +170,7 @@ select id, title from todos;
Render the rows Nerak stores under `todos_data`: Render the rows Nerak stores under `todos_data`:
**`todos.html`** **`todos.mustache.html`**
```diff ```diff
{{< home}} {{< home}}
{{$body}} {{$body}}
@@ -229,16 +229,11 @@ insert into todos(title) values({{title}});
Add the form, repopulating the field and showing the error after a failed submit: Add the form, repopulating the field and showing the error after a failed submit:
**`todos.html`** **`todos.mustache.html`**
```diff ```diff
{{< home}} {{< home}}
{{$body}} {{$body}}
<h1>My Todos</h1> <h1>My Todos</h1>
<ul>
{{#todos_data}}
<li>{{title}}</li>
{{/todos_data}}
</ul>
+ <form method='post' action='{{url:todos}}'> + <form method='post' action='{{url:todos}}'>
+ {{csrf:input}} + {{csrf:input}}
+ <input name='title' value='{{input:title}}'> + <input name='title' value='{{input:title}}'>
@@ -247,6 +242,11 @@ Add the form, repopulating the field and showing the error after a failed submit
+ {{/error:title}} + {{/error:title}}
+ <button>Add</button> + <button>Add</button>
+ </form> + </form>
<ul>
{{#todos_data}}
<li>{{title}}</li>
{{/todos_data}}
</ul>
{{/body}} {{/body}}
{{/home}} {{/home}}
``` ```
@@ -321,7 +321,7 @@ select id, todo_id, body from comments where todo_id = {{id}};
Enter `{{#todo_data}}` first; after the join, `comments` lives inside each todo record: Enter `{{#todo_data}}` first; after the join, `comments` lives inside each todo record:
**`todo.html`** **`todo.mustache.html`**
```html ```html
{{< home}} {{< home}}
{{$body}} {{$body}}
@@ -340,7 +340,7 @@ Enter `{{#todo_data}}` first; after the join, `comments` lives inside each todo
Link each list item to its detail page. `{{url:todo}}` resolves to the `todo` resource's pattern (`/todos/:id`) and fills `:id` from the current row, so no argument is needed: Link each list item to its detail page. `{{url:todo}}` resolves to the `todo` resource's pattern (`/todos/:id`) and fills `:id` from the current row, so no argument is needed:
**`todos.html`** **`todos.mustache.html`**
```diff ```diff
{{#todos_data}} {{#todos_data}}
- <li>{{title}}</li> - <li>{{title}}</li>
@@ -410,7 +410,7 @@ Both queries in one `sqlite_query()` call run concurrently. `join()` lifts `comm
Show the responses on the home page: Show the responses on the home page:
**`home.html`** **`home.mustache.html`**
```diff ```diff
<html> <html>
<body> <body>
@@ -458,6 +458,36 @@ Fetch both services concurrently before rendering:
respond("home_s") respond("home_s")
} }
); );
resource("todos", "/todos",
.get = {
sqlite_query({"todos_db", "get_todos", "todos_data"}),
mustache("todos", "todos_s"),
respond("todos_s")
},
.post = {
input({"title", n_not_empty}),
sqlite_query({"todos_db", "create_todo"}),
redirect("todos")
},
.errors = {
{n_bad_request, {reroute("todos")}}
}
);
resource("todo", "/todos/:id",
.get = {
input({"id", n_int}),
sqlite_query(
{"todos_db", "get_todo", "todo_data", .err_on_empty = true},
{"todos_db", "get_comments", "comments"}
),
join("todo_data", "id", "comments", "todo_id"),
mustache("todo", "todo_s"),
respond("todo_s")
}
);
}
``` ```
Both requests run concurrently under one `fetch()` call. The JSON parses into context tables the template walks with `{{#quote}}` and `{{#weather}}`. `fetch()` also supports other verbs, headers, request bodies, and interpolated URLs. See [fetch](#fetch). Both requests run concurrently under one `fetch()` call. The JSON parses into context tables the template walks with `{{#quote}}` and `{{#weather}}`. `fetch()` also supports other verbs, headers, request bodies, and interpolated URLs. See [fetch](#fetch).
@@ -560,15 +590,15 @@ Register the migration, define the tasks, dispatch them from the POST:
Split features into modules that talk through pub/sub events. A module is any `.c` file declaring `config(name){ ... }`, registering that module's resources, databases, tasks, and subscribers. Nerak discovers every `config(...)` on disk. A module's assets live in its `name/` folder and are private to it; assets at the project root are shared across modules (see [Assets](#assets)). Split features into modules that talk through pub/sub events. A module is any `.c` file declaring `config(name){ ... }`, registering that module's resources, databases, tasks, and subscribers. Nerak discovers every `config(...)` on disk. A module's assets live in its `name/` folder and are private to it; assets at the project root are shared across modules (see [Assets](#assets)).
This step moves todos into its own module and adds an `activity` module that records an entry whenever a todo is created. `app.c` keeps just the `home` resource; `home.html` stays at the project root, shared across modules: This step moves todos into its own module and adds an `activity` module that records an entry whenever a todo is created. `app.c` keeps just the `home` resource; `home.mustache.html` stays at the project root, shared across modules:
``` ```
. .
├── app.c ├── app.c
├── todos/ ├── todos/
│ ├── todos.c │ ├── todos.c
│ ├── todos.html │ ├── todos.mustache.html
│ ├── todo.html │ ├── todo.mustache.html
│ ├── create_todos_table.sql │ ├── create_todos_table.sql
│ ├── seed_todos.sql │ ├── seed_todos.sql
│ ├── get_todos.sql │ ├── get_todos.sql
@@ -577,11 +607,11 @@ This step moves todos into its own module and adds an `activity` module that rec
│ └── create_todo.sql │ └── create_todo.sql
├── activity/ ├── activity/
│ ├── activity.c │ ├── activity.c
│ ├── activity.html │ ├── activity.mustache.html
│ ├── create_activity_table.sql │ ├── create_activity_table.sql
│ ├── get_activities.sql │ ├── get_activities.sql
│ └── insert_activity.sql │ └── insert_activity.sql
└── home.html └── home.mustache.html
``` ```
**`app.c`** **`app.c`**
@@ -655,7 +685,7 @@ This step moves todos into its own module and adds an `activity` module that rec
Add an Activity link to the shared nav: Add an Activity link to the shared nav:
**`home.html`** **`home.mustache.html`**
```diff ```diff
<html> <html>
<body> <body>
@@ -764,7 +794,7 @@ insert into activities(kind, ref) values('created', {{title}});
select kind, ref, created_at from activities order by created_at desc; select kind, ref, created_at from activities order by created_at desc;
``` ```
**`activity/activity.html`** **`activity/activity.mustache.html`**
```html ```html
{{< home}} {{< home}}
{{$body}} {{$body}}
@@ -923,9 +953,9 @@ Built-in helpers use `{{helper:args}}` syntax. Arguments are colon-separated, in
### Assets ### Assets
Every non-`.c` file is an asset. An asset is loaded into context under its name in each module that seeds it (see below), as if `context(name, contents)` had been called there. Common types are Mustache templates (`*.html`), Markdown (`*.md`), and SQL (`*.sql`); the rule is general. Every non-`.c` file is an asset. An asset is loaded into context under its name in each module that seeds it (see below), as if `context(name, contents)` had been called there. Common types are Mustache templates (`*.mustache.html`), Markdown (`*.md`), and SQL (`*.sql`); the rule is general.
An asset's name is the filename's basename (the part before the first dot). `get_todos.sql` seeds `get_todos`, `todos.html` seeds `todos`, `home.md` seeds `home`. Steps read these like any other context value: `mustache("todos", "todos_s")`, `sqlite_query({"todos_db", "get_todos", "todos_data"})`, and `.migrations`/`.seeds` entries (`{"create_todos_table"}`). An asset's name is the filename's basename (the part before the first dot). `get_todos.sql` seeds `get_todos`, `todos.mustache.html` seeds `todos`, `home.md` seeds `home`. Steps read these like any other context value: `mustache("todos", "todos_s")`, `sqlite_query({"todos_db", "get_todos", "todos_data"})`, and `.migrations`/`.seeds` entries (`{"create_todos_table"}`).
`mustache()`, `mdm()`, and the engine `*_query()` steps read a string from context by key and interpret it as a template or SQL. The step interprets whatever is under the key when it runs. `mustache()`, `mdm()`, and the engine `*_query()` steps read a string from context by key and interpret it as a template or SQL. The step interprets whatever is under the key when it runs.
@@ -939,15 +969,15 @@ A module is seeded with every asset from its own folder up to the project root.
``` ```
. .
├── layout.html # → every module ├── layout.mustache.html # → every module
├── 404.html # → every module ├── 404.mustache.html # → every module
├── partials/ ├── partials/
│ └── footer.html # → every module (partials/ has no config()) │ └── footer.mustache.html # → every module (partials/ has no config())
└── todos/ └── todos/
├── todos.c # the "todos" module ├── todos.c # the "todos" module
├── todos.html # → todos ├── todos.mustache.html # → todos
└── detail/ └── detail/
└── todo.html # → todos (detail/ has no config()) └── todo.mustache.html # → todos (detail/ has no config())
``` ```
In dev, the scan is live: editing an asset reloads that file into every module holding it, and saving a `.c` file recompiles and reloads that module alone. A production build runs the scan once and compiles each module's assets into its binary; see [Deployment](#deployment). In dev, the scan is live: editing an asset reloads that file into every module holding it, and saving a `.c` file recompiles and reloads that module alone. A production build runs the scan once and compiles each module's assets into its binary; see [Deployment](#deployment).
@@ -1422,7 +1452,7 @@ join(.parent_ctx_key = "projects", .parent_field_key = "id", .child_ctx_key = "t
**Full context example.** Concurrent query → `join()``mustache()`: fetch parent and children from separate queries, render as one nested structure. Blog + comments, single database: **Full context example.** Concurrent query → `join()``mustache()`: fetch parent and children from separate queries, render as one nested structure. Blog + comments, single database:
**`blog.html`** **`blog.mustache.html`**
```html ```html
<article> <article>
{{#blog}} {{#blog}}
@@ -2055,16 +2085,16 @@ A typical project layout:
├── app.c # app module (config(app){ ... }) ├── app.c # app module (config(app){ ... })
├── todos/ # todos module ├── todos/ # todos module
│ ├── todos.c # config(todos){ ... } │ ├── todos.c # config(todos){ ... }
│ ├── todos.html # → todos │ ├── todos.mustache.html # → todos
│ ├── create_todos_table.sql # → todos │ ├── create_todos_table.sql # → todos
│ └── get_todos.sql # → todos │ └── get_todos.sql # → todos
├── activity/ # activity module ├── activity/ # activity module
│ └── activity.c # config(activity){ ... } │ └── activity.c # config(activity){ ... }
├── public/ # static files, served directly ├── public/ # static files, served directly
│ └── favicon.png │ └── favicon.png
├── layout.html # → every module ├── layout.mustache.html # → every module
├── 404.html # → every module ├── 404.mustache.html # → every module
└── 5xx.html # → every module └── 5xx.mustache.html # → every module
``` ```
![App Composition Tree](./images/05-app-composition-tree.svg) ![App Composition Tree](./images/05-app-composition-tree.svg)
@@ -2373,7 +2403,7 @@ Nerak prevents common C and web vulnerabilities at the framework level.
Each reactor maintains a pool of arena allocators. When a request arrives, the pipeline is assigned an arena, and all allocations draw from it. When the pipeline completes, the arena is cleared and returned to the pool. Application code does not call `malloc` or `free` (use `alloc()` and `defer_free()` from the [Imperative API](#memory) for raw buffers), avoiding leaks, double-frees, and use-after-free. Each reactor maintains a pool of arena allocators. When a request arrives, the pipeline is assigned an arena, and all allocations draw from it. When the pipeline completes, the arena is cleared and returned to the pool. Application code does not call `malloc` or `free` (use `alloc()` and `defer_free()` from the [Imperative API](#memory) for raw buffers), avoiding leaks, double-frees, and use-after-free.
All framework data structures (tables, records, strings) enforce bounds checking. Out-of-bounds reads and missing context values return `nullptr` rather than faulting. Pipelines exceeding their memory limit (default 5MB, configurable in `compose.yml`) abort with a 500, mitigating OOM denial-of-service. All framework data structures (tables, records, strings) enforce bounds checking. Out-of-bounds reads and missing context values return `nullptr` rather than faulting. Pipelines exceeding their memory limit (configurable in `compose.yml`) abort with a 500, mitigating OOM denial-of-service.
#### SQL Injection Prevention #### SQL Injection Prevention
@@ -2462,4 +2492,4 @@ Ships with integrated infrastructure: source control, issue tracking, wiki, foru
## License ## License
Nerak is licensed under the [LGPL](./LICENSE). Nerak is licensed under the [LGPL](./LICENSE). Your application code can be any license, its a Nerak plugin.