fetch-client-generator 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api-client.js +67 -0
- package/bin/cli.js +25 -5
- package/index.js +139 -2
- package/package.json +1 -1
- package/test/index.test.js +99 -3
package/api-client.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
class ApiClient {
|
|
2
|
+
constructor(baseUrl = '', options = {}) {
|
|
3
|
+
this.baseUrl = baseUrl;
|
|
4
|
+
this.defaultOptions = {
|
|
5
|
+
headers: {
|
|
6
|
+
'Content-Type': 'application/json',
|
|
7
|
+
...options.headers
|
|
8
|
+
},
|
|
9
|
+
...options
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async request(path, options = {}) {
|
|
14
|
+
const url = this.baseUrl + path;
|
|
15
|
+
const config = {
|
|
16
|
+
...this.defaultOptions,
|
|
17
|
+
...options,
|
|
18
|
+
headers: {
|
|
19
|
+
...this.defaultOptions.headers,
|
|
20
|
+
...options.headers
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const response = await fetch(url, config);
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const contentType = response.headers.get('content-type');
|
|
31
|
+
if (contentType && contentType.includes('application/json')) {
|
|
32
|
+
return await response.json();
|
|
33
|
+
} else if (contentType && contentType.includes('text/')) {
|
|
34
|
+
return await response.text();
|
|
35
|
+
} else {
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async token() {
|
|
41
|
+
return await this.request('/authentication/token', {
|
|
42
|
+
method: 'POST'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async logout(data) {
|
|
47
|
+
return await this.request('/authentication/logout', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
body: JSON.stringify(data)
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getAuthenticationPing() {
|
|
54
|
+
return await this.request('/authentication/ping', {
|
|
55
|
+
method: 'GET'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getApiOrganisations() {
|
|
60
|
+
return await this.request('/api/organisations', {
|
|
61
|
+
method: 'GET'
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default ApiClient;
|
package/bin/cli.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { program } from 'commander';
|
|
4
|
-
import { readFileSync } from 'fs';
|
|
4
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import { generate } from '../index.js';
|
|
7
8
|
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
@@ -20,10 +21,29 @@ program
|
|
|
20
21
|
.option('-i, --input <file>', 'input specification file')
|
|
21
22
|
.option('-o, --output <file>', 'output file path')
|
|
22
23
|
.action((options) => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
if (!options.input) {
|
|
25
|
+
console.error('Error: Input file is required');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!options.output) {
|
|
30
|
+
console.error('Error: Output file is required');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
console.log('Generating fetch client...');
|
|
36
|
+
console.log('Input:', options.input);
|
|
37
|
+
console.log('Output:', options.output);
|
|
38
|
+
|
|
39
|
+
const clientCode = generate(options.input);
|
|
40
|
+
writeFileSync(options.output, clientCode, 'utf8');
|
|
41
|
+
|
|
42
|
+
console.log('✓ Fetch client generated successfully!');
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error generating client:', error.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
27
47
|
});
|
|
28
48
|
|
|
29
49
|
program.parse();
|
package/index.js
CHANGED
|
@@ -1,4 +1,141 @@
|
|
|
1
|
-
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
export function parseOpenAPISpec(specPath) {
|
|
4
|
+
const spec = JSON.parse(readFileSync(specPath, 'utf8'));
|
|
5
|
+
|
|
6
|
+
const endpoints = [];
|
|
7
|
+
|
|
8
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
9
|
+
for (const [method, operation] of Object.entries(pathItem)) {
|
|
10
|
+
const endpoint = {
|
|
11
|
+
path,
|
|
12
|
+
method: method.toUpperCase(),
|
|
13
|
+
operationId: operation.operationId,
|
|
14
|
+
requestBody: operation.requestBody,
|
|
15
|
+
responses: operation.responses,
|
|
16
|
+
tags: operation.tags
|
|
17
|
+
};
|
|
18
|
+
endpoints.push(endpoint);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
info: spec.info,
|
|
24
|
+
endpoints,
|
|
25
|
+
schemas: spec.components?.schemas || {}
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function generateFetchClient(parsedSpec, options = {}) {
|
|
30
|
+
const { info, endpoints, schemas } = parsedSpec;
|
|
31
|
+
const className = options.className || 'ApiClient';
|
|
32
|
+
|
|
33
|
+
let clientCode = `class ${className} {
|
|
34
|
+
constructor(baseUrl = '', options = {}) {
|
|
35
|
+
this.baseUrl = baseUrl;
|
|
36
|
+
this.defaultOptions = {
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
...options.headers
|
|
40
|
+
},
|
|
41
|
+
...options
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async request(path, options = {}) {
|
|
46
|
+
const url = this.baseUrl + path;
|
|
47
|
+
const config = {
|
|
48
|
+
...this.defaultOptions,
|
|
49
|
+
...options,
|
|
50
|
+
headers: {
|
|
51
|
+
...this.defaultOptions.headers,
|
|
52
|
+
...options.headers
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const response = await fetch(url, config);
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(\`HTTP error! status: \${response.status}\`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const contentType = response.headers.get('content-type');
|
|
63
|
+
if (contentType && contentType.includes('application/json')) {
|
|
64
|
+
return await response.json();
|
|
65
|
+
} else if (contentType && contentType.includes('text/')) {
|
|
66
|
+
return await response.text();
|
|
67
|
+
} else {
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
for (const endpoint of endpoints) {
|
|
75
|
+
let methodName = endpoint.operationId;
|
|
76
|
+
|
|
77
|
+
if (!methodName) {
|
|
78
|
+
// Generate camelCase method name from path and method
|
|
79
|
+
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
|
80
|
+
// Convert each part to PascalCase and join
|
|
81
|
+
const pascalParts = pathParts.map(part => part.charAt(0).toUpperCase() + part.slice(1));
|
|
82
|
+
const cleanPath = pascalParts.join('');
|
|
83
|
+
methodName = `${endpoint.method.toLowerCase()}${cleanPath}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Convert to camelCase if not already
|
|
87
|
+
methodName = methodName.charAt(0).toLowerCase() + methodName.slice(1);
|
|
88
|
+
|
|
89
|
+
const hasRequestBody = endpoint.requestBody &&
|
|
90
|
+
endpoint.requestBody.content &&
|
|
91
|
+
Object.keys(endpoint.requestBody.content).length > 0;
|
|
92
|
+
|
|
93
|
+
const params = hasRequestBody ? 'data' : '';
|
|
94
|
+
const methodParams = params ? `(${params})` : '()';
|
|
95
|
+
|
|
96
|
+
clientCode += ` async ${methodName}${methodParams} {
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
if (hasRequestBody) {
|
|
100
|
+
clientCode += ` return await this.request('${endpoint.path}', {
|
|
101
|
+
method: '${endpoint.method}',
|
|
102
|
+
body: JSON.stringify(data)
|
|
103
|
+
});
|
|
104
|
+
`;
|
|
105
|
+
} else {
|
|
106
|
+
clientCode += ` return await this.request('${endpoint.path}', {
|
|
107
|
+
method: '${endpoint.method}'
|
|
108
|
+
});
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
clientCode += ` }
|
|
113
|
+
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
clientCode += `}
|
|
118
|
+
|
|
119
|
+
export default ${className};`;
|
|
120
|
+
|
|
121
|
+
return clientCode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function generate(inputPath, outputPath, options = {}) {
|
|
125
|
+
const parsedSpec = parseOpenAPISpec(inputPath);
|
|
126
|
+
const clientCode = generateFetchClient(parsedSpec, options);
|
|
127
|
+
|
|
128
|
+
if (outputPath) {
|
|
129
|
+
import('fs').then(({ writeFileSync }) => {
|
|
130
|
+
writeFileSync(outputPath, clientCode, 'utf8');
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return clientCode;
|
|
135
|
+
}
|
|
136
|
+
|
|
2
137
|
export default {
|
|
3
|
-
|
|
138
|
+
parseOpenAPISpec,
|
|
139
|
+
generateFetchClient,
|
|
140
|
+
generate
|
|
4
141
|
};
|
package/package.json
CHANGED
package/test/index.test.js
CHANGED
|
@@ -1,8 +1,104 @@
|
|
|
1
1
|
import { expect } from 'chai';
|
|
2
|
-
import
|
|
2
|
+
import { parseOpenAPISpec, generateFetchClient, generate } from '../index.js';
|
|
3
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const sampleOpenAPI = {
|
|
7
|
+
"openapi": "3.0.1",
|
|
8
|
+
"info": {
|
|
9
|
+
"title": "Test API",
|
|
10
|
+
"version": "1.0.0"
|
|
11
|
+
},
|
|
12
|
+
"paths": {
|
|
13
|
+
"/users": {
|
|
14
|
+
"get": {
|
|
15
|
+
"operationId": "getUsers",
|
|
16
|
+
"responses": {
|
|
17
|
+
"200": {
|
|
18
|
+
"description": "OK",
|
|
19
|
+
"content": {
|
|
20
|
+
"application/json": {
|
|
21
|
+
"schema": {
|
|
22
|
+
"type": "array",
|
|
23
|
+
"items": {
|
|
24
|
+
"type": "object"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"post": {
|
|
33
|
+
"operationId": "createUser",
|
|
34
|
+
"requestBody": {
|
|
35
|
+
"content": {
|
|
36
|
+
"application/json": {
|
|
37
|
+
"schema": {
|
|
38
|
+
"type": "object",
|
|
39
|
+
"properties": {
|
|
40
|
+
"name": { "type": "string" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"responses": {
|
|
47
|
+
"201": {
|
|
48
|
+
"description": "Created"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
3
55
|
|
|
4
56
|
describe('fetch-client-generator', () => {
|
|
5
|
-
|
|
6
|
-
|
|
57
|
+
let testSpecPath;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
testSpecPath = path.join(process.cwd(), 'test-spec.json');
|
|
61
|
+
writeFileSync(testSpecPath, JSON.stringify(sampleOpenAPI, null, 2));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
try {
|
|
66
|
+
unlinkSync(testSpecPath);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// File might not exist
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('parseOpenAPISpec', () => {
|
|
73
|
+
it('should parse OpenAPI specification', () => {
|
|
74
|
+
const result = parseOpenAPISpec(testSpecPath);
|
|
75
|
+
|
|
76
|
+
expect(result).to.have.property('info');
|
|
77
|
+
expect(result).to.have.property('endpoints');
|
|
78
|
+
expect(result).to.have.property('schemas');
|
|
79
|
+
expect(result.info.title).to.equal('Test API');
|
|
80
|
+
expect(result.endpoints).to.have.length(2);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('generateFetchClient', () => {
|
|
85
|
+
it('should generate fetch client code', () => {
|
|
86
|
+
const parsedSpec = parseOpenAPISpec(testSpecPath);
|
|
87
|
+
const clientCode = generateFetchClient(parsedSpec);
|
|
88
|
+
|
|
89
|
+
expect(clientCode).to.include('class ApiClient');
|
|
90
|
+
expect(clientCode).to.include('async getUsers()');
|
|
91
|
+
expect(clientCode).to.include('async createUser(data)');
|
|
92
|
+
expect(clientCode).to.include('fetch(url, config)');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('generate', () => {
|
|
97
|
+
it('should generate and return client code', () => {
|
|
98
|
+
const clientCode = generate(testSpecPath);
|
|
99
|
+
|
|
100
|
+
expect(clientCode).to.be.a('string');
|
|
101
|
+
expect(clientCode).to.include('class ApiClient');
|
|
102
|
+
});
|
|
7
103
|
});
|
|
8
104
|
});
|