├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── lib
├── aqua.ts
├── branch.ts
├── event.ts
├── fake-aqua.ts
├── index.ts
└── method.ts
├── mod.spec.ts
├── mod.ts
├── usage_playground.ts
└── x
└── get-pattern-pathname-groups.ts
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aqua
2 |
3 | Aqua is a minimal and fast web framework.
4 |
5 | > :warning: This version is a WIP and has not yet been released. Please refer to the [main branch](https://github.com/grayliquid/aqua/tree/main) for the current documentation.
6 |
7 | ## Example usage
8 |
9 | ### It starts easy,
10 |
11 | ```typescript
12 | import { Aqua } from "...";
13 |
14 | const app = new Aqua({
15 | listen: {
16 | port: 80,
17 | },
18 | });
19 |
20 | app.route("/").respond(Method.GET, (_event) => {
21 | return new Response("Hello, World!");
22 | });
23 | ```
24 |
25 | ### ... and stays easy.
26 |
27 | ```typescript
28 | const v1 = app.route("/v1").step(async (event) => {
29 | if (!event.request.headers.has("X-Api-Key")) {
30 | event.response = Response.json(
31 | { error: "MISSING_API_KEY" },
32 | {
33 | status: 400,
34 | }
35 | );
36 | return event.end();
37 | }
38 |
39 | const user = await getUserByRequest(event.request);
40 | // ^ type User
41 |
42 | return {
43 | ...event,
44 | user,
45 | };
46 | });
47 |
48 | v1.route("/user").respond(Method.GET, (event) => {
49 | return Response.json({ data: { user: event.user } });
50 | // ^ type User
51 | });
52 | ```
53 |
--------------------------------------------------------------------------------
/lib/aqua.ts:
--------------------------------------------------------------------------------
1 | import { serve } from "https://deno.land/std@0.185.0/http/server.ts";
2 | import { Branch } from "./branch.ts";
3 | import { Event, InternalizedEvent } from "./event.ts";
4 | import { Method } from "./method.ts";
5 |
6 | export type AquaOptionsCustomListenFn = ({
7 | handlerFn,
8 | abortSignal,
9 | }: {
10 | handlerFn: (request: Request) => Response | Promise;
11 | abortSignal: AbortSignal;
12 | }) => void | Promise;
13 |
14 | export interface AquaOptions {
15 | /**
16 | * `listen` either takes options that will be passed
17 | * to the std/http `serve` function, or a custom function
18 | * that starts the listening process and makes use of
19 | * Aqua's request handler function.
20 | *
21 | * @example
22 | * {
23 | * port: 80
24 | * }
25 | *
26 | * @example
27 | * // `abortSignal` ignored for the sake of simplicity
28 | * async ({ handlerFn, abortSignal }) => {
29 | * const conn = Deno.listen({ port: 80 });
30 | * const httpConn = Deno.serveHttp(await conn.accept());
31 | * const e = await httpConn.nextRequest();
32 | * if (e) e.respondWith(await handlerFn(e.request));
33 | * }
34 | */
35 | listen?:
36 | | {
37 | port?: number;
38 | hostname?: string;
39 | }
40 | | AquaOptionsCustomListenFn;
41 | /**
42 | * @default false
43 | */
44 | shouldRepectTrailingSlash?: boolean;
45 | }
46 |
47 | export type StepFn<_Event extends Event> = (
48 | event: _Event
49 | ) => _Event | void | Promise<_Event | void>;
50 |
51 | export type RespondFn<_Event extends Event> = (
52 | event: _Event
53 | ) => _Event["response"] | Promise<_Event["response"]>;
54 |
55 | export interface RouteOptions<_Event extends Event> {
56 | steps?: StepFn<_Event>[];
57 | }
58 |
59 | interface AquaInternals<_Event extends Event> {
60 | options: AquaOptions;
61 | setRoute<__Event extends _Event>(
62 | method: Method,
63 | path: string,
64 | steps: StepFn<__Event>[]
65 | ): void;
66 | }
67 |
68 | const URL_PATTERN_PREFIX = "http://0.0.0.0";
69 |
70 | export function getDefaultResponse() {
71 | return new Response("Not found.", { status: 404 });
72 | }
73 |
74 | export class Aqua<_Event extends Event = Event> {
75 | private abortController: AbortController;
76 |
77 | protected routes: Record<
78 | string,
79 | {
80 | path: string;
81 | method: Method;
82 | urlPattern: URLPattern;
83 | // @todo Solve this `any` situation
84 | steps: StepFn[];
85 | }
86 | > = {};
87 |
88 | public _internal: AquaInternals<_Event>;
89 |
90 | constructor(options: AquaOptions = {}) {
91 | this._internal = {
92 | options,
93 | setRoute: <__Event extends _Event>(
94 | method: Method,
95 | path: string,
96 | steps: StepFn<__Event>[]
97 | ) => {
98 | this.routes[method + path] = {
99 | path,
100 | method,
101 | urlPattern: new URLPattern(URL_PATTERN_PREFIX + path),
102 | steps,
103 | };
104 | },
105 | };
106 | this.abortController = new AbortController();
107 |
108 | this.listen(options?.listen);
109 | }
110 |
111 | protected async listen(listen: AquaOptions["listen"]) {
112 | const handlerFn = async (request: Request) => {
113 | try {
114 | return await this.handleRequest(this.createInternalEvent(request));
115 | } catch (error) {
116 | console.error(error);
117 |
118 | return new Response(error, { status: 500 });
119 | }
120 | };
121 |
122 | if (typeof listen === "function") {
123 | await listen({
124 | handlerFn,
125 | abortSignal: this.abortController.signal,
126 | });
127 | return;
128 | }
129 |
130 | await serve(handlerFn, {
131 | hostname: listen?.hostname,
132 | port: listen?.port,
133 | signal: this.abortController.signal,
134 | });
135 | }
136 |
137 | protected createInternalEvent(request: Request): InternalizedEvent<_Event> {
138 | return {
139 | _internal: {
140 | hasCalledEnd: false,
141 | urlPatternResult: null,
142 | },
143 | request,
144 | response: getDefaultResponse(),
145 | end() {
146 | if (this._internal.hasCalledEnd) return;
147 | this._internal.hasCalledEnd = true;
148 | },
149 | } as InternalizedEvent<_Event>;
150 | }
151 |
152 | protected async handleRequest(event: InternalizedEvent<_Event>) {
153 | let pathName = new URL(event.request.url).pathname;
154 | if (
155 | !this._internal.options.shouldRepectTrailingSlash &&
156 | !pathName.endsWith("/")
157 | ) {
158 | pathName += "/";
159 | }
160 |
161 | const requestMethod = event.request.method.toUpperCase();
162 |
163 | let route = this.routes[requestMethod + pathName];
164 |
165 | if (!route) {
166 | // Try to find matching pattern if there was no direct match
167 | const urlPatternTestPath = URL_PATTERN_PREFIX + pathName;
168 |
169 | for (const _route of Object.values(this.routes)) {
170 | if (
171 | _route.urlPattern.test(urlPatternTestPath) &&
172 | _route.method === requestMethod
173 | ) {
174 | event._internal.urlPatternResult =
175 | _route.urlPattern.exec(urlPatternTestPath);
176 | route = _route;
177 | break;
178 | }
179 | }
180 |
181 | if (!route) {
182 | return event.response;
183 | }
184 | }
185 |
186 | for (const step of route.steps) {
187 | const returnedEvent = await step(event);
188 |
189 | // Called `event.end()`. Ignore all further statements.
190 | if (event._internal.hasCalledEnd) {
191 | break;
192 | }
193 |
194 | if (!returnedEvent) {
195 | continue;
196 | }
197 |
198 | event = returnedEvent as InternalizedEvent<_Event>;
199 | }
200 |
201 | return event.response;
202 | }
203 |
204 | public route<__Event extends _Event>(
205 | path: string,
206 | options: RouteOptions<__Event> = {}
207 | ): Branch<__Event> {
208 | if (!path.startsWith("/")) {
209 | throw new Error('Route paths must start with a "/".');
210 | }
211 |
212 | if (
213 | !this._internal.options.shouldRepectTrailingSlash &&
214 | !path.endsWith("/")
215 | ) {
216 | path += "/";
217 | }
218 |
219 | return new Branch<__Event>({
220 | path,
221 | aquaInstance: this,
222 | steps: options.steps ?? [],
223 | });
224 | }
225 |
226 | public kill() {
227 | this.abortController.abort();
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/lib/branch.ts:
--------------------------------------------------------------------------------
1 | import { Aqua, RespondFn, RouteOptions, StepFn } from "./aqua.ts";
2 | import { Event } from "./event.ts";
3 | import { Method } from "./method.ts";
4 |
5 | export interface BranchOptions<_Event extends Event>
6 | extends Required, "steps">> {
7 | path: string;
8 | aquaInstance: Aqua;
9 | }
10 |
11 | interface BranchInternals<_Event extends Event> {
12 | options: BranchOptions<_Event>;
13 | path: string;
14 | }
15 |
16 | export class Branch<_Event extends Event> {
17 | private steps: StepFn<_Event>[] = [];
18 |
19 | public _internal: BranchInternals<_Event>;
20 |
21 | constructor(options: BranchOptions<_Event>) {
22 | this._internal = {
23 | options,
24 | path: options.path,
25 | };
26 | this.steps = options.steps;
27 | }
28 |
29 | public route(path: string, options: RouteOptions<_Event> = {}) {
30 | if (!path.startsWith("/")) {
31 | throw new Error('Route paths must start with a "/".');
32 | }
33 |
34 | const joinedPath = this._internal.options.path.replace(/\/$/, "") + path;
35 |
36 | return this._internal.options.aquaInstance.route<_Event>(joinedPath, {
37 | steps: [...this.steps, ...(options?.steps ?? [])],
38 | });
39 | }
40 |
41 | /**
42 | * Injects a function into the event lifecycle.
43 | *
44 | * @example
45 | * // Check whether a header is set and throw otherwise
46 | * .step((event) => {
47 | * if (!event.request.headers.has("X-Api-Key")) {
48 | * event.response = Response.json(
49 | * { error: "MISSING_API_KEY" },
50 | * {
51 | * status: 400,
52 | * }
53 | * );
54 | * return event.end();
55 | * }
56 | * });
57 | *
58 | * @example
59 | * // Early-return
60 | * .step((event) => {
61 | * if (event.request.headers.has("early-return")) {
62 | * event.response = Response.json({ data: {} });
63 | * return event.end();
64 | * }
65 | * });
66 | *
67 | * @example
68 | * // Provide additional event information
69 | * .step((event) => {
70 | * return {
71 | * ...event,
72 | * isTesting: event.request.url.startsWith("http://localhost"),
73 | * };
74 | * });
75 | */
76 | public step<_StepFn extends StepFn<_Event>>(stepFn: _StepFn) {
77 | this.steps.push(stepFn);
78 |
79 | return this as Awaited> extends never
80 | ? never
81 | : Awaited> extends _Event
82 | ? Branch>>
83 | : this;
84 | }
85 |
86 | /**
87 | * @example
88 | * .respond(Method.GET, (_event) => new Response("Hello, World!"));
89 | */
90 | public respond<_RespondFn extends RespondFn<_Event>>(
91 | method: Method,
92 | respondFn: _RespondFn
93 | ) {
94 | this._internal.options.aquaInstance._internal.setRoute(
95 | method,
96 | this._internal.path,
97 | [
98 | ...this.steps,
99 | async (event: _Event) => {
100 | event.response = await respondFn(event);
101 | return event;
102 | },
103 | ]
104 | );
105 |
106 | return new ResponderBranch(this);
107 | }
108 | }
109 |
110 | /**
111 | * Used for everything after the first `respond(...)` call.
112 | */
113 | export class ResponderBranch<_Event extends Event>
114 | implements Omit, "route" | "step">
115 | {
116 | get _internal() {
117 | return this.branch._internal;
118 | }
119 |
120 | constructor(private branch: Branch<_Event>) {}
121 |
122 | /**
123 | * @example
124 | * .respond(Method.GET, (_event) => new Response("Hello, World!"));
125 | */
126 | public respond<_RespondFn extends RespondFn<_Event>>(
127 | method: Method,
128 | respondFn: _RespondFn
129 | ) {
130 | return this.branch.respond(method, respondFn);
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/lib/event.ts:
--------------------------------------------------------------------------------
1 | export interface Event {
2 | request: Request;
3 | response: Response;
4 | /**
5 | * Calling `end()` tells Aqua to respond to the event after running the
6 | * current step function.
7 | * Please make sure to return after calling `end()` to not accidentally modify
8 | * the event response any further.
9 | *
10 | * @example
11 | * .step((event) => {
12 | * if (event.request.headers.has("early-return")) {
13 | * event.response = Response.json({ data: {} });
14 | * return event.end();
15 | * }
16 | * });
17 | */
18 | end(): void;
19 | [key: string]: unknown;
20 | }
21 |
22 | export interface InternalEvent extends Event {
23 | _internal: {
24 | urlPatternResult: null | URLPatternResult;
25 | hasCalledEnd: boolean;
26 | };
27 | }
28 |
29 | export type InternalizedEvent<_Event extends Event> = _Event & InternalEvent;
30 |
--------------------------------------------------------------------------------
/lib/fake-aqua.ts:
--------------------------------------------------------------------------------
1 | import { Aqua, AquaOptions } from "./aqua.ts";
2 |
3 | export class FakeAqua extends Aqua {
4 | constructor(options: AquaOptions = {}) {
5 | super(options);
6 | }
7 |
8 | protected async listen() {}
9 |
10 | public async fakeCall(request: Request): Promise {
11 | try {
12 | return await this.handleRequest(this.createInternalEvent(request));
13 | } catch (error) {
14 | return new Response(error, { status: 500 });
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./aqua.ts";
2 | export * from "./branch.ts";
3 | export * from "./event.ts";
4 | export * from "./fake-aqua.ts";
5 | export * from "./method.ts";
6 |
--------------------------------------------------------------------------------
/lib/method.ts:
--------------------------------------------------------------------------------
1 | export enum Method {
2 | GET = "GET",
3 | HEAD = "HEAD",
4 | POST = "POST",
5 | PUT = "PUT",
6 | DELETE = "DELETE",
7 | CONNECT = "CONNECT",
8 | OPTIONS = "OPTIONS",
9 | TRACE = "TRACE",
10 | PATCH = "PATCH",
11 | }
12 |
--------------------------------------------------------------------------------
/mod.spec.ts:
--------------------------------------------------------------------------------
1 | import { assert } from "https://deno.land/std@0.192.0/testing/asserts.ts";
2 | import { FakeAqua, Method } from "./mod.ts";
3 | import { getPatternPathnameGroups } from "./x/get-pattern-pathname-groups.ts";
4 |
5 | Deno.test(async function notFound() {
6 | const app = new FakeAqua();
7 |
8 | const res = await app.fakeCall(new Request("http://localhost/"));
9 |
10 | assert(res.status === 404);
11 | assert((await res.text()) === "Not found.");
12 | });
13 |
14 | Deno.test(async function simpleDELETE() {
15 | const app = new FakeAqua();
16 |
17 | app.route("/").respond(Method.DELETE, (_event) => new Response("worked!"));
18 |
19 | const res = await app.fakeCall(
20 | new Request("http://localhost/", {
21 | method: "DELETE",
22 | })
23 | );
24 |
25 | assert(res.status === 200);
26 | assert((await res.text()) === "worked!");
27 | });
28 |
29 | Deno.test(async function simpleGET() {
30 | const app = new FakeAqua();
31 |
32 | app.route("/").respond(Method.GET, (_event) => new Response("worked!"));
33 |
34 | const res = await app.fakeCall(new Request("http://localhost/"));
35 |
36 | assert(res.status === 200);
37 | assert((await res.text()) === "worked!");
38 | });
39 |
40 | Deno.test(async function getWithUrlPattern() {
41 | const app = new FakeAqua();
42 |
43 | app
44 | .route("/hello/:text")
45 | .respond(
46 | Method.GET,
47 | (event) => new Response(getPatternPathnameGroups(event).get("text"))
48 | );
49 |
50 | const res = await app.fakeCall(new Request("http://localhost/hello/world"));
51 |
52 | assert(res.status === 200);
53 | assert((await res.text()) === "world");
54 | });
55 |
56 | Deno.test(async function simpleOPTIONS() {
57 | const app = new FakeAqua();
58 |
59 | app.route("/").respond(Method.OPTIONS, (_event) => new Response("worked!"));
60 |
61 | const res = await app.fakeCall(
62 | new Request("http://localhost/", {
63 | method: "OPTIONS",
64 | })
65 | );
66 |
67 | assert(res.status === 200);
68 | assert((await res.text()) === "worked!");
69 | });
70 |
71 | Deno.test(async function simpleHEAD() {
72 | const app = new FakeAqua();
73 |
74 | app.route("/").respond(Method.HEAD, (_event) => new Response("worked!"));
75 |
76 | const res = await app.fakeCall(
77 | new Request("http://localhost/", {
78 | method: "HEAD",
79 | })
80 | );
81 |
82 | assert(res.status === 200);
83 | assert((await res.text()) === "worked!");
84 | });
85 |
86 | Deno.test(async function simpleOPTIONS() {
87 | const app = new FakeAqua();
88 |
89 | app.route("/").respond(Method.PATCH, (_event) => new Response("worked!"));
90 |
91 | const res = await app.fakeCall(
92 | new Request("http://localhost/", {
93 | method: "PATCH",
94 | })
95 | );
96 |
97 | assert(res.status === 200);
98 | assert((await res.text()) === "worked!");
99 | });
100 |
101 | Deno.test(async function simplePOST() {
102 | const app = new FakeAqua();
103 |
104 | app.route("/").respond(Method.POST, (_event) => new Response("worked!"));
105 |
106 | const res = await app.fakeCall(
107 | new Request("http://localhost/", { method: "POST" })
108 | );
109 |
110 | assert(res.status === 200);
111 | assert((await res.text()) === "worked!");
112 | });
113 |
114 | Deno.test(async function simplePUT() {
115 | const app = new FakeAqua();
116 |
117 | app.route("/").respond(Method.PUT, (_event) => new Response("worked!"));
118 |
119 | const res = await app.fakeCall(
120 | new Request("http://localhost/", { method: "PUT" })
121 | );
122 |
123 | assert(res.status === 200);
124 | assert((await res.text()) === "worked!");
125 | });
126 |
127 | Deno.test(async function addCustomPropertyStep() {
128 | const app = new FakeAqua();
129 |
130 | app
131 | .route("/")
132 | .step((event) => {
133 | return {
134 | ...event,
135 | foo: "bar",
136 | };
137 | })
138 | .respond(Method.GET, (event) => new Response(event.foo));
139 |
140 | const res = await app.fakeCall(new Request("http://localhost/"));
141 |
142 | assert(res.status === 200);
143 | assert((await res.text()) === "bar");
144 | });
145 |
146 | Deno.test(async function failEventInStep() {
147 | const app = new FakeAqua();
148 |
149 | app
150 | .route("/")
151 | .step((event) => {
152 | // just so it doesn't infer `never`
153 | if (event.request.headers.has("test")) return event;
154 |
155 | event.response = new Response("failed", { status: 500 });
156 | return event.end();
157 | })
158 | .respond(Method.GET, (_event) => new Response("succeeded"));
159 |
160 | const res = await app.fakeCall(new Request("http://localhost/"));
161 |
162 | assert(res.status === 500);
163 | assert((await res.text()) === "failed");
164 | });
165 |
166 | Deno.test(async function samePatternRouteWithMultipleResponds() {
167 | const app = new FakeAqua();
168 |
169 | const route = app.route("/:hello");
170 |
171 | route.respond(Method.GET, (_event) => new Response("GET"));
172 | route.respond(Method.DELETE, (_event) => new Response("DELETE"));
173 |
174 | for (const method of ["GET", "DELETE"]) {
175 | const res = await app.fakeCall(
176 | new Request("http://localhost/test", {
177 | method,
178 | })
179 | );
180 |
181 | assert(res.status === 200);
182 | assert((await res.text()) === method);
183 | }
184 | });
185 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./lib/index.ts";
2 |
--------------------------------------------------------------------------------
/usage_playground.ts:
--------------------------------------------------------------------------------
1 | // @todo delete this file
2 |
3 | import { Aqua, Method } from "./mod.ts";
4 |
5 | const app = new Aqua({
6 | listen: {
7 | port: 80,
8 | },
9 | });
10 |
11 | app.route("/").respond(Method.GET, (_event) => {
12 | return new Response("Hello, World!");
13 | });
14 |
15 | // /v1
16 | const getUserByRequest = (_req: Request) => Promise.resolve({ name: "test" });
17 |
18 | const v1 = app.route("/v1").step(async (event) => {
19 | if (!event.request.headers.has("X-Api-Key")) {
20 | event.response = Response.json(
21 | { error: "MISSING_API_KEY" },
22 | {
23 | status: 400,
24 | }
25 | );
26 | return event.end();
27 | }
28 |
29 | const user = await getUserByRequest(event.request);
30 | // ^ type User
31 |
32 | return {
33 | ...event,
34 | user,
35 | };
36 | });
37 |
38 | v1.route("/user").respond(Method.GET, (event) => {
39 | return Response.json({ data: { user: event.user } });
40 | // ^ type User
41 | });
42 |
43 | // /test
44 | const test = app.route("/test").step((event) => {
45 | console.log("/test global step");
46 | return event;
47 | });
48 |
49 | test
50 | .route("/1")
51 | .step((e) => (console.log("/test/1 step"), e))
52 | .respond(Method.GET, () => new Response("GET"))
53 | .respond(Method.POST, () => new Response("POST"));
54 |
--------------------------------------------------------------------------------
/x/get-pattern-pathname-groups.ts:
--------------------------------------------------------------------------------
1 | import { Event, InternalizedEvent } from "../mod.ts";
2 |
3 | export function getPatternPathnameGroups<_Event extends Event>(event: _Event) {
4 | const internalEvent = event as InternalizedEvent<_Event>;
5 |
6 | const groups = new Map();
7 | for (const [name, value] of Object.entries(
8 | internalEvent._internal.urlPatternResult?.pathname?.groups ?? {}
9 | )) {
10 | groups.set(name, value);
11 | }
12 |
13 | return groups;
14 | }
15 |
--------------------------------------------------------------------------------