swaggular 0.5.0 → 1.0.0

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/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Swaggular
2
2
 
3
3
  A powerful tool to generate Angular services and models from Swagger/OpenAPI specifications.
4
- Note: Currently, it does not detects generic types or common interfaces. If you want a more
5
- customized version, please download the project in github and modify the templates folder.
4
+
5
+ You can create a configuration file to customize the generation process.
6
+
7
+ If you want a more customized version, you are invited to download the project in github and modify the project.
6
8
 
7
9
  ## Features
8
10
 
@@ -37,18 +39,74 @@ npx swaggular --input=swagger.json
37
39
  Run the generator by providing the path to your Swagger/OpenAPI file:
38
40
 
39
41
  ```bash
40
- swaggular --input=path/to/your/swagger.json
42
+ swaggular --input=path/to/your/swagger.json```
43
+
44
+
45
+ ## Configuration
46
+
47
+ You can configure Swaggular using CLI arguments or a configuration file (e.g., `swaggular.config.json`).
48
+
49
+ ### CLI Arguments (`ArgsVariables`)
50
+
51
+ | Argument | Description | Default |
52
+ | :--- | :--- | :--- |
53
+ | `--input`, `-i` | Path to the Swagger/OpenAPI file or URL. | `swagger.json` |
54
+ | `--output`, `-o` | Output directory for generated files. | `results` |
55
+ | `--mode` | Grouping mode for services: `tags` or `path`. | `path` |
56
+ | `--ignore-segments` | Comma-separated list of URL segments to ignore when generating service names. | `api` |
57
+ | `--no-generate` | Parse the Swagger file but do not generate any output files. useful for testing. | `false` |
58
+ | `--config` | Path to a configuration file. | `swaggular.config.json` (if exists) |
59
+
60
+ ### Configuration File (`SwaggularConfig`)
61
+
62
+ You can create a `swaggular.config.json` file in your project root to define advanced configurations, such as custom templates, base classes for DTOs, or generic types.
63
+
64
+ #### Example `swaggular.config.json`
65
+
66
+ ```json
67
+ {
68
+ "input": "swagger.json",
69
+ "output": "src/app/api",
70
+ "groupingMode": "tags",
71
+ "segmentsToIgnore": ["api", "v1"],
72
+ "types": {
73
+ "extendsFrom": [
74
+ {
75
+ "name": "PagedRequestDto",
76
+ "type": "interface",
77
+ "properties": [
78
+ { "name": "skipCount", "type": "number", "optional": true, "comments": "" },
79
+ { "name": "maxResultCount", "type": "number", "optional": true, "comments": "" }
80
+ ],
81
+ "imports": []
82
+ }
83
+ ],
84
+ "generic": [
85
+ {
86
+ "name": "PagedResultDto",
87
+ "type": "interface",
88
+ "properties": [
89
+ { "name": "items", "type": "T[]", "optional": true, "comments": "" },
90
+ { "name": "totalCount", "type": "number", "optional": true, "comments": "" }
91
+ ],
92
+ "imports": []
93
+ }
94
+ ]
95
+ },
96
+ "templates": {
97
+ "service": {
98
+ "httpParamsHandlerImport": "import { HttpHelper } from '@shared/utils/http-helper';",
99
+ "httpParamsHandler": "const params = HttpHelper.toHttpParams(${params} || {});"
100
+ }
101
+ }
102
+ }
41
103
  ```
42
104
 
43
- #### Options
44
-
45
- - `--input`: Path to the swagger.json file (required).
46
- - `--ignore-segments`: Path segments to ignore when generating service names. (comma separated)
47
- - `--mode`: Grouping mode. (tags or path) Default: path
48
- - `--no-generate`: Parse the file but don't generate the output. (For testing purposes)
49
- - `--output`: Path to the output folder. Default: results
105
+ This configuration allows you to:
50
106
 
51
- The generated files will be available in the `results` folder.
107
+ 1. **extendsFrom**: Automatically make generated DTOs extend a base class (e.g., `PagedRequestDto`) if they share the same properties.
108
+ 2. **generic**: Automatically detect and use generic types (e.g., `PagedResultDto<T>`) instead of generating duplicate classes like `PagedResultDtoOfUser`.
109
+ 3. **templates.service.options**: Customize how HTTP parameters are handled in generated services, allowing you to use your own helper functions.
52
110
 
53
111
  ## Development
54
112
 
@@ -68,8 +126,8 @@ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduc
68
126
  ### 🚀 Roadmap
69
127
 
70
128
  - [ ] Add TSDoc comments
71
- - [ ] Add function to identify common properties and generate interfaces
72
- - [ ] Add function to generate interfaces and services for just one path
129
+ - [ ] Add function to identify common properties and generate interfaces without any configuration
130
+ - [ ] Add function to generate interfaces and services for just one specific path
73
131
 
74
132
  ## License
75
133
 
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.main = main;
5
5
  const swagger_parser_1 = require("./parsers/swagger-parser");
6
+ const location_factory_1 = require("./utils/location-factory");
6
7
  const generate_interface_1 = require("./renderers/generate-interface");
7
8
  const generate_service_1 = require("./renderers/generate-service");
8
9
  const args_1 = require("./cli/args");
@@ -18,10 +19,11 @@ async function main() {
18
19
  segmentsToIgnore: variables.segmentsToIgnore,
19
20
  ignoreVariables: true,
20
21
  });
22
+ const locations = (0, location_factory_1.computeLocations)();
21
23
  (0, generate_interface_1.generateInterfaces)(variables);
22
24
  (0, generate_service_1.generateServices)();
23
- const interfaceFiles = (0, generate_interface_1.generateInterfacesFiles)(undefined, variables);
24
- const serviceFiles = (0, generate_service_1.generateServiceFiles)(undefined, variables.templates);
25
+ const interfaceFiles = (0, generate_interface_1.generateInterfacesFiles)(locations, variables);
26
+ const serviceFiles = (0, generate_service_1.generateServiceFiles)(locations, variables.templates);
25
27
  if (variables.noGenerate) {
26
28
  console.log('Files not generated');
27
29
  return;
@@ -20,7 +20,7 @@ function generateInterfaceComments(param) {
20
20
  comments.push(`maximum: ${param.maximum}`);
21
21
  }
22
22
  if (comments.length > 0) {
23
- return ` /**\n * ${comments.join('\n * ')}\n */`;
23
+ return `/**\n * ${comments.join('\n * ')}\n */`;
24
24
  }
25
25
  return '';
26
26
  }
@@ -1,13 +1,11 @@
1
1
  import { OpenAPIV3 } from 'openapi-types';
2
2
  import { FileContent } from '../types/file-content';
3
- import { GroupedPath } from '../types/grouped-paths';
4
3
  import { InterfaceData, InterfaceDataProperty } from '../types/interface-data';
5
4
  import { SwaggularConfig } from '../types/config';
6
5
  export declare function parametersToIProperties(parameters: (OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject)[]): InterfaceDataProperty[];
7
6
  export declare function propertiesToIProperties(properties: Record<string, OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject>): InterfaceDataProperty[];
8
7
  export declare function generateInterfaces(templatesConfig?: SwaggularConfig): void;
9
8
  export declare function generateWithParameters(templatesConfig?: SwaggularConfig): void;
10
- export declare function computeParametersName(method: string, innerPath: string, groupedPath: GroupedPath): string;
11
9
  export declare function generateComponentsSchemas(templatesConfig?: SwaggularConfig): void;
12
10
  export declare function generateInterfacesFiles(locations?: Record<string, string[]>, templatesConfig?: SwaggularConfig): FileContent[];
13
11
  export declare function generateContent(interfaceData: InterfaceData, deep?: number): string;
@@ -4,7 +4,6 @@ exports.parametersToIProperties = parametersToIProperties;
4
4
  exports.propertiesToIProperties = propertiesToIProperties;
5
5
  exports.generateInterfaces = generateInterfaces;
6
6
  exports.generateWithParameters = generateWithParameters;
7
- exports.computeParametersName = computeParametersName;
8
7
  exports.generateComponentsSchemas = generateComponentsSchemas;
9
8
  exports.generateInterfacesFiles = generateInterfacesFiles;
10
9
  exports.generateContent = generateContent;
@@ -94,6 +93,9 @@ function computeExtendsFromType(data, extendsTypes) {
94
93
  if (extendsFromTypePropertiesReq.some((p) => !dataPropertiesReq.includes(p))) {
95
94
  continue;
96
95
  }
96
+ if (!extendsFromType.properties.some((ep) => data.properties.some((dp) => dp.name === ep.name))) {
97
+ continue;
98
+ }
97
99
  const extensionProperties = extendsFromType.properties.map((p) => p.name.toLowerCase());
98
100
  return {
99
101
  ...data,
@@ -135,7 +137,7 @@ function generateWithParameters(templatesConfig) {
135
137
  const properties = parametersToIProperties(parameters);
136
138
  if (properties.length === 0)
137
139
  continue;
138
- const interfaceName = computeParametersName(httpMethod, innerPath, groupedPath);
140
+ const interfaceName = (0, path_utils_1.computeParametersName)(httpMethod, innerPath, groupedPath);
139
141
  const interData = {
140
142
  name: interfaceName,
141
143
  type: 'interface',
@@ -152,24 +154,6 @@ function generateWithParameters(templatesConfig) {
152
154
  }
153
155
  interface_state_1.interfaceState.addInterfaces(interfacesData);
154
156
  }
155
- function computeParametersName(method, innerPath, groupedPath) {
156
- const dict = {
157
- get: '',
158
- post: 'Create',
159
- put: 'Update',
160
- delete: 'Delete',
161
- patch: 'Patch',
162
- };
163
- const name = dict[method];
164
- const extra = (0, string_utils_1.kebabToPascalCase)((0, path_utils_1.removeVariablesFromPath)((0, path_utils_1.getExtraSegments)(innerPath, (0, path_utils_1.createBaseUrl)(groupedPath.baseSegments)).join('')));
165
- // Avoid duplication if extra starts with groupName
166
- let suffix = extra;
167
- const groupName = groupedPath.groupName;
168
- if (suffix.startsWith(groupName)) {
169
- suffix = suffix.substring(groupName.length);
170
- }
171
- return name + groupName + (0, string_utils_1.upFirst)(suffix) + 'Params';
172
- }
173
157
  function generateComponentsSchemas(templatesConfig) {
174
158
  const schemas = swagger_state_1.swaggerState.getSchemas();
175
159
  if (!schemas)
@@ -305,14 +289,14 @@ function generateContent(interfaceData, deep = 0) {
305
289
  .map((p) => p.type.replace('[]', ''));
306
290
  const content = `${importsTemplate}\n\nexport interface ${interfaceData.name}${genericTypes.length > 0 ? `<${genericTypes.join(', ')}>` : ''} ${interfaceData.extendsFrom && interfaceData.extendsFrom.length > 0 ? `extends ${interfaceData.extendsFrom.join(', ')}` : ''} {
307
291
  ${interfaceData.properties
308
- .map((p) => `${p.comments}\n\t${(0, string_utils_1.lowerFirst)(p.name)}${p.optional ? '?' : ''}: ${p.type};`)
292
+ .map((p) => `${p.comments}\n ${(0, string_utils_1.lowerFirst)(p.name)}${p.optional ? '?' : ''}: ${p.type};`)
309
293
  .join('\n')}
310
294
  }`;
311
295
  return content;
312
296
  }
313
297
  if (interfaceData.type === 'enum') {
314
298
  const content = `export enum ${interfaceData.name} {
315
- ${interfaceData.properties.map((p) => `${p.comments}\n\t${p.name} = '${p.type}',`).join('\n')}
299
+ ${interfaceData.properties.map((p) => `${p.comments}\n ${p.name} = '${p.type}',`).join('\n')}
316
300
  }`;
317
301
  return content;
318
302
  }
@@ -11,7 +11,7 @@ const string_utils_1 = require("../utils/string-utils");
11
11
  const type_guard_1 = require("../utils/type-guard");
12
12
  const generate_comments_1 = require("./generate-comments");
13
13
  const object_utils_1 = require("../utils/object-utils");
14
- const generate_interface_1 = require("./generate-interface");
14
+ const path_utils_2 = require("../utils/path-utils");
15
15
  const interface_state_1 = require("../core/state/interface-state");
16
16
  const template_renderer_1 = require("../utils/template-renderer");
17
17
  /**
@@ -82,13 +82,15 @@ function generateServiceFiles(locations, templatesConfig) {
82
82
  baseUrl: service.baseUrl,
83
83
  methods: methods,
84
84
  imports: allImports.join(', '),
85
- modelImports: allImports.length > 0 ? `import { ${allImports.join(', ')} } from '../models';` : '',
85
+ modelImports: allImports.length > 0
86
+ ? `import { ${allImports.join(', ')} } from '${'../'.repeat(location.length)}models';`
87
+ : '',
86
88
  hasHttpParamsHandler: hasHttpParams,
87
89
  httpParamsHandler: serviceTemplateConfig?.options?.httpParamsHandler,
88
90
  httpParamsHandlerImport: hasHttpParams
89
91
  ? serviceTemplateConfig?.options?.httpParamsHandlerImport
90
92
  : '',
91
- extraAngularImports: hasHttpParams ? ', HttpParams' : '',
93
+ extraAngularImports: hasHttpParams && !serviceTemplateConfig?.options?.httpParamsHandler ? ', HttpParams' : '',
92
94
  };
93
95
  const content = (0, template_renderer_1.renderServiceTemplate)(templatePath, templateParams);
94
96
  filesContent.push({
@@ -116,7 +118,7 @@ function buildMethods(groupedPath, pathData, serviceName) {
116
118
  const queryParams = parameters.filter((p) => p.in === 'query');
117
119
  let queryParamType;
118
120
  if (queryParams.length > 0) {
119
- queryParamType = (0, generate_interface_1.computeParametersName)(method, path, groupedPath);
121
+ queryParamType = (0, path_utils_2.computeParametersName)(method, path, groupedPath);
120
122
  }
121
123
  const commentParams = parameters.filter((p) => p.in !== 'query');
122
124
  if (queryParamType) {
@@ -359,7 +361,7 @@ function buildMethodTemplate(method, httpParamsHandlerTemplate) {
359
361
  let paramsLogic = '';
360
362
  if (method.queryParamType) {
361
363
  if (httpParamsHandlerTemplate) {
362
- paramsLogic = `\n\t\t${httpParamsHandlerTemplate.replace('${params}', 'queryParams')}`;
364
+ paramsLogic = `\n ${httpParamsHandlerTemplate.replace('${params}', 'queryParams')}`;
363
365
  }
364
366
  else {
365
367
  paramsLogic = `
@@ -9,10 +9,6 @@ export interface SwaggularConfig {
9
9
  service?: {
10
10
  path: string;
11
11
  options?: Partial<ServiceTemplateParams>;
12
- transform?: (options: ServiceTemplateParams) => string;
13
- content?: string;
14
- httpParamsHandler?: string;
15
- httpParamsHandlerImport?: string;
16
12
  };
17
13
  };
18
14
  input?: string;
@@ -0,0 +1 @@
1
+ export declare function computeLocations(): Record<string, string[]>;
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeLocations = computeLocations;
4
+ const swagger_state_1 = require("../core/state/swagger-state");
5
+ const string_utils_1 = require("./string-utils");
6
+ const path_utils_1 = require("./path-utils");
7
+ function computeLocations() {
8
+ const swagger = swagger_state_1.swaggerState.getSwagger();
9
+ const groupedPaths = swagger_state_1.swaggerState.getPathsGroupedByScope();
10
+ const locations = {};
11
+ if (!swagger || !swagger.paths) {
12
+ return locations;
13
+ }
14
+ const processSchema = (dtoName, location) => {
15
+ if (locations[dtoName])
16
+ return;
17
+ if (isEnum(dtoName))
18
+ return;
19
+ locations[dtoName] = location;
20
+ const schemas = swagger_state_1.swaggerState.getSchemas();
21
+ if (!schemas || !schemas[dtoName])
22
+ return;
23
+ const schema = schemas[dtoName];
24
+ const refs = getReferences(schema);
25
+ for (const ref of refs) {
26
+ const childName = ref.split('/').pop();
27
+ if (childName) {
28
+ processSchema(childName, location);
29
+ }
30
+ }
31
+ };
32
+ if (groupedPaths) {
33
+ for (const [groupName, group] of Object.entries(groupedPaths)) {
34
+ if (group.baseSegments && group.baseSegments.length > 0) {
35
+ const folder = (0, string_utils_1.toKebabCase)(group.baseSegments[0]);
36
+ if (!locations[groupName]) {
37
+ locations[groupName] = [folder];
38
+ }
39
+ }
40
+ }
41
+ }
42
+ // 1. Map Generated Parameter Interfaces
43
+ if (groupedPaths) {
44
+ for (const groupedPath of Object.values(groupedPaths)) {
45
+ for (const innerPath of groupedPath.paths) {
46
+ const pathInfo = swagger.paths[innerPath];
47
+ if (!pathInfo)
48
+ continue;
49
+ const methods = ['get', 'post', 'put', 'delete', 'patch'];
50
+ for (const method of methods) {
51
+ const operation = pathInfo[method];
52
+ if (!operation)
53
+ continue;
54
+ // Check if parameters exist (similar condition to generate-interface)
55
+ if (operation.parameters && operation.parameters.length > 0) {
56
+ const interfaceName = (0, path_utils_1.computeParametersName)(method, innerPath, groupedPath);
57
+ if (operation.tags && operation.tags.length > 0) {
58
+ const tag = operation.tags[0];
59
+ const folder = (0, string_utils_1.toKebabCase)(tag);
60
+ if (!locations[interfaceName]) {
61
+ locations[interfaceName] = [folder];
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ // 2. Map DTOs and Services
70
+ for (const pathItem of Object.values(swagger.paths)) {
71
+ if (!pathItem)
72
+ continue;
73
+ const operations = [
74
+ pathItem.get,
75
+ pathItem.put,
76
+ pathItem.post,
77
+ pathItem.delete,
78
+ pathItem.options,
79
+ pathItem.head,
80
+ pathItem.patch,
81
+ pathItem.trace,
82
+ ];
83
+ for (const operation of operations) {
84
+ if (!operation || !operation.tags || operation.tags.length === 0) {
85
+ continue;
86
+ }
87
+ const tag = operation.tags[0];
88
+ const folder = (0, string_utils_1.toKebabCase)(tag);
89
+ const location = [folder];
90
+ // Map Service (Tag) to location
91
+ if (!locations[tag]) {
92
+ locations[tag] = location;
93
+ }
94
+ // Map DTOs used in this operation
95
+ const refs = getReferences(operation);
96
+ for (const ref of refs) {
97
+ const dtoName = ref.split('/').pop();
98
+ if (dtoName) {
99
+ processSchema(dtoName, location);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ return locations;
105
+ }
106
+ function isEnum(name) {
107
+ const schemas = swagger_state_1.swaggerState.getSchemas();
108
+ if (!schemas)
109
+ return false;
110
+ const schema = schemas[name];
111
+ if (!schema)
112
+ return false;
113
+ if ('$ref' in schema)
114
+ return false;
115
+ return !!schema.enum;
116
+ }
117
+ function getReferences(obj) {
118
+ const refs = [];
119
+ if (!obj || typeof obj !== 'object') {
120
+ return refs;
121
+ }
122
+ if (Array.isArray(obj)) {
123
+ for (const item of obj) {
124
+ refs.push(...getReferences(item));
125
+ }
126
+ return refs;
127
+ }
128
+ for (const key of Object.keys(obj)) {
129
+ if (key === '$ref' && typeof obj[key] === 'string') {
130
+ refs.push(obj[key]);
131
+ }
132
+ else {
133
+ refs.push(...getReferences(obj[key]));
134
+ }
135
+ }
136
+ return refs;
137
+ }
@@ -1,6 +1,8 @@
1
+ import { GroupedPath } from '../types/grouped-paths';
1
2
  export declare function isVariable(segment: string): boolean;
2
3
  export declare function getExtraSegments(fullPath: string, baseUrl: string): string[];
3
4
  export declare function removeVariablesFromPath(path: string): string;
4
5
  export declare function createBaseUrl(baseSegments: string[]): string;
5
6
  export declare function findCommonBaseUrl(paths: string[]): string;
6
7
  export declare function standarizedPath(path: string): string;
8
+ export declare function computeParametersName(method: string, innerPath: string, groupedPath: GroupedPath): string;
@@ -6,6 +6,8 @@ exports.removeVariablesFromPath = removeVariablesFromPath;
6
6
  exports.createBaseUrl = createBaseUrl;
7
7
  exports.findCommonBaseUrl = findCommonBaseUrl;
8
8
  exports.standarizedPath = standarizedPath;
9
+ exports.computeParametersName = computeParametersName;
10
+ const string_utils_1 = require("./string-utils");
9
11
  function isVariable(segment) {
10
12
  return /^\{.+\}$/.test(segment);
11
13
  }
@@ -63,3 +65,21 @@ function standarizedPath(path) {
63
65
  const filtered = splitted.filter((p) => p !== '').map((p) => p.toLowerCase());
64
66
  return filtered.join('/');
65
67
  }
68
+ function computeParametersName(method, innerPath, groupedPath) {
69
+ const dict = {
70
+ get: '',
71
+ post: 'Create',
72
+ put: 'Update',
73
+ delete: 'Delete',
74
+ patch: 'Patch',
75
+ };
76
+ const name = dict[method];
77
+ const extra = (0, string_utils_1.kebabToPascalCase)(removeVariablesFromPath(getExtraSegments(innerPath, createBaseUrl(groupedPath.baseSegments)).join('')));
78
+ // Avoid duplication if extra starts with groupName
79
+ let suffix = extra;
80
+ const groupName = groupedPath.groupName;
81
+ if (suffix.startsWith(groupName)) {
82
+ suffix = suffix.substring(groupName.length);
83
+ }
84
+ return name + groupName + (0, string_utils_1.upFirst)(suffix) + 'Params';
85
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swaggular",
3
- "version": "0.5.0",
3
+ "version": "1.0.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "bin": {