Skip to content

Commit 67f286b

Browse files
authored
Merge pull request #188 from amarzavery/uml
Generate Uml diagram
2 parents cc8f51d + b946bfd commit 67f286b

File tree

14 files changed

+871
-17
lines changed

14 files changed

+871
-17
lines changed

.vscode/launch.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@
4343
],
4444
"env": {}
4545
},
46+
{
47+
"type": "node",
48+
"request": "launch",
49+
"name": "generate uml",
50+
"program": "${workspaceRoot}/cli.js",
51+
"cwd": "${workspaceRoot}",
52+
"args": [
53+
"generate-uml",
54+
"D:/sdk/azure-rest-api-specs-pr/specification/datamigration/resource-manager/Microsoft.DataMigration/2017-11-15-preview/datamigration.json"
55+
],
56+
"env": {}
57+
},
4658
{
4759
"type": "node",
4860
"request": "launch",

ChangeLog.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
### 12/11/2017 0.4.22
2+
- Added support to generate class diagram from a given swagger spec #188.
3+
- Fixed #190, #191.
14
### 12/4/2017 0.4.21
2-
- Remove the enum constraint or reference to an enum on the discriminator property if previously present before making it a constant.
5+
- Removed the enum constraint or reference to an enum on the discriminator property if previously present before making it a constant.
36

47
### 11/20/2017 0.4.20
58
- 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.

cli.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var packageVersion = require('./package.json').version;
1616
yargs
1717
.version(packageVersion)
1818
.commandDir('lib/commands')
19+
.strict()
1920
.option('h', { alias: 'help' })
2021
.option('l', {
2122
alias: 'logLevel',
@@ -26,7 +27,7 @@ yargs
2627
.option('f', {
2728
alias: 'logFilepath',
2829
describe: `Set the log file path. It must be an absolute filepath. ` +
29-
`By default the logs will stored in a timestamp based log file at "${defaultLogDir}".`
30+
`By default the logs will stored in a timestamp based log file at "${defaultLogDir}".`
3031
})
3132
.global(['h', 'l', 'f'])
3233
.help()

lib/commands/extract-xmsexamples.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ exports.builder = {
1414
d: {
1515
alias: 'outDir',
1616
describe: 'The output directory where the x-ms-examples files need to be stored. If not provided ' +
17-
'then the output will be stored in a folder name "output" adjacent to the working directory.',
17+
'then the output will be stored in a folder name "output" adjacent to the working directory.',
1818
string: true
1919
},
2020
m: {

lib/commands/generate-uml.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
'use strict';
5+
var util = require('util'),
6+
log = require('../util/logging'),
7+
validate = require('../validate');
8+
9+
exports.command = 'generate-uml <spec-path>';
10+
11+
exports.describe = 'Generates a class diagram of the model definitions in the given swagger spec.';
12+
13+
exports.builder = {
14+
d: {
15+
alias: 'outputDir',
16+
describe: 'Output directory where the class diagram will be stored.',
17+
string: true,
18+
default: './'
19+
},
20+
p: {
21+
alias: 'disableProperties',
22+
describe: 'Should model properties not be generated?',
23+
boolean: true,
24+
default: false
25+
},
26+
a: {
27+
alias: 'disableAllof',
28+
describe: 'Should allOf references not be generated?',
29+
boolean: true,
30+
default: false
31+
},
32+
r: {
33+
alias: 'disableRefs',
34+
describe: 'Should model references not be generated?',
35+
boolean: true,
36+
default: false
37+
},
38+
i: {
39+
alias: 'direction',
40+
describe: 'The direction of the generated diagram:\n' +
41+
'"TB" - TopToBottom (default),\n' + '"LR" - "LeftToRight",\n' + '"RL" - "RightToLeft"',
42+
string: true,
43+
default: "TB",
44+
choices: ["TB", "LR", "RL"]
45+
}
46+
};
47+
48+
exports.handler = function (argv) {
49+
log.debug(argv);
50+
let specPath = argv.specPath;
51+
let vOptions = {};
52+
vOptions.consoleLogLevel = argv.logLevel;
53+
vOptions.logFilepath = argv.f;
54+
vOptions.shouldDisableProperties = argv.p;
55+
vOptions.shouldDisableAllof = argv.a;
56+
vOptions.shouldDisableRefs = argv.r;
57+
vOptions.direction = argv.i;
58+
function execGenerateUml() {
59+
return validate.generateUml(specPath, argv.d, vOptions);
60+
}
61+
return execGenerateUml().catch((err) => { process.exitCode = 1; });
62+
};
63+
64+
exports = module.exports;

lib/commands/generate-wireformat.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ exports.builder = {
1414
d: {
1515
alias: 'outDir',
1616
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 ' +
17-
'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 ' +
18-
'output will be stored in a folder named "wire-fromat" inside the current working directory.',
17+
'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 ' +
18+
'output will be stored in a folder named "wire-fromat" inside the current working directory.',
1919
strting: true
2020
},
2121
o: {
2222
alias: 'operationIds',
2323
describe: 'A comma separated string of operationIds for which the examples ' +
24-
'need to be transformed. If operationIds are not provided then the entire spec will be processed. ' +
25-
'Example: "StorageAccounts_Create, StorageAccounts_List, Usages_List".',
24+
'need to be transformed. If operationIds are not provided then the entire spec will be processed. ' +
25+
'Example: "StorageAccounts_Create, StorageAccounts_List, Usages_List".',
2626
string: true
2727
},
2828
y: {
2929
alias: 'inYaml',
3030
describe: 'A boolean flag when provided will indicate the tool to ' +
31-
'generate wireformat in a yaml doc. Default is a markdown doc.',
31+
'generate wireformat in a yaml doc. Default is a markdown doc.',
3232
boolean: true
3333
}
3434
};
@@ -42,11 +42,15 @@ exports.handler = function (argv) {
4242
let emitYaml = argv.inYaml;
4343
vOptions.consoleLogLevel = argv.logLevel;
4444
vOptions.logFilepath = argv.f;
45-
if (specPath.match(/.*composite.*/ig) !== null) {
46-
return validate.generateWireFormatInCompositeSpec(specPath, outDir, emitYaml, vOptions);
47-
} else {
48-
return validate.generateWireFormat(specPath, outDir, emitYaml, operationIds, vOptions);
45+
46+
function execWireFormat() {
47+
if (specPath.match(/.*composite.*/ig) !== null) {
48+
return validate.generateWireFormatInCompositeSpec(specPath, outDir, emitYaml, vOptions);
49+
} else {
50+
return validate.generateWireFormat(specPath, outDir, emitYaml, operationIds, vOptions);
51+
}
4952
}
53+
return execWireFormat().catch((err) => { process.exitCode = 1; });
5054
}
5155

5256
exports = module.exports;

lib/commands/resolve-spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ exports.handler = function (argv) {
8282
return validate.resolveSpec(specPath, argv.d, vOptions);
8383
}
8484
}
85-
execResolve();
85+
return execResolve().catch((err) => { process.exitCode = 1; });
8686
};
8787

8888
exports = module.exports;

lib/umlGenerator.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
'use strict';
5+
6+
var util = require('util'),
7+
JsonRefs = require('json-refs'),
8+
yuml2svg = require('yuml2svg'),
9+
utils = require('./util/utils'),
10+
Constants = require('./util/constants'),
11+
log = require('./util/logging'),
12+
ErrorCodes = Constants.ErrorCodes;
13+
14+
/**
15+
* @class
16+
* Generates a Uml Diagaram in svg format.
17+
*/
18+
class UmlGenerator {
19+
20+
/**
21+
* @constructor
22+
* Initializes a new instance of the UmlGenerator class.
23+
*
24+
* @param {object} specInJson the parsed spec in json format
25+
*
26+
* @return {object} An instance of the UmlGenerator class.
27+
*/
28+
constructor(specInJson, options) {
29+
if (specInJson === null || specInJson === undefined || typeof specInJson !== 'object') {
30+
throw new Error('specInJson is a required property of type object')
31+
}
32+
this.specInJson = specInJson;
33+
this.graphDefinition = '';
34+
if (!options) options = {};
35+
this.options = options;
36+
this.bg = '{bg:cornsilk}';
37+
}
38+
39+
generateGraphDefinition() {
40+
this.generateModelPropertiesGraph();
41+
if (!this.options.shouldDisableAllof) {
42+
this.generateAllOfGraph();
43+
}
44+
}
45+
46+
generateAllOfGraph() {
47+
let spec = this.specInJson;
48+
let definitions = spec.definitions;
49+
for (let modelName in definitions) {
50+
let model = definitions[modelName];
51+
if (model.allOf) {
52+
model.allOf.map((item) => {
53+
let referencedModel = item;
54+
let ref = item['$ref'];
55+
let segments = ref.split('/');
56+
let parent = segments[segments.length - 1];
57+
this.graphDefinition += `\n[${parent}${this.bg}]^-.-allOf[${modelName}${this.bg}]`;
58+
});
59+
}
60+
}
61+
}
62+
63+
generateModelPropertiesGraph() {
64+
let spec = this.specInJson;
65+
let definitions = spec.definitions;
66+
let references = [];
67+
for (let modelName in definitions) {
68+
let model = definitions[modelName];
69+
let modelProperties = model.properties;
70+
let props = '';
71+
if (modelProperties) {
72+
for (let propertyName in modelProperties) {
73+
let property = modelProperties[propertyName];
74+
let propertyType = this.getPropertyType(modelName, property, references);
75+
let discriminator = '';
76+
if (model.discriminator && model.discriminator === propertyName) {
77+
discriminator = '(discriminator)';
78+
}
79+
props += `-${propertyName}${discriminator}:${propertyType};`;
80+
}
81+
}
82+
if (!this.options.shouldDisableProperties) {
83+
this.graphDefinition += props.length ? `[${modelName}|${props}${this.bg}]\n` : `[${modelName}${this.bg}]\n`;
84+
}
85+
86+
}
87+
if (references.length && !this.options.shouldDisableRefs) {
88+
this.graphDefinition += references.join('\n');
89+
}
90+
}
91+
92+
getPropertyType(modelName, property, references) {
93+
if (property.type && property.type.match(/^(string|number|boolean)$/i) !== null) {
94+
return property.type;
95+
}
96+
97+
if (property.type === 'array') {
98+
let result = 'Array<'
99+
if (property.items) {
100+
result += this.getPropertyType(modelName, property.items, references);
101+
}
102+
result += '>';
103+
return result;
104+
}
105+
106+
if (property['$ref']) {
107+
let segments = property['$ref'].split('/');
108+
let referencedModel = segments[segments.length - 1];
109+
references.push(`[${modelName}${this.bg}]->[${referencedModel}${this.bg}]`);
110+
return referencedModel;
111+
}
112+
113+
if (property.additionalProperties && typeof property.additionalProperties === 'object') {
114+
let result = 'Dictionary<';
115+
result += this.getPropertyType(modelName, property.additionalProperties, references);
116+
result += '>';
117+
return result;
118+
}
119+
120+
if (property.type === 'object') {
121+
return 'Object'
122+
}
123+
return '';
124+
}
125+
126+
generateDiagramFromGraph() {
127+
this.generateGraphDefinition();
128+
let svg = '';
129+
try {
130+
log.info(this.graphDefinition);
131+
svg = yuml2svg(this.graphDefinition, false, { dir: this.options.direction, type: 'class' });
132+
//console.log(svg);
133+
} catch (err) {
134+
return Promise.reject(err);
135+
}
136+
return Promise.resolve(svg);
137+
}
138+
}
139+
140+
module.exports = UmlGenerator;

lib/validate.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ var fs = require('fs'),
1616
SpecValidator = require('./validators/specValidator'),
1717
WireFormatGenerator = require('./wireFormatGenerator'),
1818
XMsExampleExtractor = require('./xMsExampleExtractor'),
19-
SpecResolver = require('./validators/specResolver');
19+
SpecResolver = require('./validators/specResolver'),
20+
UmlGenerator = require('./umlGenerator');
2021

2122
exports = module.exports;
2223

@@ -206,6 +207,42 @@ exports.generateWireFormatInCompositeSpec = function generateWireFormatInComposi
206207
});
207208
};
208209

210+
exports.generateUml = function generateUml(specPath, outputDir, options) {
211+
if (!options) options = {};
212+
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
213+
log.filepath = options.logFilepath || log.filepath;
214+
let specFileName = path.basename(specPath);
215+
let resolver;
216+
let resolverOptions = {};
217+
resolverOptions.shouldResolveRelativePaths = true;
218+
resolverOptions.shouldResolveXmsExamples = false;
219+
resolverOptions.shouldResolveAllOf = false;
220+
resolverOptions.shouldSetAdditionalPropertiesFalse = false;
221+
resolverOptions.shouldResolvePureObjects = false;
222+
resolverOptions.shouldResolveDiscriminator = false;
223+
resolverOptions.shouldResolveParameterizedHost = false;
224+
resolverOptions.shouldResolveNullableTypes = false;
225+
return utils.parseJson(specPath).then((result) => {
226+
resolver = new SpecResolver(specPath, result, resolverOptions);
227+
return resolver.resolve();
228+
}).then(() => {
229+
let umlGenerator = new UmlGenerator(resolver.specInJson, options);
230+
return umlGenerator.generateDiagramFromGraph();
231+
}).then((svgGraph) => {
232+
if (outputDir !== './' && !fs.existsSync(outputDir)) {
233+
fs.mkdirSync(outputDir);
234+
}
235+
let svgFile = specFileName.replace(path.extname(specFileName), '.svg');
236+
let outputFilepath = `${path.join(outputDir, svgFile)}`;
237+
fs.writeFileSync(`${path.join(outputDir, svgFile)}`, svgGraph, { encoding: 'utf8' });
238+
console.log(`Saved the uml at "${outputFilepath}". Please open the file in a browser.`);
239+
return Promise.resolve();
240+
}).catch((err) => {
241+
log.error(err);
242+
return Promise.reject(err);
243+
});
244+
};
245+
209246
exports.updateEndResultOfSingleValidation = function updateEndResultOfSingleValidation(validator) {
210247
if (validator.specValidationResult.validityStatus) {
211248
if (!(log.consoleLogLevel === 'json' || log.consoleLogLevel === 'off')) {
@@ -216,6 +253,7 @@ exports.updateEndResultOfSingleValidation = function updateEndResultOfSingleVali
216253
}
217254
}
218255
if (!validator.specValidationResult.validityStatus) {
256+
process.exitCode = 1;
219257
exports.finalValidationResult.validityStatus = validator.specValidationResult.validityStatus;
220258
}
221259
return;

lib/xMsExampleExtractor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ class xMsExampleExtractor {
222222
log.error(`${JSON.stringify(accErrors)}`);
223223
}
224224
}).catch(function (err) {
225+
process.exitCode = 1;
225226
log.error(err);
226227
});
227228
}

0 commit comments

Comments
 (0)