From 7921344cf76f8f9f7a4fcd112c1390ae432714c1 Mon Sep 17 00:00:00 2001 From: Amar Zavery Date: Mon, 4 Dec 2017 10:42:00 -0800 Subject: [PATCH] generate uml diagram --- .vscode/launch.json | 12 +++ lib/commands/generate-uml.js | 35 ++++++++ lib/umlGenerator.js | 151 +++++++++++++++++++++++++++++++++ lib/validate.js | 38 ++++++++- package-lock.json | 156 ++++++++++++++++++++++++----------- package.json | 5 +- 6 files changed, 347 insertions(+), 50 deletions(-) create mode 100644 lib/commands/generate-uml.js create mode 100644 lib/umlGenerator.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 298d25d5..789f6659 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,6 +43,18 @@ ], "env": {} }, + { + "type": "node", + "request": "launch", + "name": "generate uml", + "program": "${workspaceRoot}/cli.js", + "cwd": "${workspaceRoot}", + "args": [ + "generate-uml", + "D:/sdk/azure-rest-api-specs-pr/specification/datamigration/resource-manager/Microsoft.DataMigration/2017-11-15-preview/datamigration.json" + ], + "env": {} + }, { "type": "node", "request": "launch", diff --git a/lib/commands/generate-uml.js b/lib/commands/generate-uml.js new file mode 100644 index 00000000..6e7018ac --- /dev/null +++ b/lib/commands/generate-uml.js @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +'use strict'; +var util = require('util'), + log = require('../util/logging'), + validate = require('../validate'); + +exports.command = 'generate-uml '; + +exports.describe = 'Generates a class diagram of the model definitions in the given swagger spec.'; + +exports.builder = { + d: { + alias: 'outputDir', + describe: 'Output directory where the class diagram will be stored.', + string: true, + default: './' + } +}; + +exports.handler = function (argv) { + log.debug(argv); + let specPath = argv.specPath; + let vOptions = {}; + vOptions.consoleLogLevel = argv.logLevel; + vOptions.logFilepath = argv.f; + + function execGenerateUml() { + return validate.generateUml(specPath, argv.d, vOptions); + } + execGenerateUml(); +}; + +exports = module.exports; \ No newline at end of file diff --git a/lib/umlGenerator.js b/lib/umlGenerator.js new file mode 100644 index 00000000..e44d3c86 --- /dev/null +++ b/lib/umlGenerator.js @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +'use strict'; + +var util = require('util'), + JsonRefs = require('json-refs'), + yuml2svg = require('yuml2svg'), + utils = require('./util/utils'), + Constants = require('./util/constants'), + ErrorCodes = Constants.ErrorCodes; + +/** + * @class + * Generates a Uml Diagaram in svg format. + */ +class UmlGenerator { + + /** + * @constructor + * Initializes a new instance of the UmlGenerator class. + * + * @param {object} specInJson the parsed spec in json format + * + * @return {object} An instance of the UmlGenerator class. + */ + constructor(specInJson, options) { + if (specInJson === null || specInJson === undefined || typeof specInJson !== 'object') { + throw new Error('specInJson is a required property of type object') + } + this.specInJson = specInJson; + this.graphDefinition = ''; + } + + generateGraphDefinition() { + this.generateModelPropertiesGraph(); + this.generateAllOfGraph(); + } + + generateAllOfGraph() { + let spec = this.specInJson; + let definitions = spec.definitions; + for (let modelName in definitions) { + let model = definitions[modelName]; + if (model.allOf) { + model.allOf.map((item) => { + let referencedModel = item; + let ref = item['$ref']; + let segments = ref.split('/'); + let parent = segments[segments.length - 1]; + this.graphDefinition += `\n[${parent}]^-.-allOf[${modelName}]`; + }); + } + } + } + + generateModelPropertiesGraph() { + let spec = this.specInJson; + let definitions = spec.definitions; + let references = []; + for (let modelName in definitions) { + let model = definitions[modelName]; + let modelProperties = model.properties; + let props = ''; + let bg = '{bg:cornsilk}'; + if (modelProperties) { + for (let propertyName in modelProperties) { + let property = modelProperties[propertyName]; + let propertyType = this.getPropertyType(modelName, property, references); + let discriminator = ''; + if (model.discriminator && model.discriminator === propertyName) { + discriminator = '(discriminator)'; + } + props += `-${propertyName}${discriminator}:${propertyType};`; + } + } + this.graphDefinition += props.length ? `[${modelName}|${props}${bg}]\n` : `[${modelName}${bg}]\n`; + + } + if (references.length) { + this.graphDefinition += references.join('\n'); + } + } + + getPropertyType(modelName, property, references) { + if (property.type && property.type.match(/^(string|number|boolean)$/i) !== null) { + return property.type; + } + + if (property.type === 'array') { + let result = 'Array<' + if (property.items) { + result += this.getPropertyType(modelName, property.items, references); + } + result += '>'; + return result; + } + + if (property['$ref']) { + let segments = property['$ref'].split('/'); + let referencedModel = segments[segments.length - 1]; + references.push(`[${modelName}]->[${referencedModel}]`); + return referencedModel; + } + + if (property.additionalProperties && typeof property.additionalProperties === 'object') { + let result = 'Dictionary<'; + result += this.getPropertyType(modelName, property.additionalProperties, references); + result += '>'; + return result; + } + + if (property.type === 'object') { + return 'Object' + } + return ''; + } + + generateDiagramFromGraph() { + this.generateGraphDefinition(); + let svg = ''; + try { + console.log(this.graphDefinition); + svg = yuml2svg(this.graphDefinition); + //console.log(svg); + } catch (err) { + return Promise.reject(err); + } + return Promise.resolve(svg); + } + + generateInheritanceGraph() { + let self = this; + let spec = self.specInJson; + let definitions = spec.definitions; + let modelNames = Object.keys(definitions); + let subTreeMap = new Map(); + + modelNames.map((modelName) => { + if (definitions[modelName].allOf) { + let rootNode = subTreeMap.get(modelName) + if (!rootNode) { + rootNode = utils.createInheritanceTree(spec, modelName, subTreeMap, { discriminator: definitions[modelName].discriminator }); + } + self.updateReferencesWithOneOf(subTreeMap, references); + } + }); + } +} + +module.exports = UmlGenerator; \ No newline at end of file diff --git a/lib/validate.js b/lib/validate.js index 9d6ceb9d..4f794801 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -16,7 +16,8 @@ var fs = require('fs'), SpecValidator = require('./validators/specValidator'), WireFormatGenerator = require('./wireFormatGenerator'), XMsExampleExtractor = require('./xMsExampleExtractor'), - SpecResolver = require('./validators/specResolver'); + SpecResolver = require('./validators/specResolver'), + UmlGenerator = require('./umlGenerator'); exports = module.exports; @@ -206,6 +207,41 @@ exports.generateWireFormatInCompositeSpec = function generateWireFormatInComposi }); }; +exports.generateUml = function generateUml(specPath, outputDir, options) { + if (!options) options = {}; + log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel; + log.filepath = options.logFilepath || log.filepath; + let specFileName = path.basename(specPath); + let resolver; + options.shouldResolveRelativePaths = true; + options.shouldResolveXmsExamples = false; + options.shouldResolveAllOf = false; + options.shouldSetAdditionalPropertiesFalse = false; + options.shouldResolvePureObjects = false; + options.shouldResolveDiscriminator = false; + options.shouldResolveParameterizedHost = false; + options.shouldResolveNullableTypes = false; + return utils.parseJson(specPath).then((result) => { + resolver = new SpecResolver(specPath, result, options); + return resolver.resolve(); + }).then(() => { + let umlGenerator = new UmlGenerator(resolver.specInJson); + return umlGenerator.generateDiagramFromGraph(); + }).then((svgGraph) => { + if (outputDir !== './' && !fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + let svgFile = specFileName.replace(path.extname(specFileName), '.svg'); + let outputFilepath = `${path.join(outputDir, svgFile)}`; + fs.writeFileSync(`${path.join(outputDir, svgFile)}`, svgGraph, { encoding: 'utf8' }); + console.log(`Saved the uml at "${outputFilepath}".`) + return Promise.resolve(); + }).catch((err) => { + log.error(err); + return Promise.reject(err); + }); +}; + exports.updateEndResultOfSingleValidation = function updateEndResultOfSingleValidation(validator) { if (validator.specValidationResult.validityStatus) { if (!(log.consoleLogLevel === 'json' || log.consoleLogLevel === 'off')) { diff --git a/package-lock.json b/package-lock.json index 3ebb1d60..604fc10d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "integrity": "sha1-7is7jqoRwJOCiZU2BrdFtzjFSx4=", "requires": { "@types/node": "8.0.53" } @@ -21,12 +21,12 @@ "@types/node": { "version": "8.0.53", "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.53.tgz", - "integrity": "sha512-54Dm6NwYeiSQmRB1BLXKr5GELi0wFapR1npi8bnZhEcu84d/yQKqnwwXQ56hZ0RUbTG6L5nqDZaN3dgByQXQRQ==" + "integrity": "sha1-OWs1r4JvpmqtRyyMt7jV4nf05tg=" }, "@types/request": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.0.8.tgz", - "integrity": "sha512-fp8gsp0Qlq5wRas4UDjzayBxzWtQVcIumsMaHnNJzrk1Skx4WRpX5/HchSdZZf5/3Jp9m59EUBIGSI6mQEMOOg==", + "integrity": "sha1-Qk094lWGgQftTdZpXGXF8XZquoA=", "requires": { "@types/form-data": "2.2.1", "@types/node": "8.0.53" @@ -41,7 +41,7 @@ "@types/uuid": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.3.tgz", - "integrity": "sha512-5fRLCYhLtDb3hMWqQyH10qtF+Ud2JnNCXTCZ+9ktNdCcgslcuXkDTkFcJNk++MT29yDntDnlF1+jD+uVGumsbw==", + "integrity": "sha1-EhrOJl9Vac5A9PbQ/3ijOMcyp1Q=", "requires": { "@types/node": "8.0.53" } @@ -333,6 +333,37 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color/-/color-2.0.1.tgz", + "integrity": "sha512-ubUCVVKfT7r2w2D3qtHakj8mbmKms+tThR8gI8zEYCbUBl8/voqFGt3kgBqGwXAopgXybnkuOq+qMYCRrp4cXw==", + "requires": { + "color-convert": "1.9.1", + "color-string": "1.5.2" + } + }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.2.tgz", + "integrity": "sha1-JuRYFLw8mny9Z1FkikFDRRSnc6k=", + "requires": { + "color-name": "1.1.3", + "simple-swizzle": "0.2.2" + } + }, "colors": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", @@ -465,6 +496,14 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" }, + "deref": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/deref/-/deref-0.7.1.tgz", + "integrity": "sha1-/gbyAyyjwiLFjHweld24BzL4RoE=", + "requires": { + "deep-extend": "0.4.2" + } + }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", @@ -683,6 +722,11 @@ "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" + }, "fast-deep-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", @@ -934,7 +978,7 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha1-76ouqdqg16suoTqXsritUf776L4=" }, "is-builtin-module": { "version": "1.0.0", @@ -1087,6 +1131,32 @@ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" }, + "json-schema-faker": { + "version": "0.5.0-rc9", + "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rc9.tgz", + "integrity": "sha1-yv8wpFX0+LJnxZdGuzqXVBQIfBE=", + "requires": { + "deref": "0.7.1", + "json-schema-ref-parser": "3.3.1", + "randexp": "0.4.6", + "tslib": "1.8.0" + }, + "dependencies": { + "json-schema-ref-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-3.3.1.tgz", + "integrity": "sha1-hudRuAmTV79gGnz+QtEBI+6QajI=", + "requires": { + "call-me-maybe": "1.0.1", + "debug": "3.1.0", + "es6-promise": "4.1.1", + "js-yaml": "3.10.0", + "ono": "4.0.2", + "z-schema": "3.18.4" + } + } + } + }, "json-schema-ref-parser": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-1.4.1.tgz", @@ -1660,7 +1730,7 @@ "ms-rest": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/ms-rest/-/ms-rest-2.2.7.tgz", - "integrity": "sha512-mpzbZaeYSN7OaKHW0qUbI2pDZ47oddjGJK6AzjRh7UZ7doFlr1LrgMEWQAiZDDFO9jE8usmCOz4kqWATJq/+nQ==", + "integrity": "sha1-zKD0s1VV4t9kdEAo4lI9QBdejig=", "requires": { "@types/node": "8.0.53", "@types/request": "2.0.8", @@ -1678,7 +1748,7 @@ "ms-rest-azure": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/ms-rest-azure/-/ms-rest-azure-2.4.5.tgz", - "integrity": "sha512-O6ho+0EQB0QcRhR5SnTdfRols5WW8FoPCikUwWPQU4f5YOtu/eFNuEZjrsLeIZ06HV2erKh2KH1kUyMHXe1jTw==", + "integrity": "sha1-vyenyP9fEKVPDxhBML/Qt5dOdVM=", "requires": { "@types/node": "8.0.53", "@types/uuid": "3.4.3", @@ -2093,6 +2163,21 @@ "integrity": "sha1-ATKgVBemEmhmQmrPEW8e1WI6XNA=", "dev": true }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "0.3.1" + }, + "dependencies": { + "is-arrayish": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.1.tgz", + "integrity": "sha1-wt/DhquqDD4zxI2z/ocFnmkGXv0=" + } + } + }, "slash": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", @@ -2342,7 +2427,7 @@ } }, "sway": { - "version": "github:amarzavery/sway#b54dceada91eb998744720ec166ff19d4e603a2c", + "version": "github:amarzavery/sway#3eb975c383770c33f01297ab3b150f5dfe7e69c3", "requires": { "debug": "3.1.0", "faker": "4.1.0", @@ -2358,48 +2443,11 @@ "z-schema": "3.18.4" }, "dependencies": { - "deref": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/deref/-/deref-0.7.1.tgz", - "integrity": "sha512-XSRDBknHtbTPqrLwZyqZ8Cr7kRE3vhyVnizbYeIkgZUQHr54u6QMRXGOoYwxoEgy0Xw5dPCXirHtGHFPYm4IwQ==", - "requires": { - "deep-extend": "0.4.2" - } - }, - "faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, - "json-schema-faker": { - "version": "0.5.0-rc9", - "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rc9.tgz", - "integrity": "sha1-yv8wpFX0+LJnxZdGuzqXVBQIfBE=", - "requires": { - "deref": "0.7.1", - "json-schema-ref-parser": "3.3.1", - "randexp": "0.4.6", - "tslib": "1.8.0" - } - }, - "json-schema-ref-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-3.3.1.tgz", - "integrity": "sha512-stQTMhec2R/p2L9dH4XXRlpNCP0mY8QrLd/9Kl+8SHJQmwHtE1nDfXH4wbsSM+GkJMl8t92yZbI0OIol432CIQ==", - "requires": { - "call-me-maybe": "1.0.1", - "debug": "3.1.0", - "es6-promise": "4.1.1", - "js-yaml": "3.10.0", - "ono": "4.0.2", - "z-schema": "3.18.4" - } - }, "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", @@ -2441,7 +2489,7 @@ "tunnel": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.5.tgz", - "integrity": "sha512-gj5sdqherx4VZKMcBA4vewER7zdK25Td+z1npBqpbDys4eJrLx+SlYjJvq1bDXs2irkuJM5pf8ktaEQVipkrbA==" + "integrity": "sha1-0VMiVHSe02Yg/NEBCGVJWh+p0K4=" }, "tunnel-agent": { "version": "0.6.0", @@ -2530,6 +2578,11 @@ "extsprintf": "1.3.0" } }, + "viz.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/viz.js/-/viz.js-1.8.0.tgz", + "integrity": "sha1-4Mta0kE2jjWxpulgaR66RUwklR8=" + }, "vscode-jsonrpc": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-3.4.1.tgz", @@ -2627,6 +2680,15 @@ "camelcase": "3.0.0" } }, + "yuml2svg": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/yuml2svg/-/yuml2svg-3.1.0.tgz", + "integrity": "sha512-gTGq+637C+ZdURr9yyjiAKw4JcL15ZvSiifOEtvSQ1cU1FDOlC6P2+bMnlo6mwkSJaATuKwCOjYjvBRVGkA5Rw==", + "requires": { + "color": "2.0.1", + "viz.js": "1.8.0" + } + }, "z-schema": { "version": "3.18.4", "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", @@ -2639,4 +2701,4 @@ } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 91b0c793..c9e00f90 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "jsonpath": "^0.2.11", "vscode-jsonrpc": "^3.2.0", "autorest-extension-base": "olydis/autorest-extension-base", - "lodash": "^1.0.0" + "lodash": "^1.0.0", + "yuml2svg": "^3.1.0" }, "devDependencies": { "jshint": "2.9.4", @@ -54,4 +55,4 @@ "test": "npm -s run-script jshint && mocha -t 100000", "start": "node ./lib/autorestPlugin/pluginHost.js" } -} \ No newline at end of file +}