Files
JetCore/jetc/jetc.cpp
T
Brad ArantandClaude Sonnet 5 634eb7a9a9 jetc: add #api directive + jetc_api_<name> route metadata export
Stage 1 of the APIServer plan: a compiled --shared .so currently exports
only a bare jetc_render_<name>(Global&, MString&) with no way for a host
to discover what HTTP method+path it wants to serve. Adds a leading
"#api METHOD /path" directive (one or more, parsed the same way jetc
already strips a leading "#!" shebang line - not modeled as a new
AST/tag-attribute concept, since jetc's parser has no existing notion of
the enclosing <jet> tag's attributes to hang this off of).

In --shared mode, JetCodegen now also emits a second exported function
alongside the render entry point:

  struct JetApiRoute { const char *method; const char *path; };
  extern "C" const JetApiRoute *jetc_api_<name>(int *count) { ... }

<name> is derived the same way for both symbols (sanitizeIdent(
baseNameNoExt(inputPath))), so a future host loader can find both by
convention from just the .so's filename - no name needs to be embedded
in the metadata itself, and the routes a module declares can never drift
out of sync with the render function that implements them (the failure
mode a sidecar manifest file would have).

Malformed directives (missing method/path, path without a leading '/')
are a hard compile error; #api directives present without --shared print
a non-fatal warning (the metadata has nowhere to go without a host to
load a .so, but the render-only executable/CGI path still works fine).

Verified live, not just compiled: rebuilt jetc, compiled demo3.jet (a new
worked example with two #api lines sharing one handler) via
build-shared.sh into a real demo3.so, and added api_test.cpp (mirrors
host_test.cpp's dlopen()/dlsym() pattern) confirming jetc_api_demo3
returns the expected two routes at runtime. Re-ran host_test against the
same .so to confirm the render path is unaffected. Confirmed demo.jet/
demo2.jet (no #api lines) emit no JetApiRoute code at all - no regression
for templates that don't declare routes.

Also fixed a gitignore gap surfaced while committing this: an earlier
"jetc/*.cpp" ignore rule (meant for generated *.compiled.cpp output) was
a blanket pattern that silently hid this commit's own api_test.cpp from
git status. Narrowed to "jetc/*.compiled.cpp" (the actual generated-file
naming, per build-compiled.sh/build-shared.sh) and added explicit ignores
for the host_test/api_test/demo3 binaries plus two known stale generated
leftovers (demo.cpp/demo2.cpp) that predate this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:24:10 -07:00

119 lines
4.5 KiB
C++

#include "JetParser.h"
#include "JetCodegen.h"
#include <fstream>
#include <sstream>
#include <iostream>
#include <cctype>
#include <vector>
namespace {
std::string baseNameNoExt(const std::string &path) {
size_t slash = path.find_last_of("/\\");
std::string base = (slash == std::string::npos) ? path : path.substr(slash + 1);
size_t dot = base.find_last_of('.');
if (dot != std::string::npos) base = base.substr(0, dot);
return base;
}
std::string sanitizeIdent(const std::string &s) {
std::string r;
for (char c : s) r += (std::isalnum((unsigned char)c) || c == '_') ? c : '_';
return r;
}
}
int main(int argc, char **argv) {
bool shared = false;
std::vector<std::string> pos;
for (int i = 1; i < argc; ++i) {
std::string a = argv[i];
if (a == "--shared") shared = true;
else pos.push_back(a);
}
if (pos.size() < 2) {
std::cerr << "usage: jetc [--shared] <input.jet> <output.cpp>" << std::endl;
std::cerr << " --shared emit a dlsym()-able entry point (see build-shared.sh)" << std::endl;
std::cerr << " instead of a standalone main()." << std::endl;
return 1;
}
const std::string &inputPath = pos[0];
const std::string &outputPath = pos[1];
std::ifstream in(inputPath);
if (!in) {
std::cerr << "jetc: cannot open " << inputPath << std::endl;
return 1;
}
std::ostringstream ss;
ss << in.rdbuf();
std::string src = ss.str();
// Skip a leading shebang line, mirroring jet-2.0.cpp's own data.goeol()
// call (run before it constructs the top-level __jet tag) so a
// "#!../jet-2.0" first line doesn't get compiled in as literal output.
if (src.rfind("#!", 0) == 0) {
size_t nl = src.find('\n');
src = (nl == std::string::npos) ? std::string() : src.substr(nl + 1);
}
// Consume zero or more leading "#api METHOD /path" directive lines
// (right after the optional shebang), the same way the shebang itself
// is stripped - a plain directive line, not a new tag/attribute concept,
// since jetc's parser has no existing notion of the enclosing <jet> tag's
// attributes to hang this off of. Each becomes one route this template
// wants to serve once loaded as a .so (see JetCodegen's apiRoutes).
std::vector<jetc::ApiRoute> apiRoutes;
while (src.rfind("#api ", 0) == 0) {
size_t nl = src.find('\n');
std::string line = (nl == std::string::npos) ? src : src.substr(0, nl);
src = (nl == std::string::npos) ? std::string() : src.substr(nl + 1);
std::istringstream ls(line);
std::string tag, method, path;
ls >> tag >> method >> path;
if (method.empty() || path.empty() || path[0] != '/') {
std::cerr << "jetc: malformed #api directive (expected \"#api METHOD /path\"): "
<< line << std::endl;
return 1;
}
for (auto &c : method) c = (char)std::toupper((unsigned char)c);
apiRoutes.push_back({method, path});
}
try {
jetc::Parser parser(src);
auto nodes = parser.parseNodes();
jetc::Codegen codegen;
// Both symbols are derived from the input filename (not the output
// path) so they stay stable and predictable for whatever builds and
// dlsym()'s the resulting .so - unique per template, so multiple
// compiled templates' .so's can coexist in one process even under
// dlopen(..., RTLD_GLOBAL) instead of requiring isolated handles.
std::string baseIdent = sanitizeIdent(baseNameNoExt(inputPath));
std::string symbol = "jetc_render_" + baseIdent;
std::string apiSymbol = "jetc_api_" + baseIdent;
if (!apiRoutes.empty() && !shared)
std::cerr << "jetc: warning: #api directive(s) present but --shared not specified - "
"route metadata will not be emitted" << std::endl;
std::string cpp = codegen.generate(nodes, std::string("compiled from ") + inputPath, shared,
symbol, apiRoutes, apiSymbol);
std::ofstream outFile(outputPath);
outFile << cpp;
outFile.close();
if (shared) {
std::cout << "jetc: wrote " << outputPath << " (entry symbol: " << symbol << ")";
if (!apiRoutes.empty())
std::cout << " (api symbol: " << apiSymbol << ", " << apiRoutes.size() << " route(s))";
std::cout << std::endl;
} else {
std::cout << "jetc: wrote " << outputPath << std::endl;
}
} catch (std::exception &e) {
std::cerr << "jetc: " << e.what() << std::endl;
return 1;
}
return 0;
}