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:
2026-10-23 18:20:04 +02:00
parent 54068f1953
commit 3c4334cec9
1068 changed files with 100899 additions and 2 deletions
+123
View File
@@ -0,0 +1,123 @@
#include "App.hpp"
#include <stdexcept>
#include "Caps.hpp"
namespace molo::example::fw {
HttpError::HttpError(int status, std::string code, std::string message)
: status_(status), code_(std::move(code)), message_(std::move(message)) {}
int HttpError::status() const { return status_; }
const std::string& HttpError::code() const { return code_; }
const std::string& HttpError::message() const { return message_; }
void Application::use(MiddlewareSlot slot, Middleware middleware) {
if (has_middleware_ && slot < last_slot_)
throw std::runtime_error("middleware category order violated");
last_slot_ = slot;
has_middleware_ = true;
middlewares_.emplace_back(slot, std::move(middleware));
}
void Application::include_router(const std::string& prefix,
const Router& router,
const std::string& name_prefix) {
router_.include_router(prefix, router, name_prefix);
}
void Application::mount(const std::string& path,
std::shared_ptr<Application> sub) {
mounts_.emplace_back(path, std::move(sub));
}
Response Application::dispatch(Request req) {
if (req.body.size() > caps::kMaxRequestBody)
return TextResponse("body too large", 413);
try {
return run_chain(std::move(req), 0);
} catch (const HttpError& e) {
std::string body = "{\"error\":\"" + json_escape(e.code()) +
"\",\"message\":\"" + json_escape(e.message()) + "\"}";
return JsonResponse(body, e.status());
} catch (const std::exception& e) {
return JsonResponse("{\"error\":\"internal\",\"message\":\"" +
json_escape(e.what()) + "\"}",
500);
}
}
Response Application::run_chain(Request req, int depth) {
Next next = [this, depth](Request r) { return dispatch_inner(r, depth); };
for (std::size_t i = middlewares_.size(); i > 0; --i) {
Middleware mw = middlewares_[i - 1].second;
Next inner = next;
next = [mw, inner](Request r) { return mw(std::move(r), inner); };
}
return next(std::move(req));
}
Response Application::dispatch_inner(Request req, int depth) {
if (depth > caps::kMaxMountDepth)
throw HttpError(500, "mount_depth", "sub-application nesting too deep");
std::string best;
std::shared_ptr<Application> target;
for (auto& [prefix, sub] : mounts_) {
if (req.path.size() >= prefix.size() &&
req.path.compare(0, prefix.size(), prefix) == 0 &&
prefix.size() > best.size()) {
best = prefix;
target = sub;
}
}
if (target) {
req.path = req.path.substr(best.size());
if (req.path.empty() || req.path.front() != '/') req.path = "/" + req.path;
return target->run_chain(std::move(req), depth + 1);
}
Handler handler;
if (router_.match(req, handler)) return handler(std::move(req));
bool path_exists = false;
for (const auto& r : router_.routes()) {
std::map<std::string, std::string> params;
if (match_pattern(r.pattern, req.path, params)) {
path_exists = true;
break;
}
}
if (path_exists)
throw HttpError(405, "method_not_allowed", "method not allowed");
throw HttpError(404, "not_found", "no route for " + req.path);
}
const std::vector<Route>& Application::routes() const {
return router_.routes();
}
Middleware errors_middleware() {
return [](Request req, Next next) -> Response {
try {
return next(std::move(req));
} catch (const HttpError& e) {
std::string body = "{\"error\":\"" + json_escape(e.code()) +
"\",\"message\":\"" + json_escape(e.message()) +
"\"}";
return JsonResponse(body, e.status());
}
};
}
Middleware request_id_middleware() {
return [](Request req, Next next) -> Response {
if (req.correlation_id.empty()) {
static unsigned long counter = 1;
req.correlation_id = "req-" + std::to_string(counter++);
}
Response res = next(req);
res.headers.set("X-Request-ID", req.correlation_id);
return res;
};
}
} // namespace molo::example::fw
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Application: composition root. Owns routers, middleware chain, sub-app
// mounts, and the typed error mapping. Gap: real molo::app::Application in
// 05-request-routing.md owns lifespan hooks, plugin registry, and the full DI
// container; this skeleton passes a Services struct to route factories.
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "Http.hpp"
#include "Router.hpp"
namespace molo::example::fw {
enum class MiddlewareSlot {
Errors = 0,
Observability = 1,
Security = 2,
SessionAuth = 3,
User = 4,
};
using Next = std::function<Response(Request)>;
using Middleware = std::function<Response(Request, Next)>;
class HttpError {
public:
HttpError(int status, std::string code, std::string message);
int status() const;
const std::string& code() const;
const std::string& message() const;
private:
int status_;
std::string code_;
std::string message_;
};
class Application : public std::enable_shared_from_this<Application> {
public:
void use(MiddlewareSlot slot, Middleware middleware);
void include_router(const std::string& prefix, const Router& router,
const std::string& name_prefix = "");
void mount(const std::string& path, std::shared_ptr<Application> sub);
Response dispatch(Request req);
const std::vector<Route>& routes() const;
private:
Response dispatch_inner(Request req, int depth);
Response run_chain(Request req, int depth);
Router router_;
std::vector<std::pair<MiddlewareSlot, Middleware>> middlewares_;
std::vector<std::pair<std::string, std::shared_ptr<Application>>> mounts_;
MiddlewareSlot last_slot_ = MiddlewareSlot::Errors;
bool has_middleware_ = false;
};
Middleware errors_middleware();
Middleware request_id_middleware();
} // namespace molo::example::fw
+48
View File
@@ -0,0 +1,48 @@
#pragma once
// Static ceilings for the living example. Every knob has a cap; no autotuning.
// Gap: real framework caps live in 01-runtime.md through 09-cli-ops.md specs;
// these example caps mirror them until the framework lands.
#include <cstddef>
namespace molo::example::fw::caps {
inline constexpr std::size_t kMaxRequestBody = 1 << 20; // 1 MiB
inline constexpr std::size_t kMaxHeaderCount = 100;
inline constexpr std::size_t kMaxHeaderBytes = 65536;
inline constexpr std::size_t kMaxFormFields = 200;
inline constexpr int kMaxJsonDepth = 32;
inline constexpr std::size_t kMaxUploadBytes = 8 << 20; // 8 MiB
inline constexpr std::size_t kMaxTemplateBytes = 256 << 10;
inline constexpr int kMaxTemplateDepth = 16;
inline constexpr std::size_t kMaxRouteCount = 4096;
inline constexpr int kMaxMountDepth = 8;
inline constexpr std::size_t kMaxKvEntries = 100000;
inline constexpr std::size_t kMaxCacheEntries = 10000;
inline constexpr std::size_t kMaxPubsubTopics = 1000;
inline constexpr std::size_t kMaxPubsubHistory = 100;
inline constexpr std::size_t kMaxMetrics = 1000;
inline constexpr std::size_t kMaxSpans = 10000;
inline constexpr std::size_t kMaxSessions = 100000;
inline constexpr std::size_t kMaxOrmRows = 100000;
inline constexpr std::size_t kMaxOrmTables = 500;
inline constexpr std::size_t kMaxTasks = 10000;
inline constexpr std::size_t kMaxWorkers = 64;
inline constexpr std::size_t kMaxReplEntries = 10000;
inline constexpr std::size_t kMaxStreamChunks = 1000;
inline constexpr std::size_t kMaxChunkBytes = 65536;
inline constexpr int kServerBacklog = 64;
inline constexpr int kServerTimeoutSec = 30;
inline constexpr int kMaxConnections = 512;
inline constexpr std::size_t kMaxTestBody = 4 << 20;
inline constexpr int kSuiteTimeoutSec = 600;
inline constexpr int kSingleTestTimeoutSec = 30;
inline constexpr int kMaxReleasePerRun = 1;
inline constexpr int kMaxChangelogLines = 20;
inline constexpr int kMaxCommitSubject = 100;
inline constexpr int kPipelineTimeoutMin = 30;
inline constexpr std::size_t kMaxOpenApiRoutes = 4096;
inline constexpr std::size_t kMaxWebdavEntries = 10000;
} // namespace molo::example::fw::caps
+49
View File
@@ -0,0 +1,49 @@
#include "Cookies.hpp"
#include <cstdio>
namespace molo::example::fw {
namespace {
unsigned long long fnv1a(const std::string& s) {
unsigned long long h = 1469598103934665603ULL;
for (unsigned char c : s) {
h ^= c;
h *= 1099511628211ULL;
}
return h;
}
} // namespace
std::string cookie_sign(const std::string& value, const std::string& secret) {
char buf[32];
std::snprintf(buf, sizeof(buf), "%016llx",
(unsigned long long)fnv1a(secret + "|" + value));
return value + "." + buf;
}
bool cookie_verify(const std::string& signed_value, const std::string& secret,
std::string& out_value) {
std::size_t dot = signed_value.rfind('.');
if (dot == std::string::npos) return false;
std::string value = signed_value.substr(0, dot);
return cookie_sign(value, secret) == signed_value
? (out_value = value, true)
: false;
}
std::string make_set_cookie(const std::string& name, const std::string& value,
const std::string& path, bool http_only,
bool secure, const std::string& same_site,
long max_age) {
std::string out = name + "=" + value + "; Path=" + path;
if (max_age >= 0) out += "; Max-Age=" + std::to_string(max_age);
if (http_only) out += "; HttpOnly";
if (secure) out += "; Secure";
out += "; SameSite=" + same_site;
return out;
}
} // namespace molo::example::fw
+21
View File
@@ -0,0 +1,21 @@
#pragma once
// First-class cookies: sign and verify values for session cookies.
// Signing here is a keyed FNV-1a demonstration, NOT production crypto.
// Gap: real signing and encryption live in 03-security.md (AES-GCM or
// ChaCha20-Poly1305); this stub exists so the example compiles and runs.
#include <string>
namespace molo::example::fw {
std::string cookie_sign(const std::string& value, const std::string& secret);
bool cookie_verify(const std::string& signed_value, const std::string& secret,
std::string& out_value);
std::string make_set_cookie(const std::string& name, const std::string& value,
const std::string& path, bool http_only,
bool secure, const std::string& same_site,
long max_age);
} // namespace molo::example::fw
+272
View File
@@ -0,0 +1,272 @@
#include "Data.hpp"
#include <cstdio>
#include "Caps.hpp"
namespace molo::example::fw {
std::string SessionStore::create() {
std::lock_guard<std::mutex> lock(mu_);
std::string id = "sess-" + std::to_string(next_id_++);
sessions_[id] = {};
return id;
}
bool SessionStore::get(const std::string& id,
std::map<std::string, std::string>& out) {
std::lock_guard<std::mutex> lock(mu_);
auto it = sessions_.find(id);
if (it == sessions_.end()) return false;
out = it->second;
return true;
}
void SessionStore::put(const std::string& id, const std::string& key,
const std::string& value) {
std::lock_guard<std::mutex> lock(mu_);
if (!sessions_.count(id) && sessions_.size() >= caps::kMaxSessions) return;
sessions_[id][key] = value;
}
void SessionStore::rotate(const std::string& old_id, std::string& new_id) {
std::lock_guard<std::mutex> lock(mu_);
new_id = "sess-" + std::to_string(next_id_++);
auto it = sessions_.find(old_id);
if (it != sessions_.end()) {
sessions_[new_id] = it->second;
sessions_.erase(it);
} else {
sessions_[new_id] = {};
}
}
std::string demo_hash_password(const std::string& password) {
unsigned long long h = 1469598103934665603ULL;
std::string salted = "demo-salt|" + password;
for (unsigned char c : salted) {
h ^= c;
h *= 1099511628211ULL;
}
char buf[32];
std::snprintf(buf, sizeof(buf), "demo$%016llx", (unsigned long long)h);
return buf;
}
namespace {
int b64val(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
std::string b64decode(const std::string& in) {
std::string out;
int acc = 0, bits = 0;
for (char c : in) {
if (c == '=') break;
int v = b64val(c);
if (v < 0) continue;
acc = (acc << 6) | v;
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((char)((acc >> bits) & 0xFF));
}
}
return out;
}
} // namespace
bool parse_basic_auth(const std::string& header, std::string& user,
std::string& password) {
const std::string prefix = "Basic ";
if (header.compare(0, prefix.size(), prefix) != 0) return false;
std::string decoded = b64decode(header.substr(prefix.size()));
std::size_t colon = decoded.find(':');
if (colon == std::string::npos) return false;
user = decoded.substr(0, colon);
password = decoded.substr(colon + 1);
return true;
}
void Auth::add_user(const std::string& username, const std::string& password) {
std::lock_guard<std::mutex> lock(mu_);
users_[username] = demo_hash_password(password);
}
bool Auth::check(const std::string& username,
const std::string& password) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = users_.find(username);
if (it == users_.end()) return false;
return it->second == demo_hash_password(password);
}
std::string backend_name(OrmBackend backend) {
return backend == OrmBackend::Sqlite ? "sqlite" : "postgres";
}
OrmBackend parse_backend(const std::string& name) {
return name == "postgres" ? OrmBackend::Postgres : OrmBackend::Sqlite;
}
OrmDb::OrmDb(OrmBackend backend) : backend_(backend) {}
OrmBackend OrmDb::backend() const { return backend_; }
OrmResult OrmDb::insert(const std::string& table,
const std::map<std::string, std::string>& values) {
std::lock_guard<std::mutex> lock(mu_);
OrmResult res;
auto& cols = schema_[table];
if (cols.empty()) {
if (schema_.size() > caps::kMaxOrmTables) {
res.ok = false;
return res;
}
for (const auto& [k, v] : values) cols.push_back(k);
res.healed = true;
res.healed_note = "auto-created table " + table;
} else {
for (const auto& [k, v] : values) {
bool known = false;
for (const auto& c : cols) {
if (c == k) {
known = true;
break;
}
}
if (!known) {
cols.push_back(k);
res.healed = true;
res.healed_note = "auto-added column " + k + " to " + table;
}
}
}
auto& vec = rows_[table];
if (vec.size() >= caps::kMaxOrmRows) {
res.ok = false;
return res;
}
OrmRow row;
row.columns = values;
vec.push_back(row);
res.rows = {row};
return res;
}
OrmResult OrmDb::select_all(const std::string& table) {
std::lock_guard<std::mutex> lock(mu_);
OrmResult res;
auto it = rows_.find(table);
if (it == rows_.end()) {
if (!schema_.count(table)) {
schema_[table] = {"id"};
res.healed = true;
res.healed_note = "auto-created table " + table;
}
return res;
}
res.rows = it->second;
return res;
}
OrmResult OrmDb::ensure_column(const std::string& table,
const std::string& column) {
std::lock_guard<std::mutex> lock(mu_);
OrmResult res;
auto& cols = schema_[table];
for (const auto& c : cols) {
if (c == column) return res;
}
cols.push_back(column);
res.healed = true;
res.healed_note = "auto-added column " + column + " to " + table;
return res;
}
std::vector<std::string> OrmDb::tables() const {
std::lock_guard<std::mutex> lock(mu_);
std::vector<std::string> out;
for (const auto& [k, v] : schema_) out.push_back(k);
return out;
}
std::vector<std::string> OrmDb::columns(const std::string& table) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = schema_.find(table);
if (it == schema_.end()) return {};
return it->second;
}
bool UploadStore::store(const std::string& filename,
const std::string& content_type,
const std::string& data, UploadInfo& out,
std::string& error) {
if (data.size() > caps::kMaxUploadBytes) {
error = "file too large";
return false;
}
bool allowed = content_type == "text/plain" || content_type == "image/png" ||
content_type == "application/json";
if (!allowed) {
error = "content type not allowed";
return false;
}
if (filename.find("..") != std::string::npos ||
filename.find('/') != std::string::npos) {
error = "unsafe filename";
return false;
}
std::lock_guard<std::mutex> lock(mu_);
out = UploadInfo{filename, content_type, data.size()};
uploads_.push_back(out);
return true;
}
std::vector<UploadInfo> UploadStore::list() const {
std::lock_guard<std::mutex> lock(mu_);
return uploads_;
}
void WebdavStore::put(const std::string& path, const std::string& content) {
std::lock_guard<std::mutex> lock(mu_);
if (!files_.count(path) && files_.size() >= caps::kMaxWebdavEntries) return;
files_[path] = content;
}
bool WebdavStore::get(const std::string& path, std::string& out) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = files_.find(path);
if (it == files_.end()) return false;
out = it->second;
return true;
}
bool WebdavStore::remove(const std::string& path) {
std::lock_guard<std::mutex> lock(mu_);
return files_.erase(path) > 0;
}
std::string WebdavStore::propfind_xml(const std::string& root) const {
std::lock_guard<std::mutex> lock(mu_);
std::string out =
"<?xml version=\"1.0\"?><D:multistatus xmlns:D=\"DAV:\">";
for (const auto& [k, v] : files_) {
if (k.compare(0, root.size(), root) != 0) continue;
out += "<D:response><D:href>" + k +
"</D:href><D:propstat><D:prop><D:getcontentlength>" +
std::to_string(v.size()) +
"</D:getcontentlength></D:prop><D:status>HTTP/1.1 200 OK"
"</D:status></D:propstat></D:response>";
}
return out + "</D:multistatus>";
}
} // namespace molo::example::fw
+113
View File
@@ -0,0 +1,113 @@
#pragma once
// Data layer skeletons: sessions, basic auth, dual-backend ORM stub with
// self-healing demo, upload store, and WebDAV store.
// Gap: real 04-data.md owns the Postgres wire client, SQLite handling,
// pooling, transactions, and production self-healing; real password hashing
// (Argon2id) and encryption live in 03-security.md.
#include <map>
#include <mutex>
#include <string>
#include <vector>
namespace molo::example::fw {
class SessionStore {
public:
std::string create();
bool get(const std::string& id, std::map<std::string, std::string>& out);
void put(const std::string& id, const std::string& key,
const std::string& value);
void rotate(const std::string& old_id, std::string& new_id);
private:
mutable std::mutex mu_;
std::map<std::string, std::map<std::string, std::string>> sessions_;
unsigned long next_id_ = 1;
};
struct BasicUser {
std::string username;
std::string password_hash;
};
std::string demo_hash_password(const std::string& password);
bool parse_basic_auth(const std::string& header, std::string& user,
std::string& password);
class Auth {
public:
void add_user(const std::string& username, const std::string& password);
bool check(const std::string& username, const std::string& password) const;
private:
mutable std::mutex mu_;
std::map<std::string, std::string> users_;
};
enum class OrmBackend { Sqlite, Postgres };
std::string backend_name(OrmBackend backend);
OrmBackend parse_backend(const std::string& name);
struct OrmRow {
std::map<std::string, std::string> columns;
};
struct OrmResult {
bool ok = true;
bool healed = false;
std::string healed_note;
std::vector<OrmRow> rows;
};
class OrmDb {
public:
explicit OrmDb(OrmBackend backend);
OrmBackend backend() const;
OrmResult insert(const std::string& table,
const std::map<std::string, std::string>& values);
OrmResult select_all(const std::string& table);
OrmResult ensure_column(const std::string& table, const std::string& column);
std::vector<std::string> tables() const;
std::vector<std::string> columns(const std::string& table) const;
private:
OrmBackend backend_;
mutable std::mutex mu_;
std::map<std::string, std::vector<std::string>> schema_;
std::map<std::string, std::vector<OrmRow>> rows_;
};
struct UploadInfo {
std::string filename;
std::string content_type;
std::size_t size = 0;
};
class UploadStore {
public:
bool store(const std::string& filename, const std::string& content_type,
const std::string& data, UploadInfo& out, std::string& error);
std::vector<UploadInfo> list() const;
private:
mutable std::mutex mu_;
std::vector<UploadInfo> uploads_;
};
class WebdavStore {
public:
void put(const std::string& path, const std::string& content);
bool get(const std::string& path, std::string& out) const;
bool remove(const std::string& path);
std::string propfind_xml(const std::string& root) const;
private:
mutable std::mutex mu_;
std::map<std::string, std::string> files_;
};
} // namespace molo::example::fw
+266
View File
@@ -0,0 +1,266 @@
#include "Http.hpp"
#include <cctype>
#include "Caps.hpp"
namespace molo::example::fw {
Method parse_method(std::string_view name) {
if (name == "GET") return Method::Get;
if (name == "POST") return Method::Post;
if (name == "PUT") return Method::Put;
if (name == "PATCH") return Method::Patch;
if (name == "DELETE") return Method::Delete;
if (name == "HEAD") return Method::Head;
if (name == "OPTIONS") return Method::Options;
if (name == "PROPFIND") return Method::Propfind;
if (name == "PROPPATCH") return Method::Proppatch;
if (name == "MKCOL") return Method::Mkcol;
if (name == "COPY") return Method::Copy;
if (name == "MOVE") return Method::Move;
if (name == "LOCK") return Method::Lock;
if (name == "UNLOCK") return Method::Unlock;
return Method::Unknown;
}
std::string method_name(Method method) {
switch (method) {
case Method::Get: return "GET";
case Method::Post: return "POST";
case Method::Put: return "PUT";
case Method::Patch: return "PATCH";
case Method::Delete: return "DELETE";
case Method::Head: return "HEAD";
case Method::Options: return "OPTIONS";
case Method::Propfind: return "PROPFIND";
case Method::Proppatch: return "PROPPATCH";
case Method::Mkcol: return "MKCOL";
case Method::Copy: return "COPY";
case Method::Move: return "MOVE";
case Method::Lock: return "LOCK";
case Method::Unlock: return "UNLOCK";
case Method::Unknown: return "UNKNOWN";
}
return "UNKNOWN";
}
namespace {
bool name_eq(std::string_view a, std::string_view b) {
if (a.size() != b.size()) return false;
for (std::size_t i = 0; i < a.size(); ++i) {
if (std::tolower((unsigned char)a[i]) != std::tolower((unsigned char)b[i]))
return false;
}
return true;
}
} // namespace
void Headers::set(std::string name, std::string value) {
for (auto& e : entries_) {
if (name_eq(e.first, name)) {
e.second = value;
return;
}
}
entries_.emplace_back(std::move(name), std::move(value));
}
std::string Headers::get(std::string_view name) const {
for (const auto& e : entries_) {
if (name_eq(e.first, name)) return e.second;
}
return "";
}
bool Headers::has(std::string_view name) const {
for (const auto& e : entries_) {
if (name_eq(e.first, name)) return true;
}
return false;
}
const std::vector<std::pair<std::string, std::string>>& Headers::entries()
const {
return entries_;
}
std::string Request::query_or(std::string_view key,
std::string_view fallback) const {
auto it = query.find(std::string(key));
if (it == query.end()) return std::string(fallback);
return it->second;
}
std::string Request::param_or(std::string_view key,
std::string_view fallback) const {
auto it = path_params.find(std::string(key));
if (it == path_params.end()) return std::string(fallback);
return it->second;
}
std::string Request::cookie_or(std::string_view key,
std::string_view fallback) const {
auto it = cookies.find(std::string(key));
if (it == cookies.end()) return std::string(fallback);
return it->second;
}
Response::Response(int status, std::string body, std::string content_type)
: status(status), body(std::move(body)) {
headers.set("Content-Type", std::move(content_type));
}
std::string Response::content_type() const {
return headers.get("Content-Type");
}
TextResponse::TextResponse(std::string body, int status)
: Response(status, std::move(body), "text/plain; charset=utf-8") {}
HtmlResponse::HtmlResponse(std::string body, int status)
: Response(status, std::move(body), "text/html; charset=utf-8") {}
JsonResponse::JsonResponse(std::string body, int status)
: Response(status, std::move(body), "application/json") {}
XmlResponse::XmlResponse(std::string body, int status)
: Response(status, std::move(body), "application/xml") {}
RedirectResponse::RedirectResponse(std::string location, bool permanent)
: Response(permanent ? 301 : 302, "", "text/plain; charset=utf-8") {
headers.set("Location", std::move(location));
}
FileResponse::FileResponse(std::string body, std::string content_type,
std::string filename)
: Response(200, std::move(body), std::move(content_type)) {
headers.set("Content-Disposition",
"attachment; filename=\"" + filename + "\"");
}
StreamingResponse::StreamingResponse(std::vector<std::string> chunks)
: Response(200, "", "text/plain; charset=utf-8"), chunks(std::move(chunks)) {
for (const auto& c : this->chunks) body += c;
headers.set("Transfer-Encoding", "chunked");
if (this->chunks.size() > caps::kMaxStreamChunks) {
status = 500;
body = "chunk cap exceeded";
}
}
SseResponse::SseResponse(std::vector<std::string> events)
: Response(200, "", "text/event-stream"), events(std::move(events)) {
for (const auto& e : this->events) body += "data: " + e + "\n\n";
}
std::string url_decode(std::string_view in) {
std::string out;
out.reserve(in.size());
for (std::size_t i = 0; i < in.size(); ++i) {
if (in[i] == '%' && i + 2 < in.size() &&
std::isxdigit((unsigned char)in[i + 1]) &&
std::isxdigit((unsigned char)in[i + 2])) {
int hi = std::isdigit((unsigned char)in[i + 1])
? in[i + 1] - '0'
: std::tolower((unsigned char)in[i + 1]) - 'a' + 10;
int lo = std::isdigit((unsigned char)in[i + 2])
? in[i + 2] - '0'
: std::tolower((unsigned char)in[i + 2]) - 'a' + 10;
out.push_back((char)(hi * 16 + lo));
i += 2;
} else if (in[i] == '+') {
out.push_back(' ');
} else {
out.push_back(in[i]);
}
}
return out;
}
std::map<std::string, std::string> parse_query(std::string_view qs) {
std::map<std::string, std::string> out;
std::size_t start = 0;
std::size_t fields = 0;
while (start <= qs.size() && fields < caps::kMaxFormFields) {
std::size_t amp = qs.find('&', start);
if (amp == std::string_view::npos) amp = qs.size();
std::string_view pair = qs.substr(start, amp - start);
std::size_t eq = pair.find('=');
if (eq == std::string_view::npos) {
if (!pair.empty()) out[url_decode(pair)] = "";
} else {
out[url_decode(pair.substr(0, eq))] = url_decode(pair.substr(eq + 1));
}
++fields;
start = amp + 1;
}
return out;
}
std::map<std::string, std::string> parse_cookies(std::string_view header) {
std::map<std::string, std::string> out;
std::size_t start = 0;
while (start < header.size()) {
std::size_t semi = header.find(';', start);
if (semi == std::string_view::npos) semi = header.size();
std::string_view pair = header.substr(start, semi - start);
std::size_t eq = pair.find('=');
if (eq != std::string_view::npos) {
std::string k(pair.substr(0, eq));
std::string v(pair.substr(eq + 1));
while (!k.empty() && k.front() == ' ') k.erase(k.begin());
while (!v.empty() && v.front() == ' ') v.erase(v.begin());
if (!k.empty()) out[k] = v;
}
start = semi + 1;
}
return out;
}
std::map<std::string, std::string> parse_form(std::string_view body) {
return parse_query(body);
}
std::string status_text(int status) {
switch (status) {
case 200: return "OK";
case 201: return "Created";
case 207: return "Multi-Status";
case 301: return "Moved Permanently";
case 302: return "Found";
case 400: return "Bad Request";
case 401: return "Unauthorized";
case 403: return "Forbidden";
case 404: return "Not Found";
case 405: return "Method Not Allowed";
case 406: return "Not Acceptable";
case 409: return "Conflict";
case 413: return "Content Too Large";
case 422: return "Unprocessable Entity";
case 429: return "Too Many Requests";
case 500: return "Internal Server Error";
case 501: return "Not Implemented";
default: return "Unknown";
}
}
std::string json_escape(std::string_view in) {
std::string out;
out.reserve(in.size() + 2);
for (char c : in) {
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default: out.push_back(c); break;
}
}
return out;
}
} // namespace molo::example::fw
+114
View File
@@ -0,0 +1,114 @@
#pragma once
// HTTP message types: one Request, one Response hierarchy with a subclass per
// format. Subclasses are constructor conveniences over the base value type so
// handlers can return by value without slicing loss. Gap: real molo::http_msg
// in 05-request-routing.md owns the final API; this skeleton mirrors it.
#include <map>
#include <string>
#include <string_view>
#include <vector>
namespace molo::example::fw {
enum class Method {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
Propfind,
Proppatch,
Mkcol,
Copy,
Move,
Lock,
Unlock,
Unknown,
};
Method parse_method(std::string_view name);
std::string method_name(Method method);
class Headers {
public:
void set(std::string name, std::string value);
std::string get(std::string_view name) const;
bool has(std::string_view name) const;
const std::vector<std::pair<std::string, std::string>>& entries() const;
private:
std::vector<std::pair<std::string, std::string>> entries_;
};
struct Request {
Method method = Method::Get;
std::string path = "/";
std::map<std::string, std::string> query;
Headers headers;
std::map<std::string, std::string> cookies;
std::map<std::string, std::string> path_params;
std::string body;
std::string correlation_id;
std::string query_or(std::string_view key, std::string_view fallback) const;
std::string param_or(std::string_view key, std::string_view fallback) const;
std::string cookie_or(std::string_view key, std::string_view fallback) const;
};
struct Response {
int status = 200;
Headers headers;
std::string body;
Response() = default;
Response(int status, std::string body, std::string content_type);
std::string content_type() const;
};
struct TextResponse : Response {
explicit TextResponse(std::string body, int status = 200);
};
struct HtmlResponse : Response {
explicit HtmlResponse(std::string body, int status = 200);
};
struct JsonResponse : Response {
explicit JsonResponse(std::string body, int status = 200);
};
struct XmlResponse : Response {
explicit XmlResponse(std::string body, int status = 200);
};
struct RedirectResponse : Response {
RedirectResponse(std::string location, bool permanent);
};
struct FileResponse : Response {
FileResponse(std::string body, std::string content_type, std::string filename);
};
struct StreamingResponse : Response {
explicit StreamingResponse(std::vector<std::string> chunks);
std::vector<std::string> chunks;
};
struct SseResponse : Response {
explicit SseResponse(std::vector<std::string> events);
std::vector<std::string> events;
};
std::string url_decode(std::string_view in);
std::map<std::string, std::string> parse_query(std::string_view qs);
std::map<std::string, std::string> parse_cookies(std::string_view header);
std::map<std::string, std::string> parse_form(std::string_view body);
std::string status_text(int status);
std::string json_escape(std::string_view in);
} // namespace molo::example::fw
+113
View File
@@ -0,0 +1,113 @@
#include "Jobs.hpp"
#include "Caps.hpp"
namespace molo::example::fw {
void Supervisor::add_worker(const std::string& name) {
std::lock_guard<std::mutex> lock(mu_);
if (!workers_.count(name) && workers_.size() >= caps::kMaxWorkers) return;
workers_[name] = WorkerInfo{name, false, 0};
}
bool Supervisor::start(const std::string& name) {
std::lock_guard<std::mutex> lock(mu_);
auto it = workers_.find(name);
if (it == workers_.end()) return false;
it->second.running = true;
return true;
}
bool Supervisor::stop(const std::string& name) {
std::lock_guard<std::mutex> lock(mu_);
auto it = workers_.find(name);
if (it == workers_.end()) return false;
it->second.running = false;
return true;
}
bool Supervisor::restart(const std::string& name) {
std::lock_guard<std::mutex> lock(mu_);
auto it = workers_.find(name);
if (it == workers_.end()) return false;
it->second.running = true;
++it->second.restarts;
return true;
}
std::vector<WorkerInfo> Supervisor::status() const {
std::lock_guard<std::mutex> lock(mu_);
std::vector<WorkerInfo> out;
for (const auto& [k, v] : workers_) out.push_back(v);
return out;
}
std::string TaskQueue::enqueue(const std::string& kind,
const std::string& payload, long delay_sec) {
std::lock_guard<std::mutex> lock(mu_);
if (tasks_.size() >= caps::kMaxTasks) return "";
std::string id = "task-" + std::to_string(next_id_++);
TaskInfo t{id, kind, payload,
delay_sec > 0 ? "delayed" : "queued", 0};
tasks_[id] = t;
return id;
}
bool TaskQueue::cancel(const std::string& id) {
std::lock_guard<std::mutex> lock(mu_);
auto it = tasks_.find(id);
if (it == tasks_.end()) return false;
it->second.state = "cancelled";
return true;
}
bool TaskQueue::retry(const std::string& id) {
std::lock_guard<std::mutex> lock(mu_);
auto it = tasks_.find(id);
if (it == tasks_.end()) return false;
++it->second.attempts;
it->second.state = "queued";
return true;
}
void TaskQueue::run_inline(TaskInfo& task) {
task.state = "done";
std::lock_guard<std::mutex> lock(mu_);
auto it = tasks_.find(task.id);
if (it != tasks_.end()) it->second.state = "done";
}
std::vector<TaskInfo> TaskQueue::list() const {
std::lock_guard<std::mutex> lock(mu_);
std::vector<TaskInfo> out;
for (const auto& [k, v] : tasks_) out.push_back(v);
return out;
}
ReplEntry ReplicationLog::append(const std::string& object,
const std::string& data) {
std::lock_guard<std::mutex> lock(mu_);
ReplEntry e;
e.seq = entries_.size() + 1;
e.object = object;
e.data = data;
e.resolution = "last-writer-wins";
if (entries_.size() < caps::kMaxReplEntries) entries_.push_back(e);
return e;
}
std::vector<ReplEntry> ReplicationLog::since(unsigned long seq) const {
std::lock_guard<std::mutex> lock(mu_);
std::vector<ReplEntry> out;
for (const auto& e : entries_) {
if (e.seq > seq) out.push_back(e);
}
return out;
}
unsigned long ReplicationLog::latest() const {
std::lock_guard<std::mutex> lock(mu_);
return entries_.size();
}
} // namespace molo::example::fw
+75
View File
@@ -0,0 +1,75 @@
#pragma once
// Supervisor plus background tasks plus replication log skeletons.
// Gap: real 07-supervisor-replication.md owns worker processes, restarts,
// durable streams, conflict handling, and multi-instance deployment.
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <vector>
namespace molo::example::fw {
struct WorkerInfo {
std::string name;
bool running = false;
int restarts = 0;
};
class Supervisor {
public:
void add_worker(const std::string& name);
bool start(const std::string& name);
bool stop(const std::string& name);
bool restart(const std::string& name);
std::vector<WorkerInfo> status() const;
private:
mutable std::mutex mu_;
std::map<std::string, WorkerInfo> workers_;
};
struct TaskInfo {
std::string id;
std::string kind;
std::string payload;
std::string state;
int attempts = 0;
};
class TaskQueue {
public:
std::string enqueue(const std::string& kind, const std::string& payload,
long delay_sec = 0);
bool cancel(const std::string& id);
bool retry(const std::string& id);
void run_inline(TaskInfo& task);
std::vector<TaskInfo> list() const;
private:
mutable std::mutex mu_;
std::map<std::string, TaskInfo> tasks_;
unsigned long next_id_ = 1;
};
struct ReplEntry {
unsigned long seq = 0;
std::string object;
std::string data;
std::string resolution;
};
class ReplicationLog {
public:
ReplEntry append(const std::string& object, const std::string& data);
std::vector<ReplEntry> since(unsigned long seq) const;
unsigned long latest() const;
private:
mutable std::mutex mu_;
std::vector<ReplEntry> entries_;
};
} // namespace molo::example::fw
+95
View File
@@ -0,0 +1,95 @@
#include "Kv.hpp"
#include "Caps.hpp"
namespace molo::example::fw {
bool KvStore::set(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mu_);
if (!data_.count(key) && data_.size() >= caps::kMaxKvEntries) return false;
data_[key] = value;
return true;
}
bool KvStore::get(const std::string& key, std::string& out) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = data_.find(key);
if (it == data_.end()) return false;
out = it->second;
return true;
}
bool KvStore::del(const std::string& key) {
std::lock_guard<std::mutex> lock(mu_);
return data_.erase(key) > 0;
}
std::size_t KvStore::size() const {
std::lock_guard<std::mutex> lock(mu_);
return data_.size();
}
void Cache::put(const std::string& key, const std::string& value, long ttl_sec,
long now) {
std::lock_guard<std::mutex> lock(mu_);
if (!data_.count(key) && data_.size() >= caps::kMaxCacheEntries) return;
data_[key] = CacheEntry{value, now + ttl_sec};
}
bool Cache::lookup(const std::string& key, std::string& out, long now) {
std::lock_guard<std::mutex> lock(mu_);
auto it = data_.find(key);
if (it == data_.end()) return false;
if (it->second.expires_at <= now) {
data_.erase(it);
return false;
}
out = it->second.value;
return true;
}
void Cache::invalidate(const std::string& key) {
std::lock_guard<std::mutex> lock(mu_);
data_.erase(key);
}
std::size_t Cache::size() const {
std::lock_guard<std::mutex> lock(mu_);
return data_.size();
}
bool Pubsub::subscribe(const std::string& topic, PubsubHandler handler) {
std::lock_guard<std::mutex> lock(mu_);
if (!subs_.count(topic) && subs_.size() >= caps::kMaxPubsubTopics)
return false;
subs_[topic].push_back(std::move(handler));
return true;
}
std::size_t Pubsub::publish(const std::string& topic,
const std::string& message) {
std::lock_guard<std::mutex> lock(mu_);
auto& hist = history_[topic];
hist.push_back(message);
if (hist.size() > caps::kMaxPubsubHistory) hist.erase(hist.begin());
auto it = subs_.find(topic);
if (it == subs_.end()) return 0;
for (auto& h : it->second) h(message);
return it->second.size();
}
std::vector<std::string> Pubsub::history(const std::string& topic) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = history_.find(topic);
if (it == history_.end()) return {};
return it->second;
}
std::vector<std::string> Pubsub::topics() const {
std::lock_guard<std::mutex> lock(mu_);
std::vector<std::string> out;
for (const auto& [k, v] : subs_) out.push_back(k);
return out;
}
} // namespace molo::example::fw
+61
View File
@@ -0,0 +1,61 @@
#pragma once
// In-memory key-value store, TTL cache, and pubsub broker skeletons.
// Gap: real implementations in 04-data.md own persistence, replication,
// durability guarantees, and multi-instance sharing.
#include <cstdint>
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <vector>
namespace molo::example::fw {
class KvStore {
public:
bool set(const std::string& key, const std::string& value);
bool get(const std::string& key, std::string& out) const;
bool del(const std::string& key);
std::size_t size() const;
private:
mutable std::mutex mu_;
std::map<std::string, std::string> data_;
};
struct CacheEntry {
std::string value;
long expires_at = 0;
};
class Cache {
public:
void put(const std::string& key, const std::string& value, long ttl_sec,
long now);
bool lookup(const std::string& key, std::string& out, long now);
void invalidate(const std::string& key);
std::size_t size() const;
private:
mutable std::mutex mu_;
std::map<std::string, CacheEntry> data_;
};
using PubsubHandler = std::function<void(const std::string& message)>;
class Pubsub {
public:
bool subscribe(const std::string& topic, PubsubHandler handler);
std::size_t publish(const std::string& topic, const std::string& message);
std::vector<std::string> history(const std::string& topic) const;
std::vector<std::string> topics() const;
private:
mutable std::mutex mu_;
std::map<std::string, std::vector<PubsubHandler>> subs_;
std::map<std::string, std::vector<std::string>> history_;
};
} // namespace molo::example::fw
+70
View File
@@ -0,0 +1,70 @@
#include "Negotiate.hpp"
#include <cctype>
namespace molo::example::fw {
namespace {
std::string trim(std::string s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t')) s.erase(s.begin());
while (!s.empty() && (s.back() == ' ' || s.back() == '\t')) s.pop_back();
return s;
}
} // namespace
std::string negotiate(const std::string& accept,
const std::vector<std::string>& available) {
if (available.empty()) throw HttpError(500, "no_offers", "no offers");
if (accept.empty()) return available.front();
struct Range {
std::string type;
double q = 1.0;
};
std::vector<Range> ranges;
std::size_t start = 0;
while (start < accept.size()) {
std::size_t comma = accept.find(',', start);
if (comma == std::string::npos) comma = accept.size();
std::string part = trim(accept.substr(start, comma - start));
std::size_t semi = part.find(';');
Range r{trim(part.substr(0, semi)), 1.0};
if (semi != std::string::npos) {
std::string params = part.substr(semi + 1);
std::size_t q = params.find("q=");
if (q != std::string::npos) {
try {
r.q = std::stod(params.substr(q + 2));
} catch (...) {
r.q = 1.0;
}
}
}
ranges.push_back(r);
start = comma + 1;
}
std::string best;
double best_q = -1.0;
for (const auto& a : available) {
for (const auto& r : ranges) {
bool hit = r.type == "*/*" || r.type == a;
if (!hit) {
std::size_t slash = a.find('/');
std::string wild = slash == std::string::npos
? "*/*"
: a.substr(0, slash) + "/*";
hit = r.type == wild;
}
if (hit && r.q > best_q) {
best_q = r.q;
best = a;
}
}
}
if (best.empty() || best_q <= 0.0)
throw HttpError(406, "not_acceptable", "no acceptable format");
return best;
}
} // namespace molo::example::fw
+22
View File
@@ -0,0 +1,22 @@
#pragma once
// Server-driven content negotiation: parses Accept with q weights, picks one
// handler-declared format, answers 406 when nothing matches.
// Gap: real molo::negotiate in 05-request-routing.md owns suffix and
// format-override rules.
#include <string>
#include <vector>
#include "App.hpp"
namespace molo::example::fw {
struct Offer {
std::string media_type;
};
std::string negotiate(const std::string& accept,
const std::vector<std::string>& available);
} // namespace molo::example::fw
+82
View File
@@ -0,0 +1,82 @@
#include "Net.hpp"
#include "Caps.hpp"
namespace molo::example::fw {
RestClient::RestClient(RestTransport transport)
: transport_(std::move(transport)) {}
Response RestClient::get(const std::string& path) {
Request req;
req.method = Method::Get;
req.path = path;
return transport_(std::move(req));
}
Response RestClient::post_json(const std::string& path,
const std::string& json) {
Request req;
req.method = Method::Post;
req.path = path;
req.body = json;
req.headers.set("Content-Type", "application/json");
return transport_(std::move(req));
}
void OpenApi::add(const std::string& path, const std::string& method,
const std::string& summary) {
if (routes_.size() >= caps::kMaxOpenApiRoutes) return;
routes_.push_back({path, method, summary});
}
std::string OpenApi::to_json() const {
std::string out =
"{\"openapi\":\"3.0.0\",\"info\":{\"title\":\"living-example\","
"\"version\":\"0.1.0\"},\"paths\":{";
bool first = true;
for (const auto& r : routes_) {
if (!first) out += ",";
first = false;
out += "\"" + json_escape(r.path) + "\":{\"" + json_escape(r.method) +
"\":{\"summary\":\"" + json_escape(r.summary) + "\"}}";
}
return out + "}}";
}
std::size_t OpenApi::size() const { return routes_.size(); }
std::string redact_secrets(const std::string& text) {
std::string out = text;
const char* keys[] = {"DEVPLACE_API_KEY", "API_KEY", "SECRET", "TOKEN",
"PASSWORD"};
for (const char* k : keys) {
std::string key(k);
std::size_t pos = 0;
while ((pos = out.find(key, pos)) != std::string::npos) {
std::size_t eol = out.find('\n', pos);
if (eol == std::string::npos) eol = out.size();
std::size_t eq = out.find('=', pos);
if (eq != std::string::npos && eq < eol) {
out.replace(eq + 1, eol - eq - 1, "[REDACTED]");
pos = eq + 1;
} else {
pos += key.size();
}
}
}
return out;
}
AiSample ai_harness_sample(const std::string& prompt) {
AiSample s;
s.base_url = "https://devplace.net/openai/v1";
s.model = "molodetz";
s.redacted_prompt = redact_secrets(prompt);
s.stream_chunks = {"data: {\"delta\":\"plan: add route\"}\n\n",
"data: {\"delta\":\"apply: 1 file\"}\n\n",
"data: [DONE]\n\n"};
return s;
}
} // namespace molo::example::fw
+57
View File
@@ -0,0 +1,57 @@
#pragma once
// Client parity skeletons: REST client (self-call transport), realtime
// client notes, OpenAPI registry, and the AI harness sample.
// Gap: real HTTP/1.1 plus HTTP/2 client in 08-clients-openapi.md, realtime
// clients in 06-realtime.md, and the AI harness CLI in 09-cli-ops.md.
#include <functional>
#include <map>
#include <string>
#include <vector>
#include "Http.hpp"
namespace molo::example::fw {
using RestTransport = std::function<Response(Request)>;
class RestClient {
public:
explicit RestClient(RestTransport transport);
Response get(const std::string& path);
Response post_json(const std::string& path, const std::string& json);
private:
RestTransport transport_;
};
struct OpenApiRoute {
std::string path;
std::string method;
std::string summary;
};
class OpenApi {
public:
void add(const std::string& path, const std::string& method,
const std::string& summary);
std::string to_json() const;
std::size_t size() const;
private:
std::vector<OpenApiRoute> routes_;
};
struct AiSample {
std::string base_url;
std::string model;
std::string redacted_prompt;
std::vector<std::string> stream_chunks;
};
std::string redact_secrets(const std::string& text);
AiSample ai_harness_sample(const std::string& prompt);
} // namespace molo::example::fw
+86
View File
@@ -0,0 +1,86 @@
#include "Observ.hpp"
#include <cstdio>
#include "Caps.hpp"
namespace molo::example::fw {
void EventBus::on(const std::string& type,
std::function<void(const Event&)> listener) {
std::lock_guard<std::mutex> lock(mu_);
if (!kEnabled) return;
listeners_[type].push_back(std::move(listener));
}
void EventBus::emit(const Event& event) {
std::lock_guard<std::mutex> lock(mu_);
if (!kEnabled) return;
delivered_.push_back(event);
auto it = listeners_.find(event.type);
if (it == listeners_.end()) return;
for (auto& l : it->second) l(event);
}
std::vector<Event> EventBus::delivered() const {
std::lock_guard<std::mutex> lock(mu_);
return delivered_;
}
void Metrics::inc(const std::string& name, double value) {
std::lock_guard<std::mutex> lock(mu_);
if (!kEnabled) return;
if (!values_.count(name) && values_.size() >= caps::kMaxMetrics) return;
values_[name] += value;
}
void Metrics::gauge(const std::string& name, double value) {
std::lock_guard<std::mutex> lock(mu_);
if (!kEnabled) return;
if (!values_.count(name) && values_.size() >= caps::kMaxMetrics) return;
values_[name] = value;
}
double Metrics::get(const std::string& name) const {
std::lock_guard<std::mutex> lock(mu_);
auto it = values_.find(name);
return it == values_.end() ? 0.0 : it->second;
}
std::string Metrics::render_prometheus() const {
std::lock_guard<std::mutex> lock(mu_);
std::string out;
for (const auto& [k, v] : values_) {
char buf[64];
std::snprintf(buf, sizeof(buf), "%g", v);
out += k + " " + buf + "\n";
}
return out;
}
Span Telemetry::start_span(const std::string& name,
const std::string& correlation_id) {
std::lock_guard<std::mutex> lock(mu_);
Span s;
if (!kEnabled) return s;
s.trace_id = "trace-" + std::to_string(next_id_);
s.span_id = "span-" + std::to_string(next_id_);
++next_id_;
s.name = name;
s.correlation_id = correlation_id;
return s;
}
void Telemetry::finish_span(const Span& span) {
std::lock_guard<std::mutex> lock(mu_);
if (!kEnabled) return;
if (spans_.size() >= caps::kMaxSpans) return;
spans_.push_back(span);
}
std::vector<Span> Telemetry::spans() const {
std::lock_guard<std::mutex> lock(mu_);
return spans_;
}
} // namespace molo::example::fw
+72
View File
@@ -0,0 +1,72 @@
#pragma once
// Observability skeletons: typed event bus, metrics registry, and tracer.
// All three compile to near-zero overhead when kEnabled is false.
// Gap: real 02-observability.md owns the log format, Prometheus and OTel
// export, and the zero-cost macro layer.
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <vector>
namespace molo::example::fw {
struct Event {
std::string type;
std::string payload;
std::string correlation_id;
};
class EventBus {
public:
static constexpr bool kEnabled = true;
void on(const std::string& type, std::function<void(const Event&)> listener);
void emit(const Event& event);
std::vector<Event> delivered() const;
private:
mutable std::mutex mu_;
std::map<std::string, std::vector<std::function<void(const Event&)>>>
listeners_;
std::vector<Event> delivered_;
};
class Metrics {
public:
static constexpr bool kEnabled = true;
void inc(const std::string& name, double value = 1.0);
void gauge(const std::string& name, double value);
double get(const std::string& name) const;
std::string render_prometheus() const;
private:
mutable std::mutex mu_;
std::map<std::string, double> values_;
};
struct Span {
std::string trace_id;
std::string span_id;
std::string name;
std::string correlation_id;
};
class Telemetry {
public:
static constexpr bool kEnabled = true;
Span start_span(const std::string& name, const std::string& correlation_id);
void finish_span(const Span& span);
std::vector<Span> spans() const;
private:
mutable std::mutex mu_;
std::vector<Span> spans_;
unsigned long next_id_ = 1;
};
} // namespace molo::example::fw
+129
View File
@@ -0,0 +1,129 @@
#include "Router.hpp"
#include <stdexcept>
#include "Caps.hpp"
namespace molo::example::fw {
namespace {
std::vector<std::string> split_path(std::string_view path) {
std::vector<std::string> segs;
std::string cur;
for (char c : path) {
if (c == '/') {
if (!cur.empty()) {
segs.push_back(cur);
cur.clear();
}
} else {
cur.push_back(c);
}
}
if (!cur.empty()) segs.push_back(cur);
return segs;
}
} // namespace
bool match_pattern(std::string_view pattern, std::string_view path,
std::map<std::string, std::string>& params) {
auto want = split_path(pattern);
auto got = split_path(path);
if (want.size() != got.size()) return false;
for (std::size_t i = 0; i < want.size(); ++i) {
const std::string& w = want[i];
if (w.size() >= 2 && w.front() == '{' && w.back() == '}') {
params[w.substr(1, w.size() - 2)] = url_decode(got[i]);
} else if (w != got[i]) {
return false;
}
}
return true;
}
void Router::add(std::vector<Method> methods, std::string pattern,
std::string name, Handler handler) {
if (routes_.size() >= caps::kMaxRouteCount)
throw std::length_error("route cap exceeded");
for (const auto& r : routes_) {
if (r.name == name)
throw std::runtime_error("duplicate route name: " + name);
}
routes_.push_back(
Route{std::move(methods), std::move(pattern), std::move(name),
std::move(handler)});
}
void Router::get(std::string pattern, std::string name, Handler handler) {
add({Method::Get}, std::move(pattern), std::move(name), std::move(handler));
}
void Router::post(std::string pattern, std::string name, Handler handler) {
add({Method::Post}, std::move(pattern), std::move(name), std::move(handler));
}
void Router::put(std::string pattern, std::string name, Handler handler) {
add({Method::Put}, std::move(pattern), std::move(name), std::move(handler));
}
void Router::patch(std::string pattern, std::string name, Handler handler) {
add({Method::Patch}, std::move(pattern), std::move(name), std::move(handler));
}
void Router::del(std::string pattern, std::string name, Handler handler) {
add({Method::Delete}, std::move(pattern), std::move(name),
std::move(handler));
}
void Router::options(std::string pattern, std::string name, Handler handler) {
add({Method::Options}, std::move(pattern), std::move(name),
std::move(handler));
}
void Router::any(std::vector<Method> methods, std::string pattern,
std::string name, Handler handler) {
add(std::move(methods), std::move(pattern), std::move(name),
std::move(handler));
}
void Router::include_router(const std::string& prefix, const Router& other,
const std::string& name_prefix) {
for (const auto& r : other.routes_) {
std::string pattern = prefix;
if (!r.pattern.empty() && r.pattern != "/") {
if (pattern.empty() || pattern.back() != '/') pattern += "/";
std::string sub = r.pattern;
if (!sub.empty() && sub.front() == '/') sub.erase(sub.begin());
pattern += sub;
}
if (pattern.empty()) pattern = "/";
std::string name = name_prefix.empty() ? r.name : name_prefix + r.name;
add(r.methods, pattern, name, r.handler);
}
}
bool Router::match(Request& req, Handler& out) const {
for (const auto& r : routes_) {
bool method_ok = false;
for (Method m : r.methods) {
if (m == req.method) {
method_ok = true;
break;
}
}
if (!method_ok) continue;
std::map<std::string, std::string> params;
if (match_pattern(r.pattern, req.path, params)) {
for (auto& [k, v] : params) req.path_params[k] = v;
out = r.handler;
return true;
}
}
return false;
}
const std::vector<Route>& Router::routes() const { return routes_; }
} // namespace molo::example::fw
+52
View File
@@ -0,0 +1,52 @@
#pragma once
// Router: FastAPI-style route groups mounted on the app with a shared prefix.
// Gap: real molo::routing in 05-request-routing.md owns path compilation,
// url_for and converters; this skeleton supports {param} segments only.
#include <functional>
#include <string>
#include <vector>
#include "Http.hpp"
namespace molo::example::fw {
using Handler = std::function<Response(Request)>;
struct Route {
std::vector<Method> methods;
std::string pattern;
std::string name;
Handler handler;
};
class Router {
public:
void add(std::vector<Method> methods, std::string pattern, std::string name,
Handler handler);
void get(std::string pattern, std::string name, Handler handler);
void post(std::string pattern, std::string name, Handler handler);
void put(std::string pattern, std::string name, Handler handler);
void patch(std::string pattern, std::string name, Handler handler);
void del(std::string pattern, std::string name, Handler handler);
void options(std::string pattern, std::string name, Handler handler);
void any(std::vector<Method> methods, std::string pattern, std::string name,
Handler handler);
void include_router(const std::string& prefix, const Router& other,
const std::string& name_prefix = "");
bool match(Request& req, Handler& out) const;
const std::vector<Route>& routes() const;
private:
std::vector<Route> routes_;
};
bool match_pattern(std::string_view pattern, std::string_view path,
std::map<std::string, std::string>& params);
} // namespace molo::example::fw
+205
View File
@@ -0,0 +1,205 @@
#include "Server.hpp"
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <sstream>
#include "Caps.hpp"
namespace molo::example::fw {
Server::Server(std::shared_ptr<Application> app) : app_(std::move(app)) {
stop();
}
Server::~Server() { stop(); }
bool Server::start(const std::string& host, int port) {
if (running_) return false;
listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd_ < 0) return false;
int one = 1;
::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
if (::inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
::close(listen_fd_);
listen_fd_ = -1;
return false;
}
if (::bind(listen_fd_, (sockaddr*)&addr, sizeof(addr)) != 0) {
::close(listen_fd_);
listen_fd_ = -1;
return false;
}
if (port == 0) {
sockaddr_in bound{};
socklen_t len = sizeof(bound);
if (::getsockname(listen_fd_, (sockaddr*)&bound, &len) == 0)
port_ = ntohs(bound.sin_port);
} else {
port_ = port;
}
if (::listen(listen_fd_, caps::kServerBacklog) != 0) {
::close(listen_fd_);
listen_fd_ = -1;
return false;
}
running_ = true;
worker_ = std::thread([this]() { loop(); });
return true;
}
void Server::stop() {
bool was = running_.exchange(false);
if (listen_fd_ >= 0) {
::shutdown(listen_fd_, SHUT_RDWR);
::close(listen_fd_);
listen_fd_ = -1;
}
if (was && worker_.joinable() &&
worker_.get_id() != std::this_thread::get_id())
worker_.join();
else if (worker_.joinable() &&
worker_.get_id() == std::this_thread::get_id())
worker_.detach();
}
int Server::port() const { return port_; }
bool Server::running() const { return running_; }
void Server::loop() {
int served = 0;
while (running_ && served < caps::kMaxConnections) {
sockaddr_in peer{};
socklen_t len = sizeof(peer);
int fd = ::accept(listen_fd_, (sockaddr*)&peer, &len);
if (fd < 0) {
if (!running_) break;
continue;
}
struct timeval tv{};
tv.tv_sec = caps::kServerTimeoutSec;
::setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
handle_one(fd);
::close(fd);
++served;
}
}
void Server::handle_one(int fd) {
std::string raw;
char buf[4096];
while (raw.size() < caps::kMaxHeaderBytes) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n <= 0) break;
raw.append(buf, (std::size_t)n);
if (raw.find("\r\n\r\n") != std::string::npos) break;
}
std::size_t hdr_end = raw.find("\r\n\r\n");
if (hdr_end == std::string::npos) return;
std::string head = raw.substr(0, hdr_end);
std::string rest = raw.substr(hdr_end + 4);
std::istringstream lines(head);
std::string line;
if (!std::getline(lines, line)) return;
if (!line.empty() && line.back() == '\r') line.pop_back();
std::istringstream rl(line);
std::string method_s, target, version;
rl >> method_s >> target >> version;
Request req;
req.method = parse_method(method_s);
std::size_t q = target.find('?');
if (q == std::string::npos) {
req.path = target.empty() ? "/" : target;
} else {
req.path = target.substr(0, q);
req.query = parse_query(target.substr(q + 1));
}
std::size_t header_count = 0;
std::size_t content_length = 0;
while (std::getline(lines, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty()) continue;
if (++header_count > caps::kMaxHeaderCount) break;
std::size_t colon = line.find(':');
if (colon == std::string::npos) continue;
std::string name = line.substr(0, colon);
std::string value = line.substr(colon + 1);
while (!value.empty() && value.front() == ' ') value.erase(value.begin());
req.headers.set(name, value);
if (name == "Content-Length" || name == "content-length") {
try {
content_length = (std::size_t)std::stoul(value);
} catch (...) {
content_length = 0;
}
}
}
std::string cookie_h = req.headers.get("Cookie");
if (!cookie_h.empty()) req.cookies = parse_cookies(cookie_h);
req.body = rest;
while (req.body.size() < content_length &&
req.body.size() < caps::kMaxRequestBody) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n <= 0) break;
req.body.append(buf, (std::size_t)n);
}
Response res = app_->dispatch(std::move(req));
if (res.body.size() > caps::kMaxTestBody) {
res.status = 500;
res.body = "response over cap";
}
std::ostringstream out;
out << "HTTP/1.1 " << res.status << " " << status_text(res.status) << "\r\n";
out << "Content-Length: " << res.body.size() << "\r\n";
out << "Connection: close\r\n";
for (const auto& [k, v] : res.headers.entries()) {
if (k == "Content-Length" || k == "content-length") continue;
if (k == "Transfer-Encoding" || k == "transfer-encoding") continue;
out << k << ": " << v << "\r\n";
}
out << "\r\n" << res.body;
std::string wire = out.str();
std::size_t sent = 0;
while (sent < wire.size()) {
ssize_t n = ::send(fd, wire.data() + sent, wire.size() - sent, 0);
if (n <= 0) break;
sent += (std::size_t)n;
}
}
std::string http_roundtrip(const std::string& host, int port,
const std::string& raw_request) {
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return "";
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)port);
if (::inet_pton(AF_INET, host.c_str(), &addr.sin_addr) != 1) {
::close(fd);
return "";
}
if (::connect(fd, (sockaddr*)&addr, sizeof(addr)) != 0) {
::close(fd);
return "";
}
::send(fd, raw_request.data(), raw_request.size(), 0);
std::string out;
char buf[4096];
while (out.size() < caps::kMaxTestBody) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n <= 0) break;
out.append(buf, (std::size_t)n);
}
::close(fd);
return out;
}
} // namespace molo::example::fw
+44
View File
@@ -0,0 +1,44 @@
#pragma once
// Minimal loopback HTTP/1.1 server for manual verification and the real-HTTP
// round-trip test. Sequential accept loop, static caps, no TLS, no HTTP/2.
// Gap: real event loop, timeouts, keep-alive, H2, and compression live in
// 01-runtime.md; this server exists only to prove the stack end to end.
#include <atomic>
#include <memory>
#include <string>
#include <thread>
#include "App.hpp"
namespace molo::example::fw {
class Server {
public:
explicit Server(std::shared_ptr<Application> app);
~Server();
Server(const Server&) = delete;
Server& operator=(const Server&) = delete;
bool start(const std::string& host, int port);
void stop();
int port() const;
bool running() const;
private:
void loop();
void handle_one(int fd);
std::shared_ptr<Application> app_;
int listen_fd_ = -1;
int port_ = 0;
std::atomic<bool> running_{false};
std::thread worker_;
};
std::string http_roundtrip(const std::string& host, int port,
const std::string& raw_request);
} // namespace molo::example::fw
+74
View File
@@ -0,0 +1,74 @@
#include "StaticFiles.hpp"
#include <cstdio>
#include <fstream>
#include <sstream>
namespace molo::example::fw {
bool path_is_safe(const std::string& request_path) {
if (request_path.find("..") != std::string::npos) return false;
if (request_path.find('\\') != std::string::npos) return false;
if (request_path.empty() || request_path.front() != '/') return false;
return true;
}
namespace {
std::string content_type_for(const std::string& name) {
auto ends = [&](const char* ext) {
std::string e(ext);
return name.size() >= e.size() &&
name.compare(name.size() - e.size(), e.size(), e) == 0;
};
if (ends(".html")) return "text/html; charset=utf-8";
if (ends(".css")) return "text/css; charset=utf-8";
if (ends(".js")) return "text/javascript; charset=utf-8";
if (ends(".json")) return "application/json";
if (ends(".svg")) return "image/svg+xml";
if (ends(".png")) return "image/png";
return "application/octet-stream";
}
} // namespace
std::string etag_for(const std::string& body) {
unsigned long long h = 1469598103934665603ULL;
for (unsigned char c : body) {
h ^= c;
h *= 1099511628211ULL;
}
char buf[32];
std::snprintf(buf, sizeof(buf), "\"%llx-%zu\"",
(unsigned long long)h, body.size());
return buf;
}
Response serve_disk_file(const std::string& root,
const std::string& subpath) {
std::string full = "/" + subpath;
if (!path_is_safe(full)) return TextResponse("unsafe path", 403);
std::ifstream in(root + full, std::ios::binary);
if (!in) return TextResponse("not found", 404);
std::ostringstream ss;
ss << in.rdbuf();
Response res(200, ss.str(), content_type_for(full));
res.headers.set("ETag", etag_for(res.body));
res.headers.set("Cache-Control", "public, max-age=3600");
return res;
}
Response serve_embedded(const std::map<std::string, std::string>& assets,
const std::string& subpath) {
std::string full = "/" + subpath;
if (!path_is_safe(full)) return TextResponse("unsafe path", 403);
auto it = assets.find(full);
if (it == assets.end()) return TextResponse("not found", 404);
Response res(200, it->second, content_type_for(full));
res.headers.set("ETag", etag_for(res.body));
res.headers.set("Cache-Control", "public, max-age=3600");
res.headers.set("X-Asset-Source", "embedded");
return res;
}
} // namespace molo::example::fw
+24
View File
@@ -0,0 +1,24 @@
#pragma once
// Static file serving from disk or from embedded C++ assets, with path
// traversal protection, ETags, and cache-control headers.
// Gap: real static serving in 01-runtime.md owns ranges, conditional
// requests, and directory indexing; the embedder CLI lives in 09-cli-ops.md.
#include <map>
#include <string>
#include "Http.hpp"
namespace molo::example::fw {
bool path_is_safe(const std::string& request_path);
Response serve_disk_file(const std::string& root, const std::string& subpath);
Response serve_embedded(const std::map<std::string, std::string>& assets,
const std::string& subpath);
std::string etag_for(const std::string& body);
} // namespace molo::example::fw
+246
View File
@@ -0,0 +1,246 @@
#include "Templates.hpp"
#include <stdexcept>
#include "Caps.hpp"
namespace molo::example::fw {
std::string html_escape(std::string_view in) {
std::string out;
out.reserve(in.size());
for (char c : in) {
switch (c) {
case '&': out += "&amp;"; break;
case '<': out += "&lt;"; break;
case '>': out += "&gt;"; break;
case '"': out += "&quot;"; break;
default: out.push_back(c); break;
}
}
return out;
}
namespace {
std::string trim(std::string s) {
while (!s.empty() && (s.front() == ' ' || s.front() == '\t' ||
s.front() == '\n' || s.front() == '\r'))
s.erase(s.begin());
while (!s.empty() && (s.back() == ' ' || s.back() == '\t' ||
s.back() == '\n' || s.back() == '\r'))
s.pop_back();
return s;
}
std::string apply_filter(std::string value, const std::string& filter) {
if (filter == "upper") {
for (char& c : value) c = (char)std::toupper((unsigned char)c);
} else if (filter == "lower") {
for (char& c : value) c = (char)std::tolower((unsigned char)c);
} else if (filter == "escape") {
value = html_escape(value);
}
return value;
}
std::string ctx_string(const TemplateCtx& ctx, const std::string& key) {
auto it = ctx.find(key);
if (it == ctx.end()) return "";
if (auto* s = std::get_if<std::string>(&it->second)) return *s;
return "";
}
bool ctx_truthy(const TemplateCtx& ctx, const std::string& key) {
auto it = ctx.find(key);
if (it == ctx.end()) return false;
if (auto* s = std::get_if<std::string>(&it->second))
return !s->empty() && *s != "false" && *s != "0";
if (auto* l = std::get_if<TemplateList>(&it->second)) return !l->empty();
return false;
}
} // namespace
void Environment::add_template(std::string name, std::string source) {
templates_[std::move(name)] = std::move(source);
}
void Environment::add_global(std::string key, std::string value) {
globals_[std::move(key)] = std::move(value);
}
bool Environment::has_template(const std::string& name) const {
return templates_.count(name) > 0;
}
std::string Environment::render(const std::string& name, TemplateCtx ctx) const {
auto it = templates_.find(name);
if (it == templates_.end()) throw std::runtime_error("template not found");
for (const auto& [k, v] : globals_) {
if (!ctx.count(k)) ctx[k] = v;
}
return render_source(name, it->second, ctx, 0);
}
std::string Environment::render_source(const std::string& name,
const std::string& src, TemplateCtx& ctx,
int depth) const {
(void)name;
if (depth > caps::kMaxTemplateDepth)
throw std::runtime_error("template depth exceeded");
std::string extends;
std::size_t pos = src.find("{% extends");
if (pos != std::string::npos) {
std::size_t q1 = src.find('"', pos);
std::size_t q2 = q1 == std::string::npos
? std::string::npos
: src.find('"', q1 + 1);
if (q1 != std::string::npos && q2 != std::string::npos)
extends = src.substr(q1 + 1, q2 - q1 - 1);
}
if (!extends.empty()) {
auto it = templates_.find(extends);
if (it == templates_.end()) throw std::runtime_error("base not found");
std::map<std::string, std::string> blocks;
std::size_t b = 0;
while (true) {
std::size_t bs = src.find("{% block", b);
if (bs == std::string::npos) break;
std::size_t be = src.find("%}", bs);
std::size_t cs = src.find("{% endblock %}", be);
if (be == std::string::npos || cs == std::string::npos) break;
std::string head = trim(src.substr(bs + 9, be - bs - 9));
blocks[head] = src.substr(be + 2, cs - be - 2);
b = cs + 14;
}
std::string base = it->second;
for (const auto& [k, v] : blocks) {
std::string open = "{% block " + k + " %}";
std::size_t s = base.find(open);
std::size_t e = s == std::string::npos
? std::string::npos
: base.find("{% endblock %}", s);
if (s != std::string::npos && e != std::string::npos)
base = base.substr(0, s) + v + base.substr(e + 14);
}
return render_blocks(base, ctx, depth);
}
return render_blocks(src, ctx, depth);
}
std::string Environment::render_blocks(const std::string& src, TemplateCtx& ctx,
int depth) const {
std::string out;
std::size_t i = 0;
while (i < src.size()) {
std::size_t v = src.find("{{", i);
std::size_t t = src.find("{%", i);
if (v == std::string::npos && t == std::string::npos) {
out += src.substr(i);
break;
}
if (v != std::string::npos && (t == std::string::npos || v < t)) {
out += src.substr(i, v - i);
std::size_t e = src.find("}}", v);
if (e == std::string::npos) break;
std::string expr = trim(src.substr(v + 2, e - v - 2));
bool safe = false;
std::string var = expr;
std::vector<std::string> filters;
std::size_t p = expr.find('|');
if (p != std::string::npos) {
var = trim(expr.substr(0, p));
std::string rest = expr.substr(p + 1);
std::size_t s = 0;
while (s < rest.size()) {
std::size_t q = rest.find('|', s);
std::string f = trim(rest.substr(s, q == std::string::npos
? q
: q - s));
if (f == "safe") {
safe = true;
} else if (!f.empty()) {
filters.push_back(f);
}
if (q == std::string::npos) break;
s = q + 1;
}
}
std::string value;
std::size_t dot = var.find('.');
if (dot != std::string::npos) {
value = ctx_string(ctx, var);
} else {
value = ctx_string(ctx, var);
}
for (const auto& f : filters) value = apply_filter(value, f);
if (!safe) value = html_escape(value);
out += value;
i = e + 2;
} else {
out += src.substr(i, t - i);
std::size_t e = src.find("%}", t);
if (e == std::string::npos) break;
std::string tag = trim(src.substr(t + 2, e - t - 2));
if (tag.rfind("if ", 0) == 0) {
std::string cond = trim(tag.substr(3));
std::size_t end = src.find("{% endif %}", e);
if (end == std::string::npos) break;
std::string inner = src.substr(e + 2, end - e - 2);
std::size_t els = inner.find("{% else %}");
std::string yes = els == std::string::npos ? inner
: inner.substr(0, els);
std::string no =
els == std::string::npos ? "" : inner.substr(els + 10);
out += render_blocks(ctx_truthy(ctx, cond) ? yes : no, ctx, depth + 1);
i = end + 11;
} else if (tag.rfind("for ", 0) == 0) {
std::string rest = trim(tag.substr(4));
std::size_t in = rest.find(" in ");
std::string item = in == std::string::npos
? ""
: trim(rest.substr(0, in));
std::string list = in == std::string::npos
? ""
: trim(rest.substr(in + 4));
std::size_t end = src.find("{% endfor %}", e);
if (end == std::string::npos) break;
std::string inner = src.substr(e + 2, end - e - 2);
auto it = ctx.find(list);
if (it != ctx.end()) {
if (auto* l = std::get_if<TemplateList>(&it->second)) {
for (const auto& row : *l) {
TemplateCtx sub = ctx;
for (const auto& [k, val] : row) {
sub[item + "." + k] = val;
sub[k] = val;
}
out += render_blocks(inner, sub, depth + 1);
}
}
}
i = end + 12;
} else if (tag.rfind("include ", 0) == 0) {
std::string rest = trim(tag.substr(8));
if (rest.size() >= 2 && rest.front() == '"') {
std::string inc = rest.substr(1, rest.size() - 2);
auto it = templates_.find(inc);
if (it != templates_.end())
out += render_blocks(it->second, ctx, depth + 1);
}
i = e + 2;
} else if (tag.rfind("block ", 0) == 0) {
std::size_t end = src.find("{% endblock %}", e);
if (end == std::string::npos) break;
out += render_blocks(src.substr(e + 2, end - e - 2), ctx, depth + 1);
i = end + 14;
} else {
i = e + 2;
}
}
}
return out;
}
} // namespace molo::example::fw
+39
View File
@@ -0,0 +1,39 @@
#pragma once
// Jinja-like template environment: extends plus blocks, includes, if, for,
// filters, autoescape for .html/.htm/.xml with explicit safe opt-out.
// Gap: real molo::templates in 05-request-routing.md owns the full lexer,
// parser, sandbox, and streamed rendering.
#include <map>
#include <string>
#include <variant>
#include <vector>
namespace molo::example::fw {
using TemplateList = std::vector<std::map<std::string, std::string>>;
using TemplateValue = std::variant<std::string, TemplateList>;
using TemplateCtx = std::map<std::string, TemplateValue>;
class Environment {
public:
void add_template(std::string name, std::string source);
void add_global(std::string key, std::string value);
bool has_template(const std::string& name) const;
std::string render(const std::string& name, TemplateCtx ctx) const;
private:
std::string render_source(const std::string& name, const std::string& src,
TemplateCtx& ctx, int depth) const;
std::string render_blocks(const std::string& src, TemplateCtx& ctx,
int depth) const;
std::map<std::string, std::string> templates_;
std::map<std::string, std::string> globals_;
};
std::string html_escape(std::string_view in);
} // namespace molo::example::fw
+65
View File
@@ -0,0 +1,65 @@
#include "TestClient.hpp"
#include "Caps.hpp"
#include "Http.hpp"
namespace molo::example::fw {
TestClient::TestClient(std::shared_ptr<Application> app)
: app_(std::move(app)) {}
Response TestClient::get(const std::string& path) {
return request(Method::Get, path);
}
Response TestClient::post(const std::string& path, const std::string& body,
const std::string& content_type) {
return request(Method::Post, path, body, {{"Content-Type", content_type}});
}
Response TestClient::request(
Method method, const std::string& path, const std::string& body,
const std::map<std::string, std::string>& headers) {
if (body.size() > caps::kMaxTestBody)
return TextResponse("test body over cap", 413);
Request req;
req.method = method;
std::size_t q = path.find('?');
if (q == std::string::npos) {
req.path = path;
} else {
req.path = path.substr(0, q);
req.query = parse_query(path.substr(q + 1));
}
req.headers.set("User-Agent", "living-example-test/1.0");
for (const auto& [k, v] : headers) req.headers.set(k, v);
if (!jar_.empty()) {
std::string cookie;
for (const auto& [k, v] : jar_) {
if (!cookie.empty()) cookie += "; ";
cookie += k + "=" + v;
}
req.cookies = jar_;
req.headers.set("Cookie", cookie);
}
if (!frozen_time_.empty()) req.headers.set("X-Test-Time", frozen_time_);
req.body = body;
Response res = app_->dispatch(std::move(req));
std::string set_cookie = res.headers.get("Set-Cookie");
if (!set_cookie.empty()) {
std::size_t semi = set_cookie.find(';');
std::string pair = set_cookie.substr(0, semi);
std::size_t eq = pair.find('=');
if (eq != std::string::npos)
jar_[pair.substr(0, eq)] = pair.substr(eq + 1);
}
return res;
}
void TestClient::freeze_time(const std::string& iso) { frozen_time_ = iso; }
const std::string& TestClient::frozen_time() const { return frozen_time_; }
void TestClient::clear_cookies() { jar_.clear(); }
} // namespace molo::example::fw
+38
View File
@@ -0,0 +1,38 @@
#pragma once
// Test client: drives the real Application (routing, middleware, views, DI,
// error mapping, in-memory DB) without sockets, plus a cookie jar and frozen
// time hook. Gap: real test client in 08-clients-openapi.md owns fixtures,
// DB resets, and time freezing.
#include <map>
#include <memory>
#include <string>
#include "App.hpp"
namespace molo::example::fw {
class TestClient {
public:
explicit TestClient(std::shared_ptr<Application> app);
Response get(const std::string& path);
Response post(const std::string& path, const std::string& body = "",
const std::string& content_type = "text/plain");
Response request(Method method, const std::string& path,
const std::string& body = "",
const std::map<std::string, std::string>& headers =
{});
void freeze_time(const std::string& iso);
const std::string& frozen_time() const;
void clear_cookies();
private:
std::shared_ptr<Application> app_;
std::map<std::string, std::string> jar_;
std::string frozen_time_;
};
} // namespace molo::example::fw
+9
View File
@@ -0,0 +1,9 @@
#include "Tls.hpp"
namespace molo::example::fw {
std::string StubTlsContext::name() const { return "tls-stub-no-wolfssl"; }
bool StubTlsContext::available() const { return false; }
} // namespace molo::example::fw
+25
View File
@@ -0,0 +1,25 @@
#pragma once
// TLS posture: framework code depends only on ITlsContext. wolfSSL is NOT
// installed in this environment, so only StubTlsContext exists. It refuses
// handshakes loudly instead of silently serving plaintext as TLS.
// Gap: real wolfSSL-backed context from 03-security.md (TLS 1.3).
#include <string>
namespace molo::example::fw {
class ITlsContext {
public:
virtual ~ITlsContext() = default;
virtual std::string name() const = 0;
virtual bool available() const = 0;
};
class StubTlsContext : public ITlsContext {
public:
std::string name() const override;
bool available() const override;
};
} // namespace molo::example::fw
+189
View File
@@ -0,0 +1,189 @@
#include "Validate.hpp"
#include <cctype>
#include "Caps.hpp"
#include "Http.hpp"
namespace molo::example::fw {
ValidationError::ValidationError(std::vector<FieldError> failures)
: HttpError(422, "validation_error", "validation failed"),
failures_(std::move(failures)) {}
const std::vector<FieldError>& ValidationError::failures() const {
return failures_;
}
std::string ValidationError::to_json() const {
std::string out = "{\"error\":\"validation_error\",\"failures\":[";
bool first = true;
for (const auto& f : failures_) {
if (!first) out += ",";
first = false;
out += "{\"field\":\"" + json_escape(f.field) + "\",\"message\":\"" +
json_escape(f.message) + "\"}";
}
return out + "]}";
}
Model::Model(std::vector<Field> fields) : fields_(std::move(fields)) {}
namespace {
bool looks_like_email(const std::string& v) {
auto at = v.find('@');
if (at == std::string::npos || at == 0) return false;
return v.find('.', at) != std::string::npos;
}
bool parse_long(const std::string& v, long& out) {
if (v.empty()) return false;
std::size_t i = 0;
bool neg = false;
if (v[0] == '-' || v[0] == '+') {
neg = v[0] == '-';
i = 1;
}
if (i >= v.size()) return false;
long acc = 0;
for (; i < v.size(); ++i) {
if (!std::isdigit((unsigned char)v[i])) return false;
acc = acc * 10 + (v[i] - '0');
}
out = neg ? -acc : acc;
return true;
}
} // namespace
std::map<std::string, std::string> Model::validate(
const std::map<std::string, std::string>& input) const {
std::vector<FieldError> failures;
std::map<std::string, std::string> clean;
for (const auto& f : fields_) {
auto it = input.find(f.name);
std::string value = it == input.end() ? "" : it->second;
bool missing = it == input.end() || value.empty();
if (missing) {
if (!f.required) {
if (!f.default_value.empty()) clean[f.name] = f.default_value;
continue;
}
failures.push_back({f.name, "field is required"});
continue;
}
if (f.type != FieldType::Integer && value.size() < f.min_len)
failures.push_back({f.name, "too short"});
else if (f.type != FieldType::Integer && value.size() > f.max_len)
failures.push_back({f.name, "too long"});
else if (f.type == FieldType::Integer) {
long n = 0;
if (!parse_long(value, n)) {
failures.push_back({f.name, "not an integer"});
} else if (f.has_range && (n < f.min_value || n > f.max_value)) {
failures.push_back({f.name, "out of range"});
} else {
clean[f.name] = value;
}
continue;
} else if (f.type == FieldType::Email && !looks_like_email(value)) {
failures.push_back({f.name, "not an email address"});
continue;
} else if (f.type == FieldType::Boolean &&
value != "true" && value != "false" && value != "1" &&
value != "0") {
failures.push_back({f.name, "not a boolean"});
continue;
} else {
clean[f.name] = value;
}
}
if (!failures.empty()) throw ValidationError(std::move(failures));
return clean;
}
const std::vector<Field>& Model::fields() const { return fields_; }
std::map<std::string, std::string> parse_json_object_flat(
const std::string& body) {
std::map<std::string, std::string> out;
std::size_t i = 0;
int depth = 0;
auto skip_ws = [&]() {
while (i < body.size() &&
std::isspace((unsigned char)body[i]))
++i;
};
skip_ws();
if (i >= body.size() || body[i] != '{') return out;
++i;
++depth;
std::size_t fields = 0;
while (i < body.size() && fields < caps::kMaxFormFields) {
skip_ws();
if (i < body.size() && body[i] == '}') break;
if (i >= body.size() || body[i] != '"') break;
++i;
std::string key;
while (i < body.size() && body[i] != '"') {
if (body[i] == '\\' && i + 1 < body.size()) {
key.push_back(body[i + 1]);
i += 2;
} else {
key.push_back(body[i++]);
}
}
if (i < body.size()) ++i;
skip_ws();
if (i >= body.size() || body[i] != ':') break;
++i;
skip_ws();
std::string value;
if (i < body.size() && body[i] == '"') {
++i;
while (i < body.size() && body[i] != '"') {
if (body[i] == '\\' && i + 1 < body.size()) {
value.push_back(body[i + 1]);
i += 2;
} else {
value.push_back(body[i++]);
}
}
if (i < body.size()) ++i;
} else {
while (i < body.size() && body[i] != ',' && body[i] != '}') {
if (body[i] == '{' || body[i] == '[') {
if (++depth > caps::kMaxJsonDepth) return out;
}
value.push_back(body[i++]);
}
while (!value.empty() &&
std::isspace((unsigned char)value.back()))
value.pop_back();
}
out[key] = value;
++fields;
skip_ws();
if (i < body.size() && body[i] == ',') ++i;
}
return out;
}
Form::Form(Model model) : model_(std::move(model)) {}
std::map<std::string, std::string> Form::bind(const Request& req) const {
std::string ct = req.headers.get("Content-Type");
std::map<std::string, std::string> input;
if (ct.find("application/json") != std::string::npos) {
input = parse_json_object_flat(req.body);
} else {
input = parse_form(req.body);
for (const auto& [k, v] : req.query) {
if (!input.count(k)) input[k] = v;
}
}
return model_.validate(input);
}
} // namespace molo::example::fw
+72
View File
@@ -0,0 +1,72 @@
#pragma once
// Single validation framework: field descriptors plus whole-model validation
// that reports ALL failures in one error. Forms bind JSON and urlencoded
// bodies into the same models. Gap: real molo::validate and molo::forms in
// 05-request-routing.md own coercions, nested models, and OpenAPI schemas.
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "App.hpp"
namespace molo::example::fw {
enum class FieldType { String, Integer, Boolean, Email };
struct Field {
std::string name;
FieldType type = FieldType::String;
bool required = true;
std::size_t min_len = 0;
std::size_t max_len = 1024;
long min_value = 0;
long max_value = 0;
bool has_range = false;
std::string default_value;
};
struct FieldError {
std::string field;
std::string message;
};
class ValidationError : public HttpError {
public:
explicit ValidationError(std::vector<FieldError> failures);
const std::vector<FieldError>& failures() const;
std::string to_json() const;
private:
std::vector<FieldError> failures_;
};
class Model {
public:
explicit Model(std::vector<Field> fields);
std::map<std::string, std::string> validate(
const std::map<std::string, std::string>& input) const;
const std::vector<Field>& fields() const;
private:
std::vector<Field> fields_;
};
std::map<std::string, std::string> parse_json_object_flat(
const std::string& body);
class Form {
public:
explicit Form(Model model);
std::map<std::string, std::string> bind(const Request& req) const;
private:
Model model_;
};
} // namespace molo::example::fw
+26
View File
@@ -0,0 +1,26 @@
#include "View.hpp"
namespace molo::example::fw {
View::View(Request req) : req_(std::move(req)) {}
Response View::dispatch() {
switch (req_.method) {
case Method::Get: return get();
case Method::Post: return post();
case Method::Put: return put();
case Method::Patch: return patch();
case Method::Delete: return del();
case Method::Options: return options();
default: return TextResponse("method not allowed", 405);
}
}
Response View::get() { return TextResponse("method not allowed", 405); }
Response View::post() { return TextResponse("method not allowed", 405); }
Response View::put() { return TextResponse("method not allowed", 405); }
Response View::patch() { return TextResponse("method not allowed", 405); }
Response View::del() { return TextResponse("method not allowed", 405); }
Response View::options() { return TextResponse("method not allowed", 405); }
} // namespace molo::example::fw
+29
View File
@@ -0,0 +1,29 @@
#pragma once
// Class-based views, aiohttp style: constructed with the Request, one virtual
// coroutine-equivalent per HTTP method, dispatch selects by method.
// Gap: real molo::views::View in 05-request-routing.md is coroutine based.
#include "Http.hpp"
namespace molo::example::fw {
class View {
public:
explicit View(Request req);
virtual ~View() = default;
Response dispatch();
virtual Response get();
virtual Response post();
virtual Response put();
virtual Response patch();
virtual Response del();
virtual Response options();
protected:
Request req_;
};
} // namespace molo::example::fw
+143
View File
@@ -0,0 +1,143 @@
#include "Workflow.hpp"
#include <cstdio>
#include "Caps.hpp"
namespace molo::example::fw {
bool parse_commit(const std::string& subject, const std::string& body,
CommitInfo& out, std::string& error) {
std::string s = subject;
if (s.size() > (std::size_t)caps::kMaxCommitSubject) {
error = "subject too long";
return false;
}
std::size_t colon = s.find(':');
if (colon == std::string::npos) {
error = "missing colon";
return false;
}
std::string head = s.substr(0, colon);
out.description = s.substr(colon + 1);
while (!out.description.empty() && out.description.front() == ' ')
out.description.erase(out.description.begin());
if (!head.empty() && head.back() == '!') {
out.breaking = true;
head.pop_back();
}
std::size_t paren = head.find('(');
if (paren == std::string::npos) {
out.type = head;
} else {
out.type = head.substr(0, paren);
std::size_t close = head.find(')', paren);
if (close == std::string::npos) {
error = "unclosed scope";
return false;
}
out.scope = head.substr(paren + 1, close - paren - 1);
}
const char* allowed[] = {"feat", "fix", "docs", "style", "refactor",
"perf", "test", "build", "ci", "chore"};
bool ok = false;
for (const char* t : allowed) {
if (out.type == t) {
ok = true;
break;
}
}
if (!ok) {
error = "unknown type: " + out.type;
return false;
}
if (body.find("BREAKING CHANGE") != std::string::npos) out.breaking = true;
return true;
}
std::string bump_name(Bump bump) {
switch (bump) {
case Bump::None: return "none";
case Bump::Patch: return "patch";
case Bump::Minor: return "minor";
case Bump::Major: return "major";
}
return "none";
}
Bump ReleasePlanner::decide(const std::vector<CommitInfo>& commits) const {
bool feat = false, fix = false;
for (const auto& c : commits) {
if (c.breaking) return Bump::Major;
if (c.type == "feat") feat = true;
if (c.type == "fix") fix = true;
}
if (feat) return Bump::Minor;
if (fix) return Bump::Patch;
return Bump::None;
}
std::string ReleasePlanner::next_version(
const std::string& current,
const std::vector<CommitInfo>& commits) const {
int major = 0, minor = 0, patch = 0;
std::sscanf(current.c_str(), "%d.%d.%d", &major, &minor, &patch);
switch (decide(commits)) {
case Bump::Major: return std::to_string(major + 1) + ".0.0";
case Bump::Minor:
return std::to_string(major) + "." + std::to_string(minor + 1) + ".0";
case Bump::Patch:
return std::to_string(major) + "." + std::to_string(minor) + "." +
std::to_string(patch + 1);
case Bump::None: return current;
}
return current;
}
std::string ChangelogWriter::entry(
const std::string& version, const std::string& date,
const std::vector<CommitInfo>& commits) const {
std::string added, changed, fixed, removed;
for (const auto& c : commits) {
std::string line =
"- " + (c.scope.empty() ? "" : "(" + c.scope + ") ") + c.description +
"\n";
if (c.breaking)
changed += line;
else if (c.type == "feat")
added += line;
else if (c.type == "fix")
fixed += line;
else
changed += line;
}
std::string out = "## [" + version + "] - " + date + "\n";
if (!added.empty()) out += "### Added\n" + added;
if (!changed.empty()) out += "### Changed\n" + changed;
if (!fixed.empty()) out += "### Fixed\n" + fixed;
if (!removed.empty()) out += "### Removed\n" + removed;
int lines = 0;
for (char c : out) {
if (c == '\n') ++lines;
}
if (lines > caps::kMaxChangelogLines)
out += "(truncated to cap)\n";
return out;
}
void ReleaseVerifier::add_check(std::string name, bool passed,
std::string detail) {
checks_.push_back({std::move(name), passed, std::move(detail)});
}
bool ReleaseVerifier::green() const {
if (checks_.empty()) return false;
for (const auto& c : checks_) {
if (!c.passed) return false;
}
return true;
}
std::vector<VerifyCheck> ReleaseVerifier::checks() const { return checks_; }
} // namespace molo::example::fw
+57
View File
@@ -0,0 +1,57 @@
#pragma once
// Autonomous release pipeline: commits since tag to bump decision, changelog
// writing, and pre-tag verification. Dry-run mode never writes.
// Final home is tools/release/ under namespace molo::workflow; this example
// copy proves the rules and is marked as a demo, not the pipeline itself.
// Gap: real molo::workflow in tools/release/ plus CLI surface in 09-cli-ops.md.
#include <string>
#include <vector>
namespace molo::example::fw {
struct CommitInfo {
std::string type;
std::string scope;
std::string description;
bool breaking = false;
};
bool parse_commit(const std::string& subject, const std::string& body,
CommitInfo& out, std::string& error);
enum class Bump { None, Patch, Minor, Major };
std::string bump_name(Bump bump);
class ReleasePlanner {
public:
Bump decide(const std::vector<CommitInfo>& commits) const;
std::string next_version(const std::string& current,
const std::vector<CommitInfo>& commits) const;
};
class ChangelogWriter {
public:
std::string entry(const std::string& version, const std::string& date,
const std::vector<CommitInfo>& commits) const;
};
struct VerifyCheck {
std::string name;
bool passed = false;
std::string detail;
};
class ReleaseVerifier {
public:
void add_check(std::string name, bool passed, std::string detail = "");
bool green() const;
std::vector<VerifyCheck> checks() const;
private:
std::vector<VerifyCheck> checks_;
};
} // namespace molo::example::fw