diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a15427e..1238404 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -28,6 +28,9 @@ jobs: - name: Run linter run: deno lint + - name: Check doc + run: deno task check-doc + - name: Run tests run: | deno test -A --coverage=cov_profile diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 809f30e..a409dba 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,6 +26,9 @@ jobs: - name: Run linter run: deno lint --ignore=\*\*\/\*_test.ts + - name: Check doc + run: deno task check-doc + - name: Run tests run: deno test -A diff --git a/.gitignore b/.gitignore index 4cbe51f..221d00a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ Temporary Items cov_profile cov_profile.lcov +docs diff --git a/.vscode/settings.json b/.vscode/settings.json index d40f5ad..e883e40 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "editor.tabSize": 2, "deno.enable": true, "editor.formatOnSave": true, - "editor.defaultFormatter": "denoland.vscode-deno" + "editor.defaultFormatter": "denoland.vscode-deno", + "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da13cb..8ab96bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +## [0.13.0] - 2025-02-01 + +### Added + +- Laxer usage of `ControllerMethodArgs` decorator: now allowing `queries`, + `params`, `header` as literal arguments, so that things still work even if + users accidentally / deliberately use the undocumented singular / plural forms +- Support for Open API Spec v3.1 +- Support for `operationId` and `tags` in OAS path request declarations +- Support for top-level `tags` in OAS document + +### Changed + +- switched from `deps.ts` and `dev_deps.ts` to `deno.jsonc` +- revamped documentation (JSDoc) +- code format & code format settings for VS Code users +- upgraded dependencies (`zod@^3.24.1`, `@std/assert@^1.0.10`, + `@std/testing@^1.0.8`) +- updated typing for `OakOpenApiSpec` (added prop: `request`, untyped unproven + prop: `requestBody`) +- upgraded dependencies: `jsr:@oak/oak@^17.1.4`, `jsr:@std/assert@^1.0.11`, + `jsr:@std/io@^0.225.2`, `jsr:@std/testing@^1.0.9` + +## Removed + +- the file `jsr.json` is removed in favour of the file `deno.jsonc` + ## [0.12.2] - 2024-12-06 ### Added diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..644b9e9 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,46 @@ +{ + "name": "@dklab/oak-routing-ctrl", + "version": "0.13.0", + "exports": { + ".": "./mod.ts", + "./mod": "./mod.ts" + }, + "publish": { + "exclude": [ + "./test_utils", + "**/*_test.ts", + "./CONTRIBUTING.md", + "./GOVERNANCE.md" + ] + }, + "tasks": { + "pretty": "deno lint --ignore=docs && deno check . && deno fmt", + "test": "deno test -RE", + "check-doc": "deno check --doc .", + "doc": "deno doc --html mod.ts" + }, + "imports": { + "@asteasolutions/zod-to-openapi": "npm:@asteasolutions/zod-to-openapi@^7.3.0", + "@oak/oak": "jsr:@oak/oak@^17.1.4", + "@std/assert": "jsr:@std/assert@^1.0.11", + "@std/io": "jsr:@std/io@^0.225.2", + "@std/path": "jsr:@std/path@^1.0.8", + "@std/testing": "jsr:@std/testing@^1.0.9", + "zod": "npm:zod@^3.24.1" + }, + "fmt": { + "useTabs": false, + "indentWidth": 2, + "semiColons": true, + "singleQuote": false, + "proseWrap": "always" + }, + "exclude": [ + "./docs", + "cov_profile", + "cov_profile.lcov", + "**/__snapshots__", + ".github", + ".vscode" + ] +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..7e46502 --- /dev/null +++ b/deno.lock @@ -0,0 +1,147 @@ +{ + "version": "4", + "specifiers": { + "jsr:@oak/commons@1": "1.0.0", + "jsr:@oak/oak@^17.1.4": "17.1.4", + "jsr:@std/assert@1": "1.0.11", + "jsr:@std/assert@^1.0.10": "1.0.11", + "jsr:@std/assert@^1.0.11": "1.0.11", + "jsr:@std/bytes@1": "1.0.5", + "jsr:@std/bytes@^1.0.5": "1.0.5", + "jsr:@std/crypto@1": "1.0.4", + "jsr:@std/encoding@1": "1.0.7", + "jsr:@std/encoding@^1.0.7": "1.0.7", + "jsr:@std/fs@^1.0.9": "1.0.11", + "jsr:@std/http@1": "1.0.13", + "jsr:@std/internal@^1.0.5": "1.0.5", + "jsr:@std/io@~0.225.2": "0.225.2", + "jsr:@std/media-types@1": "1.1.0", + "jsr:@std/path@1": "1.0.8", + "jsr:@std/path@^1.0.8": "1.0.8", + "jsr:@std/testing@^1.0.9": "1.0.9", + "npm:@asteasolutions/zod-to-openapi@^7.3.0": "7.3.0_zod@3.24.1", + "npm:@types/node@*": "22.5.4", + "npm:path-to-regexp@^6.3.0": "6.3.0", + "npm:zod@^3.24.1": "3.24.1" + }, + "jsr": { + "@oak/commons@1.0.0": { + "integrity": "49805b55603c3627a9d6235c0655aa2b6222d3036b3a13ff0380c16368f607ac", + "dependencies": [ + "jsr:@std/assert@1", + "jsr:@std/bytes@1", + "jsr:@std/crypto", + "jsr:@std/encoding@1", + "jsr:@std/http", + "jsr:@std/media-types" + ] + }, + "@oak/oak@17.1.4": { + "integrity": "60530b582bf276ff741e39cc664026781aa08dd5f2bc5134d756cc427bf2c13e", + "dependencies": [ + "jsr:@oak/commons", + "jsr:@std/assert@1", + "jsr:@std/bytes@1", + "jsr:@std/http", + "jsr:@std/media-types", + "jsr:@std/path@1", + "npm:path-to-regexp" + ] + }, + "@std/assert@1.0.11": { + "integrity": "2461ef3c368fe88bc60e186e7744a93112f16fd110022e113a0849e94d1c83c1", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/bytes@1.0.5": { + "integrity": "4465dd739d7963d964c809202ebea6d5c6b8e3829ef25c6a224290fbb8a1021e" + }, + "@std/crypto@1.0.4": { + "integrity": "cee245c453bd5366207f4d8aa25ea3e9c86cecad2be3fefcaa6cb17203d79340" + }, + "@std/encoding@1.0.7": { + "integrity": "f631247c1698fef289f2de9e2a33d571e46133b38d042905e3eac3715030a82d" + }, + "@std/fs@1.0.11": { + "integrity": "ba674672693340c5ebdd018b4fe1af46cb08741f42b4c538154e97d217b55bdd", + "dependencies": [ + "jsr:@std/path@^1.0.8" + ] + }, + "@std/http@1.0.13": { + "integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e", + "dependencies": [ + "jsr:@std/encoding@^1.0.7" + ] + }, + "@std/internal@1.0.5": { + "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" + }, + "@std/io@0.225.2": { + "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7", + "dependencies": [ + "jsr:@std/bytes@^1.0.5" + ] + }, + "@std/media-types@1.1.0": { + "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/testing@1.0.9": { + "integrity": "9bdd4ac07cb13e7594ac30e90f6ceef7254ac83a9aeaa089be0008f33aab5cd4", + "dependencies": [ + "jsr:@std/assert@^1.0.10", + "jsr:@std/fs", + "jsr:@std/internal", + "jsr:@std/path@^1.0.8" + ] + } + }, + "npm": { + "@asteasolutions/zod-to-openapi@7.3.0_zod@3.24.1": { + "integrity": "sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==", + "dependencies": [ + "openapi3-ts", + "zod" + ] + }, + "@types/node@22.5.4": { + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "dependencies": [ + "undici-types" + ] + }, + "openapi3-ts@4.4.0": { + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "dependencies": [ + "yaml" + ] + }, + "path-to-regexp@6.3.0": { + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, + "undici-types@6.19.8": { + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "yaml@2.7.0": { + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==" + }, + "zod@3.24.1": { + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@oak/oak@^17.1.4", + "jsr:@std/assert@^1.0.11", + "jsr:@std/io@~0.225.2", + "jsr:@std/path@^1.0.8", + "jsr:@std/testing@^1.0.9", + "npm:@asteasolutions/zod-to-openapi@^7.3.0", + "npm:zod@^3.24.1" + ] + } +} diff --git a/dev_deps.ts b/dev_deps.ts deleted file mode 100644 index f65be58..0000000 --- a/dev_deps.ts +++ /dev/null @@ -1,41 +0,0 @@ -export { - assert, - assertEquals, - assertInstanceOf, - assertObjectMatch, - assertStringIncludes, - assertThrows, -} from "jsr:@std/assert@^1.0.7"; - -export { - type BodyType, - type Middleware, - Request, - testing as oakTesting, -} from "jsr:@oak/oak@^17.1.3"; - -export { Body } from "jsr:@oak/oak@^17.1.3/body"; - -export { - assertSpyCall, - assertSpyCallArg, - assertSpyCalls, - type MethodSpy, - type Spy, - spy, - type Stub, - stub, -} from "jsr:@std/testing@^1.0.4/mock"; - -export { assertSnapshot } from "jsr:@std/testing@^1.0.4/snapshot"; - -export { Buffer } from "jsr:@std/io@^0.225.0"; - -export { - afterEach, - beforeEach, - describe, - it, -} from "jsr:@std/testing@^1.0.4/bdd"; - -export { ZodObject } from "npm:zod@^3.23.8"; diff --git a/jsr.json b/jsr.json deleted file mode 100644 index 2acb861..0000000 --- a/jsr.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "@dklab/oak-routing-ctrl", - "version": "0.12.2", - "exports": { - ".": "./mod.ts", - "./mod": "./mod.ts" - }, - "exclude": [ - "./test_utils", - "**/*_test.ts", - "cov_profile", - "cov_profile.lcov", - "dev_deps.ts", - "**/__snapshots__", - ".github", - ".vscode", - "./CONTRIBUTING.md", - "./GOVERNANCE.md" - ] -} diff --git a/mod.ts b/mod.ts index b644165..cd43c36 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,5 @@ -export { useOakServer } from "./src/useOakServer.ts"; -export { useOakServer as useOak } from "./src/useOakServer.ts"; -export { useOas } from "./src/useOas.ts"; +export { useOak, useOakServer } from "./src/useOakServer.ts"; +export { useOas, type UseOasConfig } from "./src/useOas.ts"; export { Controller } from "./src/Controller.ts"; export { type ControllerMethodArg, @@ -14,4 +13,8 @@ export { Delete } from "./src/Delete.ts"; export { Options } from "./src/Options.ts"; export { Head } from "./src/Head.ts"; -export { type OakOpenApiSpec, z, type zInfer } from "./deps.ts"; +export { + type OakOpenApiSpec, + z, + type zInfer, +} from "./src/utils/schema_utils.ts"; diff --git a/src/Controller.ts b/src/Controller.ts index 6d108c9..a9d0e87 100644 --- a/src/Controller.ts +++ b/src/Controller.ts @@ -1,20 +1,31 @@ +import { join } from "@std/path"; import { debug } from "./utils/logger.ts"; -import { join } from "../deps.ts"; import { store } from "./Store.ts"; import { patchOasPath } from "./oasStore.ts"; /** - * Just a standard Class, that can be decorated with the `@Controller` decorator + * Just a standard Class, that can be decorated with the {@linkcode Controller} decorator */ export type ControllerClass = new (args?: unknown) => unknown; +type ClassDecorator = ( + target: ControllerClass, + context?: ClassDecoratorContext, +) => void; + /** * Decorator that should be used on the Controller Class * @NOTE under `experimentalDecorators`, `context` is not available + * @example + * ```ts + * ;@Controller("/api/") + * class ExampleClass { + * // functions that handle endpoints starting with `/api/` + * } + * ``` */ export const Controller = - (pathPrefix: string = "") => - (target: ControllerClass, context?: ClassDecoratorContext): void => { + (pathPrefix: string = ""): ClassDecorator => (target, context): void => { debug( `invoking ControllerDecorator for ${target.name} -`, "runtime provides context:", diff --git a/src/ControllerMethodArgs.ts b/src/ControllerMethodArgs.ts index 8276c78..90fd90a 100644 --- a/src/ControllerMethodArgs.ts +++ b/src/ControllerMethodArgs.ts @@ -1,16 +1,18 @@ import { debug } from "./utils/logger.ts"; -import { type Context, type RouteContext } from "../deps.ts"; +import type { Context, RouteContext } from "@oak/oak"; import { ERR_UNSUPPORTED_CLASS_METHOD_DECORATOR_RUNTIME_BEHAVIOR } from "./Constants.ts"; /** - * literal keywords that can be used as arguments for the `@ControllerMethodArgs` + * literal keywords that can be used as arguments for the {@linkcode ControllerMethodArgs} * decorator; these keywords **MUST** appear in the same order that their counterpart * arguments show up in the actual (decorated) handler function * @example * ```ts - * (@)ControllerMethodArgs("query", "body") - * doSomething(query, body) { - * console.log(query, body); + * class ExampleClass { + * ;@ControllerMethodArgs("query", "body") + * doSomething(query: object, body: object) { + * console.log(query, body); + * } * } * ``` */ @@ -27,36 +29,44 @@ type EnhancedHandler = ( ...args: unknown[] ) => Promise; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, + // deno-lint-ignore no-explicit-any + ...rest: any[] + // deno-lint-ignore no-explicit-any +) => any; + /** * Decorator that should be used on the Controller Method * when we need to refer to the request body, param, query, etc. * in the Method Body * @returns a function with the signature (arguments) matching that * of the provided `desirableParams`, which can then be **decorated** - * with one of the Class Method decorators (e.g. `Get`, `Post`, etc.) + * with one of the Class Method decorators (e.g. {@linkcode Get}, {@linkcode Post}, etc.) * @example - * ```ts - * (@)Controller("/api/v1") + * ``` + * import { Controller, Post, ControllerMethodArgs } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() * class MyClass { - * (@)Post("/:resource") - * (@)ControllerMethodArgs("body", "query", "param") - * doSomething(body, query, param, ctx): void { + * ;@Post("/:resource") + * ;@ControllerMethodArgs("body", "query", "param", "headers") + * doSomething(body, query, param, headers, ctx): void { * console.log(`endpoint called: /api/v1/${ctx.params.resource}`); - * console.log(`now let's do something with`, body, query, param); + * console.log(`now let's do something with`, body, query, param, headers); * } * } * ``` */ export const ControllerMethodArgs = - (...desirableParams: ControllerMethodArg[]) => + (...desirableParams: ControllerMethodArg[]): MethodDecorator => ( - // deno-lint-ignore ban-types - arg1: Function | object, - arg2: ClassMethodDecoratorContext | string, - // deno-lint-ignore no-explicit-any - ...rest: any[] - // deno-lint-ignore no-explicit-any - ): any => { + arg1, + arg2, + ...rest + ) => { if (typeof arg1 === "function" && typeof arg2 === "object") { return decorateClassMethodTypeStandard( arg1, @@ -170,6 +180,7 @@ function getEnhancedHandler( for (const p of consumerDesirableParams) { switch (true) { case p === "param": + case p === "params" as ControllerMethodArg: // undocumented on purpose // path param decoratedArgs.push(ctx.params); break; @@ -178,10 +189,13 @@ function getEnhancedHandler( decoratedArgs.push(parsedReqBody); break; case p === "query": + case p === "queries" as ControllerMethodArg: // undocumented on purpose // search query a.k.a URLSearchParams decoratedArgs.push(parsedReqSearchParams); break; - case p === "headers": { + case p === "headers": + case p === "header" as ControllerMethodArg: // undocumented on purpose + { // request headers const headers: Record = {}; ctx.request.headers.forEach((v: string, k: string) => headers[k] = v); diff --git a/src/ControllerMethodArgs_test.ts b/src/ControllerMethodArgs_test.ts index f550b88..9d4d8f5 100644 --- a/src/ControllerMethodArgs_test.ts +++ b/src/ControllerMethodArgs_test.ts @@ -1,14 +1,8 @@ -import { - assertEquals, - assertSpyCalls, - assertStringIncludes, - assertThrows, - Body, - type BodyType, - Buffer, - oakTesting, - spy, -} from "../dev_deps.ts"; +import { type BodyType, testing as oakTesting } from "@oak/oak"; +import { Body } from "@oak/oak/body"; +import { assertEquals, assertStringIncludes, assertThrows } from "@std/assert"; +import { assertSpyCalls, spy } from "@std/testing/mock"; +import { Buffer } from "@std/io"; import { ERR_UNSUPPORTED_CLASS_METHOD_DECORATOR_RUNTIME_BEHAVIOR } from "./Constants.ts"; import { _internal, @@ -893,6 +887,44 @@ Deno.test("getEnhancedHandler - not declaring any param", async () => { assertSpyCalls(testHandler, 1); }); +Deno.test("getEnhancedHandler - declaring undocumented params", async () => { + const spyParseOakRequestBody = spy(_internal, "parseOakReqBody"); + // deno-lint-ignore no-explicit-any + const testHandler = spy((..._rest: any[]) => 44); + // deno-lint-ignore ban-types + const enhancedHandler: Function = _internal.getEnhancedHandler( + testHandler, + "context" as ControllerMethodArg, + "params" as ControllerMethodArg, + "queries" as ControllerMethodArg, + "header" as ControllerMethodArg, + "hiddenFeature" as ControllerMethodArg, + ); + const ctx = createMockContext({ + path: "/hello/world", + params: { lorem: "undocumented usage", hiddenFeature: "84" }, + headers: [["X-Foo", "Bearer Bar"]], + }); + Object.defineProperty(ctx.request, "body", { + get: () => createMockRequestBody("binary"), + }); + Object.defineProperty(ctx.request.url, "searchParams", { + value: new Map([["ipsum", "dolor"]]), + }); + await enhancedHandler(ctx); + const [context, param, query, headers, hiddenFeature] = + testHandler.calls[0].args; + assertEquals(param, { lorem: "undocumented usage", hiddenFeature: "84" }); + assertEquals(query, { ipsum: "dolor" }); + assertEquals(hiddenFeature, "84"); + assertEquals(context, ctx); + assertEquals(headers, { "x-foo": "Bearer Bar" }); + assertEquals(testHandler.calls[0].returned, 44); + assertSpyCalls(testHandler, 1); + assertSpyCalls(spyParseOakRequestBody, 1); + spyParseOakRequestBody.restore(); +}); + /** * @NOTE if/when `oak` supports such a method, better import from there instead */ diff --git a/src/Delete.ts b/src/Delete.ts index 9fae93a..0ef772d 100644 --- a/src/Delete.ts +++ b/src/Delete.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for DELETE endpoints + * for `DELETE` endpoints + * @example + * ```ts + * import { Controller, Delete } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Delete("/:resource") + * async deleteSomething() { + * // implementation + * } + * } + * ``` */ export const Delete = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Delete MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Delete_test.ts b/src/Delete_test.ts index ba99825..20f4ab8 100644 --- a/src/Delete_test.ts +++ b/src/Delete_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Delete.ts"; diff --git a/src/Get.ts b/src/Get.ts index 4368081..9fb8d7d 100644 --- a/src/Get.ts +++ b/src/Get.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for GET endpoints + * for `GET` endpoints + * @example + * ```ts + * import { Controller, Get } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Get("/") + * async retrieveSomething() { + * // implementation + * } + * } + * ``` */ export const Get = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Get MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Get_test.ts b/src/Get_test.ts index 822d52c..3d6f599 100644 --- a/src/Get_test.ts +++ b/src/Get_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Get.ts"; diff --git a/src/Head.ts b/src/Head.ts index b8ca4b5..2609970 100644 --- a/src/Head.ts +++ b/src/Head.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for HEAD endpoints + * for `HEAD` endpoints + * @example + * ```ts + * import { Controller, Head } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Head("/") + * async lookupSomething() { + * // implementation + * } + * } + * ``` */ export const Head = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Head MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Head_test.ts b/src/Head_test.ts index 9268360..533af72 100644 --- a/src/Head_test.ts +++ b/src/Head_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Head.ts"; diff --git a/src/Options.ts b/src/Options.ts index bf433a5..6bf2b8e 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for OPTIONS endpoints + * for `OPTIONS` endpoints + * @example + * ```ts + * import { Controller, Options } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Options("/") + * async doSomething() { + * // implementation + * } + * } + * ``` */ export const Options = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Options MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Options_test.ts b/src/Options_test.ts index 042c79f..9a9aab1 100644 --- a/src/Options_test.ts +++ b/src/Options_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Options.ts"; diff --git a/src/Patch.ts b/src/Patch.ts index 9e5f04f..cd225c4 100644 --- a/src/Patch.ts +++ b/src/Patch.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for PATCH endpoints + * for `PATCH` endpoints + * @example + * ```ts + * import { Controller, Patch } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Patch("/") + * async adjustSomething() { + * // implementation + * } + * } + * ``` */ export const Patch = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Patch MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Patch_test.ts b/src/Patch_test.ts index eb8d2d0..7f7b086 100644 --- a/src/Patch_test.ts +++ b/src/Patch_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Patch.ts"; diff --git a/src/Post.ts b/src/Post.ts index bbff422..97a40bc 100644 --- a/src/Post.ts +++ b/src/Post.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for POST endpoints + * for `POST` endpoints + * @example + * ```ts + * import { Controller, Post } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Post("/") + * async updateSomething() { + * // implementation + * } + * } + * ``` */ export const Post = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Post MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Post_test.ts b/src/Post_test.ts index f42d63c..d3314c4 100644 --- a/src/Post_test.ts +++ b/src/Post_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Post.ts"; diff --git a/src/Put.ts b/src/Put.ts index 1706fd7..4bc5d9d 100644 --- a/src/Put.ts +++ b/src/Put.ts @@ -1,19 +1,36 @@ import { debug } from "./utils/logger.ts"; import { register } from "./Store.ts"; import { getUserSuppliedDecoratedMethodName } from "./utils/getUserSuppliedDecoratedMethodName.ts"; -import { type OakOpenApiSpec } from "../deps.ts"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; import { updateOas } from "./oasStore.ts"; +type MethodDecorator = ( + // deno-lint-ignore ban-types + arg1: Function | object, + arg2: ClassMethodDecoratorContext | string, +) => void; + /** * Decorator that should be used on the Controller Class Method - * for PUT endpoints + * for `PUT` endpoints + * @example + * ```ts + * import { Controller, Put } from "@dklab/oak-routing-ctrl" + * + * ;@Controller() + * class ExampleClass { + * ;@Put("/") + * async updateSomething() { + * // implementation + * } + * } + * ``` */ export const Put = ( path: string = "", openApiSpec?: OakOpenApiSpec, -) => -// deno-lint-ignore ban-types -(arg1: Function | object, arg2: ClassMethodDecoratorContext | string): void => { +): MethodDecorator => +(arg1, arg2): void => { const fnName: string = getUserSuppliedDecoratedMethodName(arg1, arg2); debug( `invoking Put MethodDecorator for ${fnName} with pathPrefix ${path} -`, diff --git a/src/Put_test.ts b/src/Put_test.ts index 4a4972f..5a82f5b 100644 --- a/src/Put_test.ts +++ b/src/Put_test.ts @@ -1,11 +1,10 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; import { - assertEquals, - assertInstanceOf, assertSpyCall, assertSpyCalls, type MethodSpy, spy, -} from "../dev_deps.ts"; +} from "@std/testing/mock"; import { store } from "./Store.ts"; import { _internal } from "./Put.ts"; diff --git a/src/Store_test.ts b/src/Store_test.ts index 4a62589..4a32646 100644 --- a/src/Store_test.ts +++ b/src/Store_test.ts @@ -1,9 +1,5 @@ -import { - assertEquals, - assertSpyCall, - assertSpyCalls, - spy, -} from "../dev_deps.ts"; +import { assertEquals } from "@std/assert"; +import { assertSpyCall, assertSpyCalls, spy } from "@std/testing/mock"; import { register, store } from "./Store.ts"; Deno.test("Store", () => { diff --git a/src/__snapshots__/useOakServer_test.ts.snap b/src/__snapshots__/useOakServer_test.ts.snap index 2e4a738..8c1d08a 100644 --- a/src/__snapshots__/useOakServer_test.ts.snap +++ b/src/__snapshots__/useOakServer_test.ts.snap @@ -57,6 +57,26 @@ snapshot[`useOakServer - fully decorated Controller 1`] = ` path: "/test/baz/:zaz", regexp: /^\\/test\\/baz(?:\\/([^\\/#\\?]+?))[\\/#\\?]?\$/i, }, + { + methods: [ + "HEAD", + "GET", + ], + middleware: [ + [AsyncFunction (anonymous)], + ], + options: { + end: undefined, + ignoreCaptures: undefined, + sensitive: undefined, + strict: undefined, + }, + paramNames: [ + "zaz_zaz", + ], + path: "/test/dolor/:zaz_zaz", + regexp: /^\\/test\\/dolor(?:\\/([^\\/#\\?]+?))[\\/#\\?]?\$/i, + }, { methods: [ "PUT", diff --git a/src/__snapshots__/useOas_test.ts.snap b/src/__snapshots__/useOas_test.ts.snap index 6371bf0..b8fbe9c 100644 --- a/src/__snapshots__/useOas_test.ts.snap +++ b/src/__snapshots__/useOas_test.ts.snap @@ -1,6 +1,6 @@ export const snapshot = {}; -snapshot[`useOas standard behavior > testApiDocSnapshot 1`] = ` +snapshot[`useOas standard behavior - OpenApi v3.0 > testApiDocSnapshot 1`] = ` { components: { parameters: {}, @@ -15,6 +15,7 @@ snapshot[`useOas standard behavior > testApiDocSnapshot 1`] = ` paths: { "/hello/{name}": { post: { + operationId: undefined, parameters: [ { in: "path", @@ -26,6 +27,7 @@ snapshot[`useOas standard behavior > testApiDocSnapshot 1`] = ` }, ], responses: {}, + tags: undefined, }, }, }, @@ -34,5 +36,65 @@ snapshot[`useOas standard behavior > testApiDocSnapshot 1`] = ` url: "/mock/", }, ], + tags: [ + { + description: "Example description for Example Section", + externalDocs: { + url: "http://localhost", + }, + name: "Example Section", + }, + ], +} +`; + +snapshot[`useOas standard behavior - OpenApi v3.1 > testApiDocSnapshot 1`] = ` +{ + components: { + parameters: {}, + schemas: {}, + }, + info: { + description: "this is a mock API", + title: "mock API", + version: "0.1.0", + }, + openapi: "3.1.0", + paths: { + "/hello/{name}": { + post: { + operationId: "my-unique-test-op-id", + parameters: [ + { + in: "path", + name: "name", + required: true, + schema: { + type: "string", + }, + }, + ], + responses: {}, + tags: [ + "Example Section", + ], + }, + }, + }, + servers: [ + { + url: "/mock/", + }, + ], + tags: [ + { + description: "Example description for Example Section", + externalDocs: { + url: "http://localhost", + }, + name: "Example Section", + }, + ], + webhooks: {}, } `; diff --git a/src/oasStore.ts b/src/oasStore.ts index 2feacdb..d802a66 100644 --- a/src/oasStore.ts +++ b/src/oasStore.ts @@ -1,9 +1,14 @@ -import { type OakOpenApiSpec, type RouteConfig } from "../deps.ts"; -import { SupportedVerb } from "./Store.ts"; +import type { RouteConfig } from "@asteasolutions/zod-to-openapi"; +import type { OakOpenApiSpec } from "./utils/schema_utils.ts"; +import type { SupportedVerb } from "./Store.ts"; import { debug } from "./utils/logger.ts"; +type TheRouteConfig = RouteConfig & { + tags?: string[]; +}; + // fnName|method|path => OasRouteConfig -export const oasStore: Map = new Map(); +export const oasStore: Map = new Map(); const getRouteId = ( fnName: string, @@ -61,6 +66,8 @@ export const updateOas = ( ...existing.responses, ...specs?.responses, }, + operationId: specs?.operationId, + tags: specs?.tags, }; debug(`OpenApiSpec: recording for [${method}] ${path}`); diff --git a/src/oasStore_test.ts b/src/oasStore_test.ts index f8aeb02..c612e1c 100644 --- a/src/oasStore_test.ts +++ b/src/oasStore_test.ts @@ -1,5 +1,6 @@ -import { z } from "../deps.ts"; -import { assertEquals, assertInstanceOf, ZodObject } from "../dev_deps.ts"; +import { z } from "./utils/schema_utils.ts"; +import { assertEquals, assertInstanceOf } from "@std/assert"; +import { ZodObject } from "zod"; import { oasStore, patchOasPath, updateOas } from "./oasStore.ts"; import { _internal } from "./oasStore.ts"; diff --git a/src/useOakServer.ts b/src/useOakServer.ts index 7834106..d267949 100644 --- a/src/useOakServer.ts +++ b/src/useOakServer.ts @@ -1,5 +1,6 @@ import { debug } from "./utils/logger.ts"; -import { type Application, Router, Status, z } from "../deps.ts"; +import { type Application, Router, Status } from "@oak/oak"; +import { z } from "./utils/schema_utils.ts"; import type { ControllerClass } from "./Controller.ts"; import { store } from "./Store.ts"; @@ -52,4 +53,9 @@ export const useOakServer = ( app.use(oakRouter.allowedMethods()); }; +/** + * alias of {@linkcode useOakServer} + */ +export const useOak = useOakServer; + export const _internal = { oakRouter }; diff --git a/src/useOakServer_test.ts b/src/useOakServer_test.ts index 2545242..394e174 100644 --- a/src/useOakServer_test.ts +++ b/src/useOakServer_test.ts @@ -2,18 +2,14 @@ import type { SupportedVerb } from "./Store.ts"; import { type Context, type ErrorStatus, - RouteContext, + type RouteContext, Status, - z, -} from "../deps.ts"; -import { - assertEquals, - assertSnapshot, - assertSpyCallArg, - assertSpyCalls, - oakTesting, - spy, -} from "../dev_deps.ts"; + testing as oakTesting, +} from "@oak/oak"; +import { z } from "./utils/schema_utils.ts"; +import { assertEquals } from "@std/assert"; +import { assertSpyCallArg, assertSpyCalls, spy } from "@std/testing/mock"; +import { assertSnapshot } from "@std/testing/snapshot"; import { Controller, type ControllerMethodArg, @@ -70,6 +66,11 @@ class TestController { baz(query: Record, param: Record) { return `hello, path /baz/${param.zaz} with query ${query.someKey}`; } + @Get("/dolor/:zaz_zaz") + @ControllerMethodArgs("params" as ControllerMethodArg) // intentional coercing to test undocumented usage + dolor(param: Record) { + return `hello, path /dolor/${param.zaz_zaz}`; + } @Put("/taz/:someId") @ControllerMethodArgs("body") taz(body: ArrayBuffer, ctx: RouteContext<"/taz/:someId">) { @@ -192,6 +193,12 @@ Deno.test({ mockRequestPathParams: { zaz: "jaz" }, expectedResponse: "hello, path /baz/jaz with query chaz", }, + { + caseDescription: "handler with 'params' as undocumented usage", + method: "get", + mockRequestPathParams: { zaz_zaz: "jaz_jaz" }, + expectedResponse: "hello, path /dolor/jaz_jaz", + }, { caseDescription: "handler for a request with a binary payload", method: "put", diff --git a/src/useOas.ts b/src/useOas.ts index 60dc67d..670b7de 100644 --- a/src/useOas.ts +++ b/src/useOas.ts @@ -1,11 +1,12 @@ -import { Application } from "../deps.ts"; +import type { Application } from "@oak/oak"; import { oasStore } from "./oasStore.ts"; import { OpenApiGeneratorV3, - type OpenAPIObjectConfig, + OpenApiGeneratorV31, OpenAPIRegistry, type RouteConfig, -} from "../deps.ts"; +} from "@asteasolutions/zod-to-openapi"; +import type { OpenAPIObjectConfig } from "@asteasolutions/zod-to-openapi/dist/v3.0/openapi-generator"; import { debug } from "./utils/logger.ts"; import { inspect } from "./utils/inspect.ts"; @@ -26,10 +27,19 @@ const defaultOasUiTemplate = ` const registry = new OpenAPIRegistry(); -type UseOasConfig = Partial & { +/** + * interface for an object used to configure how the Open API Spec + * is generated (e.g. `/oas.json`) + */ +export type UseOasConfig = Partial & { jsonPath?: string; uiPath?: string; uiTemplate?: string; + tags?: { + name: string; + description?: string; + externalDocs?: { url: string }; + }[]; }; type UseOas = ( @@ -58,7 +68,11 @@ const _useOas: UseOas = ( } }); - const generator = new OpenApiGeneratorV3(registry.definitions); + const OpenApiGenerator = oasCfg.openapi?.startsWith("3.0") + ? OpenApiGeneratorV3 + : OpenApiGeneratorV31; + + const generator = new OpenApiGenerator(registry.definitions); const apiDoc = generator.generateDocument({ openapi: "3.0.0", info: { @@ -85,9 +99,9 @@ const _useOas: UseOas = ( /** * helper method to enable Open API Spec for the routes * declared with oak-routing-ctrl decorators - * @param app the oak Application instance - * @param cfg optional configuration object to - * finetune the OAS spec documentation + * @param {Application} app the {@linkcode Application} instance from `@oak/oak` + * @param {UseOasConfig} [cfg] optional configuration object to + * finetune the Open API spec documentation */ export const useOas = ( app: Application, diff --git a/src/useOas_test.ts b/src/useOas_test.ts index 4bfd90e..efe441c 100644 --- a/src/useOas_test.ts +++ b/src/useOas_test.ts @@ -1,14 +1,12 @@ +import { type Middleware, testing as oakTesting } from "@oak/oak"; +import { assertEquals } from "@std/assert"; +import { assertSpyCall, assertSpyCalls, spy, stub } from "@std/testing/mock"; +import { assertSnapshot } from "@std/testing/snapshot"; import { - assertEquals, - assertSnapshot, - assertSpyCall, - assertSpyCalls, - type Middleware, - oakTesting, - spy, - stub, -} from "../dev_deps.ts"; -import { OpenApiGeneratorV3, z } from "../deps.ts"; + OpenApiGeneratorV3, + OpenApiGeneratorV31, +} from "@asteasolutions/zod-to-openapi"; +import { z } from "./utils/schema_utils.ts"; import { oasStore, updateOas } from "./oasStore.ts"; import { _internal, useOas } from "./useOas.ts"; import { mockRequestInternals } from "../test_utils/mockRequestInternals.ts"; @@ -34,7 +32,8 @@ Deno.test("useOas with a non-conforming Application instance", () => { ' info: { version: "1.0.0", title: "My API", description: "This is the API" },\n' + ' servers: [ { url: "/" } ],\n' + " components: { schemas: {}, parameters: {} },\n" + - " paths: {}\n" + + " paths: {},\n" + + " webhooks: {}\n" + "}", ], }); @@ -78,7 +77,7 @@ Deno.test("useOas with empty definitions", () => { assertEquals(_internal.registry.definitions, []); }); -Deno.test("useOas standard behavior", async (t) => { +Deno.test("useOas standard behavior - OpenApi v3.0", async (t) => { const fnName = "doSomething"; const method = "post"; const path = "/hello/:name"; @@ -103,6 +102,13 @@ Deno.test("useOas standard behavior", async (t) => { description: "this is a mock API", }, servers: [{ url: "/mock/" }], + tags: [{ + name: "Example Section", + description: "Example description for Example Section", + externalDocs: { + url: "http://localhost", + }, + }], }; let apiDoc; @@ -153,3 +159,88 @@ Deno.test("useOas standard behavior", async (t) => { ), ); }); + +Deno.test("useOas standard behavior - OpenApi v3.1", async (t) => { + const fnName = "doSomething"; + const method = "post"; + const path = "/hello/:name"; + + updateOas(fnName, method, path, { + request: { + params: z.object({ name: z.string() }), + }, + operationId: "my-unique-test-op-id", + tags: ["Example Section"], + }); + + const uiPath = "/my/swagger-31"; + const uiTemplate = "mock"; + const jsonPath = "/my/swagger-31/json"; + const oasConfig = { + uiPath, + uiTemplate, + jsonPath, + openapi: "3.1.0", + info: { + version: "0.1.0", + title: "mock API", + description: "this is a mock API", + }, + servers: [{ url: "/mock/" }], + tags: [{ + name: "Example Section", + description: "Example description for Example Section", + externalDocs: { + url: "http://localhost", + }, + }], + }; + let apiDoc; + + await t.step(async function testApiDocSnapshot(t) { + const mockCtx = createMockContext(); + useOas(mockCtx.app, oasConfig); + const generator = new OpenApiGeneratorV31(_internal.registry.definitions); + const { + jsonPath: _jsonPath, + uiPath: _uiPath, + uiTemplate: _uiTemplate, + ...oasCfg + } = oasConfig; + apiDoc = generator.generateDocument(oasCfg); + await assertSnapshot(t, apiDoc); + }); + + const cases: { mockRequestPath: string; expectedResponseBody: unknown }[] = [{ + mockRequestPath: uiPath, + expectedResponseBody: uiTemplate, + }, { + mockRequestPath: jsonPath, + expectedResponseBody: apiDoc, + }]; + + await Promise.all( + cases.map(({ mockRequestPath, expectedResponseBody }) => + t.step({ + name: `request oasMiddleware at ${mockRequestPath}`, + fn: async () => { + const mockCtx = createMockContext(); + mockRequestInternals(mockCtx.request, { mockRequestPath }); + const mockNxt = spy(() => Promise.resolve()); + const spyAppUse = spy(mockCtx.app, "use"); + + useOas(mockCtx.app, oasConfig); + + const oasMiddleware = spyAppUse.calls[0].args[0] as Middleware; + await oasMiddleware(mockCtx, mockNxt); + + assertSpyCalls(mockNxt, 1); + assertEquals(mockCtx.response.body, expectedResponseBody); + }, + sanitizeOps: false, + sanitizeResources: false, + sanitizeExit: false, + }) + ), + ); +}); diff --git a/src/utils/getUserSuppliedDecoratedMethodName_test.ts b/src/utils/getUserSuppliedDecoratedMethodName_test.ts index 1b1666e..3d535d7 100644 --- a/src/utils/getUserSuppliedDecoratedMethodName_test.ts +++ b/src/utils/getUserSuppliedDecoratedMethodName_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertThrows } from "../../dev_deps.ts"; +import { assertEquals, assertThrows } from "@std/assert"; import { ERR_UNSUPPORTED_CLASS_METHOD_DECORATOR_RUNTIME_BEHAVIOR } from "../Constants.ts"; import { getUserSuppliedDecoratedMethodName } from "./getUserSuppliedDecoratedMethodName.ts"; diff --git a/src/utils/inspect_test.ts b/src/utils/inspect_test.ts index d51567f..7f0c7c6 100644 --- a/src/utils/inspect_test.ts +++ b/src/utils/inspect_test.ts @@ -1,4 +1,4 @@ -import { assertSnapshot } from "../../dev_deps.ts"; +import { assertSnapshot } from "@std/testing/snapshot"; import { inspect } from "./inspect.ts"; const testObj = { diff --git a/src/utils/logger_test.ts b/src/utils/logger_test.ts index a8b2fa1..08fef12 100644 --- a/src/utils/logger_test.ts +++ b/src/utils/logger_test.ts @@ -1,14 +1,11 @@ import { debug } from "./logger.ts"; +import { afterEach, beforeEach, describe, it } from "@std/testing/bdd"; import { - afterEach, assertSpyCall, assertSpyCalls, - beforeEach, - describe, - it, type Stub, stub, -} from "../../dev_deps.ts"; +} from "@std/testing/mock"; let stubConsoleDebug: Stub; @@ -66,6 +63,6 @@ function stubGetEnv(envName: string, stubValue: string | undefined): Stub { return stub( Deno.env, "get", - (key) => key === envName ? stubValue : Deno.env.get(key), + (key: string) => key === envName ? stubValue : Deno.env.get(key), ); } diff --git a/deps.ts b/src/utils/schema_utils.ts similarity index 72% rename from deps.ts rename to src/utils/schema_utils.ts index a5e5166..08a9712 100644 --- a/deps.ts +++ b/src/utils/schema_utils.ts @@ -1,41 +1,43 @@ -export { join } from "jsr:@std/path@^1.0.8"; - -export { Router, Status } from "jsr:@oak/oak@^17.1.3"; - -export type { - Application, - Context, - ErrorStatus, - Next, - RouteContext, -} from "jsr:@oak/oak@^17.1.3"; - import { extendZodWithOpenApi, type ResponseConfig, type RouteConfig, -} from "npm:@asteasolutions/zod-to-openapi@^7.2.0"; +} from "@asteasolutions/zod-to-openapi"; +/** + * Open API Schema interface, usable when composing the request/response + * schema for a REST endpoint (declared when using the + * decorators such as {@linkcode Get}, {@linkcode Post}, etc.) + * @example + * ```ts + * import { Get } from "@dklab/oak-routing-ctrl" + * + * const GetItemsSchema: OakOpenApiSpec = { + * responses: { + * "200": { "description": "OK" } + * } + * } + * + * // later inside the Controller Class + * class ExampleClass { + * ;@Get("/", GetItemsSchema) + * async getSomething() { + * // + * } + * } + * ``` + */ export type OakOpenApiSpec = - & Omit + & Omit & { + request?: RouteConfig["request"]; responses?: { [statusCode: string]: ResponseConfig; }; }; -export { type ResponseConfig, type RouteConfig }; - -export { - OpenApiGeneratorV3, - OpenAPIRegistry, - type ZodRequestBody, -} from "npm:@asteasolutions/zod-to-openapi@^7.2.0"; - -export { type OpenAPIObjectConfig } from "npm:@asteasolutions/zod-to-openapi@^7.2.0/dist/v3.0/openapi-generator"; - // must import from `npm:` instead of from `deno.land` to be compatible with `@asteasolutions/zod-to-openapi` -import { z as slowTypedZ } from "npm:zod@^3.23.8"; +import { z as slowTypedZ } from "zod"; extendZodWithOpenApi(slowTypedZ); type SubsetOfZ = Pick< typeof slowTypedZ, diff --git a/test_utils/mockRequestInternals.ts b/test_utils/mockRequestInternals.ts index de63ba3..0715d7b 100644 --- a/test_utils/mockRequestInternals.ts +++ b/test_utils/mockRequestInternals.ts @@ -1,4 +1,4 @@ -import { type BodyType, type Request } from "../dev_deps.ts"; +import type { BodyType, Request } from "@oak/oak"; export type MockRequestBodyDefinition = { type: BodyType;