@opra/cli 0.33.13 → 1.0.0-alpha.7
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/bin/bin/oprimp.mjs +1 -1
- package/bin/oprimp.mjs +1 -1
- package/cjs/code-block.js +17 -0
- package/cjs/index.js +1 -1
- package/cjs/oprimp-cli.js +9 -9
- package/cjs/ts-generator/http-controller-node.js +21 -0
- package/cjs/ts-generator/index.js +4 -0
- package/cjs/ts-generator/processors/clean-directory.js +35 -0
- package/cjs/ts-generator/processors/process-data-types.js +252 -0
- package/cjs/ts-generator/processors/process-document.js +60 -0
- package/cjs/ts-generator/processors/process-http-api.js +45 -0
- package/cjs/ts-generator/processors/process-http-controller.js +189 -0
- package/cjs/ts-generator/ts-file.js +101 -0
- package/cjs/ts-generator/ts-generator.js +108 -0
- package/cjs/ts-generator/utils/locate-named-type.js +16 -0
- package/cjs/{utils → ts-generator/utils}/string-utils.js +17 -16
- package/esm/code-block.js +13 -0
- package/esm/index.js +1 -1
- package/esm/oprimp-cli.js +9 -9
- package/esm/ts-generator/http-controller-node.js +18 -0
- package/esm/ts-generator/index.js +1 -0
- package/esm/ts-generator/processors/clean-directory.js +30 -0
- package/esm/ts-generator/processors/process-data-types.js +241 -0
- package/esm/ts-generator/processors/process-document.js +55 -0
- package/esm/ts-generator/processors/process-http-api.js +40 -0
- package/esm/ts-generator/processors/process-http-controller.js +184 -0
- package/esm/ts-generator/ts-file.js +96 -0
- package/esm/ts-generator/ts-generator.js +103 -0
- package/esm/ts-generator/utils/locate-named-type.js +12 -0
- package/esm/{utils → ts-generator/utils}/string-utils.js +17 -16
- package/package.json +8 -6
- package/types/code-block.d.ts +5 -0
- package/types/{api-exporter/file-writer.d.ts → file-writer.d.ts} +1 -1
- package/types/index.d.ts +1 -1
- package/types/ts-generator/http-controller-node.d.ts +1 -0
- package/types/ts-generator/index.d.ts +1 -0
- package/types/ts-generator/processors/clean-directory.d.ts +2 -0
- package/types/ts-generator/processors/process-data-types.d.ts +30 -0
- package/types/ts-generator/processors/process-document.d.ts +8 -0
- package/types/ts-generator/processors/process-http-api.d.ts +3 -0
- package/types/ts-generator/processors/process-http-controller.d.ts +3 -0
- package/types/{api-exporter → ts-generator}/ts-file.d.ts +4 -4
- package/types/ts-generator/ts-generator.d.ts +70 -0
- package/types/ts-generator/utils/locate-named-type.d.ts +2 -0
- package/cjs/api-exporter/api-exporter.js +0 -115
- package/cjs/api-exporter/process-resources.js +0 -131
- package/cjs/api-exporter/process-types.js +0 -261
- package/cjs/api-exporter/ts-file.js +0 -104
- package/cjs/utils/get-caller-file.util.js +0 -24
- package/esm/api-exporter/api-exporter.js +0 -110
- package/esm/api-exporter/process-resources.js +0 -126
- package/esm/api-exporter/process-types.js +0 -249
- package/esm/api-exporter/ts-file.js +0 -99
- package/esm/utils/get-caller-file.util.js +0 -20
- package/types/api-exporter/api-exporter.d.ts +0 -46
- package/types/api-exporter/process-resources.d.ts +0 -4
- package/types/api-exporter/process-types.d.ts +0 -62
- package/types/utils/get-caller-file.util.d.ts +0 -1
- /package/cjs/{api-exporter/file-writer.js → file-writer.js} +0 -0
- /package/esm/{api-exporter/file-writer.js → file-writer.js} +0 -0
- /package/types/{utils → ts-generator/utils}/string-utils.d.ts +0 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { ComplexType, EnumType, MappedType, MixinType, SimpleType } from '@opra/common';
|
|
3
|
+
import { CodeBlock } from '../../code-block.js';
|
|
4
|
+
import { wrapJSDocString } from '../utils/string-utils.js';
|
|
5
|
+
const internalTypeNames = ['any', 'boolean', 'bigint', 'number', 'null', 'string', 'object'];
|
|
6
|
+
export async function processDataType(dataType) {
|
|
7
|
+
const doc = dataType.node.getDocument();
|
|
8
|
+
if (doc.id !== this._document?.id) {
|
|
9
|
+
const { generator } = await this.processDocument(doc);
|
|
10
|
+
return await generator.processDataType(dataType);
|
|
11
|
+
}
|
|
12
|
+
const typeName = dataType.name;
|
|
13
|
+
if (typeName && internalTypeNames.includes(typeName))
|
|
14
|
+
return;
|
|
15
|
+
if (!typeName)
|
|
16
|
+
throw new TypeError(`DataType has no name`);
|
|
17
|
+
let file = this._filesMap.get(dataType);
|
|
18
|
+
if (file)
|
|
19
|
+
return file;
|
|
20
|
+
if (dataType instanceof SimpleType)
|
|
21
|
+
file = this.addFile(path.join(this._documentRoot, '/simple-types.ts'), true);
|
|
22
|
+
else {
|
|
23
|
+
if (dataType instanceof EnumType)
|
|
24
|
+
file = this.addFile(path.join(this._typesRoot, 'enums', typeName + '.ts'));
|
|
25
|
+
else
|
|
26
|
+
file = this.addFile(path.join(this._typesRoot, 'types', typeName + '.ts'));
|
|
27
|
+
}
|
|
28
|
+
this._filesMap.set(dataType, file);
|
|
29
|
+
file = this._filesMap.get(dataType);
|
|
30
|
+
if (file.exportTypes.includes(typeName))
|
|
31
|
+
return file;
|
|
32
|
+
file.exportTypes.push(typeName);
|
|
33
|
+
const typesIndexTs = this.addFile(path.join(this._typesRoot, 'index.ts'), true);
|
|
34
|
+
const indexTs = this.addFile('/index.ts', true);
|
|
35
|
+
indexTs.addExport(typesIndexTs.filename);
|
|
36
|
+
const codeBlock = (file.code['type_' + typeName] = new CodeBlock());
|
|
37
|
+
codeBlock.head = `/**\n * ${typeName}`;
|
|
38
|
+
if (dataType.description)
|
|
39
|
+
codeBlock.head += `\n * ${wrapJSDocString(dataType.description || '')}`;
|
|
40
|
+
codeBlock.head += `
|
|
41
|
+
* @url ${path.posix.join(doc.url || this.serviceUrl, '$schema', '#types/' + typeName)}
|
|
42
|
+
*/
|
|
43
|
+
export `;
|
|
44
|
+
if (dataType instanceof EnumType)
|
|
45
|
+
codeBlock.typeDef = await this.generateEnumTypeDefinition(dataType, 'scope');
|
|
46
|
+
else if (dataType instanceof ComplexType)
|
|
47
|
+
codeBlock.typeDef = await this.generateComplexTypeDefinition(dataType, file, 'scope');
|
|
48
|
+
else if (dataType instanceof SimpleType)
|
|
49
|
+
codeBlock.typeDef = await this.generateSimpleTypeDefinition(dataType, 'scope');
|
|
50
|
+
else if (dataType instanceof MappedType)
|
|
51
|
+
codeBlock.typeDef = await this.generateMappedTypeDefinition(dataType, file, 'scope');
|
|
52
|
+
else if (dataType instanceof MixinType)
|
|
53
|
+
codeBlock.typeDef = await this.generateMixinTypeDefinition(dataType, file, 'scope');
|
|
54
|
+
else
|
|
55
|
+
throw new TypeError(`${dataType.kind} data type (${typeName}) can not be directly exported`);
|
|
56
|
+
typesIndexTs.addExport(file.filename);
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
export async function generateEnumTypeDefinition(dataType, intent) {
|
|
63
|
+
if (intent === 'field')
|
|
64
|
+
return ('(' +
|
|
65
|
+
Object.keys(dataType.attributes)
|
|
66
|
+
.map(t => `'${t}'`)
|
|
67
|
+
.join(' | ') +
|
|
68
|
+
')');
|
|
69
|
+
if (intent !== 'scope')
|
|
70
|
+
throw new TypeError(`Can't generate EnumType for "${intent}" intent`);
|
|
71
|
+
if (!dataType.name)
|
|
72
|
+
throw new TypeError(`Name required to generate EnumType for "${intent}" intent`);
|
|
73
|
+
let out = `enum ${dataType.name} {\n\t`;
|
|
74
|
+
for (const [value, info] of Object.entries(dataType.attributes)) {
|
|
75
|
+
// Print JSDoc
|
|
76
|
+
let jsDoc = '';
|
|
77
|
+
if (dataType.attributes[value].description)
|
|
78
|
+
jsDoc += ` * ${dataType.attributes[value].description}\n`;
|
|
79
|
+
if (jsDoc)
|
|
80
|
+
out += `/**\n${jsDoc} */\n`;
|
|
81
|
+
out +=
|
|
82
|
+
`${info.alias || value} = ` + (typeof value === 'number' ? value : "'" + String(value).replace("'", "\\'") + "'");
|
|
83
|
+
out += ',\n\n';
|
|
84
|
+
}
|
|
85
|
+
return out + '\b}';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
*
|
|
89
|
+
*/
|
|
90
|
+
export async function generateComplexTypeDefinition(dataType, file, intent) {
|
|
91
|
+
if (intent === 'scope' && !dataType.name)
|
|
92
|
+
throw new TypeError(`Name required to generate ComplexType for "${intent}" intent`);
|
|
93
|
+
let out = intent === 'scope' ? `interface ${dataType.name} ` : '';
|
|
94
|
+
const ownFields = [...dataType.fields.values()].filter(f => f.origin === dataType);
|
|
95
|
+
if (dataType.base) {
|
|
96
|
+
const base = await this.resolveTypeNameOrDef(dataType.base, file, 'extends');
|
|
97
|
+
const omitBaseFields = dataType.base ? ownFields.filter(f => dataType.base.fields.has(f.name)) : [];
|
|
98
|
+
const baseDef = omitBaseFields.length
|
|
99
|
+
? `Omit<${base}, ${omitBaseFields.map(x => "'" + x.name + "'").join(' | ')}>`
|
|
100
|
+
: `${base}`;
|
|
101
|
+
if (intent === 'scope')
|
|
102
|
+
out += `extends ${baseDef} `;
|
|
103
|
+
else {
|
|
104
|
+
out += baseDef;
|
|
105
|
+
if (!ownFields.length)
|
|
106
|
+
return out;
|
|
107
|
+
out += ' & ';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
out += '{\n\t';
|
|
111
|
+
let i = 0;
|
|
112
|
+
for (const field of ownFields) {
|
|
113
|
+
if (i++)
|
|
114
|
+
out += '\n';
|
|
115
|
+
// Print JSDoc
|
|
116
|
+
out += `/**\n * ${field.description || ''}\n`;
|
|
117
|
+
if (field.default)
|
|
118
|
+
out += ` * @default ` + field.default + '\n';
|
|
119
|
+
// if (field.format)
|
|
120
|
+
// jsDoc += ` * @format ` + field.format + '\n';
|
|
121
|
+
if (field.exclusive)
|
|
122
|
+
out += ` * @exclusive\n`;
|
|
123
|
+
if (field.readonly)
|
|
124
|
+
out += ` * @readonly\n`;
|
|
125
|
+
if (field.writeonly)
|
|
126
|
+
out += ` * @writeonly\n`;
|
|
127
|
+
if (field.deprecated)
|
|
128
|
+
out += ` * @deprecated ` + (typeof field.deprecated === 'string' ? field.deprecated : '') + '\n';
|
|
129
|
+
out += ' */\n';
|
|
130
|
+
// Print field name
|
|
131
|
+
if (field.readonly)
|
|
132
|
+
out += 'readonly ';
|
|
133
|
+
out += `${field.name}${field.required ? '' : '?'}: `;
|
|
134
|
+
if (field.fixed) {
|
|
135
|
+
const t = typeof field.fixed;
|
|
136
|
+
out += `${t === 'number' || t === 'boolean' || t === 'bigint' ? field.fixed : "'" + field.fixed + "'"}\n`;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
out += (await this.resolveTypeNameOrDef(field.type, file, 'field')) + `${field.isArray ? '[]' : ''};\n`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (dataType.additionalFields)
|
|
143
|
+
out += '[key: string]: any;\n';
|
|
144
|
+
return out + '\b}';
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
*
|
|
148
|
+
*/
|
|
149
|
+
export async function generateSimpleTypeDefinition(dataType, intent) {
|
|
150
|
+
if (intent === 'scope' && !dataType.name)
|
|
151
|
+
throw new TypeError(`Name required to generate SimpleType for "${intent}" intent`);
|
|
152
|
+
let out = intent === 'scope' ? `type ${dataType.name} = ` : '';
|
|
153
|
+
out += dataType.nameMappings.js || 'any';
|
|
154
|
+
return intent === 'scope' ? out + ';' : out;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
*
|
|
158
|
+
*/
|
|
159
|
+
export async function generateMixinTypeDefinition(dataType, file, intent) {
|
|
160
|
+
return (await Promise.all(dataType.types.map(t => this.resolveTypeNameOrDef(t, file, intent))))
|
|
161
|
+
.map(t => (t.includes('|') ? '(' + t + ')' : t))
|
|
162
|
+
.join(intent === 'extends' ? ', ' : ' & ');
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
*
|
|
166
|
+
*/
|
|
167
|
+
export async function generateMappedTypeDefinition(dataType, file, intent) {
|
|
168
|
+
const typeDef = await this.resolveTypeNameOrDef(dataType.base, file, intent);
|
|
169
|
+
const pick = dataType.pick?.length ? dataType.pick : undefined;
|
|
170
|
+
const omit = !pick && dataType.omit?.length ? dataType.omit : undefined;
|
|
171
|
+
const partial = dataType.partial === true || (Array.isArray(dataType.partial) && dataType.partial.length > 0)
|
|
172
|
+
? dataType.partial
|
|
173
|
+
: undefined;
|
|
174
|
+
const required = dataType.required === true || (Array.isArray(dataType.required) && dataType.required.length > 0)
|
|
175
|
+
? dataType.required
|
|
176
|
+
: undefined;
|
|
177
|
+
if (!(pick || omit || partial || required))
|
|
178
|
+
return typeDef;
|
|
179
|
+
let out = '';
|
|
180
|
+
if (partial === true)
|
|
181
|
+
out += 'Partial<';
|
|
182
|
+
else if (partial) {
|
|
183
|
+
out += 'PartialSome<';
|
|
184
|
+
file.addExport('ts-gems', ['PartialSome']);
|
|
185
|
+
}
|
|
186
|
+
if (required === true)
|
|
187
|
+
out += 'Partial<';
|
|
188
|
+
else if (required) {
|
|
189
|
+
out += 'RequiredSome<';
|
|
190
|
+
file.addExport('ts-gems', ['RequiredSome']);
|
|
191
|
+
}
|
|
192
|
+
if (pick)
|
|
193
|
+
out += 'Pick<';
|
|
194
|
+
else if (omit)
|
|
195
|
+
out += 'Omit<';
|
|
196
|
+
out += typeDef;
|
|
197
|
+
if (omit || pick)
|
|
198
|
+
out +=
|
|
199
|
+
', ' +
|
|
200
|
+
(omit || pick)
|
|
201
|
+
.filter(x => !!x)
|
|
202
|
+
.map(x => `'${x}'`)
|
|
203
|
+
.join(' | ') +
|
|
204
|
+
'>';
|
|
205
|
+
if (partial) {
|
|
206
|
+
if (Array.isArray(partial))
|
|
207
|
+
out +=
|
|
208
|
+
', ' +
|
|
209
|
+
partial
|
|
210
|
+
.filter(x => !!x)
|
|
211
|
+
.map(x => `'${x}'`)
|
|
212
|
+
.join(' | ');
|
|
213
|
+
out += '>';
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
*
|
|
219
|
+
*/
|
|
220
|
+
export async function resolveTypeNameOrDef(dataType, file, intent) {
|
|
221
|
+
if (dataType.name && !dataType.embedded) {
|
|
222
|
+
if (internalTypeNames.includes(dataType.name))
|
|
223
|
+
return dataType.name;
|
|
224
|
+
const f = await this.processDataType(dataType);
|
|
225
|
+
if (!f)
|
|
226
|
+
return '';
|
|
227
|
+
file.addImport(f.filename, [dataType.name], true);
|
|
228
|
+
return dataType.name;
|
|
229
|
+
}
|
|
230
|
+
if (dataType instanceof SimpleType)
|
|
231
|
+
return this.generateSimpleTypeDefinition(dataType, intent);
|
|
232
|
+
if (dataType instanceof EnumType)
|
|
233
|
+
return this.generateEnumTypeDefinition(dataType, intent);
|
|
234
|
+
if (dataType instanceof MixinType)
|
|
235
|
+
return this.generateMixinTypeDefinition(dataType, file, intent);
|
|
236
|
+
if (dataType instanceof MappedType)
|
|
237
|
+
return this.generateMappedTypeDefinition(dataType, file, intent);
|
|
238
|
+
if (dataType instanceof ComplexType)
|
|
239
|
+
return this.generateComplexTypeDefinition(dataType, file, intent);
|
|
240
|
+
return '';
|
|
241
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { pascalCase } from 'putil-varhelpers';
|
|
4
|
+
import { OpraHttpClient } from '@opra/client';
|
|
5
|
+
import { HttpApi } from '@opra/common';
|
|
6
|
+
export async function processDocument(document, options) {
|
|
7
|
+
if (!document || typeof document === 'string') {
|
|
8
|
+
if (document) {
|
|
9
|
+
const out = this._documentsMap.get(document);
|
|
10
|
+
if (out)
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
this.emit('log', chalk.cyan('Fetching document schema from ') + chalk.blueBright(this.serviceUrl));
|
|
14
|
+
const client = new OpraHttpClient(this.serviceUrl);
|
|
15
|
+
document = await client.fetchDocument({ documentId: document });
|
|
16
|
+
}
|
|
17
|
+
this._document = document;
|
|
18
|
+
let out = this._documentsMap.get(document.id);
|
|
19
|
+
if (out)
|
|
20
|
+
return out;
|
|
21
|
+
out = {
|
|
22
|
+
document,
|
|
23
|
+
generator: this,
|
|
24
|
+
};
|
|
25
|
+
this._documentsMap.set(document.id, out);
|
|
26
|
+
this.emit('log', chalk.white('[' + document.id + '] ') + chalk.cyan('Processing document ') + chalk.magenta(document.info.title));
|
|
27
|
+
if (document.references.size) {
|
|
28
|
+
this.emit('log', chalk.white('[' + document.id + '] ') + chalk.cyan(`Processing references`));
|
|
29
|
+
for (const ref of document.references.values()) {
|
|
30
|
+
const generator = this.extend();
|
|
31
|
+
generator._document = ref;
|
|
32
|
+
generator._documentRoot = '/references/' + (ref.info.title ? pascalCase(ref.info.title) : ref.id);
|
|
33
|
+
generator._typesRoot = path.join(generator._documentRoot, 'models');
|
|
34
|
+
await generator.processDocument(ref, { typesOnly: true });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
this._fileHeaderDocInfo = `/*
|
|
38
|
+
* ${document.info.title}
|
|
39
|
+
* Id: ${document.id}
|
|
40
|
+
* Version: ${document.info.version}
|
|
41
|
+
* ${this.serviceUrl}
|
|
42
|
+
*/`;
|
|
43
|
+
if (document.types.size) {
|
|
44
|
+
this.emit('log', chalk.white('[' + document.id + ']'), chalk.cyan(`Processing data types`));
|
|
45
|
+
for (const t of document.types.values()) {
|
|
46
|
+
await this.processDataType(t);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (options?.typesOnly)
|
|
50
|
+
return out;
|
|
51
|
+
if (document.api instanceof HttpApi) {
|
|
52
|
+
await this.processHttpApi(document.api);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { camelCase, pascalCase } from 'putil-varhelpers';
|
|
3
|
+
import { CodeBlock } from '../../code-block.js';
|
|
4
|
+
import { httpControllerNodeScript } from '../http-controller-node.js';
|
|
5
|
+
import { wrapJSDocString } from '../utils/string-utils.js';
|
|
6
|
+
export async function processHttpApi(api) {
|
|
7
|
+
let file = this._filesMap.get(api);
|
|
8
|
+
if (file)
|
|
9
|
+
return file;
|
|
10
|
+
const className = api.name ? pascalCase(api.name) : 'Api';
|
|
11
|
+
file = this.addFile(className + '.ts');
|
|
12
|
+
file.addImport('@opra/client', ['kClient', 'OpraHttpClient']);
|
|
13
|
+
const indexTs = this.addFile('/index.ts', true);
|
|
14
|
+
indexTs.addExport('.' + file.filename);
|
|
15
|
+
const httpApiNodeFile = this.addFile('./http-controller-node.ts');
|
|
16
|
+
httpApiNodeFile.code.content = httpControllerNodeScript;
|
|
17
|
+
const classBlock = (file.code[className] = new CodeBlock());
|
|
18
|
+
classBlock.doc = `/**
|
|
19
|
+
* ${wrapJSDocString(api.description || '')}
|
|
20
|
+
* @class ${className}
|
|
21
|
+
* @url ${path.posix.join(this.serviceUrl, '$schema')}
|
|
22
|
+
*/`;
|
|
23
|
+
classBlock.head = `\nexport class ${className} {\n\t`;
|
|
24
|
+
classBlock.properties = 'readonly [kClient]: OpraHttpClient;';
|
|
25
|
+
const classConstBlock = (classBlock.classConstBlock = new CodeBlock());
|
|
26
|
+
classConstBlock.head = `\n\nconstructor(client: OpraHttpClient) {`;
|
|
27
|
+
classConstBlock.body = `\n\tthis[kClient] = client;`;
|
|
28
|
+
classConstBlock.tail = `\b\n}\n`;
|
|
29
|
+
for (const controller of api.controllers.values()) {
|
|
30
|
+
const generator = this.extend();
|
|
31
|
+
const f = await generator.processHttpController(controller);
|
|
32
|
+
const childClassName = pascalCase(controller.name) + 'Controller';
|
|
33
|
+
file.addImport('.' + f.filename, [childClassName]);
|
|
34
|
+
const property = '$' + controller.name.charAt(0).toLowerCase() + camelCase(controller.name.substring(1));
|
|
35
|
+
classBlock.properties += `\nreadonly ${property}: ${childClassName};`;
|
|
36
|
+
classConstBlock.body += `\nthis.${property} = new ${childClassName}(client);`;
|
|
37
|
+
}
|
|
38
|
+
classBlock.tail = `\b}`;
|
|
39
|
+
return file;
|
|
40
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { camelCase, pascalCase } from 'putil-varhelpers';
|
|
3
|
+
import { CodeBlock } from '../../code-block.js';
|
|
4
|
+
import { locateNamedType } from '../utils/locate-named-type.js';
|
|
5
|
+
import { wrapJSDocString } from '../utils/string-utils.js';
|
|
6
|
+
export async function processHttpController(controller) {
|
|
7
|
+
let file = this._filesMap.get(controller);
|
|
8
|
+
if (file)
|
|
9
|
+
return file;
|
|
10
|
+
const className = pascalCase(controller.name) + 'Controller';
|
|
11
|
+
file = this.addFile(path.join(this._apiPath, className + '.ts'));
|
|
12
|
+
file.addImport('@opra/client', ['HttpRequestObservable', 'kClient', 'OpraHttpClient']);
|
|
13
|
+
file.addImport(path.relative(file.dirname, '/http-controller-node.ts'), ['HttpControllerNode']);
|
|
14
|
+
const classBlock = (file.code[className] = new CodeBlock());
|
|
15
|
+
classBlock.doc = `/**
|
|
16
|
+
* ${wrapJSDocString(controller.description || '')}
|
|
17
|
+
* @class ${className}
|
|
18
|
+
* @url ${path.posix.join(this.serviceUrl, '$schema', '#resources/' + className)}
|
|
19
|
+
*/`;
|
|
20
|
+
classBlock.head = `\nexport class ${className} extends HttpControllerNode {\n\t`;
|
|
21
|
+
classBlock.properties = '';
|
|
22
|
+
const classConstBlock = (classBlock.classConstBlock = new CodeBlock());
|
|
23
|
+
classConstBlock.head = `\n\nconstructor(client: OpraHttpClient) {`;
|
|
24
|
+
classConstBlock.body = `\n\tsuper(client);`;
|
|
25
|
+
classConstBlock.tail = `\b\n}\n`;
|
|
26
|
+
if (controller.controllers.size) {
|
|
27
|
+
for (const child of controller.controllers.values()) {
|
|
28
|
+
const generator = this.extend();
|
|
29
|
+
generator._apiPath = path.join(this._apiPath, className);
|
|
30
|
+
const f = await generator.processHttpController(child);
|
|
31
|
+
const childClassName = pascalCase(child.name) + 'Controller';
|
|
32
|
+
file.addImport(f.filename, [childClassName]);
|
|
33
|
+
const property = '$' + child.name.charAt(0).toLowerCase() + camelCase(child.name.substring(1));
|
|
34
|
+
classBlock.properties += `\nreadonly ${property}: ${childClassName};`;
|
|
35
|
+
classConstBlock.body += `\nthis.${property} = new ${childClassName}(client);`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Process operations */
|
|
39
|
+
for (const operation of controller.operations.values()) {
|
|
40
|
+
const operationBlock = (classBlock['operation_' + operation.name] = new CodeBlock());
|
|
41
|
+
operationBlock.doc = `
|
|
42
|
+
/**
|
|
43
|
+
* ${wrapJSDocString(operation.description || operation.name + ' operation')}`;
|
|
44
|
+
if (operation.parameters.length) {
|
|
45
|
+
const block = new CodeBlock();
|
|
46
|
+
block.doc = '\n *\n * RegExp parameters:';
|
|
47
|
+
let i = 0;
|
|
48
|
+
for (const prm of operation.parameters) {
|
|
49
|
+
if (!(prm.name instanceof RegExp))
|
|
50
|
+
continue;
|
|
51
|
+
i++;
|
|
52
|
+
block.doc +=
|
|
53
|
+
`\n * > ${String(prm.name)} - ${prm.description || ''}` +
|
|
54
|
+
`\n * - location: ${prm.location}` +
|
|
55
|
+
`\n * - type: ${locateNamedType(prm.type)?.name || 'any'}${prm.isArray ? '[' + prm.arraySeparator + ']' : ''}` +
|
|
56
|
+
(prm.required ? `\n * required: ${prm.required}` : '') +
|
|
57
|
+
(prm.deprecated ? `\n * deprecated: ${prm.deprecated}` : '');
|
|
58
|
+
}
|
|
59
|
+
if (i)
|
|
60
|
+
operationBlock.doc += block;
|
|
61
|
+
}
|
|
62
|
+
operationBlock.doc += `\n */\n`;
|
|
63
|
+
operationBlock.head = `${operation.name}(`;
|
|
64
|
+
/** Process operation parameters */
|
|
65
|
+
const mergedParams = [...controller.parameters, ...operation.parameters];
|
|
66
|
+
const pathParams = [];
|
|
67
|
+
const queryParams = [];
|
|
68
|
+
const headerParams = [];
|
|
69
|
+
if (mergedParams.length) {
|
|
70
|
+
const pathParamsMap = {};
|
|
71
|
+
const queryParamsMap = {};
|
|
72
|
+
const headerParamsMap = {};
|
|
73
|
+
for (const prm of mergedParams) {
|
|
74
|
+
if (typeof prm.name !== 'string')
|
|
75
|
+
continue;
|
|
76
|
+
if (prm.location === 'path')
|
|
77
|
+
pathParamsMap[prm.name] = prm;
|
|
78
|
+
if (prm.location === 'query')
|
|
79
|
+
queryParamsMap[prm.name] = prm;
|
|
80
|
+
if (prm.location === 'header')
|
|
81
|
+
headerParamsMap[prm.name] = prm;
|
|
82
|
+
}
|
|
83
|
+
pathParams.push(...Object.values(pathParamsMap));
|
|
84
|
+
queryParams.push(...Object.values(queryParamsMap));
|
|
85
|
+
headerParams.push(...Object.values(headerParamsMap));
|
|
86
|
+
}
|
|
87
|
+
let argIndex = 0;
|
|
88
|
+
for (const prm of pathParams) {
|
|
89
|
+
const type = locateNamedType(prm.type);
|
|
90
|
+
if (argIndex++ > 0)
|
|
91
|
+
operationBlock.head += ', ';
|
|
92
|
+
operationBlock.head += `${prm.name}: ${type?.name || 'any'}`;
|
|
93
|
+
}
|
|
94
|
+
let hasBody = false;
|
|
95
|
+
if (operation.requestBody?.content.length) {
|
|
96
|
+
if (argIndex++ > 0)
|
|
97
|
+
operationBlock.head += ', ';
|
|
98
|
+
let typeArr = [];
|
|
99
|
+
for (const content of operation.requestBody.content) {
|
|
100
|
+
if (content.type) {
|
|
101
|
+
const dtFile = this._filesMap.get(content.type);
|
|
102
|
+
if (dtFile) {
|
|
103
|
+
const typeName = await this.resolveTypeNameOrDef(content.type, dtFile, 'field');
|
|
104
|
+
typeArr.push(typeName);
|
|
105
|
+
file.addImport(dtFile.filename, [typeName]);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
typeArr = [];
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
if (typeArr.length) {
|
|
113
|
+
if (operation.requestBody.partial) {
|
|
114
|
+
file.addImport('ts-gems', ['PartialDTO']);
|
|
115
|
+
operationBlock.head += `$body: PartialDTO<${typeArr.join(' | ')}>`;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
file.addImport('ts-gems', ['DTO']);
|
|
119
|
+
operationBlock.head += `$body: DTO<${typeArr.join(' | ')}>`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else
|
|
123
|
+
operationBlock.head += `$body: any`;
|
|
124
|
+
hasBody = true;
|
|
125
|
+
}
|
|
126
|
+
/** process query params */
|
|
127
|
+
const isQueryRequired = queryParams.find(p => p.required);
|
|
128
|
+
const isHeadersRequired = queryParams.find(p => p.required);
|
|
129
|
+
if (queryParams.length) {
|
|
130
|
+
if (argIndex++ > 0)
|
|
131
|
+
operationBlock.head += ', ';
|
|
132
|
+
operationBlock.head += '\n\t$params' + (isHeadersRequired || isQueryRequired ? '' : '?') + ': {\n\t';
|
|
133
|
+
for (const prm of queryParams) {
|
|
134
|
+
const type = locateNamedType(prm.type);
|
|
135
|
+
operationBlock.head += `/**\n * ${prm.description || ''}\n */\n`;
|
|
136
|
+
operationBlock.head += `${prm.name}${prm.required ? '' : '?'}: `;
|
|
137
|
+
if (type?.name) {
|
|
138
|
+
const typeFile = await this.processDataType(type);
|
|
139
|
+
if (typeFile) {
|
|
140
|
+
file.addImport(typeFile.filename, [type.name]);
|
|
141
|
+
operationBlock.head += `${type.name};\n`;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
operationBlock.head += `${type?.name || 'any'};\n`;
|
|
146
|
+
}
|
|
147
|
+
operationBlock.head += '\b}\b';
|
|
148
|
+
}
|
|
149
|
+
/** process header params */
|
|
150
|
+
if (headerParams.length) {
|
|
151
|
+
if (argIndex++ > 0)
|
|
152
|
+
operationBlock.head += ', \n';
|
|
153
|
+
operationBlock.head += '\t$headers' + (isHeadersRequired ? '' : '?') + ': {\n\t';
|
|
154
|
+
for (const prm of headerParams) {
|
|
155
|
+
const type = locateNamedType(prm.type);
|
|
156
|
+
operationBlock.head += `/**\n * ${prm.description || ''}\n */\n`;
|
|
157
|
+
operationBlock.head += `${prm.name}${prm.required ? '' : '?'}: `;
|
|
158
|
+
if (type?.name) {
|
|
159
|
+
const typeFile = await this.processDataType(type);
|
|
160
|
+
if (typeFile) {
|
|
161
|
+
file.addImport(typeFile.filename, [type.name]);
|
|
162
|
+
operationBlock.head += `${type.name};\n`;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
operationBlock.head += `${type?.name || 'any'};\n`;
|
|
167
|
+
}
|
|
168
|
+
operationBlock.head += '\b}\b';
|
|
169
|
+
}
|
|
170
|
+
operationBlock.head += `\n): HttpRequestObservable<any>{`;
|
|
171
|
+
operationBlock.body = `\n\t`;
|
|
172
|
+
operationBlock.body +=
|
|
173
|
+
`const url = this._prepareUrl('${operation.getFullUrl()}', {` + pathParams.map(p => p.name).join(', ') + '});';
|
|
174
|
+
operationBlock.body +=
|
|
175
|
+
`\nreturn this[kClient].request(url, {` +
|
|
176
|
+
(hasBody ? ' body: $body,' : '') +
|
|
177
|
+
(queryParams.length ? ' params: $params as any,' : '') +
|
|
178
|
+
(headerParams.length ? ' headers: $headers as any,' : '') +
|
|
179
|
+
'});';
|
|
180
|
+
operationBlock.tail = `\b\n};\n`;
|
|
181
|
+
}
|
|
182
|
+
classBlock.tail = `\b}`;
|
|
183
|
+
return file;
|
|
184
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import flattenText from 'putil-flattentext';
|
|
3
|
+
import { CodeBlock } from '../code-block.js';
|
|
4
|
+
export class TsFile {
|
|
5
|
+
constructor(filename) {
|
|
6
|
+
this.filename = filename;
|
|
7
|
+
this.imports = {};
|
|
8
|
+
this.exportFiles = {};
|
|
9
|
+
this.exportTypes = [];
|
|
10
|
+
this.code = new CodeBlock();
|
|
11
|
+
this.dirname = path.dirname(filename);
|
|
12
|
+
this.code.header = '';
|
|
13
|
+
this.code.imports = '';
|
|
14
|
+
this.code.exports = '';
|
|
15
|
+
}
|
|
16
|
+
addImport(filename, items, typeImport) {
|
|
17
|
+
if (isLocalFile(filename)) {
|
|
18
|
+
filename = path.relative(this.dirname, path.resolve(this.dirname, filename));
|
|
19
|
+
if (!filename.startsWith('.'))
|
|
20
|
+
filename = './' + filename;
|
|
21
|
+
}
|
|
22
|
+
if (filename.endsWith('.d.ts'))
|
|
23
|
+
filename = filename.substring(0, filename.length - 5);
|
|
24
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
25
|
+
filename = filename.substring(0, filename.length - 3);
|
|
26
|
+
const imp = (this.imports[filename] = this.imports[filename] || { items: [], typeImport });
|
|
27
|
+
if (!typeImport)
|
|
28
|
+
imp.typeImport = false;
|
|
29
|
+
items?.forEach(x => {
|
|
30
|
+
if (!imp.items.includes(x))
|
|
31
|
+
imp.items.push(x);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
addExport(filename, types) {
|
|
35
|
+
if (isLocalFile(filename)) {
|
|
36
|
+
filename = path.relative(this.dirname, path.resolve(this.dirname, filename));
|
|
37
|
+
if (!filename.startsWith('.'))
|
|
38
|
+
filename = './' + filename;
|
|
39
|
+
}
|
|
40
|
+
if (filename.endsWith('.d.ts'))
|
|
41
|
+
filename = filename.substring(0, filename.length - 5);
|
|
42
|
+
if (filename.endsWith('.ts') || filename.endsWith('.js'))
|
|
43
|
+
filename = filename.substring(0, filename.length - 3);
|
|
44
|
+
this.exportFiles[filename] = this.exportFiles[filename] || [];
|
|
45
|
+
types?.forEach(x => {
|
|
46
|
+
if (!this.exportFiles[filename].includes(x))
|
|
47
|
+
this.exportFiles[filename].push(x);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
generate(options) {
|
|
51
|
+
this.code.imports = Object.keys(this.imports)
|
|
52
|
+
.sort((a, b) => {
|
|
53
|
+
if (a.startsWith('@'))
|
|
54
|
+
return -1;
|
|
55
|
+
if (b.startsWith('@'))
|
|
56
|
+
return 1;
|
|
57
|
+
if (!a.startsWith('.'))
|
|
58
|
+
return -1;
|
|
59
|
+
if (!b.startsWith('.'))
|
|
60
|
+
return 1;
|
|
61
|
+
return a.toLowerCase().localeCompare(b.toLowerCase());
|
|
62
|
+
})
|
|
63
|
+
.map(filename => {
|
|
64
|
+
const imp = this.imports[filename];
|
|
65
|
+
let relFile = filename;
|
|
66
|
+
if (!isPackageName(filename)) {
|
|
67
|
+
if (options?.importExt)
|
|
68
|
+
relFile += '.js';
|
|
69
|
+
}
|
|
70
|
+
return `import${imp.typeImport ? ' type' : ''} ${imp.items.length ? '{ ' + imp.items.join(', ') + ' } from ' : ''}'${relFile}';`;
|
|
71
|
+
})
|
|
72
|
+
.join('\n');
|
|
73
|
+
this.code.exports = Object.keys(this.exportFiles)
|
|
74
|
+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
|
75
|
+
.map(filename => {
|
|
76
|
+
const types = this.exportFiles[filename];
|
|
77
|
+
if (!isPackageName(filename)) {
|
|
78
|
+
if (options?.importExt)
|
|
79
|
+
filename += '.js';
|
|
80
|
+
}
|
|
81
|
+
return `export ${types.length ? '{ ' + types.join(', ') + ' }' : '*'} from '${filename}';`;
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
84
|
+
if (this.code.imports || this.code.exports)
|
|
85
|
+
this.code.exports += '\n\n';
|
|
86
|
+
return ('/* #!oprimp_auto_generated!# !! Do NOT remove this line */\n/* eslint-disable */\n' +
|
|
87
|
+
flattenText(String(this.code)));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function isLocalFile(s) {
|
|
91
|
+
return typeof s === 'string' && (s.startsWith('.') || s.startsWith('/'));
|
|
92
|
+
}
|
|
93
|
+
const PACKAGENAME_PATTERN = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
94
|
+
function isPackageName(s) {
|
|
95
|
+
return PACKAGENAME_PATTERN.test(s);
|
|
96
|
+
}
|