Skip to content

Instantly share code, notes, and snippets.

@JTRNS
Last active July 19, 2023 22:57
Show Gist options
  • Save JTRNS/31f3cefb8e5ab8cb800e8bac136e3122 to your computer and use it in GitHub Desktop.
Save JTRNS/31f3cefb8e5ab8cb800e8bac136e3122 to your computer and use it in GitHub Desktop.
URLPattern Router with basic pattern to parameter type inference
import type { ConnInfo, Handler } from "https://deno.land/std@0.194.0/http/server.ts";
type RemoveModifier<Pattern> = Pattern extends `${infer P}?` ? P
: Pattern;
type RemoveMatchingRule<Pattern> = Pattern extends `${infer P}(${infer R})` ? P
: Pattern;
type ExtractGroupName<Pattern> = RemoveModifier<RemoveMatchingRule<Pattern>>;
type PatternParamValue<Key> = Key extends `${infer PatternString}?`
? string | undefined
: string;
type IsPattern<Segment> = Segment extends `:${infer P}` ? RemoveMatchingRule<P>
: never;
type Patterns<Path> = Path extends `${infer SegmentA}/${infer SegmentB}`
? IsPattern<SegmentA> | Patterns<SegmentB>
: IsPattern<Path>;
export type PatternParams<Path> = {
[K in Patterns<Path> as ExtractGroupName<K>]: PatternParamValue<K>;
};
interface Context<T extends string> extends ConnInfo {
params: PatternParams<T>;
}
type RequestMethod =
| "GET"
| "POST"
| "DELETE"
| "PATCH"
| "PUT"
| "OPTIONS"
| "HEAD";
export type Router =
& {
[K in Lowercase<RequestMethod>]: <T extends string>(
path: T,
handler: RouteHandler<T>,
) => void;
}
& {
routes: RouteMap;
add: <T extends string>(
method: RequestMethod,
path: T,
handler: RouteHandler<T>,
) => void;
handle: Handler;
extend: (router: Router) => Router;
};
type RouteHandler<T extends string = string> = (
request: Request,
context: Context<T>,
) => Response | Promise<Response>;
type RouteMap =
& {
[K in RequestMethod]: Map<URLPattern, RouteHandler>;
}
& {
// required to use method property of Request as key
[key: string]: Map<URLPattern, RouteHandler>;
};
export const Router = (): Router => ({
routes: {
GET: new Map(),
POST: new Map(),
DELETE: new Map(),
PATCH: new Map(),
PUT: new Map(),
OPTIONS: new Map(),
HEAD: new Map(),
},
add(method, path, handler) {
const pattern = new URLPattern({ pathname: path });
this.routes[method].set(pattern, handler as RouteHandler);
},
get(path, handler) {
this.add("GET", path, handler);
},
head(path, handler) {
this.add("HEAD", path, handler);
},
options(path, handler) {
this.add("OPTIONS", path, handler);
},
post(path, handler) {
this.add("POST", path, handler);
},
delete(path, handler) {
this.add("DELETE", path, handler);
},
patch(path, handler) {
this.add("PATCH", path, handler);
},
put(path, handler) {
this.add("PUT", path, handler);
},
handle(request: Request, connInfo: ConnInfo) {
const url = new URL(request.url);
// determine method
const method = request.method.toLowerCase();
// get patterns for method
const methodRoutes = this.routes[method];
if (methodRoutes.size === 0) {
return new Response("Unsupported method", { status: 405 });
}
for (const [pattern, handler] of methodRoutes.entries()) {
if (pattern.test(request.url)) {
const results = pattern.exec(request.url);
const params = results?.pathname.groups as PatternParams<
typeof url.pathname
>;
const context = { ...connInfo, params };
try {
return handler(request, context);
} catch (_error) {
return new Response("Internal server error", { status: 500 });
}
}
}
return new Response("Not found", { status: 404 });
},
extend(router) {
for (const [method, routes] of Object.entries(router.routes)) {
for (const [pattern, handler] of routes.entries()) {
this.routes[method as RequestMethod].set(pattern, handler);
}
}
return this;
},
});
@JTRNS
Copy link
Author

JTRNS commented Jul 19, 2023

not tried and untested 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment