Skip to content

Creating a Custom Policy

A policy is a struct with static methods that return CrateRule objects. TypeRouter discovers these methods at compile time and generates the corresponding operations.

struct XmlApiPolicy(PolicyConfig config = PolicyConfig.init) {
enum policyConfig = config;
static immutable {
string name = "XML API";
string mime = "application/xml";
}
alias Serializer = XmlSerializer;
alias Routing = XmlRouting;
static:
private CrateRule templateRule(const ModelDescription definition) {
auto serializer = new Serializer(definition);
CrateRule rule;
rule.request.serializer = serializer;
rule.request.requiredHeaders = ["Accept: " ~ mime];
rule.response.serializer = serializer;
rule.schemas = serializer.schemas;
return rule;
}
CrateRule getList(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.getList;
rule.request.method = HTTPMethod.GET;
rule.response.statusCode = 200;
return rule;
}
CrateRule getItem(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.getItem;
rule.request.method = HTTPMethod.GET;
rule.response.statusCode = 200;
return rule;
}
CrateRule createItem(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.createItem;
rule.request.method = HTTPMethod.POST;
rule.response.statusCode = 201;
return rule;
}
CrateRule updateItem(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.updateItem;
rule.request.method = HTTPMethod.PATCH;
rule.response.statusCode = 200;
return rule;
}
CrateRule replaceItem(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.replaceItem;
rule.request.method = HTTPMethod.PUT;
rule.response.statusCode = 200;
return rule;
}
CrateRule deleteItem(const ModelDescription definition) {
auto routing = new Routing(definition);
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ routing.deleteItem;
rule.request.method = HTTPMethod.DELETE;
rule.response.statusCode = 204;
return rule;
}
}

The struct needs:

MemberPurpose
policyConfigEnum storing the PolicyConfig template parameter
nameHuman-readable protocol name
mimeDefault MIME type
SerializerAlias to your ModelSerializer implementation
RoutingAlias to your routing helper
templateRule()Private helper that sets common defaults for all rules

Each static method returning CrateRule becomes an operation. TypeRouter discovers them via compile-time reflection.

Implement the ModelSerializer interface:

class XmlSerializer : ModelSerializer {
private ModelDescription _definition;
this(const ModelDescription definition) pure {
_definition = definition.dup;
}
@safe:
ModelDescription definition() { return _definition; }
InputRange!string toResponseList(InputRange!Json data, OperationContext ctx) @trusted {
// Convert Json items to XML string chunks
}
InputRange!string toResponseItem(Json data, OperationContext ctx) @trusted {
// Convert single Json item to XML string
}
Json fromRequest(string id, Json data) @trusted {
// Parse incoming request body into internal Json
}
string respondItemSchemaKey() { return _definition.singular ~ "XmlResponse"; }
string respondListSchemaKey() { return _definition.plural ~ "XmlResponse"; }
string itemKey() { return _definition.singular; }
Schema[string] schemas() { /* OpenAPI schemas */ }
}

The serializer sits between the database layer (which works with Json) and the HTTP response. toResponseList and toResponseItem return InputRange!string for streaming — chunks are written to the response as they’re produced.

A simple helper that generates URL paths from model metadata:

class XmlRouting {
private string plural;
this(const ModelDescription definition) {
this.plural = definition.plural;
}
@property string getList() { return "/" ~ plural; }
@property string getItem() { return "/" ~ plural ~ "/:id"; }
@property string createItem() { return "/" ~ plural; }
@property string updateItem() { return "/" ~ plural ~ "/:id"; }
@property string replaceItem() { return "/" ~ plural ~ "/:id"; }
@property string deleteItem() { return "/" ~ plural ~ "/:id"; }
}

Optionally define hooks for protocol-level setup:

struct XmlApiPolicy(PolicyConfig config = PolicyConfig.init) {
// ... fields and methods ...
static void onRouterInit(ITypeRouter router) {
// Called once when the first model is added
// Register protocol-level endpoints (schema discovery, etc.)
}
static void onRouterReady(IRouterContext router) {
// Called lazily on the first HTTP request
// Finalize configuration after all models are registered
}
}

CrateRouter checks if these methods exist at compile time and only calls them if defined.

For protocols like MCP and GraphQL that use a single endpoint, set bodyMatcher on the request:

CrateRule getItem(const ModelDescription definition) {
auto rule = templateRule(definition);
rule.request.path = config.baseUrl ~ "/xml-rpc";
rule.request.method = HTTPMethod.POST;
rule.request.bodyMatcher = Json(["action": Json("getItem"),
"model": Json(definition.singular)]);
return rule;
}

The router matches incoming request bodies against bodyMatcher to dispatch to the correct operation.

auto crateRouter = router.crateSetup!(XmlApiPolicy!());
crateRouter.add(productCrate);
// Or combine with built-in policies:
auto crateRouter = router.crateSetup!(RestApi, XmlApiPolicy!());
crateRouter.add(productCrate);