@jmm-devkit/ngx-form-generator 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2020 Verizon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # Changes and Improvements over the Original
2
+
3
+ ## Fixes
4
+
5
+ - Fixed the handling of the `properties` property when generating fields from the OpenAPI definition. Now, if `properties` is `undefined`, an empty array is returned, allowing the rest of the code to execute correctly.
6
+
7
+ ## Improvements
8
+
9
+ - The form constant is now an arrow function. This allows the form to be reused without persisting its state between uses, as previously, being an object, the form state was retained.
10
+
11
+ # Angular Form Generator
12
+
13
+ Generates an Angular ReactiveForm from a Swagger or OpenAPI definition.
14
+
15
+ Validation rules should have a single source of truth. These rules should be exposed to consumers to apply them. By doing this we can be sure that the same rules for UI validation are enforced at the API layer.
16
+
17
+ [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ # install locally in project
23
+ npm install @jmm-devkit/ngx-form-generator --save-dev
24
+
25
+ # install globally
26
+ npm install @jmm-devkit/ngx-form-generator -g
27
+ ```
28
+
29
+ ## CLI Usage
30
+
31
+ ```bash
32
+ ngx-form-generator -f swagger.json -o projects/forms/src/lib/
33
+
34
+ # when installed locally in project run with npx
35
+ npx ngx-form-generator -f swagger.yaml -o projects/forms/src/lib/
36
+ ```
37
+
38
+ | Option | Alias | Comment | Required |
39
+ | ------------ | ---------------- | ------------------------------------------------ | -------- |
40
+ | --version | | Show version number | |
41
+ | --input-spec | -i, --swaggerUrl | Location of the OpenAPI spec as URL or file path | ✓ |
42
+ | --output | -o, --outDir | Where to write the generated files | |
43
+ | --file-name | -f, --outFile | Generated file name | |
44
+ | --help | -h | Show help | |
45
+
46
+ ### Example CLI Usage
47
+
48
+ ```bash
49
+ ngx-form-generator -i https://petstore.swagger.io/v2/swagger.json -o petstore-forms
50
+ ngx-form-generator -i https://petstore.swagger.io/v2/swagger.yaml -o petstore-forms
51
+ npx ngx-form-generator -i swagger.json -o project/form/src/lib
52
+ ```
53
+
54
+ ## Library Usage
55
+
56
+ ```typescript
57
+ import { loadSpec, makeForm, saveFile } from '@verizonconnect/ngx-form-generator';
58
+
59
+ async function main() {
60
+ const spec = await loadSpec('swagger.json');
61
+ const form = makeForm(spec);
62
+ await saveFile(form, 'projects/forms/src/lib/index.ts');
63
+ }
64
+ await main();
65
+ ```
66
+
67
+ ## Examples Generated Form
68
+
69
+ ```typescript
70
+ import { FormGroup, FormControl, Validators } from '@angular/forms';
71
+
72
+ export const addressModelForm = () => new FormGroup({
73
+ firstName: new FormControl(null, [
74
+ Validators.required,
75
+ Validators.pattern(/^[a-zA-Z\'\s]+$/),
76
+ Validators.minLength(1),
77
+ Validators.maxLength(100)
78
+ ]),
79
+ lastName: new FormControl(null, [
80
+ Validators.required,
81
+ Validators.pattern(/^[a-zA-Z\'\s]+$/),
82
+ Validators.minLength(1),
83
+ Validators.maxLength(100)
84
+ ]),
85
+ address: new FormControl(null, [
86
+ Validators.required,
87
+ Validators.pattern(/^[\w\'\s]+$/),
88
+ Validators.minLength(1),
89
+ Validators.maxLength(100)
90
+ ]),
91
+ address2: new FormControl(null, [
92
+ Validators.pattern(/^[\w\'\s]+$/),
93
+ Validators.minLength(1),
94
+ Validators.maxLength(100)
95
+ ]),
96
+ city: new FormControl(null, [
97
+ Validators.required,
98
+ Validators.pattern(/^[\w\'\s]+$/),
99
+ Validators.minLength(1),
100
+ Validators.maxLength(100)
101
+ ]),
102
+ postalCode: new FormControl(null, [
103
+ Validators.required,
104
+ Validators.pattern(/^[\w\s]+$/),
105
+ Validators.minLength(4),
106
+ Validators.maxLength(8)
107
+ ]),
108
+ emailAddress: new FormControl(null, [
109
+ Validators.required,
110
+ Validators.pattern(/^[\w\@\!\#\%\&\'\*\+\-\/\=\?\`\{\|\}\~\.]+$/),
111
+ Validators.email
112
+ ])
113
+ });
114
+ ```
115
+
116
+ ## License
117
+
118
+ [MIT](./LICENSE)
@@ -0,0 +1,47 @@
1
+ #! /usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * @license
5
+ * Licensed under the MIT License, (“the License”); you may not use this
6
+ * file except in compliance with the License.
7
+ *
8
+ * Copyright (c) 2020 Verizon
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const generator_lib_1 = require("./generator-lib");
12
+ const path_1 = require("path");
13
+ const yargs = require('yargs');
14
+ async function main() {
15
+ const argv = yargs
16
+ .option('input-spec', {
17
+ alias: ['i', 'swaggerUrl'],
18
+ description: 'Location of the OpenAPI spec as a URL or file path',
19
+ type: 'string',
20
+ require: true
21
+ })
22
+ .option('output', {
23
+ alias: ['o', 'outDir'],
24
+ description: 'Where to write the generated files',
25
+ type: 'string'
26
+ })
27
+ .option('file-name', {
28
+ alias: ['f', 'outFile'],
29
+ description: 'Generated file name',
30
+ type: 'string'
31
+ })
32
+ .help()
33
+ .wrap(null)
34
+ .usage('Generates Angular ReactiveForms from an OpenAPI v2 or v3 spec.\n\n Usage: $0 -i <spec> -o <path>')
35
+ .example('ngx-form-generator -i https://petstore.swagger.io/v2/swagger.json -o petstore-forms')
36
+ .example('ngx-form-generator -i https://petstore.swagger.io/v2/swagger.yaml -o petstore-forms')
37
+ .example('npx ngx-form-generator -i swagger.json -o project/form/src/lib')
38
+ .alias('help', 'h').argv;
39
+ const spec = await (0, generator_lib_1.loadSpec)(argv['input-spec']);
40
+ const file = (0, generator_lib_1.makeForm)(spec);
41
+ let fileName = argv['file-name'] || (0, generator_lib_1.makeFileName)(spec) || 'forms.ts';
42
+ if (argv.output) {
43
+ fileName = (0, path_1.join)(argv.output, fileName);
44
+ }
45
+ await (0, generator_lib_1.saveFile)(file, fileName);
46
+ }
47
+ main();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @license
3
+ * Licensed under the MIT License, (“the License”); you may not use this
4
+ * file except in compliance with the License.
5
+ *
6
+ * Copyright (c) 2020 Verizon
7
+ */
8
+ import { Rule } from './rules';
9
+ import { OpenAPI } from 'openapi-types';
10
+ export declare function addRule(rule: Rule): void;
11
+ export declare function resetRules(): void;
12
+ export declare function makeFileName(swagger: OpenAPI.Document): string | undefined;
13
+ export declare function makeForm(spec: OpenAPI.Document): string;
14
+ export declare function saveFile(file: string, fileName: string): Promise<void>;
15
+ export declare function loadSpec(fileOrUrlPath: string): Promise<OpenAPI.Document>;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Licensed under the MIT License, (“the License”); you may not use this
5
+ * file except in compliance with the License.
6
+ *
7
+ * Copyright (c) 2020 Verizon
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.loadSpec = exports.saveFile = exports.makeForm = exports.makeFileName = exports.resetRules = exports.addRule = void 0;
14
+ const ts_morph_1 = require("ts-morph");
15
+ const prettier_1 = __importDefault(require("prettier"));
16
+ const camelcase_1 = __importDefault(require("camelcase"));
17
+ const rules_1 = require("./rules");
18
+ const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
19
+ const DEFAULT_RULES = [rules_1.requiredRule, rules_1.patternRule, rules_1.minLengthRule, rules_1.maxLengthRule, rules_1.emailRule, rules_1.minimumRule, rules_1.maximumRule];
20
+ let rules = [...DEFAULT_RULES];
21
+ function addRule(rule) {
22
+ rules.push(rule);
23
+ }
24
+ exports.addRule = addRule;
25
+ function resetRules() {
26
+ rules = [...DEFAULT_RULES];
27
+ }
28
+ exports.resetRules = resetRules;
29
+ function makeFileName(swagger) {
30
+ if (swagger.info && swagger.info.title) {
31
+ return `${(0, camelcase_1.default)(swagger.info.title)}.ts`;
32
+ }
33
+ }
34
+ exports.makeFileName = makeFileName;
35
+ function makeFieldRules(fieldName, definition) {
36
+ return rules
37
+ .map(rule => rule(fieldName, definition))
38
+ .filter(item => item != '')
39
+ .join();
40
+ }
41
+ function makeField(fieldName, definition) {
42
+ if (definition.properties[fieldName]['type'] === 'array') {
43
+ const itemDefinition = definition.properties[fieldName]['items'];
44
+ const minItems = +definition.properties[fieldName]['minItems'] || 1;
45
+ const items = [];
46
+ if (itemDefinition['type'] === 'object') {
47
+ for (let i = 0; i <= minItems; i++) {
48
+ items.push(`new FormGroup({${makeFieldsBody(itemDefinition)}})`);
49
+ }
50
+ }
51
+ else {
52
+ const _dummyProps = {
53
+ properties: {
54
+ dummy: itemDefinition
55
+ }
56
+ };
57
+ const value = 'default' in _dummyProps.properties.dummy ? `'${_dummyProps.properties.dummy.default}'` : null;
58
+ for (let i = 1; i <= minItems; i++) {
59
+ items.push(`new FormControl(${value}, [${makeFieldRules('dummy', _dummyProps)}])`);
60
+ }
61
+ }
62
+ return `"${fieldName}": new FormArray([${items.join(',')}])`;
63
+ }
64
+ else if (definition.properties[fieldName]['type'] === 'object') {
65
+ const constructFormGroup = makeFieldsBody(definition.properties[fieldName]);
66
+ return `"${fieldName}": new FormGroup({${constructFormGroup}})`;
67
+ }
68
+ const value = 'default' in definition.properties[fieldName] ? `'${definition.properties[fieldName].default}'` : null;
69
+ return `"${fieldName}": new FormControl(${value}, [${makeFieldRules(fieldName, definition)}])`;
70
+ }
71
+ function makeFieldsBody(definition) {
72
+ if ('allOf' in definition) {
73
+ const definitionKeys = Object.keys(definition.allOf);
74
+ const allOfFieldsBody = definitionKeys
75
+ .map(key => makeFieldsBody(definition.allOf[key]))
76
+ .reduce((acc, val) => acc.concat(val), []);
77
+ return allOfFieldsBody;
78
+ }
79
+ if (!definition.properties)
80
+ return [];
81
+ const fields = Object.keys(definition.properties);
82
+ const fieldsBody = fields.map(fieldName => makeField(fieldName, definition)).filter(item => item !== '');
83
+ return fieldsBody;
84
+ }
85
+ function makeDefinition(definitionName, definition) {
86
+ const fieldsBody = makeFieldsBody(definition);
87
+ return `
88
+ export const ${(0, camelcase_1.default)(definitionName)}Form = () => new FormGroup({
89
+ ${fieldsBody}
90
+ });
91
+ `;
92
+ }
93
+ function makeHeader(body) {
94
+ return `import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';
95
+
96
+ ${body}`;
97
+ }
98
+ function makeForm(spec) {
99
+ var _a;
100
+ let definitions;
101
+ if ('definitions' in spec) {
102
+ definitions = spec.definitions;
103
+ }
104
+ else if ('components' in spec) {
105
+ definitions = (_a = spec.components) === null || _a === void 0 ? void 0 : _a.schemas;
106
+ }
107
+ else {
108
+ throw new Error('Cannot find schemas/definitions');
109
+ }
110
+ if (!definitions) {
111
+ throw new Error('Cannot find schemas/definitions');
112
+ }
113
+ const definitionKeys = Object.keys(definitions);
114
+ const forms = definitionKeys
115
+ .map(key => makeDefinition(key, definitions[key]))
116
+ .filter(item => item != '')
117
+ .join('');
118
+ const file = makeHeader(forms);
119
+ return prettier_1.default.format(file, { parser: 'typescript', singleQuote: true });
120
+ }
121
+ exports.makeForm = makeForm;
122
+ async function saveFile(file, fileName) {
123
+ const project = new ts_morph_1.Project();
124
+ project.createSourceFile(fileName, file, { overwrite: true });
125
+ return project.save();
126
+ }
127
+ exports.saveFile = saveFile;
128
+ async function loadSpec(fileOrUrlPath) {
129
+ const parser = new swagger_parser_1.default();
130
+ return parser.dereference(fileOrUrlPath);
131
+ }
132
+ exports.loadSpec = loadSpec;
@@ -0,0 +1,2 @@
1
+ export * from './generator-lib';
2
+ export * from './rules';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./generator-lib"), exports);
18
+ __exportStar(require("./rules"), exports);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Licensed under the MIT License, (“the License”); you may not use this
4
+ * file except in compliance with the License.
5
+ *
6
+ * Copyright (c) 2020 Verizon
7
+ */
8
+ import { OpenAPIV2 } from 'openapi-types';
9
+ export declare type Property = {
10
+ format?: string;
11
+ pattern?: string;
12
+ maxLength?: number;
13
+ minLength?: number;
14
+ minimum?: number;
15
+ maximum?: number;
16
+ };
17
+ export declare type Properties = Record<string, Property>;
18
+ export declare type Rule = (fieldName: string, properties: Definition) => string;
19
+ export declare type Definition = OpenAPIV2.DefinitionsObject;
20
+ export declare function requiredRule(fieldName: string, definition: Definition): string;
21
+ export declare function patternRule(fieldName: string, definition: Definition): string;
22
+ export declare function minLengthRule(fieldName: string, definition: Definition): string;
23
+ export declare function maxLengthRule(fieldName: string, definition: Definition): string;
24
+ export declare function emailRule(fieldName: string, definition: Definition): string;
25
+ export declare function minimumRule(fieldName: string, definition: Definition): string;
26
+ export declare function maximumRule(fieldName: string, definition: Definition): string;
package/dist/rules.js ADDED
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Licensed under the MIT License, (“the License”); you may not use this
5
+ * file except in compliance with the License.
6
+ *
7
+ * Copyright (c) 2020 Verizon
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.maximumRule = exports.minimumRule = exports.emailRule = exports.maxLengthRule = exports.minLengthRule = exports.patternRule = exports.requiredRule = void 0;
11
+ function hasMetadata(fieldName, definition, metadataName) {
12
+ return definition.properties[fieldName].hasOwnProperty(metadataName);
13
+ }
14
+ function abstractRule(fieldName, definition, ruleName) {
15
+ return hasMetadata(fieldName, definition, ruleName)
16
+ ? `Validators.${ruleName}(${definition.properties[fieldName][ruleName]})`
17
+ : '';
18
+ }
19
+ function requiredRule(fieldName, definition) {
20
+ return definition.required && definition.required.includes(fieldName) ? `Validators.required` : '';
21
+ }
22
+ exports.requiredRule = requiredRule;
23
+ function patternRule(fieldName, definition) {
24
+ return hasMetadata(fieldName, definition, 'pattern')
25
+ ? `Validators.pattern(/${definition.properties[fieldName]['pattern']}/)`
26
+ : '';
27
+ }
28
+ exports.patternRule = patternRule;
29
+ function minLengthRule(fieldName, definition) {
30
+ return abstractRule(fieldName, definition, 'minLength');
31
+ }
32
+ exports.minLengthRule = minLengthRule;
33
+ function maxLengthRule(fieldName, definition) {
34
+ return abstractRule(fieldName, definition, 'maxLength');
35
+ }
36
+ exports.maxLengthRule = maxLengthRule;
37
+ function emailRule(fieldName, definition) {
38
+ return definition.properties[fieldName].format === 'email' ? `Validators.email` : '';
39
+ }
40
+ exports.emailRule = emailRule;
41
+ function minimumRule(fieldName, definition) {
42
+ return hasMetadata(fieldName, definition, 'minimum')
43
+ ? `Validators.min(${definition.properties[fieldName]['minimum']})`
44
+ : '';
45
+ }
46
+ exports.minimumRule = minimumRule;
47
+ function maximumRule(fieldName, definition) {
48
+ return hasMetadata(fieldName, definition, 'maximum')
49
+ ? `Validators.max(${definition.properties[fieldName]['maximum']})`
50
+ : '';
51
+ }
52
+ exports.maximumRule = maximumRule;
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@jmm-devkit/ngx-form-generator",
3
+ "version": "1.2.1",
4
+ "description": "Generates an Angular ReactiveForm from a Swagger or OpenAPI definition",
5
+ "main": "dist/generator-lib.js",
6
+ "repository": "github:jmm-devkit/ngx-form-generator",
7
+ "bin": {
8
+ "ngx-form-generator": "dist/generator-cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "npm run build && jasmine dist/*.spec.js",
12
+ "build": "tsc --project .\\",
13
+ "lint": "eslint . --ext .ts",
14
+ "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s && git add CHANGELOG.md"
15
+ },
16
+ "author": "Martin McWhorter <martin@mcwhorter.org> (https://github.com/martinmcwhorter)",
17
+ "license": "MIT",
18
+ "keywords": [
19
+ "angular",
20
+ "validation",
21
+ "form",
22
+ "reactive-forms",
23
+ "swagger",
24
+ "openapi",
25
+ "typescript"
26
+ ],
27
+ "devDependencies": {
28
+ "@commitlint/cli": "^17.1.2",
29
+ "@commitlint/config-conventional": "^17.0.3",
30
+ "@types/camelcase": "^4.1.0",
31
+ "@types/jasmine": "^4.3.0",
32
+ "@types/node-fetch": "^2.5.5",
33
+ "@types/prettier": "^1.19.1",
34
+ "@types/yaml": "^1.2.0",
35
+ "@types/yargs": "^17.0.13",
36
+ "@typescript-eslint/eslint-plugin": "^5.0.0",
37
+ "@typescript-eslint/parser": "^5.0.0",
38
+ "commitiquette": "^1.2.1",
39
+ "commitizen": "^4.2.4",
40
+ "eslint": "^8.0.1",
41
+ "eslint-config-prettier": "^6.10.1",
42
+ "husky": "^7.0.4",
43
+ "jasmine": "^4.4.0",
44
+ "lint-staged": "^13.0.3",
45
+ "openapi-types": "^7.0.1",
46
+ "typescript": "^4.8.4"
47
+ },
48
+ "dependencies": {
49
+ "@apidevtools/swagger-parser": "^10.0.2",
50
+ "camelcase": "^5.0.0",
51
+ "prettier": "^1.19.1",
52
+ "ts-morph": "^16.0.0",
53
+ "yaml": "^2.1.3",
54
+ "yargs": "^17.6.0"
55
+ },
56
+ "husky": {
57
+ "hooks": {
58
+ "pre-commit": "lint-staged",
59
+ "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true",
60
+ "commit-msg": "commitLint -E HUSKY_GIT_PARAMS"
61
+ }
62
+ },
63
+ "lint-staged": {
64
+ "*.{js,css,json,md}": [
65
+ "prettier --write",
66
+ "git add"
67
+ ],
68
+ "*.ts": [
69
+ "eslint --fix",
70
+ "prettier --write",
71
+ "git add"
72
+ ]
73
+ },
74
+ "config": {
75
+ "commitizen": {
76
+ "path": "commitiquette"
77
+ }
78
+ }
79
+ }