-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add initial openapi-msw implementation (#3)
- Loading branch information
1 parent
ff4e55d
commit b74d819
Showing
14 changed files
with
1,481 additions
and
101 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"cSpell.words": ["openapi"], | ||
"testing.automaticallyOpenPeekView": "never" | ||
} |
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,8 @@ | ||
export { createOpenApiHttp } from "../src/openapi-http.js"; | ||
export type { HttpOptions, OpenApiHttpHandlers } from "../src/openapi-http.js"; | ||
|
||
export type { | ||
AnyApiSpec, | ||
HttpHandlerFactory, | ||
HttpMethod, | ||
} from "../src/type-helpers.js"; |
Large diffs are not rendered by default.
Oops, something went wrong.
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 was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
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,67 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import { describe, expect, it, vi } from "vitest"; | ||
import { http as mswHttp } from "msw"; | ||
import { createOpenApiHttp } from "./openapi-http.js"; | ||
import type { HttpMethod } from "./type-helpers.js"; | ||
|
||
const methods: HttpMethod[] = [ | ||
"get", | ||
"put", | ||
"post", | ||
"delete", | ||
"options", | ||
"head", | ||
"patch", | ||
]; | ||
|
||
describe(createOpenApiHttp, () => { | ||
it("should create an http handlers object", () => { | ||
const http = createOpenApiHttp(); | ||
|
||
expect(http).toBeTypeOf("object"); | ||
for (const method of methods) { | ||
expect(http[method]).toBeTypeOf("function"); | ||
} | ||
}); | ||
|
||
it("should include the original MSW methods in its return type", () => { | ||
const http = createOpenApiHttp(); | ||
|
||
expect(http.untyped).toBe(mswHttp); | ||
}); | ||
}); | ||
|
||
describe.each(methods)("openapi %s http handlers", (method) => { | ||
it("should forward its arguments to MSW", () => { | ||
const spy = vi.spyOn(mswHttp, method); | ||
const resolver = vi.fn(); | ||
|
||
const http = createOpenApiHttp<any>(); | ||
http[method]("/test", resolver, { once: false }); | ||
|
||
expect(spy).toHaveBeenCalledOnce(); | ||
expect(spy).toHaveBeenCalledWith("/test", resolver, { once: false }); | ||
}); | ||
|
||
it("should convert openapi paths to MSW compatible paths", () => { | ||
const spy = vi.spyOn(mswHttp, method); | ||
const resolver = vi.fn(); | ||
|
||
const http = createOpenApiHttp<any>(); | ||
http[method]("/test/{id}", resolver); | ||
|
||
expect(spy).toHaveBeenCalledOnce(); | ||
expect(spy).toHaveBeenCalledWith("/test/:id", resolver, undefined); | ||
}); | ||
|
||
it("should prepend a configured baseUrl to the path for MSW", () => { | ||
const spy = vi.spyOn(mswHttp, method); | ||
const resolver = vi.fn(); | ||
|
||
const http = createOpenApiHttp<any>({ baseUrl: "*/api/rest" }); | ||
http[method]("/test", resolver); | ||
|
||
expect(spy).toHaveBeenCalledOnce(); | ||
expect(spy).toHaveBeenCalledWith("*/api/rest/test", resolver, undefined); | ||
}); | ||
}); |
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,43 @@ | ||
import { http as mswHttp } from "msw"; | ||
import { convertToColonPath } from "./path-mapping.js"; | ||
import type { | ||
AnyApiSpec, | ||
HttpHandlerFactory, | ||
HttpMethod, | ||
} from "./type-helpers.js"; | ||
|
||
export type OpenApiHttpHandlers<ApiSpec extends AnyApiSpec> = { | ||
[Method in HttpMethod]: HttpHandlerFactory<ApiSpec, Method>; | ||
} & { untyped: typeof mswHttp }; | ||
|
||
export interface HttpOptions { | ||
baseUrl?: string; | ||
} | ||
|
||
export function createOpenApiHttp<ApiSpec extends AnyApiSpec>( | ||
options?: HttpOptions, | ||
): OpenApiHttpHandlers<ApiSpec> { | ||
return { | ||
get: createHttpWrapper<ApiSpec, "get">("get", options), | ||
put: createHttpWrapper<ApiSpec, "put">("put", options), | ||
post: createHttpWrapper<ApiSpec, "post">("post", options), | ||
delete: createHttpWrapper<ApiSpec, "delete">("delete", options), | ||
options: createHttpWrapper<ApiSpec, "options">("options", options), | ||
head: createHttpWrapper<ApiSpec, "head">("head", options), | ||
patch: createHttpWrapper<ApiSpec, "patch">("patch", options), | ||
untyped: mswHttp, | ||
}; | ||
} | ||
|
||
function createHttpWrapper< | ||
ApiSpec extends AnyApiSpec, | ||
Method extends HttpMethod, | ||
>( | ||
method: Method, | ||
httpOptions?: HttpOptions, | ||
): HttpHandlerFactory<ApiSpec, Method> { | ||
return (path, resolver, options) => { | ||
const mswPath = convertToColonPath(path as string, httpOptions?.baseUrl); | ||
return mswHttp[method](mswPath, resolver, options); | ||
}; | ||
} |
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,30 @@ | ||
import { convertToColonPath } from "./path-mapping.js"; | ||
import { describe, it, expect } from "vitest"; | ||
|
||
describe(convertToColonPath, () => { | ||
it("should leave paths with no path fragments untouched", () => { | ||
const result = convertToColonPath("/users"); | ||
|
||
expect(result).toBe("/users"); | ||
}); | ||
|
||
it("should convert a path fragment to colon convention", () => { | ||
const result = convertToColonPath("/users/{id}"); | ||
|
||
expect(result).toBe("/users/:id"); | ||
}); | ||
|
||
it("should convert all fragment in a path to colon convention", () => { | ||
const result = convertToColonPath("/users/{userId}/posts/{postIds}"); | ||
|
||
expect(result).toBe("/users/:userId/posts/:postIds"); | ||
}); | ||
|
||
it("should append baseUrl to the path when provided", () => { | ||
const noBaseUrl = convertToColonPath("/users"); | ||
const withBaseUrl = convertToColonPath("/users", "https://localhost:3000"); | ||
|
||
expect(noBaseUrl).toBe("/users"); | ||
expect(withBaseUrl).toBe("https://localhost:3000/users"); | ||
}); | ||
}); |
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,12 @@ | ||
/** | ||
* Converts a OpenAPI path fragment convention to the colon convention that is | ||
* commonly used in Node.js and also MSW. | ||
* | ||
* @example /users/{id} --> /users/:id | ||
*/ | ||
export function convertToColonPath(path: string, baseUrl?: string): string { | ||
const resolvedPath = path.replaceAll("{", ":").replaceAll("}", ""); | ||
if (!baseUrl) return resolvedPath; | ||
|
||
return baseUrl + resolvedPath; | ||
} |
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,100 @@ | ||
import type { | ||
FilterKeys, | ||
MediaType, | ||
OperationRequestBodyContent, | ||
PathsWithMethod, | ||
ResponseObjectMap, | ||
SuccessResponse, | ||
} from "openapi-typescript-helpers"; | ||
import type { http } from "msw"; | ||
|
||
/** Base type that any api spec should extend. */ | ||
export type AnyApiSpec = NonNullable<unknown>; | ||
|
||
/** Intersection of HTTP methods that are supported by both OpenApi-TS and MSW. */ | ||
export type HttpMethod = | ||
| "get" | ||
| "put" | ||
| "post" | ||
| "delete" | ||
| "options" | ||
| "head" | ||
| "patch"; | ||
|
||
/** Returns a union of all paths that exists in an api spec for a given method. */ | ||
export type PathsForMethod< | ||
ApiSpec extends AnyApiSpec, | ||
Method extends HttpMethod, | ||
> = PathsWithMethod<ApiSpec, Method>; | ||
|
||
/** Extract the params of a given path and method from an api spec. */ | ||
export type PathParams< | ||
ApiSpec extends AnyApiSpec, | ||
Path extends keyof ApiSpec, | ||
Method extends HttpMethod, | ||
> = Method extends keyof ApiSpec[Path] | ||
? ApiSpec[Path][Method] extends { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
parameters: { path: any }; | ||
} | ||
? Required<ApiSpec[Path][Method]["parameters"]["path"]> | ||
: never | ||
: never; | ||
|
||
/** Extract the request body of a given path and method from an api spec. */ | ||
export type RequestBody< | ||
ApiSpec extends AnyApiSpec, | ||
Path extends keyof ApiSpec, | ||
Method extends HttpMethod, | ||
> = Method extends keyof ApiSpec[Path] | ||
? OperationRequestBodyContent<ApiSpec[Path][Method]> | ||
: never; | ||
|
||
/** Extract the response body of a given path and method from an api spec. */ | ||
export type ResponseBody< | ||
ApiSpec extends AnyApiSpec, | ||
Path extends keyof ApiSpec, | ||
Method extends HttpMethod, | ||
> = Method extends keyof ApiSpec[Path] | ||
? ApiSpec[Path][Method] extends { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
responses: any; | ||
} | ||
? FilterKeys< | ||
SuccessResponse<ResponseObjectMap<ApiSpec[Path][Method]>>, | ||
MediaType | ||
> | ||
: never | ||
: never; | ||
|
||
/** MSW http handler factory with type inference for provided api paths. */ | ||
export type HttpHandlerFactory< | ||
ApiSpec extends AnyApiSpec, | ||
Method extends HttpMethod, | ||
> = <Path extends PathsForMethod<ApiSpec, Method>>( | ||
path: Path, | ||
resolver: ResponseResolver<ApiSpec, Path, Method>, | ||
options?: RequestHandlerOptions, | ||
) => ReturnType<typeof http.all>; | ||
|
||
/** MSW handler options. */ | ||
export type RequestHandlerOptions = Required<Parameters<typeof http.all>[2]>; | ||
|
||
/** MSW response resolver function that is made type-safe through an api spec. */ | ||
export interface ResponseResolver< | ||
ApiSpec extends AnyApiSpec, | ||
Path extends keyof ApiSpec, | ||
Method extends HttpMethod, | ||
> extends ResponseResolverType<ApiSpec, Path, Method> {} | ||
|
||
type ResponseResolverType< | ||
ApiSpec extends AnyApiSpec, | ||
Path extends keyof ApiSpec, | ||
Method extends HttpMethod, | ||
> = Parameters< | ||
typeof http.all< | ||
PathParams<ApiSpec, Path, Method>, | ||
RequestBody<ApiSpec, Path, Method>, | ||
ResponseBody<ApiSpec, Path, Method> | ||
> | ||
>[1]; |
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 |
---|---|---|
|
@@ -30,5 +30,5 @@ | |
"skipDefaultLibCheck": true, | ||
"skipLibCheck": true | ||
}, | ||
"include": ["src"] | ||
"include": ["src", "exports"] | ||
} |