zod-codegen 1.2.2 → 1.4.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.
Files changed (84) hide show
  1. package/.claude/settings.local.json +43 -0
  2. package/.github/workflows/ci.yml +4 -4
  3. package/.github/workflows/release.yml +1 -1
  4. package/CHANGELOG.md +17 -0
  5. package/README.md +61 -9
  6. package/dist/scripts/update-manifest.d.ts +14 -0
  7. package/dist/scripts/update-manifest.d.ts.map +1 -0
  8. package/dist/src/cli.d.ts +3 -0
  9. package/dist/src/cli.d.ts.map +1 -0
  10. package/dist/src/cli.js +31 -2
  11. package/dist/src/generator.d.ts +23 -0
  12. package/dist/src/generator.d.ts.map +1 -0
  13. package/dist/src/generator.js +3 -2
  14. package/dist/src/http/fetch-client.d.ts +15 -0
  15. package/dist/src/http/fetch-client.d.ts.map +1 -0
  16. package/dist/src/interfaces/code-generator.d.ts +20 -0
  17. package/dist/src/interfaces/code-generator.d.ts.map +1 -0
  18. package/dist/src/interfaces/file-reader.d.ts +13 -0
  19. package/dist/src/interfaces/file-reader.d.ts.map +1 -0
  20. package/dist/src/polyfills/fetch.d.ts +5 -0
  21. package/dist/src/polyfills/fetch.d.ts.map +1 -0
  22. package/dist/src/services/code-generator.service.d.ts +57 -0
  23. package/dist/src/services/code-generator.service.d.ts.map +1 -0
  24. package/dist/src/services/code-generator.service.js +43 -6
  25. package/dist/src/services/file-reader.service.d.ts +9 -0
  26. package/dist/src/services/file-reader.service.d.ts.map +1 -0
  27. package/dist/src/services/file-writer.service.d.ts +10 -0
  28. package/dist/src/services/file-writer.service.d.ts.map +1 -0
  29. package/dist/src/services/import-builder.service.d.ts +14 -0
  30. package/dist/src/services/import-builder.service.d.ts.map +1 -0
  31. package/dist/src/services/type-builder.service.d.ts +12 -0
  32. package/dist/src/services/type-builder.service.d.ts.map +1 -0
  33. package/dist/src/types/generator-options.d.ts +59 -0
  34. package/dist/src/types/generator-options.d.ts.map +1 -0
  35. package/dist/src/types/generator-options.js +1 -0
  36. package/dist/src/types/http.d.ts +25 -0
  37. package/dist/src/types/http.d.ts.map +1 -0
  38. package/dist/src/types/openapi.d.ts +1120 -0
  39. package/dist/src/types/openapi.d.ts.map +1 -0
  40. package/dist/src/utils/error-handler.d.ts +3 -0
  41. package/dist/src/utils/error-handler.d.ts.map +1 -0
  42. package/dist/src/utils/error-handler.js +2 -2
  43. package/dist/src/utils/execution-time.d.ts +2 -0
  44. package/dist/src/utils/execution-time.d.ts.map +1 -0
  45. package/dist/src/utils/manifest.d.ts +8 -0
  46. package/dist/src/utils/manifest.d.ts.map +1 -0
  47. package/dist/src/utils/naming-convention.d.ts +80 -0
  48. package/dist/src/utils/naming-convention.d.ts.map +1 -0
  49. package/dist/src/utils/naming-convention.js +135 -0
  50. package/dist/src/utils/reporter.d.ts +7 -0
  51. package/dist/src/utils/reporter.d.ts.map +1 -0
  52. package/dist/src/utils/signal-handler.d.ts +3 -0
  53. package/dist/src/utils/signal-handler.d.ts.map +1 -0
  54. package/dist/src/utils/signal-handler.js +2 -2
  55. package/dist/src/utils/tty.d.ts +2 -0
  56. package/dist/src/utils/tty.d.ts.map +1 -0
  57. package/dist/tests/integration/cli.test.d.ts +2 -0
  58. package/dist/tests/integration/cli.test.d.ts.map +1 -0
  59. package/dist/tests/integration/cli.test.js +2 -2
  60. package/dist/tests/unit/code-generator.test.d.ts +2 -0
  61. package/dist/tests/unit/code-generator.test.d.ts.map +1 -0
  62. package/dist/tests/unit/code-generator.test.js +170 -1
  63. package/dist/tests/unit/file-reader.test.d.ts +2 -0
  64. package/dist/tests/unit/file-reader.test.d.ts.map +1 -0
  65. package/dist/tests/unit/generator.test.d.ts +2 -0
  66. package/dist/tests/unit/generator.test.d.ts.map +1 -0
  67. package/dist/tests/unit/naming-convention.test.d.ts +2 -0
  68. package/dist/tests/unit/naming-convention.test.d.ts.map +1 -0
  69. package/dist/tests/unit/naming-convention.test.js +231 -0
  70. package/dist/vitest.config.d.ts +3 -0
  71. package/dist/vitest.config.d.ts.map +1 -0
  72. package/package.json +12 -7
  73. package/scripts/republish-versions.sh +94 -0
  74. package/src/cli.ts +34 -3
  75. package/src/generator.ts +8 -1
  76. package/src/services/code-generator.service.ts +74 -7
  77. package/src/types/generator-options.ts +60 -0
  78. package/src/utils/error-handler.ts +2 -2
  79. package/src/utils/naming-convention.ts +214 -0
  80. package/src/utils/signal-handler.ts +2 -2
  81. package/tests/integration/cli.test.ts +2 -2
  82. package/tests/unit/code-generator.test.ts +189 -1
  83. package/tests/unit/naming-convention.test.ts +263 -0
  84. package/tsconfig.json +2 -0
@@ -0,0 +1,231 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { transformNamingConvention } from '../../src/utils/naming-convention.js';
3
+ describe('transformNamingConvention', () => {
4
+ describe('transform', () => {
5
+ const testCases = [
6
+ // camelCase
7
+ {
8
+ input: 'get_user_by_id',
9
+ convention: 'camelCase',
10
+ expected: 'getUserById',
11
+ description: 'should convert snake_case to camelCase',
12
+ },
13
+ {
14
+ input: 'GetUserById',
15
+ convention: 'camelCase',
16
+ expected: 'getUserById',
17
+ description: 'should convert PascalCase to camelCase',
18
+ },
19
+ {
20
+ input: 'get-user-by-id',
21
+ convention: 'camelCase',
22
+ expected: 'getUserById',
23
+ description: 'should convert kebab-case to camelCase',
24
+ },
25
+ {
26
+ input: 'getUserById',
27
+ convention: 'camelCase',
28
+ expected: 'getUserById',
29
+ description: 'should keep camelCase as camelCase',
30
+ },
31
+ // PascalCase
32
+ {
33
+ input: 'get_user_by_id',
34
+ convention: 'PascalCase',
35
+ expected: 'GetUserById',
36
+ description: 'should convert snake_case to PascalCase',
37
+ },
38
+ {
39
+ input: 'get-user-by-id',
40
+ convention: 'PascalCase',
41
+ expected: 'GetUserById',
42
+ description: 'should convert kebab-case to PascalCase',
43
+ },
44
+ {
45
+ input: 'getUserById',
46
+ convention: 'PascalCase',
47
+ expected: 'GetUserById',
48
+ description: 'should convert camelCase to PascalCase',
49
+ },
50
+ // snake_case
51
+ {
52
+ input: 'getUserById',
53
+ convention: 'snake_case',
54
+ expected: 'get_user_by_id',
55
+ description: 'should convert camelCase to snake_case',
56
+ },
57
+ {
58
+ input: 'GetUserById',
59
+ convention: 'snake_case',
60
+ expected: 'get_user_by_id',
61
+ description: 'should convert PascalCase to snake_case',
62
+ },
63
+ {
64
+ input: 'get-user-by-id',
65
+ convention: 'snake_case',
66
+ expected: 'get_user_by_id',
67
+ description: 'should convert kebab-case to snake_case',
68
+ },
69
+ // kebab-case
70
+ {
71
+ input: 'getUserById',
72
+ convention: 'kebab-case',
73
+ expected: 'get-user-by-id',
74
+ description: 'should convert camelCase to kebab-case',
75
+ },
76
+ {
77
+ input: 'GetUserById',
78
+ convention: 'kebab-case',
79
+ expected: 'get-user-by-id',
80
+ description: 'should convert PascalCase to kebab-case',
81
+ },
82
+ {
83
+ input: 'get_user_by_id',
84
+ convention: 'kebab-case',
85
+ expected: 'get-user-by-id',
86
+ description: 'should convert snake_case to kebab-case',
87
+ },
88
+ // SCREAMING_SNAKE_CASE
89
+ {
90
+ input: 'getUserById',
91
+ convention: 'SCREAMING_SNAKE_CASE',
92
+ expected: 'GET_USER_BY_ID',
93
+ description: 'should convert camelCase to SCREAMING_SNAKE_CASE',
94
+ },
95
+ {
96
+ input: 'get-user-by-id',
97
+ convention: 'SCREAMING_SNAKE_CASE',
98
+ expected: 'GET_USER_BY_ID',
99
+ description: 'should convert kebab-case to SCREAMING_SNAKE_CASE',
100
+ },
101
+ // SCREAMING-KEBAB-CASE
102
+ {
103
+ input: 'getUserById',
104
+ convention: 'SCREAMING-KEBAB-CASE',
105
+ expected: 'GET-USER-BY-ID',
106
+ description: 'should convert camelCase to SCREAMING-KEBAB-CASE',
107
+ },
108
+ {
109
+ input: 'get_user_by_id',
110
+ convention: 'SCREAMING-KEBAB-CASE',
111
+ expected: 'GET-USER-BY-ID',
112
+ description: 'should convert snake_case to SCREAMING-KEBAB-CASE',
113
+ },
114
+ // Edge cases
115
+ {
116
+ input: '',
117
+ convention: 'camelCase',
118
+ expected: '',
119
+ description: 'should handle empty string',
120
+ },
121
+ {
122
+ input: 'a',
123
+ convention: 'camelCase',
124
+ expected: 'a',
125
+ description: 'should handle single character',
126
+ },
127
+ {
128
+ input: 'API',
129
+ convention: 'camelCase',
130
+ expected: 'aPI',
131
+ description: 'should handle all uppercase (splits at uppercase boundaries)',
132
+ },
133
+ {
134
+ input: 'getUser123ById',
135
+ convention: 'snake_case',
136
+ expected: 'get_user_123_by_id',
137
+ description: 'should handle numbers in identifiers (splits at digit boundaries)',
138
+ },
139
+ ];
140
+ testCases.forEach(({ input, convention, expected, description }) => {
141
+ it(description, () => {
142
+ const result = transformNamingConvention(input, convention);
143
+ expect(result).toBe(expected);
144
+ });
145
+ });
146
+ });
147
+ describe('edge cases', () => {
148
+ describe('consecutive delimiters', () => {
149
+ it('should handle consecutive underscores', () => {
150
+ expect(transformNamingConvention('get__user__by__id', 'camelCase')).toBe('getUserById');
151
+ });
152
+ it('should handle consecutive hyphens', () => {
153
+ expect(transformNamingConvention('get--user--by--id', 'snake_case')).toBe('get_user_by_id');
154
+ });
155
+ it('should handle mixed consecutive delimiters', () => {
156
+ expect(transformNamingConvention('get__user--by_id', 'kebab-case')).toBe('get-user-by-id');
157
+ });
158
+ });
159
+ describe('mixed delimiters', () => {
160
+ it('should handle snake_case and kebab-case mixed', () => {
161
+ // Note: Delimiters split the string, so 'byId' becomes 'by' and 'id' (normalized to lowercase)
162
+ expect(transformNamingConvention('get_user-byId', 'camelCase')).toBe('getUserByid');
163
+ });
164
+ it('should handle camelCase with underscores', () => {
165
+ // Note: Underscore delimiter splits, so 'byId' becomes 'by' and 'id'
166
+ expect(transformNamingConvention('getUser_byId', 'PascalCase')).toBe('GetuserByid');
167
+ });
168
+ it('should handle dots as delimiters', () => {
169
+ expect(transformNamingConvention('get.user.by.id', 'snake_case')).toBe('get_user_by_id');
170
+ });
171
+ it('should handle spaces as delimiters', () => {
172
+ expect(transformNamingConvention('get user by id', 'kebab-case')).toBe('get-user-by-id');
173
+ });
174
+ });
175
+ describe('unicode and special characters', () => {
176
+ it('should handle accented characters', () => {
177
+ expect(transformNamingConvention('getRésumé', 'snake_case')).toBe('get_résumé');
178
+ });
179
+ it('should handle numbers at start', () => {
180
+ expect(transformNamingConvention('123getUser', 'camelCase')).toBe('123getUser');
181
+ });
182
+ it('should handle single uppercase letter', () => {
183
+ expect(transformNamingConvention('getX', 'snake_case')).toBe('get_x');
184
+ });
185
+ it('should handle all numbers', () => {
186
+ expect(transformNamingConvention('123456', 'camelCase')).toBe('123456');
187
+ });
188
+ });
189
+ describe('acronyms and abbreviations', () => {
190
+ it('should handle acronyms in camelCase (splits on uppercase boundaries)', () => {
191
+ // Note: Algorithm splits on uppercase boundaries, so 'XML' becomes 'X', 'M', 'L'
192
+ // This is correct behavior - detecting acronyms would require a dictionary
193
+ expect(transformNamingConvention('getXMLData', 'snake_case')).toBe('get_x_m_l_data');
194
+ });
195
+ it('should handle multiple acronyms (splits on uppercase boundaries)', () => {
196
+ // Note: 'JSON' and 'XML' are split into individual letters
197
+ // 'parseJSONToXML' → ['parse', 'j', 's', 'o', 'n', 'to', 'x', 'm', 'l']
198
+ expect(transformNamingConvention('parseJSONToXML', 'kebab-case')).toBe('parse-j-s-o-n-to-x-m-l');
199
+ });
200
+ it('should handle ID abbreviation (splits on uppercase boundaries)', () => {
201
+ // Note: 'ID' is split into 'I' and 'D' because the algorithm splits on uppercase boundaries
202
+ // This is correct behavior - detecting acronyms would require a dictionary
203
+ expect(transformNamingConvention('getUserID', 'snake_case')).toBe('get_user_i_d');
204
+ });
205
+ });
206
+ describe('already transformed names', () => {
207
+ it('should be idempotent for camelCase', () => {
208
+ const input = 'getUserById';
209
+ const result = transformNamingConvention(input, 'camelCase');
210
+ expect(transformNamingConvention(result, 'camelCase')).toBe(result);
211
+ });
212
+ it('should be idempotent for snake_case', () => {
213
+ const input = 'get_user_by_id';
214
+ const result = transformNamingConvention(input, 'snake_case');
215
+ expect(transformNamingConvention(result, 'snake_case')).toBe(result);
216
+ });
217
+ });
218
+ describe('special patterns', () => {
219
+ it('should handle single word', () => {
220
+ expect(transformNamingConvention('user', 'PascalCase')).toBe('User');
221
+ });
222
+ it('should handle two words', () => {
223
+ expect(transformNamingConvention('getUser', 'snake_case')).toBe('get_user');
224
+ });
225
+ it('should handle very long names', () => {
226
+ const longName = 'getVeryLongOperationNameWithManyWords';
227
+ expect(transformNamingConvention(longName, 'kebab-case')).toBe('get-very-long-operation-name-with-many-words');
228
+ });
229
+ });
230
+ });
231
+ });
@@ -0,0 +1,3 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
3
+ //# sourceMappingURL=vitest.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest.config.d.ts","sourceRoot":"","sources":["../vitest.config.ts"],"names":[],"mappings":";AAGA,wBAmCG"}
package/package.json CHANGED
@@ -19,7 +19,7 @@
19
19
  "typescript": "^5.9.3",
20
20
  "url-pattern": "^1.0.3",
21
21
  "yargs": "^18.0.0",
22
- "zod": "^4.1.12"
22
+ "zod": "^4.1.13"
23
23
  },
24
24
  "description": "A powerful TypeScript code generator that creates Zod schemas and type-safe clients from OpenAPI specifications",
25
25
  "keywords": [
@@ -45,25 +45,26 @@
45
45
  "@types/jsonpath": "^0.2.4",
46
46
  "@types/node": "^24.10.1",
47
47
  "@types/yargs": "^17.0.35",
48
- "@vitest/coverage-v8": "^4.0.9",
48
+ "@vitest/coverage-v8": "^4.0.14",
49
49
  "eslint": "^9.39.1",
50
50
  "eslint-config-prettier": "^10.1.8",
51
51
  "husky": "^9.1.7",
52
- "lint-staged": "^16.2.6",
52
+ "lint-staged": "^16.2.7",
53
53
  "semantic-release": "^25.0.2",
54
54
  "ts-node": "^10.9.2",
55
55
  "typescript-eslint": "^8.46.4",
56
56
  "undici": "^7.16.0",
57
- "vitest": "^4.0.8",
57
+ "vitest": "^4.0.14",
58
58
  "yarn-audit-fix": "^10.1.1"
59
59
  },
60
60
  "optionalDependencies": {
61
61
  "undici": "^7.16.0"
62
62
  },
63
63
  "homepage": "https://github.com/julienandreu/zod-codegen",
64
- "license": "MIT",
64
+ "license": "Apache-2.0",
65
65
  "name": "zod-codegen",
66
66
  "type": "module",
67
+ "types": "./dist/src/generator.d.ts",
67
68
  "exports": {
68
69
  ".": {
69
70
  "types": "./dist/src/generator.d.ts",
@@ -84,7 +85,11 @@
84
85
  "tar": "^7.5.2"
85
86
  },
86
87
  "glob": "^11.1.0",
87
- "tar": "^7.5.2"
88
+ "tar": "^7.5.2",
89
+ "yargs": "^18.0.0"
90
+ },
91
+ "resolutions": {
92
+ "yargs": "^18.0.0"
88
93
  },
89
94
  "scripts": {
90
95
  "audit:fix": "yarn-audit-fix",
@@ -110,5 +115,5 @@
110
115
  "release": "semantic-release",
111
116
  "release:dry": "semantic-release --dry-run"
112
117
  },
113
- "version": "1.2.2"
118
+ "version": "1.4.0"
114
119
  }
@@ -0,0 +1,94 @@
1
+ #!/bin/bash
2
+
3
+ # Script to republish missing versions to npm
4
+ # Usage: ./scripts/republish-versions.sh [version1] [version2]
5
+ # Example: ./scripts/republish-versions.sh 1.3.0 1.4.0
6
+
7
+ set -e
8
+
9
+ # Colors for output
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Function to publish a version
16
+ publish_version() {
17
+ local version=$1
18
+ local tag="v${version}"
19
+
20
+ echo -e "${YELLOW}📦 Publishing version ${version}...${NC}"
21
+
22
+ # Check if tag exists
23
+ if ! git rev-parse "$tag" >/dev/null 2>&1; then
24
+ echo -e "${RED}❌ Tag ${tag} does not exist${NC}"
25
+ return 1
26
+ fi
27
+
28
+ # Check if version is already published
29
+ if npm view "zod-codegen@${version}" version >/dev/null 2>&1; then
30
+ echo -e "${YELLOW}⚠️ Version ${version} already exists on npm. Skipping...${NC}"
31
+ return 0
32
+ fi
33
+
34
+ # Save current branch/commit
35
+ local current_branch=$(git rev-parse --abbrev-ref HEAD)
36
+ local current_commit=$(git rev-parse HEAD)
37
+
38
+ echo -e "${GREEN}✓ Checking out tag ${tag}${NC}"
39
+ git checkout "$tag" --quiet
40
+
41
+ # Verify package.json version matches
42
+ local package_version=$(node -p "require('./package.json').version")
43
+ if [ "$package_version" != "$version" ]; then
44
+ echo -e "${RED}❌ Package.json version (${package_version}) doesn't match tag version (${version})${NC}"
45
+ git checkout "$current_branch" --quiet 2>/dev/null || git checkout "$current_commit" --quiet
46
+ return 1
47
+ fi
48
+
49
+ # Install dependencies
50
+ echo -e "${GREEN}✓ Installing dependencies...${NC}"
51
+ yarn install --frozen-lockfile
52
+
53
+ # Build project
54
+ echo -e "${GREEN}✓ Building project...${NC}"
55
+ yarn build
56
+
57
+ # Run prepublishOnly checks (build, test, lint, type-check)
58
+ echo -e "${GREEN}✓ Running prepublish checks...${NC}"
59
+ yarn test
60
+ yarn lint:check
61
+ yarn type-check
62
+
63
+ # Publish to npm
64
+ echo -e "${GREEN}✓ Publishing to npm...${NC}"
65
+ npm publish --access public
66
+
67
+ echo -e "${GREEN}✅ Successfully published version ${version}${NC}"
68
+
69
+ # Return to original branch/commit
70
+ echo -e "${GREEN}✓ Returning to ${current_branch}...${NC}"
71
+ git checkout "$current_branch" --quiet 2>/dev/null || git checkout "$current_commit" --quiet
72
+
73
+ echo ""
74
+ }
75
+
76
+ # Check if npm is authenticated
77
+ if ! npm whoami >/dev/null 2>&1; then
78
+ echo -e "${RED}❌ Not authenticated to npm. Please run 'npm login' first.${NC}"
79
+ exit 1
80
+ fi
81
+
82
+ echo -e "${GREEN}✓ Authenticated as $(npm whoami)${NC}"
83
+ echo ""
84
+
85
+ # Get versions from arguments or use defaults
86
+ VERSIONS=("${@:-1.3.0 1.4.0}")
87
+
88
+ # Publish each version
89
+ for version in "${VERSIONS[@]}"; do
90
+ publish_version "$version"
91
+ done
92
+
93
+ echo -e "${GREEN}🎉 All versions published successfully!${NC}"
94
+
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import yargs from 'yargs';
4
4
  import {hideBin} from 'yargs/helpers';
5
- import {Generator} from './generator.js';
5
+ import {Generator, type GeneratorOptions, type NamingConvention} from './generator.js';
6
6
  import {readFileSync} from 'node:fs';
7
7
  import {fileURLToPath} from 'node:url';
8
8
  import {dirname, join} from 'node:path';
@@ -72,15 +72,46 @@ const argv = yargs(hideBin(process.argv))
72
72
  description: 'Directory to output the generated files',
73
73
  default: 'generated',
74
74
  })
75
+ .option('naming-convention', {
76
+ alias: 'n',
77
+ type: 'string',
78
+ description: 'Naming convention to apply to operation IDs',
79
+ choices: ['camelCase', 'PascalCase', 'snake_case', 'kebab-case', 'SCREAMING_SNAKE_CASE', 'SCREAMING-KEBAB-CASE'],
80
+ default: undefined,
81
+ })
75
82
  .strict()
76
83
  .help()
77
84
  .parseSync();
78
85
 
79
- const {input, output} = argv;
86
+ const {input, output, namingConvention} = argv;
87
+
88
+ /**
89
+ * Type guard to validate that a string is a valid naming convention.
90
+ * This ensures type safety when parsing CLI arguments.
91
+ *
92
+ * @param value - The value to check
93
+ * @returns True if the value is a valid NamingConvention
94
+ */
95
+ function isValidNamingConvention(value: string | undefined): value is NamingConvention {
96
+ if (value === undefined) {
97
+ return false;
98
+ }
99
+ const validConventions: readonly NamingConvention[] = [
100
+ 'camelCase',
101
+ 'PascalCase',
102
+ 'snake_case',
103
+ 'kebab-case',
104
+ 'SCREAMING_SNAKE_CASE',
105
+ 'SCREAMING-KEBAB-CASE',
106
+ ] as const;
107
+ return validConventions.includes(value as NamingConvention);
108
+ }
80
109
 
81
110
  void (async () => {
82
111
  try {
83
- const generator = new Generator(name, version, reporter, input, output);
112
+ const options: GeneratorOptions = isValidNamingConvention(namingConvention) ? {namingConvention} : {};
113
+
114
+ const generator = new Generator(name, version, reporter, input, output, options);
84
115
  const exitCode = await generator.run();
85
116
  process.exit(exitCode);
86
117
  } catch (error) {
package/src/generator.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  import type {Reporter} from './utils/reporter.js';
2
2
  import type {OpenApiSpecType} from './types/openapi.js';
3
+ import type {GeneratorOptions} from './types/generator-options.js';
3
4
  import {OpenApiFileParserService, SyncFileReaderService} from './services/file-reader.service.js';
4
5
  import {TypeScriptCodeGeneratorService} from './services/code-generator.service.js';
5
6
  import {SyncFileWriterService} from './services/file-writer.service.js';
6
7
 
8
+ // Re-export types for library users
9
+ export type {GeneratorOptions} from './types/generator-options.js';
10
+ export type {NamingConvention, OperationDetails, OperationNameTransformer} from './utils/naming-convention.js';
11
+
7
12
  export class Generator {
8
13
  private readonly fileReader = new SyncFileReaderService();
9
14
  private readonly fileParser = new OpenApiFileParserService();
10
- private readonly codeGenerator = new TypeScriptCodeGeneratorService();
15
+ private readonly codeGenerator: TypeScriptCodeGeneratorService;
11
16
  private readonly fileWriter: SyncFileWriterService;
12
17
  private readonly outputPath: string;
13
18
 
@@ -17,9 +22,11 @@ export class Generator {
17
22
  private readonly reporter: Reporter,
18
23
  private readonly inputPath: string,
19
24
  private readonly _outputDir: string,
25
+ options: GeneratorOptions = {},
20
26
  ) {
21
27
  this.fileWriter = new SyncFileWriterService(this._name, this._version, inputPath);
22
28
  this.outputPath = this.fileWriter.resolveOutputPath(this._outputDir);
29
+ this.codeGenerator = new TypeScriptCodeGeneratorService(options);
23
30
  }
24
31
 
25
32
  async run(): Promise<number> {
@@ -3,14 +3,28 @@ import * as ts from 'typescript';
3
3
  import {z} from 'zod';
4
4
  import type {CodeGenerator, SchemaBuilder} from '../interfaces/code-generator.js';
5
5
  import type {MethodSchemaType, OpenApiSpecType, ReferenceType} from '../types/openapi.js';
6
+ import type {GeneratorOptions} from '../types/generator-options.js';
6
7
  import {MethodSchema, Reference, SchemaProperties} from '../types/openapi.js';
7
8
  import {TypeScriptImportBuilderService} from './import-builder.service.js';
8
9
  import {TypeScriptTypeBuilderService} from './type-builder.service.js';
10
+ import {
11
+ type NamingConvention,
12
+ type OperationDetails,
13
+ type OperationNameTransformer,
14
+ transformNamingConvention,
15
+ } from '../utils/naming-convention.js';
9
16
 
10
17
  export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuilder {
11
18
  private readonly typeBuilder = new TypeScriptTypeBuilderService();
12
19
  private readonly importBuilder = new TypeScriptImportBuilderService();
13
20
  private readonly printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
21
+ private readonly namingConvention: NamingConvention | undefined;
22
+ private readonly operationNameTransformer: OperationNameTransformer | undefined;
23
+
24
+ constructor(options: GeneratorOptions = {}) {
25
+ this.namingConvention = options.namingConvention;
26
+ this.operationNameTransformer = options.operationNameTransformer;
27
+ }
14
28
 
15
29
  private readonly ZodAST = z.object({
16
30
  type: z.enum(['string', 'number', 'boolean', 'object', 'array', 'unknown', 'record']),
@@ -894,6 +908,36 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
894
908
  }, []);
895
909
  }
896
910
 
911
+ /**
912
+ * Transforms operation ID according to the configured naming convention or transformer
913
+ * Ensures the result is a valid TypeScript identifier
914
+ */
915
+ private transformOperationName(operationId: string, method: string, path: string, schema: MethodSchemaType): string {
916
+ let transformed: string;
917
+
918
+ // Custom transformer takes precedence
919
+ if (this.operationNameTransformer) {
920
+ const details: OperationDetails = {
921
+ operationId,
922
+ method,
923
+ path,
924
+ ...(schema.tags !== undefined && {tags: schema.tags}),
925
+ ...(schema.summary !== undefined && {summary: schema.summary}),
926
+ ...(schema.description !== undefined && {description: schema.description}),
927
+ };
928
+ transformed = this.operationNameTransformer(details);
929
+ } else if (this.namingConvention) {
930
+ // Apply naming convention if specified
931
+ transformed = transformNamingConvention(operationId, this.namingConvention);
932
+ } else {
933
+ // Return original operationId if no transformation is configured
934
+ transformed = operationId;
935
+ }
936
+
937
+ // Sanitize to ensure valid TypeScript identifier (handles edge cases from custom transformers)
938
+ return this.typeBuilder.sanitizeIdentifier(transformed);
939
+ }
940
+
897
941
  private buildEndpointMethod(
898
942
  method: string,
899
943
  path: string,
@@ -979,10 +1023,12 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
979
1023
  statements.push(ts.factory.createReturnStatement(ts.factory.createAwaitExpression(makeRequestCall)));
980
1024
  }
981
1025
 
1026
+ const transformedOperationId = this.transformOperationName(String(schema.operationId), method, path, schema);
1027
+
982
1028
  const methodDeclaration = ts.factory.createMethodDeclaration(
983
1029
  [ts.factory.createToken(ts.SyntaxKind.AsyncKeyword)],
984
1030
  undefined,
985
- ts.factory.createIdentifier(String(schema.operationId)),
1031
+ ts.factory.createIdentifier(transformedOperationId),
986
1032
  undefined,
987
1033
  undefined,
988
1034
  parameters,
@@ -2286,7 +2332,10 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
2286
2332
  switch (prop['format']) {
2287
2333
  case 'email':
2288
2334
  stringSchema = ts.factory.createCallExpression(
2289
- ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('email')),
2335
+ ts.factory.createPropertyAccessExpression(
2336
+ ts.factory.createIdentifier('z'),
2337
+ ts.factory.createIdentifier('email'),
2338
+ ),
2290
2339
  undefined,
2291
2340
  [],
2292
2341
  );
@@ -2294,28 +2343,46 @@ export class TypeScriptCodeGeneratorService implements CodeGenerator, SchemaBuil
2294
2343
  case 'uri':
2295
2344
  case 'url':
2296
2345
  stringSchema = ts.factory.createCallExpression(
2297
- ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('url')),
2346
+ ts.factory.createPropertyAccessExpression(
2347
+ ts.factory.createIdentifier('z'),
2348
+ ts.factory.createIdentifier('url'),
2349
+ ),
2298
2350
  undefined,
2299
2351
  [],
2300
2352
  );
2301
2353
  break;
2302
2354
  case 'uuid':
2303
2355
  stringSchema = ts.factory.createCallExpression(
2304
- ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('uuid')),
2356
+ ts.factory.createPropertyAccessExpression(
2357
+ ts.factory.createIdentifier('z'),
2358
+ ts.factory.createIdentifier('uuid'),
2359
+ ),
2305
2360
  undefined,
2306
2361
  [],
2307
2362
  );
2308
2363
  break;
2309
2364
  case 'date-time':
2310
2365
  stringSchema = ts.factory.createCallExpression(
2311
- ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('datetime')),
2366
+ ts.factory.createPropertyAccessExpression(
2367
+ ts.factory.createPropertyAccessExpression(
2368
+ ts.factory.createIdentifier('z'),
2369
+ ts.factory.createIdentifier('iso'),
2370
+ ),
2371
+ ts.factory.createIdentifier('datetime'),
2372
+ ),
2312
2373
  undefined,
2313
- [],
2374
+ [this.buildDefaultValue({local: true})],
2314
2375
  );
2315
2376
  break;
2316
2377
  case 'date':
2317
2378
  stringSchema = ts.factory.createCallExpression(
2318
- ts.factory.createPropertyAccessExpression(stringSchema, ts.factory.createIdentifier('date')),
2379
+ ts.factory.createPropertyAccessExpression(
2380
+ ts.factory.createPropertyAccessExpression(
2381
+ ts.factory.createIdentifier('z'),
2382
+ ts.factory.createIdentifier('iso'),
2383
+ ),
2384
+ ts.factory.createIdentifier('date'),
2385
+ ),
2319
2386
  undefined,
2320
2387
  [],
2321
2388
  );