UNPKG

12.4 kBJavaScriptView Raw
1/**
2 * Script for parsing the swagger.json and
3 * generating the services, data models commands
4 * the manifest.
5 *
6 * Command pattern is: <api group> <action> <target> <subtarget> --<args>
7 */
8
9const swagger = require('./swagger');
10const fs = require('fs-extra');
11const cc = require('camelcase');
12const pascalCase = require('pascal-case');
13const md5 = require('md5');
14
15/**
16 * Services template extends ServiceBase
17 */
18const classTpl = (cfg) => {
19 return `const {ServiceBase} = require('./serviceBase');
20class ${cfg.className} extends ServiceBase {
21 constructor() {
22 super('${cfg.url}');
23 }
24${operationTpl(cfg.operations)}
25}
26module.exports = ${cfg.className};
27`;
28};
29
30/**
31 * Operation method template. Used to populate the
32 * operations array used in the classTpl
33 */
34const 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 * Data model template. All data models
51 *
52 * have 2 ways to hydrate data:
53 * 1. pass an object into the constructor
54 * 2. use the static fromJSON() method
55 *
56 * Either way, only the properties that are
57 * passed to the body of the request are extracted
58 */
59const modelTpl = (modelCfg) => `
60${modelCfg.imports.toString().replace(/[,]/g, '')}
61class ${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
80module.exports = ${modelCfg.className};
81`;
82
83/**
84 * Doc block template used to populate the
85 * docBlocks array used in the modelTpl
86 */
87const propertyDocBlockTpl = (type, name) => `
88 /**
89 * @property {${type}} ${name}
90 */
91`;
92
93/**
94 * Template used for typed property assignments
95 * from source data inside the fromJSON() static
96 * method.
97 *
98 * @param {string} rawType The data model class name containing a fromJSON static
99 * @param {string} name The property name on the source object containing the object to type
100 *
101 * @returns {string} The hydrated template string
102 */
103const assignmentTpl = (rawType, name) => `
104 src.${name} = ${rawType}.fromJSON(src.${name}) || undefined;
105`;
106
107/**
108 * Finds the entity (if present) that should be passed
109 * as the body of the request. PUT, POST and PATCH only.
110 *
111 * @param {*} swaggerOperation The swagger operation containing an array of params to search
112 * @returns {*} The operation containing the info about the body of the request
113 */
114function findEntity(swaggerOperation) {
115 return (swaggerOperation.parameters || []).find(param => param.in === 'body');
116}
117
118/**
119 * Finds the root and node from a url path.
120 * if {knowledgeBaseID} exist withing the
121 * path, these become the root and the first
122 * bracket pair after are the node.
123 *
124 * @param {string} path The endpoint path to parse
125 * @returns {{root:string, node:string}} The object containing the root and node
126 */
127function 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
148const definitionsMap = {};
149
150function 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// Builds the Service classes and luis.json
181const configsMap = {};
182Object.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 // bail, we're deprecated - uncomment to include deprecated APIs
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 // Pull out the operation name - this is used in the operationTpl
201 // as the name for the method
202 // e.g. transforms "apps - Get applications list" to "getApplicationsList"
203 const operationName = swaggerOperation.operationId
204 .replace(/(')/g, '') // single quotes exist somewhere in the manifest
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 // Find the entity to send in the request. If a schema does not exist,
215 // attempt to create one using the example.
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 // Build the command example for the help output: luis <api group> <action> <target> <subtarget> --<args>
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 // Build the operation entry for the manifest
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 // If a body is expected in the request, keep
247 // information about this in the luis.json
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// Generates the data models from the swagger.json
258const modelTypesByName = {};
259Object.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 // This is a complex data type containing a property
273 // which is itself another typed object.
274 // import the dependency and hold for use
275 // in the fromJSON() static
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// Hydrates the service class templates
301// and writes them to disk.
302let classNames = {};
303Object.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// Writes the index.js files for each
312// directory of service classes.
313let apiIndexJs = '';
314Object.keys(classNames).forEach(fileName => {
315 apiIndexJs += `module.exports.${fileName} = require('./${fileName}');\n`;
316});
317fs.outputFileSync('lib/api/index.js', apiIndexJs);
318
319// Hydrates the data model templates an
320// writes them to disk
321let modelNames = [];
322Object.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// Creates and writes the index.js for the data models
329const modelIndexJS = modelNames.sort().map(clazz => `module.exports.${clazz} = require('./${cc(clazz)}');`).join('\n');
330
331fs.outputFileSync('lib/api/dataModels/index.js', modelIndexJS);
332// Write the qnamaker.json
333fs.writeJsonSync('lib/api/qnamaker.json', configsMap);