Skip to content

Commit

Permalink
feat(openapi): path define and document generation
Browse files Browse the repository at this point in the history
  • Loading branch information
moontai0724 committed May 22, 2024
1 parent 5a324f0 commit 44f2a89
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 126 deletions.
Empty file removed src/index.spec.ts
Empty file.
126 changes: 0 additions & 126 deletions src/openapi.ts

This file was deleted.

76 changes: 76 additions & 0 deletions src/openapi/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { OpenAPIObject, PathItemObject } from "@moontai0724/openapi-types";
import YAML from "json-to-pretty-yaml";

import {
type HttpMethod,
type OperationSchemas,
transformPathItem,
type TransformPathItemOptions,
} from "../transformers";
import { deepMerge } from "../utils/deep-merge";
import { getOrInit } from "../utils/get-or-init";

export interface BasicOpenAPIObject extends Omit<OpenAPIObject, "paths"> {}

export class OpenAPI {
protected operationSchemas: Map<string, OperationSchemas> = new Map();

/**
* Create a new instance to define and generate OpenAPI document.
* @param document OpenAPI document initial value, will be merged with the result of `define` method
* @param options Options for features about this instance
*/
constructor(protected document: BasicOpenAPIObject) {}

/**
* Define an operation for a path, save original schemas and also transform the schemas into OpenAPI format.
* @param path API endpoint path, used as key in paths object.
* @param method HTTP method that this operation is for, will overwrite existing path item if it exists.
* @param operationSchemas Schemas for this operation.
* @param pathItemOptions Additional options to overwrite properties.
* @returns Generated path item object.
*/
public define(
path: `/${string}`,
method: HttpMethod,
operationSchemas: OperationSchemas,
options: TransformPathItemOptions = {},
): PathItemObject {
this.operationSchemas.set(
`${method.toUpperCase()} ${path}`,
operationSchemas,
);

const paths = getOrInit(this.document as OpenAPIObject, "paths", {});
const existingPathItem = paths[path] ?? {};
const pathItemOptions: TransformPathItemOptions = deepMerge(options, {
pathItem: existingPathItem,
});

const pathItem = transformPathItem(
method,
operationSchemas,
pathItemOptions,
);

paths[path] = pathItem;

return pathItem;
}

/**
* Stringify the document to JSON format.
* @returns OpenAPI document string in JSON format.
*/
public json(): string {
return JSON.stringify(this.document);
}

/**
* Stringify the document to YAML format.
* @returns OpenAPI document string in YAML format.
*/
public yaml(): string {
return YAML.stringify(this.document);
}
}
60 changes: 60 additions & 0 deletions src/openapi/json/empty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type {
OpenAPIObject,
OperationObject,
} from "@moontai0724/openapi-types";
import { beforeAll, beforeEach, expect, it, vi } from "vitest";

const expectedOperation: OperationObject = {
parameters: [],
requestBody: {
content: {},
},
responses: {
"200": {
description: "No Description.",
},
},
};

const transformPathItem = vi.fn();

vi.mock("../../transformers", () => ({
transformPathItem,
}));

let OpenAPI: typeof import("..").OpenAPI;

beforeAll(async () => {
OpenAPI = await import("..").then((m) => m.OpenAPI);
});

const baseOpenAPIDocument = {
openapi: "3.1.0",
info: {
title: "Example API",
version: "1.0.0",
},
};

let openapi: InstanceType<typeof OpenAPI>;

beforeEach(() => {
openapi = new OpenAPI(baseOpenAPIDocument);
});

it("should be able to transform empty schemas and pass correct schemas to transformers", () => {
const path = "/";
const method = "patch";

transformPathItem.mockReturnValueOnce({ [method]: expectedOperation });
openapi.define(path, method, {});

const expected = {
...baseOpenAPIDocument,
paths: { [path]: { [method]: expectedOperation } },
} satisfies OpenAPIObject;

expect(JSON.parse(openapi.json())).toEqual(expected);

expect(transformPathItem).toHaveBeenCalledWith(method, {}, { pathItem: {} });
});
85 changes: 85 additions & 0 deletions src/openapi/json/normal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type {
OpenAPIObject,
OperationObject,
} from "@moontai0724/openapi-types";
import { Type } from "@sinclair/typebox";
import { beforeAll, beforeEach, expect, it, vi } from "vitest";

import type { OperationSchemas } from "../../transformers";

const expectedOperation: OperationObject = {
parameters: [],
requestBody: {
content: {},
},
responses: {
"200": {
description: "No Description.",
},
},
};

const transformPathItem = vi.fn();

vi.mock("../../transformers", () => ({
transformPathItem,
}));

let OpenAPI: typeof import("..").OpenAPI;

beforeAll(async () => {
OpenAPI = await import("..").then((m) => m.OpenAPI);
});

const baseOpenAPIDocument = {
openapi: "3.1.0",
info: {
title: "Example API",
version: "1.0.0",
},
};

let openapi: InstanceType<typeof OpenAPI>;

beforeEach(() => {
openapi = new OpenAPI(baseOpenAPIDocument);
});

it("should be able to transform schemas and pass correct schemas to transformers", () => {
const path = "/";
const method = "patch";
const schemas: OperationSchemas = {
body: Type.Object({
body1: Type.String(),
}),
cookie: Type.Object({
cookie1: Type.String(),
}),
header: Type.Object({
header1: Type.String(),
}),
path: Type.Object({
path1: Type.String(),
}),
query: Type.Object({
query1: Type.String(),
}),
response: Type.Object({
response1: Type.String(),
}),
};

transformPathItem.mockReturnValueOnce({ [method]: expectedOperation });
openapi.define(path, method, schemas);

const expected = {
...baseOpenAPIDocument,
paths: { [path]: { [method]: expectedOperation } },
} satisfies OpenAPIObject;

expect(JSON.parse(openapi.json())).toEqual(expected);

expect(transformPathItem).toHaveBeenCalledWith(method, schemas, {
pathItem: {},
});
});
Loading

0 comments on commit 44f2a89

Please sign in to comment.