pi/validation/endpoints.nix
2026-04-08 11:41:12 +02:00

205 lines
10 KiB
Nix

let
lib = import <nixpkgs/lib>;
allowedEndpointKeys = [ "type" "domain" "endpoint" "port" "force_ssl" "content" ];
allowedProxyContentKeys = [ "type" "ip" "port" "proxyWebsockets" ];
allowedWebContentKeys = [ "type" "files" ];
allowedWebFileKeys = [ "path" "filePath" "contentType" "status" ];
allowedInlineContentKeys = [ "body" "contentType" "headers" "status" ];
allowedRedirectContentKeys = [ "target" "status" ];
ensureNoUnknownKeys = context: obj: allowedKeys:
let
unknown = builtins.filter (key: !(lib.elem key allowedKeys)) (builtins.attrNames obj);
in
if unknown != [] then
throw "${context} contains unknown keys: ${builtins.concatStringsSep ", " unknown}"
else
obj;
ensurePath = value:
builtins.isString value
&& builtins.substring 0 1 value == "/";
ensurePort = value:
builtins.isInt value && value > 0 && value <= 65535;
validateEndpointShape = index: endpoint:
let
_ = if !builtins.isAttrs endpoint then throw "Endpoint at index ${toString index} must be an attrset." else null;
__ = ensureNoUnknownKeys "Endpoint at index ${toString index}" endpoint allowedEndpointKeys;
___ =
if endpoint ? forceSsl then
throw "Endpoint at index ${toString index} uses forceSsl. Use force_ssl instead."
else
null;
typeValue =
if endpoint ? type && builtins.isString endpoint.type then
endpoint.type
else
throw "Endpoint at index ${toString index} must define type as a string.";
_type =
if lib.elem typeValue [ "proxy" "web" "inline" "redirect" ] then
null
else
throw "Endpoint at index ${toString index} type must be 'proxy', 'web', 'inline', or 'redirect'.";
_domain =
if endpoint ? domain && builtins.isString endpoint.domain && endpoint.domain != "" then
null
else
throw "Endpoint at index ${toString index} must define a non-empty domain string.";
_endpoint =
if endpoint ? endpoint && ensurePath endpoint.endpoint then
null
else
throw "Endpoint at index ${toString index} must define endpoint as a path starting with '/'.";
_port =
if endpoint ? port && ensurePort endpoint.port then
null
else
throw "Endpoint at index ${toString index} must define port as int in range 1..65535.";
_forceSsl =
if endpoint ? force_ssl && builtins.isBool endpoint.force_ssl then
null
else
throw "Endpoint at index ${toString index} must define force_ssl as a bool.";
contentValue =
if endpoint ? content then
endpoint.content
else
throw "Endpoint at index ${toString index} must define content.";
_content =
if typeValue == "proxy" then
let
_attrs = if builtins.isAttrs contentValue then null else throw "Proxy endpoint at index ${toString index} must define content as an attrset.";
____ = ensureNoUnknownKeys "Proxy content at endpoint index ${toString index}" contentValue allowedProxyContentKeys;
in
if !(contentValue ? type) || !builtins.isString contentValue.type then
throw "Proxy endpoint at index ${toString index} must define content.type as a string."
else if !(contentValue ? ip) || !builtins.isString contentValue.ip || contentValue.ip == "" then
throw "Proxy endpoint at index ${toString index} must define content.ip as a non-empty string."
else if !(contentValue ? port) || !ensurePort contentValue.port then
throw "Proxy endpoint at index ${toString index} must define content.port as int in range 1..65535."
else if !(contentValue ? proxyWebsockets) || !builtins.isBool contentValue.proxyWebsockets then
throw "Proxy endpoint at index ${toString index} must define content.proxyWebsockets as a bool."
else
null
else
if typeValue == "web" then
let
_attrs = if builtins.isAttrs contentValue then null else throw "Web endpoint at index ${toString index} must define content as an attrset.";
____ = ensureNoUnknownKeys "Web content at endpoint index ${toString index}" contentValue allowedWebContentKeys;
filesValue =
if contentValue ? files && builtins.isList contentValue.files then
contentValue.files
else
throw "Web endpoint at index ${toString index} must define content.files as a list.";
_____ =
if contentValue ? type && contentValue.type == "store" then
null
else
throw "Web endpoint at index ${toString index} must define content.type = 'store'.";
______ = lib.imap0 (fileIndex: file:
let
_______ =
if builtins.isAttrs file then
null
else
throw "Web endpoint at index ${toString index} file at position ${toString fileIndex} must be an attrset.";
________ = ensureNoUnknownKeys "Web endpoint at index ${toString index} file at position ${toString fileIndex}" file allowedWebFileKeys;
_________ =
if file ? path && builtins.isString file.path && file.path != "" && builtins.substring 0 1 file.path != "/" then
null
else
throw "Web endpoint at index ${toString index} file at position ${toString fileIndex} must define path as a relative path string.";
__________ =
if file ? filePath && (builtins.isString file.filePath || builtins.isPath file.filePath) then
null
else
throw "Web endpoint at index ${toString index} file at position ${toString fileIndex} must define filePath as string or path.";
___________ =
if file ? contentType && builtins.isString file.contentType && file.contentType != "" then
null
else
throw "Web endpoint at index ${toString index} file at position ${toString fileIndex} must define contentType as non-empty string.";
____________ =
if file ? status && builtins.isInt file.status then
null
else
throw "Web endpoint at index ${toString index} file at position ${toString fileIndex} must define status as int.";
in
file
) filesValue;
in
null
else if typeValue == "redirect" then
if builtins.isString contentValue then
null
else if builtins.isAttrs contentValue then
let
____ = ensureNoUnknownKeys "Redirect content at endpoint index ${toString index}" contentValue allowedRedirectContentKeys;
_____ =
if contentValue ? target && builtins.isString contentValue.target && contentValue.target != "" then
null
else
throw "Redirect endpoint at index ${toString index} must define content.target as a non-empty string when content is an attrset.";
______ =
if !(contentValue ? status) || builtins.isInt contentValue.status then
null
else
throw "Redirect endpoint at index ${toString index} content.status must be an int when provided.";
in
null
else
throw "Redirect endpoint at index ${toString index} must define content as a string or an attrset."
else
if builtins.isString contentValue then
null
else if builtins.isAttrs contentValue then
let
____ = ensureNoUnknownKeys "Inline content at endpoint index ${toString index}" contentValue allowedInlineContentKeys;
_____ =
if contentValue ? body && builtins.isString contentValue.body then
null
else
throw "Inline endpoint at index ${toString index} must define content.body as a string when content is an attrset.";
______ =
if !(contentValue ? contentType) || (builtins.isString contentValue.contentType && contentValue.contentType != "") then
null
else
throw "Inline endpoint at index ${toString index} content.contentType must be a non-empty string when provided.";
________ =
if contentValue ? headers then
if builtins.isAttrs contentValue.headers then
let
headerValues = builtins.attrValues contentValue.headers;
in
if lib.all (value: builtins.isString value && value != "") headerValues then
null
else
throw "Inline endpoint at index ${toString index} content.headers values must be non-empty strings."
else
throw "Inline endpoint at index ${toString index} content.headers must be an attrset when provided."
else
null;
_______ =
if !(contentValue ? status) || builtins.isInt contentValue.status then
null
else
throw "Inline endpoint at index ${toString index} content.status must be an int when provided.";
in
null
else
throw "Inline endpoint at index ${toString index} must define content as a string or an attrset.";
in
endpoint;
validateEndpointsShape = endpoints:
if !builtins.isList endpoints then
throw "config/endpoints.nix must evaluate to a list of endpoint attrsets."
else
lib.imap0 validateEndpointShape endpoints;
in
{
inherit validateEndpointShape validateEndpointsShape;
}