-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { serve, ServeInit } from "https://deno.land/[email protected]/http/server.ts"; | ||
import { Handler } from "./handler.ts"; | ||
import {CatchFunc, HandleFunc, Matcher} from "./types.ts"; | ||
import { Request as RoarterRequest } from "./request.ts"; | ||
|
||
export class Application { | ||
handlers: Handler[] = []; | ||
catchFunc: CatchFunc = () => { | ||
throw new Error("Not Implemented"); | ||
}; | ||
|
||
match(custom: Matcher): Handler { | ||
const handler = new Handler().match(custom); | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
get get(): Handler { | ||
const handler = new Handler().get; | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
get post(): Handler { | ||
const handler = new Handler().post; | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
get put(): Handler { | ||
const handler = new Handler().put; | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
get delete(): Handler { | ||
const handler = new Handler().delete; | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
get patch(): Handler { | ||
const handler = new Handler().patch; | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
path(path: string): Handler { | ||
const handler = new Handler().path(path); | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
queries(queries: string[]): Handler { | ||
const handler = new Handler().queries(queries); | ||
this.handlers.push(handler); | ||
return handler; | ||
} | ||
|
||
handle(handler: HandleFunc | Application): Handler { | ||
const h = new Handler().handle(handler); | ||
this.handlers.push(h); | ||
return h; | ||
} | ||
|
||
catch(fn: CatchFunc) { | ||
this.catchFunc = fn; | ||
} | ||
|
||
// Handler runs all the registered handlers and returns the first | ||
// response it finds. After returning the first response, it runs | ||
// the remaining handlers. The purpose of this is to support middleware | ||
// after a response has been sent. | ||
async runHandlers(req: RoarterRequest): Promise<Response> { | ||
let runRemaining = true | ||
let i = 0; | ||
try { | ||
try { | ||
for (i; i < this.handlers.length; i++) { | ||
const handler = this.handlers[i]; | ||
const response = await handler.run(req); | ||
if (response) { | ||
i++; | ||
return response; | ||
} | ||
} | ||
} catch (e) { | ||
// If an error occurs, we wan't to skip all handlers and run the catchFunc | ||
runRemaining = false | ||
return await this.catchFunc(req, e); | ||
} | ||
// If after running all the handlers there is no response, throw the error | ||
throw new Error( | ||
`No Response was sent for ${req.method} ${req.url}`, | ||
); | ||
} finally { | ||
if (runRemaining) { | ||
// Since at this point we have already returned a response, | ||
// we run the remaining handlers ignoring their response and errors | ||
for (i; i < this.handlers.length; i++) { | ||
const handler = this.handlers[i]; | ||
handler.run(req).catch(e => { | ||
this.catchFunc(req, e) | ||
}) | ||
} | ||
} | ||
} | ||
} | ||
|
||
async handler(domReq: Request): Promise<Response> { | ||
const req = new RoarterRequest(domReq); | ||
return this.runHandlers(req) | ||
} | ||
|
||
async serve(options?: ServeInit) { | ||
this.handler = this.handler.bind(this); | ||
await serve(this.handler, options); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { HandleFunc, Matcher } from "./types.ts"; | ||
import { | ||
buildPathMatcher, | ||
buildQueryMatcher, | ||
deleteMatcher, | ||
getMatcher, | ||
patchMatcher, | ||
postMatcher, | ||
putMatcher, | ||
} from "./matchers.ts"; | ||
import { Response } from "./response.ts"; | ||
import { Request } from "./request.ts"; | ||
import {Application} from "./application.ts"; | ||
|
||
export class Handler { | ||
matchers: Matcher[] = []; | ||
handler: (HandleFunc | Application) | null = null; | ||
|
||
private isHandlerASubRouter(): boolean { | ||
return this.handler instanceof Application | ||
} | ||
|
||
runMatchers(req: Request): boolean { | ||
for (let i = 0; i < this.matchers.length; i++) { | ||
const matcher: Matcher = this.matchers[i]; | ||
if (!matcher(req, this.isHandlerASubRouter())) return false; | ||
} | ||
return true; | ||
} | ||
|
||
async runHandler(req: Request) { | ||
if (!this.handler) return; | ||
if (this.isHandlerASubRouter()) { | ||
return (this.handler as Application).runHandlers(req) | ||
} else { | ||
return (this.handler as HandleFunc)(req); | ||
} | ||
} | ||
|
||
match(custom: Matcher): Handler { | ||
this.matchers.push(custom); | ||
return this; | ||
} | ||
|
||
get get(): Handler { | ||
return this.match(getMatcher); | ||
} | ||
|
||
get post(): Handler { | ||
return this.match(postMatcher); | ||
} | ||
|
||
get put(): Handler { | ||
return this.match(putMatcher); | ||
} | ||
|
||
get delete(): Handler { | ||
return this.match(deleteMatcher); | ||
} | ||
|
||
get patch(): Handler { | ||
return this.match(patchMatcher); | ||
} | ||
|
||
path(urlPath: string): Handler { | ||
return this.match(buildPathMatcher(urlPath)); | ||
} | ||
|
||
queries(queries: string[]): Handler { | ||
return this.match(buildQueryMatcher(queries)); | ||
} | ||
|
||
handle(handler: HandleFunc | Application) { | ||
this.handler = handler; | ||
return this; | ||
} | ||
|
||
async run(req: Request) { | ||
if (this.runMatchers(req)) { | ||
return this.runHandler(req); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Matcher, ROMap } from "./types.ts"; | ||
import { extract, match } from "./utils/path.ts"; | ||
|
||
export const getMatcher: Matcher = (req) => { | ||
return req.method === "GET"; | ||
}; | ||
|
||
export const postMatcher: Matcher = (req) => { | ||
return req.method === "POST"; | ||
}; | ||
|
||
export const putMatcher: Matcher = (req) => { | ||
return req.method === "PUT"; | ||
}; | ||
|
||
export const deleteMatcher: Matcher = (req) => { | ||
return req.method === "DELETE"; | ||
}; | ||
|
||
export const patchMatcher: Matcher = (req) => { | ||
return req.method === "PATCH"; | ||
}; | ||
|
||
export const buildPathMatcher: (path: string) => Matcher = (path) => { | ||
const prefixKey = "PathMatcher#PathPrefix" | ||
return (req, matcherIsForASubRouter) => { | ||
const url = new URL(req.url); | ||
|
||
// If the matcher is for a subrouter (as opposed to a handlerFunc), then we want to match the prefix | ||
// of the path name and store it for later use | ||
if (matcherIsForASubRouter) { | ||
let pathCopy = path | ||
if (req.vars.has(prefixKey)) { | ||
pathCopy = req.vars.get(prefixKey) + pathCopy | ||
} | ||
if (!match(pathCopy, url.pathname, true)) return false; | ||
req.vars.set(prefixKey, pathCopy) | ||
const m = extract(pathCopy, url.pathname, true); | ||
for (const [key, val] of m) { | ||
req.params.set(key, val) | ||
} | ||
return true | ||
} else { | ||
let pathCopy = path | ||
if (req.vars.has(prefixKey)) { | ||
pathCopy = req.vars.get(prefixKey) + pathCopy | ||
} | ||
if (!match(pathCopy, url.pathname)) return false; | ||
const m = extract(pathCopy, url.pathname); | ||
for (const [key, val] of m) { | ||
req.params.set(key, val) | ||
} | ||
return true; | ||
} | ||
}; | ||
}; | ||
|
||
export const buildQueryMatcher: (queries: string[]) => Matcher = (queries) => { | ||
return (req) => { | ||
for (const query of queries) { | ||
if (!req.queries.has(query)) return false; | ||
} | ||
return true; | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { ROMap } from "./types.ts"; | ||
|
||
class RoarterRequest extends Request { | ||
public params: Map<string, string> = new Map<string, string>(); | ||
public vars: Map<string, string> = new Map<string, string>(); | ||
public queries: URLSearchParams; | ||
public pathname: string; | ||
|
||
constructor(input: RequestInfo, init?: RequestInit) { | ||
super(input, init); | ||
const url = new URL(this.url); | ||
this.pathname = url.pathname; | ||
this.queries = url.searchParams; | ||
} | ||
} | ||
|
||
export { RoarterRequest as Request }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
class RoarterResponse extends Response { | ||
// @ts-ignore | ||
constructor(body?: BodyInit | null, init?: ResponseInit) { | ||
// @ts-ignore | ||
super(body, init); | ||
} | ||
} | ||
|
||
export { RoarterResponse as Response }; |