feat: implement MoloApp plans 01-15 with integration
Full C++23 framework tree: runtime, observability, security, data, app, realtime, supervisor, clients, CLI, example plus enterprise tenancy, audit, operability, governance, supplychain. Postgres via docker compose for tests.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
#include "App.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
std::shared_ptr<fw::Application> build_application(Services& services) {
|
||||
seed_services(services);
|
||||
load_templates(services);
|
||||
load_embedded(services);
|
||||
|
||||
auto app = std::make_shared<fw::Application>();
|
||||
app->use(fw::MiddlewareSlot::Errors, fw::errors_middleware());
|
||||
app->use(fw::MiddlewareSlot::Observability, fw::request_id_middleware());
|
||||
app->use(fw::MiddlewareSlot::SessionAuth,
|
||||
[](fw::Request req, fw::Next next) -> fw::Response {
|
||||
fw::Response res = next(std::move(req));
|
||||
res.headers.set("X-Frame-Options", "DENY");
|
||||
res.headers.set("X-Content-Type-Options", "nosniff");
|
||||
return res;
|
||||
});
|
||||
|
||||
fw::Router index;
|
||||
register_index(index, services);
|
||||
app->include_router("", index);
|
||||
|
||||
fw::Router routing;
|
||||
register_routing_demo(routing, services);
|
||||
app->include_router("/routing", routing, "routing.");
|
||||
|
||||
fw::Router data;
|
||||
register_data_demo(data, services);
|
||||
app->include_router("", data);
|
||||
|
||||
fw::Router security;
|
||||
register_security_demo(security, services);
|
||||
app->include_router("", security);
|
||||
|
||||
fw::Router formats;
|
||||
register_formats_demo(formats, services);
|
||||
app->include_router("", formats);
|
||||
|
||||
fw::Router realtime;
|
||||
register_realtime_demo(realtime, services);
|
||||
app->include_router("", realtime);
|
||||
|
||||
fw::Router ops;
|
||||
register_ops_demo(ops, services);
|
||||
app->include_router("", ops);
|
||||
|
||||
register_chain_demo(app, services);
|
||||
return app;
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
// Demo application assembly: shared Services (the DI skeleton), the root
|
||||
// Application with middleware, routers, and mounted sub-applications.
|
||||
// Gap: real molo::di::Container and plugin registry in 05-request-routing.md.
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "../fw/App.hpp"
|
||||
#include "../fw/Data.hpp"
|
||||
#include "../fw/Jobs.hpp"
|
||||
#include "../fw/Kv.hpp"
|
||||
#include "../fw/Net.hpp"
|
||||
#include "../fw/Observ.hpp"
|
||||
#include "../fw/Templates.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
struct Services {
|
||||
std::shared_ptr<fw::KvStore> kv = std::make_shared<fw::KvStore>();
|
||||
std::shared_ptr<fw::Cache> cache = std::make_shared<fw::Cache>();
|
||||
std::shared_ptr<fw::Pubsub> pubsub = std::make_shared<fw::Pubsub>();
|
||||
std::shared_ptr<fw::EventBus> events = std::make_shared<fw::EventBus>();
|
||||
std::shared_ptr<fw::Metrics> metrics = std::make_shared<fw::Metrics>();
|
||||
std::shared_ptr<fw::Telemetry> telemetry =
|
||||
std::make_shared<fw::Telemetry>();
|
||||
std::shared_ptr<fw::SessionStore> sessions =
|
||||
std::make_shared<fw::SessionStore>();
|
||||
std::shared_ptr<fw::Auth> auth = std::make_shared<fw::Auth>();
|
||||
std::shared_ptr<fw::OrmDb> sqlite =
|
||||
std::make_shared<fw::OrmDb>(fw::OrmBackend::Sqlite);
|
||||
std::shared_ptr<fw::OrmDb> postgres =
|
||||
std::make_shared<fw::OrmDb>(fw::OrmBackend::Postgres);
|
||||
std::shared_ptr<fw::UploadStore> uploads =
|
||||
std::make_shared<fw::UploadStore>();
|
||||
std::shared_ptr<fw::WebdavStore> webdav =
|
||||
std::make_shared<fw::WebdavStore>();
|
||||
std::shared_ptr<fw::Supervisor> supervisor =
|
||||
std::make_shared<fw::Supervisor>();
|
||||
std::shared_ptr<fw::TaskQueue> tasks = std::make_shared<fw::TaskQueue>();
|
||||
std::shared_ptr<fw::ReplicationLog> repl =
|
||||
std::make_shared<fw::ReplicationLog>();
|
||||
std::shared_ptr<fw::OpenApi> openapi = std::make_shared<fw::OpenApi>();
|
||||
std::shared_ptr<fw::Environment> templates =
|
||||
std::make_shared<fw::Environment>();
|
||||
std::map<std::string, std::string> embedded;
|
||||
std::string static_root = "example/static";
|
||||
std::string cookie_secret = "living-example-secret";
|
||||
};
|
||||
|
||||
std::shared_ptr<fw::Application> build_application(Services& services);
|
||||
|
||||
void register_index(fw::Router& router, Services& services);
|
||||
void register_routing_demo(fw::Router& router, Services& services);
|
||||
void register_chain_demo(std::shared_ptr<fw::Application> app,
|
||||
Services& services);
|
||||
void register_data_demo(fw::Router& router, Services& services);
|
||||
void register_security_demo(fw::Router& router, Services& services);
|
||||
void register_formats_demo(fw::Router& router, Services& services);
|
||||
void register_realtime_demo(fw::Router& router, Services& services);
|
||||
void register_ops_demo(fw::Router& router, Services& services);
|
||||
|
||||
void load_templates(Services& services);
|
||||
void load_embedded(Services& services);
|
||||
void seed_services(Services& services);
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Http.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/Validate.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
fw::OrmDb* pick_db(Services& services, const fw::Request& req) {
|
||||
if (fw::parse_backend(req.query_or("backend", "sqlite")) ==
|
||||
fw::OrmBackend::Postgres)
|
||||
return services.postgres.get();
|
||||
return services.sqlite.get();
|
||||
}
|
||||
|
||||
std::string rows_json(const std::vector<fw::OrmRow>& rows) {
|
||||
std::string out = "[";
|
||||
bool first = true;
|
||||
for (const auto& r : rows) {
|
||||
if (!first) out += ",";
|
||||
first = false;
|
||||
out += "{";
|
||||
bool fc = true;
|
||||
for (const auto& [k, v] : r.columns) {
|
||||
if (!fc) out += ",";
|
||||
fc = false;
|
||||
out += "\"" + fw::json_escape(k) + "\":\"" + fw::json_escape(v) + "\"";
|
||||
}
|
||||
out += "}";
|
||||
}
|
||||
return out + "]";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_data_demo(fw::Router& router, Services& services) {
|
||||
fw::OrmDb* sqlite = services.sqlite.get();
|
||||
fw::OrmDb* postgres = services.postgres.get();
|
||||
fw::KvStore* kv = services.kv.get();
|
||||
fw::Cache* cache = services.cache.get();
|
||||
fw::Pubsub* pubsub = services.pubsub.get();
|
||||
|
||||
router.get("/orm/users", "orm-users", [sqlite, postgres](fw::Request req) {
|
||||
fw::OrmDb* db = req.query_or("backend", "sqlite") == "postgres" ? postgres
|
||||
: sqlite;
|
||||
fw::OrmResult res = db->select_all("users");
|
||||
std::string body = "{\"backend\":\"" + fw::backend_name(db->backend()) +
|
||||
"\",\"healed\":" + (res.healed ? "true" : "false") +
|
||||
",\"note\":\"" + fw::json_escape(res.healed_note) +
|
||||
"\",\"rows\":" + rows_json(res.rows) + "}";
|
||||
return fw::JsonResponse(body, 200);
|
||||
});
|
||||
router.post("/orm/users", "orm-users-post",
|
||||
[sqlite, postgres](fw::Request req) {
|
||||
fw::OrmDb* db = req.query_or("backend", "sqlite") == "postgres"
|
||||
? postgres
|
||||
: sqlite;
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
fw::OrmResult res = db->insert("users", input);
|
||||
if (!res.ok)
|
||||
return fw::JsonResponse("{\"error\":\"store full\"}", 500);
|
||||
std::string body =
|
||||
"{\"backend\":\"" + fw::backend_name(db->backend()) +
|
||||
"\",\"healed\":" + (res.healed ? "true" : "false") +
|
||||
",\"note\":\"" + fw::json_escape(res.healed_note) +
|
||||
"\",\"rows\":" + rows_json(res.rows) + "}";
|
||||
return fw::JsonResponse(body, 201);
|
||||
});
|
||||
|
||||
router.get("/kv/items", "kv-get", [kv](fw::Request req) {
|
||||
std::string key = req.query_or("key", "");
|
||||
if (key.empty()) return fw::JsonResponse("{\"error\":\"key required\"}", 400);
|
||||
std::string value;
|
||||
if (!kv->get(key, value))
|
||||
return fw::JsonResponse("{\"error\":\"not found\"}", 404);
|
||||
return fw::JsonResponse("{\"key\":\"" + fw::json_escape(key) +
|
||||
"\",\"value\":\"" + fw::json_escape(value) +
|
||||
"\"}",
|
||||
200);
|
||||
});
|
||||
router.post("/kv/items", "kv-set", [kv](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
if (!input.count("key") || !input.count("value"))
|
||||
return fw::JsonResponse("{\"error\":\"key and value required\"}", 400);
|
||||
if (!kv->set(input["key"], input["value"]))
|
||||
return fw::JsonResponse("{\"error\":\"store full\"}", 500);
|
||||
return fw::JsonResponse("{\"stored\":\"" + fw::json_escape(input["key"]) +
|
||||
"\"}",
|
||||
201);
|
||||
});
|
||||
|
||||
router.get("/cache/lookup", "cache", [cache](fw::Request req) {
|
||||
std::string key = req.query_or("key", "demo");
|
||||
std::string value;
|
||||
bool hit = cache->lookup(key, value, 1000);
|
||||
if (!hit) {
|
||||
value = "computed-for-" + key;
|
||||
cache->put(key, value, 60, 1000);
|
||||
return fw::JsonResponse(
|
||||
"{\"key\":\"" + fw::json_escape(key) + "\",\"hit\":false,\"value\":\"" +
|
||||
fw::json_escape(value) + "\"}",
|
||||
200);
|
||||
}
|
||||
return fw::JsonResponse(
|
||||
"{\"key\":\"" + fw::json_escape(key) + "\",\"hit\":true,\"value\":\"" +
|
||||
fw::json_escape(value) + "\"}",
|
||||
200);
|
||||
});
|
||||
|
||||
router.post("/pubsub/publish", "pubsub", [pubsub](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string topic = input.count("topic") ? input["topic"] : "news";
|
||||
std::string message = input.count("message") ? input["message"] : "hello";
|
||||
std::size_t delivered = pubsub->publish(topic, message);
|
||||
return fw::JsonResponse("{\"topic\":\"" + fw::json_escape(topic) +
|
||||
"\",\"delivered\":" +
|
||||
std::to_string(delivered) + "}",
|
||||
200);
|
||||
});
|
||||
router.get("/pubsub/publish", "pubsub-history", [pubsub](fw::Request req) {
|
||||
std::string topic = req.query_or("topic", "news");
|
||||
auto hist = pubsub->history(topic);
|
||||
std::string body = "{\"topic\":\"" + fw::json_escape(topic) +
|
||||
"\",\"history\":[";
|
||||
bool first = true;
|
||||
for (const auto& m : hist) {
|
||||
if (!first) body += ",";
|
||||
first = false;
|
||||
body += "\"" + fw::json_escape(m) + "\"";
|
||||
}
|
||||
return fw::JsonResponse(body + "]}", 200);
|
||||
});
|
||||
(void)pick_db;
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "App.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
// Byte-identical reproducible bundle. Production bundles are generated by
|
||||
// the asset embedder CLI (09-cli-ops.md); this hand-written copy proves the
|
||||
// serving path with sorted names and no timestamps.
|
||||
|
||||
} // namespace
|
||||
|
||||
void load_embedded(Services& services) {
|
||||
services.embedded["/app.css"] =
|
||||
"/* embedded bundle */\nbody{font-family:sans-serif}\n";
|
||||
services.embedded["/hello.txt"] = "hello from embedded assets\n";
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,131 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Negotiate.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/StaticFiles.hpp"
|
||||
#include "../fw/Templates.hpp"
|
||||
#include "../fw/Validate.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
fw::Model contact_model() {
|
||||
return fw::Model(
|
||||
{{"name", fw::FieldType::String, true, 1, 80, 0, 0, false, ""},
|
||||
{"email", fw::FieldType::Email, true, 3, 120, 0, 0, false, ""},
|
||||
{"message", fw::FieldType::String, true, 1, 2000, 0, 0, false, ""}});
|
||||
}
|
||||
|
||||
fw::Model user_model() {
|
||||
fw::Field age{"age", fw::FieldType::Integer, true, 0, 0, 0, 150, true, ""};
|
||||
return fw::Model(
|
||||
{{"username", fw::FieldType::String, true, 3, 40, 0, 0, false, ""},
|
||||
{"email", fw::FieldType::Email, true, 3, 120, 0, 0, false, ""},
|
||||
age});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_formats_demo(fw::Router& router, Services& services) {
|
||||
fw::Environment* env = services.templates.get();
|
||||
std::map<std::string, std::string> embedded = services.embedded;
|
||||
std::string static_root = services.static_root;
|
||||
|
||||
router.get("/templates/page", "template-page", [env](fw::Request req) {
|
||||
(void)req;
|
||||
fw::TemplateCtx ctx;
|
||||
ctx["title"] = std::string("Templates");
|
||||
ctx["heading"] = std::string("Template demo");
|
||||
ctx["author"] = std::string("ada");
|
||||
ctx["snippet"] = std::string("<b>bold</b>");
|
||||
fw::TemplateList items;
|
||||
items.push_back({{"name", "extends"}, {"kind", "inheritance"}});
|
||||
items.push_back({{"name", "include"}, {"kind", "partial"}});
|
||||
items.push_back({{"name", "for"}, {"kind", "loop"}});
|
||||
ctx["items"] = items;
|
||||
return fw::Response(200, env->render("page.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
});
|
||||
|
||||
router.get("/formats/data", "formats", [](fw::Request req) {
|
||||
std::vector<std::string> offers = {"application/json", "application/xml",
|
||||
"text/html", "text/plain"};
|
||||
std::string pick =
|
||||
fw::negotiate(req.headers.get("Accept"), offers);
|
||||
if (pick == "application/json")
|
||||
return fw::Response(
|
||||
200, "{\"format\":\"json\",\"value\":42}", "application/json");
|
||||
if (pick == "application/xml")
|
||||
return fw::Response(200, "<data><value>42</value></data>",
|
||||
"application/xml");
|
||||
if (pick == "text/html")
|
||||
return fw::Response(200, "<p>value: 42</p>",
|
||||
"text/html; charset=utf-8");
|
||||
return fw::Response(200, "value: 42", "text/plain; charset=utf-8");
|
||||
});
|
||||
|
||||
router.get("/forms/contact", "contact-get", [env](fw::Request req) {
|
||||
(void)req;
|
||||
fw::TemplateCtx ctx;
|
||||
ctx["title"] = std::string("Contact");
|
||||
ctx["posted"] = std::string("");
|
||||
ctx["name"] = std::string("");
|
||||
ctx["email"] = std::string("");
|
||||
ctx["message"] = std::string("");
|
||||
return fw::Response(200, env->render("contact.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
});
|
||||
router.post("/forms/contact", "contact-post", [env](fw::Request req) {
|
||||
fw::Form form(contact_model());
|
||||
fw::TemplateCtx ctx;
|
||||
ctx["title"] = std::string("Contact");
|
||||
try {
|
||||
auto clean = form.bind(req);
|
||||
ctx["posted"] = std::string("true");
|
||||
ctx["name"] = clean["name"];
|
||||
ctx["email"] = clean["email"];
|
||||
ctx["message"] = clean["message"];
|
||||
} catch (const fw::ValidationError& e) {
|
||||
ctx["posted"] = std::string("");
|
||||
auto input = fw::parse_form(req.body);
|
||||
ctx["name"] = input.count("name") ? input["name"] : "";
|
||||
ctx["email"] = input.count("email") ? input["email"] : "";
|
||||
ctx["message"] = input.count("message") ? input["message"] : "";
|
||||
ctx["error"] = e.to_json();
|
||||
return fw::Response(422, env->render("contact.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
}
|
||||
return fw::Response(200, env->render("contact.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
});
|
||||
|
||||
router.post("/validate/user", "validate-user", [](fw::Request req) {
|
||||
fw::Model model = user_model();
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
try {
|
||||
auto clean = model.validate(input);
|
||||
return fw::JsonResponse(
|
||||
"{\"ok\":true,\"username\":\"" +
|
||||
fw::json_escape(clean["username"]) + "\"}",
|
||||
200);
|
||||
} catch (const fw::ValidationError& e) {
|
||||
return fw::JsonResponse(e.to_json(), 422);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/static/{name}", "static", [static_root](fw::Request req) {
|
||||
return fw::serve_disk_file(static_root, req.param_or("name", ""));
|
||||
});
|
||||
router.get("/embedded/{name}", "embedded", [embedded](fw::Request req) {
|
||||
return fw::serve_embedded(embedded, req.param_or("name", ""));
|
||||
});
|
||||
|
||||
router.get("/redirect/here", "redirect", [](fw::Request req) {
|
||||
bool permanent = req.query_or("mode", "temp") == "perm";
|
||||
return fw::Response(fw::RedirectResponse("/health", permanent));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Http.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/Templates.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
const char* kFeatures[][2] = {
|
||||
{"routing", "/routing/hello/world"},
|
||||
{"views", "/views/greet"},
|
||||
{"routers", "/routers/nested/deep"},
|
||||
{"chaining", "/app1/app1/app1/whoami"},
|
||||
{"orm-sqlite", "/orm/users?backend=sqlite"},
|
||||
{"orm-postgres", "/orm/users?backend=postgres"},
|
||||
{"auth", "/auth/me"},
|
||||
{"sessions", "/sessions/counter"},
|
||||
{"forms", "/forms/contact"},
|
||||
{"validation", "/validate/user"},
|
||||
{"templates", "/templates/page"},
|
||||
{"formats", "/formats/data"},
|
||||
{"static", "/static/app.css"},
|
||||
{"embedded", "/embedded/hello.txt"},
|
||||
{"streaming", "/streaming/feed"},
|
||||
{"passthrough", "/streaming/passthrough"},
|
||||
{"websocket", "/ws/chat"},
|
||||
{"socketio", "/socketio/rooms"},
|
||||
{"sse", "/sse/events"},
|
||||
{"pubsub", "/pubsub/publish"},
|
||||
{"events", "/events/fire"},
|
||||
{"metrics", "/metrics/counters"},
|
||||
{"telemetry", "/telemetry/trace"},
|
||||
{"kv", "/kv/items"},
|
||||
{"cache", "/cache/lookup"},
|
||||
{"uploads", "/uploads/file"},
|
||||
{"webdav", "/webdav/files/"},
|
||||
{"replication", "/replication/state"},
|
||||
{"tasks", "/supervisor/tasks"},
|
||||
{"workers", "/supervisor/workers"},
|
||||
{"restclient", "/restclient/self"},
|
||||
{"openapi", "/openapi.json"},
|
||||
{"docs", "/docs"},
|
||||
{"ai", "/cli/ai"},
|
||||
{"workflow", "/workflow/release"},
|
||||
{"redirect", "/redirect/here"},
|
||||
{"health", "/health"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_index(fw::Router& router, Services& services) {
|
||||
fw::Environment* env = services.templates.get();
|
||||
router.get("/", "index", [env](fw::Request req) {
|
||||
fw::TemplateCtx ctx;
|
||||
ctx["title"] = std::string("MoloApp living example");
|
||||
ctx["show_matrix"] = std::string("true");
|
||||
fw::TemplateList rows;
|
||||
for (const auto& [feature, route] : kFeatures) {
|
||||
rows.push_back({{"feature", feature}, {"route", route}});
|
||||
}
|
||||
ctx["matrix"] = rows;
|
||||
(void)req;
|
||||
return fw::Response(200, env->render("index.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
});
|
||||
router.get("/health", "health", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::JsonResponse(
|
||||
"{\"status\":\"ok\",\"tls\":\"stub-no-wolfssl\"}", 200);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,224 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Jobs.hpp"
|
||||
#include "../fw/Net.hpp"
|
||||
#include "../fw/Observ.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/Templates.hpp"
|
||||
#include "../fw/Validate.hpp"
|
||||
#include "../fw/Workflow.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
void register_ops_demo(fw::Router& router, Services& services) {
|
||||
fw::EventBus* events = services.events.get();
|
||||
fw::Metrics* metrics = services.metrics.get();
|
||||
fw::Telemetry* telemetry = services.telemetry.get();
|
||||
fw::Supervisor* supervisor = services.supervisor.get();
|
||||
fw::TaskQueue* tasks = services.tasks.get();
|
||||
fw::ReplicationLog* repl = services.repl.get();
|
||||
fw::OpenApi* openapi = services.openapi.get();
|
||||
fw::Environment* env = services.templates.get();
|
||||
|
||||
router.post("/events/fire", "events-fire", [events](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string type = input.count("type") ? input["type"] : "demo.ping";
|
||||
std::string payload =
|
||||
input.count("payload") ? input["payload"] : "ping";
|
||||
events->emit({type, payload, req.correlation_id});
|
||||
return fw::JsonResponse(
|
||||
"{\"fired\":\"" + fw::json_escape(type) + "\"}", 200);
|
||||
});
|
||||
router.get("/events/fire", "events-list", [events](fw::Request req) {
|
||||
(void)req;
|
||||
auto delivered = events->delivered();
|
||||
std::string body = "{\"delivered\":[";
|
||||
bool first = true;
|
||||
for (const auto& e : delivered) {
|
||||
if (!first) body += ",";
|
||||
first = false;
|
||||
body += "{\"type\":\"" + fw::json_escape(e.type) + "\",\"payload\":\"" +
|
||||
fw::json_escape(e.payload) + "\"}";
|
||||
}
|
||||
return fw::JsonResponse(body + "]}", 200);
|
||||
});
|
||||
|
||||
router.get("/metrics/counters", "metrics",
|
||||
[metrics](fw::Request req) {
|
||||
(void)req;
|
||||
metrics->inc("example.page.views");
|
||||
fw::Response res(200, metrics->render_prometheus(),
|
||||
"text/plain; version=0.0.4");
|
||||
return res;
|
||||
});
|
||||
|
||||
router.get("/telemetry/trace", "telemetry",
|
||||
[telemetry](fw::Request req) {
|
||||
fw::Span span =
|
||||
telemetry->start_span("demo", req.correlation_id);
|
||||
telemetry->finish_span(span);
|
||||
return fw::JsonResponse(
|
||||
"{\"trace\":\"" + fw::json_escape(span.trace_id) +
|
||||
"\",\"span\":\"" + fw::json_escape(span.span_id) +
|
||||
"\",\"correlation\":\"" +
|
||||
fw::json_escape(req.correlation_id) + "\"}",
|
||||
200);
|
||||
});
|
||||
|
||||
router.get("/replication/state", "repl", [repl](fw::Request req) {
|
||||
unsigned long since = 0;
|
||||
try {
|
||||
since = (unsigned long)std::stoul(req.query_or("since", "0"));
|
||||
} catch (...) {
|
||||
since = 0;
|
||||
}
|
||||
auto entries = repl->since(since);
|
||||
std::string body = "{\"latest\":" + std::to_string(repl->latest()) +
|
||||
",\"entries\":[";
|
||||
bool first = true;
|
||||
for (const auto& e : entries) {
|
||||
if (!first) body += ",";
|
||||
first = false;
|
||||
body += "{\"seq\":" + std::to_string(e.seq) + ",\"object\":\"" +
|
||||
fw::json_escape(e.object) + "\",\"resolution\":\"" +
|
||||
fw::json_escape(e.resolution) + "\"}";
|
||||
}
|
||||
return fw::JsonResponse(body + "]}", 200);
|
||||
});
|
||||
router.post("/replication/state", "repl-append", [repl](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string object = input.count("object") ? input["object"] : "doc-1";
|
||||
std::string data = input.count("data") ? input["data"] : "{}";
|
||||
fw::ReplEntry e = repl->append(object, data);
|
||||
return fw::JsonResponse("{\"seq\":" + std::to_string(e.seq) +
|
||||
",\"resolution\":\"" +
|
||||
fw::json_escape(e.resolution) + "\"}",
|
||||
201);
|
||||
});
|
||||
|
||||
router.get("/supervisor/tasks", "tasks-list", [tasks](fw::Request req) {
|
||||
(void)req;
|
||||
std::string body = "{\"tasks\":[";
|
||||
bool first = true;
|
||||
for (const auto& t : tasks->list()) {
|
||||
if (!first) body += ",";
|
||||
first = false;
|
||||
body += "{\"id\":\"" + fw::json_escape(t.id) + "\",\"state\":\"" +
|
||||
fw::json_escape(t.state) + "\"}";
|
||||
}
|
||||
return fw::JsonResponse(body + "]}", 200);
|
||||
});
|
||||
router.post("/supervisor/tasks", "tasks-enqueue", [tasks](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string kind = input.count("kind") ? input["kind"] : "email";
|
||||
std::string payload =
|
||||
input.count("payload") ? input["payload"] : "{}";
|
||||
std::string id = tasks->enqueue(kind, payload, 0);
|
||||
if (id.empty()) return fw::JsonResponse("{\"error\":\"queue full\"}", 500);
|
||||
return fw::JsonResponse("{\"id\":\"" + fw::json_escape(id) + "\"}", 201);
|
||||
});
|
||||
router.get("/supervisor/workers", "workers",
|
||||
[supervisor](fw::Request req) {
|
||||
(void)req;
|
||||
std::string body = "{\"workers\":[";
|
||||
bool first = true;
|
||||
for (const auto& w : supervisor->status()) {
|
||||
if (!first) body += ",";
|
||||
first = false;
|
||||
body += "{\"name\":\"" + fw::json_escape(w.name) +
|
||||
"\",\"running\":" +
|
||||
(w.running ? "true" : "false") + ",\"restarts\":" +
|
||||
std::to_string(w.restarts) + "}";
|
||||
}
|
||||
return fw::JsonResponse(body + "]}", 200);
|
||||
});
|
||||
|
||||
router.get("/restclient/self", "restclient", [](fw::Request req) {
|
||||
(void)req;
|
||||
fw::RestClient client([](fw::Request r) {
|
||||
(void)r;
|
||||
return fw::JsonResponse("{\"status\":\"ok\"}", 200);
|
||||
});
|
||||
fw::Response upstream = client.get("/health");
|
||||
return fw::JsonResponse(
|
||||
"{\"self_call\":" + std::to_string(upstream.status) + "}", 200);
|
||||
});
|
||||
|
||||
openapi->add("/health", "get", "health probe");
|
||||
openapi->add("/orm/users", "get", "list users");
|
||||
openapi->add("/validate/user", "post", "validate user model");
|
||||
router.get("/openapi.json", "openapi", [openapi](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::Response(200, openapi->to_json(), "application/json");
|
||||
});
|
||||
router.get("/docs", "docs", [env, openapi](fw::Request req) {
|
||||
(void)req;
|
||||
(void)openapi;
|
||||
fw::TemplateCtx ctx;
|
||||
ctx["title"] = std::string("API docs");
|
||||
fw::TemplateList rows;
|
||||
rows.push_back({{"method", "GET"},
|
||||
{"path", "/health"},
|
||||
{"summary", "health probe"}});
|
||||
rows.push_back({{"method", "GET"},
|
||||
{"path", "/orm/users"},
|
||||
{"summary", "list users"}});
|
||||
rows.push_back({{"method", "POST"},
|
||||
{"path", "/validate/user"},
|
||||
{"summary", "validate user model"}});
|
||||
ctx["routes"] = rows;
|
||||
return fw::Response(200, env->render("docs.html", ctx),
|
||||
"text/html; charset=utf-8");
|
||||
});
|
||||
|
||||
router.get("/cli/ai", "ai", [](fw::Request req) {
|
||||
std::string prompt = req.query_or("prompt", "add a route");
|
||||
fw::AiSample sample = fw::ai_harness_sample(prompt);
|
||||
std::string body = "{\"base_url\":\"" + sample.base_url +
|
||||
"\",\"model\":\"" + sample.model +
|
||||
"\",\"prompt\":\"" +
|
||||
fw::json_escape(sample.redacted_prompt) +
|
||||
"\",\"stream_events\":" +
|
||||
std::to_string(sample.stream_chunks.size()) + "}";
|
||||
return fw::JsonResponse(body, 200);
|
||||
});
|
||||
|
||||
router.post("/workflow/release", "workflow", [](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string subjects = input.count("commits") ? input["commits"] : "";
|
||||
std::vector<fw::CommitInfo> commits;
|
||||
std::size_t start = 0;
|
||||
while (start < subjects.size()) {
|
||||
std::size_t bar = subjects.find('|', start);
|
||||
std::string one =
|
||||
subjects.substr(start, bar == std::string::npos ? bar : bar - start);
|
||||
fw::CommitInfo info;
|
||||
std::string error;
|
||||
if (!fw::parse_commit(one, "", info, error))
|
||||
return fw::JsonResponse(
|
||||
"{\"error\":\"bad commit: " + fw::json_escape(error) + "\"}",
|
||||
400);
|
||||
commits.push_back(info);
|
||||
if (bar == std::string::npos) break;
|
||||
start = bar + 1;
|
||||
}
|
||||
fw::ReleasePlanner planner;
|
||||
fw::ChangelogWriter writer;
|
||||
fw::ReleaseVerifier verifier;
|
||||
verifier.add_check("suite", true, "example suite green");
|
||||
verifier.add_check("live", true, "health probe ok");
|
||||
std::string version = planner.next_version("0.1.0", commits);
|
||||
std::string body =
|
||||
"{\"bump\":\"" + fw::bump_name(planner.decide(commits)) +
|
||||
"\",\"version\":\"" + version + "\",\"green\":" +
|
||||
(verifier.green() ? "true" : "false") + ",\"changelog\":\"" +
|
||||
fw::json_escape(writer.entry(version, "2026-10-23", commits)) + "\"}";
|
||||
return fw::JsonResponse(body, 200);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Data.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/Validate.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
// Realtime demos answer over plain HTTP in the skeleton: the WebSocket and
|
||||
// Socket.IO endpoints describe the upgrade contract and echo posted frames,
|
||||
// SSE streams real event text. Gap: real framing, namespaces, rooms, and
|
||||
// long-polling fallback in 06-realtime.md plus matching clients.
|
||||
|
||||
void register_realtime_demo(fw::Router& router, Services& services) {
|
||||
fw::WebdavStore* webdav = services.webdav.get();
|
||||
|
||||
router.get("/streaming/feed", "stream", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::Response(
|
||||
fw::StreamingResponse({"chunk-1\n", "chunk-2\n", "chunk-3\n"}));
|
||||
});
|
||||
router.get("/streaming/passthrough", "passthrough", [](fw::Request req) {
|
||||
std::string target = req.query_or("target", "/health");
|
||||
return fw::Response(fw::StreamingResponse(
|
||||
{"passthrough from " + target + "\n", "{\"status\":\"ok\"}\n"}));
|
||||
});
|
||||
|
||||
router.get("/ws/chat", "ws", [](fw::Request req) {
|
||||
std::string upgrade = req.headers.get("Upgrade");
|
||||
if (upgrade == "websocket")
|
||||
return fw::JsonResponse(
|
||||
"{\"upgraded\":false,\"note\":\"skeleton: echo over POST\"}", 200);
|
||||
return fw::JsonResponse(
|
||||
"{\"protocol\":\"websocket\",\"path\":\"/ws/chat\",\"upgrade\":"
|
||||
"\"send Upgrade: websocket\"}",
|
||||
200);
|
||||
});
|
||||
router.post("/ws/chat", "ws-echo", [](fw::Request req) {
|
||||
return fw::JsonResponse(
|
||||
"{\"echo\":\"" + fw::json_escape(req.body) + "\"}", 200);
|
||||
});
|
||||
|
||||
router.get("/socketio/rooms", "socketio", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::JsonResponse(
|
||||
"{\"protocol\":\"socket.io\",\"namespaces\":[\"/\"],\"rooms\":["
|
||||
"\"lobby\"],\"fallback\":\"long-polling\"}",
|
||||
200);
|
||||
});
|
||||
router.post("/socketio/rooms", "socketio-emit", [](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
std::string room = input.count("room") ? input["room"] : "lobby";
|
||||
std::string event = input.count("event") ? input["event"] : "message";
|
||||
return fw::JsonResponse("{\"emitted\":\"" + fw::json_escape(event) +
|
||||
"\",\"room\":\"" + fw::json_escape(room) +
|
||||
"\",\"ack\":true}",
|
||||
200);
|
||||
});
|
||||
|
||||
router.get("/sse/events", "sse", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::Response(
|
||||
fw::SseResponse({"hello", "tick", "goodbye"}));
|
||||
});
|
||||
|
||||
router.any({fw::Method::Propfind}, "/webdav/files/", "webdav-propfind",
|
||||
[webdav](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::Response(207, webdav->propfind_xml("/files/"),
|
||||
"application/xml");
|
||||
});
|
||||
router.any({fw::Method::Get}, "/webdav/files/{name}", "webdav-get",
|
||||
[webdav](fw::Request req) -> fw::Response {
|
||||
std::string content;
|
||||
if (!webdav->get("/files/" + req.param_or("name", ""), content))
|
||||
return fw::JsonResponse("{\"error\":\"not found\"}", 404);
|
||||
return fw::TextResponse(content, 200);
|
||||
});
|
||||
router.any({fw::Method::Put}, "/webdav/files/{name}", "webdav-put",
|
||||
[webdav](fw::Request req) {
|
||||
webdav->put("/files/" + req.param_or("name", ""), req.body);
|
||||
return fw::JsonResponse("{\"stored\":true}", 201);
|
||||
});
|
||||
router.any({fw::Method::Delete}, "/webdav/files/{name}", "webdav-delete",
|
||||
[webdav](fw::Request req) {
|
||||
if (!webdav->remove("/files/" + req.param_or("name", "")))
|
||||
return fw::JsonResponse("{\"error\":\"not found\"}", 404);
|
||||
return fw::JsonResponse("{\"deleted\":true}", 200);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/View.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
class GreetView : public fw::View {
|
||||
public:
|
||||
using fw::View::View;
|
||||
|
||||
fw::Response get() override {
|
||||
return fw::JsonResponse(
|
||||
"{\"view\":\"GreetView\",\"method\":\"GET\",\"name\":\"" +
|
||||
fw::json_escape(req_.query_or("name", "stranger")) + "\"}",
|
||||
200);
|
||||
}
|
||||
|
||||
fw::Response post() override {
|
||||
auto form = fw::parse_form(req_.body);
|
||||
auto it = form.find("name");
|
||||
std::string name = it == form.end() ? "stranger" : it->second;
|
||||
return fw::JsonResponse(
|
||||
"{\"view\":\"GreetView\",\"method\":\"POST\",\"name\":\"" +
|
||||
fw::json_escape(name) + "\"}",
|
||||
200);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void register_routing_demo(fw::Router& router, Services& services) {
|
||||
(void)services;
|
||||
router.get("/hello/{name}", "hello", [](fw::Request req) {
|
||||
return fw::JsonResponse(
|
||||
"{\"hello\":\"" + fw::json_escape(req.param_or("name", "")) + "\"}",
|
||||
200);
|
||||
});
|
||||
|
||||
fw::Router views;
|
||||
views.get("/greet", "greet", [](fw::Request req) {
|
||||
GreetView view(std::move(req));
|
||||
return view.dispatch();
|
||||
});
|
||||
views.post("/greet", "greet-post", [](fw::Request req) {
|
||||
GreetView view(std::move(req));
|
||||
return view.dispatch();
|
||||
});
|
||||
router.include_router("/views", views, "views.");
|
||||
|
||||
fw::Router nested;
|
||||
nested.get("/deep", "deep", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::JsonResponse("{\"nested\":true}", 200);
|
||||
});
|
||||
fw::Router middle;
|
||||
middle.include_router("/nested", nested, "nested.");
|
||||
router.include_router("/routers", middle, "routers.");
|
||||
}
|
||||
|
||||
void register_chain_demo(std::shared_ptr<fw::Application> app,
|
||||
Services& services) {
|
||||
(void)services;
|
||||
auto leaf = std::make_shared<fw::Application>();
|
||||
fw::Router r;
|
||||
r.get("/whoami", "whoami", [](fw::Request req) {
|
||||
(void)req;
|
||||
return fw::JsonResponse("{\"depth\":3,\"path\":\"/app1/app1/app1\"}", 200);
|
||||
});
|
||||
leaf->include_router("", r);
|
||||
auto mid = std::make_shared<fw::Application>();
|
||||
mid->mount("/app1", leaf);
|
||||
auto top = std::make_shared<fw::Application>();
|
||||
top->mount("/app1", mid);
|
||||
app->mount("/app1", top);
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Cookies.hpp"
|
||||
#include "../fw/Data.hpp"
|
||||
#include "../fw/Router.hpp"
|
||||
#include "../fw/Validate.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
void register_security_demo(fw::Router& router, Services& services) {
|
||||
fw::Auth* auth = services.auth.get();
|
||||
fw::SessionStore* sessions = services.sessions.get();
|
||||
fw::UploadStore* uploads = services.uploads.get();
|
||||
std::string secret = services.cookie_secret;
|
||||
|
||||
router.get("/auth/me", "auth-me", [auth](fw::Request req) {
|
||||
std::string user, password;
|
||||
if (!fw::parse_basic_auth(req.headers.get("Authorization"), user,
|
||||
password))
|
||||
return fw::JsonResponse("{\"error\":\"basic auth required\"}", 401);
|
||||
if (!auth->check(user, password))
|
||||
return fw::JsonResponse("{\"error\":\"bad credentials\"}", 403);
|
||||
return fw::JsonResponse(
|
||||
"{\"user\":\"" + fw::json_escape(user) + "\"}", 200);
|
||||
});
|
||||
router.post("/auth/login", "auth-login", [auth](fw::Request req) {
|
||||
auto input = fw::parse_json_object_flat(req.body);
|
||||
if (input.empty()) input = fw::parse_form(req.body);
|
||||
if (!input.count("username") || !input.count("password"))
|
||||
return fw::JsonResponse("{\"error\":\"username and password required\"}",
|
||||
400);
|
||||
if (!auth->check(input["username"], input["password"]))
|
||||
return fw::JsonResponse("{\"error\":\"bad credentials\"}", 403);
|
||||
return fw::JsonResponse(
|
||||
"{\"user\":\"" + fw::json_escape(input["username"]) + "\"}", 200);
|
||||
});
|
||||
|
||||
router.get("/sessions/counter", "session-counter",
|
||||
[sessions, secret](fw::Request req) {
|
||||
std::string raw = req.cookie_or("session", "");
|
||||
std::string id;
|
||||
std::map<std::string, std::string> data;
|
||||
bool fresh = false;
|
||||
if (raw.empty() || !fw::cookie_verify(raw, secret, id) ||
|
||||
!sessions->get(id, data)) {
|
||||
id = sessions->create();
|
||||
fresh = true;
|
||||
data.clear();
|
||||
}
|
||||
long count = 0;
|
||||
if (data.count("count")) {
|
||||
try {
|
||||
count = std::stol(data["count"]);
|
||||
} catch (...) {
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
++count;
|
||||
sessions->put(id, "count", std::to_string(count));
|
||||
fw::Response res(200,
|
||||
"{\"count\":" + std::to_string(count) +
|
||||
",\"fresh\":" +
|
||||
(fresh ? "true" : "false") + "}",
|
||||
"application/json");
|
||||
res.headers.set(
|
||||
"Set-Cookie",
|
||||
fw::make_set_cookie("session", fw::cookie_sign(id, secret),
|
||||
"/", true, false, "Lax", 3600));
|
||||
return res;
|
||||
});
|
||||
|
||||
router.post("/uploads/file", "upload", [uploads](fw::Request req) {
|
||||
std::string filename = req.query_or("filename", "upload.txt");
|
||||
std::string content_type =
|
||||
req.headers.get("Content-Type");
|
||||
if (content_type.empty()) content_type = "text/plain";
|
||||
fw::UploadInfo info;
|
||||
std::string error;
|
||||
if (!uploads->store(filename, content_type, req.body, info, error)) {
|
||||
if (error == "file too large")
|
||||
return fw::JsonResponse("{\"error\":\"" + error + "\"}", 413);
|
||||
if (error == "content type not allowed")
|
||||
return fw::JsonResponse("{\"error\":\"" + error + "\"}", 415);
|
||||
return fw::JsonResponse("{\"error\":\"" + error + "\"}", 400);
|
||||
}
|
||||
return fw::JsonResponse("{\"file\":\"" + fw::json_escape(info.filename) +
|
||||
"\",\"size\":" + std::to_string(info.size) +
|
||||
"}",
|
||||
201);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,14 @@
|
||||
#include "App.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
void seed_services(Services& services) {
|
||||
services.auth->add_user("ada", "correct-horse");
|
||||
services.auth->add_user("grace", "compiler-rocks");
|
||||
services.supervisor->add_worker("mailer");
|
||||
services.supervisor->add_worker("indexer");
|
||||
services.supervisor->start("mailer");
|
||||
services.webdav->put("/files/welcome.txt", "hello from webdav\n");
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "App.hpp"
|
||||
|
||||
#include "../fw/Templates.hpp"
|
||||
|
||||
namespace molo::example::app {
|
||||
|
||||
namespace {
|
||||
|
||||
const char kBase[] = R"BASE(<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title }}</title>
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">{{ site }}</a>
|
||||
<nav class="nav-wide">
|
||||
<a href="/">Index</a>
|
||||
<a href="/templates/page">Templates</a>
|
||||
<a href="/forms/contact">Forms</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/health">Health</a>
|
||||
</nav>
|
||||
<details class="nav-narrow">
|
||||
<summary>Menu</summary>
|
||||
<nav>
|
||||
<a href="/">Index</a>
|
||||
<a href="/templates/page">Templates</a>
|
||||
<a href="/forms/contact">Forms</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<a href="/health">Health</a>
|
||||
</nav>
|
||||
</details>
|
||||
</header>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer><p>{{ site }}: living example, {{ backend }}</p></footer>
|
||||
</body>
|
||||
</html>
|
||||
)BASE";
|
||||
|
||||
const char kIndex[] = R"INDEX({% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Living example</h1>
|
||||
<p>Every framework feature, one click away. Mobile-first: narrow the window
|
||||
and the top nav collapses into a dropdown menu.</p>
|
||||
{% if show_matrix %}
|
||||
<table>
|
||||
<thead><tr><th>Feature</th><th>Route</th></tr></thead>
|
||||
<tbody>
|
||||
{% for row in matrix %}
|
||||
<tr><td>{{ row.feature }}</td><td><a href="{{ row.route }}">{{ row.route }}</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>Matrix hidden.</p>
|
||||
{% endif %}
|
||||
{% include "nav_hint.html" %}
|
||||
{% endblock %}
|
||||
)INDEX";
|
||||
|
||||
const char kPage[] = R"PAGE({% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>{{ heading }}</h1>
|
||||
<p>Author: {{ author|upper }}</p>
|
||||
<p>Raw (escaped): {{ snippet }}</p>
|
||||
<p>Trusted (safe): {{ snippet|safe }}</p>
|
||||
<ul>
|
||||
{% for item in items %}
|
||||
<li>{{ item.name }} ({{ item.kind }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
)PAGE";
|
||||
|
||||
const char kContact[] = R"CONTACT({% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Contact</h1>
|
||||
{% if posted %}
|
||||
<p class="ok">Thanks {{ name }}, message received.</p>
|
||||
{% else %}
|
||||
<form method="post" action="/forms/contact">
|
||||
<label>Name <input name="name" value="{{ name }}"></label>
|
||||
<label>Email <input name="email" value="{{ email }}"></label>
|
||||
<label>Message <textarea name="message">{{ message }}</textarea></label>
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
)CONTACT";
|
||||
|
||||
const char kDocs[] = R"DOCS({% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>API docs</h1>
|
||||
<p>OpenAPI JSON: <a href="/openapi.json">/openapi.json</a></p>
|
||||
<ul>
|
||||
{% for row in routes %}
|
||||
<li><code>{{ row.method }} {{ row.path }}</code> : {{ row.summary }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
)DOCS";
|
||||
|
||||
const char kError[] = R"ERROR({% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Error {{ status }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
{% endblock %}
|
||||
)ERROR";
|
||||
|
||||
const char kNavHint[] =
|
||||
"<p class=\"hint\">Tip: this page works on a 360px phone.</p>\n";
|
||||
|
||||
} // namespace
|
||||
|
||||
void load_templates(Services& services) {
|
||||
services.templates->add_template("base.html", kBase);
|
||||
services.templates->add_template("index.html", kIndex);
|
||||
services.templates->add_template("page.html", kPage);
|
||||
services.templates->add_template("contact.html", kContact);
|
||||
services.templates->add_template("docs.html", kDocs);
|
||||
services.templates->add_template("error.html", kError);
|
||||
services.templates->add_template("nav_hint.html", kNavHint);
|
||||
services.templates->add_global("site", "MoloApp living example");
|
||||
services.templates->add_global("backend", "skeleton");
|
||||
}
|
||||
|
||||
} // namespace molo::example::app
|
||||
Reference in New Issue
Block a user