Compile-Time Reflection
Overview
Section titled “Overview”Crate makes heavy use of D’s compile-time features to generate routing code with zero runtime overhead. This page explains the key patterns.
__traits(allMembers) — Introspecting Model Fields
Section titled “__traits(allMembers) — Introspecting Model Fields”D’s __traits(allMembers, Type) returns a compile-time tuple of all member names in a type. Crate uses this to discover action methods on model structs:
static foreach (member; __traits(allMembers, Type)) { static if (!IsEnumType!(Type, member)) { static if (isAction!(typeof(__traits(getMember, Type, member)), member)) {{ typeRouter.expose!member; }} }}This loop runs at compile time. For each member of the model struct:
- Skip enum members (
IsEnumTypefilter) - Check if the member is callable and qualifies as an action (
isAction) - If yes, generate an expose call
The double braces {{ }} create a separate scope for each iteration, preventing variable name collisions in the generated code.
static foreach with Policies
Section titled “static foreach with Policies”When CrateRouter has multiple policies, static foreach generates code for each one:
static foreach (i, Policy; Policies) {{ auto typeRouter = new TypeRouter!(Policy, Type)(crate); this.types ~= typeRouter;
allRouters ~= typeRouter.blankRouter; static if (i == 0) { firstRouter = typeRouter; }}}For CrateRouter!(RestApi, Mcp), the compiler generates two iterations:
i=0, Policy=RestApi: Creates aTypeRouter!(RestApi, Type)i=1, Policy=Mcp: Creates aTypeRouter!(Mcp, Type)
The static if (i == 0) captures only the first router into firstRouter.
Conditional Return Types
Section titled “Conditional Return Types”The return type of prepare() and prepareFor() depends on how many policies are selected:
static if (SelectedPolicies.length == 1) { return firstRouter; // Returns TypeRouter!(Policy, Type)} else { return allRouters; // Returns BlankRouter[]}D’s auto return type deduction handles this — the function has two possible return types, but only one branch compiles based on the template parameters. The caller sees either TypeRouter or BlankRouter[].
__traits(compiles) — Feature Detection
Section titled “__traits(compiles) — Feature Detection”Crate uses __traits(compiles, expression) to check if code is valid at compile time, enabling optional features:
// Only call onRouterInit if the policy defines itstatic foreach (Policy; Policies) { static if (__traits(compiles, Policy.onRouterInit(policyRouter))) { Policy.onRouterInit(policyRouter); }}This is D’s version of duck typing at compile time. If a policy doesn’t define onRouterInit, the static if branch is simply not generated.
describe!Policy — Compile-Time Introspection
Section titled “describe!Policy — Compile-Time Introspection”TypeRouter uses the described library to introspect policy structs:
enum policyDescription = describe!Policy;
static foreach (method; policyDescription.methods) { static if (method.returns.name == "CrateRule") { // Generate operation for this method }}describe!Policy returns a compile-time description of the policy’s methods, parameters, and return types. This is used to auto-discover which CRUD operations a policy supports.
String Mixins — Code Generation
Section titled “String Mixins — Code Generation”TypeRouter generates operation instantiation code as strings and compiles them:
enum opName = operationClassFromMethod(method.name);// "getList" → "GetListApiOperation"
enum newExprTyped = `new ` ~ opName ~ `!Type(crate, Policy.` ~ method.name ~ `(definition))`;// Generates: new GetListApiOperation!User(crate, RestApi.getList(definition))
static if (__traits(compiles, mixin(newExprTyped))) { this.blankRouter.operations ~= mixin(newExprTyped);}The mixin() function compiles a string into actual D code at compile time. Combined with __traits(compiles), it tries the typed version first and falls back to untyped.
Nested Template Pattern
Section titled “Nested Template Pattern”prepareFor uses D’s nested template pattern to separate explicit and inferred template parameters:
template prepareFor(SelectedPolicies...) { auto prepareFor(Type)(Crate!Type crate) { // SelectedPolicies are explicit, Type is inferred from crate }}Called as: crateRouter.prepareFor!(RestApi)(userCrate)
The outer template prepareFor!(RestApi) binds SelectedPolicies. The inner function template infers Type from the Crate!Type argument. This avoids ambiguity with D’s template resolution.
staticIndexOf — Compile-Time Validation
Section titled “staticIndexOf — Compile-Time Validation”prepareFor validates that selected policies exist in the router:
static foreach (SP; SelectedPolicies) { static assert(staticIndexOf!(SP, Policies) != -1, SP.stringof ~ " is not configured on this router");}staticIndexOf!(SP, Policies) searches the Policies tuple for SP at compile time. If not found (returns -1), compilation fails with a clear error message.
collectRequiredGetters — Relation Discovery
Section titled “collectRequiredGetters — Relation Discovery”The collectRequiredGetters!T template walks a model’s fields at compile time to find all referenced model types:
string[] collectRequiredGetters(T)() { enum description = describeModel!T; return collectFromFields(description.fields);}For a model like:
struct Campaign { string _id; Team team; // References Team model Picture cover; // References Picture model}It returns ["Team", "Picture"] at compile time. CrateRouter uses this to validate that all required crateGetters are registered before the first request.
IsEnumType — Member Filtering
Section titled “IsEnumType — Member Filtering”A template that checks whether a member is an enum value (not an enum type):
template IsEnumType(T, string name) { static if (__traits(compiles, IsEnumType!(__traits(getMember, T, name)))) { alias IsEnumType = IsEnumType!(__traits(getMember, T, name)); } else { enum IsEnumType = false; }}This uses multiple template specializations with constraints (if(is(T == enum))) to handle different type categories. It prevents enum members from being treated as action methods during model introspection.
Performance
Section titled “Performance”All compile-time reflection happens during compilation. The generated code is identical to hand-written code — no runtime overhead, no reflection cost, no dynamic dispatch for routing decisions.
The trade-off is compile time: heavily templated D code can be slow to compile. Crate mitigates this by caching intermediate results in enum values.