Skip to content

Commit 2f8d8b0

Browse files
committed
- First draft of the new client generation mechanism
1 parent 8b15c1e commit 2f8d8b0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1406
-1572
lines changed

jest.config.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ module.exports = {
1515
displayName: 'E2E',
1616
testEnvironment: 'node',
1717
testMatch: [
18-
// '<rootDir>/test/e2e/v2.fetch.spec.js',
19-
// '<rootDir>/test/e2e/v2.xhr.spec.js',
18+
'<rootDir>/test/e2e/v2.fetch.spec.js',
19+
'<rootDir>/test/e2e/v2.xhr.spec.js',
2020
'<rootDir>/test/e2e/v2.node.spec.js',
21-
// '<rootDir>/test/e2e/v3.fetch.spec.js',
22-
// '<rootDir>/test/e2e/v3.xhr.spec.js',
23-
// '<rootDir>/test/e2e/v3.node.spec.js',
21+
'<rootDir>/test/e2e/v3.fetch.spec.js',
22+
'<rootDir>/test/e2e/v3.xhr.spec.js',
23+
'<rootDir>/test/e2e/v3.node.spec.js',
2424
],
2525
},
2626
],
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiResult } from './ApiResult';
2+
3+
export class ApiError extends Error {
4+
public readonly url: string;
5+
public readonly status: number;
6+
public readonly statusText: string;
7+
public readonly body: any;
8+
9+
constructor(response: ApiResult, message: string) {
10+
super(message);
11+
12+
this.url = response.url;
13+
this.status = response.status;
14+
this.statusText = response.statusText;
15+
this.body = response.body;
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface ApiRequestOptions {
2+
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
3+
readonly path: string;
4+
readonly cookies?: Record<string, any>;
5+
readonly headers?: Record<string, any>;
6+
readonly query?: Record<string, any>;
7+
readonly formData?: Record<string, any>;
8+
readonly body?: any;
9+
readonly responseHeader?: string;
10+
readonly errors?: Record<number, string>;
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ApiResult {
2+
readonly url: string;
3+
readonly ok: boolean;
4+
readonly status: number;
5+
readonly statusText: string;
6+
readonly body: any;
7+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface Config {
2+
BASE: string;
3+
VERSION: string;
4+
CLIENT: 'fetch' | 'xhr' | 'node';
5+
WITH_CREDENTIALS: boolean;
6+
TOKEN: string;
7+
}
8+
9+
export const OpenAPI: Config = {
10+
BASE: 'http://localhost:3000/base',
11+
VERSION: '1.0',
12+
CLIENT: 'fetch',
13+
WITH_CREDENTIALS: false,
14+
TOKEN: '',
15+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { ApiError } from './ApiError';
2+
import { ApiRequestOptions } from './ApiRequestOptions';
3+
import { ApiResult } from './ApiResult';
4+
import { OpenAPI } from './OpenAPI';
5+
6+
function isDefined<T>(value: T | null | undefined): value is Exclude<T, null | undefined> {
7+
return value !== undefined && value !== null;
8+
}
9+
function getQueryString(params: Record<string, any>): string {
10+
const qs: string[] = [];
11+
12+
Object.keys(params).forEach(key => {
13+
const value = params[key];
14+
if (isDefined(value)) {
15+
if (Array.isArray(value)) {
16+
value.forEach(value => {
17+
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
18+
});
19+
} else {
20+
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
21+
}
22+
}
23+
});
24+
25+
if (qs.length > 0) {
26+
return `?${qs.join('&')}`;
27+
}
28+
29+
return '';
30+
}
31+
function getUrl(options: ApiRequestOptions): string {
32+
const path = options.path.replace(/[:]/g, '_');
33+
const url = `${OpenAPI.BASE}${path}`;
34+
35+
if (options.query) {
36+
return url + getQueryString(options.query);
37+
}
38+
39+
return url;
40+
}
41+
function getFormData(params: Record<string, any>): FormData {
42+
const formData = new FormData();
43+
44+
Object.keys(params).forEach(key => {
45+
const value = params[key];
46+
if (isDefined(value)) {
47+
formData.append(key, value);
48+
}
49+
});
50+
51+
return formData;
52+
}
53+
function getHeaders(options: ApiRequestOptions): Headers {
54+
const headers = new Headers({
55+
Accept: 'application/json',
56+
...options.headers,
57+
});
58+
59+
if (OpenAPI.TOKEN !== null && OpenAPI.TOKEN !== '') {
60+
headers.append('Authorization', `Bearer ${OpenAPI.TOKEN}`);
61+
}
62+
63+
if (options.body) {
64+
if (options.body instanceof Blob) {
65+
if (options.body.type) {
66+
headers.append('Content-Type', options.body.type);
67+
}
68+
} else if (typeof options.body === 'string') {
69+
headers.append('Content-Type', 'text/plain');
70+
} else {
71+
headers.append('Content-Type', 'application/json');
72+
}
73+
}
74+
75+
return headers;
76+
}
77+
function getRequestBody(options: ApiRequestOptions): BodyInit | undefined {
78+
if (options.formData) {
79+
return getFormData(options.formData);
80+
}
81+
82+
if (options.body) {
83+
if (options.body instanceof Blob) {
84+
return options.body;
85+
} else if (typeof options.body === 'string') {
86+
return options.body;
87+
} else {
88+
return JSON.stringify(options.body);
89+
}
90+
}
91+
92+
return undefined;
93+
}
94+
async function sendRequest(options: ApiRequestOptions, url: string): Promise<Response> {
95+
const request: RequestInit = {
96+
method: options.method,
97+
headers: getHeaders(options),
98+
body: getRequestBody(options),
99+
};
100+
101+
return await fetch(url, request);
102+
}
103+
function getResponseHeader(response: Response, responseHeader?: string): string | null {
104+
if (responseHeader) {
105+
const content = response.headers.get(responseHeader);
106+
if (typeof content === 'string') {
107+
return content;
108+
}
109+
}
110+
111+
return null;
112+
}
113+
async function getResponseBody(response: Response): Promise<any> {
114+
try {
115+
const contentType = response.headers.get('Content-Type');
116+
if (contentType) {
117+
switch (contentType.toLowerCase()) {
118+
case 'application/json':
119+
case 'application/json; charset=utf-8':
120+
return await response.json();
121+
122+
default:
123+
return await response.text();
124+
}
125+
}
126+
} catch (e) {
127+
console.error(e);
128+
}
129+
130+
return null;
131+
}
132+
function catchErrors(options: ApiRequestOptions, result: ApiResult): void {
133+
const errors: Record<number, string> = {
134+
400: 'Bad Request',
135+
401: 'Unauthorized',
136+
403: 'Forbidden',
137+
404: 'Not Found',
138+
500: 'Internal Server Error',
139+
502: 'Bad Gateway',
140+
503: 'Service Unavailable',
141+
...options.errors,
142+
};
143+
144+
const error = errors[result.status];
145+
if (error) {
146+
throw new ApiError(result, error);
147+
}
148+
149+
if (!result.ok) {
150+
throw new ApiError(result, 'Generic Error');
151+
}
152+
}
153+
154+
/**
155+
* Request using fetch
156+
* @param options Request options
157+
* @result ApiResult
158+
* @throws ApiError
159+
*/
160+
export async function request(options: ApiRequestOptions): Promise<ApiResult> {
161+
const url = getUrl(options);
162+
const response = await sendRequest(options, url);
163+
const responseBody = await getResponseBody(response);
164+
const responseHeader = getResponseHeader(response, options.responseHeader);
165+
166+
const result: ApiResult = {
167+
url,
168+
ok: response.ok,
169+
status: response.status,
170+
statusText: response.statusText,
171+
body: responseHeader || responseBody,
172+
};
173+
174+
catchErrors(options, result);
175+
return result;
176+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { ApiResult } from './ApiResult';
2+
3+
export class ApiError extends Error {
4+
public readonly url: string;
5+
public readonly status: number;
6+
public readonly statusText: string;
7+
public readonly body: any;
8+
9+
constructor(response: ApiResult, message: string) {
10+
super(message);
11+
12+
this.url = response.url;
13+
this.status = response.status;
14+
this.statusText = response.statusText;
15+
this.body = response.body;
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface ApiRequestOptions {
2+
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
3+
readonly path: string;
4+
readonly cookies?: Record<string, any>;
5+
readonly headers?: Record<string, any>;
6+
readonly query?: Record<string, any>;
7+
readonly formData?: Record<string, any>;
8+
readonly body?: any;
9+
readonly responseHeader?: string;
10+
readonly errors?: Record<number, string>;
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ApiResult {
2+
readonly url: string;
3+
readonly ok: boolean;
4+
readonly status: number;
5+
readonly statusText: string;
6+
readonly body: any;
7+
}

src/generated/v3/node/core/OpenAPI.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface Config {
2+
BASE: string;
3+
VERSION: string;
4+
CLIENT: 'fetch' | 'xhr' | 'node';
5+
WITH_CREDENTIALS: boolean;
6+
TOKEN: string;
7+
}
8+
9+
export const OpenAPI: Config = {
10+
BASE: 'http://localhost:3000/base',
11+
VERSION: '1.0',
12+
CLIENT: 'node',
13+
WITH_CREDENTIALS: false,
14+
TOKEN: '',
15+
};

0 commit comments

Comments
 (0)