1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 | const swagger = require('./swagger');
|
10 | const fs = require('fs-extra');
|
11 | const cc = require('camelcase');
|
12 | const pascalCase = require('pascal-case');
|
13 | const md5 = require('md5');
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | const classTpl = (cfg) => {
|
19 | return `const {ServiceBase} = require('./serviceBase');
|
20 | class ${cfg.className} extends ServiceBase {
|
21 | constructor() {
|
22 | super('${cfg.url}');
|
23 | }
|
24 | ${operationTpl(cfg.operations)}
|
25 | }
|
26 | module.exports = ${cfg.className};
|
27 | `;
|
28 | };
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 | const operationTpl = (operations) => {
|
35 | let tpl = '';
|
36 | Object.keys(operations).forEach(key => {
|
37 | const operation = operations[key];
|
38 | tpl += `
|
39 | /**
|
40 | * ${operation.description}
|
41 | */
|
42 | ${operation.name}(params${operation.entityName ? ` , ${operation.entityName}` : ''}${(operation.entityType ? `/* ${operation.entityType} */` : '')}) {
|
43 | return this.createRequest('${operation.pathFragment}', params, '${operation.method}'${operation.entityName ? ', ' + operation.entityName : ''});
|
44 | }`;
|
45 | });
|
46 | return tpl;
|
47 | };
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | const modelTpl = (modelCfg) => `
|
60 | ${modelCfg.imports.toString().replace(/[,]/g, '')}
|
61 | class ${modelCfg.className} {
|
62 | ${modelCfg.docBlocks.toString().replace(/[,]/g, '')}
|
63 |
|
64 | constructor({${modelCfg.props}} = {}) {
|
65 | Object.assign(this, {${modelCfg.props}});
|
66 | }
|
67 | }
|
68 | ${modelCfg.className}.fromJSON = function(src) {
|
69 | if (!src) {
|
70 | return null;
|
71 | }
|
72 | if (Array.isArray(src)) {
|
73 | return src.map(${modelCfg.className}.fromJSON);
|
74 | }
|
75 | ${modelCfg.assignments.toString().replace(/[,]/g, '')}
|
76 | const {${modelCfg.props}} = src;
|
77 | return new ${modelCfg.className}({${modelCfg.props}});
|
78 | };
|
79 |
|
80 | module.exports = ${modelCfg.className};
|
81 | `;
|
82 |
|
83 |
|
84 |
|
85 |
|
86 |
|
87 | const propertyDocBlockTpl = (type, name) => `
|
88 | /**
|
89 | * @property {${type}} ${name}
|
90 | */
|
91 | `;
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | const assignmentTpl = (rawType, name) => `
|
104 | src.${name} = ${rawType}.fromJSON(src.${name}) || undefined;
|
105 | `;
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | function findEntity(swaggerOperation) {
|
115 | return (swaggerOperation.parameters || []).find(param => param.in === 'body');
|
116 | }
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 | function findRootAndNodeFromPath(path) {
|
128 | const parts = path.split('/');
|
129 | let i = parts.length;
|
130 | const info = {};
|
131 |
|
132 | while (i--) {
|
133 | if (parts[i] && !/({[\w]+})/.test(parts[i])) {
|
134 | info.node = parts[i];
|
135 | break;
|
136 | }
|
137 | }
|
138 |
|
139 | while (i--) {
|
140 | if (parts[i] && /({knowledgeBaseID})/i.test(parts[i]) && parts[i + 1]) {
|
141 | info.root = parts[i + 1];
|
142 | break;
|
143 | }
|
144 | }
|
145 | return info;
|
146 | }
|
147 |
|
148 | const definitionsMap = {};
|
149 |
|
150 | function addDefinitionFromExample(json, className) {
|
151 | className = pascalCase(className);
|
152 | const type = typeof json;
|
153 | const isArray = Array.isArray(json);
|
154 | const definition = { type: isArray ? 'array' : type };
|
155 |
|
156 | if (!isArray && type === 'object') {
|
157 | const keys = Object.keys(json);
|
158 | const signature = md5(keys.toString());
|
159 | const $ref = `#/definitions/${className}`;
|
160 | if (!definitionsMap[signature]) {
|
161 | const definitionEntry = Object.assign({}, definition);
|
162 | definitionEntry.properties = {};
|
163 | keys.forEach(key => {
|
164 | definitionEntry.properties[key] = addDefinitionFromExample(json[key], key);
|
165 | });
|
166 | if (definitionsMap[$ref]) {
|
167 | console.warn($ref + ' already exists');
|
168 | }
|
169 | (swagger.definitions || (swagger.definitions = {}))[className] = definitionEntry;
|
170 | definitionsMap[signature] = definitionEntry;
|
171 | }
|
172 | definition.$ref = $ref;
|
173 | } else if (isArray) {
|
174 | className = className.substr(0, className.length - 1);
|
175 | definition.items = addDefinitionFromExample(json[0], className);
|
176 | }
|
177 | return definition;
|
178 | }
|
179 |
|
180 |
|
181 | const configsMap = {};
|
182 | Object.keys(swagger.paths).sort().forEach(pathName => {
|
183 | const { root, node } = findRootAndNodeFromPath(pathName);
|
184 | const fileName = root || node;
|
185 | const pathFragment = pathName.substr(pathName.indexOf(fileName) + fileName.length);
|
186 | const { [pathName]: path } = swagger.paths;
|
187 | const keys = Object.keys(path);
|
188 |
|
189 | let i = keys.length;
|
190 | while (i--) {
|
191 | const method = keys[i];
|
192 | const { [method]: swaggerOperation } = path;
|
193 |
|
194 | if (swaggerOperation.description && swaggerOperation.description.toLowerCase().includes('deprecated')) {
|
195 | continue;
|
196 | }
|
197 | const className = fileName.replace(/[\w]/, match => match.toUpperCase());
|
198 | const cfg = configsMap[fileName] || { className, url: pathName, operations: {} };
|
199 | const params = (swaggerOperation.parameters || []).filter(param => (!/(body)/.test(param.in)));
|
200 |
|
201 |
|
202 |
|
203 | const operationName = swaggerOperation.operationId
|
204 | .replace(/(')/g, '')
|
205 | .split('-')
|
206 | .pop()
|
207 | .toLowerCase()
|
208 | .replace(/( \w)/g, (...args) => args[2] ? args[0]
|
209 | .trim()
|
210 | .toUpperCase() : args[0]
|
211 | .trim()
|
212 | .toLowerCase());
|
213 |
|
214 |
|
215 |
|
216 | const entityToConsume = findEntity(swaggerOperation) || { name: '', schema: { $ref: '' } };
|
217 | if (!entityToConsume.schema.$ref && entityToConsume.schema.example) {
|
218 | const entity = JSON.parse(entityToConsume.schema.example);
|
219 | entityToConsume.name = operationName;
|
220 | entityToConsume.schema = addDefinitionFromExample(entity, operationName);
|
221 | }
|
222 |
|
223 |
|
224 | let command = `qnamaker ${operationName}`;
|
225 | if (root && root !== node) {
|
226 | command += ` ${node} `;
|
227 | }
|
228 | command += entityToConsume.name ? ` --in ${entityToConsume.name}.json` : '';
|
229 | command += params
|
230 | .slice()
|
231 | .reduce((agg, param) => (agg += ` --${param.name} <${param.type}>`), '');
|
232 |
|
233 | const operation = {
|
234 | name: operationName,
|
235 | method,
|
236 | command: command.trim(),
|
237 | pathFragment: pathName.includes(pathFragment) ? '' : pathFragment,
|
238 | params,
|
239 | description: (swaggerOperation.description || '').replace(/[\r]/g, ''),
|
240 | };
|
241 |
|
242 | if (!operation.params.length) {
|
243 | delete operation.params;
|
244 | }
|
245 |
|
246 |
|
247 |
|
248 | if (entityToConsume.name) {
|
249 | operation.entityName = entityToConsume.name;
|
250 | operation.entityType = (entityToConsume.schema.$ref || '').split('/').pop();
|
251 | }
|
252 | cfg.operations[operationName] = operation;
|
253 | configsMap[fileName] = cfg;
|
254 | }
|
255 | });
|
256 |
|
257 |
|
258 | const modelTypesByName = {};
|
259 | Object.keys((swagger.definitions || {})).forEach(key => {
|
260 | const def = swagger.definitions[key];
|
261 | const { properties, items } = def;
|
262 | if (items) {
|
263 | console.log(def);
|
264 | }
|
265 | if (properties) {
|
266 | const importsMap = {};
|
267 | const model = { className: key, imports: [], props: [], assignments: [], docBlocks: [] };
|
268 | Object.keys(properties).forEach(propName => {
|
269 | const propDetails = properties[propName];
|
270 | const name = cc(propName);
|
271 | let type;
|
272 |
|
273 |
|
274 |
|
275 |
|
276 | if (propDetails.type === 'object') {
|
277 | type = propDetails.$ref.split('/').pop();
|
278 | if (type.toLowerCase() != name.toLowerCase())
|
279 | model.imports.push(`const ${type} = require('./${name}');`);
|
280 | model.assignments.push(assignmentTpl(type, name));
|
281 | } else if (propDetails.type === 'array') {
|
282 | const $ref = (propDetails.items.$ref || '').split('/').pop() || propDetails.items.type;
|
283 | type = `${$ref}[]`;
|
284 | if (!/^(string|integer|number|boolean)$/.test($ref) && !importsMap[$ref]) {
|
285 | if ($ref.toLowerCase() != cc($ref).toLowerCase())
|
286 | model.imports.push(`const ${$ref} = require('./${cc($ref)}');\n`);
|
287 | model.assignments.push(assignmentTpl($ref, name));
|
288 | importsMap[$ref] = true;
|
289 | }
|
290 | } else {
|
291 | type = propDetails.type;
|
292 | }
|
293 | model.docBlocks.push(propertyDocBlockTpl(type, name));
|
294 | model.props.push(`${name} /* ${type} */`);
|
295 | });
|
296 | modelTypesByName[`#/definitions/${key}`] = model;
|
297 | }
|
298 | });
|
299 |
|
300 |
|
301 |
|
302 | let classNames = {};
|
303 | Object.keys(configsMap).forEach(fileName => {
|
304 | const cfg = configsMap[fileName];
|
305 | const clazz = classTpl(cfg);
|
306 | const path = `${fileName}`;
|
307 | fs.outputFileSync(`lib/api/${path}.js`, clazz);
|
308 | (classNames[fileName] || (classNames[fileName] = [])).push({ path, name: cfg.className });
|
309 | });
|
310 |
|
311 |
|
312 |
|
313 | let apiIndexJs = '';
|
314 | Object.keys(classNames).forEach(fileName => {
|
315 | apiIndexJs += `module.exports.${fileName} = require('./${fileName}');\n`;
|
316 | });
|
317 | fs.outputFileSync('lib/api/index.js', apiIndexJs);
|
318 |
|
319 |
|
320 |
|
321 | let modelNames = [];
|
322 | Object.keys(modelTypesByName).forEach(key => {
|
323 | const modelCfg = modelTypesByName[key];
|
324 | const model = modelTpl(modelCfg);
|
325 | fs.outputFileSync(`lib/api/dataModels/${cc(modelCfg.className)}.js`, model);
|
326 | modelNames.push(modelCfg.className);
|
327 | });
|
328 |
|
329 | const modelIndexJS = modelNames.sort().map(clazz => `module.exports.${clazz} = require('./${cc(clazz)}');`).join('\n');
|
330 |
|
331 | fs.outputFileSync('lib/api/dataModels/index.js', modelIndexJS);
|
332 |
|
333 | fs.writeJsonSync('lib/api/qnamaker.json', configsMap);
|