Skip to content

Commit

Permalink
Merge pull request #188 from amarzavery/uml
Browse files Browse the repository at this point in the history
Generate Uml diagram
amarzavery authored Dec 12, 2017
2 parents cc8f51d + b946bfd commit 67f286b
Showing 14 changed files with 871 additions and 17 deletions.
12 changes: 12 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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",
5 changes: 4 additions & 1 deletion ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
### 12/11/2017 0.4.22
- Added support to generate class diagram from a given swagger spec #188.
- Fixed #190, #191.
### 12/4/2017 0.4.21
- Remove the enum constraint or reference to an enum on the discriminator property if previously present before making it a constant.
- Removed the enum constraint or reference to an enum on the discriminator property if previously present before making it a constant.

### 11/20/2017 0.4.20
- Added support for processing [`"x-ms-parameterized-host": {}`](https://github.com/Azure/autorest/tree/master/docs/extensions#x-ms-parameterized-host) extension if present in the 2.0 swagger spec.
3 changes: 2 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ var packageVersion = require('./package.json').version;
yargs
.version(packageVersion)
.commandDir('lib/commands')
.strict()
.option('h', { alias: 'help' })
.option('l', {
alias: 'logLevel',
@@ -26,7 +27,7 @@ yargs
.option('f', {
alias: 'logFilepath',
describe: `Set the log file path. It must be an absolute filepath. ` +
`By default the logs will stored in a timestamp based log file at "${defaultLogDir}".`
`By default the logs will stored in a timestamp based log file at "${defaultLogDir}".`
})
.global(['h', 'l', 'f'])
.help()
2 changes: 1 addition & 1 deletion lib/commands/extract-xmsexamples.js
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ exports.builder = {
d: {
alias: 'outDir',
describe: 'The output directory where the x-ms-examples files need to be stored. If not provided ' +
'then the output will be stored in a folder name "output" adjacent to the working directory.',
'then the output will be stored in a folder name "output" adjacent to the working directory.',
string: true
},
m: {
64 changes: 64 additions & 0 deletions lib/commands/generate-uml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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 <spec-path>';

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: './'
},
p: {
alias: 'disableProperties',
describe: 'Should model properties not be generated?',
boolean: true,
default: false
},
a: {
alias: 'disableAllof',
describe: 'Should allOf references not be generated?',
boolean: true,
default: false
},
r: {
alias: 'disableRefs',
describe: 'Should model references not be generated?',
boolean: true,
default: false
},
i: {
alias: 'direction',
describe: 'The direction of the generated diagram:\n' +
'"TB" - TopToBottom (default),\n' + '"LR" - "LeftToRight",\n' + '"RL" - "RightToLeft"',
string: true,
default: "TB",
choices: ["TB", "LR", "RL"]
}
};

exports.handler = function (argv) {
log.debug(argv);
let specPath = argv.specPath;
let vOptions = {};
vOptions.consoleLogLevel = argv.logLevel;
vOptions.logFilepath = argv.f;
vOptions.shouldDisableProperties = argv.p;
vOptions.shouldDisableAllof = argv.a;
vOptions.shouldDisableRefs = argv.r;
vOptions.direction = argv.i;
function execGenerateUml() {
return validate.generateUml(specPath, argv.d, vOptions);
}
return execGenerateUml().catch((err) => { process.exitCode = 1; });
};

exports = module.exports;
22 changes: 13 additions & 9 deletions lib/commands/generate-wireformat.js
Original file line number Diff line number Diff line change
@@ -14,21 +14,21 @@ exports.builder = {
d: {
alias: 'outDir',
describe: 'The output directory where the raw request/response markdown files need to be stored. If not provided and if the spec-path is a ' +
'local file path then the output will be stored in a folder named "wire-format" adjacent to the directory of the swagger spec. If the spec-path is a url then ' +
'output will be stored in a folder named "wire-fromat" inside the current working directory.',
'local file path then the output will be stored in a folder named "wire-format" adjacent to the directory of the swagger spec. If the spec-path is a url then ' +
'output will be stored in a folder named "wire-fromat" inside the current working directory.',
strting: true
},
o: {
alias: 'operationIds',
describe: 'A comma separated string of operationIds for which the examples ' +
'need to be transformed. If operationIds are not provided then the entire spec will be processed. ' +
'Example: "StorageAccounts_Create, StorageAccounts_List, Usages_List".',
'need to be transformed. If operationIds are not provided then the entire spec will be processed. ' +
'Example: "StorageAccounts_Create, StorageAccounts_List, Usages_List".',
string: true
},
y: {
alias: 'inYaml',
describe: 'A boolean flag when provided will indicate the tool to ' +
'generate wireformat in a yaml doc. Default is a markdown doc.',
'generate wireformat in a yaml doc. Default is a markdown doc.',
boolean: true
}
};
@@ -42,11 +42,15 @@ exports.handler = function (argv) {
let emitYaml = argv.inYaml;
vOptions.consoleLogLevel = argv.logLevel;
vOptions.logFilepath = argv.f;
if (specPath.match(/.*composite.*/ig) !== null) {
return validate.generateWireFormatInCompositeSpec(specPath, outDir, emitYaml, vOptions);
} else {
return validate.generateWireFormat(specPath, outDir, emitYaml, operationIds, vOptions);

function execWireFormat() {
if (specPath.match(/.*composite.*/ig) !== null) {
return validate.generateWireFormatInCompositeSpec(specPath, outDir, emitYaml, vOptions);
} else {
return validate.generateWireFormat(specPath, outDir, emitYaml, operationIds, vOptions);
}
}
return execWireFormat().catch((err) => { process.exitCode = 1; });
}

exports = module.exports;
2 changes: 1 addition & 1 deletion lib/commands/resolve-spec.js
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ exports.handler = function (argv) {
return validate.resolveSpec(specPath, argv.d, vOptions);
}
}
execResolve();
return execResolve().catch((err) => { process.exitCode = 1; });
};

exports = module.exports;
140 changes: 140 additions & 0 deletions lib/umlGenerator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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'),
log = require('./util/logging'),
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 = '';
if (!options) options = {};
this.options = options;
this.bg = '{bg:cornsilk}';
}

generateGraphDefinition() {
this.generateModelPropertiesGraph();
if (!this.options.shouldDisableAllof) {
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}${this.bg}]^-.-allOf[${modelName}${this.bg}]`;
});
}
}
}

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 = '';
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};`;
}
}
if (!this.options.shouldDisableProperties) {
this.graphDefinition += props.length ? `[${modelName}|${props}${this.bg}]\n` : `[${modelName}${this.bg}]\n`;
}

}
if (references.length && !this.options.shouldDisableRefs) {
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}${this.bg}]->[${referencedModel}${this.bg}]`);
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 {
log.info(this.graphDefinition);
svg = yuml2svg(this.graphDefinition, false, { dir: this.options.direction, type: 'class' });
//console.log(svg);
} catch (err) {
return Promise.reject(err);
}
return Promise.resolve(svg);
}
}

module.exports = UmlGenerator;
40 changes: 39 additions & 1 deletion lib/validate.js
Original file line number Diff line number Diff line change
@@ -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,42 @@ 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;
let resolverOptions = {};
resolverOptions.shouldResolveRelativePaths = true;
resolverOptions.shouldResolveXmsExamples = false;
resolverOptions.shouldResolveAllOf = false;
resolverOptions.shouldSetAdditionalPropertiesFalse = false;
resolverOptions.shouldResolvePureObjects = false;
resolverOptions.shouldResolveDiscriminator = false;
resolverOptions.shouldResolveParameterizedHost = false;
resolverOptions.shouldResolveNullableTypes = false;
return utils.parseJson(specPath).then((result) => {
resolver = new SpecResolver(specPath, result, resolverOptions);
return resolver.resolve();
}).then(() => {
let umlGenerator = new UmlGenerator(resolver.specInJson, options);
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}". Please open the file in a browser.`);
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')) {
@@ -216,6 +253,7 @@ exports.updateEndResultOfSingleValidation = function updateEndResultOfSingleVali
}
}
if (!validator.specValidationResult.validityStatus) {
process.exitCode = 1;
exports.finalValidationResult.validityStatus = validator.specValidationResult.validityStatus;
}
return;
1 change: 1 addition & 0 deletions lib/xMsExampleExtractor.js
Original file line number Diff line number Diff line change
@@ -222,6 +222,7 @@ class xMsExampleExtractor {
log.error(`${JSON.stringify(accErrors)}`);
}
}).catch(function (err) {
process.exitCode = 1;
log.error(err);
});
}
Loading

0 comments on commit 67f286b

Please sign in to comment.