UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

119 lines (103 loc) 3.43 kB
/** * Parsed path segment from a Beignet route template. */ export type PathTemplateSegment = | { kind: "static"; value: string } | { kind: "dynamic"; name: string; raw: string }; /** * Parsed representation of a Beignet route path template. */ export interface ParsedPathTemplate { /** * Dynamic parameter names in declaration order. */ keys: string[]; /** * Static and dynamic path segments. */ segments: PathTemplateSegment[]; /** * Normalized Beignet path using `:param` dynamic syntax. */ normalizedPath: string; /** * Route shape used for ambiguity detection, ignoring dynamic parameter names. */ shapeKey: string; /** * OpenAPI-compatible path using `{param}` syntax. */ openApiPath: string; } const PARAM_NAME = "[A-Za-z0-9_-]+"; const COLON_PARAM = new RegExp(`^:(${PARAM_NAME})$`); const BRACKET_PARAM = new RegExp(`^\\[(${PARAM_NAME})\\]$`); function createPathTemplateError(path: string, segment: string): Error { return new Error( `Unsupported path template segment "${segment}" in "${path}". ` + "Use single-segment params like :id or [id]. Contract catch-all params such as [...path] and partial-segment params are not supported. Expose a platform catch-all route to the Beignet server, but keep individual contracts on concrete paths.", ); } function parsePathSegment(path: string, segment: string): PathTemplateSegment { const colonMatch = segment.match(COLON_PARAM); if (colonMatch) { return { kind: "dynamic", name: colonMatch[1], raw: segment }; } const bracketMatch = segment.match(BRACKET_PARAM); if (bracketMatch) { return { kind: "dynamic", name: bracketMatch[1], raw: segment }; } if ( segment.startsWith(":") || segment.startsWith("[") || segment.endsWith("]") ) { throw createPathTemplateError(path, segment); } return { kind: "static", value: segment }; } /** * Parse a Beignet route path template. * * Paths must start with `/`. Dynamic segments may use `:id` or `[id]`. * Contract catch-all segments and partial dynamic segments are intentionally rejected. */ export function parsePathTemplate(path: string): ParsedPathTemplate { if (!path.startsWith("/")) { throw new Error( `Invalid path template "${path}". Paths must start with "/".`, ); } const segments = path .split("/") .filter(Boolean) .map((segment) => parsePathSegment(path, segment)); const keys = segments .filter( (segment): segment is Extract<PathTemplateSegment, { kind: "dynamic" }> => segment.kind === "dynamic", ) .map((segment) => segment.name); const duplicateKeys = keys.filter( (key, index) => keys.indexOf(key) !== index, ); if (duplicateKeys.length) { throw new Error( `Invalid path template "${path}". Path parameter names must be unique; duplicate parameter "${duplicateKeys[0]}" was found.`, ); } const normalizedPath = `/${segments .map((segment) => segment.kind === "static" ? segment.value : `:${segment.name}`, ) .join("/")}`; const shapeKey = `/${segments .map((segment) => (segment.kind === "static" ? segment.value : ":")) .join("/")}`; const openApiPath = `/${segments .map((segment) => segment.kind === "static" ? segment.value : `{${segment.name}}`, ) .join("/")}`; return { keys, segments, normalizedPath, shapeKey, openApiPath }; }