genoc 0.2.0 → 0.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.
- package/README.md +30 -163
- package/dist/analyzer/analyze.d.ts +18 -0
- package/dist/analyzer/analyze.js +534 -0
- package/dist/analyzer/naming.js +3 -2
- package/dist/analyzer/path-analyzer.d.ts +45 -6
- package/dist/analyzer/path-analyzer.js +82 -81
- package/dist/analyzer/schema-mapper.d.ts +3 -2
- package/dist/analyzer/schema-mapper.js +16 -8
- package/dist/analyzer/types.d.ts +119 -0
- package/dist/cli/app.d.ts +5 -2
- package/dist/cli/app.js +30 -4
- package/dist/cli/config-loader.d.ts +20 -0
- package/dist/cli/config-loader.js +186 -0
- package/dist/cli/config-schema.d.ts +105 -0
- package/dist/cli/config-schema.js +183 -0
- package/dist/cli/impl.d.ts +9 -1
- package/dist/cli/impl.js +190 -31
- package/dist/generator/client-generator.d.ts +13 -14
- package/dist/generator/client-generator.js +45 -77
- package/dist/generator/contracts-generator.d.ts +11 -9
- package/dist/generator/contracts-generator.js +45 -482
- package/dist/generator/method-generator.d.ts +1 -1
- package/dist/generator/method-generator.js +10 -150
- package/dist/parser/ref-resolver.d.ts +1 -1
- package/dist/parser/ref-resolver.js +1 -2
- package/dist/parser/spec-reader.d.ts +3 -2
- package/dist/parser/spec-reader.js +5 -4
- package/dist/parser/version/index.d.ts +4 -16
- package/dist/parser/version/index.js +3 -14
- package/dist/parser/version/v3.0/validator.d.ts +1 -1
- package/dist/parser/version/v3.1/validator.d.ts +17 -0
- package/dist/parser/version/v3.1/validator.js +126 -0
- package/dist/parser/version/validate.d.ts +9 -0
- package/dist/parser/version/validate.js +12 -0
- package/dist/parser/version/version-detector.d.ts +21 -0
- package/dist/parser/version/version-detector.js +19 -0
- package/dist/pipeline.d.ts +13 -0
- package/dist/pipeline.js +24 -0
- package/dist/types/client.d.ts +1 -12
- package/dist/utils/generator-helpers.d.ts +23 -25
- package/dist/utils/generator-helpers.js +144 -76
- package/dist/utils/operation-naming.d.ts +84 -0
- package/dist/utils/operation-naming.js +178 -0
- package/dist/utils/proxy.d.ts +6 -0
- package/dist/utils/proxy.js +81 -0
- package/package.json +5 -8
- package/dist/generator/error-types.d.ts +0 -13
- package/dist/generator/error-types.js +0 -67
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -8
- package/dist/parser/validators.d.ts +0 -7
- package/dist/parser/validators.js +0 -79
- package/dist/parser/version/normalized-spec.d.ts +0 -199
- package/dist/parser/version/registry.d.ts +0 -28
- package/dist/parser/version/registry.js +0 -44
- package/dist/parser/version/v3.0/index.d.ts +0 -3
- package/dist/parser/version/v3.0/index.js +0 -3
- package/dist/parser/version/v3.0/normalizer.d.ts +0 -15
- package/dist/parser/version/v3.0/normalizer.js +0 -389
- package/dist/parser/version/v3.0/strategy.d.ts +0 -27
- package/dist/parser/version/v3.0/strategy.js +0 -96
- package/dist/parser/version/v3.1/index.d.ts +0 -1
- package/dist/parser/version/v3.1/index.js +0 -1
- package/dist/parser/version/v3.1/strategy.d.ts +0 -42
- package/dist/parser/version/v3.1/strategy.js +0 -513
- package/dist/parser/version/v3.2/index.d.ts +0 -4
- package/dist/parser/version/v3.2/index.js +0 -4
- package/dist/parser/version/v3.2/strategy.d.ts +0 -39
- package/dist/parser/version/v3.2/strategy.js +0 -57
- package/dist/parser/version/version-strategy.d.ts +0 -31
- package/dist/parser/version/version-strategy.js +0 -1
- /package/dist/{parser/version/normalized-spec.js → analyzer/types.js} +0 -0
|
@@ -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
|
+
}
|
package/dist/cli/impl.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
import type { CommandContext } from '@stricli/core';
|
|
2
2
|
import type { AppFlags as Flags } from './app.js';
|
|
3
|
-
|
|
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>;
|