From 0f21da4a92b4e0ef11a5f274468d3679fc9784ee Mon Sep 17 00:00:00 2001 From: Michel Jonathan Schmitz Date: Mon, 27 May 2019 13:10:41 +0200 Subject: [PATCH] feat: add the uml generator --- .gitlab-ci.yml | 27 + Dockerfile | 1 + README.md | 65 ++- package-lock.json | 334 +++++++++--- package.json | 9 + src/cli.ts | 104 +++- src/common.ts | 61 ++- src/uml/createDiagram.ts | 274 ++++++++++ src/uml/model/LightweightClassDefinition.ts | 56 ++ src/uml/model/LightweightDefinition.ts | 28 + src/uml/model/LightweightEnumDefinition.ts | 30 ++ src/uml/model/LightweightProperty.ts | 54 ++ src/uml/model/LightweightType.ts | 85 +++ src/uml/readDefinitions.ts | 423 +++++++++++++++ src/uml/umlConfig.ts | 54 ++ test/Common.spec.ts | 1 + test/CreateDiagram.spec.ts | 85 +++ test/ReadDefinitions.spec.ts | 29 + test/model/TestClass.ts | 34 ++ test/model/TestEnum.ts | 25 + test/model/TestFunction.ts | 17 + test/model/TestInterface.ts | 34 ++ test/model/TestUnion.ts | 24 + test/model/generatedModel.ts | 552 ++++++++++++++++++++ 24 files changed, 2310 insertions(+), 96 deletions(-) create mode 100644 Dockerfile create mode 100644 src/uml/createDiagram.ts create mode 100644 src/uml/model/LightweightClassDefinition.ts create mode 100644 src/uml/model/LightweightDefinition.ts create mode 100644 src/uml/model/LightweightEnumDefinition.ts create mode 100644 src/uml/model/LightweightProperty.ts create mode 100644 src/uml/model/LightweightType.ts create mode 100644 src/uml/readDefinitions.ts create mode 100644 src/uml/umlConfig.ts create mode 100644 test/CreateDiagram.spec.ts create mode 100644 test/ReadDefinitions.spec.ts create mode 100644 test/model/TestClass.ts create mode 100644 test/model/TestEnum.ts create mode 100644 test/model/TestFunction.ts create mode 100644 test/model/TestInterface.ts create mode 100644 test/model/TestUnion.ts create mode 100644 test/model/generatedModel.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e89afa22..821369b9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,6 +21,21 @@ build: paths: - lib +package: + dependencies: + - build + tags: + - secrecy + stage: deploy + script: + - echo "//registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN" > /root/.npmrc + - npm publish + only: + - /^v[0-9]+.[0-9]+.[0-9]+$/ + artifacts: + paths: + - lib + test: tags: - docker @@ -71,6 +86,18 @@ package: paths: - lib +uml: + cache: {} + dependencies: + - build + stage: test + image: node:10 + services: + - name: registry.gitlab.com/openstapps/core-tools:latest + alias: plantuml + script: + - node lib/cli.js plantuml test/model http://plantuml:8080 --showProperties --showOptionalProperties --showInheritedProperties --showEnumValues --showAssociations --showInheritance --excludeExternals + pages: stage: deploy script: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2c031ae9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +FROM plantuml/plantuml-server:tomcat \ No newline at end of file diff --git a/README.md b/README.md index 88e02ed9..6bb173a6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # @openstapps/core-tools -[![pipeline status](https://img.shields.io/gitlab/pipeline/openstapps/core-tools.svg?style=flat-square)](https://gitlab.com/openstapps/core-tools/commits/master) +[![pipeline status](https://img.shields.io/gitlab/pipeline/openstapps/core-tools.svg?style=flat-square)](https://gitlab.com/openstapps/core-tools/commits/master) [![npm](https://img.shields.io/npm/v/@openstapps/core-tools.svg?style=flat-square)](https://npmjs.com/package/@openstapps/core-tools) [![license)](https://img.shields.io/npm/l/@openstapps/core-tools.svg?style=flat-square)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![documentation](https://img.shields.io/badge/documentation-online-blue.svg?style=flat-square)](https://openstapps.gitlab.io/core-tools) @@ -15,7 +15,7 @@ JSON schema files are needed for run-time validation of SC-type objects, as this The StAppsCore Validator is a tool for run-time validation of objects (determining whether a JavaScript/JSON object is a valid object of the corresponding SC-type. It consumes JSON schema files from StAppsCore as the definitions of SC-types against which are validated concrete (actual) objects (as an example SCDish object in the example below). -## Installation +## Installation Installation of the npm package (using `npm install`) makes the tool available as an executable with the name `openstapps-core-tools`. @@ -57,7 +57,7 @@ import {join} from 'path'; const objectToValidate: SCDish = { type: SCThingType.Dish, -// more properties + // more properties }; // instantiate a new validator @@ -127,7 +127,7 @@ Inside of a script in `package.json` or if the npm package is installed globally openstapps-core-tools validate lib/schema src/test/resources report.html ``` -## Generate documentation for routes +## Generate documentation for routes To generate a documentation for the routes use the following command. @@ -142,3 +142,60 @@ To pack all the different files into two distribution files - one for definition ```shell openstapps-core-tools pack ``` + +## How to use the UML generator + +The UML Generator generates PlantUML from the project reflection of the source files. By default it will include externals, which will take considerably longer to execute, you can disable this behaviour via an option. It can help you to visually explore the data model or document a specific part. + +You can either use the public PlantUML-server or start your own local instance. To build the image and run, restart or stop the container use the scripts provided in the `package.json`. + +### Generating from source-files + +```shell +openstapps-core-tools plantuml PATH/TO/SOURCEFILES http://PLANTUMLSERVER +``` + +Executing this command will generate a `.svg` file in your current working directory. + +Multiple options can be set to enhance the diagram. By default all additional information other than the definitions are disabled. You can use: + +- `--showProperties` to show all mandatory attributes of the classes and interfaces. +- `--showOptionalProperties` to show all mandatory attributes of the classes and interfaces. `--showProperties` must be set! +- `--showEnumValues` to show all enumeration and type (enumeration-like) values +- `--showInheritance` to show the hierarchy of the classes and interfaces. Inherited attributes will only be shown in their parent. +- `--showAssociations` to show all references of classes and interfaces between one another +- `--excludeExternals` to exclude external definitions +- `--definitions ` to show only specific definitions to reduce the output of the diagram. `` is a comma seperated list of definitions. + +The best way to explore models is to enable `--showInheritance` and `--showAssociations`. Start with just one definition in your `--definition `-list, generate the diagram, look at it, add a new definition that you have seen to your command and generate anew. + +#### Examples + +Show the class hierarchy of the whole project: + +```shell +openstapps-core-tools plantuml PATH/TO/SRCDIR http://PLANTUMLSERVER --showInheritance +``` + +Show the dish-module: + +```shell +openstapps-core-tools plantuml ../core http://localhost:8080 --showProperties --showOptionalProperties --showInheritance --showAssociations --showEnumValues --definitions SCDish,SCThingThatCanBeOfferedOffer +``` + +### Generating from existing file + +The plantuml code is persisted inside the generated filea at the very bottom. You can tweak the model by using the function to generate UML from a PlantUML-file(simple text file). Extract the code (starting from `@startuml` to `@enduml`), edit it manually and execute the function. + +```shell +openstapps-core-tools plantuml-file /PATH/TO/Project.plantuml http://PLANTUMLSERVER +``` + +Example-File-Content of Project.plantuml +``` +@startuml +interface MyClass{ + myProperty: string +} +@enduml +``` diff --git a/package-lock.json b/package-lock.json index d4bb5107..3aa265f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,19 @@ "nodemailer": "6.2.1" } }, + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "@types/chai": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", @@ -104,6 +117,15 @@ "@types/node": "*" } }, + "@types/got": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@types/got/-/got-9.4.4.tgz", + "integrity": "sha512-IGAJokJRE9zNoBdY5csIwN4U5qQn+20HxC0kM+BbUdfTKIXa7bOX/pdhy23NnLBRP8Wvyhx7X5e6EHJs+4d8HA==", + "requires": { + "@types/node": "*", + "@types/tough-cookie": "*" + } + }, "@types/handlebars": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@types/handlebars/-/handlebars-4.1.0.tgz", @@ -143,6 +165,15 @@ "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-0.8.32.tgz", "integrity": "sha512-RTVWV485OOf4+nO2+feurk0chzHkSjkjALiejpHltyuMf/13fGymbbNNFrSKdSSUg1TIwzszXdWsVirxgqYiFA==" }, + "@types/nock": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.2.tgz", + "integrity": "sha512-jOdoZ3zVLmPWZOoPJDoks+Zo6GsogdZuVBWs8/prWau993qno5PPtukVXKfc+WtS/ROgTPWpiru92z2K6KFYgQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "10.14.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.8.tgz", @@ -181,6 +212,11 @@ "@types/node": "*" } }, + "@types/tough-cookie": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.5.tgz", + "integrity": "sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==" + }, "@types/yaml": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/yaml/-/yaml-1.0.2.tgz", @@ -315,6 +351,30 @@ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, + "cacheable-request": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.0.0.tgz", + "integrity": "sha512-2N7AmszH/WPPpl5Z3XMw1HAP+8d+xugnKQAeKvxFZ/04dbT/CAznqwbl+7eSr3HkwdepNwtb2yx3CAMQWvG01Q==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^4.0.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^1.0.1", + "normalize-url": "^3.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "camelcase": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", @@ -371,6 +431,14 @@ "wrap-ansi": "^2.0.0" } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -554,9 +622,9 @@ "dev": true }, "conventional-changelog-writer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.6.tgz", - "integrity": "sha512-ou/sbrplJMM6KQpR5rKFYNVQYesFjN7WpNGdudQSWNi6X+RgyFUcSv871YBYkrUYV9EX8ijMohYVzn9RUb+4ag==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.5.tgz", + "integrity": "sha512-g/Myp4MaJ1A+f7Ai+SnVhkcWtaHk6flw0SYN7A+vQ+MTu0+gSovQWs4Pg4NtcNUcIztYQ9YHsoxHP+GGQplI7Q==", "dev": true, "requires": { "compare-func": "^1.3.1", @@ -566,7 +634,7 @@ "json-stringify-safe": "^5.0.1", "lodash": "^4.2.1", "meow": "^4.0.0", - "semver": "^6.0.0", + "semver": "^5.5.0", "split": "^1.0.0", "through2": "^3.0.0" } @@ -582,13 +650,13 @@ } }, "conventional-commits-parser": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.3.tgz", - "integrity": "sha512-KaA/2EeUkO4bKjinNfGUyqPTX/6w9JGshuQRik4r/wJz7rUw3+D3fDG6sZSEqJvKILzKXFQuFkpPLclcsAuZcg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.2.tgz", + "integrity": "sha512-y5eqgaKR0F6xsBNVSQ/5cI5qIF3MojddSUi1vKIggRkqUTbkqFKH9P5YX/AT1BVZp9DtSzBTIkvjyVLotLsVog==", "dev": true, "requires": { "JSONStream": "^1.0.4", - "is-text-path": "^2.0.0", + "is-text-path": "^1.0.0", "lodash": "^4.2.1", "meow": "^4.0.0", "split2": "^2.0.0", @@ -688,6 +756,14 @@ } } }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -696,6 +772,17 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "defer-to-connect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.2.tgz", + "integrity": "sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -751,6 +838,11 @@ "is-obj": "^1.0.0" } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -761,7 +853,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -1154,36 +1245,6 @@ "through2": "^2.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -1273,6 +1334,34 @@ } } }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "dependencies": { + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + } + } + }, "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", @@ -1332,6 +1421,11 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "http-cache-semantics": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", + "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==" + }, "humanize-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/humanize-string/-/humanize-string-2.1.0.tgz", @@ -1516,6 +1610,11 @@ "esprima": "^4.0.0" } }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -1565,6 +1664,14 @@ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -1659,6 +1766,11 @@ "signal-exit": "^3.0.0" } }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -1736,6 +1848,11 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -2051,6 +2168,34 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nock": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", + "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "deep-equal": "^1.0.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.5", + "mkdirp": "^0.5.0", + "propagate": "^1.0.0", + "qs": "^6.5.1", + "semver": "^5.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "node-environment-flags": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", @@ -2094,6 +2239,11 @@ } } }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -2176,6 +2326,11 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -2223,6 +2378,11 @@ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, + "pako": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.3.tgz", + "integrity": "sha1-X1FbDGci4ZgpIK6ABerLC3ynPM8=" + }, "parse-github-repo-url": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", @@ -2306,6 +2466,15 @@ "pinkie": "^2.0.0" } }, + "plantuml-encoder": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/plantuml-encoder/-/plantuml-encoder-1.2.5.tgz", + "integrity": "sha512-viV7Sz+BJNX/sC3iyebh2VfLyAZKuu3+JuBs2ISms8+zoTGwPqwk3/WEDw/zROmGAJ/xD4sNd8zsBw/YmTo7ng==", + "requires": { + "pako": "1.0.3", + "utf8-bytes": "0.0.1" + } + }, "prepend-file": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/prepend-file/-/prepend-file-1.3.1.tgz", @@ -2333,6 +2502,11 @@ } } }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", @@ -2344,6 +2518,12 @@ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -2354,7 +2534,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -2371,6 +2550,12 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, "quick-lru": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", @@ -2462,6 +2647,14 @@ "path-parse": "^1.0.6" } }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -2583,36 +2776,6 @@ "through2": "^2.0.2" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -2642,9 +2805,9 @@ } }, "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { "safe-buffer": "~5.1.0" @@ -2731,6 +2894,11 @@ "os-tmpdir": "~1.0.1" } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, "toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", @@ -2757,13 +2925,6 @@ "glob": "~7.1.4", "json-stable-stringify": "^1.0.1", "typescript": "~3.4.5" - }, - "dependencies": { - "typescript": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz", - "integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==" - } } }, "ts-node": { @@ -2926,6 +3087,19 @@ "punycode": "^2.1.0" } }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, + "utf8-bytes": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/utf8-bytes/-/utf8-bytes-0.0.1.tgz", + "integrity": "sha1-EWsCVEjJtQAIHN+/H01sbDfYg30=" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index ca522da3..663793f5 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,10 @@ "check-configuration": "openstapps-configuration", "compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'", "documentation": "typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src", + "plantuml-build": "docker build -t openstapps/plantuml-server .", + "plantuml-start": "docker run --name plantuml-server -d -p 8080:8080 openstapps/plantuml-server", + "plantuml-restart": "docker restart plantuml-server", + "plantuml-stop": "docker stop plantuml-server", "postversion": "npm run changelog", "prepublishOnly": "npm ci && npm run build", "preversion": "npm run prepublishOnly", @@ -44,6 +48,7 @@ "@krlwlfrt/async-pool": "0.1.0", "@openstapps/logger": "0.3.1", "@types/glob": "7.1.1", + "@types/got": "9.4.4", "@types/mustache": "0.8.32", "@types/node": "10.14.8", "ajv": "6.10.0", @@ -51,9 +56,11 @@ "commander": "2.20.0", "del": "4.1.1", "glob": "7.1.4", + "got": "9.6.0", "humanize-string": "2.1.0", "jsonschema": "1.2.4", "mustache": "3.0.1", + "plantuml-encoder": "1.2.5", "toposort": "2.0.2", "ts-json-schema-generator": "0.42.0", "ts-node": "8.2.0", @@ -64,9 +71,11 @@ "@types/chai": "4.1.7", "@types/mocha": "5.2.7", "@types/rimraf": "2.0.2", + "@types/nock": "10.0.2", "conventional-changelog-cli": "2.0.21", "mocha": "6.1.4", "mocha-typescript": "1.1.17", + "nock": "10.0.6", "prepend-file-cli": "1.0.6", "rimraf": "2.6.3", "tslint": "5.17.0", diff --git a/src/cli.ts b/src/cli.ts index c36cb73c..1af962d2 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,10 +16,22 @@ import {Logger} from '@openstapps/logger'; import * as commander from 'commander'; import {existsSync, readFileSync, writeFileSync} from 'fs'; import {join, resolve} from 'path'; -import {getProjectReflection, mkdirPromisified, readFilePromisified} from './common'; +import { + getProjectReflection, + mkdirPromisified, + readFilePromisified, + toArray, +} from './common'; import {pack} from './pack'; -import {gatherRouteInformation, generateDocumentationForRoute, getNodeMetaInformationMap} from './routes'; +import { + gatherRouteInformation, + generateDocumentationForRoute, + getNodeMetaInformationMap, +} from './routes'; import {Converter, getValidatableTypesFromReflection} from './schema'; +import {createDiagram, createDiagramFromString} from './uml/createDiagram'; +import {readDefinitions} from './uml/readDefinitions'; +import {UMLConfig} from './uml/umlConfig'; import {validateFiles, writeReport} from './validate'; // handle unhandled promise rejections @@ -53,7 +65,10 @@ commander // generate documentation for all routes routes.forEach((routeWithMetaInformation) => { - output += generateDocumentationForRoute(routeWithMetaInformation, getNodeMetaInformationMap(projectReflection)); + output += generateDocumentationForRoute( + routeWithMetaInformation, + getNodeMetaInformationMap(projectReflection), + ); }); // write documentation to file @@ -76,7 +91,9 @@ commander const projectReflection = getProjectReflection(srcPath); // get validatable types - const validatableTypes = getValidatableTypesFromReflection(projectReflection); + const validatableTypes = getValidatableTypesFromReflection( + projectReflection, + ); Logger.info(`Found ${validatableTypes.length} type(s) to generate schemas for.`); @@ -152,14 +169,85 @@ commander } }); +commander.command('pack').action(async () => { + await pack(); +}); + commander - .command('pack') - .action(async () => { - await pack(); + .command('plantuml ') + .option( + '--definitions ', + 'Shows these specific definitions (class or enum)', + toArray, + ) + .option('--showAssociations', 'Shows associations of classes') + .option( + '--showInheritance', + 'Shows extensions and implementations of classes', + ) + .option('--showEnumValues', 'Show enum values') + .option('--showProperties', 'Show attributes') + .option( + '--showInheritedProperties', + 'Shows inherited attributes, needs --showProperties', + ) + .option( + '--showOptionalProperties', + 'Shows optional attributes and relations, needs --showProperties', + ) + .option( + '--excludeExternals', + 'Exclude external definitions', + ) + .action(async (relativeSrcPath, plantumlserver, options) => { + const plantUmlConfig: UMLConfig = { + definitions: + typeof options.definitions !== 'undefined' ? options.definitions : [], + showAssociations: + typeof options.showAssociations !== 'undefined' + ? options.showAssociations + : false, + showEnumValues: + typeof options.showEnumValues !== 'undefined' + ? options.showEnumValues + : false, + showInheritance: + typeof options.showInheritance !== 'undefined' + ? options.showInheritance + : false, + showInheritedProperties: + typeof options.showInheritedProperties !== 'undefined' + ? options.showInheritedProperties + : false, + showOptionalProperties: + typeof options.showOptionalProperties !== 'undefined' + ? options.showOptionalProperties + : false, + showProperties: + typeof options.showProperties !== 'undefined' + ? options.showEnumValues + : false, + }; + + Logger.log(`PlantUML options: ${JSON.stringify(plantUmlConfig)}`); + + const srcPath = resolve(relativeSrcPath); + + const projectReflection = getProjectReflection(srcPath, !options.excludeExternals ? false : true); + + const definitions = readDefinitions(projectReflection); + + await createDiagram(definitions, plantUmlConfig, plantumlserver); }); commander - .parse(process.argv); + .command('plantuml-file ') + .action(async (file: string, plantumlserver: string) => { + const fileContent = readFileSync(resolve(file)).toString(); + await createDiagramFromString(fileContent, plantumlserver); + }); + +commander.parse(process.argv); if (commander.args.length < 1) { commander.outputHelp(); diff --git a/src/common.ts b/src/common.ts index a713e0f1..ca2d954b 100644 --- a/src/common.ts +++ b/src/common.ts @@ -21,6 +21,7 @@ import {join, sep} from 'path'; import {Definition} from 'ts-json-schema-generator'; import {Application, ProjectReflection} from 'typedoc'; import {promisify} from 'util'; +import {LightweightType} from './uml/model/LightweightType'; export const globPromisified = promisify(glob); export const mkdirPromisified = promisify(mkdir); @@ -142,14 +143,14 @@ export interface ExpectableValidationErrors { * * @param srcPath Path to get reflection from */ -export function getProjectReflection(srcPath: PathLike): ProjectReflection { +export function getProjectReflection(srcPath: PathLike, excludeExternals: boolean = true): ProjectReflection { Logger.info(`Generating project reflection for ${srcPath.toString()}.`); const tsconfigPath = getTsconfigPath(srcPath.toString()); // initialize new Typedoc application const app = new Application({ - excludeExternals: true, + excludeExternals: excludeExternals, includeDeclarations: true, module: 'commonjs', tsconfig: join(tsconfigPath, 'tsconfig.json'), @@ -178,7 +179,9 @@ export function getProjectReflection(srcPath: PathLike): ProjectReflection { * * @param schema Schema to check */ -export function isSchemaWithDefinitions(schema: JSONSchema): schema is SchemaWithDefinitions { +export function isSchemaWithDefinitions( + schema: JSONSchema, +): schema is SchemaWithDefinitions { return typeof schema.definitions !== 'undefined'; } @@ -212,7 +215,9 @@ export function getTsconfigPath(startPath: string): string { // repeat until a tsconfig.json is found while (!existsSync(join(tsconfigPath, 'tsconfig.json'))) { if (tsconfigPath === root) { - throw new Error(`Reached file system root ${root} while searching for 'tsconfig.json' in ${startPath}!`); + throw new Error( + `Reached file system root ${root} while searching for 'tsconfig.json' in ${startPath}!`, + ); } // pop last directory @@ -225,3 +230,51 @@ export function getTsconfigPath(startPath: string): string { return tsconfigPath; } + +/** + * Converts a comma seperated string into a string array + * + * @param val Comma seperated string + */ +export function toArray(val: string): string[] { + return val.split(','); +} + +/** + * Creates the full name of a lightweight type recursivly + * + * @param type Type to get the full name of + */ +export function getFullTypeName(type: LightweightType): string { + // init name + let fullName: string = type.name; + if (type.isTypeParameter) { + // type parameters are a sink + return fullName; + } + if (type.isLiteral) { + // literals are a sink + return "'" + fullName + "'"; + } + if (type.isUnion && type.specificationTypes.length > 0) { + const tempNames: string[] = []; + for (const easyType of type.specificationTypes) { + tempNames.push(getFullTypeName(easyType)); + } + // since unions can't be applied to other types, it is a sink. + return tempNames.join(' | '); + } + // check if type is generic and has a type attached + if (type.isTyped && type.genericsTypes.length > 0) { + const tempNames: string[] = []; + for (const easyType of type.genericsTypes) { + tempNames.push(getFullTypeName(easyType)); + } + fullName += '<' + tempNames.join(', ') + '>'; + } + // check if type is array + if (type.isArray) { + fullName += '[]'; + } + return fullName; +} diff --git a/src/uml/createDiagram.ts b/src/uml/createDiagram.ts new file mode 100644 index 00000000..485b7250 --- /dev/null +++ b/src/uml/createDiagram.ts @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Logger} from '@openstapps/logger'; +import {createWriteStream} from 'fs'; +import * as request from 'got'; +import {getFullTypeName} from '../common'; +import {LightweightClassDefinition} from './model/LightweightClassDefinition'; +import {LightweightDefinition} from './model/LightweightDefinition'; +import {LightweightEnumDefinition} from './model/LightweightEnumDefinition'; +import {LightweightProperty} from './model/LightweightProperty'; +import {LightweightType} from './model/LightweightType'; +import {UMLConfig} from './umlConfig'; + +/** + * Converts the lightweight class/enum definitions according to the configuration, + * to valid PlantUML Code, which will then be encoded, converted by the plantuml server + * and saved as a .svg file in directory, in which this method was called + * + * @param definitions all type definitons of the project + * @param config contains information on how the PlantUML should be generated + * @param plantUmlBaseURL Hostname of the PlantUML-Server + */ +export async function createDiagram( + definitions: LightweightDefinition[], + config: UMLConfig, + plantUmlBaseURL: string, +): Promise { + + // when non definitions were specified use all + if (config.definitions.length === 0) { + config.definitions = []; + definitions.forEach((definition) => { + config.definitions.push(definition.name); + }); + } + + // when providing definitions and either showing associations or inheritance the + // inherited definitions will be added automatically + if (config.showInheritance) { + const inheritedDefinitions = gatherTypeAssociations( + definitions, + config.definitions, + ); + + config.definitions = config.definitions.concat(inheritedDefinitions); + } + + let modelPlantUMLCode: string = ''; + // creates a UML definition for every specified definition name + // however if no definitions were provided all definitions will be transformed + for (const definition of definitions) { + if ( + config.definitions.length > 0 && + !config.definitions.includes(definition.name) + ) { + // current definition not specified + continue; + } + // either the definitions are empty or the definition was specified, proceed + + let definitionPlantUMLCode: string = ''; + if (definition instanceof LightweightClassDefinition) { + definitionPlantUMLCode = createPlantUMLCodeForClass(config, definition); + } else if (definition instanceof LightweightEnumDefinition) { + definitionPlantUMLCode = createPlantUMLCodeForEnum(config, definition); + } else { + continue; + } + modelPlantUMLCode += definitionPlantUMLCode; + } + + return await createDiagramFromString(modelPlantUMLCode, plantUmlBaseURL); +} + +/** + * This will encode the plantuml code and post the code to the plantuml server + * The server will then parse the code and create a corresponding diagram + * + * @param modelPlantUMLCode + */ +export async function createDiagramFromString(modelPlantUMLCode: string, plantUmlBaseURL: string) { + const plantumlEncoder = require('plantuml-encoder'); + const plantUMLCode = plantumlEncoder.encode(modelPlantUMLCode); + const url = `${plantUmlBaseURL}/svg/${plantUMLCode}`; + let response; + try { + response = await request(url); + if (response.statusCode !== 200) { + Logger.error(`Plantuml Server responded with an error.\n${response.statusMessage}`); + throw new Error('Response not okay'); + } + } catch (e) { + Logger.log(`Please try using the public plantuml server:\nhttp://www.plantuml.com/plantuml/svg/${plantUMLCode}`); + throw e; + } + const fileName: string = `Diagram-${new Date().toISOString()}.svg`; + try { + createWriteStream(fileName).write(response.body); + Logger.log(`Writen data to file: ${fileName}`); + } catch (e) { + throw new Error('Could not write file. Are you missing permissions?'); + } + return fileName; +} + +/** + * Recursivly iterates over all types, to find implemented generic types and parents + * + * @param definitions all type definitons of the project + * @param abstractionNames currently known string values of inherited classes + */ +function gatherTypeAssociations( + definitions: LightweightDefinition[], + abstractionNames: string[], +): string[] { + let abstractions: string[] = []; + for (const name of abstractionNames) { + const declaration = definitions.find( + (definition) => definition.name === name, + ); + if (declaration instanceof LightweightClassDefinition) { + const currentAbstractions: string[] = declaration.extendedDefinitions.concat( + declaration.implementedDefinitions, + ); + + abstractions = abstractions.concat(currentAbstractions); + abstractions = abstractions.concat( + gatherTypeAssociations(definitions, currentAbstractions), + ); + } + } + return abstractions; +} + +/** + * Collects all reference information of this type. + * + * Reference information is everything that is indirectly referencing a type or class by name. + * + * @param type Type with references to other types + */ +function getReferenceTypes(type: LightweightType): string[] { + const types: string[] = []; + if (type.isReference) { + types.push(type.name); + } + if (type.isTyped && type.genericsTypes.length > 0) { + for (const specificType of type.genericsTypes) { + for (const value of getReferenceTypes(specificType)) { + types.push(value); + } + } + } + if ( + (type.isUnion && type.specificationTypes.length > 0) || + (type.isArray && type.specificationTypes.length > 0) + ) { + for (const specificType of type.specificationTypes) { + for (const value of getReferenceTypes(specificType)) { + types.push(value); + } + } + } + return types; +} + +/** + * Creates Plant UML code according to the config for the provided class + * + * @param config Configuration for how the UML should be tweaked + * @param readerClass Class or interface representation + */ +function createPlantUMLCodeForClass( + config: UMLConfig, + readerClass: LightweightClassDefinition, +): string { + // create the definition header, what type the definition is, it's name and it's inheritance + let model: string = `${readerClass.type} ${readerClass.name}`; + + if (readerClass.typeParameters.length > 0) { + model += `<${readerClass.typeParameters.join(', ')}>`; + } + + if (config.showInheritance && readerClass.extendedDefinitions.length > 0) { + // PlantUML will automatically create links, when using extends + model += ` extends ${readerClass.extendedDefinitions.join(', ')}`; + } + if (config.showInheritance && readerClass.implementedDefinitions.length > 0) { + // PlantUML will automatically create links, when using implenents + model += ` implements ${readerClass.implementedDefinitions.join(', ')}`; + } + model += '{'; + + // add the properties to the definition body + if (config.showProperties) { + for (const property of readerClass.properties) { + if (property.optional && !config.showOptionalProperties) { + // don't show optional attributes + continue; + } + if (property.inherited && !config.showInheritedProperties) { + // don't show inherited properties + continue; + } + model += `\n\t${createPropertyLine(property)}`; + } + } + + // close the definition body + model += '\n}\n'; + + // add associations from properties with references + for (const property of readerClass.properties) { + const types: string[] = getReferenceTypes(property.type); + for (const type of types) { + if ( config.showAssociations) { + if (property.inherited && !config.showInheritedProperties) { + continue; + } + model += `${readerClass.name} -up-> ${type} : ${property.name} >\n`; + } + } + } + + return model; +} + +/** + * Creates PlantUML code according to the config for the provided enum/-like definition + * + * @param config Configuration for how the UML should be tweaked + * @param readerEnum Enum/-like representation + */ +function createPlantUMLCodeForEnum( + config: UMLConfig, + readerEnum: LightweightEnumDefinition, +): string { + // create enum header + let model: string = `enum ${readerEnum.name} {`; + // add values + if (config.showEnumValues) { + for (const value of readerEnum.values) { + model += `\n\t${value.toString()}`; + } + } + model += '\n}\n'; + + return model; +} + +/** + * Creates a property PlantUML Line + */ +function createPropertyLine(property: LightweightProperty): string { + return ( + (property.inherited ? '/ ' : '') + + (property.optional ? '?' : '') + + property.name + + ' : ' + + getFullTypeName(property.type) + ); +} diff --git a/src/uml/model/LightweightClassDefinition.ts b/src/uml/model/LightweightClassDefinition.ts new file mode 100644 index 00000000..59662fdd --- /dev/null +++ b/src/uml/model/LightweightClassDefinition.ts @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {LightweightDefinition} from './LightweightDefinition'; +import {LightweightProperty} from './LightweightProperty'; +/** + * Represents a class definition + */ +export class LightweightClassDefinition extends LightweightDefinition { + /** + * String values of the extended definitions + */ + public extendedDefinitions: string[]; + + /** + * String values of the implemented definitions + */ + public implementedDefinitions: string[]; + + /** + * Properties of the definition + */ + public properties: LightweightProperty[]; + + /** + * The definition type + * e.g. `interface`/[`abstract`] `class` + */ + public type: string; + + /** + * Generic type parameters of this class + */ + public typeParameters: string[]; + + constructor(name: string, type: string) { + super(name); + this.type = type; + this.properties = []; + this.extendedDefinitions = []; + this.implementedDefinitions = []; + this.typeParameters = []; + } +} diff --git a/src/uml/model/LightweightDefinition.ts b/src/uml/model/LightweightDefinition.ts new file mode 100644 index 00000000..b4318cc1 --- /dev/null +++ b/src/uml/model/LightweightDefinition.ts @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +/** + * Represents any definition without specifics + */ +export abstract class LightweightDefinition { + /** + * Name of the definiton + */ + public name: string; + + constructor(name: string) { + this.name = name; + } +} diff --git a/src/uml/model/LightweightEnumDefinition.ts b/src/uml/model/LightweightEnumDefinition.ts new file mode 100644 index 00000000..61592bf2 --- /dev/null +++ b/src/uml/model/LightweightEnumDefinition.ts @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {LightweightDefinition} from './LightweightDefinition'; +/** + * Represents an enum definition + */ +export class LightweightEnumDefinition extends LightweightDefinition { + /** + * Enumeration or union values + */ + public values: string[]; + + constructor(name: string) { + super(name); + this.values = []; + } +} diff --git a/src/uml/model/LightweightProperty.ts b/src/uml/model/LightweightProperty.ts new file mode 100644 index 00000000..c309623f --- /dev/null +++ b/src/uml/model/LightweightProperty.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {LightweightType} from './LightweightType'; + +/** + * Represents a property definition + */ +export class LightweightProperty { + /** + * Is the property inherited from another definition + */ + public inherited: boolean; + + /** + * Name of the property + */ + public name: string; + + /** + * Is the property marked as optional + */ + public optional: boolean; + + /** + * Type of the property + */ + public type: LightweightType; + + /** + * Constructor for LightweightProperty + * + * @param name Name of the property + * @param type Type of the property + * @param optional Is the property optional + */ + constructor(name: string, type: LightweightType, optional: boolean = true) { + this.name = name; + this.optional = optional; + this.inherited = false; + this.type = type; + } +} diff --git a/src/uml/model/LightweightType.ts b/src/uml/model/LightweightType.ts new file mode 100644 index 00000000..98dae5e1 --- /dev/null +++ b/src/uml/model/LightweightType.ts @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +/** + * Describes an easy to use type definition. + */ +export class LightweightType { + /** + * Contains all types inside of <> brackets + */ + genericsTypes: LightweightType[]; + + /** + * Does the type have generic-parameters + */ + hasTypeInformation: boolean = false; + + /** + * Does the type represent an array type + */ + isArray: boolean = false; + + /** + * Does the type represent a literal type + */ + isLiteral: boolean = false; + + /** + * Does the type represent a primitive type + */ + isPrimitive: boolean = false; + + /** + * Does the type contain a reference to + */ + isReference: boolean = false; + + /** + * Is the type a reflection and not avaiblabe at compile time + */ + isReflection: boolean = false; + + /** + * Does the type have type parameters + */ + isTyped: boolean = false; + + /** + * Is the type a typed parameter + */ + isTypeParameter: boolean = false; + + /** + * Is the type a union type + */ + isUnion: boolean = false; + + /** + * Name of the type + */ + name: string; + + /** + * Type specifications, if the type is combined by either an array, union or a typeOperator + */ + specificationTypes: LightweightType[]; + + constructor() { + this.specificationTypes = []; + this.genericsTypes = []; + this.name = ''; + } +} diff --git a/src/uml/readDefinitions.ts b/src/uml/readDefinitions.ts new file mode 100644 index 00000000..c847d15f --- /dev/null +++ b/src/uml/readDefinitions.ts @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Logger} from '@openstapps/logger'; +import { + ArrayType, + DeclarationReflection, + IntrinsicType, + ProjectReflection, + ReferenceType, + ReflectionKind, + ReflectionType, + StringLiteralType, + Type, + TypeOperatorType, + TypeParameterType, + UnionType, +} from 'typedoc/dist/lib/models'; +import {getFullTypeName} from '../common'; +import {LightweightClassDefinition} from './model/LightweightClassDefinition'; +import {LightweightDefinition} from './model/LightweightDefinition'; +import {LightweightEnumDefinition} from './model/LightweightEnumDefinition'; +import {LightweightProperty} from './model/LightweightProperty'; +import {LightweightType} from './model/LightweightType'; + +/** + * Reads the reflection model from typedoc and converts it into a flatter, easier to handle model + * + * @param srcPath Path to source file directory + */ +export function readDefinitions(projectReflection: ProjectReflection): LightweightDefinition[] { + + const definitions: LightweightDefinition[] = []; + + // define known types and categorize them + const enumLike: string[] = ['Type alias', 'Enumeration']; + const classLike: string[] = ['Class', 'Interface']; + const unused: string[] = ['Function', 'Object literal', 'Variable']; + + // children need to be not undefined, if they are return empty + if (typeof projectReflection.children === 'undefined') { + return []; + } + + for (const module of projectReflection.children) { + if (Array.isArray(module.children) && module.children.length > 0) { + // iterate over class and enum declarations + for (const type of module.children) { + // only if kindString is set + if (typeof type.kindString !== 'undefined') { + // check if declaration is enum + if (classLike.includes(type.kindString)) { + definitions.push(readAsClassDefinition(type)); + } else if (enumLike.includes(type.kindString)) { + definitions.push(readAsEnumDefinition(type)); + } else if (unused.includes(type.kindString)) { + Logger.info(`Unconverted ${type.kindString} : ${type.name}`); + } else { + Logger.log( + `Uncaught declaration type (${type.kindString}) : ${type.name}`, + ); + } + } + } + } + } + + return definitions; +} + +/** + * Transforms the declaration into a `LightweightClassDefinition` + * + * @param declaration declaration + */ +export function readAsEnumDefinition( + declaration: DeclarationReflection, +): LightweightEnumDefinition { + // init enum definition + const enumDefinition: LightweightEnumDefinition = new LightweightEnumDefinition( + declaration.name, + ); + + // get enum values according to type + if (declaration.kindString === 'Enumeration' && declaration.children) { + // standard enumeration + for (const child of declaration.children) { + if (child.kindString === 'Enumeration member') { + let value = child.name; + if (typeof child.defaultValue !== 'undefined') { + value = `${value} = ${child.defaultValue}`; + } + enumDefinition.values.push(value); + } else { + Logger.log( + "Every enumeration member should be an 'EnumerationMemberType'", + ); + } + } + } else if ( + declaration.kindString === 'Type alias' && + typeof declaration.type !== 'undefined' + ) { + // enum like declaration + try { + const a = readTypeInformation(declaration.type); + enumDefinition.values = enumDefinition.values.concat( + getTypeInformation(a), + ); + } catch (e) { + Logger.warn( + `Could not read the light type for ${declaration.name}. ${e}`, + ); + } + } + + return enumDefinition; +} + +/** + * Used for enumrations to get the type value + */ +function getTypeInformation(type: LightweightType): string[] { + const values: string[] = []; + if (!type.hasTypeInformation) { + for (const specificType of type.specificationTypes) { + for (const value of getTypeInformation(specificType)) { + values.push(value); + } + } + } else { + values.push(type.name); + } + return values; +} + +/** + * Transforms the declaration into a `LightweightClassDefinition` + * + * @param declaration declaration + */ +export function readAsClassDefinition( + declaration: DeclarationReflection, +): LightweightClassDefinition { + let type = declaration.kindString ? declaration.kindString.toLowerCase() : ''; + type = (declaration.flags.isAbstract ? 'abstract ' : '') + type; + + const classDefinition: LightweightClassDefinition = new LightweightClassDefinition( + declaration.name, + type, + ); + + // get generic types + if (typeof declaration.typeParameters !== 'undefined') { + const typeParameters: string[] = []; + declaration.typeParameters.forEach((typeParameter) => + typeParameters.push(typeParameter.name), + ); + classDefinition.typeParameters = typeParameters; + } + + // extracts extended types of the declaration + if (typeof declaration.extendedTypes !== 'undefined') { + for (const extType of declaration.extendedTypes) { + classDefinition.extendedDefinitions.push((extType as ReferenceType).name); + } + } + + // extracts implemented types of the declaration + // HINT: typedoc automatically adds inherited interfaces to the declaration directly + if (typeof declaration.implementedTypes !== 'undefined') { + for (const implType of declaration.implementedTypes) { + classDefinition.implementedDefinitions.push( + (implType as ReferenceType).name, + ); + } + } + + if (typeof declaration.children !== 'undefined') { + for (const child of declaration.getChildrenByKind( + ReflectionKind.Property, + )) { + try { + if (typeof child.type === 'undefined') { + throw new Error(); + } + + const myType: LightweightType = readTypeInformation(child.type); + const property = new LightweightProperty(child.name, myType); + + const flags = child.flags; + if (flags.isOptional !== undefined) { + property.optional = flags.isOptional as boolean; + property.inherited = !( + child.inheritedFrom === undefined || child.inheritedFrom === null + ); + } + classDefinition.properties.push(property); + } catch (e) { + Logger.warn(e); + } + } + } + + return classDefinition; +} + +/** + * The structure of reflection type has a huge overhead + * This method and all submethods will convert these types in easier to process Types + * + * @param declarationType Type to be converted + */ +function readTypeInformation(declarationType: Type): LightweightType { + if (declarationType instanceof ReflectionType) { + return readAsReflectionType(declarationType); + } else if (declarationType instanceof TypeOperatorType) { + return readAsTypeOperatorType(declarationType); + } else if (declarationType instanceof TypeParameterType) { + return readAsTypeParameterType(declarationType); + } else if (declarationType instanceof IntrinsicType) { + return readAsIntrinsicType(declarationType); + } else if (declarationType instanceof StringLiteralType) { + return readAsStringLiteralType(declarationType); + } else if (declarationType instanceof ReferenceType) { + return readAsReferenceType(declarationType); + } else if (declarationType instanceof ArrayType) { + return readAsArrayType(declarationType); + } else if (declarationType instanceof UnionType) { + return readAsUnionType(declarationType); + } else { + throw new Error(`Could not read type ${declarationType.type}`); + } +} + +/** + * Conversion method for IntrinsicType's + * + * e.g. remainingAttendeeCapacity?: number; + * + * @param type Type to be converted + */ +function readAsIntrinsicType(type: IntrinsicType): LightweightType { + const easyType: LightweightType = new LightweightType(); + easyType.name = type.name; + easyType.isPrimitive = true; + easyType.hasTypeInformation = true; + return easyType; +} + +/** + * Conversion method for StringLiteralType's + * + * e.g. inputType: 'multipleChoice'; + * + * @param type Type to be converted + */ +function readAsStringLiteralType(type: StringLiteralType): LightweightType { + const returnType: LightweightType = new LightweightType(); + returnType.name = type.value; + returnType.isLiteral = true; + returnType.hasTypeInformation = true; + return returnType; +} + +/** + * Conversion method for ReferenceType's + * + * Everything that is a user or API designed definition and not a primitive type or core-language feature. + * + * e.g. publishers?: Array; + * + * Array, SCPersonWithoutReferences and SCOrganizationWithoutReferences will be recognized as reference types! + * + * @param type Type to be converted + */ +function readAsReferenceType(type: ReferenceType): LightweightType { + const returnType: LightweightType = new LightweightType(); + returnType.name = type.name; + + if (type.typeArguments !== undefined && type.typeArguments.length > 0) { + const typeArguments: LightweightType[] = []; + + for (const value of type.typeArguments) { + typeArguments.push(readTypeInformation(value)); + } + + returnType.isTyped = true; + returnType.genericsTypes = typeArguments; + } + + if (type.reflection !== undefined && type.reflection !== null) { + const tempTypeReflection = type.reflection as DeclarationReflection; + // interfaces and classes in a type are a sink, since their declaration are defined elsewhere + if ( + tempTypeReflection.kindString && + ['Interface', 'Class', 'Enumeration', 'Type alias'].indexOf( + tempTypeReflection.kindString, + ) > -1 + ) { + returnType.isReference = true; + } + } + + returnType.hasTypeInformation = true; + + return returnType; +} + +/** + * Conversion method for ArrayType's + * + * The actual type of the array is stored in the first element of specificationTypes. + * + * e.g. articleBody?: string[]; + * + * @param type Type to be converted + */ +function readAsArrayType(type: ArrayType): LightweightType { + const returnType: LightweightType = new LightweightType(); + const typeOfArray: LightweightType = readTypeInformation(type.elementType); + returnType.name = getFullTypeName(typeOfArray); + returnType.specificationTypes = [typeOfArray]; + returnType.isArray = true; + return returnType; +} + +/** + * Conversion method for UnionType's + * + * The Union-LightType store the single types of the union inside a + * separate LightType inside specificationTypes. + * + * e.g. maintainer?: SCPerson | SCOrganization; + * + * @param type Type to be converted + */ +function readAsUnionType(type: UnionType): LightweightType { + const returnType: LightweightType = new LightweightType(); + const typesOfUnion: LightweightType[] = []; + for (const value of type.types) { + typesOfUnion.push(readTypeInformation(value)); + } + returnType.specificationTypes = typesOfUnion; + returnType.name = getFullTypeName(returnType); + returnType.isUnion = true; + return returnType; +} + +/** + * Conversion method for ReflectionType's + * + * The explicit type is not contained in reflection! + * It might be possible to get the structure of type by reading tempType.decoration.children, + * but this structure is currently not supported in the data-model. + * + * e.g. categorySpecificValues?: { [s: string]: U }; + * + * @param type Type to be converted + */ +function readAsReflectionType(type: ReflectionType): LightweightType { + const returnType: LightweightType = new LightweightType(); + if (type.declaration.sources) { + const src = type.declaration.sources[0]; + Logger.warn( + `${src.line} : ${src.fileName}: Reflection Type not recognized. Refactoring to explicit class is advised.`, + ); + } + returnType.name = 'object'; + returnType.isReflection = true; + return returnType; +} + +/** + * Conversion method for TypeOperatorType's + * + * This type is similar to reflection, that the actual type can only be evaluated at runtime. + * + * e.g. universityRole: keyof SCSportCoursePriceGroup; + * + * @param type Type to be converted + */ +function readAsTypeOperatorType(type: TypeOperatorType): LightweightType { + const returnType: LightweightType = new LightweightType(); + const typeOf: LightweightType = readTypeInformation(type.target); + returnType.name = `keyof ${getFullTypeName(typeOf)}`; + returnType.specificationTypes = [typeOf]; + // can't be traced deeper! so might as well be a primitive + returnType.isPrimitive = true; + returnType.hasTypeInformation = true; + return returnType; +} + +/** + * Conversion method for TypeParameterType's + * + * Should only be called in generic classes/interfaces, when a property is + * referencing the generic-type. + * + * e.g. prices?: T; + * + * Does not match on Arrays of the generic type. Those will be matched with ArrayType. + * + * @param type Needs to be a TypeParameterType + */ +function readAsTypeParameterType(type: TypeParameterType): LightweightType { + const returnType: LightweightType = new LightweightType(); + returnType.name = type.name; + returnType.isTypeParameter = true; + returnType.hasTypeInformation = true; + return returnType; +} diff --git a/src/uml/umlConfig.ts b/src/uml/umlConfig.ts new file mode 100644 index 00000000..a2a65432 --- /dev/null +++ b/src/uml/umlConfig.ts @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +/** + * Holds configuration information of how the UML code should be build + */ +export interface UMLConfig { + /** + * Defines which definitions are shown + */ + definitions: string[]; + + /** + * Should the associations between definitions be shown + */ + showAssociations: boolean; + + /** + * Should enum/-like values be shown + */ + showEnumValues: boolean; + + /** + * Should the inheritance be shown + */ + showInheritance: boolean; + + /** + * Should the inherited properties be shown + */ + showInheritedProperties: boolean; + + /** + * Should optional properties be shown + */ + showOptionalProperties: boolean; + + /** + * Should properties be shown + */ + showProperties: boolean; +} diff --git a/test/Common.spec.ts b/test/Common.spec.ts index 588fb071..20d67e72 100644 --- a/test/Common.spec.ts +++ b/test/Common.spec.ts @@ -29,4 +29,5 @@ export class CommonSpec { async getTsconfigPath() { expect(getTsconfigPath(__dirname)).to.be.equal(cwd()); } + } diff --git a/test/CreateDiagram.spec.ts b/test/CreateDiagram.spec.ts new file mode 100644 index 00000000..15e57ddb --- /dev/null +++ b/test/CreateDiagram.spec.ts @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {expect} from 'chai'; +import {resolve} from 'path'; +import {existsSync, unlinkSync} from 'fs'; +import {slow, suite, test, timeout} from 'mocha-typescript'; +import {getProjectReflection} from '../src/common'; +import {createDiagram, createDiagramFromString} from '../src/uml/createDiagram'; +import {UMLConfig} from './../lib/uml/umlConfig.d'; +import {readDefinitions} from '../src/uml/readDefinitions'; +import {LightweightDefinition} from '../src/uml/model/LightweightDefinition'; +import nock = require('nock'); + +@suite(timeout(15000), slow(5000)) +export class CreateDiagramSpec { + plantUmlConfig: UMLConfig; + definitions: LightweightDefinition[]; + + constructor() { + this.plantUmlConfig = { + definitions: [], + showAssociations: true, + showEnumValues: true, + showInheritance: true, + showInheritedProperties: true, + showOptionalProperties: true, + showProperties: true, + }; + + const projectReflection = getProjectReflection('./test/model', true); + this.definitions = readDefinitions(projectReflection); + } + + @test + async shouldRefuseRequest() { + const testPlantUmlCode: string = 'class Test{\n}'; + try { + await createDiagramFromString(testPlantUmlCode, "http://plantuml:8080"); + } catch (e) { + expect(e.message).to.equal(new Error('getaddrinfo ENOTFOUND plantuml plantuml:8080').message); + } + } + + /** + * This test will only test the functionality of the method + * - Converting the definitions to plantuml code + * - Sending the code to a server + * - Writing the response to a file + * This test will not check the file content + */ + @test + async shouldCreateDiagrams() { + nock('http://plantuml:8080') + .persist() + .get(() => true) + .reply(200, 'This will be the file content') + + let fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080"); + let filePath = resolve(__dirname, '..', fileName); + expect(await existsSync(filePath)).to.equal(true); + await unlinkSync(fileName); + + this.plantUmlConfig.showAssociations = false; + this.plantUmlConfig.showInheritance = false; + + fileName = await createDiagram(this.definitions, this.plantUmlConfig, "http://plantuml:8080"); + filePath = resolve(__dirname, '..', fileName); + expect(await existsSync(filePath)).to.equal(true); + await unlinkSync(fileName); + + nock.cleanAll(); + } +} diff --git a/test/ReadDefinitions.spec.ts b/test/ReadDefinitions.spec.ts new file mode 100644 index 00000000..c6b1ff33 --- /dev/null +++ b/test/ReadDefinitions.spec.ts @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {expect} from 'chai'; +import {slow, suite, test, timeout} from 'mocha-typescript'; +import {getProjectReflection} from '../src/common'; +import {readDefinitions} from '../src/uml/readDefinitions'; +import {generatedModel} from './model/generatedModel'; + +@suite(timeout(10000), slow(5000)) +export class ReadDefinitionsSpec { + @test + async testReadDefinitions() { + const projectReflection = getProjectReflection('./test/model', true); + const definitions = readDefinitions(projectReflection); + expect(definitions).to.be.deep.equal(generatedModel); + } +} diff --git a/test/model/TestClass.ts b/test/model/TestClass.ts new file mode 100644 index 00000000..f1c7239c --- /dev/null +++ b/test/model/TestClass.ts @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {TestFirstUnion} from './TestUnion'; + +export class TestClass { + test2: T; + test4: TestFirstUnion; + + constructor(type: T) { + this.test2 = type; + this.test4 = 'test1'; + } + + /** + * Should not be processed at all + */ + testClassFunction(): boolean { + return true; + } +} + +export class TestSecondClass extends TestClass {} diff --git a/test/model/TestEnum.ts b/test/model/TestEnum.ts new file mode 100644 index 00000000..db9fff42 --- /dev/null +++ b/test/model/TestEnum.ts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +export enum TestFirstEnum { + TEST1, + TEST2, + TEST3, +} + +export enum TestSecondEnum { + TEST1 = 'one', + TEST2 = 'two', + TEST3 = 'three', +} diff --git a/test/model/TestFunction.ts b/test/model/TestFunction.ts new file mode 100644 index 00000000..0508ad94 --- /dev/null +++ b/test/model/TestFunction.ts @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +export function testFunction(): boolean { + return true; +} diff --git a/test/model/TestInterface.ts b/test/model/TestInterface.ts new file mode 100644 index 00000000..650e4565 --- /dev/null +++ b/test/model/TestInterface.ts @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {TestClass, TestSecondClass} from './TestClass'; +import {TestFirstEnum} from './TestEnum'; +import {TestThirdUnion} from './TestUnion'; + +export interface TestInterface { + articleBody: string[]; + categorySpecificValues?: { [s: string]: string }; + inputType: 'multipleChoice'; + maintainer: TestThirdUnion | TestFirstEnum; + remainingAttendeeCapacity?: number; + test1: Array; + test2: TestClass; + test3: 'test1' | 'test2'; + test4: TestSecondClass; + universityRole: keyof TestFirstEnum; +} + +export interface TestSecondInterface { + [k: string]: string; +} diff --git a/test/model/TestUnion.ts b/test/model/TestUnion.ts new file mode 100644 index 00000000..1adfa426 --- /dev/null +++ b/test/model/TestUnion.ts @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018-2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +export type TestFirstUnion = 'test1' | 'test2'; + +export type TestSecondUnion = 'test3'; + +export type TestThirdUnion = TestFirstUnion | TestSecondUnion; + +export type TestFourthUnion = T extends TestFirstUnion + ? TestFirstUnion + : never; diff --git a/test/model/generatedModel.ts b/test/model/generatedModel.ts new file mode 100644 index 00000000..6c4cbbe3 --- /dev/null +++ b/test/model/generatedModel.ts @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2019 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {LightweightClassDefinition} from '../../src/uml/model/LightweightClassDefinition'; +import {LightweightDefinition} from '../../src/uml/model/LightweightDefinition'; +import {LightweightEnumDefinition} from '../../src/uml/model/LightweightEnumDefinition'; + +export const generatedModel: Array< + LightweightDefinition | LightweightClassDefinition | LightweightEnumDefinition +> = [ + { + name: 'TestClass', + type: 'class', + properties: [ + { + name: 'test2', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: true, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'T', + }, + }, + { + name: 'test4', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestFirstUnion', + }, + }, + ], + extendedDefinitions: [], + implementedDefinitions: [], + typeParameters: ['T'], + }, + { + name: 'TestSecondClass', + type: 'class', + properties: [ + { + name: 'test2', + optional: false, + inherited: true, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'string', + }, + }, + { + name: 'test4', + optional: false, + inherited: true, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestFirstUnion', + }, + }, + ], + extendedDefinitions: ['TestClass'], + implementedDefinitions: [], + typeParameters: [], + }, + { + name: 'TestFirstEnum', + values: ['TEST1', 'TEST2', 'TEST3'], + }, + { + name: 'TestSecondEnum', + values: ['TEST1 = "one"', 'TEST2 = "two"', 'TEST3 = "three"'], + }, + { + name: 'TestInterface', + type: 'interface', + properties: [ + { + name: 'articleBody', + optional: false, + inherited: false, + type: { + hasTypeInformation: false, + isArray: true, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'string', + }, + ], + genericsTypes: [], + name: 'string', + }, + }, + { + name: 'categorySpecificValues', + optional: true, + inherited: false, + type: { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: true, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'undefined', + }, + { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: true, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'object', + }, + ], + genericsTypes: [], + name: '', + }, + }, + { + name: 'inputType', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: true, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'multipleChoice', + }, + }, + { + name: 'maintainer', + optional: false, + inherited: false, + type: { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: true, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestThirdUnion', + }, + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestFirstEnum', + }, + ], + genericsTypes: [], + name: '', + }, + }, + { + name: 'remainingAttendeeCapacity', + optional: true, + inherited: false, + type: { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: true, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'undefined', + }, + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'number', + }, + ], + genericsTypes: [], + name: '', + }, + }, + { + name: 'test1', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: true, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [ + { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: true, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestThirdUnion', + }, + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestFirstEnum', + }, + ], + genericsTypes: [], + name: '', + }, + ], + name: 'Array', + }, + }, + { + name: 'test2', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: true, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'string', + }, + ], + name: 'TestClass', + }, + }, + { + name: 'test3', + optional: false, + inherited: false, + type: { + hasTypeInformation: false, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: true, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: true, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'test1', + }, + { + hasTypeInformation: true, + isArray: false, + isLiteral: true, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'test2', + }, + ], + genericsTypes: [], + name: '', + }, + }, + { + name: 'test4', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: true, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestSecondClass', + }, + }, + { + name: 'universityRole', + optional: false, + inherited: false, + type: { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: true, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [ + { + hasTypeInformation: true, + isArray: false, + isLiteral: false, + isPrimitive: false, + isReference: false, + isReflection: false, + isTyped: false, + isTypeParameter: false, + isUnion: false, + specificationTypes: [], + genericsTypes: [], + name: 'TestFirstEnum', + }, + ], + genericsTypes: [], + name: 'keyof TestFirstEnum', + }, + }, + ], + extendedDefinitions: [], + implementedDefinitions: [], + typeParameters: [], + }, + { + name: 'TestSecondInterface', + type: 'interface', + properties: [], + extendedDefinitions: [], + implementedDefinitions: [], + typeParameters: [], + }, + { + name: 'TestFirstUnion', + values: ['test1', 'test2'], + }, + { + name: 'TestFourthUnion', + values: [], + }, + { + name: 'TestSecondUnion', + values: ['test3'], + }, + { + name: 'TestThirdUnion', + values: ['TestFirstUnion', 'TestSecondUnion'], + }, +];