MaCH repo

This commit is contained in:
2025-07-24 12:46:01 -05:00
committed by Nick Ricketts
commit 354a31a290
106 changed files with 6263 additions and 0 deletions

13
00_hello_text/main.c Normal file
View File

@@ -0,0 +1,13 @@
#include <mach.h>
config mach(){
return (config) {
.resources = {{
{"home", "/", mime_txt,
.get = {{
render((r){"hello", .mime = mime_txt})
}}
}
}}
};
}

View File

@@ -0,0 +1,139 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 920 500" font-family="system-ui, -apple-system, sans-serif">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#64748b"/>
</marker>
<marker id="arrow-amber" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#f59e0b"/>
</marker>
<marker id="arrow-emerald" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<path d="M0,0 L8,3 L0,6" fill="#10b981"/>
</marker>
</defs>
<!-- Background -->
<rect width="920" height="500" rx="12" fill="#0f172a"/>
<!-- Title -->
<text x="460" y="38" text-anchor="middle" fill="#f8fafc" font-size="18" font-weight="600">Multi-Reactor Architecture</text>
<!-- HTTP Clients -->
<rect x="30" y="105" width="120" height="44" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1.5"/>
<text x="90" y="132" text-anchor="middle" fill="#94a3b8" font-size="13" font-weight="500">HTTP Clients</text>
<!-- Arrows from clients to request reactors -->
<line x1="150" y1="117" x2="200" y2="117" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="150" y1="127" x2="200" y2="200" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
<line x1="150" y1="137" x2="200" y2="285" stroke="#64748b" stroke-width="1.5" marker-end="url(#arrow)"/>
<!-- Request Reactors Group -->
<rect x="195" y="72" width="280" height="264" rx="10" fill="none" stroke="#334155" stroke-width="1" stroke-dasharray="6,4"/>
<text x="335" y="92" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="600" letter-spacing="0.5">REQUEST REACTORS</text>
<!-- Request Reactor 0 -->
<rect x="210" y="104" width="250" height="44" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1.5"/>
<text x="225" y="131" fill="#93c5fd" font-size="11" font-weight="600">Core 0</text>
<text x="280" y="131" fill="#cbd5e1" font-size="11">event loop</text>
<rect x="370" y="113" width="80" height="26" rx="5" fill="#1e293b" stroke="#475569" stroke-width="1"/>
<text x="410" y="131" text-anchor="middle" fill="#94a3b8" font-size="10">pipeline</text>
<!-- Request Reactor 1 -->
<rect x="210" y="162" width="250" height="44" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1.5"/>
<text x="225" y="189" fill="#93c5fd" font-size="11" font-weight="600">Core 1</text>
<text x="280" y="189" fill="#cbd5e1" font-size="11">event loop</text>
<rect x="370" y="171" width="80" height="26" rx="5" fill="#1e293b" stroke="#475569" stroke-width="1"/>
<text x="410" y="189" text-anchor="middle" fill="#94a3b8" font-size="10">pipeline</text>
<!-- Ellipsis dots -->
<circle cx="335" cy="224" r="2" fill="#475569"/>
<circle cx="335" cy="234" r="2" fill="#475569"/>
<circle cx="335" cy="244" r="2" fill="#475569"/>
<!-- Request Reactor N -->
<rect x="210" y="258" width="250" height="44" rx="8" fill="#1e3a5f" stroke="#3b82f6" stroke-width="1.5"/>
<text x="225" y="285" fill="#93c5fd" font-size="11" font-weight="600">Core N</text>
<text x="280" y="285" fill="#cbd5e1" font-size="11">event loop</text>
<rect x="370" y="267" width="80" height="26" rx="5" fill="#1e293b" stroke="#475569" stroke-width="1"/>
<text x="410" y="285" text-anchor="middle" fill="#94a3b8" font-size="10">pipeline</text>
<!-- Task Reactor Group -->
<rect x="195" y="365" width="280" height="90" rx="10" fill="none" stroke="#334155" stroke-width="1" stroke-dasharray="6,4"/>
<text x="335" y="385" text-anchor="middle" fill="#fbbf24" font-size="12" font-weight="600" letter-spacing="0.5">TASK REACTOR</text>
<rect x="210" y="395" width="250" height="44" rx="8" fill="#422006" stroke="#f59e0b" stroke-width="1.5"/>
<text x="225" y="422" fill="#fcd34d" font-size="11" font-weight="600">Task</text>
<text x="260" y="422" fill="#cbd5e1" font-size="11">event loop</text>
<rect x="370" y="404" width="80" height="26" rx="5" fill="#1e293b" stroke="#475569" stroke-width="1"/>
<text x="410" y="422" text-anchor="middle" fill="#94a3b8" font-size="10">cron / jobs</text>
<!-- Task DB -->
<rect x="30" y="395" width="120" height="44" rx="8" fill="#1e293b" stroke="#f59e0b" stroke-width="1.5"/>
<text x="90" y="414" text-anchor="middle" fill="#fcd34d" font-size="11" font-weight="600">mach_tasks</text>
<text x="90" y="429" text-anchor="middle" fill="#94a3b8" font-size="10">database</text>
<!-- Arrows: task DB to/from task reactor -->
<line x1="150" y1="410" x2="205" y2="412" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<line x1="205" y1="424" x2="150" y2="424" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow-amber)"/>
<!-- ==================== SHARED THREAD POOL ==================== -->
<rect x="560" y="72" width="330" height="264" rx="10" fill="none" stroke="#334155" stroke-width="1" stroke-dasharray="6,4"/>
<text x="725" y="96" text-anchor="middle" fill="#34d399" font-size="12" font-weight="600" letter-spacing="0.5">SHARED THREAD POOL</text>
<!-- Work queue label above queue image -->
<text x="725" y="118" text-anchor="middle" fill="#6b7280" font-size="10">work queue</text>
<!-- Queue visualization -->
<rect x="588" y="126" width="274" height="36" rx="6" fill="#1e293b" stroke="#475569" stroke-width="1"/>
<!-- Filled queue slots -->
<rect x="596" y="132" width="24" height="24" rx="4" fill="#064e3b" stroke="#10b981" stroke-width="1"/>
<rect x="626" y="132" width="24" height="24" rx="4" fill="#064e3b" stroke="#10b981" stroke-width="1"/>
<rect x="656" y="132" width="24" height="24" rx="4" fill="#064e3b" stroke="#10b981" stroke-width="1"/>
<rect x="686" y="132" width="24" height="24" rx="4" fill="#064e3b" stroke="#10b981" stroke-width="1"/>
<!-- Empty queue slots -->
<rect x="716" y="132" width="24" height="24" rx="4" fill="#0f172a" stroke="#334155" stroke-width="1" stroke-dasharray="3,2"/>
<rect x="746" y="132" width="24" height="24" rx="4" fill="#0f172a" stroke="#334155" stroke-width="1" stroke-dasharray="3,2"/>
<rect x="776" y="132" width="24" height="24" rx="4" fill="#0f172a" stroke="#334155" stroke-width="1" stroke-dasharray="3,2"/>
<rect x="806" y="132" width="24" height="24" rx="4" fill="#0f172a" stroke="#334155" stroke-width="1" stroke-dasharray="3,2"/>
<rect x="836" y="132" width="18" height="24" rx="4" fill="#0f172a" stroke="#334155" stroke-width="1" stroke-dasharray="3,2"/>
<!-- Arrows from queue down to threads -->
<line x1="640" y1="162" x2="640" y2="198" stroke="#475569" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="740" y1="162" x2="740" y2="198" stroke="#475569" stroke-width="1" marker-end="url(#arrow)"/>
<line x1="840" y1="162" x2="840" y2="198" stroke="#475569" stroke-width="1" marker-end="url(#arrow)"/>
<!-- Thread boxes -->
<rect x="580" y="202" width="110" height="48" rx="6" fill="#064e3b" stroke="#10b981" stroke-width="1.5"/>
<text x="635" y="231" text-anchor="middle" fill="#6ee7b7" font-size="11" font-weight="500">Thread 1</text>
<rect x="700" y="202" width="80" height="48" rx="6" fill="#064e3b" stroke="#10b981" stroke-width="1.5"/>
<text x="740" y="231" text-anchor="middle" fill="#6ee7b7" font-size="11" font-weight="500">Thread 2</text>
<rect x="790" y="202" width="86" height="48" rx="6" fill="#064e3b" stroke="#10b981" stroke-width="1.5"/>
<text x="833" y="231" text-anchor="middle" fill="#6ee7b7" font-size="11" font-weight="500">Thread N</text>
<!-- Result return -->
<text x="725" y="284" text-anchor="middle" fill="#6b7280" font-size="10">on complete, resumes pipeline on reactor</text>
<!-- ==================== INVOKE ARROWS ==================== -->
<!-- Arrow from request reactors to pool -->
<path d="M 460,184 L 555,150" fill="none" stroke="#10b981" stroke-width="1.5" marker-end="url(#arrow-emerald)" stroke-dasharray="5,3"/>
<!-- Label above arrow -->
<text x="492" y="155" fill="#34d399" font-size="10" font-weight="500">invoke()</text>
<!-- Arrow from task reactor to pool -->
<path d="M 460,410 Q 530,380 555,300" fill="none" stroke="#10b981" stroke-width="1.5" marker-end="url(#arrow-emerald)" stroke-dasharray="5,3"/>
<!-- Label right of curve -->
<text x="538" y="370" fill="#34d399" font-size="10" font-weight="500">invoke()</text>
<!-- ==================== TASK ARROWS ==================== -->
<!-- task() from request reactors to task DB -->
<path d="M 210,316 Q 140,350 100,390" fill="none" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow-amber)" stroke-dasharray="5,3"/>
<!-- Label left of curve -->
<text x="120" y="348" fill="#fbbf24" font-size="10" font-weight="500">task()</text>
<!-- task() from task reactor to task DB (self-enqueue) -->
<path d="M 210,445 Q 160,470 105,443" fill="none" stroke="#f59e0b" stroke-width="1.5" marker-end="url(#arrow-amber)" stroke-dasharray="5,3"/>
<!-- Label below curve -->
<text x="160" y="478" fill="#fbbf24" font-size="10" font-weight="500">task()</text>
</svg>

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1,19 @@
#include <mach.h>
config mach(){
return (config) {
.resources = {{
{"home", "/", mime_txt,
.get = {{
validate((v){
.name = "name",
.validation = "^\\S{1,16}$",
.fallback = "world",
.message = "must be 1-16 characters, no spaces"
}),
render((r){"Hello {{name}}", .mime = mime_txt})
}}
}
}}
};
}

View File

@@ -0,0 +1,95 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 300" width="760" height="300">
<rect width="760" height="300" fill="#09080f"/>
<defs>
<linearGradient id="sg1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
<linearGradient id="sg2" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#8b5cf6"/>
<stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<linearGradient id="sg3" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<linearGradient id="sg4" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#10b981"/>
<stop offset="100%" stop-color="#059669"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- Outer container -->
<rect x="20" y="16" width="720" height="268" rx="14" fill="#0f0d1a" stroke="#1e1b2e" stroke-width="1.5"/>
<!-- Route label -->
<text x="44" y="46" fill="#94a3b8" font-size="12" font-weight="600">POST /todos</text>
<!-- .before label -->
<text x="44" y="72" fill="#475569" font-size="9">.before</text>
<!-- session() box: x=44 w=108 → right edge 152 -->
<rect x="44" y="80" width="108" height="40" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="1"/>
<text x="98" y="105" text-anchor="middle" fill="#93c5fd" font-size="11" font-weight="600">session()</text>
<!-- Arrow 1: line 152→170, triangle tip at 180 -->
<line x1="152" y1="100" x2="170" y2="100" stroke="#64748b" stroke-width="1.5"/>
<polygon points="180,100 170,95 170,105" fill="#64748b"/>
<!-- .post pipeline label -->
<text x="188" y="72" fill="#475569" font-size="9">.post pipeline</text>
<!-- param() box: x=188 w=108 → right edge 296 -->
<rect x="188" y="80" width="108" height="40" rx="8" fill="url(#sg1)"/>
<text x="242" y="98" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">param()</text>
<text x="242" y="113" text-anchor="middle" fill="rgba(255,255,255,0.65)" font-size="8">validate title</text>
<!-- Arrow 2: line 296→314, triangle tip at 324 -->
<line x1="296" y1="100" x2="314" y2="100" stroke="#64748b" stroke-width="1.5"/>
<polygon points="324,100 314,95 314,105" fill="#64748b"/>
<!-- db() box: x=324 w=108 → right edge 432 -->
<rect x="324" y="80" width="108" height="40" rx="8" fill="url(#sg2)"/>
<text x="378" y="98" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">db()</text>
<text x="378" y="113" text-anchor="middle" fill="rgba(255,255,255,0.65)" font-size="8">INSERT todo</text>
<!-- Arrow 3: line 432→450, triangle tip at 460 -->
<line x1="432" y1="100" x2="450" y2="100" stroke="#64748b" stroke-width="1.5"/>
<polygon points="460,100 450,95 450,105" fill="#64748b"/>
<!-- emit() box: x=460 w=108 → right edge 568 -->
<rect x="460" y="80" width="108" height="40" rx="8" fill="url(#sg3)"/>
<text x="514" y="98" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">emit()</text>
<text x="514" y="113" text-anchor="middle" fill="rgba(255,255,255,0.65)" font-size="8">todo_created</text>
<!-- Arrow 4: line 568→586, triangle tip at 596 -->
<line x1="568" y1="100" x2="586" y2="100" stroke="#64748b" stroke-width="1.5"/>
<polygon points="596,100 586,95 586,105" fill="#64748b"/>
<!-- redirect() box: x=596 w=108 → right edge 704 -->
<rect x="596" y="80" width="108" height="40" rx="8" fill="url(#sg4)"/>
<text x="650" y="98" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">redirect()</text>
<text x="650" y="113" text-anchor="middle" fill="rgba(255,255,255,0.65)" font-size="8">302 → /todos</text>
<!-- ===== Pipeline Context ===== -->
<rect x="44" y="146" width="672" height="120" rx="10" fill="#111827" stroke="#1f2937" stroke-width="1"/>
<text x="64" y="170" fill="#6b7280" font-size="10" font-weight="600">PIPELINE CONTEXT</text>
<!-- Context cards: each 148w x 52h, gap 12 -->
<rect x="60" y="182" width="148" height="52" rx="6" fill="#1e1b2e" stroke="#3b82f6" stroke-width="0.75"/>
<text x="134" y="206" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">user_id</text>
<text x="134" y="222" text-anchor="middle" fill="#6b7280" font-size="9">← session</text>
<rect x="220" y="182" width="148" height="52" rx="6" fill="#1e1b2e" stroke="#3b82f6" stroke-width="0.75"/>
<text x="294" y="206" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">title</text>
<text x="294" y="222" text-anchor="middle" fill="#6b7280" font-size="9">← param</text>
<rect x="380" y="182" width="148" height="52" rx="6" fill="#1e1b2e" stroke="#8b5cf6" stroke-width="0.75"/>
<text x="454" y="206" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">todo</text>
<text x="454" y="222" text-anchor="middle" fill="#6b7280" font-size="9">← db .set</text>
<rect x="540" y="182" width="148" height="52" rx="6" fill="#1e1b2e" stroke="#374151" stroke-width="0.75"/>
<text x="614" y="206" text-anchor="middle" fill="#e2e8f0" font-size="11" font-weight="600">base_layout</text>
<text x="614" y="222" text-anchor="middle" fill="#6b7280" font-size="9">← .context</text>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,26 @@
#include <mach.h>
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
validate((v){
.name = "name",
.validation = "^\\S{1,16}$",
.fallback = "world",
.message = "must be 1-16 characters, no spaces"
}),
render((r){
"<html>"
"<head></head>"
"<body>"
"<p>Hello {{name}}</p>"
"</body>"
"</html>"
})
}}
}
}}
};
}

66
03-event-pub-sub.svg Normal file
View File

@@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 300" width="900" height="300">
<rect width="900" height="300" fill="#09080f"/>
<defs>
<linearGradient id="pubG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#d97706"/>
</linearGradient>
<linearGradient id="subG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#06b6d4"/>
<stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="busG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="50%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- ===== Publisher: todos module ===== -->
<rect x="30" y="30" width="220" height="130" rx="12" fill="#1e1b2e" stroke="#f59e0b" stroke-width="1.5"/>
<rect x="30" y="30" width="220" height="36" rx="12" fill="url(#pubG)"/>
<text x="140" y="53" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">todos module</text>
<text x="50" y="86" fill="#fcd34d" font-size="10">.post pipeline:</text>
<text x="58" y="102" fill="#e2e8f0" font-size="10">param() → db()</text>
<text x="58" y="118" fill="#e2e8f0" font-size="10">→ emit("todo_created")</text>
<text x="58" y="134" fill="#e2e8f0" font-size="10">→ redirect("/todos")</text>
<!-- .publishes contract -->
<text x="140" y="175" text-anchor="middle" fill="#92400e" font-size="9" font-weight="600">.publishes</text>
<rect x="40" y="182" width="200" height="28" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="0.75"/>
<text x="140" y="201" text-anchor="middle" fill="#fcd34d" font-size="9">todo_created → user_id, title</text>
<!-- ===== Event Bus pill ===== -->
<rect x="330" y="118" width="140" height="44" rx="22" fill="url(#busG)"/>
<text x="400" y="145" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">Event Bus</text>
<!-- Arrow: publisher → bus -->
<line x1="250" y1="140" x2="320" y2="140" stroke="#f59e0b" stroke-width="2"/>
<polygon points="330,140 320,135 320,145" fill="#f59e0b"/>
<!-- ===== Subscriber 1: activity module ===== -->
<rect x="570" y="20" width="300" height="100" rx="12" fill="#1e1b2e" stroke="#06b6d4" stroke-width="1.5"/>
<rect x="570" y="20" width="300" height="36" rx="12" fill="url(#subG)"/>
<text x="720" y="43" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">activity module</text>
<text x="595" y="74" fill="#67e8f9" font-size="10">.events:</text>
<text x="603" y="92" fill="#e2e8f0" font-size="9">"todo_created" → db(log.sql)</text>
<!-- Elbow: bus right → subscriber 1 -->
<path d="M470,140 L525,140 L525,70 L560,70" stroke="#06b6d4" stroke-width="1.5" fill="none"/>
<polygon points="570,70 560,65 560,75" fill="#06b6d4"/>
<!-- ===== Subscriber 2: notifications module ===== -->
<rect x="570" y="160" width="300" height="100" rx="12" fill="#1e1b2e" stroke="#06b6d4" stroke-width="1.5"/>
<rect x="570" y="160" width="300" height="36" rx="12" fill="url(#subG)"/>
<text x="720" y="183" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">notifications module</text>
<text x="595" y="214" fill="#67e8f9" font-size="10">.events:</text>
<text x="603" y="232" fill="#e2e8f0" font-size="9">"todo_created" → call(notify)</text>
<!-- Elbow: bus right → subscriber 2 -->
<path d="M470,140 L525,140 L525,210 L560,210" stroke="#06b6d4" stroke-width="1.5" fill="none"/>
<polygon points="570,210 560,205 560,215" fill="#06b6d4"/>
<!-- Footer -->
<text x="450" y="290" text-anchor="middle" fill="#64748b" font-size="10">Modules are decoupled — publishers don't know about subscribers</text>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,47 @@
#include <mach.h>
#include <sqlite.h>
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
query((d){
.set = "greeting",
.db = "hello_db",
"select name "
"from greetings "
"limit 1;"
}),
render((r){
"<html>"
"<body>"
"{{#greeting}}"
"<p>Hello {{name}}</p>"
"{{/greeting}}"
"</body>"
"</html>"
})
}}
}
}},
.databases = {{
.engine = sqlite_db,
.name = "hello_db",
.connect = "file:hello.db?mode=rwc",
.migrations = {
"CREATE TABLE greetings ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"name TEXT NOT NULL"
");"
},
.seeds = {
"INSERT INTO greetings(name)"
"VALUES('World');"
}
}},
.modules = {sqlite}
};
}

46
04-error-resolution.svg Normal file
View File

@@ -0,0 +1,46 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 320" width="760" height="320">
<rect width="760" height="320" fill="#09080f"/>
<defs>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- Level 1: Root Config — y=24 h=60 → bottom 84 -->
<rect x="40" y="24" width="680" height="60" rx="10" fill="#2d2640" stroke="#7c3aed" stroke-width="1.5"/>
<text x="380" y="50" text-anchor="middle" fill="#c084fc" font-size="12" font-weight="700">Root Config</text>
<text x="380" y="70" text-anchor="middle" fill="#94a3b8" font-size="10">.errors = { 404 → render 404.html }</text>
<!-- Gap: 84 → 118 (34px gap) -->
<!-- Level 2: Module Config — y=118 h=60 → bottom 178 -->
<rect x="120" y="118" width="520" height="60" rx="10" fill="#1e293b" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="380" y="144" text-anchor="middle" fill="#60a5fa" font-size="12" font-weight="700">Module Config</text>
<text x="380" y="164" text-anchor="middle" fill="#94a3b8" font-size="10">.errors = { ... }</text>
<!-- Gap: 178 → 212 (34px gap) -->
<!-- Level 3: Route — y=212 h=60 → bottom 272 -->
<rect x="200" y="212" width="360" height="60" rx="10" fill="#0c2d3e" stroke="#06b6d4" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="380" y="238" text-anchor="middle" fill="#22d3ee" font-size="12" font-weight="700">Route /todos/:id</text>
<text x="380" y="258" text-anchor="middle" fill="#94a3b8" font-size="10">.errors = { ... }</text>
<!-- Error origin dot: y=296 -->
<circle cx="380" cy="296" r="10" fill="#ef4444" opacity="0.85"/>
<text x="380" y="300" text-anchor="middle" fill="#fff" font-size="10" font-weight="700">!</text>
<text x="400" y="300" fill="#fca5a5" font-size="10"> 404 raised in pipeline step</text>
<!-- Arrow 1: error dot (286) → route bottom (272). Line + up triangle -->
<line x1="380" y1="286" x2="380" y2="275" stroke="#ef4444" stroke-width="2"/>
<polygon points="380,272 375,279 385,279" fill="#ef4444"/>
<!-- Arrow 2: route top (212) → module bottom (178). Line in gap + up triangle -->
<line x1="380" y1="212" x2="380" y2="181" stroke="#ef4444" stroke-width="2"/>
<polygon points="380,178 375,185 385,185" fill="#ef4444"/>
<!-- Label -->
<text x="400" y="200" fill="#6b7280" font-size="9">no 404 handler → bubble up</text>
<!-- Arrow 3: module top (118) → root bottom (84). Line in gap + up triangle -->
<line x1="380" y1="118" x2="380" y2="87" stroke="#ef4444" stroke-width="2"/>
<polygon points="380,84 375,91 385,91" fill="#ef4444"/>
<!-- Label -->
<text x="400" y="106" fill="#22c55e" font-size="9" font-weight="600">✓ matched → render 404.html</text>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

224
04_todo_single_file/main.c Normal file
View File

@@ -0,0 +1,224 @@
#include <mach.h>
#include <sqlite.h>
#include <session_auth.h>
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
render((r){
"{{< layout}}"
"{{$body}}"
"<p>home</p>"
"{{/body}}"
"{{/layout}}"
})
}}
},
{"about", "/about",
.get = {{
render((r){
"{{< layout}}"
"{{$body}}"
"<p>about us</p>"
"{{/body}}"
"{{/layout}}"
})
}}
},
{"contact", "/contact",
.get = {{
render((r){
"{{< layout}}"
"{{$body}}"
"<p>contact us</p>"
"{{/body}}"
"{{/layout}}"
})
}}
},
{"todos", "/todos",
.get = {{
query((d){
.set = "todos",
.db = "todos_db",
.query =
"select id, title, finished "
"from todos "
"where user_id = {{user_id}};"
}),
render((r){
"{{< layout}}"
"{{$body}}"
"<form action='{{url:todos}}' method='post'>"
"<input type='text' name='title' placeholder='new todo' required>"
"<button type='submit'>add</button>"
"</form>"
"{{^todos}}"
"<p>no todos</p>"
"{{/todos}}"
"{{#todos}}"
"{{#.}}"
"{{> todo}}"
"{{/.}}"
"{{/todos}}"
"{{/body}}"
"{{/layout}}"
})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^\\S{1,16}$",
.message = "must be 1-16 characters, no spaces"
}),
query((d){
.db = "todos_db",
.query =
"insert into todos(user_id, title) "
"values({{user_id}}, {{title}});"
}),
redirect((u){"todos"})
}},
.before = {logged_in()},
.context = {
{"todo",
"<div>"
"<form action='{{url:todo:id}}' method='post' style='display:inline'>"
"<input type='hidden' name='http_method' value='patch'>"
"{{^finished}}"
"<input type='checkbox' name='finished' value='1'>"
"{{/finished}}"
"{{#finished}}"
"<input type='checkbox' name='finished' value='1' checked>"
"{{/finished}}"
"{{title}}"
"<button type='submit'>save</button>"
"</form>"
"<form action='{{url:todo:id}}' method='post' style='display:inline'>"
"<input type='hidden' name='http_method' value='delete'>"
"<button type='submit'>delete</button>"
"</form>"
"</div>"
}
}
},
{"todo", "/todos/:id",
.patch = {{
validate((v){
.name = "finished",
.optional = true,
.validation = "1",
.message = "must be 1"
}),
query((d){
.db = "todos_db",
.query =
"update todos "
"set finished = {{finished}} "
"where user_id = {{user_id}} "
"and id = {{id}};"
}),
redirect((u){"todos"})
}},
.delete = {{
query((d){
.db = "todos_db",
.query =
"delete from todos "
"where user_id = {{user_id}} "
"and id = {{id}};"
}),
redirect((u){"todos"})
}},
.before = {
logged_in(),
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "must be between 1-9999999999"
})
}
}},
.before = {session()},
.context = {
{"layout",
"<html>"
"<head>"
"<link rel='icon' href='{{asset:favicon.png}}'>"
"</head>"
"<body>"
"<p>"
"{{^user}}"
"<a href='{{url:login}}'>sign in</a>"
"{{/user}}"
"{{#user}}"
"welcome, {{short_name}}"
"{{/user}}"
"</p>"
"<nav>"
"<a href='{{url:home}}'>home</a>"
"<a href='{{url:about}}'>about us</a>"
"<a href='{{url:contact}}'>contact us</a>"
"<a href='{{url:todos}}'>todos</a>"
"</nav>"
"{{$body}}"
"{{/body}}"
"</body>"
"</html>"
}
},
.errors = {
{http_error, {
render((r){
"{{< layout}}"
"{{$body}}"
"<p>error</p>"
"{{/body}}"
"{{/layout}}"
})
}},
{http_not_found, {
render((r){
"{{< layout}}"
"{{$body}}"
"<p>not found</p>"
"{{/body}}"
"{{/layout}}"
})
}}
}
},
.databases = {{
.engine = sqlite_db,
.name = "todos_db",
.connect = "file:todo.db?mode=rwc",
.migrations = {
"CREATE TABLE IF NOT EXISTS todos ( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"user_id INTEGER NOT NULL, "
"title TEXT NOT NULL, "
"finished INTEGER CHECK(finished IN (1))"
"); "
"CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id);"
}
}},
.modules = {sqlite, session_auth}
};
}

View File

View File

@@ -0,0 +1,58 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 270" width="760" height="270">
<rect width="760" height="270" fill="#09080f"/>
<defs>
<linearGradient id="rG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#a855f7"/>
<stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<linearGradient id="mG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#3b82f6"/>
<stop offset="100%" stop-color="#2563eb"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- Root node: x=290 y=20 w=180 h=44 → center 380, bottom 64 -->
<rect x="290" y="20" width="180" height="44" rx="10" fill="url(#rG)"/>
<text x="380" y="47" text-anchor="middle" fill="#fff" font-size="12" font-weight="700">mach() — root</text>
<!-- Elbow connectors: root bottom center → down to 82 → horizontal → down to child tops at 108 -->
<!-- Vertical trunk from root -->
<line x1="380" y1="64" x2="380" y2="82" stroke="#64748b" stroke-width="1.5"/>
<!-- Horizontal bar -->
<line x1="130" y1="82" x2="630" y2="82" stroke="#64748b" stroke-width="1.5"/>
<!-- Vertical drops to each child -->
<line x1="130" y1="82" x2="130" y2="108" stroke="#64748b" stroke-width="1.5"/>
<line x1="380" y1="82" x2="380" y2="108" stroke="#64748b" stroke-width="1.5"/>
<line x1="630" y1="82" x2="630" y2="108" stroke="#64748b" stroke-width="1.5"/>
<!-- Level 1: each w=170 h=40, top at y=108 → bottom at 148 -->
<rect x="45" y="108" width="170" height="40" rx="8" fill="url(#mG)"/>
<text x="130" y="133" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">sqlite_config()</text>
<rect x="295" y="108" width="170" height="40" rx="8" fill="url(#mG)"/>
<text x="380" y="133" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">todos_config()</text>
<rect x="545" y="108" width="170" height="40" rx="8" fill="url(#mG)"/>
<text x="630" y="133" text-anchor="middle" fill="#fff" font-size="11" font-weight="600">auth_config()</text>
<!-- Elbow connectors: todos bottom center → down to 162 → horizontal → down to leaf tops at 178 -->
<line x1="380" y1="148" x2="380" y2="162" stroke="#64748b" stroke-width="1"/>
<line x1="260" y1="162" x2="500" y2="162" stroke="#64748b" stroke-width="1"/>
<line x1="260" y1="162" x2="260" y2="178" stroke="#64748b" stroke-width="1"/>
<line x1="500" y1="162" x2="500" y2="178" stroke="#64748b" stroke-width="1"/>
<!-- Level 2 leaf nodes: w=170 h=64, top at y=178 -->
<rect x="175" y="178" width="170" height="64" rx="8" fill="#0f0d1a" stroke="#10b981" stroke-width="1"/>
<text x="260" y="200" text-anchor="middle" fill="#6ee7b7" font-size="10">.databases</text>
<text x="260" y="216" text-anchor="middle" fill="#6ee7b7" font-size="10">.resources</text>
<text x="260" y="232" text-anchor="middle" fill="#6ee7b7" font-size="10">.events</text>
<rect x="415" y="178" width="170" height="64" rx="8" fill="#0f0d1a" stroke="#10b981" stroke-width="1"/>
<text x="500" y="200" text-anchor="middle" fill="#6ee7b7" font-size="10">.publishes</text>
<text x="500" y="216" text-anchor="middle" fill="#6ee7b7" font-size="10">.context</text>
<text x="500" y="232" text-anchor="middle" fill="#6ee7b7" font-size="10">.errors</text>
<!-- Footer -->
<text x="380" y="262" text-anchor="middle" fill="#64748b" font-size="10">First registration wins — root can override module defaults</text>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>not found</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>error</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>about us</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>contact us</p>
{{/body}}
{{/layout}}

2
05_todo/create_todo.sql Normal file
View File

@@ -0,0 +1,2 @@
insert into todos(user_id, title)
values({{user_id}}, {{title}});

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
finished INTEGER CHECK(finished IN (1))
);
CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id);

3
05_todo/delete_todo.sql Normal file
View File

@@ -0,0 +1,3 @@
delete from todos
where user_id = {{user_id}}
and id = {{id}};

3
05_todo/get_todos.sql Normal file
View File

@@ -0,0 +1,3 @@
select id, title, finished
from todos
where user_id = {{user_id}};

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>home</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,23 @@
<html>
<head>
<link rel='icon' href='{{asset:favicon.png}}'>
</head>
<body>
<p>
{{^user}}
<a href='{{url:login}}'>sign in</a>
{{/user}}
{{#user}}
welcome, {{short_name}}
{{/user}}
</p>
<nav>
<a href='{{url:home}}'>home</a>
<a href='{{url:about}}'>about us</a>
<a href='{{url:contact}}'>contact us</a>
<a href='{{url:todos}}'>todos</a>
</nav>
{{$body}}
{{/body}}
</body>
</html>

141
05_todo/main.c Normal file
View File

@@ -0,0 +1,141 @@
#include <mach.h>
#include <sqlite.h>
#include <session_auth.h>
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
render((r){(asset){
#embed "home.mustache.html"
}})
}}
},
{"about", "/about",
.get = {{
render((r){(asset){
#embed "about.mustache.html"
}})
}}
},
{"contact", "/contact",
.get = {{
render((r){(asset){
#embed "contact.mustache.html"
}})
}}
},
{"todos", "/todos",
.get = {{
query((d){
.set = "todos",
.db = "todos_db",
.query = (asset){
#embed "get_todos.sql"
}
}),
render((r){(asset){
#embed "todos.mustache.html"
}})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^\\S{1,16}$",
.message = "must be 1-16 characters, no spaces"
}),
query((d){
.db = "todos_db",
.query = (asset){
#embed "create_todo.sql"
}
}),
redirect((u){"todos"})
}},
.before = {logged_in()},
.context = {
{"todo", (asset){
#embed "todo.mustache.html"
}}
}
},
{"todo", "/todos/:id",
.patch = {{
validate((v){
.name = "finished",
.optional = true,
.validation = "1",
.message = "must be 1"
}),
find((d){
.db = "todos_db",
.query = (asset){
#embed "update_todo.sql"
}
}),
redirect((u){"todos"})
}},
.delete = {{
find((d){
.db = "todos_db",
.query = (asset){
#embed "delete_todo.sql"
}
}),
redirect((u){"todos"})
}},
.before = {
logged_in(),
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "must be between 1-9999999999"
})
}
}},
.before = {session()},
.context = {
{"layout", (asset){
#embed "layout.mustache.html"
}}
},
.errors = {
{http_error, {
render((r){(asset){
#embed "5xx.mustache.html"
}})
}},
{http_not_found, {
render((r){(asset){
#embed "404.mustache.html"
}})
}}
}
},
.databases = {{
.engine = sqlite_db,
.name = "todos_db",
.connect = "file:todo.db?mode=rwc",
.migrations = {(asset){
#embed "create_todos_table.sql"
}}
}},
.modules = {sqlite, session_auth}
};
}

View File

View File

@@ -0,0 +1,17 @@
<div>
<form action="{{url:todo:id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="patch">
{{^finished}}
<input type="checkbox" name="finished" value="1">
{{/finished}}
{{#finished}}
<input type="checkbox" name="finished" value="1" checked>
{{/finished}}
{{title}}
<button type="submit">save</button>
</form>
<form action="{{url:todo:id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="delete">
<button type="submit">delete</button>
</form>
</div>

View File

@@ -0,0 +1,16 @@
{{< layout}}
{{$body}}
<form action="{{url:todos}}" method="post">
<input type="text" name="title" placeholder="new todo" required>
<button type="submit">add</button>
</form>
{{^todos}}
<p>no todos</p>
{{/todos}}
{{#todos}}
{{#.}}
{{> todo}}
{{/.}}
{{/todos}}
{{/body}}
{{/layout}}

4
05_todo/update_todo.sql Normal file
View File

@@ -0,0 +1,4 @@
update todos
set finished = {{finished}}
where user_id = {{user_id}}
and id = {{id}};

52
06-middleware-scoping.svg Normal file
View File

@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 360" width="760" height="360">
<rect width="760" height="360" fill="#09080f"/>
<defs>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- Outermost: Global .before / .after -->
<rect x="30" y="20" width="700" height="310" rx="14" fill="#1a1328" stroke="#7c3aed" stroke-width="1.5"/>
<text x="50" y="46" fill="#c084fc" font-size="11" font-weight="700">Global .before</text>
<text x="50" y="62" fill="#9580b8" font-size="9">session()</text>
<text x="610" y="46" fill="#c084fc" font-size="11" font-weight="700">Global .after</text>
<text x="610" y="62" fill="#9580b8" font-size="9">call(log_request)</text>
<!-- Middle: Resource .before / .after -->
<rect x="60" y="78" width="640" height="230" rx="12" fill="#111827" stroke="#3b82f6" stroke-width="1.5"/>
<text x="80" y="102" fill="#60a5fa" font-size="11" font-weight="700">Resource .before</text>
<text x="80" y="118" fill="#7b9ec9" font-size="9">logged_in()</text>
<text x="570" y="102" fill="#60a5fa" font-size="11" font-weight="700">Resource .after</text>
<!-- Inner: Route pipeline -->
<rect x="90" y="134" width="580" height="152" rx="10" fill="#0f0d1a" stroke="#06b6d4" stroke-width="1.5"/>
<text x="110" y="158" fill="#22d3ee" font-size="11" font-weight="700">Route Pipeline — POST /todos</text>
<!-- Steps inside route -->
<rect x="110" y="172" width="100" height="36" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-width="0.75"/>
<text x="160" y="194" text-anchor="middle" fill="#93c5fd" font-size="10" font-weight="600">param()</text>
<line x1="210" y1="190" x2="228" y2="190" stroke="#64748b" stroke-width="1.5"/>
<polygon points="238,190 228,185 228,195" fill="#64748b"/>
<rect x="238" y="172" width="100" height="36" rx="6" fill="#1e293b" stroke="#8b5cf6" stroke-width="0.75"/>
<text x="288" y="194" text-anchor="middle" fill="#c4b5fd" font-size="10" font-weight="600">db()</text>
<line x1="338" y1="190" x2="356" y2="190" stroke="#64748b" stroke-width="1.5"/>
<polygon points="366,190 356,185 356,195" fill="#64748b"/>
<rect x="366" y="172" width="100" height="36" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-width="0.75"/>
<text x="416" y="194" text-anchor="middle" fill="#fcd34d" font-size="10" font-weight="600">emit()</text>
<line x1="466" y1="190" x2="484" y2="190" stroke="#64748b" stroke-width="1.5"/>
<polygon points="494,190 484,185 484,195" fill="#64748b"/>
<rect x="494" y="172" width="100" height="36" rx="6" fill="#1e293b" stroke="#10b981" stroke-width="0.75"/>
<text x="544" y="194" text-anchor="middle" fill="#6ee7b7" font-size="10" font-weight="600">redirect()</text>
<!-- Route .before / .after labels -->
<text x="110" y="244" fill="#22d3ee" font-size="9">Route .before runs first inside this scope</text>
<text x="110" y="260" fill="#22d3ee" font-size="9">Route .after runs last inside this scope</text>
<!-- Execution order arrows at bottom -->
<text x="380" y="348" text-anchor="middle" fill="#64748b" font-size="10">Execution order: global .before → resource .before → route pipeline → resource .after → global .after</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>not found</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>error</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>about us</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>contact us</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,3 @@
insert into todos(user_id, title)
values({{user_id}}, {{title}})
returning id, title, finished;

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
finished INTEGER CHECK(finished IN (1))
);
CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id);

View File

@@ -0,0 +1,4 @@
delete from todos
where user_id = {{user_id}}
and id = {{id}}
returning id;

View File

@@ -0,0 +1,3 @@
select id, title, finished
from todos
where user_id = {{user_id}};

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>home</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,25 @@
<html>
<head>
<link rel='icon' href='{{asset:favicon.png}}'>
{{$head}}
{{/head}}
</head>
<body>
<p>
{{^user}}
<a href='{{url:login}}'>sign in</a>
{{/user}}
{{#user}}
welcome, {{short_name}}
{{/user}}
</p>
<nav>
<a href='{{url:home}}'>home</a>
<a href='{{url:about}}'>about us</a>
<a href='{{url:contact}}'>contact us</a>
<a href='{{url:todos}}'>todos</a>
</nav>
{{$body}}
{{/body}}
</body>
</html>

157
06_todo_sse_datastar/main.c Normal file
View File

@@ -0,0 +1,157 @@
#include <mach.h>
#include <sqlite.h>
#include <datastar.h>
#include <session_auth.h>
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
render((r){(asset){
#embed "home.mustache.html"
}})
}}
},
{"about", "/about",
.get = {{
render((r){(asset){
#embed "about.mustache.html"
}})
}}
},
{"contact", "/contact",
.get = {{
render((r){(asset){
#embed "contact.mustache.html"
}})
}}
},
{"todos", "/todos",
.sse = {.channel = "todos:{{user_id}}"},
.get = {{
query((d){
.set = "todos",
.db = "todos_db",
.query = (asset){
#embed "get_todos.sql"
}
}),
render((r){(asset){
#embed "todos.mustache.html"
}})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^\\S{1,16}$",
.message = "must be 1-16 characters, no spaces"
}),
query((d){
.set = "todo",
.db = "todos_db",
.query = (asset){
#embed "create_todo.sql"
}
}),
ds_sse((ds){
.target = "todos",
.mode = mode_prepend,
.channel = "todos:{{user_id}}",
.elements = {.asset = "todo"}
})
}},
.before = {logged_in()}
},
{"todo", "/todos/:id",
.patch = {{
validate((v){
.name = "finished",
.optional = true,
.validation = "1",
.message = "must be 1"
}),
find((d){
.set = "todo",
.db = "todos_db",
.query = (asset){
#embed "update_todo.sql"
}
}),
ds_sse((ds){
.mode = mode_replace,
.target = "todo_{{id}}",
.channel = "todos:{{user_id}}",
.elements = {.asset = "todo"}
})
}},
.delete = {{
find((d){
.db = "todos_db",
.query = (asset){
#embed "delete_todo.sql"
}
}),
ds_sse((ds){
.mode = mode_remove,
.target = "todo_{{id}}",
.channel = "todos:{{user_id}}"
})
}},
.before = {
logged_in(),
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "must be between 1-9999999999"
})
}
}},
.before = {session()},
.context = {
{"layout", (asset){
#embed "layout.mustache.html"
}},
{"todo", (asset){
#embed "todo.mustache.html"
}}
},
.errors = {
{http_error, {
render((r){(asset){
#embed "5xx.mustache.html"
}})
}},
{http_not_found, {
render((r){(asset){
#embed "404.mustache.html"
}})
}}
}
},
.databases = {{
.engine = sqlite_db,
.name = "todos_db",
.connect = "file:todo.db?mode=rwc",
.migrations = {(asset){
#embed "create_todos_table.sql"
}}
}},
.modules = {sqlite, datastar, session_auth}
};
}

View File

View File

@@ -0,0 +1,10 @@
<div id="todo_{{id}}">
{{^finished}}
<input type="checkbox" data-on:click__prevent="@patch('{{url:todo:id}}?finished=1')">
{{/finished}}
{{#finished}}
<input type="checkbox" checked data-on:click__prevent="@patch('{{url:todo:id}}')">
{{/finished}}
{{title}}
<button data-on:click="@delete('{{url:todo:id}}')">delete</button>
</div>

View File

@@ -0,0 +1,21 @@
{{< layout}}
{{$head}}
{{> datastar_script }}
{{/head}}
{{$body}}
<input type="text" placeholder="new todo" data-bind:title
data-on:keydown="evt.key === 'Enter' && @post('{{utl:todos}}') && ($title = '');"
>
<button data-on:click="@post('{{url:todos}}') && ($title = '')">add</button>
<div id="todos" data-init="@get('{{url:todos}}?http_method=sse')">
{{^todos}}
<p>no todos</p>
{{/todos}}
{{#todos}}
{{#.}}
{{> todo}}
{{/.}}
{{/todos}}
</div>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
update todos
set finished = {{finished}}
where user_id = {{user_id}}
and id = {{id}}
returning id, title, finished;

50
07-context-scoping.svg Normal file
View File

@@ -0,0 +1,50 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 360" width="760" height="360">
<rect width="760" height="360" fill="#09080f"/>
<defs>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<!-- Outermost: Global .context -->
<rect x="30" y="20" width="700" height="310" rx="14" fill="#1a1328" stroke="#7c3aed" stroke-width="1.5"/>
<text x="50" y="46" fill="#c084fc" font-size="11" font-weight="700">Global .context</text>
<text x="50" y="62" fill="#9580b8" font-size="9">base_layout, site_name</text>
<!-- Middle: Resource .context -->
<rect x="60" y="78" width="640" height="230" rx="12" fill="#111827" stroke="#3b82f6" stroke-width="1.5"/>
<text x="80" y="102" fill="#60a5fa" font-size="11" font-weight="700">Resource .context</text>
<text x="80" y="118" fill="#7b9ec9" font-size="9">layout, sidebar</text>
<!-- Inner: Route pipeline -->
<rect x="90" y="134" width="580" height="152" rx="10" fill="#0f0d1a" stroke="#06b6d4" stroke-width="1.5"/>
<text x="110" y="158" fill="#22d3ee" font-size="11" font-weight="700">Route Pipeline — GET /todos</text>
<!-- Steps inside route -->
<rect x="110" y="172" width="120" height="36" rx="6" fill="#1e293b" stroke="#8b5cf6" stroke-width="0.75"/>
<text x="170" y="194" text-anchor="middle" fill="#c4b5fd" font-size="10" font-weight="600">db() → todos</text>
<line x1="230" y1="190" x2="258" y2="190" stroke="#64748b" stroke-width="1.5"/>
<polygon points="268,190 258,185 258,195" fill="#64748b"/>
<rect x="268" y="172" width="120" height="36" rx="6" fill="#1e293b" stroke="#f59e0b" stroke-width="0.75"/>
<text x="328" y="194" text-anchor="middle" fill="#fcd34d" font-size="10" font-weight="600">call() → count</text>
<line x1="388" y1="190" x2="416" y2="190" stroke="#64748b" stroke-width="1.5"/>
<polygon points="426,190 416,185 416,195" fill="#64748b"/>
<rect x="426" y="172" width="120" height="36" rx="6" fill="#1e293b" stroke="#10b981" stroke-width="0.75"/>
<text x="486" y="194" text-anchor="middle" fill="#6ee7b7" font-size="10" font-weight="600">render()</text>
<!-- Available context note -->
<text x="110" y="237" fill="#22d3ee" font-size="9">render() sees: base_layout, site_name, layout, sidebar, todos, count</text>
<text x="110" y="253" fill="#22d3ee" font-size="9">Context merges top-down; first named registration wins</text>
<!-- Cascade arrows on the left side -->
<line x1="42" y1="68" x2="42" y2="78" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="3,2"/>
<polygon points="42,84 38,76 46,76" fill="#7c3aed"/>
<line x1="72" y1="124" x2="72" y2="134" stroke="#3b82f6" stroke-width="1.5" stroke-dasharray="3,2"/>
<polygon points="72,140 68,132 76,132" fill="#3b82f6"/>
<!-- Bottom summary -->
<text x="380" y="348" text-anchor="middle" fill="#64748b" font-size="10">Context cascades: global .context → resource .context → pipeline steps add to context → render() merges all</text>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>not found</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>error</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>about us</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>contact us</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,2 @@
insert into todos(title)
values({{title}});

View File

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
finished INTEGER CHECK(finished IN (1))
);

View File

@@ -0,0 +1,2 @@
delete from todos
where id = {{id}};

View File

@@ -0,0 +1,2 @@
select id, title, finished
from todos;

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>home</p>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,23 @@
<html>
<head>
<link rel='icon' href='{{asset:favicon.png}}'>
</head>
<body>
<p>
{{^user}}
<a href='{{url:login}}'>sign in</a>
{{/user}}
{{#user}}
welcome, {{short_name}}
{{/user}}
</p>
<nav>
<a href='{{url:home}}'>home</a>
<a href='{{url:about}}'>about us</a>
<a href='{{url:contact}}'>contact us</a>
<a href='{{url:todos}}'>todos</a>
</nav>
{{$body}}
{{/body}}
</body>
</html>

141
07_todo_db_per_user/main.c Normal file
View File

@@ -0,0 +1,141 @@
#include <mach.h>
#include <sqlite.h>
#include <session_auth.h>
config mach(){
return (config) {
.resources = {{
{"home","/",
.get = {{
render((r){(asset){
#embed "home.mustache.html"
}})
}}
},
{"about", "/about",
.get = {{
render((r){(asset){
#embed "about.mustache.html"
}})
}}
},
{"contact", "/contact",
.get = {{
render((r){(asset){
#embed "contact.mustache.html"
}})
}}
},
{"todos", "/todos",
.get = {{
query((d){
.set = "todos",
.db = "todos_db",
.query = (asset){
#embed "get_todos.sql"
}
}),
render((r){(asset){
#embed "todos.mustache.html"
}})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^\\S{1,16}$",
.message = "must be 1-16 characters, no spaces"
}),
query((d){
.db = "todos_db",
.query = (asset){
#embed "create_todo.sql"
}
}),
redirect((u){"todos"})
}},
.before = {logged_in()},
.context = {
{"todo", (asset){
#embed "todo.mustache.html"
}}
}
},
{"todo", "/todos/:id",
.patch = {{
validate((v){
.name = "finished",
.optional = true,
.validation = "1",
.message = "must be 1"
}),
find((d){
.db = "todos_db",
.query = (asset){
#embed "update_todo.sql"
}
}),
redirect((u){"todos"})
}},
.delete = {{
find((d){
.db = "todos_db",
.query = (asset){
#embed "delete_todo.sql"
}
}),
redirect((u){"todos"})
}},
.before = {
logged_in(),
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "must be between 1-9999999999"
})
}
}},
.before = {session()},
.context = {
{"layout", (asset){
#embed "layout.mustache.html"
}}
},
.errors = {
{http_error, {
render((r){(asset){
#embed "5xx.mustache.html"
}})
}},
{http_not_found, {
render((r){(asset){
#embed "404.mustache.html"
}})
}}
}
},
.databases = {{
.engine = sqlite_db,
.name = "todos_db",
.connect = "file:{{user_id}}_todo.db?mode=rwc",
.migrations = {(asset){
#embed "create_todos_table.sql"
}}
}},
.modules = {sqlite, session_auth}
};
}

View File

View File

@@ -0,0 +1,17 @@
<div>
<form action="{{url:todo:id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="patch">
{{^finished}}
<input type="checkbox" name="finished" value="1">
{{/finished}}
{{#finished}}
<input type="checkbox" name="finished" value="1" checked>
{{/finished}}
{{title}}
<button type="submit">save</button>
</form>
<form action="{{url:todo:id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="delete">
<button type="submit">delete</button>
</form>
</div>

View File

@@ -0,0 +1,16 @@
{{< layout}}
{{$body}}
<form action="{{url:todos}}" method="post">
<input type="text" name="title" placeholder="new todo" required>
<button type="submit">add</button>
</form>
{{^todos}}
<p>no todos</p>
{{/todos}}
{{#todos}}
{{#.}}
{{> todo}}
{{/.}}
{{/todos}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,3 @@
update todos
set finished = {{finished}}
where id = {{id}};

96
08-sse-datastar-flow.svg Normal file
View File

@@ -0,0 +1,96 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 400" width="760" height="400">
<rect width="760" height="400" fill="#09080f"/>
<defs>
<linearGradient id="clientG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#06b6d4"/>
<stop offset="100%" stop-color="#0891b2"/>
</linearGradient>
<linearGradient id="serverG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#8b5cf6"/>
<stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<linearGradient id="chanG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="50%" stop-color="#fbbf24"/>
<stop offset="100%" stop-color="#f59e0b"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<text x="380" y="28" text-anchor="middle" fill="#e2e8f0" font-size="13" font-weight="700">SSE / Datastar — Real-Time Push</text>
<!-- ===== Left: Client A (subscriber) ===== -->
<rect x="30" y="50" width="180" height="80" rx="10" fill="#1e1b2e" stroke="#06b6d4" stroke-width="1.5"/>
<rect x="30" y="50" width="180" height="32" rx="10" fill="url(#clientG)"/>
<text x="120" y="71" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">Client A</text>
<text x="50" y="100" fill="#67e8f9" font-size="9">GET /todos</text>
<text x="50" y="114" fill="#67e8f9" font-size="9">.sse channel connected</text>
<!-- ===== Left: Client B (subscriber) ===== -->
<rect x="30" y="150" width="180" height="80" rx="10" fill="#1e1b2e" stroke="#06b6d4" stroke-width="1.5"/>
<rect x="30" y="150" width="180" height="32" rx="10" fill="url(#clientG)"/>
<text x="120" y="171" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">Client B</text>
<text x="50" y="200" fill="#67e8f9" font-size="9">GET /todos</text>
<text x="50" y="214" fill="#67e8f9" font-size="9">.sse channel connected</text>
<!-- ===== Channel pill ===== -->
<rect x="280" y="110" width="200" height="44" rx="22" fill="url(#chanG)"/>
<text x="380" y="128" text-anchor="middle" fill="#1e1b2e" font-size="10" font-weight="700">Channel</text>
<text x="380" y="143" text-anchor="middle" fill="#78350f" font-size="9">todos/{{user_id}}</text>
<!-- Arrows: clients → channel (subscribe) -->
<line x1="210" y1="90" x2="270" y2="132" stroke="#06b6d4" stroke-width="1.5"/>
<polygon points="280,132 270,127 268,137" fill="#06b6d4"/>
<line x1="210" y1="190" x2="270" y2="132" stroke="#06b6d4" stroke-width="1.5"/>
<polygon points="280,132 270,127 268,137" fill="#06b6d4"/>
<text x="220" y="86" fill="#475569" font-size="8">subscribe</text>
<text x="220" y="198" fill="#475569" font-size="8">subscribe</text>
<!-- ===== Right: Server pipeline (POST) ===== -->
<rect x="30" y="270" width="700" height="110" rx="12" fill="#0f0d1a" stroke="#7c3aed" stroke-width="1.5"/>
<text x="50" y="296" fill="#c084fc" font-size="11" font-weight="700">POST /todos — another client submits a form</text>
<!-- Pipeline steps -->
<rect x="50" y="310" width="90" height="32" rx="6" fill="#1e293b" stroke="#3b82f6" stroke-width="0.75"/>
<text x="95" y="330" text-anchor="middle" fill="#93c5fd" font-size="9">param()</text>
<line x1="140" y1="326" x2="155" y2="326" stroke="#64748b" stroke-width="1"/>
<polygon points="163,326 155,322 155,330" fill="#64748b"/>
<rect x="163" y="310" width="90" height="32" rx="6" fill="#1e293b" stroke="#8b5cf6" stroke-width="0.75"/>
<text x="208" y="330" text-anchor="middle" fill="#c4b5fd" font-size="9">db()</text>
<line x1="253" y1="326" x2="268" y2="326" stroke="#64748b" stroke-width="1"/>
<polygon points="276,326 268,322 268,330" fill="#64748b"/>
<!-- datastar / sse step highlighted -->
<rect x="276" y="306" width="180" height="40" rx="6" fill="#451a03" stroke="#f59e0b" stroke-width="1.5"/>
<text x="366" y="324" text-anchor="middle" fill="#fcd34d" font-size="10" font-weight="700">datastar() / sse()</text>
<text x="366" y="339" text-anchor="middle" fill="#fbbf24" font-size="8">.channel = "todos/{{user_id}}"</text>
<!-- Arrow from datastar step up to channel -->
<path d="M366,306 L366,280 L380,280 L380,162" stroke="#f59e0b" stroke-width="2" fill="none"/>
<polygon points="380,154 375,162 385,162" fill="#f59e0b"/>
<text x="394" y="230" fill="#f59e0b" font-size="9" font-weight="600">push to channel</text>
<!-- Arrows: channel → clients (broadcast) -->
<line x1="280" y1="125" x2="218" y2="94" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="210,90 218,89 216,99" fill="#f59e0b"/>
<line x1="280" y1="140" x2="218" y2="186" stroke="#f59e0b" stroke-width="1.5" stroke-dasharray="4,3"/>
<polygon points="210,190 218,181 216,191" fill="#f59e0b"/>
<text x="222" y="110" fill="#92400e" font-size="8">broadcast</text>
<text x="222" y="180" fill="#92400e" font-size="8">broadcast</text>
<!-- Datastar DOM update note -->
<rect x="530" y="100" width="200" height="64" rx="8" fill="#111827" stroke="#1f2937" stroke-width="1"/>
<text x="550" y="120" fill="#6b7280" font-size="9" font-weight="600">DATASTAR MODE</text>
<text x="550" y="136" fill="#94a3b8" font-size="9">.target = "todos"</text>
<text x="550" y="152" fill="#94a3b8" font-size="9">.mode = mode_prepend</text>
<!-- Footer -->
<text x="380" y="396" text-anchor="middle" fill="#64748b" font-size="10">Without .channel, events push directly to the requesting client</text>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -0,0 +1,53 @@
#include <mach.h>
#include <sqlite.h>
#include <session_auth.h>
config activity(){
return (config) {
.name = "activity",
.resources = {{
{"activity", "/activity",
.get = {{
query((d){
.set = "activity",
.db = "activity_db",
.query = (asset){
#embed "get_activities.sql"
}
}),
render((r){(asset){
#embed "activity.mustache.html"
}})
}}
}},
.before = {
session(),
logged_in()
}
},
.events = {
{"todo_created", {
query((d){
.db = "activity_db",
.query = (asset){
#embed "insert_activity.sql"
}
})
}}
},
.databases = {{
.name = "activity_db",
.engine = sqlite_db,
.connect = "file:activity.db?mode=rwc",
.migrations = {(asset){
#embed "create_activity_table.sql"
}}
}},
.modules = {sqlite, session_auth}
};
}

View File

@@ -0,0 +1,13 @@
{{< layout}}
{{$body}}
{{^activity}}
<p>no activity</p>
{{/activity}}
{{#activity}}
<p>activity</p>
{{#.}}
<p>{{action}}: {{title}} ({{created_at}})</p>
{{/.}}
{{/activity}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS activity (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
action TEXT NOT NULL,
title TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_activity_user_id ON activity(user_id);

View File

@@ -0,0 +1,5 @@
select action, title, created_at
from activity
where user_id = {{user_id}}
order by created_at desc
limit 50;

View File

@@ -0,0 +1,2 @@
insert into activity(user_id, action, title)
values({{user_id}}, 'created', {{title}});

42
08_todo_events/main.c Normal file
View File

@@ -0,0 +1,42 @@
#include <mach.h>
#include <session_auth.h>
#include "todos/todos.c"
#include "activity/activity.c"
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
render((r){(asset){
#embed "static/home.mustache.html"
}})
}}
}},
.before = {session()},
.context = {
{"layout", (asset){
#embed "static/layout.mustache.html"
}}
},
.errors = {
{http_error, {
render((r){(asset){
#embed "static/5xx.mustache.html"
}})
}},
{http_not_found, {
render((r){(asset){
#embed "static/404.mustache.html"
}})
}}
}
},
.modules = {todos, activity, session_auth}
};
}

View File

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>not found</p>
{{/body}}
{{/layout}};

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>error</p>
{{/body}}
{{/layout}};

View File

@@ -0,0 +1,5 @@
{{< layout}}
{{$body}}
<p>home</p>
{{/body}}
{{/layout}};

View File

@@ -0,0 +1,20 @@
<html>
<head>
<link rel='icon' href='/favicon.png'>
</head>
<body>
<p>
{{^user}}
<a href='/login'>sign in</a>
{{/user}}
{{#user}}
welcome, {{short_name}}
{{/user}}
</p>
<nav>
<a href='/'>home</a>
</nav>
{{$body}}
{{/body}}
</body>
</html>

View File

@@ -0,0 +1,2 @@
insert into todos(user_id, title)
values({{user_id}}, {{title}});

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
finished INTEGER CHECK(finished IN (1))
);
CREATE INDEX IF NOT EXISTS idx_todos_user_id ON todos(user_id);

View File

@@ -0,0 +1,3 @@
delete from todos
where user_id = {{user_id}}
and id = {{id}};

View File

@@ -0,0 +1,3 @@
select id, title, finished
from todos
where user_id = {{user_id}};

View File

@@ -0,0 +1,17 @@
<div>
<form action="/todos/{{id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="PATCH">
{{^finished}}
<input type="checkbox" name="finished" value="1">
{{/finished}}
{{#finished}}
<input type="checkbox" name="finished" value="1" checked>
{{/finished}}
{{title}}
<button type="submit">save</button>
</form>
<form action="/todos/{{id}}" method="post" style="display:inline">
<input type="hidden" name="http_method" value="delete">
<button type="submit">delete</button>
</form>
</div>

View File

@@ -0,0 +1,108 @@
#include <mach.h>
#include <sqlite.h>
#include <session_auth.h>
config todos(){
return (config) {
.name = "todos",
.resources = {{
{"todos", "/todos",
.get = {{
query((d){
.set = "todos",
.db = "todos_db",
.query = (asset){
#embed "get_todos.sql"
}
}),
render((r){(asset){
#embed "todos.mustache.html"
}})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^\\S{1,16}$",
.message = "must be 1-16 characters, no spaces"
}),
query((d){
.db = "todos_db",
.query = (asset){
#embed "create_todo.sql"
}
}),
emit((e){"todo_created"}),
redirect((u){"todos"})
}},
.context = {
{"todo", (asset){
#embed "todo.mustache.html"
}}
}
},
{"todo", "/todos/:id",
.patch = {{
validate((v){
.name = "finished",
.optional = true,
.validation = "1",
.message = "must be 1"
}),
find((d){
.db = "todos_db",
.query = (asset){
#embed "update_todo.sql"
}
}),
redirect((u){"todos"})
}},
.delete = {{
find((d){
.db = "todos_db",
.query = (asset){
#embed "delete_todo.sql"
}
}),
redirect((u){"todos"})
}},
.before = {
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "must be between 1-9999999999"
})
}
}},
.before = {
session(),
logged_in()
}
},
.databases = {{
.engine = sqlite_db,
.name = "todos_db",
.connect = "file:todo.db?mode=rwc",
.migrations = {(asset){
#embed "create_todos_table.sql"
}}
}},
.publishes = {
{"todo_created",
.with = {
"user_id",
"title"
}
}
},
.modules = {sqlite, session_auth}
};
}

View File

@@ -0,0 +1,16 @@
{{< layout}}
{{$body}}
<form action="/todos" method="post">
<input type="text" name="title" placeholder="new todo" required>
<button type="submit">add</button>
</form>
{{^todos}}
<p>no todos</p>
{{/todos}}
{{#todos}}
{{#.}}
{{> todo}}
{{/.}}
{{/todos}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,4 @@
update todos
set finished = {{finished}}
where user_id = {{user_id}}
and id = {{id}};

View File

@@ -0,0 +1,89 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 340" width="760" height="340">
<rect width="760" height="340" fill="#09080f"/>
<defs>
<linearGradient id="dbG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#8b5cf6"/>
<stop offset="100%" stop-color="#7c3aed"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<text x="380" y="28" text-anchor="middle" fill="#e2e8f0" font-size="13" font-weight="700">Database Multi-Tenancy</text>
<!-- Config block -->
<rect x="40" y="50" width="320" height="86" rx="10" fill="#111827" stroke="#1f2937" stroke-width="1"/>
<text x="60" y="74" fill="#6b7280" font-size="9" font-weight="600">.databases config</text>
<text x="60" y="96" fill="#c4b5fd" font-size="10">.connect =</text>
<text x="60" y="114" fill="#fcd34d" font-size="10">"file:<tspan fill="#f59e0b" font-weight="700">{{user_id}}</tspan>_todo.db?mode=rwc"</text>
<!-- Arrow right -->
<line x1="360" y1="93" x2="398" y2="93" stroke="#64748b" stroke-width="1.5"/>
<polygon points="408,93 398,88 398,98" fill="#64748b"/>
<!-- Resolution box -->
<rect x="408" y="50" width="180" height="86" rx="10" fill="#111827" stroke="#f59e0b" stroke-width="1"/>
<text x="428" y="74" fill="#f59e0b" font-size="9" font-weight="600">RUNTIME RESOLUTION</text>
<text x="428" y="96" fill="#94a3b8" font-size="10">user_id from</text>
<text x="428" y="114" fill="#94a3b8" font-size="10">pipeline context</text>
<!-- Three request arrows going down -->
<!-- User alice -->
<rect x="40" y="170" width="180" height="36" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="1"/>
<text x="130" y="192" text-anchor="middle" fill="#93c5fd" font-size="10">Request: user_id = alice</text>
<!-- User bob -->
<rect x="40" y="224" width="180" height="36" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="1"/>
<text x="130" y="246" text-anchor="middle" fill="#93c5fd" font-size="10">Request: user_id = bob</text>
<!-- User carol -->
<rect x="40" y="278" width="180" height="36" rx="8" fill="#1e293b" stroke="#3b82f6" stroke-width="1"/>
<text x="130" y="300" text-anchor="middle" fill="#93c5fd" font-size="10">Request: user_id = carol</text>
<!-- Arrows to databases -->
<line x1="220" y1="188" x2="398" y2="188" stroke="#64748b" stroke-width="1"/>
<polygon points="408,188 398,183 398,193" fill="#64748b"/>
<line x1="220" y1="242" x2="398" y2="242" stroke="#64748b" stroke-width="1"/>
<polygon points="408,242 398,237 398,247" fill="#64748b"/>
<line x1="220" y1="296" x2="398" y2="296" stroke="#64748b" stroke-width="1"/>
<polygon points="408,296 398,291 398,301" fill="#64748b"/>
<!-- Database: alice -->
<rect x="408" y="170" width="220" height="36" rx="8" fill="#2d2640" stroke="#8b5cf6" stroke-width="1.5"/>
<text x="440" y="192" fill="#c4b5fd" font-size="10">file:</text>
<text x="472" y="192" fill="#fcd34d" font-size="10" font-weight="700">alice</text>
<text x="505" y="192" fill="#c4b5fd" font-size="10">_todo.db</text>
<!-- Database: bob -->
<rect x="408" y="224" width="220" height="36" rx="8" fill="#2d2640" stroke="#8b5cf6" stroke-width="1.5"/>
<text x="440" y="246" fill="#c4b5fd" font-size="10">file:</text>
<text x="472" y="246" fill="#fcd34d" font-size="10" font-weight="700">bob</text>
<text x="495" y="246" fill="#c4b5fd" font-size="10">_todo.db</text>
<!-- Database: carol -->
<rect x="408" y="278" width="220" height="36" rx="8" fill="#2d2640" stroke="#8b5cf6" stroke-width="1.5"/>
<text x="440" y="300" fill="#c4b5fd" font-size="10">file:</text>
<text x="472" y="300" fill="#fcd34d" font-size="10" font-weight="700">carol</text>
<text x="505" y="300" fill="#c4b5fd" font-size="10">_todo.db</text>
<!-- DB icons (simple cylinder shapes) -->
<g transform="translate(648, 170)">
<ellipse cx="18" cy="6" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
<rect x="0" y="6" width="36" height="24" fill="#7c3aed" opacity="0.3"/>
<ellipse cx="18" cy="30" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
</g>
<g transform="translate(648, 224)">
<ellipse cx="18" cy="6" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
<rect x="0" y="6" width="36" height="24" fill="#7c3aed" opacity="0.3"/>
<ellipse cx="18" cy="30" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
</g>
<g transform="translate(648, 278)">
<ellipse cx="18" cy="6" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
<rect x="0" y="6" width="36" height="24" fill="#7c3aed" opacity="0.3"/>
<ellipse cx="18" cy="30" rx="18" ry="6" fill="#7c3aed" opacity="0.5"/>
</g>
<!-- Footer -->
<text x="380" y="336" text-anchor="middle" fill="#64748b" font-size="10">One config, per-user databases — Mustache interpolation in .connect at runtime</text>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -0,0 +1 @@
compare to https://github.com/t3dotgg/1app5stacks

View File

@@ -0,0 +1,180 @@
#include <mach.h>
#include <htmx.h>
#include <sqlite.h>
#include <tailwind.h>
void assign_opponents(){
auto const t = get("challengers");
auto const p0 = table_get(t, 0);
auto const p1 = table_get(t, 1);
record_set(p0, "opponent_id", record_get(p1, "id"));
record_set(p1, "opponent_id", record_get(p0, "id"));
}
config mach(){
return (config) {
.resources = {{
{"home", "/",
.get = {{
query((d){
.set = "challengers",
.db = "pokemon_db",
.query =
"select id, name, sprite "
"from pokemons "
"order by random() "
"limit 2;"
}),
invoke((i){assign_opponents}),
render((r){.asset = "home"})
}},
.post = {{
validate((v){
.name = "winner",
.validation = "^\\d{1,8}$",
.message = "must be 1-99999999"
}),
validate((v){
.name = "loser",
.validation = "^\\d{1,8}$",
.message = "must be 1-99999999"
}),
find((d){
.db = "pokemon_db",
.query =
"begin transaction; "
"update pokemons "
"set wins = wins + 1 "
"where id = {{winner}}; "
"update pokemons "
"set loses = loses + 1 "
"where id = {{loser}}; "
"commit;"
}),
reroute((u){"home"})
}}
},
{"result", "/results",
.get = {{
query((d){
.set = "results",
.db = "pokemon_db",
.query =
"select id, name, sprite, wins, loses, "
"cast(wins as real) / nullif(loses, 0) as ratio_of_wins_to_loses, "
"row_number() over (order by (cast(wins as real) / nullif(loses, 0)) desc) as rank "
"from pokemons "
"order by ratio_of_wins_to_loses desc;"
}),
render((r){
"{{< home}}"
"{{$body}}"
"<div class='container mx-auto px-4 py-8 text-white'>"
"<div class='grid gap-4'>"
"{{#results}}"
"<div class='flex items-center gap-6 p-6 bg-gray-800/40 rounded-lg shadow hover:shadow-md transition-shadow'>"
"<div class='text-2xl font-bold text-gray-400 w-8'>"
"{{rank}}"
"</div>"
"<img src='{{sprite}}' alt='{{name}}' loading='lazy' class='w-20 h-20'>"
"<div class='flex-grow'>"
"<div class='text-gray-400 text-sm'>#{{id}}</div>"
"<h2 class='text-xl font-semibold capitalize'>{{name}}</h2>"
"</div>"
"<div class='text-right'>"
"<div class='text-2xl font-bold text-blue-400'>"
"{{ratio_of_wins_to_loses}}"
"</div>"
"<div class='text-sm text-gray-400'>"
"{{wins}}W - {{loses}}L"
"</div>"
"</div>"
"</div>"
"{{/results}}"
"</div>"
"</div>"
"{{/body}}"
"{{/home}}"
})
}}
}},
.context = {
{"home",
"<!DOCTYPE html>"
"<html>"
"<head>"
"<title>Roundest (MaCH Version)</title>"
"{{> htmx_script}}"
"{{> tailwind_script}}"
"</head>"
"<body hx-boost='true'>"
"<div class='bg-gray-950 text-white flex flex-col justify-between font-geist min-h-screen border-t-2 border-red-300'>"
"<nav class='flex flex-row justify-between items-center py-4 px-8'>"
"<div class='flex items-baseline'>"
"<a href='{{url:home}}' class='font-bold text-3xl'>"
"Round<span class='text-red-300'>est</span>"
"</a>"
"<p class='text-gray-400 font-extralight pl-2 text-2xl'>"
"(MaCH Version)"
"</p>"
"</div>"
"<div class='flex flex-row items-center gap-8'>"
"<a href='{{url:results}}' class='hover:underline text-lg'>Results</a>"
"</div>"
"</nav>"
"<main>"
"{{$body}}"
"<div class='container mx-auto px-4'>"
"<h1 class='text-3xl font-bold text-center mb-8'>Vote for which is roundest</h1>"
"<div class='flex justify-center gap-16 items-center min-h-[80vh]'>"
"{{#challengers}}"
"<form action='{{url:home}}' method='post'>"
"<div class='flex flex-col items-center gap-4'>"
"<img src='{{sprite}}' alt='{{name}}' class='w-64 h-64 sprite'>"
"<div class='text-center'>"
"<span class='text-gray-500 text-lg'>#{{id}}</span>"
"<h2 class='text-2xl font-bold capitalize'>{{name}}</h2>"
"<input type='hidden' name='winner' value='{{id}}'>"
"<input type='hidden' name='loser' value='{{opponent_id}}'>"
"<button class='mt-4 px-8 py-3 bg-blue-500 text-white rounded-lg text-lg font-semibold hover:bg-blue-600 transition-colors'>"
"Vote"
"</button>"
"</div>"
"</div>"
"</form>"
"{{/challengers}}"
"</div>"
"</div>"
"{{/body}}"
"</main>"
"<footer class='font-light text-center py-3 text-gray-500'>"
"<a href='https://code.nightshadecoder.dev/nightshade/mach_examples' target='_blank'>GitHub</a>"
"</footer>"
"</div>"
"</body>"
"</html>"
}
}
},
.databases = {{
.name = "pokemon_db",
.engine = sqlite_db,
.connect = "file::memory:?cache=shared",
.migrations = {
"CREATE TABLE IF NOT EXISTS pokemons ( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT NOT NULL, "
"sprite TEXT NOT NULL, "
"wins INTEGER NOT NULL DEFAULT 0, "
"loses INTEGER NOT NULL DEFAULT 0 "
");"
}
}},
.modules = {htmx, sqlite, tailwind}
};
}

View File

@@ -0,0 +1,82 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 300" width="760" height="300">
<rect width="760" height="300" fill="#09080f"/>
<defs>
<linearGradient id="bootG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#7c3aed"/>
<stop offset="100%" stop-color="#a855f7"/>
</linearGradient>
<linearGradient id="warmG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#f59e0b"/>
<stop offset="100%" stop-color="#fbbf24"/>
</linearGradient>
<linearGradient id="runG" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#10b981"/>
<stop offset="100%" stop-color="#34d399"/>
</linearGradient>
<style>text{font-family:'JetBrains Mono','Fira Code',monospace}</style>
</defs>
<text x="380" y="28" text-anchor="middle" fill="#e2e8f0" font-size="13" font-weight="700">Boot-Time Compilation</text>
<!-- Phase 1: config() -->
<rect x="30" y="50" width="200" height="110" rx="10" fill="#1e1b2e" stroke="#7c3aed" stroke-width="1.5"/>
<rect x="30" y="50" width="200" height="32" rx="10" fill="url(#bootG)"/>
<text x="130" y="71" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">config mach()</text>
<text x="50" y="96" fill="#c4b5fd" font-size="9">.includes</text>
<text x="50" y="112" fill="#c4b5fd" font-size="9">.databases</text>
<text x="50" y="128" fill="#c4b5fd" font-size="9">.resources</text>
<text x="50" y="144" fill="#c4b5fd" font-size="9">.events / .errors</text>
<!-- Arrow 1 -->
<line x1="230" y1="105" x2="268" y2="105" stroke="#64748b" stroke-width="1.5"/>
<polygon points="278,105 268,100 268,110" fill="#64748b"/>
<!-- Phase 2: Precompile -->
<rect x="278" y="50" width="200" height="110" rx="10" fill="#1e1b2e" stroke="#f59e0b" stroke-width="1.5"/>
<rect x="278" y="50" width="200" height="32" rx="10" fill="url(#warmG)"/>
<text x="378" y="71" text-anchor="middle" fill="#1e1b2e" font-size="11" font-weight="700">Precompile</text>
<text x="298" y="96" fill="#fcd34d" font-size="9">Compile pipelines</text>
<text x="298" y="112" fill="#fcd34d" font-size="9">Compile templates</text>
<text x="298" y="128" fill="#fcd34d" font-size="9">Prepare SQL statements</text>
<text x="298" y="144" fill="#fcd34d" font-size="9">Build route table</text>
<!-- Arrow 2 -->
<line x1="478" y1="105" x2="516" y2="105" stroke="#64748b" stroke-width="1.5"/>
<polygon points="526,105 516,100 516,110" fill="#64748b"/>
<!-- Phase 3: Execution Graph -->
<rect x="526" y="50" width="200" height="110" rx="10" fill="#1e1b2e" stroke="#10b981" stroke-width="1.5"/>
<rect x="526" y="50" width="200" height="32" rx="10" fill="url(#runG)"/>
<text x="626" y="71" text-anchor="middle" fill="#fff" font-size="11" font-weight="700">Execution Graph</text>
<text x="546" y="96" fill="#6ee7b7" font-size="9">Pre-warmed pipelines</text>
<text x="546" y="112" fill="#6ee7b7" font-size="9">Optimized query plans</text>
<text x="546" y="128" fill="#6ee7b7" font-size="9">Compiled templates</text>
<text x="546" y="144" fill="#6ee7b7" font-size="9">Ready to serve</text>
<!-- Phase labels -->
<text x="130" y="180" text-anchor="middle" fill="#7c3aed" font-size="9" font-weight="600">RUNS ONCE AT BOOT</text>
<text x="378" y="180" text-anchor="middle" fill="#f59e0b" font-size="9" font-weight="600">RUNS ONCE AT BOOT</text>
<text x="626" y="180" text-anchor="middle" fill="#10b981" font-size="9" font-weight="600">SERVES ALL REQUESTS</text>
<!-- Runtime section -->
<rect x="30" y="200" width="696" height="76" rx="10" fill="#0f0d1a" stroke="#10b981" stroke-width="1" stroke-dasharray="5,3"/>
<text x="50" y="224" fill="#6ee7b7" font-size="10" font-weight="600">Runtime: each request executes a pre-warmed pipeline</text>
<!-- Request arrows -->
<text x="50" y="248" fill="#475569" font-size="9">req →</text>
<rect x="90" y="236" width="80" height="24" rx="4" fill="#1e293b" stroke="#10b981" stroke-width="0.75"/>
<text x="130" y="252" text-anchor="middle" fill="#6ee7b7" font-size="9">pipeline</text>
<line x1="170" y1="248" x2="184" y2="248" stroke="#10b981" stroke-width="1"/>
<polygon points="192,248 184,244 184,252" fill="#10b981"/>
<text x="200" y="252" fill="#475569" font-size="9">→ response</text>
<text x="320" y="248" fill="#475569" font-size="9">req →</text>
<rect x="360" y="236" width="80" height="24" rx="4" fill="#1e293b" stroke="#10b981" stroke-width="0.75"/>
<text x="400" y="252" text-anchor="middle" fill="#6ee7b7" font-size="9">pipeline</text>
<line x1="440" y1="248" x2="454" y2="248" stroke="#10b981" stroke-width="1"/>
<polygon points="462,248 454,244 454,252" fill="#10b981"/>
<text x="470" y="252" fill="#475569" font-size="9">→ response</text>
<!-- Footer -->
<text x="380" y="296" text-anchor="middle" fill="#64748b" font-size="10">Zero per-request compilation — config builds the graph, requests just execute it</text>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

189
10_fetch_weather/main.c Normal file
View File

@@ -0,0 +1,189 @@
#include <mach.h>
config mach() {
return (config) {
.resources = {{
{"home", "/",
.get = {{
render((r){
"{{< layout}}"
"{{$body}}"
"<div class='card'>"
"<h1>Weather Lookup</h1>"
"<p class='sub'>Enter a US zip code to get the current forecast</p>"
"<form action='{{url:weather}}' method='get'>"
"<div class='form-row'>"
"<input type='text' name='zipcode' placeholder='e.g. 90210' "
"maxlength='5' pattern='[0-9]{5}' required />"
"<button type='submit'>Search</button>"
"</div>"
"</form>"
"</div>"
"{{/body}}"
"{{/layout}}"
})
}}
},
{"weather", "/weather",
.get = {{
validate((v){
.name = "zipcode",
.validation = "^[0-9]{5}$",
.message = "Zip code must be exactly 5 digits"
}),
fetch((f){
.set = "weather",
.url = "https://wttr.in/{{zipcode}}?format=j1",
}),
render((r){
"{{< layout}}"
"{{$body}}"
"<div class='card'>"
"<h1>Current Weather</h1>"
"{{#nearest_area}}"
"<p class='location'>{{areaName}} &mdash; {{region}}, {{country}}</p>"
"{{/nearest_area}}"
"{{#current_condition}}"
"<div class='temp-main'>{{temp_F}}&deg;F</div>"
"<div class='desc'>{{weatherDesc}}</div>"
"<div class='details'>"
"<div class='detail'>"
"<div class='label'>Feels Like</div>"
"<div class='value'>{{FeelsLikeF}}&deg;F</div>"
"</div>"
"<div class='detail'>"
"<div class='label'>Humidity</div>"
"<div class='value'>{{humidity}}%</div>"
"</div>"
"<div class='detail'>"
"<div class='label'>Wind</div>"
"<div class='value'>{{windspeedMiles}} mph</div>"
"</div>"
"<div class='detail'>"
"<div class='label'>UV Index</div>"
"<div class='value'>{{uvIndex}}</div>"
"</div>"
"</div>"
"{{/current_condition}}"
"<a class='back' href='{{url:home}}'>&larr; Search another zip code</a>"
"</div>"
"{{/body}}"
"{{/layout}}"
})
}},
.errors = {
{http_bad_request, {
render((r){
"{{< layout}}"
"{{$body}}"
"<div class='card center'>"
"<h1 class='error-icon'>Oops</h1>"
"<p class='error-msg'>Invalid zip code. Please enter exactly 5 digits.</p>"
"<a class='back' href='{{url:home}}'>&larr; Try again</a>"
"</div>"
"{{/body}}"
"{{/layout}}"
})
}}
}
}
}},
.context = {
{"layout",
"<html>"
"<head>"
"<title>Weather App</title>"
"<style>"
"* { margin: 0; padding: 0; box-sizing: border-box; }"
"body {"
"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;"
"background: linear-gradient(135deg, #0f2027, #203a43, #2c5364);"
"min-height: 100vh;"
"display: flex;"
"justify-content: center;"
"align-items: center;"
"color: #e0e0e0;"
"}"
".card {"
"background: rgba(255,255,255,0.08);"
"backdrop-filter: blur(12px);"
"border-radius: 16px;"
"padding: 48px;"
"width: 500px;"
"box-shadow: 0 8px 32px rgba(0,0,0,0.3);"
"border: 1px solid rgba(255,255,255,0.1);"
"}"
".card.center { text-align: center; }"
"h1 { font-size: 24px; margin-bottom: 4px; }"
".sub { color: #9ab; margin-bottom: 32px; font-size: 14px; }"
".location { color: #9ab; font-size: 14px; margin-bottom: 32px; }"
".form-row { display: flex; gap: 12px; }"
"input {"
"flex: 1;"
"padding: 14px 18px;"
"border-radius: 10px;"
"border: 1px solid rgba(255,255,255,0.15);"
"background: rgba(255,255,255,0.06);"
"color: #fff;"
"font-size: 16px;"
"outline: none;"
"}"
"input::placeholder { color: #789; }"
"input:focus { border-color: #5ba3d9; }"
"button {"
"padding: 14px 28px;"
"border-radius: 10px;"
"border: none;"
"background: #5ba3d9;"
"color: #fff;"
"font-size: 16px;"
"font-weight: 600;"
"cursor: pointer;"
"}"
"button:hover { background: #4a8ec4; }"
".temp-main {"
"font-size: 72px;"
"font-weight: 700;"
"line-height: 1;"
"margin-bottom: 4px;"
"}"
".desc {"
"font-size: 18px;"
"color: #acd;"
"margin-bottom: 32px;"
"}"
".details {"
"display: grid;"
"grid-template-columns: 1fr 1fr;"
"gap: 16px;"
"}"
".detail {"
"background: rgba(255,255,255,0.05);"
"border-radius: 10px;"
"padding: 16px;"
"}"
".detail .label { font-size: 12px; color: #789; text-transform: uppercase; }"
".detail .value { font-size: 20px; font-weight: 600; margin-top: 4px; }"
".back {"
"display: inline-block;"
"margin-top: 32px;"
"color: #5ba3d9;"
"text-decoration: none;"
"font-size: 14px;"
"}"
".back:hover { text-decoration: underline; }"
".error-icon { font-size: 48px; margin-bottom: 16px; }"
".error-msg { color: #e88; font-size: 16px; margin-bottom: 24px; }"
"</style>"
"</head>"
"<body>"
"{{$body}}{{/body}}"
"</body>"
"</html>"
}
}
};
}

437
11_rails_15_min_blog/main.c Normal file
View File

@@ -0,0 +1,437 @@
#include <mach.h>
#include <sqlite.h>
#include <tailwind.h>
// ─────────────────────────────────────────────
// 15 Minute Blog in MACH
// ─────────────────────────────────────────────
// Resources:
// GET / → redirect to posts
// GET /posts → list all posts
// POST /posts → create a post
// GET /posts/new → new post form
// GET /posts/:id → show a post
// PATCH /posts/:id → update a post
// DEL /posts/:id → delete a post
// GET /posts/:id/edit→ edit post form
// ─────────────────────────────────────────────
config mach() {
return (config) {
// ── Resources ──────────────────────────────
.resources = {{
// Home: just redirect to the posts index
{"home", "/",
.get = {{
redirect((u){"posts"})
}}
},
// Posts index + create
{"posts", "/posts",
.get = {{
query((d){
.set = "posts",
.db = "blog_db",
"select id, title, body, created_at "
"from posts "
"order by created_at desc;"
}),
render((r){
"{{< layout}}"
"{{$body}}"
"<div class=\"flex items-center justify-between mb-8\">"
"<h1 class=\"text-3xl font-bold text-gray-900\">"
"All Posts"
"</h1>"
"<a href=\"{{url:new_post}}\" "
"class=\"bg-blue-600 text-white px-4 py-2 rounded-lg "
"hover:bg-blue-700 transition\">"
"New Post"
"</a>"
"</div>"
"{{#posts}}"
"{{> post_card}}"
"{{/posts}}"
"{{^posts}}"
"<div class=\"text-center py-16 text-gray-400\">"
"<p class=\"text-xl\">No posts yet.</p>"
"<a href=\"{{url:new_post}}\" "
"class=\"text-blue-600 hover:underline mt-2 inline-block\">"
"Write your first post"
"</a>"
"</div>"
"{{/posts}}"
"{{/body}}"
"{{/layout}}"
})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^.{1,200}$",
.message = "Title is required (max 200 characters)"
}),
validate((v){
.name = "body",
.validation = "^[\\s\\S]{1,50000}$",
.message = "Body is required"
}),
query((d){
.db = "blog_db",
"insert into posts (title, body) "
"values ({{title}}, {{body}});"
}),
redirect((u){"posts"})
}}
},
// New post form
{"new_post", "/posts/new",
.get = {{
render((r){
"{{< layout}}"
"{{$body}}"
"<h1 class=\"text-3xl font-bold text-gray-900 mb-8\">"
"New Post"
"</h1>"
"<form method=\"post\" action=\"{{url:posts}}\">"
"{{> post_form}}"
"</form>"
"<a href=\"{{url:posts}}\" "
"class=\"text-gray-500 hover:text-gray-700 mt-4 inline-block\">"
"&larr; Back to posts"
"</a>"
"{{/body}}"
"{{/layout}}"
})
}}
},
// Single post: show, update, delete
{"post", "/posts/:id",
.get = {{
find((d){
.set = "post",
.db = "blog_db",
"select id, title, body, created_at "
"from posts "
"where id = {{id}};"
}),
render((r){
"{{< layout}}"
"{{$body}}"
"{{#post}}"
"<article class=\"max-w-none\">"
"<h1 class=\"text-4xl font-bold text-gray-900 mb-2\">"
"{{title}}"
"</h1>"
"<p class=\"text-sm text-gray-400 mb-8\">"
"Published {{created_at}}"
"</p>"
"<div class=\"prose prose-lg text-gray-700 "
"whitespace-pre-line leading-relaxed\">"
"{{body}}"
"</div>"
"</article>"
"<div class=\"flex gap-4 mt-10 pt-6 border-t border-gray-200\">"
"<a href=\"{{url:edit_post:id}}\" "
"class=\"bg-gray-100 text-gray-700 px-4 py-2 "
"rounded-lg hover:bg-gray-200 transition\">"
"Edit"
"</a>"
"<form method=\"post\" action=\"{{url:post:id}}\" "
"class=\"inline\">"
"<input type=\"hidden\" name=\"http_method\" value=\"delete\">"
"<button type=\"submit\" "
"class=\"bg-red-50 text-red-600 px-4 py-2 "
"rounded-lg hover:bg-red-100 transition\" "
"onclick=\"return confirm('Delete this post?')\">"
"Delete"
"</button>"
"</form>"
"<a href=\"{{url:posts}}\" "
"class=\"text-gray-500 hover:text-gray-700 px-4 py-2\">"
"&larr; Back"
"</a>"
"</div>"
"{{/post}}"
"{{/body}}"
"{{/layout}}"
})
}},
.patch = {{
validate((v){
.name = "title",
.validation = "^.{1,200}$",
.message = "Title is required (max 200 characters)"
}),
validate((v){
.name = "body",
.validation = "^[\\s\\S]{1,50000}$",
.message = "Body is required"
}),
query((d){
.db = "blog_db",
"update posts "
"set title = {{title}}, body = {{body}} "
"where id = {{id}};"
}),
redirect((u){"post"})
}},
.delete = {{
query((d){
.db = "blog_db",
"delete from posts "
"where id = {{id}};"
}),
redirect((u){"posts"})
}},
.before = {
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "Invalid post ID"
})
}
},
// Edit post form
{"edit_post", "/posts/:id/edit",
.get = {{
find((d){
.set = "post",
.db = "blog_db",
"select id, title, body "
"from posts "
"where id = {{id}};"
}),
render((r){
"{{< layout}}"
"{{$body}}"
"{{#post}}"
"<h1 class=\"text-3xl font-bold text-gray-900 mb-8\">"
"Edit Post"
"</h1>"
"<form method=\"post\" action=\"{{url:post:id}}\">"
"<input type=\"hidden\" name=\"http_method\" value=\"patch\">"
"{{> post_form}}"
"</form>"
"<a href=\"{{url:post:id}}\" "
"class=\"text-gray-500 hover:text-gray-700 mt-4 "
"inline-block\">"
"&larr; Back to post"
"</a>"
"{{/post}}"
"{{/body}}"
"{{/layout}}"
})
}},
.before = {
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "Invalid post ID"
})
}
}
},
// ── Shared Layout & Partials ───────────────
.context = {
// Layout — the HTML shell every page inherits from
{"layout",
"<!DOCTYPE html>"
"<html lang=\"en\">"
"<head>"
"<meta charset=\"utf-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
"<title>MACH Blog</title>"
"{{> tailwind_script}}"
"</head>"
"<body class=\"bg-gray-50 min-h-screen\">"
"<nav class=\"bg-white border-b border-gray-200\">"
"<div class=\"max-w-3xl mx-auto px-6 py-4 "
"flex items-center justify-between\">"
"<a href=\"{{url:posts}}\" "
"class=\"text-xl font-bold text-gray-900 "
"hover:text-blue-600 transition\">"
"MACH Blog"
"</a>"
"<span class=\"text-xs text-gray-400\">"
"built in 15 minutes"
"</span>"
"</div>"
"</nav>"
"<main class=\"max-w-3xl mx-auto px-6 py-10\">"
"{{$body}}{{/body}}"
"</main>"
"<footer class=\"max-w-3xl mx-auto px-6 py-6 text-center "
"text-xs text-gray-300\">"
"Powered by MACH"
"</footer>"
"</body>"
"</html>"
},
// Partial: post card for the index listing
{"post_card",
"<article class=\"bg-white rounded-xl shadow-sm border border-gray-100 "
"p-6 mb-4 hover:shadow-md transition\">"
"<a href=\"{{url:post:id}}\" class=\"block\">"
"<h2 class=\"text-xl font-semibold text-gray-900 "
"hover:text-blue-600 transition\">"
"{{title}}"
"</h2>"
"<p class=\"text-sm text-gray-400 mt-1\">"
"{{created_at}}"
"</p>"
"<p class=\"text-gray-600 mt-3 line-clamp-2\">"
"{{body}}"
"</p>"
"</a>"
"</article>"
},
// Partial: reusable form fields for new + edit
{"post_form",
"<div class=\"space-y-6\">"
"<div>"
"<label for=\"title\" "
"class=\"block text-sm font-medium text-gray-700 mb-1\">"
"Title"
"</label>"
"<input type=\"text\" id=\"title\" name=\"title\" "
"value=\"{{title}}\" "
"placeholder=\"Your post title\" "
"class=\"w-full px-4 py-2 border border-gray-300 rounded-lg "
"focus:ring-2 focus:ring-blue-500 focus:border-blue-500 "
"outline-none transition\">"
"</div>"
"<div>"
"<label for=\"body\" "
"class=\"block text-sm font-medium text-gray-700 mb-1\">"
"Body"
"</label>"
"<textarea id=\"body\" name=\"body\" rows=\"12\" "
"placeholder=\"Write your post...\" "
"class=\"w-full px-4 py-2 border border-gray-300 rounded-lg "
"focus:ring-2 focus:ring-blue-500 "
"focus:border-blue-500 outline-none transition "
"resize-y\">"
"{{body}}"
"</textarea>"
"</div>"
"<button type=\"submit\" "
"class=\"bg-blue-600 text-white px-6 py-2 rounded-lg "
"hover:bg-blue-700 transition font-medium\">"
"Save Post"
"</button>"
"</div>"
}
},
// ── Error Handling ─────────────────────────
.errors = {
{http_not_found, {
render((r){
"{{< layout}}"
"{{$body}}"
"<div class=\"text-center py-20\">"
"<h1 class=\"text-6xl font-bold text-gray-200\">404</h1>"
"<p class=\"text-gray-500 mt-4\">Post not found.</p>"
"<a href=\"{{url:posts}}\" "
"class=\"text-blue-600 hover:underline mt-4 inline-block\">"
"&larr; Back to all posts"
"</a>"
"</div>"
"{{/body}}"
"{{/layout}}"
})
}},
{http_bad_request, {
render((r){
.status = http_bad_request,
"{{< layout}}"
"{{$body}}"
"<div class=\"text-center py-20\">"
"<h1 class=\"text-4xl font-bold text-gray-200\">"
"Validation Error"
"</h1>"
"<p class=\"text-red-500 mt-4\">"
"Please check your input and try again."
"</p>"
"<a href=\"{{url:posts}}\" "
"class=\"text-blue-600 hover:underline mt-4 inline-block\">"
"&larr; Back to all posts"
"</a>"
"</div>"
"{{/body}}"
"{{/layout}}"
})
}}
}
},
// ── Database ───────────────────────────────
.databases = {{
.engine = sqlite_db,
.name = "blog_db",
.connect = "file:blog.db?mode=rwc",
.migrations = {
"CREATE TABLE posts ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"title TEXT NOT NULL,"
"body TEXT NOT NULL,"
"created_at TEXT NOT NULL "
"DEFAULT (strftime('%Y-%m-%d %H:%M', 'now'))"
");"
},
.seeds = {
"INSERT INTO posts (title, body) VALUES"
"('Hello World', "
" 'Welcome to the MACH blog! This post was created by the "
"database seed. MACH is a high-performance, declarative web "
"framework for C that lets you build full CRUD apps in a "
"single file with zero boilerplate.'),"
"('Why Declarative C?', "
" 'Traditional C web development means manual memory management, "
"string wrangling, and security pitfalls at every turn. MACH "
"flips the script: you declare resources, pipelines, and "
"templates. The runtime handles allocation, prepared statements, "
"XSS escaping, and async I/O. You get the speed of C with the "
"ergonomics of a modern framework.');"
}
}},
// ── Modules ────────────────────────────────
.modules = {sqlite, tailwind}
};
}

View File

@@ -0,0 +1,11 @@
{{< layout}}
{{$body}}
<div class="text-center py-20">
<h1 class="text-4xl font-bold text-gray-200">Validation Error</h1>
<p class="text-red-500 mt-4">Please check your input and try again.</p>
<a href="{{url:posts}}" class="text-blue-600 hover:underline mt-4 inline-block">
&larr; Back to all posts
</a>
</div>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,11 @@
{{< layout}}
{{$body}}
<div class="text-center py-20">
<h1 class="text-6xl font-bold text-gray-200">404</h1>
<p class="text-gray-500 mt-4">Post not found.</p>
<a href="{{url:posts}}" class="text-blue-600 hover:underline mt-4 inline-block">
&larr; Back to all posts
</a>
</div>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,6 @@
CREATE TABLE posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now'))
);

View File

@@ -0,0 +1,2 @@
DELETE FROM posts
WHERE id = {{id}};

View File

@@ -0,0 +1,16 @@
{{< layout}}
{{$body}}
{{#post}}
<h1 class="text-3xl font-bold text-gray-900 mb-8">Edit Post</h1>
<form method="post" action="{{url:post:id}}">
<input type="hidden" name="http_method" value="patch">
{{> post_form}}
</form>
<a href="{{url:post:id}}" class="text-gray-500 hover:text-gray-700 mt-4 inline-block">
&larr; Back to post
</a>
{{/post}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,3 @@
SELECT id, title, body, created_at
FROM posts
WHERE id = {{id}};

View File

@@ -0,0 +1,3 @@
SELECT id, title, body, created_at
FROM posts
ORDER BY created_at DESC;

View File

@@ -0,0 +1,2 @@
INSERT INTO posts (title, body)
VALUES ({{title}}, {{body}});

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MACH Blog</title>
{{> tailwind_script}}
</head>
<body class="bg-gray-50 min-h-screen">
<nav class="bg-white border-b border-gray-200">
<div class="max-w-3xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="{{url:posts}}" class="text-xl font-bold text-gray-900 hover:text-blue-600 transition">
MACH Blog
</a>
<span class="text-xs text-gray-400">built in 15 minutes</span>
</div>
</nav>
<main class="max-w-3xl mx-auto px-6 py-10">
{{$body}}{{/body}}
</main>
<footer class="max-w-3xl mx-auto px-6 py-6 text-center text-xs text-gray-300">
Powered by MACH
</footer>
</body>
</html>

View File

@@ -0,0 +1,205 @@
#include <mach.h>
#include <sqlite.h>
#include <tailwind.h>
// ─────────────────────────────────────────────
// 15 Minute Blog in MACH
// ─────────────────────────────────────────────
// Resources:
// GET / → redirect to posts
// GET /posts → list all posts
// POST /posts → create a post
// GET /posts/new → new post form
// GET /posts/:id → show a post
// PATCH /posts/:id → update a post
// DEL /posts/:id → delete a post
// GET /posts/:id/edit→ edit post form
// ─────────────────────────────────────────────
config mach() {
return (config) {
// ── Resources ──────────────────────────────
.resources = {{
{"home", "/",
.get = {{
redirect((u){"posts"})
}}
},
{"posts", "/posts",
.get = {{
query((d){
.set = "posts",
.db = "blog_db",
(asset){
#embed "get_posts.sql"
}
}),
render((r){(asset){
#embed "posts.mustache.html"
}})
}},
.post = {{
validate((v){
.name = "title",
.validation = "^.{1,200}$",
.message = "Title is required (max 200 characters)"
}),
validate((v){
.name = "body",
.validation = "^[\\s\\S]{1,50000}$",
.message = "Body is required"
}),
query((d){
.db = "blog_db",
(asset){
#embed "insert_post.sql"
}
}),
redirect((u){"posts"})
}}
},
{"new_post", "/posts/new",
.get = {{
render((r){(asset){
#embed "new_post.mustache.html"
}})
}}
},
{"edit_post", "/posts/:id/edit",
.get = {{
find((d){
.set = "post",
.db = "blog_db",
(asset){
#embed "get_post.sql"
}
}),
render((r){(asset){
#embed "edit_post.mustache.html"
}})
}},
.before = {
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "Invalid post ID"
})
}
},
{"post", "/posts/:id",
.get = {{
find((d){
.set = "post",
.db = "blog_db",
(asset){
#embed "get_post.sql"
}
}),
render((r){(asset){
#embed "post_show.mustache.html"
}})
}},
.patch = {{
validate((v){
.name = "title",
.validation = "^.{1,200}$",
.message = "Title is required (max 200 characters)"
}),
validate((v){
.name = "body",
.validation = "^[\\s\\S]{1,50000}$",
.message = "Body is required"
}),
query((d){
.db = "blog_db",
(asset){
#embed "update_post.sql"
}
}),
redirect((u){"post"})
}},
.delete = {{
query((d){
.db = "blog_db",
(asset){
#embed "delete_post.sql"
}
}),
redirect((u){"posts"})
}},
.before = {
validate((v){
.name = "id",
.validation = "^\\d{1,10}$",
.message = "Invalid post ID"
})
}
}
},
// ── Shared Layout & Partials ───────────────
.context = {
{"layout", (asset){
#embed "layout.mustache.html"
}},
{"post_card", (asset){
#embed "post_card.mustache.html"
}},
{"post_form", (asset){
#embed "post_form.mustache.html"
}}
},
// ── Error Handling ─────────────────────────
.errors = {
{http_not_found, {
render((r){(asset){
#embed "404.mustache.html"
}})
}},
{http_bad_request, {
render((r){
.status = http_bad_request,
(asset){
#embed "400.mustache.html"
}
})
}}
}
},
// ── Database ───────────────────────────────
.databases = {{
.engine = sqlite_db,
.name = "blog_db",
.connect = "file:blog.db?mode=rwc",
.migrations = {(asset){
#embed "create_posts.sql"
}},
.seeds = {(asset){
#embed "seed_posts.sql"
}}
}},
// ── Modules ────────────────────────────────
.modules = {sqlite, tailwind}
};
}

View File

@@ -0,0 +1,13 @@
{{< layout}}
{{$body}}
<h1 class="text-3xl font-bold text-gray-900 mb-8">New Post</h1>
<form method="post" action="{{url:posts}}">
{{> post_form}}
</form>
<a href="{{url:posts}}" class="text-gray-500 hover:text-gray-700 mt-4 inline-block">
&larr; Back to posts
</a>
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,9 @@
<article class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-4 hover:shadow-md transition">
<a href="{{url:post:id}}" class="block">
<h2 class="text-xl font-semibold text-gray-900 hover:text-blue-600 transition">
{{title}}
</h2>
<p class="text-sm text-gray-400 mt-1">{{created_at}}</p>
<p class="text-gray-600 mt-3 line-clamp-2">{{body}}</p>
</a>
</article>

View File

@@ -0,0 +1,19 @@
<div class="space-y-6">
<div>
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
<input type="text" id="title" name="title"
value="{{title}}"
placeholder="Your post title"
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition">
</div>
<div>
<label for="body" class="block text-sm font-medium text-gray-700 mb-1">Body</label>
<textarea id="body" name="body" rows="12"
placeholder="Write your post..."
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition resize-y">{{body}}</textarea>
</div>
<button type="submit"
class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition font-medium">
Save Post
</button>
</div>

View File

@@ -0,0 +1,31 @@
{{< layout}}
{{$body}}
{{#post}}
<article class="max-w-none">
<h1 class="text-4xl font-bold text-gray-900 mb-2">{{title}}</h1>
<p class="text-sm text-gray-400 mb-8">Published {{created_at}}</p>
<div class="prose prose-lg text-gray-700 whitespace-pre-line leading-relaxed">
{{body}}
</div>
</article>
<div class="flex gap-4 mt-10 pt-6 border-t border-gray-200">
<a href="{{url:edit_post:id}}"
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-200 transition">
Edit
</a>
<form method="post" action="{{url:post:id}}" class="inline">
<input type="hidden" name="http_method" value="delete">
<button type="submit"
class="bg-red-50 text-red-600 px-4 py-2 rounded-lg hover:bg-red-100 transition"
onclick="return confirm('Delete this post?')">
Delete
</button>
</form>
<a href="{{url:posts}}" class="text-gray-500 hover:text-gray-700 px-4 py-2">
&larr; Back
</a>
</div>
{{/post}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,24 @@
{{< layout}}
{{$body}}
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold text-gray-900">All Posts</h1>
<a href="{{url:new_post}}"
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition">
New Post
</a>
</div>
{{#posts}}
{{> post_card}}
{{/posts}}
{{^posts}}
<div class="text-center py-16 text-gray-400">
<p class="text-xl">No posts yet.</p>
<a href="{{url:new_post}}" class="text-blue-600 hover:underline mt-2 inline-block">
Write your first post
</a>
</div>
{{/posts}}
{{/body}}
{{/layout}}

View File

@@ -0,0 +1,5 @@
INSERT INTO posts (title, body) VALUES
('Hello World',
'Welcome to the MACH blog! This post was created by the database seed. MACH is a high-performance, declarative web framework for C that lets you build full CRUD apps in a single file with zero boilerplate.'),
('Why Declarative C?',
'Traditional C web development means manual memory management, string wrangling, and security pitfalls at every turn. MACH flips the script: you declare resources, pipelines, and templates. The runtime handles allocation, prepared statements, XSS escaping, and async I/O. You get the speed of C with the ergonomics of a modern framework.');

Some files were not shown because too many files have changed in this diff Show More