rexglue-skate3/src/codegen/template_registry.cpp
2026-05-18 22:41:13 +01:00

205 lines
7.3 KiB
C++

/**
* @file codegen/template_registry.cpp
* @brief Template registry implementation
*
* @copyright Copyright (c) 2026 Tom Clay <tomc@tctechstuff.com>
* @license BSD 3-Clause License
*/
#include <rex/codegen/template_registry.h>
#include "template_registry_internal.h"
#include <inja/inja.hpp>
#include <rex/logging.h>
#include "codegen_logging.h"
// Generated at build time by cmake/embed_templates.cmake
#include "embedded_templates.h"
#include <algorithm>
#include <unordered_map>
namespace rex::codegen {
// ---------------------------------------------------------------------------
// TemplateError
// ---------------------------------------------------------------------------
TemplateError::TemplateError(const std::string& templateId, const std::string& message,
const std::string& source)
: std::runtime_error("[" + source + "] template '" + templateId + "': " + message),
templateId_(templateId),
source_(source) {}
// ---------------------------------------------------------------------------
// TemplateRegistry::Impl
// ---------------------------------------------------------------------------
struct TemplateRegistry::Impl {
inja::Environment env_;
std::unordered_map<std::string, std::string_view> embedded_;
std::unordered_map<std::string, inja::Template> overrides_;
std::unordered_map<std::string, inja::Template> parsedCache_;
Impl() {
embedded_ = embeddedTemplates();
// cmake_var callback: wraps a variable name in ${ }
env_.add_callback("cmake_var", 1, [](inja::Arguments& args) {
return "${" + args.at(0)->get<std::string>() + "}";
});
// hex callback: format an integer as 0x-prefixed hex
env_.add_callback("hex", 1, [](inja::Arguments& args) {
auto val = args.at(0)->get<uint64_t>();
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase << val;
return oss.str();
});
// Resolve {% include "<id>" %} against the embedded registry. Templates
// are embedded under canonical IDs without the .inja extension; accept
// either form so include directives can use either.
env_.set_search_included_templates_in_files(false);
env_.set_include_callback(
[this](const std::filesystem::path&, const std::string& name) -> inja::Template {
std::string id = name;
if (id.size() > 5 && id.compare(id.size() - 5, 5, ".inja") == 0) {
id.erase(id.size() - 5);
}
auto it = embedded_.find(id);
if (it == embedded_.end()) {
throw TemplateError(name, "include not found in embedded registry");
}
return env_.parse(std::string(it->second));
});
}
std::string renderImpl(const std::string& id, const nlohmann::json& data) {
// Check overrides first
auto ovIt = overrides_.find(id);
if (ovIt != overrides_.end()) {
return env_.render(ovIt->second, data);
}
// Check parsed cache
auto cacheIt = parsedCache_.find(id);
if (cacheIt != parsedCache_.end()) {
return env_.render(cacheIt->second, data);
}
// Look up in embedded templates
auto embIt = embedded_.find(id);
if (embIt == embedded_.end()) {
throw TemplateError(id, "no template registered with this ID");
}
// Parse, cache, and render
auto tmpl = env_.parse(std::string(embIt->second));
auto [it, _] = parsedCache_.emplace(id, std::move(tmpl));
return env_.render(it->second, data);
}
};
// ---------------------------------------------------------------------------
// TemplateRegistry special members
// ---------------------------------------------------------------------------
TemplateRegistry::TemplateRegistry() : impl_(std::make_unique<Impl>()) {}
TemplateRegistry::~TemplateRegistry() = default;
TemplateRegistry::TemplateRegistry(TemplateRegistry&&) noexcept = default;
TemplateRegistry& TemplateRegistry::operator=(TemplateRegistry&&) noexcept = default;
// ---------------------------------------------------------------------------
// TemplateRegistry public methods
// ---------------------------------------------------------------------------
void TemplateRegistry::loadOverrides(const std::filesystem::path& dir) {
if (!std::filesystem::exists(dir)) {
REXCODEGEN_WARN("Override directory does not exist: {}", dir.string());
return;
}
for (auto const& entry : std::filesystem::recursive_directory_iterator(dir)) {
if (!entry.is_regular_file())
continue;
if (entry.path().extension() != ".inja")
continue;
// Compute canonical ID from relative path, stripping .inja extension
auto relPath = std::filesystem::relative(entry.path(), dir).string();
// Normalize path separators to forward slash
std::replace(relPath.begin(), relPath.end(), '\\', '/');
// Strip .inja extension
auto id = relPath.substr(0, relPath.size() - 5); // strlen(".inja") == 5
if (impl_->embedded_.find(id) == impl_->embedded_.end()) {
REXCODEGEN_WARN("Override file does not match a known template ID: {} (from {})", id,
entry.path().string());
continue;
}
try {
auto content = [&]() {
std::ifstream ifs(entry.path());
return std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}();
auto tmpl = impl_->env_.parse(content);
impl_->overrides_[id] = std::move(tmpl);
REXCODEGEN_TRACE("Loaded template override: {} -> {}", id, entry.path().string());
} catch (const std::exception& e) {
throw TemplateError(id, e.what(), entry.path().string());
}
}
}
std::string TemplateRegistry::render(const std::string& id, const std::string& jsonData) {
try {
auto data = nlohmann::json::parse(jsonData);
return impl_->renderImpl(id, data);
} catch (const TemplateError&) {
throw; // Already wrapped
} catch (const nlohmann::json::exception& e) {
throw TemplateError(id, std::string("JSON parse error: ") + e.what());
} catch (const inja::InjaError& e) {
throw TemplateError(id, e.what());
} catch (const std::exception& e) {
throw TemplateError(id, e.what());
}
}
std::string TemplateRegistry::renderString(const std::string& templateContent,
const std::string& jsonData) {
try {
auto data = nlohmann::json::parse(jsonData);
auto tmpl = impl_->env_.parse(templateContent);
return impl_->env_.render(tmpl, data);
} catch (const nlohmann::json::exception& e) {
throw TemplateError("<inline>", std::string("JSON parse error: ") + e.what(), "string");
} catch (const inja::InjaError& e) {
throw TemplateError("<inline>", e.what(), "string");
} catch (const std::exception& e) {
throw TemplateError("<inline>", e.what(), "string");
}
}
std::vector<std::string> TemplateRegistry::registeredIds() const {
std::vector<std::string> ids;
ids.reserve(impl_->embedded_.size());
for (const auto& [id, _] : impl_->embedded_) {
ids.push_back(id);
}
return ids;
}
// ---------------------------------------------------------------------------
// Free function: renderWithJson (internal API)
// ---------------------------------------------------------------------------
std::string renderWithJson(TemplateRegistry& registry, const std::string& id,
const nlohmann::json& data) {
return registry.render(id, data.dump());
}
} // namespace rex::codegen