Skip to content

Commit

Permalink
Merge pull request #644 from drashland/feat/resource-loader
Browse files Browse the repository at this point in the history
feat: add resource loader service (takes in paths and loads resources from it)
  • Loading branch information
crookse committed Jul 2, 2022
2 parents fd73734 + a32a836 commit 42e8d59
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 3 deletions.
5 changes: 5 additions & 0 deletions src/http/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import * as Drash from "../../mod.ts";
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web
*/
export class Resource implements Drash.Interfaces.IResource {
/**
* Internal property used to identify this as a Drash resource.
*/
protected drash_resource = true;

public services: Drash.Interfaces.IResourceServices = {};
public paths: string[] = [];

Expand Down
4 changes: 2 additions & 2 deletions src/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,15 +172,15 @@ export class Server {
}

//////////////////////////////////////////////////////////////////////////////
// FILE MARKER - METHODS - PRIVATE /////////////////////////////////////////////
// FILE MARKER - METHODS - PRIVATE ///////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

/**
* Add all resources to this server -- instantiating them so that they
* are ready to handle requests at runtime.
*/
#addResources(): void {
this.#options.resources.forEach((resourceClass: typeof Drash.Resource) => {
this.#options.resources?.forEach((resourceClass: typeof Drash.Resource) => {
this.addResource(resourceClass);
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export interface IServerOptions {
key_file?: string;
port: number;
protocol: "http" | "https";
resources: typeof Resource[];
resources?: typeof Resource[];
services?: IService[];
// deno-lint-ignore no-explicit-any camelcase
error_handler?: new (...args: any[]) => IErrorHandler;
Expand Down
2 changes: 2 additions & 0 deletions src/services/resource_loader/deps.ts
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";
122 changes: 122 additions & 0 deletions src/services/resource_loader/resource_loader.ts
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 tests/integration/services/resource_loader/resource_loader_test.ts
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();
});
});
});
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",
},
]);
}
}
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>");
}
}

0 comments on commit 42e8d59

Please sign in to comment.