fetch-client-generator 1.0.1 → 1.2.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 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,11 @@
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 chokidar from 'chokidar';
8
+ import { generate } from '../index.js';
7
9
 
8
10
  const __filename = fileURLToPath(import.meta.url);
9
11
  const __dirname = path.dirname(__filename);
@@ -19,11 +21,64 @@ program
19
21
  .description('Generate fetch client code')
20
22
  .option('-i, --input <file>', 'input specification file')
21
23
  .option('-o, --output <file>', 'output file path')
24
+ .option('-w, --watch', 'watch input file for changes and regenerate automatically')
22
25
  .action((options) => {
23
- console.log('Generating fetch client...');
24
- console.log('Input:', options.input);
25
- console.log('Output:', options.output);
26
- // Implementation will go here
26
+ if (!options.input) {
27
+ console.error('Error: Input file is required');
28
+ process.exit(1);
29
+ }
30
+
31
+ if (!options.output) {
32
+ console.error('Error: Output file is required');
33
+ process.exit(1);
34
+ }
35
+
36
+ const generateClient = () => {
37
+ try {
38
+ console.log('Generating fetch client...');
39
+ console.log('Input:', options.input);
40
+ console.log('Output:', options.output);
41
+
42
+ const clientCode = generate(options.input);
43
+ writeFileSync(options.output, clientCode, 'utf8');
44
+
45
+ console.log('✓ Fetch client generated successfully!');
46
+ } catch (error) {
47
+ console.error('Error generating client:', error.message);
48
+ if (!options.watch) {
49
+ process.exit(1);
50
+ }
51
+ }
52
+ };
53
+
54
+ // Generate initially
55
+ generateClient();
56
+
57
+ // If watch flag is set, watch for changes
58
+ if (options.watch) {
59
+ console.log(`Watching ${options.input} for changes...`);
60
+
61
+ const watcher = chokidar.watch(options.input, {
62
+ persistent: true,
63
+ ignoreInitial: true
64
+ });
65
+
66
+ watcher.on('change', () => {
67
+ console.log('\nFile changed, regenerating...');
68
+ generateClient();
69
+ });
70
+
71
+ watcher.on('error', (error) => {
72
+ console.error('Watcher error:', error);
73
+ });
74
+
75
+ // Keep the process alive
76
+ process.on('SIGINT', () => {
77
+ console.log('\nStopping file watcher...');
78
+ watcher.close();
79
+ process.exit(0);
80
+ });
81
+ }
27
82
  });
28
83
 
29
84
  program.parse();
package/index.js CHANGED
@@ -1,4 +1,141 @@
1
- // fetch-client-generator main entry point
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
- // Main functionality will be implemented here
138
+ parseOpenAPISpec,
139
+ generateFetchClient,
140
+ generate
4
141
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fetch-client-generator",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "A tool for generating fetch-based HTTP client code",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/test/cli.test.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { expect } from 'chai';
2
- import { exec } from 'child_process';
2
+ import { exec, spawn } from 'child_process';
3
3
  import path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
+ import { writeFileSync, unlinkSync, existsSync, readFileSync } from 'fs';
5
6
 
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
@@ -23,4 +24,114 @@ describe('CLI', () => {
23
24
  done();
24
25
  });
25
26
  });
27
+
28
+ describe('watch functionality', () => {
29
+ const testInputPath = path.join(__dirname, 'test-input.json');
30
+ const testOutputPath = path.join(__dirname, 'test-output.js');
31
+ const sampleSpec = {
32
+ "openapi": "3.0.1",
33
+ "info": { "title": "Watch Test API", "version": "1.0.0" },
34
+ "paths": {
35
+ "/test": {
36
+ "get": {
37
+ "operationId": "getTest",
38
+ "responses": { "200": { "description": "OK" } }
39
+ }
40
+ }
41
+ }
42
+ };
43
+
44
+ beforeEach(() => {
45
+ writeFileSync(testInputPath, JSON.stringify(sampleSpec, null, 2));
46
+ });
47
+
48
+ afterEach(() => {
49
+ [testInputPath, testOutputPath].forEach(file => {
50
+ if (existsSync(file)) {
51
+ unlinkSync(file);
52
+ }
53
+ });
54
+ });
55
+
56
+ it('should regenerate client when input file changes', function(done) {
57
+ this.timeout(5000);
58
+
59
+ const cliPath = path.join(__dirname, '../bin/cli.js');
60
+ const child = spawn('node', [cliPath, 'generate', '-i', testInputPath, '-o', testOutputPath, '--watch'], {
61
+ stdio: ['pipe', 'pipe', 'pipe']
62
+ });
63
+
64
+ let outputReceived = false;
65
+ let regenerateReceived = false;
66
+
67
+ child.stdout.on('data', (data) => {
68
+ const output = data.toString();
69
+
70
+ if (output.includes('✓ Fetch client generated successfully!') && !outputReceived) {
71
+ outputReceived = true;
72
+ expect(existsSync(testOutputPath)).to.be.true;
73
+
74
+ setTimeout(() => {
75
+ const updatedSpec = { ...sampleSpec };
76
+ updatedSpec.info.title = "Updated Watch Test API";
77
+ writeFileSync(testInputPath, JSON.stringify(updatedSpec, null, 2));
78
+ }, 100);
79
+ }
80
+
81
+ if (output.includes('File changed, regenerating...') && !regenerateReceived) {
82
+ regenerateReceived = true;
83
+ }
84
+
85
+ if (outputReceived && regenerateReceived && output.includes('✓ Fetch client generated successfully!')) {
86
+ const clientCode = readFileSync(testOutputPath, 'utf8');
87
+ expect(clientCode).to.include('class ApiClient');
88
+ child.kill('SIGINT');
89
+ done();
90
+ }
91
+ });
92
+
93
+ child.stderr.on('data', (data) => {
94
+ console.error('CLI stderr:', data.toString());
95
+ });
96
+
97
+ child.on('error', (error) => {
98
+ done(error);
99
+ });
100
+ });
101
+
102
+ it('should handle watch mode with invalid input gracefully', function(done) {
103
+ this.timeout(3000);
104
+
105
+ const cliPath = path.join(__dirname, '../bin/cli.js');
106
+ const child = spawn('node', [cliPath, 'generate', '-i', testInputPath, '-o', testOutputPath, '--watch'], {
107
+ stdio: ['pipe', 'pipe', 'pipe']
108
+ });
109
+
110
+ let initialGeneration = false;
111
+
112
+ child.stdout.on('data', (data) => {
113
+ const output = data.toString();
114
+
115
+ if (output.includes('✓ Fetch client generated successfully!') && !initialGeneration) {
116
+ initialGeneration = true;
117
+
118
+ setTimeout(() => {
119
+ writeFileSync(testInputPath, 'invalid json');
120
+ }, 100);
121
+ }
122
+ });
123
+
124
+ child.stderr.on('data', (data) => {
125
+ const errorOutput = data.toString();
126
+ if (errorOutput.includes('Error generating client:')) {
127
+ child.kill('SIGINT');
128
+ done();
129
+ }
130
+ });
131
+
132
+ child.on('error', (error) => {
133
+ done(error);
134
+ });
135
+ });
136
+ });
26
137
  });
@@ -1,8 +1,104 @@
1
1
  import { expect } from 'chai';
2
- import fetchClientGenerator from '../index.js';
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
- it('should export an object', () => {
6
- expect(fetchClientGenerator).to.be.an('object');
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
  });