Skip to content

Commit

Permalink
fix: refactor transformer to use static init block
Browse files Browse the repository at this point in the history
  • Loading branch information
jfrconley committed May 5, 2023
1 parent 15be78d commit b1b7c2c
Show file tree
Hide file tree
Showing 22 changed files with 407 additions and 228 deletions.
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"author": "John Conley",
"devDependencies": {
"@jest/globals": "^29.5.0",
"@nrfcloud/ts-json-schema-transformer": "^0.2.1",
"@nrfcloud/ts-json-schema-transformer": "^0.2.2",
"@types/jest": "^29.4.0",
"@types/node": "^18.15.11",
"esbuild": "^0.17.18",
Expand All @@ -22,8 +22,8 @@
},
"exports": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
"require": "./dist/index.cjs",
"import": "./dist/index.js"
},
"files": [
"dist"
Expand Down
129 changes: 129 additions & 0 deletions packages/rest/__tests__/src/rest.spec.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import router, {
Controller,
GetChain,
HttpEvent,
HttpRequest,
HttpRequestEmpty,
HttpResponse,
HttpStatusCode,
PostChain
} from "../../dist/runtime/index.mjs";
import {nornir, Nornir} from "@nornir/core";

interface RouteGetInput extends HttpRequestEmpty {
headers: {
// eslint-disable-next-line sonarjs/no-duplicate-string
"content-type": "text/plain";
};
}

interface RoutePostInputJSON extends HttpRequest {
headers: {
"content-type": "application/json";
};
body: RoutePostBodyInput;
query: {
test: "boolean";
};
}

interface RoutePostInputCSV extends HttpRequest {
headers: {
"content-type": "text/csv";
/**
* This is a CSV header
* @example "cool,cool2"
* @pattern ^[a-z]+,[a-z]+$
* @minLength 5
*/
"csv-header": string;
};
body: TestStringType;
}

type RoutePostInput = RoutePostInputJSON | RoutePostInputCSV;

/**
* this is a comment
*/
interface RoutePostBodyInput {
/**
* This is a cool property
* @minLength 5
*/
cool: string;
}

/**
* Amazing string
* @pattern ^[a-z]+$
* @minLength 5
*/
type TestStringType = Nominal<string, "TestStringType">;


declare class Tagged<N extends string> {
protected _nominal_: N;
}

type Nominal<T, N extends string, E extends T & Tagged<string> = T & Tagged<N>> = (T & Tagged<N>) | E;

const basePath = "/basepath";


@Controller(basePath)
class TestController {
/**
* A simple get route
* @summary Cool Route
*/
@GetChain("/route")
public getRoute(chain: Nornir<RouteGetInput>) {
return chain
.use(input => input.headers["content-type"])
.use(contentType => ({
statusCode: HttpStatusCode.Ok,
body: `Content-Type: ${contentType}`,
headers: {
// eslint-disable-next-line sonarjs/no-duplicate-string
"content-type": "text/plain" as const,
},
}));
}

@PostChain("/route")
public postRoute(chain: Nornir<RoutePostInput>) {
return chain
.use(contentType => ({
statusCode: HttpStatusCode.Ok,
body: `Content-Type: ${contentType}`,
headers: {
"content-type": "text/plain" as const,
},
}));
}
}


const handler: (event: HttpEvent) => Promise<HttpResponse> = nornir<HttpEvent>()
.use(router())
.build();

describe("REST tests", () => {
describe("Valid requests", () => {
it("Should process a basic GET request", async () => {
const response = await handler({
method: "GET",
path: "/basepath/route",
headers: {
"content-type": "text/plain",
},
query: {}
});
expect(response.statusCode).toEqual(HttpStatusCode.Ok);
expect(response.body).toBe("Content-Type: text/plain");
expect(response.headers["content-type"]).toBe("text/plain");
})
})
});

5 changes: 0 additions & 5 deletions packages/rest/__tests__/src/test.spec.ts

This file was deleted.

6 changes: 5 additions & 1 deletion packages/rest/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"pretty": true,

"plugins": [
{
"transform": "../dist/transform/transform.js"
}
],
"baseUrl": ".",
"outDir": "dist",
"rootDir": "src",
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testRegex: "__tests__/dist/.+\\.spec\\.js$",
testRegex: "__tests__/dist/.+\\.spec\\.m?js$",
transform: {
'^.+\\.(ts)$': ['babel-jest', { rootMode: 'upward' }],
}
Expand Down
2 changes: 1 addition & 1 deletion packages/rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "A nornir library",
"version": "1.0.0",
"dependencies": {
"@nrfcloud/ts-json-schema-transformer": "^0.2.1",
"@nrfcloud/ts-json-schema-transformer": "^0.2.2",
"ajv": "^8.12.0",
"openapi-types": "^12.1.0",
"trouter": "^3.2.1",
Expand Down
91 changes: 48 additions & 43 deletions packages/rest/src/runtime/router.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,55 @@ import {AttachmentRegistry, Nornir, Result} from '@nornir/core';
type RouteHandler = (request: Result<HttpRequest>, registry: AttachmentRegistry) => Promise<Result<HttpResponse>>;

export class Router {
private static readonly instance = new Router();

public static get(): Router {
return Router.instance;
}

private readonly router = new Trouter<RouteHandler>();
private readonly routeHolders: RouteHolder[] = [];
private readonly routes: { method: HttpMethod, path: string, builder: RouteBuilder }[] = []

/**
* @internal
*/
public register(route: RouteHolder) {
this.routeHolders.push(route);
}

public static build() {
return Router.get().build();
}

public build(): (request: HttpEvent, registry: AttachmentRegistry) => Promise<HttpResponse> {
for (const routeHolder of this.routeHolders) {
this.routes.push(...routeHolder.routes);
private static readonly instance = new Router();

public static get(): Router {
return Router.instance;
}

private readonly router = new Trouter<RouteHandler>();
private readonly routeHolders: RouteHolder[] = [];
private readonly routes: { method: HttpMethod, path: string, builder: RouteBuilder }[] = []

/**
* @internal
*/
public register(route: RouteHolder) {
this.routeHolders.push(route);
}

public static build() {
return Router.get().build();
}
for (const {method, path, builder} of this.routes) this.router.add(method, path, builder(new Nornir<HttpRequest>()).buildWithContext())
;

return async (event, registry): Promise<HttpResponse> => {
// eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference
const {params, handlers: [handler]} = this.router.find(event.method, event.path);
if (handler == undefined) {
return {
statusCode: HttpStatusCode.NotFound,
body: "Not Found",
headers: {}

public build(): (request: HttpEvent, registry: AttachmentRegistry) => Promise<HttpResponse> {
for (const routeHolder of this.routeHolders) {
this.routes.push(...routeHolder.routes);
}
for (const {
method,
path,
builder
} of this.routes) {
this.router.add(method, path, builder(new Nornir<HttpRequest>()).buildWithContext())
}

return async (event, registry): Promise<HttpResponse> => {
// eslint-disable-next-line unicorn/no-array-method-this-argument, unicorn/no-array-callback-reference
const {params, handlers: [handler]} = this.router.find(event.method, event.path);
if (handler == undefined) {
return {
statusCode: HttpStatusCode.NotFound,
body: "Not Found",
headers: {}
}
}
const request: HttpRequest = {
...event,
pathParams: params,
}
const response = await handler(Result.ok(request), registry);
return response.unwrap();
}
}
const request: HttpRequest = {
...event,
pathParams: params,
}
const response = await handler(Result.ok(request), registry);
return response.unwrap();
}
}
}
9 changes: 9 additions & 0 deletions packages/rest/src/transform/controller-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class ControllerMeta {
private routeHolderIdentifier?: ts.Identifier;
private controllerInstanceIdentifier?: ts.Identifier;
private basePath?: string;
public readonly initializationStatements: ts.Statement[] = [];

// public static getRoutes(): RouteInfo[] {
// const methods = ControllerMeta.routes.values();
Expand Down Expand Up @@ -68,6 +69,14 @@ export class ControllerMeta {
return this.routeHolderIdentifier!;
}

public addInitializationStatement(statement: ts.Statement) {
this.initializationStatements.push(statement);
}

public getInitializationMethod(): ts.ClassStaticBlockDeclaration {
return ts.factory.createClassStaticBlockDeclaration(ts.factory.createBlock(this.initializationStatements, true));
}

public getControllerInstanceIdentifier(): ts.Identifier {
if (!this.isRegistered) {
throw new Error("Controller not registered");
Expand Down
8 changes: 7 additions & 1 deletion packages/rest/src/transform/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ export function getStringLiteralOrConst(project: Project, node: ts.Expression):
return undefined;
}

export interface NornirDecoratorInfo {
decorator: ts.Decorator;
signature: ts.Signature;
declaration: ts.Declaration;
}

export function separateNornirDecorators(
project: Project,
originalDecorators: Readonly<ts.Decorator[]>,
): {
otherDecorators: ts.Decorator[];
nornirDecorators: { decorator: ts.Decorator; signature: ts.Signature; declaration: ts.Declaration }[];
nornirDecorators: NornirDecoratorInfo[];
} {
const nornirDecorators: { decorator: ts.Decorator; signature: ts.Signature; declaration: ts.Declaration }[] = [];
const decorators: ts.Decorator[] = [];
Expand Down
32 changes: 5 additions & 27 deletions packages/rest/src/transform/transformers/class-transformer.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import ts from "typescript";
import { ControllerMeta } from "../controller-meta";
import { separateNornirDecorators } from "../lib";
import { Project } from "../project";
import { ControllerProcessor } from "./decorator-transofmers/controller-processor";
import { ControllerProcessor } from "./processors/controller-processor";

export abstract class ClassTransformer {
public static transform(project: Project, node: ts.ClassDeclaration): ts.ClassDeclaration {
public static transform(project: Project, node: ts.ClassDeclaration, context: ts.TransformationContext): ts.Node {
const originalDecorators = ts.getDecorators(node) || [];
if (!originalDecorators) return node;
const modifiers = ts.getModifiers(node) || [];

const { otherDecorators, nornirDecorators } = separateNornirDecorators(project, originalDecorators);
const { nornirDecorators } = separateNornirDecorators(project, originalDecorators);
if (nornirDecorators.length === 0) return node;

ControllerMeta.create(project, node);

for (const { decorator, declaration } of nornirDecorators) {
const { name } = project.checker.getTypeAtLocation(declaration.parent).symbol;
const processor = CLASS_DECORATOR_PROCESSORS[name];
if (!processor) throw new Error(`No processor for decorator ${name}`);
processor(project, node, decorator);
}

return ts.factory.createClassDeclaration(
[...modifiers, ...otherDecorators],
node.name,
node.typeParameters,
node.heritageClauses,
node.members,
);
return ControllerProcessor.process(project, node, nornirDecorators, context);
}
}

type Task = (project: Project, node: ts.ClassDeclaration, decorator: ts.Decorator) => void;

const CLASS_DECORATOR_PROCESSORS: Record<string, Task> = {
Controller: ControllerProcessor.process,
};
Loading

0 comments on commit b1b7c2c

Please sign in to comment.