Skip to content

Commit

Permalink
feat: http interceptor path params (#87) (#140)
Browse files Browse the repository at this point in the history
### Features
- [#zimic] Added support to inferred path params from dynamic URLs.

```ts
interceptor.get('/users/:id').respond((request) => {
  console.log(request.pathParams.id) // '1'

  return {
    status: 200,
    body: users[0],
  };
})

await fetch('http://localhost:3000/users/1'), { method: 'GET' });
``` 

Closes #87.
  • Loading branch information
diego-aquino committed May 25, 2024
1 parent 4610c76 commit 2608ae3
Show file tree
Hide file tree
Showing 24 changed files with 774 additions and 402 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
type HttpServiceSchemaPath,
type LiteralHttpServiceSchemaPath,
type NonLiteralHttpServiceSchemaPath,
type PathParamsSchemaFromPath,
} from 'zimic0';
import {
http,
Expand Down Expand Up @@ -109,6 +110,7 @@ describe('Exports', () => {
expectTypeOf<HttpServiceSchemaPath<never, never>>().not.toBeAny();
expectTypeOf<LiteralHttpServiceSchemaPath<never, never>>().not.toBeAny();
expectTypeOf<NonLiteralHttpServiceSchemaPath<never, never>>().not.toBeAny();
expectTypeOf<PathParamsSchemaFromPath<never>>().not.toBeAny();

expectTypeOf(http.createInterceptor).not.toBeAny();
expect(typeof http.createInterceptor).toBe('function');
Expand All @@ -131,17 +133,17 @@ describe('Exports', () => {
expectTypeOf<LocalHttpInterceptorOptions>().not.toBeAny();
expectTypeOf<RemoteHttpInterceptorOptions>().not.toBeAny();
expectTypeOf<ExtractHttpInterceptorSchema<never>>().not.toBeAny();
expectTypeOf<HttpInterceptorRequest<never>>().not.toBeAny();
expectTypeOf<HttpInterceptorRequest<never, never>>().not.toBeAny();
expectTypeOf<HttpInterceptorResponse<never, never>>().not.toBeAny();
expectTypeOf<TrackedHttpInterceptorRequest<never>>().not.toBeAny();
expectTypeOf<TrackedHttpInterceptorRequest<never, {}>>().not.toBeAny();

expectTypeOf<HttpRequestHandler<never, never, never>>().not.toBeAny();
expectTypeOf<LocalHttpRequestHandler<never, never, never>>().not.toBeAny();
expectTypeOf<RemoteHttpRequestHandler<never, never, never>>().not.toBeAny();
expectTypeOf<SyncedRemoteHttpRequestHandler<never, never, never>>().not.toBeAny();
expectTypeOf<PendingRemoteHttpRequestHandler<never, never, never>>().not.toBeAny();
expectTypeOf<HttpRequestHandlerResponseDeclaration<never, never>>().not.toBeAny();
expectTypeOf<HttpRequestHandlerResponseDeclarationFactory<never, never>>().not.toBeAny();
expectTypeOf<HttpRequestHandlerResponseDeclarationFactory<never, never, never>>().not.toBeAny();

expectTypeOf<HttpRequestHandlerRestriction<never, never, never>>().not.toBeAny();
expectTypeOf<HttpRequestHandlerComputedRestriction<never, never, never>>().not.toBeAny();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,9 @@ async function declareDefaultClientTests(options: ClientTestOptionsByWorkerType)
expect(getRequests).toHaveLength(1);
expect(getRequests[0].url).toBe(`${authBaseURL}/users/${user.id}`);

expectTypeOf(getRequests[0].pathParams).toEqualTypeOf<{ id: string }>();
expect(getRequests[0].pathParams).toEqual({ id: user.id });

expectTypeOf(getRequests[0].searchParams).toEqualTypeOf<HttpSearchParams>();
expect(getRequests[0].searchParams.size).toBe(0);

Expand Down Expand Up @@ -528,6 +531,9 @@ async function declareDefaultClientTests(options: ClientTestOptionsByWorkerType)
expect(deleteRequests).toHaveLength(1);
expect(deleteRequests[0].url).toBe(`${authBaseURL}/users/${user.id}`);

expectTypeOf(deleteRequests[0].pathParams).toEqualTypeOf<{ id: string }>();
expect(deleteRequests[0].pathParams).toEqual({});

expectTypeOf(deleteRequests[0].searchParams).toEqualTypeOf<HttpSearchParams>();
expect(deleteRequests[0].searchParams.size).toBe(0);

Expand All @@ -553,7 +559,7 @@ async function declareDefaultClientTests(options: ClientTestOptionsByWorkerType)
code: 'not_found',
message: 'User not found',
};
const getHandler = await authInterceptor.delete(`/users/${user.id}`).respond({
const getHandler = await authInterceptor.delete('/users/:id').respond({
status: 404,
body: notFoundError,
});
Expand All @@ -565,6 +571,9 @@ async function declareDefaultClientTests(options: ClientTestOptionsByWorkerType)
expect(deleteRequests).toHaveLength(1);
expect(deleteRequests[0].url).toBe(`${authBaseURL}/users/${user.id}`);

expectTypeOf(deleteRequests[0].pathParams).toEqualTypeOf<{ id: string }>();
expect(deleteRequests[0].pathParams).toEqual({ id: user.id });

expectTypeOf(deleteRequests[0].searchParams).toEqualTypeOf<HttpSearchParams>();
expect(deleteRequests[0].searchParams.size).toBe(0);

Expand Down Expand Up @@ -625,6 +634,10 @@ async function declareDefaultClientTests(options: ClientTestOptionsByWorkerType)

const listRequests = await listHandler.requests();
expect(listRequests).toHaveLength(1);
expect(listRequests[0].url).toBe(`${notificationBaseURL}/notifications/${notification.userId}`);

expectTypeOf(listRequests[0].pathParams).toEqualTypeOf<{ userId: string }>();
expect(listRequests[0].pathParams).toEqual({ userId: notification.userId });

expectTypeOf(listRequests[0].searchParams).toEqualTypeOf<HttpSearchParams>();
expect(listRequests[0].searchParams.size).toBe(0);
Expand Down
21 changes: 9 additions & 12 deletions packages/zimic/src/cli/__tests__/server.cli.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:3000'.`,
'Server is running on http://localhost:3000',
);
});
});
Expand All @@ -160,10 +160,7 @@ describe('CLI (server)', async () => {
expect(server!.port()).toBe(3000);

expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://0.0.0.0:3000'.`,
);
expect(spies.log).toHaveBeenCalledWith(`${chalk.cyan('[zimic]')}`, 'Server is running on http://0.0.0.0:3000');
});
});

Expand All @@ -181,7 +178,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);
});
});
Expand Down Expand Up @@ -275,7 +272,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Ephemeral server is running on 'http://localhost:${server!.port()}'.`,
`Ephemeral server is running on http://localhost:${server!.port()}`,
);

const savedFile = await filesystem.readFile(temporarySaveFile, 'utf-8');
Expand Down Expand Up @@ -310,7 +307,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);

const savedFile = await filesystem.readFile(temporarySaveFile, 'utf-8');
Expand Down Expand Up @@ -423,7 +420,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);

expect(exitEventListeners).toHaveLength(1);
Expand Down Expand Up @@ -451,7 +448,7 @@ describe('CLI (server)', async () => {
expect(spies.log).toHaveBeenCalledTimes(1);
expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);

expect(exitEventListeners).toHaveLength(1);
Expand Down Expand Up @@ -517,7 +514,7 @@ describe('CLI (server)', async () => {

expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);

expect(exitEventListeners).toHaveLength(1);
Expand Down Expand Up @@ -573,7 +570,7 @@ describe('CLI (server)', async () => {

expect(spies.log).toHaveBeenCalledWith(
`${chalk.cyan('[zimic]')}`,
`Server is running on 'http://localhost:${server!.port()}'.`,
`Server is running on http://localhost:${server!.port()}`,
);

expect(exitEventListeners).toHaveLength(1);
Expand Down
2 changes: 1 addition & 1 deletion packages/zimic/src/cli/server/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ async function startInterceptorServer({

await server.start();

logWithPrefix(`${ephemeral ? 'Ephemeral s' : 'S'}erver is running on '${server.httpURL()}'.`);
logWithPrefix(`${ephemeral ? 'Ephemeral s' : 'S'}erver is running on ${server.httpURL()}`);

if (onReady) {
await runCommand(onReady.command, onReady.arguments);
Expand Down
2 changes: 1 addition & 1 deletion packages/zimic/src/http/types/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IfAny, Prettify, UnionToIntersection, UnionHasMoreThanOneType } from '@/types/utils';
import { IfAny, UnionToIntersection, UnionHasMoreThanOneType, Prettify } from '@/types/utils';

import { HttpHeadersSchema } from '../headers/types';
import { HttpSearchParamsSchema } from '../searchParams/types';
Expand Down
1 change: 1 addition & 0 deletions packages/zimic/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type {
LiteralHttpServiceSchemaPath,
NonLiteralHttpServiceSchemaPath,
HttpServiceSchemaPath,
PathParamsSchemaFromPath,
} from './http/types/schema';

export { HttpSearchParams, HttpHeaders };
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
HttpServiceSchemaPath,
} from '@/http/types/schema';
import { Default, PossiblePromise } from '@/types/utils';
import { joinURL, ExtendedURL } from '@/utils/urls';
import { joinURL, ExtendedURL, createRegexFromURL } from '@/utils/urls';

import HttpInterceptorWorker from '../interceptorWorker/HttpInterceptorWorker';
import LocalHttpInterceptorWorker from '../interceptorWorker/LocalHttpInterceptorWorker';
Expand Down Expand Up @@ -170,10 +170,13 @@ class HttpInterceptorClient<
}

this.handlerClientsByMethod[handler.method()].set(handler.path(), handlerClients);
const pathWithBaseURL = joinURL(this.baseURL(), handler.path());

const registrationResult = this.worker.use(this, handler.method(), pathWithBaseURL, async (context) => {
const url = joinURL(this.baseURL(), handler.path());
const urlRegex = createRegexFromURL(url);

const registrationResult = this.worker.use(this, handler.method(), url, async (context) => {
const response = await this.handleInterceptedRequest(
urlRegex,
handler.method(),
handler.path(),
context as HttpInterceptorRequestContext<Schema, Method, Path>,
Expand All @@ -190,8 +193,10 @@ class HttpInterceptorClient<
Method extends HttpServiceSchemaMethod<Schema>,
Path extends HttpServiceSchemaPath<Schema, Method>,
Context extends HttpInterceptorRequestContext<Schema, Method, Path>,
>(method: Method, path: Path, { request }: Context): Promise<HttpResponseFactoryResult> {
const parsedRequest = await HttpInterceptorWorker.parseRawRequest<Default<Schema[Path][Method]>>(request);
>(matchedURLRegex: RegExp, method: Method, path: Path, { request }: Context): Promise<HttpResponseFactoryResult> {
const parsedRequest = await HttpInterceptorWorker.parseRawRequest<Path, Default<Schema[Path][Method]>>(request, {
urlRegex: matchedURLRegex,
});
const matchedHandler = this.findMatchedHandler(method, path, parsedRequest);

if (matchedHandler) {
Expand All @@ -218,7 +223,7 @@ class HttpInterceptorClient<
>(
method: Method,
path: Path,
parsedRequest: HttpInterceptorRequest<Default<Schema[Path][Method]>>,
parsedRequest: HttpInterceptorRequest<Path, Default<Schema[Path][Method]>>,
): // eslint-disable-next-line @typescript-eslint/no-explicit-any
HttpRequestHandlerClient<Schema, Method, Path, any> | undefined {
const methodPathHandlers = this.handlerClientsByMethod[method].get(path);
Expand Down
Loading

0 comments on commit 2608ae3

Please sign in to comment.