Your First Rule
This guide writes a new rule, JsonSerializer, that generates a toJson() function for each annotated struct. It covers the three script files every rule needs and the config that ties them together.
Given:
struct [[codegen::JsonSerializer]] AppConfig { std::string host; uint16_t port; bool enableTls;};Produce:
#include <nlohmann/json.hpp>
nlohmann::json toJson(const AppConfig& v) { return { {"host", v.host}, {"port", v.port}, {"enableTls", v.enableTls}, };}Rule file layout
Section titled “Rule file layout”Directory.codegen/rules/JsonSerializer/
- JsonSerializer.config.yaml
- JsonSerializer.preamble.luau
- JsonSerializer.luau
No grouping.luau, this rule uses 1:1 routing (one struct ☛ one .g.cpp file).
Step 1: config
Section titled “Step 1: config”version: 1
output: language: cpp autoDeduceIncludes: falseautoDeduceIncludes: false means the engine will not attempt to infer #include lines. The preamble handles that explicitly.
Step 2: preamble
Section titled “Step 2: preamble”return function(input) return "#include <nlohmann/json.hpp>\n\n"endThe preamble runs once per output file. With 1:1 routing (no grouping script), “once per output file” means once per input header, which is what you want.
Step 3: transformation
Section titled “Step 3: transformation”return function(input) local node = json.decode(input) if not node then error("failed to decode input") end
local structName = node.identifier and node.identifier.name if not structName then error("struct has no name") end
-- Collect field initializer list entries local entries = {} for _, varNode in ipairs(node.memberVariables or {}) do if varNode.kind == "Variable" then local fname = varNode.identifier and varNode.identifier.name if fname then table.insert(entries, ' {"' .. fname .. '", v.' .. fname .. '},') end end end
local body = table.concat(entries, "\n")
local impl = "nlohmann::json toJson(const " .. structName .. "& v) {\n" .. " return {\n" .. body .. "\n" .. " };\n" .. "}"
local decl = "nlohmann::json toJson(const " .. structName .. "& v);"
return json.encode({ source = impl, inline = { { source = decl } } })endThe inline list injects decl at the anchor site in the original header, keeping the declaration in the header and the implementation in the .g.cpp file.
Step 4: run
Section titled “Step 4: run”codegen \ --config .codegen/rules/JsonSerializer/JsonSerializer.config.yaml \ --input ./include \ --output ./generatedKey Takeaways
- Every rule has three files:
config.yaml,preamble.luau, and the transformation.luauscript. - The transformation script receives one AST node as JSON and returns
{ source, inline }. sourcegoes to the.g.cppfile.inlineitems are injected at anchor sites in the original header.- No grouping script = 1:1 routing: one annotated entity ☛ one output file derived from the input path.
- The LuaU sandbox provides only
json.encodeandjson.decode. All logic is explicit and auditable.