wormajs 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/constant.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // 枚举常量定义 — 统一管理项目中所有硬编码的字符串/数字常量
4
4
  // ============================================================
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.FRAMEWORK_NAMES = exports.MODULE_TYPE_TO_KIND = exports.MODULE_TYPE_DIRS = exports.ModuleTypeDir = exports.PresetTemplateName = exports.TemplateSkipDir = exports.TemplatePlaceholder = exports.PluginName = exports.ModifierScope = exports.FilterScope = exports.RenameScope = exports.ParameterIn = exports.TEMPLATE_EXTENSIONS = exports.FileExtension = exports.FrameworkName = exports.ModuleKind = exports.PlatformTypeEnum = exports.ConfigTypeEnum = exports.TemplateTypeEnum = void 0;
6
+ exports.FRAMEWORK_NAMES = exports.MODULE_TYPE_TO_KIND = exports.MODULE_TYPE_DIRS = exports.ModuleTypeDir = exports.PresetTemplateName = exports.TemplateSkipDir = exports.TemplatePlaceholder = exports.PluginName = exports.FilterScope = exports.RenameScope = exports.ParameterIn = exports.TEMPLATE_EXTENSIONS = exports.FileExtension = exports.FrameworkName = exports.ModuleKind = exports.PlatformTypeEnum = exports.ConfigTypeEnum = exports.TemplateTypeEnum = void 0;
7
7
  exports.getTypeFileExtension = getTypeFileExtension;
8
8
  /** 模板代码生成类型 */
9
9
  var TemplateTypeEnum;
@@ -86,14 +86,6 @@ var FilterScope;
86
86
  FilterScope["URL"] = "url";
87
87
  FilterScope["TAG"] = "tag";
88
88
  })(FilterScope || (exports.FilterScope = FilterScope = {}));
89
- /** 参数修改 scope 类型 */
90
- var ModifierScope;
91
- (function (ModifierScope) {
92
- ModifierScope["PARAMS"] = "params";
93
- ModifierScope["PATH_PARAMS"] = "pathParams";
94
- ModifierScope["DATA"] = "data";
95
- ModifierScope["RESPONSE"] = "response";
96
- })(ModifierScope || (exports.ModifierScope = ModifierScope = {}));
97
89
  /** 内建插件名称 */
98
90
  var PluginName;
99
91
  (function (PluginName) {
@@ -13,7 +13,6 @@ const swagger2openapi_1 = __importDefault(require("swagger2openapi"));
13
13
  const poolManager_1 = require("../../../core/workerPool/poolManager");
14
14
  const helper_1 = require("../../../helper");
15
15
  const utils_1 = require("../../../utils");
16
- const supportedExtname = ['json', 'yaml'];
17
16
  function isSwagger2(data) {
18
17
  return !!data?.swagger;
19
18
  }
@@ -61,56 +60,72 @@ function convertSwagger2Async(data) {
61
60
  });
62
61
  });
63
62
  }
64
- // Parse local openapi files
65
- async function parseLocalFile(url, projectPath = process.cwd()) {
66
- const [, extname] = /\.([^.]+)$/.exec(url) ?? [];
67
- if (!supportedExtname.includes(extname)) {
68
- throw helper_1.logger.throwError(`Unsupported file type: ${extname}`, {
69
- url,
70
- projectPath,
71
- });
72
- }
63
+ // Read local openapi spec file as raw text
64
+ async function fetchRawLocalFile(url, projectPath = process.cwd()) {
73
65
  const filePath = node_path_1.default.resolve(projectPath, url);
74
- if (extname === 'yaml') {
75
- return js_yaml_1.default.load(await promises_1.default.readFile(filePath, 'utf-8'));
76
- }
77
- // M6-C3: prefer async read, fallback to require() for environments where
78
- // fs is mocked (e.g. memfs in tests) but the real file exists on disk
79
66
  try {
80
- return JSON.parse(await promises_1.default.readFile(filePath, 'utf-8'));
67
+ return await promises_1.default.readFile(filePath, 'utf-8');
81
68
  }
82
69
  catch {
83
- // Fallback: use require() which bypasses fs mocks to reach real disk
70
+ // M6-C3: fallback to require() for environments where fs is mocked
71
+ // (e.g. memfs in tests) but the real file exists on disk. Limited to JSON.
84
72
  // eslint-disable-next-line ts/no-require-imports
85
- return require(filePath);
73
+ return JSON.stringify(require(filePath), null, 2);
86
74
  }
87
75
  }
88
- // Parse remote openapi files
89
- async function parseRemoteFile(url, fetchOptions) {
90
- const dataText = (await (0, utils_1.fetchData)(url, fetchOptions)) ?? '';
91
- let data;
92
- try {
93
- // 尝试解析为 JSON 格式
94
- data = JSON.parse(dataText);
76
+ // Fetch remote openapi spec as raw text
77
+ async function fetchRawRemoteFile(url, fetchOptions) {
78
+ return (await (0, utils_1.fetchData)(url, fetchOptions)) ?? '';
79
+ }
80
+ const isRemoteUrl = (u) => /^https?:\/\//.test(u);
81
+ /**
82
+ * Fetch the raw spec text (JSON/YAML) from the first URL that succeeds.
83
+ * Returns the raw text together with the resolved URL; throws if all URLs fail.
84
+ */
85
+ async function fetchRawText(urls, options) {
86
+ if (urls.length === 0) {
87
+ throw helper_1.logger.throwError('No URLs provided to fetch OpenAPI document');
95
88
  }
96
- catch (jsonError) {
97
- try {
98
- // JSON 解析失败,尝试解析为 YAML 格式
99
- data = js_yaml_1.default.load(dataText);
100
- }
101
- catch (yamlError) {
102
- throw helper_1.logger.throwError(`Only JSON and YAML formats are supported. Parsing failed:
103
- ${jsonError instanceof Error ? jsonError.message : String(jsonError)}
104
- ${yamlError instanceof Error ? yamlError.message : String(yamlError)}`, {
105
- url,
106
- });
107
- }
89
+ const { projectPath, fetchOptions } = options;
90
+ // All URLs race in parallel: local files are fast, remote ones use network.
91
+ // Each task fetches the raw text AND validates that it is actually an OpenAPI/Swagger
92
+ // document before resolving. Invalid candidates (e.g. an HTML error page returned by
93
+ // a fallback URL) reject, so that Promise.any falls through to the next URL
94
+ // instead of resolving with garbage text.
95
+ const tasks = urls.map((u) => {
96
+ return (async () => {
97
+ const text = isRemoteUrl(u)
98
+ ? await fetchRawRemoteFile(u, fetchOptions)
99
+ : await fetchRawLocalFile(u, projectPath);
100
+ // Quick parse + validity check (full parse + Swagger2→OpenAPI3 conversion
101
+ // happens later in parseSpec, after beforeSpecParse may rewrite the text).
102
+ let probe;
103
+ try {
104
+ probe = JSON.parse(text);
105
+ }
106
+ catch (jsonError) {
107
+ try {
108
+ probe = js_yaml_1.default.load(text);
109
+ }
110
+ catch (yamlError) {
111
+ throw new Error(`${u}: ${(jsonError instanceof Error ? jsonError.message : String(jsonError))} (YAML: ${yamlError instanceof Error ? yamlError.message : String(yamlError)})`);
112
+ }
113
+ }
114
+ if (!isValidOpenApiData(probe)) {
115
+ throw new Error(`${u} did not yield a valid OpenAPI/Swagger document`);
116
+ }
117
+ return { text, url: u };
118
+ })();
119
+ });
120
+ try {
121
+ return await Promise.any(tasks);
108
122
  }
109
- // Validate if the data is valid (prevent server from returning error responses)
110
- if (!isValidOpenApiData(data)) {
111
- throw new Error(`Data retrieved from URL ${url} is not a valid OpenAPI document`);
123
+ catch (err) {
124
+ const errors = (err instanceof AggregateError)
125
+ ? err.errors.map((e) => e.message)
126
+ : [err.message];
127
+ throw helper_1.logger.throwError(`Unable to retrieve valid OpenAPI document from any URL:\n${errors.join('\n')}`);
112
128
  }
113
- return data;
114
129
  }
115
130
  // Validate OpenAPI data
116
131
  function isValidOpenApiData(data) {
@@ -124,49 +139,56 @@ function isValidOpenApiData(data) {
124
139
  // Check if it contains required OpenAPI/Swagger structure
125
140
  return !!(data.openapi || data.swagger || data.info || data.paths);
126
141
  }
127
- const isRemoteUrl = (u) => /^https?:\/\//.test(u);
128
142
  /**
129
- * Try all URLs in parallel (local & remote) first successful one wins.
130
- * Returns the parsed data together with the resolved URL; throws if all URLs fail.
143
+ * Parse a raw spec string (JSON or YAML) into an OpenAPIDocument.
144
+ * Performs format auto-detection, validity checks, and Swagger2→OpenAPI3 conversion.
131
145
  */
132
- async function tryUrls(urls, options) {
133
- if (urls.length === 0) {
134
- throw helper_1.logger.throwError('No URLs provided to fetch OpenAPI document');
146
+ async function parseSpec(text, url) {
147
+ let data;
148
+ try {
149
+ // Try to parse as JSON first
150
+ data = JSON.parse(text);
135
151
  }
136
- const { projectPath, fetchOptions } = options;
137
- // All URLs race in parallel: local files are fast, remote ones use network
138
- const tasks = urls.map((u) => {
139
- if (isRemoteUrl(u)) {
140
- return parseRemoteFile(u, fetchOptions).then(data => ({ data, url: u }), (err) => {
141
- throw new Error(`${u}: ${err instanceof Error ? err.message : String(err)}`);
152
+ catch (jsonError) {
153
+ try {
154
+ // Fall back to YAML (also covers JSON, since JSON is a subset of YAML)
155
+ data = js_yaml_1.default.load(text);
156
+ }
157
+ catch (yamlError) {
158
+ throw helper_1.logger.throwError(`Only JSON and YAML formats are supported. Parsing failed:
159
+ ${jsonError instanceof Error ? jsonError.message : String(jsonError)}
160
+ ${yamlError instanceof Error ? yamlError.message : String(yamlError)}`, {
161
+ url,
142
162
  });
143
163
  }
144
- return parseLocalFile(u, projectPath).then(data => ({ data, url: u }));
145
- });
146
- try {
147
- return await Promise.any(tasks);
148
164
  }
149
- catch (err) {
150
- const errors = (err instanceof AggregateError)
151
- ? err.errors.map((e) => e.message)
152
- : [err.message];
153
- throw helper_1.logger.throwError(`Unable to retrieve valid OpenAPI document from any URL:\n${errors.join('\n')}`);
165
+ // Validate if the data is valid (prevent server from returning error responses)
166
+ if (!isValidOpenApiData(data)) {
167
+ throw new Error(`Data retrieved from URL ${url} is not a valid OpenAPI document`);
168
+ }
169
+ // If it is a swagger2 file convert via worker to avoid main-thread blocking
170
+ if (isSwagger2(data)) {
171
+ data = await convertSwagger2Async(data);
154
172
  }
173
+ return data;
155
174
  }
156
175
  /**
157
176
  * Parse OpenAPI document and return the resolved URL alongside the data.
158
177
  * Use this when you need to know which URL actually provided the document.
178
+ *
179
+ * If `beforeSpecParse` is provided, it is invoked with the raw spec text once
180
+ * it has been fetched (but before parsing), and its returned string replaces
181
+ * the text that will be parsed.
159
182
  */
160
183
  async function getOpenApiDataWithUrl(url, options) {
161
- const { projectPath, fetchOptions } = options ?? {};
184
+ const { projectPath, fetchOptions, beforeSpecParse } = options ?? {};
162
185
  // Normalize to array — single string or array both handled uniformly
163
186
  const urls = Array.isArray(url) ? url : [url];
164
- const { data, url: resolvedUrl } = await tryUrls(urls, { projectPath, fetchOptions });
165
- let result = data;
166
- // If it is a swagger2 file convert via worker to avoid main-thread blocking
167
- if (isSwagger2(result)) {
168
- result = await convertSwagger2Async(result);
169
- }
187
+ const { text, url: resolvedUrl } = await fetchRawText(urls, { projectPath, fetchOptions });
188
+ // Allow the caller (e.g. a `beforeSpecParse` plugin hook) to transform the
189
+ // raw spec text before it is parsed into an OpenAPIDocument.
190
+ const finalText = (beforeSpecParse ? await beforeSpecParse(text) : text) ?? text;
191
+ const result = await parseSpec(finalText, resolvedUrl);
170
192
  if (!result) {
171
193
  throw helper_1.logger.throwError(`Cannot read file from ${urls.join(', ')}`, {
172
194
  projectPath,
@@ -12,6 +12,10 @@ function extendsConfig(config, newConfig) {
12
12
  }
13
13
  // handleApi is a special case, we need to merge the functions
14
14
  if (typeof newValue === 'function' && typeof srcValue === 'function') {
15
+ // Avoid chaining the same function reference with itself (idempotency guard).
16
+ if (newValue === srcValue) {
17
+ return newValue;
18
+ }
15
19
  // chain the functions
16
20
  return (...args) => {
17
21
  const result = srcValue(...args);
package/dist/generate.js CHANGED
@@ -19,12 +19,11 @@ async function generate(config, options) {
19
19
  return [];
20
20
  const projectPath = options?.projectPath ?? process.cwd();
21
21
  const emit = options?.onProgress;
22
- // Load phase (shared, no per-gen events during load) plugins may modify generator configs
23
- helper_1.logger.debug('Loading config', { projectPath });
24
- await helper_1.configHelper.load(config, projectPath);
25
- // Use the processed generators from ConfigManager (after plugin hooks have run)
26
- const generators = helper_1.configHelper.getConfig().generator;
27
- helper_1.logger.debug('Config loaded', { generatorCount: generators.length });
22
+ // Each generate() call creates its own ConfigHelper / ConfigManager,
23
+ // so multiple concurrent calls never share mutable config state.
24
+ const helper = new helper_1.ConfigHelper();
25
+ await helper.load(config, projectPath);
26
+ const generators = helper.getConfig().generator;
28
27
  // Run all generators in parallel, each with its own ProgressTracker
29
28
  const results = await Promise.all(generators.map(async (gen, i) => {
30
29
  const generatorName = gen.input || `generator-${i}`;
@@ -1,30 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.configHelper = exports.ConfigHelper = void 0;
3
+ exports.ConfigHelper = void 0;
4
4
  const lodash_1 = require("lodash");
5
5
  const helper_1 = require("../../helper");
6
- const logger_1 = require("../../helper/logger");
7
6
  const ConfigManager_1 = require("./ConfigManager");
8
7
  const GeneratorHelper_1 = require("./GeneratorHelper");
9
8
  class ConfigHelper {
10
9
  constructor() {
11
- this.configManager = ConfigManager_1.ConfigManager.getInstance();
12
- }
13
- static getInstance() {
14
- if (!ConfigHelper.instance) {
15
- ConfigHelper.instance = new ConfigHelper();
16
- }
17
- return ConfigHelper.instance;
10
+ this.configManager = new ConfigManager_1.ConfigManager();
18
11
  }
19
12
  async load(config, projectPath = process.cwd(), tracker) {
20
13
  this.projectPath = projectPath;
21
- logger_1.logger.debug('ConfigHelper.load — loading config manager', { projectPath, generatorCount: config.generator?.length ?? 0 });
22
14
  await this.configManager.load(config, projectPath, tracker);
23
- logger_1.logger.debug('ConfigHelper.load — reading cache data');
24
15
  await this.readAlovaJson();
25
- logger_1.logger.debug('ConfigHelper.load — complete');
26
16
  }
27
- async readUserConfig(userConfig) {
17
+ static async readUserConfig(userConfig) {
28
18
  if (typeof userConfig === 'function') {
29
19
  return await userConfig();
30
20
  }
@@ -76,4 +66,3 @@ class ConfigHelper {
76
66
  }
77
67
  }
78
68
  exports.ConfigHelper = ConfigHelper;
79
- exports.configHelper = ConfigHelper.getInstance();
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.configManager = exports.ConfigManager = void 0;
6
+ exports.ConfigManager = void 0;
7
7
  const zod_validation_error_1 = require("zod-validation-error");
8
8
  const prepareConfig_1 = __importDefault(require("../../functions/prepareConfig"));
9
9
  const GeneratorHelper_1 = require("../../helper/config/GeneratorHelper");
@@ -17,12 +17,6 @@ class ConfigManager {
17
17
  this.defaultGeneratorConfig = GeneratorHelper_1.generatorHelper.getDefaultConfig();
18
18
  this.config = this.defaultConfig;
19
19
  }
20
- static getInstance() {
21
- if (!ConfigManager.instance) {
22
- ConfigManager.instance = new ConfigManager();
23
- }
24
- return ConfigManager.instance;
25
- }
26
20
  /**
27
21
  * 加载并验证配置
28
22
  */
@@ -84,5 +78,3 @@ class ConfigManager {
84
78
  }
85
79
  }
86
80
  exports.ConfigManager = ConfigManager;
87
- // 导出单例实例
88
- exports.configManager = ConfigManager.getInstance();
@@ -128,10 +128,11 @@ class GeneratorHelper {
128
128
  fetchOptions: config.fetchOptions,
129
129
  });
130
130
  }
131
- static async openApiDataWithUrl(config, projectPath) {
131
+ static async openApiDataWithUrl(config, projectPath, opts) {
132
132
  return (0, helper_1.getOpenApiDataWithUrl)(config.input, {
133
133
  projectPath,
134
134
  fetchOptions: config.fetchOptions,
135
+ beforeSpecParse: opts?.beforeSpecParse,
135
136
  });
136
137
  }
137
138
  static async generate(config, { projectPath, force, tracker }) {
@@ -152,16 +153,17 @@ class GeneratorHelper {
152
153
  });
153
154
  reportCore(5, 'starting');
154
155
  const frozenConfig = Object.freeze(config);
155
- // Plugin: handle before parse openapi
156
- reportCore(10, 'beforeOpenapiParse');
157
- helper_2.logger.debug('Running beforeOpenapiParse hook');
158
- await pluginDriver.hookParallelEach('beforeOpenapiParse', () => ({
159
- config: frozenConfig,
160
- projectPath,
161
- }));
156
+ reportCore(10, 'beforeSpecParse');
157
+ helper_2.logger.debug('Fetching and parsing OpenAPI document', { input: config.input });
162
158
  reportCore(20, 'parsing openapi document');
163
- helper_2.logger.debug('Fetching OpenAPI document', { input: config.input });
164
- const openApiResult = await this.openApiDataWithUrl(config, projectPath);
159
+ const openApiResult = await this.openApiDataWithUrl(config, projectPath, {
160
+ // Plugin: beforeSpecParse receives the raw spec string, may return a modified string
161
+ beforeSpecParse: (spec) => pluginDriver.hookPipe('beforeSpecParse', spec, (_p, current, _ctx) => ({
162
+ config: frozenConfig,
163
+ spec: current,
164
+ projectPath,
165
+ })),
166
+ });
165
167
  let document = openApiResult.data;
166
168
  const resolvedInput = openApiResult.resolvedUrl;
167
169
  if (!document) {
@@ -174,10 +176,10 @@ class GeneratorHelper {
174
176
  version: document?.info?.version,
175
177
  paths: Object.keys(document?.paths || {}).length,
176
178
  });
177
- reportCore(35, 'openapi parsed');
178
- // Plugin: handle after parse openapi (openapiParsed)
179
- helper_2.logger.debug('Running openapiParsed hook', { pluginCount });
180
- const openapiParsed = await pluginDriver.hookSeqEach('openapiParsed', (_p, prevResult, _ctx) => {
179
+ reportCore(35, 'specParsed');
180
+ // Plugin: handle after parse openapi (specParsed)
181
+ helper_2.logger.debug('Running specParsed hook', { pluginCount });
182
+ const specParsed = await pluginDriver.hookSeqEach('specParsed', (_p, prevResult, _ctx) => {
181
183
  if (prevResult) {
182
184
  document = prevResult;
183
185
  }
@@ -187,11 +189,11 @@ class GeneratorHelper {
187
189
  projectPath,
188
190
  };
189
191
  });
190
- if (openapiParsed) {
191
- document = openapiParsed;
192
- helper_2.logger.debug('openapiParsed hook modified document');
192
+ if (specParsed) {
193
+ document = specParsed;
194
+ helper_2.logger.debug('specParsed hook modified document');
193
195
  }
194
- reportCore(45, 'openapiParsed');
196
+ reportCore(45, 'specParsed');
195
197
  const output = node_path_1.default.resolve(projectPath, config.output);
196
198
  const templateType = await GeneratorHelper.getTemplateType(config, projectPath);
197
199
  helper_2.logger.debug('Resolved output and template type', { output, templateType });
@@ -26,8 +26,8 @@ exports.zFetchOptions = v3_1.z.record(v3_1.z.string(), v3_1.z.any());
26
26
  exports.zApiPlugin = v3_1.z.object({
27
27
  name: v3_1.z.string().optional(),
28
28
  config: v3_1.z.function().optional(),
29
- beforeOpenapiParse: v3_1.z.function().optional(),
30
- openapiParsed: v3_1.z.function().optional(),
29
+ beforeSpecParse: v3_1.z.function().optional(),
30
+ specParsed: v3_1.z.function().optional(),
31
31
  beforeCodeGenerate: v3_1.z.function().optional(),
32
32
  beforeFileWrite: v3_1.z.function().optional(),
33
33
  codeGenerated: v3_1.z.function().optional(),
@@ -8,9 +8,9 @@ exports.CORE_PROGRESS_SOURCE = 'core';
8
8
  */
9
9
  exports.GeneratorStage = {
10
10
  INIT: 'init',
11
- BEFORE_OPENAPI_PARSE: 'beforeOpenapiParse',
11
+ BEFORE_SPEC_PARSE: 'beforeSpecParse',
12
12
  PARSE_OPENAPI: 'parseOpenapi',
13
- OPENAPI_PARSED: 'openapiParsed',
13
+ SPEC_PARSED: 'specParsed',
14
14
  TEMPLATE_LOADED: 'templateLoaded',
15
15
  TEMPLATE_DATA_PARSED: 'templateDataParsed',
16
16
  BEFORE_CODE_GENERATE: 'beforeCodeGenerate',
@@ -1,8 +1,73 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.applyModifierSchema = applyModifierSchema;
4
+ // Detect a `SchemaOptional` wrapper: { required: boolean, type: Schema }.
5
+ // The `required` must be a literal boolean so a plain SchemaReference whose
6
+ // property happens to be named "required" (e.g. { required: 'boolean' }) is not misread.
7
+ function isSchemaOptional(val) {
8
+ return !!val
9
+ && typeof val === 'object'
10
+ && !Array.isArray(val)
11
+ && typeof val.required === 'boolean'
12
+ && 'type' in val;
13
+ }
14
+ // Collapse (possibly nested) `SchemaOptional` wrappers.
15
+ // - The OUTERMOST `required` wins; inner `required` fields are ignored.
16
+ // - A non-wrapped value defaults to required (required: true).
17
+ function unwrapOptional(s) {
18
+ if (!isSchemaOptional(s)) {
19
+ return { required: true, type: s };
20
+ }
21
+ const required = s.required;
22
+ let type = s.type;
23
+ while (isSchemaOptional(type)) {
24
+ type = type.type;
25
+ }
26
+ return { required, type };
27
+ }
28
+ // Remove the internal `_$ref` marker that `removeAll$ref` stamps onto dereferenced
29
+ // component schemas. When a handler replaces a schema, the result must NOT inherit the
30
+ // original component's `_$ref` — otherwise `mergeObject`/`removeBaseReference` downstream
31
+ // treats the replacement as a reference to the original component and discards the change.
32
+ function stripInternalRef(schema) {
33
+ if (!schema || typeof schema !== 'object') {
34
+ return schema;
35
+ }
36
+ if (Array.isArray(schema)) {
37
+ return schema.map(stripInternalRef);
38
+ }
39
+ const out = {};
40
+ for (const key of Object.keys(schema)) {
41
+ if (key === '_$ref') {
42
+ continue;
43
+ }
44
+ out[key] = stripInternalRef(schema[key]);
45
+ }
46
+ return out;
47
+ }
48
+ // Set of valid SchemaPrimitive values for O(1) validation lookup
49
+ const VALID_PRIMITIVES = new Set([
50
+ 'number',
51
+ 'string',
52
+ 'boolean',
53
+ 'undefined',
54
+ 'null',
55
+ 'unknown',
56
+ 'any',
57
+ 'never',
58
+ ]);
59
+ function validatePrimitive(val) {
60
+ if (!VALID_PRIMITIVES.has(val)) {
61
+ throw new Error(`[payloadModifier] Invalid schema type "${val}". Must be one of: ${[...VALID_PRIMITIVES].join(', ')}`);
62
+ }
63
+ }
4
64
  // Convert Schema (custom spec) -> OpenAPI SchemaObject
5
65
  function toSchemaObject(base, s) {
66
+ // A `SchemaOptional` wrapper only affects requiredness (handled by the caller);
67
+ // here we care about the type shape, so fully unwrap nested wrappers first.
68
+ if (isSchemaOptional(s)) {
69
+ s = unwrapOptional(s).type;
70
+ }
6
71
  const result = { ...base };
7
72
  const cleanType = (schema) => {
8
73
  delete schema.type;
@@ -15,15 +80,18 @@ function toSchemaObject(base, s) {
15
80
  delete schema.required;
16
81
  return schema;
17
82
  };
18
- // Legacy union as array (treated as oneOf)
83
+ // Native array type (elements are Schema)
19
84
  if (Array.isArray(s)) {
20
- const baseOneOf = base.oneOf || [];
85
+ const arr = s;
21
86
  cleanType(result);
22
- result.oneOf = s.map((item, idx) => toSchemaObject(baseOneOf[idx] || {}, item));
87
+ result.type = 'array';
88
+ const items = arr.map(item => toSchemaObject({}, item));
89
+ result.items = (items.length === 1 ? items[0] : items);
23
90
  return result;
24
91
  }
25
- // Primitive types and no-op primitives
92
+ // Primitive types validate against SchemaPrimitive set during conversion
26
93
  if (typeof s === 'string') {
94
+ validatePrimitive(s);
27
95
  result.type = s;
28
96
  return result;
29
97
  }
@@ -33,64 +101,68 @@ function toSchemaObject(base, s) {
33
101
  const baseOneOf = base.oneOf || [];
34
102
  cleanType(result);
35
103
  result.oneOf = spec.oneOf.map((item, idx) => toSchemaObject(baseOneOf[idx] || {}, item));
104
+ return result;
36
105
  }
37
106
  if (s.anyOf) {
38
107
  const spec = s;
39
108
  const baseAnyOf = base.anyOf || [];
40
109
  cleanType(result);
41
110
  result.anyOf = spec.anyOf.map((item, idx) => toSchemaObject(baseAnyOf[idx] || {}, item));
111
+ return result;
42
112
  }
43
113
  if (s.allOf) {
44
114
  const spec = s;
45
115
  const baseAllOf = base.allOf || [];
46
116
  cleanType(result);
47
117
  result.allOf = spec.allOf.map((item, idx) => toSchemaObject(baseAllOf[idx] || {}, item));
118
+ return result;
48
119
  }
49
120
  // Enum: set enum and optional type
50
121
  if (s.enum) {
51
122
  const spec = s;
52
123
  result.enum = spec.enum;
53
124
  if (spec.type) {
125
+ if (typeof spec.type === 'string') {
126
+ validatePrimitive(spec.type);
127
+ }
54
128
  result.type = spec.type;
55
129
  }
56
- }
57
- // Array: set/merge items
58
- if (s.type === 'array') {
59
- const spec = s;
60
- result.type = 'array';
61
- const baseItems = result.items;
62
- if (Array.isArray(spec.items)) {
63
- // Tuple items: replace entire items with tuple
64
- const items = spec.items.map(item => toSchemaObject({}, item));
65
- result.items = items;
66
- }
67
- else {
68
- // Single items: merge into existing items schema
69
- const patchItem = toSchemaObject(typeof baseItems === 'object' ? baseItems : {}, spec.items);
70
- result.items = patchItem;
71
- }
72
130
  return result;
73
131
  }
74
- // Object (reference-like map): merge properties and required
132
+ // Object (reference-like map): replace properties and required with handler's spec
133
+ // (the SchemaReference returned by the handler fully replaces this field, only keeping
134
+ // scalar fields like description from base)
75
135
  const ref = s;
76
136
  if (ref && typeof ref === 'object') {
77
137
  result.type = 'object';
78
- const properties = { ...result.properties };
79
- const requiredSet = new Set(Array.isArray(result.required) ? result.required : []);
138
+ const properties = {};
139
+ const requiredSet = new Set();
80
140
  for (const key in ref) {
81
141
  const val = ref[key];
82
142
  if (!val) {
83
143
  continue;
84
144
  }
85
- const optional = key.endsWith('?');
86
- const cleanKey = optional ? key.slice(0, -1) : key;
87
- const baseProp = properties[cleanKey];
88
- properties[cleanKey] = toSchemaObject(baseProp || {}, val);
89
- if (optional) {
90
- requiredSet.delete(cleanKey);
145
+ // SchemaOptional wrapper: optionality expressed via { required, type };
146
+ // bare value defaults to required. Nested wrappers are collapsed — outermost
147
+ // `required` wins, inner ones are ignored.
148
+ let isOptional;
149
+ let effectiveVal;
150
+ if (isSchemaOptional(val)) {
151
+ const { required, type } = unwrapOptional(val);
152
+ isOptional = !required;
153
+ effectiveVal = type;
154
+ }
155
+ else {
156
+ isOptional = false;
157
+ effectiveVal = val;
158
+ }
159
+ const baseProp = properties[key];
160
+ properties[key] = toSchemaObject(baseProp || {}, effectiveVal);
161
+ if (isOptional) {
162
+ requiredSet.delete(key);
91
163
  }
92
164
  else {
93
- requiredSet.add(cleanKey);
165
+ requiredSet.add(key);
94
166
  }
95
167
  }
96
168
  result.properties = properties;
@@ -137,16 +209,16 @@ function toSchemaSpec(obj) {
137
209
  const type = typeof obj.type === 'string' ? obj.type : undefined;
138
210
  return { enum: obj.enum, type };
139
211
  }
140
- // Array
212
+ // Array -> native array
141
213
  if (obj.type === 'array' || obj.items) {
142
214
  const items = obj.items;
143
215
  if (Array.isArray(items)) {
144
- return { type: 'array', items: items.map((it) => toSchemaSpec(it)) };
216
+ return items.map((it) => toSchemaSpec(it));
145
217
  }
146
218
  if (items) {
147
- return { type: 'array', items: toSchemaSpec(items) };
219
+ return [toSchemaSpec(items)];
148
220
  }
149
- return { type: 'array', items: 'unknown' };
221
+ return ['unknown'];
150
222
  }
151
223
  // Object
152
224
  if (obj.type === 'object' || obj.properties) {
@@ -155,12 +227,12 @@ function toSchemaSpec(obj) {
155
227
  const result = {};
156
228
  for (const key of Object.keys(properties)) {
157
229
  const spec = toSchemaSpec(properties[key]);
158
- const finalKey = requiredSet.has(key) ? key : `${key}?`;
159
- result[finalKey] = spec;
230
+ // Required fields are written bare; optional fields wrapped with SchemaOptional
231
+ result[key] = requiredSet.has(key) ? spec : { required: false, type: spec };
160
232
  }
161
233
  return result;
162
234
  }
163
- // type union as array
235
+ // type union as array -> oneOf
164
236
  if (Array.isArray(obj.type)) {
165
237
  const typeArr = obj.type;
166
238
  const mapped = typeArr.map(schemaTypeToPrimitiveType);
@@ -169,27 +241,48 @@ function toSchemaSpec(obj) {
169
241
  return schemaTypeToPrimitiveType(obj.type);
170
242
  }
171
243
  // Replace whole schema based on handler result (used for params/pathParams)
172
- function applyModifierSchema(schema, config, { required }) {
244
+ function applyModifierSchema(schema, config, { required, key }) {
173
245
  if (!schema || typeof schema !== 'object') {
174
- return schema;
246
+ return { required, schema: schema };
175
247
  }
176
248
  const cloned = { ...schema };
177
249
  const currentSpec = toSchemaSpec(cloned);
178
- const ret = config.handler(currentSpec);
250
+ // When the field is itself optional and is a primitive, wrap it as { required, type } before passing to handler
251
+ const handlerInput = (required === false && typeof currentSpec === 'string')
252
+ ? { required: false, type: currentSpec }
253
+ : currentSpec;
254
+ const ret = config.handler(handlerInput, key);
179
255
  if (!ret) {
180
256
  return {
181
257
  required,
182
- value: null,
258
+ schema: null,
183
259
  };
184
260
  }
185
- if (typeof ret === 'object' && 'required' in ret && 'value' in ret) {
261
+ // A returned SchemaOptional means changing requiredness (driven by the `type` field).
262
+ // Nested wrappers are collapsed: the outermost `required` wins, inner ones are ignored;
263
+ // `type` may be any Schema expression (primitive, object, array, union, ...).
264
+ if (isSchemaOptional(ret)) {
265
+ const { required: nextRequired, type } = unwrapOptional(ret);
266
+ let r = stripInternalRef(toSchemaObject(cloned, type));
267
+ // When handler explicitly sets required=false, propagate to object-level required array
268
+ // so all properties become nullable as semantically expected
269
+ if (!nextRequired && r && typeof r === 'object' && !Array.isArray(r)) {
270
+ const robj = r;
271
+ if (robj.type === 'object' && Array.isArray(robj.required) && robj.required.length > 0) {
272
+ r = { ...r, required: [] };
273
+ }
274
+ }
186
275
  return {
187
- required: !!(ret.required ?? required),
188
- value: toSchemaObject(cloned, ret.value),
276
+ required: nextRequired,
277
+ schema: r,
189
278
  };
190
279
  }
280
+ // Non-SchemaOptional return: handler explicitly provides a type value,
281
+ // so default to required=true (the handler had the chance to wrap with
282
+ // { required: false, type: ... } if it wanted to keep it optional).
283
+ const r = stripInternalRef(toSchemaObject(cloned, ret));
191
284
  return {
192
- required,
193
- value: toSchemaObject(cloned, ret),
285
+ required: true,
286
+ schema: r,
194
287
  };
195
288
  }
@@ -4,13 +4,58 @@ exports.payloadModifier = payloadModifier;
4
4
  const constant_1 = require("../../../constant");
5
5
  const utils_1 = require("../utils");
6
6
  const hepler_1 = require("./hepler");
7
- // Apply modifications to object properties (for data/response scopes)
7
+ // Convert parameters of a specific type (query/path) into an object schema
8
+ function parametersToSchema(parameters, type) {
9
+ if (!parameters || !Array.isArray(parameters)) {
10
+ return { type: 'object', properties: {}, required: [] };
11
+ }
12
+ const schema = { type: 'object', properties: {}, required: [] };
13
+ for (const param of parameters) {
14
+ if (param.in === type) {
15
+ ;
16
+ schema.properties[param.name] = param.schema;
17
+ if (param.required) {
18
+ ;
19
+ schema.required.push(param.name);
20
+ }
21
+ }
22
+ }
23
+ return schema;
24
+ }
25
+ // Convert an object schema back to parameters, keeping other types untouched
26
+ function schemaToParameters(parameters, schema, type) {
27
+ if (!parameters || !Array.isArray(parameters)) {
28
+ return parameters;
29
+ }
30
+ if (!schema || typeof schema !== 'object' || !schema.properties) {
31
+ return parameters.filter(param => param.in !== type);
32
+ }
33
+ const requiredSet = new Set(Array.isArray(schema.required) ? schema.required : []);
34
+ const newParameters = [];
35
+ for (const param of parameters) {
36
+ if (param.in !== type) {
37
+ newParameters.push(param);
38
+ continue;
39
+ }
40
+ const propSchema = schema.properties[param.name];
41
+ if (!propSchema) {
42
+ continue;
43
+ }
44
+ newParameters.push({
45
+ ...param,
46
+ schema: propSchema,
47
+ required: requiredSet.has(param.name),
48
+ });
49
+ }
50
+ return newParameters;
51
+ }
52
+ // Apply modifications to properties of an object schema (used when `match` is set)
8
53
  function modifySchemaProperties(schema, config) {
9
54
  if (!schema || typeof schema !== 'object') {
10
55
  return schema;
11
56
  }
12
57
  const targetSchema = { ...schema };
13
- // union recursively
58
+ // recurse into union keywords
14
59
  if (Array.isArray(targetSchema.oneOf)) {
15
60
  targetSchema.oneOf = targetSchema.oneOf.map(item => modifySchemaProperties(item, config));
16
61
  }
@@ -20,7 +65,7 @@ function modifySchemaProperties(schema, config) {
20
65
  if (Array.isArray(targetSchema.allOf)) {
21
66
  targetSchema.allOf = targetSchema.allOf.map(item => modifySchemaProperties(item, config));
22
67
  }
23
- // modify properties
68
+ // modify matched properties
24
69
  if (targetSchema.properties) {
25
70
  const props = { ...targetSchema.properties };
26
71
  let required = Array.isArray(targetSchema.required) ? [...targetSchema.required] : [];
@@ -28,13 +73,13 @@ function modifySchemaProperties(schema, config) {
28
73
  if (!(0, utils_1.isMatch)(key, config.match)) {
29
74
  continue;
30
75
  }
31
- const { required: requiredOverride, value: valueSchema } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key) });
76
+ const { required: requiredOverride, schema: schemaValue } = (0, hepler_1.applyModifierSchema)(props[key], config, { required: required.includes(key), key });
32
77
  required = required.filter(r => r !== key);
33
- if (!valueSchema) {
78
+ if (!schemaValue) {
34
79
  delete props[key];
35
80
  continue;
36
81
  }
37
- props[key] = valueSchema;
82
+ props[key] = schemaValue;
38
83
  if (requiredOverride) {
39
84
  required.push(key);
40
85
  }
@@ -44,56 +89,59 @@ function modifySchemaProperties(schema, config) {
44
89
  }
45
90
  return targetSchema;
46
91
  }
92
+ // Apply modifications to matched parameters (used when `match` is set for params/pathParams)
47
93
  function modifyParameters(parameters, type, config) {
48
94
  if (!parameters || !Array.isArray(parameters)) {
49
95
  return parameters;
50
96
  }
51
97
  return parameters.map((param) => {
52
- if (param.in === type) {
53
- if (!(0, utils_1.isMatch)(param.name, config.match)) {
54
- return param;
55
- }
56
- const { value: schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required });
57
- if (!schema) {
58
- return null;
59
- }
60
- return {
61
- ...param,
62
- schema,
63
- required,
64
- };
98
+ if (param.in !== type || !(0, utils_1.isMatch)(param.name, config.match)) {
99
+ return param;
100
+ }
101
+ const { schema, required } = (0, hepler_1.applyModifierSchema)(param.schema, config, { required: !!param.required, key: param.name });
102
+ if (!schema) {
103
+ return null;
65
104
  }
66
- return param;
105
+ return { ...param, schema, required };
67
106
  }).filter(item => item !== null);
68
107
  }
108
+ // Apply config to a parameter scope (params or pathParams)
109
+ function applyToParameters(parameters, type, config) {
110
+ if (!parameters)
111
+ return undefined;
112
+ if (config.match) {
113
+ return modifyParameters(parameters, type, config);
114
+ }
115
+ const schema = parametersToSchema(parameters, type);
116
+ const result = (0, hepler_1.applyModifierSchema)(schema, config, { required: false });
117
+ return schemaToParameters(parameters, result.schema, type);
118
+ }
119
+ // Apply config to a schema scope (data or response)
120
+ function applyToSchemaField(schema, config) {
121
+ if (!schema)
122
+ return undefined;
123
+ if (config.match) {
124
+ return modifySchemaProperties(schema, config);
125
+ }
126
+ return (0, hepler_1.applyModifierSchema)(schema, config, { required: false }).schema ?? undefined;
127
+ }
69
128
  function payloadModifierApiDescriptor(apiDescriptor, config) {
70
- if (!apiDescriptor) {
129
+ if (!apiDescriptor)
71
130
  return null;
72
- }
73
131
  const newDescriptor = { ...apiDescriptor };
74
132
  const { scope } = config;
75
133
  switch (scope) {
76
- case constant_1.ModifierScope.PARAMS:
77
- if (newDescriptor.parameters) {
78
- newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.QUERY, config);
79
- }
134
+ case 'params':
135
+ newDescriptor.parameters = applyToParameters(newDescriptor.parameters, constant_1.ParameterIn.QUERY, config);
80
136
  break;
81
- case constant_1.ModifierScope.PATH_PARAMS:
82
- if (newDescriptor.parameters) {
83
- newDescriptor.parameters = modifyParameters(newDescriptor.parameters, constant_1.ParameterIn.PATH, config);
84
- }
137
+ case 'pathParams':
138
+ newDescriptor.parameters = applyToParameters(newDescriptor.parameters, constant_1.ParameterIn.PATH, config);
85
139
  break;
86
- case constant_1.ModifierScope.DATA:
87
- if (newDescriptor.requestBody) {
88
- newDescriptor.requestBody = modifySchemaProperties(newDescriptor.requestBody, config);
89
- }
90
- break;
91
- case constant_1.ModifierScope.RESPONSE:
92
- if (newDescriptor.responses) {
93
- newDescriptor.responses = modifySchemaProperties(newDescriptor.responses, config);
94
- }
140
+ case 'data':
141
+ newDescriptor.requestBody = applyToSchemaField(newDescriptor.requestBody, config);
95
142
  break;
96
- default:
143
+ case 'response':
144
+ newDescriptor.responses = applyToSchemaField(newDescriptor.responses, config);
97
145
  break;
98
146
  }
99
147
  return newDescriptor;
@@ -131,6 +131,8 @@ function renameUrl(url, config, apiDescriptor) {
131
131
  * Recursively processes a schema (supports nested objects and arrays),
132
132
  * applying property renaming to nested objects.
133
133
  * @param schema current schema
134
+ * @param config rename configuration
135
+ * @param apiDescriptor API descriptor
134
136
  * @param level current nesting level (starts from 0)
135
137
  * @param scopeLabel used to identify the scope in duplicate-name errors
136
138
  */
@@ -158,6 +160,9 @@ function transformSchema(schema, config, apiDescriptor, level, scopeLabel) {
158
160
  }
159
161
  /**
160
162
  * Transforms object properties using the renaming rules (supports deep recursive renaming)
163
+ * @param obj object whose properties to transform
164
+ * @param config rename configuration
165
+ * @param apiDescriptor API descriptor
161
166
  * @param level current nesting level (starts from 0), passed through to the match function
162
167
  * @param scopeLabel used to identify the scope in duplicate-name errors
163
168
  */
@@ -38,6 +38,10 @@ function processApiTags(apiDescriptor, handler) {
38
38
  // Process each tag and filter out null/undefined results
39
39
  newDescriptor.tags = newDescriptor.tags
40
40
  .map((tag) => {
41
+ // Resolve the tag to keep when the modification cannot be applied:
42
+ // keep the original tag only if it is itself valid, otherwise drop it.
43
+ // This guarantees that the final output never contains an invalid tag.
44
+ const keepOriginalOrDrop = () => isValidTagName(tag) ? tag : null;
41
45
  try {
42
46
  // Call user provided handler function
43
47
  const modifiedTag = handler(tag);
@@ -47,12 +51,12 @@ function processApiTags(apiDescriptor, handler) {
47
51
  }
48
52
  // Validate if modified tag follows naming conventions
49
53
  if (!isValidTagName(modifiedTag)) {
50
- return tag; // Keep original tag if invalid
54
+ return keepOriginalOrDrop(); // Keep original tag if valid, otherwise drop
51
55
  }
52
56
  return modifiedTag.trim(); // Return trimmed modified tag
53
57
  }
54
58
  catch {
55
- return tag; // Return original tag on error
59
+ return keepOriginalOrDrop(); // Keep original tag if valid, otherwise drop on error
56
60
  }
57
61
  })
58
62
  .filter((tag) => tag != null); // Filter out null/undefined values
@@ -34,7 +34,6 @@ async function readConfig(projectPath = process.cwd()) {
34
34
  name: 'readConfig',
35
35
  });
36
36
  }
37
- await helper_1.configHelper.load(config, projectPath);
38
37
  return config;
39
38
  }
40
39
  // 获取用户已安装的依赖
@@ -67,10 +66,8 @@ async function readConfig(projectPath = process.cwd()) {
67
66
  finally {
68
67
  await (0, promises_1.unlink)(outfile);
69
68
  }
70
- const config = await helper_1.configHelper.readUserConfig(module.default || module);
71
- // Read the cache file and save it
72
- await helper_1.configHelper.load(config, projectPath);
73
- return helper_1.configHelper.getConfig();
69
+ const config = await helper_1.ConfigHelper.readUserConfig(module.default || module);
70
+ return config;
74
71
  }
75
72
  /**
76
73
  * Get cached API docs. Cache is self-describing — no config needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormajs",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "A modern OpenAPI code generator - Generate type-safe API clients from OpenAPI specs",
5
5
  "author": "worma",
6
6
  "license": "MIT",
@@ -54,12 +54,14 @@ export interface ConfigHookParams {
54
54
  projectPath: string;
55
55
  reportProgress: ReportProgress;
56
56
  }
57
- export interface BeforeOpenapiParseHookParams {
57
+ export interface BeforeSpecParseHookParams {
58
58
  config: Readonly<GeneratorConfig>;
59
+ /** The raw OpenAPI specification text (JSON or YAML), before it is parsed. */
60
+ spec: string;
59
61
  projectPath: string;
60
62
  reportProgress: ReportProgress;
61
63
  }
62
- export interface OpenapiParsedHookParams {
64
+ export interface SpecParsedHookParams {
63
65
  config: Readonly<GeneratorConfig>;
64
66
  document: OpenAPIDocument;
65
67
  projectPath: string;
@@ -140,14 +142,16 @@ export interface ApiPlugin {
140
142
  */
141
143
  config?: (params: ConfigHookParams) => MaybePromise<GeneratorConfig | undefined | null | void>;
142
144
  /**
143
- * Called before parsing the OpenAPI file.
145
+ * Called after the raw OpenAPI spec text is fetched but before it is parsed.
146
+ * Return a (possibly modified) string to replace the spec text that will be
147
+ * parsed. Returning nothing keeps the original spec text.
144
148
  */
145
- beforeOpenapiParse?: (params: BeforeOpenapiParseHookParams) => void;
149
+ beforeSpecParse?: (params: BeforeSpecParseHookParams) => MaybePromise<string | undefined | null | void>;
146
150
  /**
147
151
  * Manipulate the openapi document after parsing.
148
152
  * Returning null does NOT replacing anything.
149
153
  */
150
- openapiParsed?: (params: OpenapiParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
154
+ specParsed?: (params: SpecParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
151
155
  /**
152
156
  * Called before code generation. Mutate `params.data` directly to inject
153
157
  * configuration data (no longer returns a value).
@@ -493,7 +497,7 @@ export declare const logger: Logger$1;
493
497
  * @param projectPath The project path where the configuration file is located. The default value is `process.cwd()`.
494
498
  * @returns a promise instance that contains configuration object.
495
499
  */
496
- export declare function readConfig(projectPath?: string): Promise<Readonly<Config>>;
500
+ export declare function readConfig(projectPath?: string): Promise<Config>;
497
501
  /**
498
502
  * Get cached API docs. Cache is self-describing — no config needed.
499
503
  * In monorepo, pass ANY sub-package path; cache is always read from the unified cacheRoot.
@@ -42,12 +42,14 @@ export interface ConfigHookParams {
42
42
  projectPath: string;
43
43
  reportProgress: ReportProgress;
44
44
  }
45
- export interface BeforeOpenapiParseHookParams {
45
+ export interface BeforeSpecParseHookParams {
46
46
  config: Readonly<GeneratorConfig>;
47
+ /** The raw OpenAPI specification text (JSON or YAML), before it is parsed. */
48
+ spec: string;
47
49
  projectPath: string;
48
50
  reportProgress: ReportProgress;
49
51
  }
50
- export interface OpenapiParsedHookParams {
52
+ export interface SpecParsedHookParams {
51
53
  config: Readonly<GeneratorConfig>;
52
54
  document: OpenAPIDocument;
53
55
  projectPath: string;
@@ -128,14 +130,16 @@ export interface ApiPlugin {
128
130
  */
129
131
  config?: (params: ConfigHookParams) => MaybePromise<GeneratorConfig | undefined | null | void>;
130
132
  /**
131
- * Called before parsing the OpenAPI file.
133
+ * Called after the raw OpenAPI spec text is fetched but before it is parsed.
134
+ * Return a (possibly modified) string to replace the spec text that will be
135
+ * parsed. Returning nothing keeps the original spec text.
132
136
  */
133
- beforeOpenapiParse?: (params: BeforeOpenapiParseHookParams) => void;
137
+ beforeSpecParse?: (params: BeforeSpecParseHookParams) => MaybePromise<string | undefined | null | void>;
134
138
  /**
135
139
  * Manipulate the openapi document after parsing.
136
140
  * Returning null does NOT replacing anything.
137
141
  */
138
- openapiParsed?: (params: OpenapiParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
142
+ specParsed?: (params: SpecParsedHookParams) => MaybePromise<OpenAPIDocument | undefined | null | void>;
139
143
  /**
140
144
  * Called before code generation. Mutate `params.data` directly to inject
141
145
  * configuration data (no longer returns a value).
@@ -529,36 +533,31 @@ export interface ImportTypeOptions {
529
533
  export declare function importType(imports: Record<string, string[]>, options?: {
530
534
  files?: string[];
531
535
  }): ApiPlugin;
532
- declare enum ModifierScope {
533
- PARAMS = "params",
534
- PATH_PARAMS = "pathParams",
535
- DATA = "data",
536
- RESPONSE = "response"
537
- }
538
- export type SchemaPrimitive = "number" | "string" | "boolean" | "undefined" | "null" | "unknown" | "any" | "never" | ({} & string);
536
+ export type ModifierScope = "params" | "pathParams" | "data" | "response";
537
+ export type SchemaPrimitive = "number" | "string" | "boolean" | "undefined" | "null" | "unknown" | "any" | "never";
539
538
  /**
540
- * 表示数组类型
539
+ * Array type: a native JS array whose elements are Schemas.
540
+ * e.g. ['string'] means string[]; ['string', 'number'] means the tuple [string, number]
541
541
  */
542
- export interface SchemaArray {
543
- type: "array";
544
- items: Schema | Schema[];
545
- }
542
+ export type SchemaArray = Schema[];
546
543
  /**
547
- * 修改参数为引用类型
548
- * 在key末端添加上?表示为可选值
544
+ * Object/reference type.
545
+ * Required properties are written directly; optional properties are wrapped
546
+ * with the `SchemaOptional` form `{ required: false, type: Schema }`
547
+ * (consistent with how a standalone optional primitive is represented).
549
548
  */
550
549
  export interface SchemaReference {
551
550
  [attr: string]: Schema;
552
551
  }
553
552
  /**
554
- * 枚举类型表示
553
+ * Enum type representation.
555
554
  */
556
555
  export interface SchemaEnum {
557
556
  enum: Array<string | number | boolean | null>;
558
557
  type?: SchemaPrimitive;
559
558
  }
560
559
  /**
561
- * 组合类型表示(与/或/交叉)
560
+ * Composite types (oneOf / anyOf / allOf).
562
561
  */
563
562
  export interface SchemaOneOf {
564
563
  oneOf: Schema[];
@@ -570,31 +569,49 @@ export interface SchemaAllOf {
570
569
  allOf: Schema[];
571
570
  }
572
571
  /**
573
- * 数据Schema
574
- * SchemaArray表示类型数组,而数组表示“或”的意思
572
+ * Standalone primitive type that is itself optional (driven by the `type` field).
573
+ * Used in handler input/output to mean "this field is optional / make it optional".
574
+ */
575
+ export interface SchemaOptional {
576
+ required: boolean;
577
+ type: Schema;
578
+ }
579
+ /**
580
+ * The data Schema.
581
+ * - SchemaArray is a native array (elements are Schemas)
582
+ * - composite types use { oneOf | anyOf | allOf: Schema[] }
583
+ * - optional object properties are wrapped with `SchemaOptional` ({ required: false, type: Schema });
584
+ * a standalone optional primitive uses the same SchemaOptional wrapper
575
585
  */
576
- export type Schema = SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum | SchemaOneOf | SchemaAnyOf | SchemaAllOf | Array<SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum>;
577
- export interface ModifierConfig<T extends Schema> {
586
+ export type Schema = SchemaPrimitive | SchemaReference | SchemaArray | SchemaEnum | SchemaOneOf | SchemaAnyOf | SchemaAllOf | SchemaOptional;
587
+ export interface ModifierConfig {
578
588
  /**
579
- * 生效范围,表示处理哪个位置的参数
589
+ * The scope the modifier applies to (which parameter location to process).
580
590
  */
581
591
  scope: ModifierScope;
582
592
  /**
583
- * 匹配规则,只有匹配到的才会进行转换,不指定则转换全部
584
- * string:原参数名包含此string;RegExp:原参数名匹配此正则;函数时接收key并返回是否匹配的boolean值
593
+ * Match rule. Only matched fields are transformed; when omitted, all fields are transformed.
594
+ * - string: the original field name contains this string
595
+ * - RegExp: the original field name matches this pattern
596
+ * - function: receives the key and returns a boolean indicating a match
585
597
  */
586
598
  match?: string | RegExp | ((key: string) => boolean);
587
599
  /**
588
- * handler用于灵活修改参数类型值
589
- * @param schema Schema中的一种,由用户自行定义
590
- * @returns 返回多种参数,具体为:Schema表示修改的类型;{ required: boolean, value: Schema }表示可将当前值修改为是否必填;void | null | undefined表示移除当前字段
600
+ * handler flexibly modifies the parameter type value.
601
+ * @param schema the original field type, already converted to the user-facing Schema representation.
602
+ * When the field itself is optional and is a primitive, it is passed as { required: false, type: 'string' }.
603
+ * Narrow the type inside handler if needed (e.g. with a cast).
604
+ * @param key the matched field key. When `match` is omitted, the whole scope object is passed to the handler
605
+ * once and `key` is `undefined`; when `match` is set, `key` is the matched field name for each call.
606
+ * @returns Schema to change the type; { required: boolean, type: Schema } to change requiredness (driven by `type`);
607
+ * void | null | undefined to remove the field.
591
608
  */
592
- handler: (schema: T) => Schema | {
609
+ handler: (schema: Schema, key?: string) => Schema | {
593
610
  required: boolean;
594
- value: Schema;
611
+ type: Schema;
595
612
  } | void | null | undefined;
596
613
  }
597
- export type PayloadModifierConfig = ModifierConfig<Schema>;
614
+ export type PayloadModifierConfig = ModifierConfig;
598
615
  export declare function payloadModifier(configs: PayloadModifierConfig[]): ApiPlugin;
599
616
  /**
600
617
  * FastAPI platform plugin.