genoc 0.3.0 → 0.4.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.
Files changed (71) hide show
  1. package/README.md +31 -187
  2. package/dist/analyzer/analyze.d.ts +18 -0
  3. package/dist/analyzer/analyze.js +493 -0
  4. package/dist/analyzer/discriminator-registry.d.ts +108 -0
  5. package/dist/analyzer/discriminator-registry.js +209 -0
  6. package/dist/analyzer/path-analyzer.d.ts +43 -6
  7. package/dist/analyzer/path-analyzer.js +57 -84
  8. package/dist/analyzer/schema-mapper.d.ts +68 -6
  9. package/dist/analyzer/schema-mapper.js +299 -40
  10. package/dist/analyzer/types.d.ts +126 -0
  11. package/dist/cli/app.d.ts +4 -2
  12. package/dist/cli/app.js +23 -4
  13. package/dist/cli/config-loader.d.ts +20 -0
  14. package/dist/cli/config-loader.js +186 -0
  15. package/dist/cli/config-schema.d.ts +105 -0
  16. package/dist/cli/config-schema.js +183 -0
  17. package/dist/cli/impl.d.ts +9 -1
  18. package/dist/cli/impl.js +189 -36
  19. package/dist/generator/client-generator.d.ts +13 -14
  20. package/dist/generator/client-generator.js +45 -77
  21. package/dist/generator/contracts-generator.d.ts +11 -9
  22. package/dist/generator/contracts-generator.js +45 -491
  23. package/dist/generator/method-generator.d.ts +1 -1
  24. package/dist/generator/method-generator.js +10 -150
  25. package/dist/parser/ref-resolver.d.ts +1 -1
  26. package/dist/parser/ref-resolver.js +1 -2
  27. package/dist/parser/version/index.d.ts +4 -16
  28. package/dist/parser/version/index.js +3 -14
  29. package/dist/parser/version/v3.0/validator.d.ts +1 -1
  30. package/dist/parser/version/v3.1/validator.d.ts +17 -0
  31. package/dist/parser/version/v3.1/validator.js +126 -0
  32. package/dist/parser/version/validate.d.ts +9 -0
  33. package/dist/parser/version/validate.js +12 -0
  34. package/dist/parser/version/version-detector.d.ts +21 -0
  35. package/dist/parser/version/version-detector.js +19 -0
  36. package/dist/pipeline.d.ts +15 -0
  37. package/dist/pipeline.js +25 -0
  38. package/dist/types/client.d.ts +0 -12
  39. package/dist/utils/generator-helpers.d.ts +1 -24
  40. package/dist/utils/generator-helpers.js +11 -78
  41. package/dist/utils/operation-naming.d.ts +84 -0
  42. package/dist/utils/operation-naming.js +178 -0
  43. package/dist/utils/proxy.d.ts +2 -0
  44. package/dist/utils/proxy.js +16 -0
  45. package/dist/utils/string.d.ts +7 -0
  46. package/dist/utils/string.js +9 -0
  47. package/package.json +19 -15
  48. package/dist/index.d.ts +0 -9
  49. package/dist/index.js +0 -11
  50. package/dist/parser/validators.d.ts +0 -7
  51. package/dist/parser/validators.js +0 -79
  52. package/dist/parser/version/normalized-spec.d.ts +0 -199
  53. package/dist/parser/version/registry.d.ts +0 -28
  54. package/dist/parser/version/registry.js +0 -44
  55. package/dist/parser/version/v3.0/index.d.ts +0 -3
  56. package/dist/parser/version/v3.0/index.js +0 -3
  57. package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
  58. package/dist/parser/version/v3.0/normalizer.js +0 -389
  59. package/dist/parser/version/v3.0/strategy.d.ts +0 -27
  60. package/dist/parser/version/v3.0/strategy.js +0 -96
  61. package/dist/parser/version/v3.1/index.d.ts +0 -1
  62. package/dist/parser/version/v3.1/index.js +0 -1
  63. package/dist/parser/version/v3.1/strategy.d.ts +0 -42
  64. package/dist/parser/version/v3.1/strategy.js +0 -513
  65. package/dist/parser/version/v3.2/index.d.ts +0 -4
  66. package/dist/parser/version/v3.2/index.js +0 -4
  67. package/dist/parser/version/v3.2/strategy.d.ts +0 -39
  68. package/dist/parser/version/v3.2/strategy.js +0 -57
  69. package/dist/parser/version/version-strategy.d.ts +0 -31
  70. package/dist/parser/version/version-strategy.js +0 -1
  71. /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
package/dist/cli/app.js CHANGED
@@ -8,8 +8,9 @@ const command = buildCommand({
8
8
  outputDir: {
9
9
  kind: 'parsed',
10
10
  parse: String,
11
- brief: 'Directory to write generated files',
11
+ brief: 'Directory to write generated files (or set outputDir in the config file)',
12
12
  placeholder: 'dir',
13
+ optional: true,
13
14
  },
14
15
  methodNameStrategy: {
15
16
  kind: 'enum',
@@ -26,8 +27,8 @@ const command = buildCommand({
26
27
  },
27
28
  strictVersion: {
28
29
  kind: 'boolean',
29
- default: true,
30
- brief: 'Enable strict version checking',
30
+ brief: 'Warn when --spec-version mismatches the detected version (default: true)',
31
+ optional: true,
31
32
  },
32
33
  runtimeImportPath: {
33
34
  kind: 'parsed',
@@ -43,14 +44,32 @@ const command = buildCommand({
43
44
  optional: true,
44
45
  placeholder: 'url',
45
46
  },
47
+ config: {
48
+ kind: 'parsed',
49
+ parse: String,
50
+ brief: 'Path to a genoc config file (.genocrc.yml / .genocrc.json); skips discovery',
51
+ optional: true,
52
+ placeholder: 'path',
53
+ },
54
+ project: {
55
+ kind: 'parsed',
56
+ parse: String,
57
+ brief: 'Run only the named client from a multi-client config',
58
+ optional: true,
59
+ placeholder: 'name',
60
+ },
61
+ },
62
+ aliases: {
63
+ o: 'outputDir',
46
64
  },
47
65
  positional: {
48
66
  kind: 'tuple',
49
67
  parameters: [
50
68
  {
51
- brief: 'Path or URL to OpenAPI 3.0 or 3.1 spec (JSON/YAML)',
69
+ brief: 'Path or URL to OpenAPI 3.0 or 3.1 spec (JSON/YAML); omit to use a config file',
52
70
  parse: String,
53
71
  placeholder: 'spec',
72
+ optional: true,
54
73
  },
55
74
  ],
56
75
  },
@@ -0,0 +1,20 @@
1
+ import type { ParsedConfig } from './config-schema.js';
2
+ export interface LoadConfigFileOptions {
3
+ /** Explicit config file path (`--config`); skips discovery when set. */
4
+ explicitPath?: string;
5
+ /** Directory to start discovery from; defaults to `process.cwd()`. */
6
+ searchDir?: string;
7
+ }
8
+ export interface LoadedConfigFile {
9
+ config: ParsedConfig;
10
+ filepath: string;
11
+ }
12
+ /**
13
+ * Load the genoc config file. With `explicitPath`, loads exactly that file
14
+ * (skipping discovery). Otherwise discovers `.genocrc.yml` / `.genocrc.json`
15
+ * walking up from `searchDir` (default `process.cwd()`), stopping at the git
16
+ * boundary (fs root fallback). Returns `null` when no config file is found;
17
+ * throws `UserError` for parse errors, schema violations, empty documents,
18
+ * and duplicate resolved output dirs.
19
+ */
20
+ export declare function loadConfigFile(options?: LoadConfigFileOptions): Promise<LoadedConfigFile | null>;
@@ -0,0 +1,186 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { lilconfig } from 'lilconfig';
4
+ import { parse as parseYaml } from 'yaml';
5
+ import { isUrl } from '../utils/url.js';
6
+ import { parseConfig } from './config-schema.js';
7
+ import { UserError } from './errors.js';
8
+ const SEARCH_PLACES = ['.genocrc.yml', '.genocrc.json'];
9
+ const SUPPORTED_EXTENSIONS = ['.yml', '.json'];
10
+ function formatYamlError(filepath, err) {
11
+ const message = err instanceof Error ? err.message.split('\n')[0] : String(err);
12
+ // yaml@2 exposes the error mark via `linePos` (1-based), not `.line`.
13
+ const mark = err.linePos?.[0];
14
+ const location = mark === undefined ? '' : ` at line ${mark.line}, column ${mark.col}`;
15
+ return new UserError(`Failed to parse config file ${filepath}: YAML error${location}: ${message}`);
16
+ }
17
+ function yamlLoader(filepath, content) {
18
+ try {
19
+ return parseYaml(content);
20
+ }
21
+ catch (err) {
22
+ throw formatYamlError(filepath, err);
23
+ }
24
+ }
25
+ function jsonLoader(filepath, content) {
26
+ try {
27
+ return JSON.parse(content);
28
+ }
29
+ catch (err) {
30
+ throw new UserError(`Failed to parse config file ${filepath} as JSON: ${err.message}`);
31
+ }
32
+ }
33
+ function createSearcher(stopDir) {
34
+ return lilconfig('genoc', {
35
+ searchPlaces: SEARCH_PLACES,
36
+ loaders: {
37
+ '.yml': yamlLoader,
38
+ '.json': jsonLoader,
39
+ },
40
+ stopDir,
41
+ // Surface empty files as results so they become UserErrors instead of
42
+ // being silently skipped during the upward search.
43
+ ignoreEmptySearchPlaces: false,
44
+ cache: false,
45
+ });
46
+ }
47
+ /**
48
+ * Nearest directory at or above `searchDir` containing a `.git` marker — the
49
+ * inclusive stop boundary for discovery. Falls back to the fs root when no
50
+ * git repository is found.
51
+ */
52
+ function findGitBoundary(searchDir) {
53
+ let dir = searchDir;
54
+ for (;;) {
55
+ if (existsSync(path.join(dir, '.git'))) {
56
+ return dir;
57
+ }
58
+ const parent = path.dirname(dir);
59
+ if (parent === dir) {
60
+ return path.parse(searchDir).root;
61
+ }
62
+ dir = parent;
63
+ }
64
+ }
65
+ function resolvePathValue(value, configDir) {
66
+ if (isUrl(value) || path.isAbsolute(value)) {
67
+ return value;
68
+ }
69
+ return path.resolve(configDir, value);
70
+ }
71
+ function resolveConfigPaths(config, configDir) {
72
+ if ('clients' in config) {
73
+ const clients = {};
74
+ for (const [name, entry] of Object.entries(config.clients)) {
75
+ clients[name] = {
76
+ ...entry,
77
+ input: resolvePathValue(entry.input, configDir),
78
+ outputDir: resolvePathValue(entry.outputDir, configDir),
79
+ };
80
+ }
81
+ return { clients };
82
+ }
83
+ const flat = { ...config };
84
+ if (flat.input !== undefined) {
85
+ flat.input = resolvePathValue(flat.input, configDir);
86
+ }
87
+ if (flat.outputDir !== undefined) {
88
+ flat.outputDir = resolvePathValue(flat.outputDir, configDir);
89
+ }
90
+ return flat;
91
+ }
92
+ /**
93
+ * Re-validate unique `outputDir`s on resolved absolute paths. The schema-level
94
+ * check inside `parseConfig` compares raw strings only, so `./src/a` and
95
+ * `a/../src/a` pass it yet would clobber each other's fixed output filenames.
96
+ */
97
+ function validateResolvedOutputDirs(config) {
98
+ if (!('clients' in config)) {
99
+ return;
100
+ }
101
+ const byOutputDir = new Map();
102
+ for (const [name, entry] of Object.entries(config.clients)) {
103
+ const names = byOutputDir.get(entry.outputDir) ?? [];
104
+ names.push(name);
105
+ byOutputDir.set(entry.outputDir, names);
106
+ }
107
+ for (const [outputDir, names] of byOutputDir) {
108
+ if (names.length > 1) {
109
+ throw new UserError(`Invalid genocrc configuration: duplicate outputDir '${outputDir}' used by clients ${names
110
+ .map((name) => `'${name}'`)
111
+ .join(' and ')}; each client must have a unique output directory.`);
112
+ }
113
+ }
114
+ }
115
+ function finalizeLoadedConfig(rawConfig, filepath, isEmpty) {
116
+ if (isEmpty || rawConfig === null || rawConfig === undefined) {
117
+ throw new UserError(`Config file is empty: ${filepath}`);
118
+ }
119
+ const parsed = parseConfig(rawConfig);
120
+ const config = resolveConfigPaths(parsed, path.dirname(filepath));
121
+ validateResolvedOutputDirs(config);
122
+ return { config, filepath };
123
+ }
124
+ async function loadFromExplicitPath(explicitPath) {
125
+ const filepath = path.resolve(explicitPath);
126
+ let stats;
127
+ try {
128
+ stats = statSync(filepath);
129
+ }
130
+ catch {
131
+ throw new UserError(`Config file not found: ${filepath}`);
132
+ }
133
+ if (stats.isDirectory()) {
134
+ throw new UserError(`Config path is a directory, not a config file: ${filepath}`);
135
+ }
136
+ const ext = path.extname(filepath);
137
+ if (!SUPPORTED_EXTENSIONS.includes(ext)) {
138
+ throw new UserError(`Unsupported config file extension "${ext}": ${filepath}. ` +
139
+ `Supported extensions: ${SUPPORTED_EXTENSIONS.join(', ')}`);
140
+ }
141
+ const searcher = createSearcher(findGitBoundary(path.dirname(filepath)));
142
+ try {
143
+ const result = await searcher.load(filepath);
144
+ if (result === null) {
145
+ throw new UserError(`Failed to load config file: ${filepath}`);
146
+ }
147
+ return finalizeLoadedConfig(result.config, result.filepath, result.isEmpty === true);
148
+ }
149
+ catch (err) {
150
+ if (err instanceof UserError) {
151
+ throw err;
152
+ }
153
+ throw new UserError(`Failed to read config file ${filepath}: ${err.message}`);
154
+ }
155
+ }
156
+ async function discoverConfigFile(searchDir) {
157
+ const startDir = path.resolve(searchDir);
158
+ const searcher = createSearcher(findGitBoundary(startDir));
159
+ try {
160
+ const result = await searcher.search(startDir);
161
+ if (result === null || result.filepath === '') {
162
+ return null;
163
+ }
164
+ return finalizeLoadedConfig(result.config, result.filepath, result.isEmpty === true);
165
+ }
166
+ catch (err) {
167
+ if (err instanceof UserError) {
168
+ throw err;
169
+ }
170
+ throw new UserError(`Failed to load config file during discovery: ${err.message}`);
171
+ }
172
+ }
173
+ /**
174
+ * Load the genoc config file. With `explicitPath`, loads exactly that file
175
+ * (skipping discovery). Otherwise discovers `.genocrc.yml` / `.genocrc.json`
176
+ * walking up from `searchDir` (default `process.cwd()`), stopping at the git
177
+ * boundary (fs root fallback). Returns `null` when no config file is found;
178
+ * throws `UserError` for parse errors, schema violations, empty documents,
179
+ * and duplicate resolved output dirs.
180
+ */
181
+ export async function loadConfigFile(options = {}) {
182
+ if (options.explicitPath !== undefined) {
183
+ return loadFromExplicitPath(options.explicitPath);
184
+ }
185
+ return discoverConfigFile(options.searchDir ?? process.cwd());
186
+ }
@@ -0,0 +1,105 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Zod-based validation for the genoc config file (`.genocrc.yml` / `.genocrc.json`).
4
+ *
5
+ * Two mutually exclusive shapes:
6
+ * - flat (single client): the 7 generator option keys, all optional at the schema level
7
+ * (requiredness is positional: file config XOR CLI flags fill them in).
8
+ * - multi (multi client): ONLY a `clients` key at the root; each entry REQUIRES
9
+ * `input` + `outputDir`, with the 5 strategy options optional.
10
+ *
11
+ * `requesterModuleName` is deliberately excluded — it is a dead field and must not
12
+ * be advertised in the config file schema.
13
+ */
14
+ export declare const METHOD_NAME_STRATEGIES: readonly ["path-based", "operationId", "operationId-with-fallback"];
15
+ /** Flat (single client) config: the 7 option keys, all optional, nothing else allowed. */
16
+ export declare const flatConfigSchema: z.ZodObject<{
17
+ input: z.ZodOptional<z.ZodString>;
18
+ outputDir: z.ZodOptional<z.ZodString>;
19
+ methodNameStrategy: z.ZodOptional<z.ZodEnum<{
20
+ operationId: "operationId";
21
+ "operationId-with-fallback": "operationId-with-fallback";
22
+ "path-based": "path-based";
23
+ }>>;
24
+ specVersion: z.ZodOptional<z.ZodString>;
25
+ strictVersion: z.ZodOptional<z.ZodBoolean>;
26
+ runtimeImportPath: z.ZodOptional<z.ZodString>;
27
+ proxy: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$strict>;
29
+ /** One `clients` map entry: `input` + `outputDir` required, strategy options optional. */
30
+ declare const clientEntrySchema: z.ZodObject<{
31
+ methodNameStrategy: z.ZodOptional<z.ZodEnum<{
32
+ operationId: "operationId";
33
+ "operationId-with-fallback": "operationId-with-fallback";
34
+ "path-based": "path-based";
35
+ }>>;
36
+ specVersion: z.ZodOptional<z.ZodString>;
37
+ strictVersion: z.ZodOptional<z.ZodBoolean>;
38
+ runtimeImportPath: z.ZodOptional<z.ZodString>;
39
+ proxy: z.ZodOptional<z.ZodString>;
40
+ input: z.ZodString;
41
+ outputDir: z.ZodString;
42
+ }, z.core.$strict>;
43
+ /**
44
+ * Multi-client config: ONLY `clients` at the root.
45
+ *
46
+ * The `superRefine` below checks raw string values pre-resolution: duplicate
47
+ * `outputDir`s are detected by exact raw string equality. Path resolution to
48
+ * absolute paths happens later in the config loader, which may add stricter
49
+ * post-resolution checks; at the schema level only raw equality applies
50
+ * (e.g. `./src/a` and `a/../src/a` are distinct here).
51
+ */
52
+ export declare const multiClientConfigSchema: z.ZodObject<{
53
+ clients: z.ZodRecord<z.ZodString, z.ZodObject<{
54
+ methodNameStrategy: z.ZodOptional<z.ZodEnum<{
55
+ operationId: "operationId";
56
+ "operationId-with-fallback": "operationId-with-fallback";
57
+ "path-based": "path-based";
58
+ }>>;
59
+ specVersion: z.ZodOptional<z.ZodString>;
60
+ strictVersion: z.ZodOptional<z.ZodBoolean>;
61
+ runtimeImportPath: z.ZodOptional<z.ZodString>;
62
+ proxy: z.ZodOptional<z.ZodString>;
63
+ input: z.ZodString;
64
+ outputDir: z.ZodString;
65
+ }, z.core.$strict>>;
66
+ }, z.core.$strict>;
67
+ /** Union of both shapes. `parseConfig` dispatches on shape for precise error messages. */
68
+ export declare const configSchema: z.ZodUnion<readonly [z.ZodObject<{
69
+ input: z.ZodOptional<z.ZodString>;
70
+ outputDir: z.ZodOptional<z.ZodString>;
71
+ methodNameStrategy: z.ZodOptional<z.ZodEnum<{
72
+ operationId: "operationId";
73
+ "operationId-with-fallback": "operationId-with-fallback";
74
+ "path-based": "path-based";
75
+ }>>;
76
+ specVersion: z.ZodOptional<z.ZodString>;
77
+ strictVersion: z.ZodOptional<z.ZodBoolean>;
78
+ runtimeImportPath: z.ZodOptional<z.ZodString>;
79
+ proxy: z.ZodOptional<z.ZodString>;
80
+ }, z.core.$strict>, z.ZodObject<{
81
+ clients: z.ZodRecord<z.ZodString, z.ZodObject<{
82
+ methodNameStrategy: z.ZodOptional<z.ZodEnum<{
83
+ operationId: "operationId";
84
+ "operationId-with-fallback": "operationId-with-fallback";
85
+ "path-based": "path-based";
86
+ }>>;
87
+ specVersion: z.ZodOptional<z.ZodString>;
88
+ strictVersion: z.ZodOptional<z.ZodBoolean>;
89
+ runtimeImportPath: z.ZodOptional<z.ZodString>;
90
+ proxy: z.ZodOptional<z.ZodString>;
91
+ input: z.ZodString;
92
+ outputDir: z.ZodString;
93
+ }, z.core.$strict>>;
94
+ }, z.core.$strict>]>;
95
+ export type FlatConfig = z.infer<typeof flatConfigSchema>;
96
+ export type ClientEntry = z.infer<typeof clientEntrySchema>;
97
+ export type MultiClientConfig = z.infer<typeof multiClientConfigSchema>;
98
+ export type ParsedConfig = FlatConfig | MultiClientConfig;
99
+ /**
100
+ * Parse and validate a raw config file document (already deserialized from YAML/JSON).
101
+ * Returns the parsed config, or throws a single `UserError` whose message names the
102
+ * offending key path, the problem, and the allowed keys.
103
+ */
104
+ export declare function parseConfig(raw: unknown): ParsedConfig;
105
+ export {};
@@ -0,0 +1,183 @@
1
+ import { z } from 'zod';
2
+ import { UserError } from './errors.js';
3
+ /**
4
+ * Zod-based validation for the genoc config file (`.genocrc.yml` / `.genocrc.json`).
5
+ *
6
+ * Two mutually exclusive shapes:
7
+ * - flat (single client): the 7 generator option keys, all optional at the schema level
8
+ * (requiredness is positional: file config XOR CLI flags fill them in).
9
+ * - multi (multi client): ONLY a `clients` key at the root; each entry REQUIRES
10
+ * `input` + `outputDir`, with the 5 strategy options optional.
11
+ *
12
+ * `requesterModuleName` is deliberately excluded — it is a dead field and must not
13
+ * be advertised in the config file schema.
14
+ */
15
+ export const METHOD_NAME_STRATEGIES = [
16
+ 'path-based',
17
+ 'operationId',
18
+ 'operationId-with-fallback',
19
+ ];
20
+ const FLAT_OPTION_KEYS = [
21
+ 'input',
22
+ 'outputDir',
23
+ 'methodNameStrategy',
24
+ 'specVersion',
25
+ 'strictVersion',
26
+ 'runtimeImportPath',
27
+ 'proxy',
28
+ ];
29
+ const methodNameStrategySchema = z.enum(METHOD_NAME_STRATEGIES);
30
+ const baseOptionsSchema = z
31
+ .object({
32
+ input: z.string().optional(),
33
+ outputDir: z.string().optional(),
34
+ methodNameStrategy: methodNameStrategySchema.optional(),
35
+ specVersion: z.string().optional(),
36
+ strictVersion: z.boolean().optional(),
37
+ runtimeImportPath: z.string().optional(),
38
+ proxy: z.string().optional(),
39
+ })
40
+ .strict();
41
+ /** Flat (single client) config: the 7 option keys, all optional, nothing else allowed. */
42
+ export const flatConfigSchema = baseOptionsSchema;
43
+ /** One `clients` map entry: `input` + `outputDir` required, strategy options optional. */
44
+ const clientEntrySchema = baseOptionsSchema
45
+ .extend({ input: z.string(), outputDir: z.string() })
46
+ .strict();
47
+ const clientsSchema = z.record(z.string().min(1), clientEntrySchema);
48
+ /**
49
+ * Multi-client config: ONLY `clients` at the root.
50
+ *
51
+ * The `superRefine` below checks raw string values pre-resolution: duplicate
52
+ * `outputDir`s are detected by exact raw string equality. Path resolution to
53
+ * absolute paths happens later in the config loader, which may add stricter
54
+ * post-resolution checks; at the schema level only raw equality applies
55
+ * (e.g. `./src/a` and `a/../src/a` are distinct here).
56
+ */
57
+ export const multiClientConfigSchema = z
58
+ .object({ clients: clientsSchema })
59
+ .strict()
60
+ .superRefine((config, ctx) => {
61
+ const names = Object.keys(config.clients);
62
+ if (names.length === 0) {
63
+ ctx.addIssue({
64
+ code: 'custom',
65
+ message: "'clients' map must contain at least one client.",
66
+ path: ['clients'],
67
+ });
68
+ return;
69
+ }
70
+ const byOutputDir = new Map();
71
+ for (const [name, entry] of Object.entries(config.clients)) {
72
+ const list = byOutputDir.get(entry.outputDir) ?? [];
73
+ list.push(name);
74
+ byOutputDir.set(entry.outputDir, list);
75
+ }
76
+ for (const [outputDir, clientNames] of byOutputDir) {
77
+ if (clientNames.length > 1) {
78
+ ctx.addIssue({
79
+ code: 'custom',
80
+ message: `Duplicate outputDir '${outputDir}' used by clients ${clientNames
81
+ .map((name) => `'${name}'`)
82
+ .join(' and ')}; each client must have a unique output directory.`,
83
+ path: ['clients'],
84
+ });
85
+ }
86
+ }
87
+ });
88
+ /** Union of both shapes. `parseConfig` dispatches on shape for precise error messages. */
89
+ export const configSchema = z.union([flatConfigSchema, multiClientConfigSchema]);
90
+ const ROOT_ALLOWED_KEYS = [...FLAT_OPTION_KEYS, 'clients'];
91
+ function isRecord(value) {
92
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
93
+ }
94
+ function formatKeyPath(path) {
95
+ return path.length === 0 ? 'configuration root' : `'${path.map(String).join('.')}'`;
96
+ }
97
+ function allowedKeysFor(path) {
98
+ if (path.length >= 2 && path[0] === 'clients') {
99
+ return FLAT_OPTION_KEYS.join(', ');
100
+ }
101
+ return ROOT_ALLOWED_KEYS.join(', ');
102
+ }
103
+ function valueAtPath(raw, path) {
104
+ let current = raw;
105
+ for (const segment of path) {
106
+ if (!isRecord(current)) {
107
+ return undefined;
108
+ }
109
+ current = current[String(segment)];
110
+ }
111
+ return current;
112
+ }
113
+ function formatIssue(issue, parentPath, raw) {
114
+ const path = issue.path.length > 0 ? issue.path : parentPath;
115
+ if (issue.code === 'unrecognized_keys' && 'keys' in issue) {
116
+ const keys = issue.keys.map((key) => `'${key}'`).join(', ');
117
+ const plural = issue.keys.length > 1 ? 's' : '';
118
+ return `- unknown key${plural} ${keys} at ${formatKeyPath(path)}; allowed keys: ${allowedKeysFor(path)}`;
119
+ }
120
+ if (issue.code === 'invalid_key') {
121
+ return `- ${formatKeyPath(path)}: invalid client name; client names must be non-empty strings`;
122
+ }
123
+ if (issue.code === 'invalid_value') {
124
+ const received = valueAtPath(raw, path);
125
+ const receivedText = received === undefined ? '' : `; received '${String(received)}'`;
126
+ return `- ${formatKeyPath(path)}: ${issue.message}${receivedText}`;
127
+ }
128
+ return `- ${formatKeyPath(path)}: ${issue.message}`;
129
+ }
130
+ function formatIssues(issues, raw) {
131
+ const lines = [];
132
+ for (const issue of issues) {
133
+ if (issue.code === 'invalid_key') {
134
+ lines.push(formatIssue(issue, issue.path, raw));
135
+ continue;
136
+ }
137
+ const subIssues = issue.issues;
138
+ if (Array.isArray(subIssues) && subIssues.length > 0) {
139
+ for (const sub of subIssues) {
140
+ lines.push(formatIssue(sub, issue.path, raw));
141
+ }
142
+ }
143
+ else {
144
+ lines.push(formatIssue(issue, issue.path, raw));
145
+ }
146
+ }
147
+ return lines.join('\n');
148
+ }
149
+ function formatUserError(issues, raw) {
150
+ return new UserError(`Invalid genocrc configuration:\n${formatIssues(issues, raw)}`);
151
+ }
152
+ /**
153
+ * Parse and validate a raw config file document (already deserialized from YAML/JSON).
154
+ * Returns the parsed config, or throws a single `UserError` whose message names the
155
+ * offending key path, the problem, and the allowed keys.
156
+ */
157
+ export function parseConfig(raw) {
158
+ if (raw === null || raw === undefined || (isRecord(raw) && Object.keys(raw).length === 0)) {
159
+ throw new UserError('Invalid genocrc configuration: no configuration keys found. ' +
160
+ `Add flat options (${FLAT_OPTION_KEYS.join(', ')}) for a single client ` +
161
+ `or a 'clients' map for multiple clients.`);
162
+ }
163
+ if (isRecord(raw) && 'clients' in raw) {
164
+ const flatKeys = FLAT_OPTION_KEYS.filter((key) => key in raw);
165
+ if (flatKeys.length > 0) {
166
+ throw new UserError(`Invalid genocrc configuration: cannot mix flat options (${flatKeys
167
+ .map((key) => `'${key}'`)
168
+ .join(', ')}) with a 'clients' map. Use EITHER flat options ` +
169
+ `(${FLAT_OPTION_KEYS.join(', ')}) for a single client OR a 'clients' map ` +
170
+ 'for multiple clients — not both.');
171
+ }
172
+ const result = multiClientConfigSchema.safeParse(raw);
173
+ if (!result.success) {
174
+ throw formatUserError(result.error.issues, raw);
175
+ }
176
+ return result.data;
177
+ }
178
+ const result = flatConfigSchema.safeParse(raw);
179
+ if (!result.success) {
180
+ throw formatUserError(result.error.issues, raw);
181
+ }
182
+ return result.data;
183
+ }
@@ -1,3 +1,11 @@
1
1
  import type { CommandContext } from '@stricli/core';
2
2
  import type { AppFlags as Flags } from './app.js';
3
- export default function (this: CommandContext, flags: Flags, spec: string): Promise<void | Error>;
3
+ /**
4
+ * Display name for a target that does not come from a `clients` map entry:
5
+ * the spec filename base (`petstore.yaml` -> `petstore`, URL last segment for
6
+ * remote specs). Chosen over the outputDir basename because several targets
7
+ * can share an input spec, but the input is what identifies the client being
8
+ * generated in the failure/aggregation output.
9
+ */
10
+ export declare function targetNameFromInput(input: string): string;
11
+ export default function (this: CommandContext, flags: Flags, spec: string | undefined): Promise<void | Error>;