-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #644 from drashland/feat/resource-loader
feat: add resource loader service (takes in paths and loads resources from it)
- Loading branch information
Showing
8 changed files
with
254 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { walkSync } from "https://deno.land/std@0.146.0/fs/mod.ts"; | ||
export { join } from "https://deno.land/std@0.146.0/path/mod.ts"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Interfaces, Resource, Service } from "../../../mod.ts"; | ||
import { join, walkSync } from "./deps.ts"; | ||
|
||
interface IOptions { | ||
/** | ||
* The paths to the resources. | ||
* | ||
* @example | ||
* ```typescript | ||
* ["./resources/api", "./resources/ssr"] | ||
* ``` | ||
*/ | ||
paths_to_resources: string[]; | ||
} | ||
|
||
export class ResourceLoaderService extends Service { | ||
#options: IOptions; | ||
|
||
/** | ||
* Autoload resources in the provided `options.paths_to_resources` option. | ||
* @param options - See `IOptions`. More information can be found at https://drash.land/drash. | ||
* | ||
* @example | ||
* ```typescript | ||
* const resourceLoader = new ResourceLoaderService({ | ||
* paths_to_resources: [ | ||
* "./resources/api", // Loads all resources in ./resources/api directory | ||
* "./resources/ssr", // Loads all resources in ./resources/ssr directory | ||
* ], | ||
* }); | ||
* | ||
* const server = new Drash.Server({ | ||
* protocol: "http", | ||
* hostname: "localhost", | ||
* port: 1337, | ||
* services: [ | ||
* resourceLoader, // Plug in the service to add the autoloaded resources | ||
* ], | ||
* }); | ||
* ``` | ||
*/ | ||
constructor(options: IOptions) { | ||
super(); | ||
this.#options = options; | ||
} | ||
|
||
public async runAtStartup( | ||
options: Interfaces.IServiceStartupOptions, | ||
): Promise<void> { | ||
for (const basePath of this.#options.paths_to_resources) { | ||
for (const entry of walkSync(basePath)) { | ||
if (!entry.isFile) { | ||
continue; | ||
} | ||
|
||
const fileAsModule = await import( | ||
this.#getUrlWithFileScheme(entry.path) | ||
); | ||
|
||
if (!fileAsModule || typeof fileAsModule !== "object") { | ||
continue; | ||
} | ||
|
||
const exportedMemberNames = Object.keys(fileAsModule); | ||
|
||
if (!exportedMemberNames || exportedMemberNames.length <= 0) { | ||
continue; | ||
} | ||
|
||
for (const exportedMemberName of exportedMemberNames) { | ||
const exportedMember = (fileAsModule as { [k: string]: unknown })[ | ||
exportedMemberName as string | ||
]; | ||
|
||
if (typeof exportedMember !== "function") { | ||
continue; | ||
} | ||
|
||
const typeSafeExportedMember = | ||
exportedMember as unknown as typeof Resource; | ||
|
||
try { | ||
const obj = new typeSafeExportedMember(); | ||
const propertyNames = Object.getOwnPropertyNames(obj); | ||
if (!propertyNames.includes("drash_resource")) { | ||
continue; | ||
} | ||
|
||
options.server.addResource(typeSafeExportedMember); | ||
} catch (_error) { | ||
// If `obj` cannot be instantiated, then skip it | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* In some cases, the file:// scheme may be missing. To make sure it is always | ||
* present when using dynamic imports, we check if it's missing and add it if | ||
* so. | ||
* | ||
* @param path - The path that may have a missing file:// scheme. | ||
* | ||
* @returns The path if file:// exists or a new path with file:// added. | ||
*/ | ||
#getUrlWithFileScheme(path: string): string { | ||
const scheme = "file://"; | ||
|
||
const url = new URL(join(Deno.cwd(), path), scheme + Deno.cwd()); | ||
|
||
let urlWithFileScheme = url.href; | ||
|
||
// If the file:// scheme is not included during URL creation, then make sure | ||
// it is added | ||
if (!urlWithFileScheme.includes(scheme)) { | ||
urlWithFileScheme = scheme + urlWithFileScheme; | ||
} | ||
|
||
return urlWithFileScheme; | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
tests/integration/services/resource_loader/resource_loader_test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { assertEquals, Drash } from "../../../deps.ts"; | ||
import { ResourceLoaderService } from "../../../../src/services/resource_loader/resource_loader.ts"; | ||
|
||
/** | ||
* Helper function to start the server in a test that can close it to prevent | ||
* leaking async ops. The server needs to start and stop in the same test. | ||
* @returns The server so it can be closed in the test. | ||
*/ | ||
async function startServer(port: number) { | ||
const resourceLoader = new ResourceLoaderService({ | ||
paths_to_resources: [ | ||
"./tests/integration/services/resource_loader/resources/api", | ||
"./tests/integration/services/resource_loader/resources/ssr", | ||
], | ||
}); | ||
|
||
const server = new Drash.Server({ | ||
protocol: "http", | ||
hostname: "localhost", | ||
port: port, | ||
services: [ | ||
resourceLoader, | ||
], | ||
}); | ||
|
||
await server.run(); | ||
|
||
return server; | ||
} | ||
|
||
//////////////////////////////////////////////////////////////////////////////// | ||
// FILE MARKER - TESTS ///////////////////////////////////////////////////////// | ||
//////////////////////////////////////////////////////////////////////////////// | ||
|
||
Deno.test("resource_loader_test.ts", async (t) => { | ||
await t.step("GET /home", async (t) => { | ||
await t.step("should return the homepage", async () => { | ||
const server = await startServer(3000); | ||
const res = await fetch(`${server.address}/home`); | ||
await server.close(); | ||
const actual = await res.text(); | ||
assertEquals(res.headers.get("content-type"), "text/html"); | ||
assertEquals(actual, "<div>Homepage</div>"); | ||
}); | ||
}); | ||
|
||
await t.step("GET /api/users", async (t) => { | ||
await t.step("should return a users array", async () => { | ||
const server = await startServer(3001); | ||
|
||
const resGet = await fetch(`${server.address}/api/users`); | ||
assertEquals(resGet.headers.get("content-type"), "application/json"); | ||
assertEquals(await resGet.json(), [ | ||
{ | ||
id: 1, | ||
name: "Ed", | ||
}, | ||
{ | ||
id: 2, | ||
name: "Breno", | ||
}, | ||
]); | ||
|
||
const resPost = await fetch(`${server.address}/api/users`, { | ||
method: "POST", | ||
}); | ||
assertEquals(resPost.headers.get("content-type"), "application/json"); | ||
assertEquals(await resPost.json(), [ | ||
{ | ||
id: 1, | ||
name: "Eric", | ||
}, | ||
{ | ||
id: 2, | ||
name: "Sara", | ||
}, | ||
]); | ||
|
||
await server.close(); | ||
}); | ||
}); | ||
}); |
31 changes: 31 additions & 0 deletions
31
tests/integration/services/resource_loader/resources/api/users_resource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { Drash } from "../../../../../deps.ts"; | ||
|
||
export class UsersResource extends Drash.Resource { | ||
paths = ["/api/users"]; | ||
|
||
public GET(_request: Drash.Request, response: Drash.Response) { | ||
response.json([ | ||
{ | ||
id: 1, | ||
name: "Ed", | ||
}, | ||
{ | ||
id: 2, | ||
name: "Breno", | ||
}, | ||
]); | ||
} | ||
|
||
public POST(_request: Drash.Request, response: Drash.Response) { | ||
response.json([ | ||
{ | ||
id: 1, | ||
name: "Eric", | ||
}, | ||
{ | ||
id: 2, | ||
name: "Sara", | ||
}, | ||
]); | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
tests/integration/services/resource_loader/resources/ssr/home_resource.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Drash } from "../../../../../deps.ts"; | ||
|
||
export class HomeResource extends Drash.Resource { | ||
paths = ["/home"]; | ||
|
||
public GET(_request: Drash.Request, response: Drash.Response) { | ||
response.html("<div>Homepage</div>"); | ||
} | ||
} |