Skip to content

Commit c0041ca

Browse files
jbl428imdudu1
andcommitted
feat: implement response body decorator
Co-authored-by: imdudu1 <[email protected]>
1 parent 587d3dc commit c0041ca

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, test, expect } from 'vitest';
2+
import { ResponseBodyBuilder } from './response-body.builder';
3+
4+
describe('ResponseBodyBuilder', () => {
5+
test('should build response body', () => {
6+
// given
7+
class TestResponse {
8+
constructor(public value: string) {}
9+
}
10+
const builder = new ResponseBodyBuilder(TestResponse);
11+
12+
// when
13+
const result: TestResponse = builder.build({ value: 'test' });
14+
15+
// then
16+
expect(result).toBeInstanceOf(TestResponse);
17+
expect(result.value).toBe('test');
18+
});
19+
});

lib/builders/response-body.builder.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {
2+
type ClassConstructor,
3+
type ClassTransformOptions,
4+
plainToInstance,
5+
} from 'class-transformer';
6+
7+
export class ResponseBodyBuilder<T> {
8+
constructor(
9+
readonly cls: ClassConstructor<T>,
10+
readonly options?: ClassTransformOptions,
11+
) {}
12+
13+
build(plain: Record<string, unknown>): T {
14+
return plainToInstance(this.cls, plain, this.options);
15+
}
16+
}

lib/decorators/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export const HTTP_EXCHANGE_METADATA = Symbol('HTTP_EXCHANGE_METADATA');
33
export const PATH_VARIABLE_METADATA = Symbol('PATH_VARIABLE_METADATA');
44
export const REQUEST_PARAM_METADATA = Symbol('REQUEST_PARAM_METADATA');
55
export const REQUEST_BODY_METADATA = Symbol('REQUEST_BODY_METADATA');
6-
export const REQUEST_FORM_METADATA = Symbol('REQUEST_BODY_METADATA');
6+
export const REQUEST_FORM_METADATA = Symbol('REQUEST_FORM_METADATA');
77
export const REQUEST_HEADER_METADATA = Symbol('REQUEST_HEADER_METADATA');
8+
export const RESPONSE_BODY_METADATA = Symbol('RESPONSE_BODY_METADATA');
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, test, expect } from 'vitest';
2+
import { RESPONSE_BODY_METADATA } from './constants';
3+
import { ResponseBody } from './response-body.decorator';
4+
import { type ResponseBodyBuilder } from '../builders/response-body.builder';
5+
6+
describe('ResponseBody', () => {
7+
test('should set response body metadata', () => {
8+
// given
9+
class TestResponse {}
10+
class TestService {
11+
@ResponseBody(TestResponse, { version: 1 })
12+
async request(): Promise<string> {
13+
return '';
14+
}
15+
}
16+
17+
// when
18+
const result: ResponseBodyBuilder<unknown> = Reflect.getMetadata(
19+
RESPONSE_BODY_METADATA,
20+
TestService.prototype,
21+
'request',
22+
);
23+
24+
// then
25+
expect(result.cls).toBe(TestResponse);
26+
expect(result.options).toEqual({ version: 1 });
27+
});
28+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {
2+
type ClassConstructor,
3+
type ClassTransformOptions,
4+
} from 'class-transformer';
5+
import { RESPONSE_BODY_METADATA } from './constants';
6+
import { ResponseBodyBuilder } from '../builders/response-body.builder';
7+
8+
export function ResponseBody<T>(
9+
cls: ClassConstructor<T>,
10+
options?: ClassTransformOptions,
11+
): MethodDecorator {
12+
return (target, propertyKey) => {
13+
Reflect.defineMetadata(
14+
RESPONSE_BODY_METADATA,
15+
new ResponseBodyBuilder(cls, options),
16+
target,
17+
propertyKey,
18+
);
19+
};
20+
}

lib/node-fetch.injector.spec.ts renamed to lib/supports/node-fetch.injector.spec.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { MetadataScanner } from '@nestjs/core';
22
import { beforeEach, describe, test, expect } from 'vitest';
3+
import { imitation } from './imitation';
4+
import { NodeFetchInjector } from './node-fetch.injector';
35
import {
46
DeleteExchange,
57
GetExchange,
@@ -14,10 +16,10 @@ import {
1416
RequestForm,
1517
RequestHeader,
1618
RequestParam,
17-
} from './decorators';
18-
import { StubDiscoveryService } from './fixtures/stub-discovery.service';
19-
import { StubHttpClient } from './fixtures/stub-http-client';
20-
import { NodeFetchInjector } from './node-fetch.injector';
19+
} from '../decorators';
20+
import { ResponseBody } from '../decorators/response-body.decorator';
21+
import { StubDiscoveryService } from '../fixtures/stub-discovery.service';
22+
import { StubHttpClient } from '../fixtures/stub-http-client';
2123

2224
describe('NodeFetchInjector', () => {
2325
const metadataScanner = new MetadataScanner();
@@ -334,4 +336,49 @@ describe('NodeFetchInjector', () => {
334336
expect(httpClient.requestInfo).toHaveLength(1);
335337
expect(httpClient.requestInfo[0].headers.get('Cookie')).toBe('value');
336338
});
339+
340+
test('should response text if there is no response body decorator', async () => {
341+
// given
342+
@HttpInterface()
343+
class SampleClient {
344+
@GetExchange('https://example.com/api')
345+
async request(): Promise<string> {
346+
return 'request';
347+
}
348+
}
349+
const instance = discoveryService.addProvider(SampleClient);
350+
httpClient.addResponse({ status: 'ok' });
351+
nodeFetchInjector.onModuleInit();
352+
353+
// when
354+
const result = await instance.request();
355+
356+
// then
357+
expect(result).toBe('{"status":"ok"}');
358+
});
359+
360+
test('should response instance provided with response body decorator', async () => {
361+
// given
362+
class ResponseTest {
363+
constructor(readonly value: string) {}
364+
}
365+
@HttpInterface()
366+
class SampleClient {
367+
@GetExchange('https://example.com/api')
368+
@ResponseBody(ResponseTest)
369+
async request(): Promise<ResponseTest> {
370+
return imitation();
371+
}
372+
}
373+
const instance = discoveryService.addProvider(SampleClient);
374+
httpClient.addResponse({ value: 'ok' });
375+
nodeFetchInjector.onModuleInit();
376+
377+
// when
378+
const result = await instance.request();
379+
380+
// then
381+
expect(result).toBeInstanceOf(ResponseTest);
382+
expect(result.value).toBe('ok');
383+
});
337384
});

lib/node-fetch.injector.ts renamed to lib/supports/node-fetch.injector.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { Injectable, type OnModuleInit } from '@nestjs/common';
22
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
33
import { type InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
4-
import { type HttpRequestBuilder } from './builders/http-request.builder';
5-
import { HTTP_EXCHANGE_METADATA, HTTP_INTERFACE_METADATA } from './decorators';
6-
import { HttpClient } from './types/http-client.interface';
4+
import { type HttpRequestBuilder } from '../builders/http-request.builder';
5+
import { type ResponseBodyBuilder } from '../builders/response-body.builder';
6+
import {
7+
HTTP_EXCHANGE_METADATA,
8+
HTTP_INTERFACE_METADATA,
9+
RESPONSE_BODY_METADATA,
10+
} from '../decorators';
11+
import { HttpClient } from '../types/http-client.interface';
712

813
@Injectable()
914
export class NodeFetchInjector implements OnModuleInit {
@@ -41,12 +46,23 @@ export class NodeFetchInjector implements OnModuleInit {
4146
return;
4247
}
4348

49+
const responseBodyBuilder: ResponseBodyBuilder<unknown> | undefined =
50+
Reflect.getMetadata(RESPONSE_BODY_METADATA, prototype, methodName);
51+
4452
httpRequestBuilder.setBaseUrl(baseUrl);
4553

4654
wrapper.instance[methodName] = async (...args: never[]) =>
4755
await this.httpClient
4856
.request(httpRequestBuilder.build(args))
49-
.then(async (response) => await response.json());
57+
.then(async (response) => {
58+
if (responseBodyBuilder != null) {
59+
const res = await response.json();
60+
61+
return responseBodyBuilder.build(res);
62+
}
63+
64+
return await response.text();
65+
});
5066
});
5167
}
5268

0 commit comments

Comments
 (0)