@lssm/lib.contracts-transformers 0.0.0-canary-20251217083314 → 1.41.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/dist/common/index.js +1 -3
- package/dist/common/utils.js +1 -102
- package/dist/index.js +1 -9
- package/dist/openapi/differ.js +2 -214
- package/dist/openapi/exporter.js +1 -150
- package/dist/openapi/importer.js +2 -264
- package/dist/openapi/index.js +1 -7
- package/dist/openapi/parser.js +1 -231
- package/dist/openapi/schema-converter.js +4 -201
- package/package.json +8 -9
- package/dist/common/index.d.ts +0 -3
- package/dist/common/types.d.ts +0 -152
- package/dist/common/utils.d.ts +0 -51
- package/dist/index.d.ts +0 -9
- package/dist/openapi/differ.d.ts +0 -41
- package/dist/openapi/exporter.d.ts +0 -27
- package/dist/openapi/importer.d.ts +0 -15
- package/dist/openapi/index.d.ts +0 -7
- package/dist/openapi/parser.d.ts +0 -31
- package/dist/openapi/schema-converter.d.ts +0 -69
- package/dist/openapi/types.d.ts +0 -221
package/dist/openapi/importer.js
CHANGED
|
@@ -1,264 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
//#region src/openapi/importer.ts
|
|
5
|
-
/**
|
|
6
|
-
* HTTP methods that typically indicate a command (state-changing).
|
|
7
|
-
*/
|
|
8
|
-
const COMMAND_METHODS = [
|
|
9
|
-
"post",
|
|
10
|
-
"put",
|
|
11
|
-
"delete",
|
|
12
|
-
"patch"
|
|
13
|
-
];
|
|
14
|
-
/**
|
|
15
|
-
* Determine if an operation is a command or query based on HTTP method.
|
|
16
|
-
*/
|
|
17
|
-
function inferOpKind(method) {
|
|
18
|
-
return COMMAND_METHODS.includes(method.toLowerCase()) ? "command" : "query";
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Determine auth level based on security requirements.
|
|
22
|
-
*/
|
|
23
|
-
function inferAuthLevel(operation, defaultAuth) {
|
|
24
|
-
if (!operation.security || operation.security.length === 0) return defaultAuth;
|
|
25
|
-
for (const sec of operation.security) if (Object.keys(sec).length === 0) return "anonymous";
|
|
26
|
-
return "user";
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Build a merged input schema from all parameter sources.
|
|
30
|
-
*/
|
|
31
|
-
function buildInputSchema(operation) {
|
|
32
|
-
const fields = [];
|
|
33
|
-
for (const param of operation.pathParams) fields.push({
|
|
34
|
-
name: param.name,
|
|
35
|
-
schema: param.schema,
|
|
36
|
-
required: true
|
|
37
|
-
});
|
|
38
|
-
for (const param of operation.queryParams) fields.push({
|
|
39
|
-
name: param.name,
|
|
40
|
-
schema: param.schema,
|
|
41
|
-
required: param.required
|
|
42
|
-
});
|
|
43
|
-
const excludedHeaders = [
|
|
44
|
-
"authorization",
|
|
45
|
-
"content-type",
|
|
46
|
-
"accept",
|
|
47
|
-
"user-agent"
|
|
48
|
-
];
|
|
49
|
-
for (const param of operation.headerParams) if (!excludedHeaders.includes(param.name.toLowerCase())) fields.push({
|
|
50
|
-
name: param.name,
|
|
51
|
-
schema: param.schema,
|
|
52
|
-
required: param.required
|
|
53
|
-
});
|
|
54
|
-
if (operation.requestBody?.schema) {
|
|
55
|
-
const bodySchema = operation.requestBody.schema;
|
|
56
|
-
if (!("$ref" in bodySchema)) {
|
|
57
|
-
const schemaObj = bodySchema;
|
|
58
|
-
const properties = schemaObj["properties"];
|
|
59
|
-
const required = schemaObj["required"] ?? [];
|
|
60
|
-
if (properties) for (const [propName, propSchema] of Object.entries(properties)) fields.push({
|
|
61
|
-
name: propName,
|
|
62
|
-
schema: propSchema,
|
|
63
|
-
required: required.includes(propName)
|
|
64
|
-
});
|
|
65
|
-
} else fields.push({
|
|
66
|
-
name: "body",
|
|
67
|
-
schema: bodySchema,
|
|
68
|
-
required: operation.requestBody.required
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
if (fields.length === 0) return {
|
|
72
|
-
schema: null,
|
|
73
|
-
fields: []
|
|
74
|
-
};
|
|
75
|
-
return {
|
|
76
|
-
schema: {
|
|
77
|
-
type: "object",
|
|
78
|
-
properties: fields.reduce((acc, f) => {
|
|
79
|
-
acc[f.name] = f.schema;
|
|
80
|
-
return acc;
|
|
81
|
-
}, {}),
|
|
82
|
-
required: fields.filter((f) => f.required).map((f) => f.name)
|
|
83
|
-
},
|
|
84
|
-
fields
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Get the output schema from the operation responses.
|
|
89
|
-
*/
|
|
90
|
-
function getOutputSchema(operation) {
|
|
91
|
-
for (const code of [
|
|
92
|
-
"200",
|
|
93
|
-
"201",
|
|
94
|
-
"202",
|
|
95
|
-
"204"
|
|
96
|
-
]) {
|
|
97
|
-
const response = operation.responses[code];
|
|
98
|
-
if (response?.schema) return response.schema;
|
|
99
|
-
}
|
|
100
|
-
for (const [code, response] of Object.entries(operation.responses)) if (code.startsWith("2") && response.schema) return response.schema;
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Generate ContractSpec TypeScript code for an operation.
|
|
105
|
-
*/
|
|
106
|
-
function generateSpecCode(operation, options, inputModel, outputModel) {
|
|
107
|
-
const specName = toSpecName(operation.operationId, options.prefix);
|
|
108
|
-
const kind = inferOpKind(operation.method);
|
|
109
|
-
const auth = inferAuthLevel(operation, options.defaultAuth ?? "user");
|
|
110
|
-
const lines = [];
|
|
111
|
-
lines.push("import { defineCommand, defineQuery } from '@lssm/lib.contracts';");
|
|
112
|
-
if (inputModel || outputModel) lines.push(generateImports([...inputModel?.fields ?? [], ...outputModel?.fields ?? []]));
|
|
113
|
-
lines.push("");
|
|
114
|
-
if (inputModel && inputModel.code) {
|
|
115
|
-
lines.push("// Input schema");
|
|
116
|
-
lines.push(inputModel.code);
|
|
117
|
-
lines.push("");
|
|
118
|
-
}
|
|
119
|
-
if (outputModel && outputModel.code) {
|
|
120
|
-
lines.push("// Output schema");
|
|
121
|
-
lines.push(outputModel.code);
|
|
122
|
-
lines.push("");
|
|
123
|
-
}
|
|
124
|
-
const defineFunc = kind === "command" ? "defineCommand" : "defineQuery";
|
|
125
|
-
const safeName = toValidIdentifier(toPascalCase(operation.operationId));
|
|
126
|
-
lines.push(`/**`);
|
|
127
|
-
lines.push(` * ${operation.summary ?? operation.operationId}`);
|
|
128
|
-
if (operation.description) {
|
|
129
|
-
lines.push(` *`);
|
|
130
|
-
lines.push(` * ${operation.description}`);
|
|
131
|
-
}
|
|
132
|
-
lines.push(` *`);
|
|
133
|
-
lines.push(` * @source OpenAPI: ${operation.method.toUpperCase()} ${operation.path}`);
|
|
134
|
-
lines.push(` */`);
|
|
135
|
-
lines.push(`export const ${safeName}Spec = ${defineFunc}({`);
|
|
136
|
-
lines.push(" meta: {");
|
|
137
|
-
lines.push(` name: '${specName}',`);
|
|
138
|
-
lines.push(" version: 1,");
|
|
139
|
-
lines.push(` stability: '${options.defaultStability ?? "stable"}',`);
|
|
140
|
-
lines.push(` owners: [${(options.defaultOwners ?? []).map((o) => `'${o}'`).join(", ")}],`);
|
|
141
|
-
lines.push(` tags: [${operation.tags.map((t) => `'${t}'`).join(", ")}],`);
|
|
142
|
-
lines.push(` description: ${JSON.stringify(operation.summary ?? operation.operationId)},`);
|
|
143
|
-
lines.push(` goal: ${JSON.stringify(operation.description ?? "Imported from OpenAPI")},`);
|
|
144
|
-
lines.push(` context: 'Imported from OpenAPI: ${operation.method.toUpperCase()} ${operation.path}',`);
|
|
145
|
-
lines.push(" },");
|
|
146
|
-
lines.push(" io: {");
|
|
147
|
-
if (inputModel) lines.push(` input: ${inputModel.name},`);
|
|
148
|
-
else lines.push(" input: null,");
|
|
149
|
-
if (outputModel) lines.push(` output: ${outputModel.name},`);
|
|
150
|
-
else lines.push(" output: null, // TODO: Define output schema");
|
|
151
|
-
lines.push(" },");
|
|
152
|
-
lines.push(" policy: {");
|
|
153
|
-
lines.push(` auth: '${auth}',`);
|
|
154
|
-
lines.push(" },");
|
|
155
|
-
lines.push(" transport: {");
|
|
156
|
-
lines.push(" rest: {");
|
|
157
|
-
lines.push(` method: '${operation.method.toUpperCase()}',`);
|
|
158
|
-
lines.push(` path: '${operation.path}',`);
|
|
159
|
-
lines.push(" },");
|
|
160
|
-
lines.push(" },");
|
|
161
|
-
lines.push("});");
|
|
162
|
-
return lines.join("\n");
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Import operations from a parsed OpenAPI document.
|
|
166
|
-
*/
|
|
167
|
-
function importFromOpenApi(parseResult, options = {}) {
|
|
168
|
-
const { tags, exclude = [], include } = options;
|
|
169
|
-
const specs = [];
|
|
170
|
-
const skipped = [];
|
|
171
|
-
const errors = [];
|
|
172
|
-
for (const operation of parseResult.operations) {
|
|
173
|
-
if (tags && tags.length > 0) {
|
|
174
|
-
if (!operation.tags.some((t) => tags.includes(t))) {
|
|
175
|
-
skipped.push({
|
|
176
|
-
sourceId: operation.operationId,
|
|
177
|
-
reason: `No matching tags (has: ${operation.tags.join(", ")})`
|
|
178
|
-
});
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
if (include && include.length > 0) {
|
|
183
|
-
if (!include.includes(operation.operationId)) {
|
|
184
|
-
skipped.push({
|
|
185
|
-
sourceId: operation.operationId,
|
|
186
|
-
reason: "Not in include list"
|
|
187
|
-
});
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
|
-
} else if (exclude.includes(operation.operationId)) {
|
|
191
|
-
skipped.push({
|
|
192
|
-
sourceId: operation.operationId,
|
|
193
|
-
reason: "In exclude list"
|
|
194
|
-
});
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (operation.deprecated && options.defaultStability !== "deprecated") {
|
|
198
|
-
skipped.push({
|
|
199
|
-
sourceId: operation.operationId,
|
|
200
|
-
reason: "Deprecated operation"
|
|
201
|
-
});
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
const { schema: inputSchema } = buildInputSchema(operation);
|
|
206
|
-
const inputModel = inputSchema ? generateSchemaModelCode(inputSchema, `${operation.operationId}Input`) : null;
|
|
207
|
-
const outputSchema = getOutputSchema(operation);
|
|
208
|
-
const code = generateSpecCode(operation, options, inputModel, outputSchema ? generateSchemaModelCode(outputSchema, `${operation.operationId}Output`) : null);
|
|
209
|
-
const fileName = toFileName(toSpecName(operation.operationId, options.prefix));
|
|
210
|
-
const transportHints = { rest: {
|
|
211
|
-
method: operation.method.toUpperCase(),
|
|
212
|
-
path: operation.path,
|
|
213
|
-
params: {
|
|
214
|
-
path: operation.pathParams.map((p) => p.name),
|
|
215
|
-
query: operation.queryParams.map((p) => p.name),
|
|
216
|
-
header: operation.headerParams.map((p) => p.name),
|
|
217
|
-
cookie: operation.cookieParams.map((p) => p.name)
|
|
218
|
-
}
|
|
219
|
-
} };
|
|
220
|
-
const source = {
|
|
221
|
-
type: "openapi",
|
|
222
|
-
sourceId: operation.operationId,
|
|
223
|
-
operationId: operation.operationId,
|
|
224
|
-
openApiVersion: parseResult.version,
|
|
225
|
-
importedAt: /* @__PURE__ */ new Date()
|
|
226
|
-
};
|
|
227
|
-
specs.push({
|
|
228
|
-
spec: {},
|
|
229
|
-
code,
|
|
230
|
-
fileName,
|
|
231
|
-
source,
|
|
232
|
-
transportHints
|
|
233
|
-
});
|
|
234
|
-
} catch (error) {
|
|
235
|
-
errors.push({
|
|
236
|
-
sourceId: operation.operationId,
|
|
237
|
-
error: error instanceof Error ? error.message : String(error)
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
return {
|
|
242
|
-
specs,
|
|
243
|
-
skipped,
|
|
244
|
-
errors,
|
|
245
|
-
summary: {
|
|
246
|
-
total: parseResult.operations.length,
|
|
247
|
-
imported: specs.length,
|
|
248
|
-
skipped: skipped.length,
|
|
249
|
-
errors: errors.length
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Import a single operation to ContractSpec code.
|
|
255
|
-
*/
|
|
256
|
-
function importOperation(operation, options = {}) {
|
|
257
|
-
const { schema: inputSchema } = buildInputSchema(operation);
|
|
258
|
-
const inputModel = inputSchema ? generateSchemaModelCode(inputSchema, `${operation.operationId}Input`) : null;
|
|
259
|
-
const outputSchema = getOutputSchema(operation);
|
|
260
|
-
return generateSpecCode(operation, options, inputModel, outputSchema ? generateSchemaModelCode(outputSchema, `${operation.operationId}Output`) : null);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
//#endregion
|
|
264
|
-
export { importFromOpenApi, importOperation };
|
|
1
|
+
import{toFileName as e,toPascalCase as t,toSpecName as n,toValidIdentifier as r}from"../common/utils.js";import{generateImports as i,generateSchemaModelCode as a}from"./schema-converter.js";const o=[`post`,`put`,`delete`,`patch`];function s(e){return o.includes(e.toLowerCase())?`command`:`query`}function c(e,t){if(!e.security||e.security.length===0)return t;for(let t of e.security)if(Object.keys(t).length===0)return`anonymous`;return`user`}function l(e){let t=[];for(let n of e.pathParams)t.push({name:n.name,schema:n.schema,required:!0});for(let n of e.queryParams)t.push({name:n.name,schema:n.schema,required:n.required});let n=[`authorization`,`content-type`,`accept`,`user-agent`];for(let r of e.headerParams)n.includes(r.name.toLowerCase())||t.push({name:r.name,schema:r.schema,required:r.required});if(e.requestBody?.schema){let n=e.requestBody.schema;if(`$ref`in n)t.push({name:`body`,schema:n,required:e.requestBody.required});else{let e=n,r=e.properties,i=e.required??[];if(r)for(let[e,n]of Object.entries(r))t.push({name:e,schema:n,required:i.includes(e)})}}return t.length===0?{schema:null,fields:[]}:{schema:{type:`object`,properties:t.reduce((e,t)=>(e[t.name]=t.schema,e),{}),required:t.filter(e=>e.required).map(e=>e.name)},fields:t}}function u(e){for(let t of[`200`,`201`,`202`,`204`]){let n=e.responses[t];if(n?.schema)return n.schema}for(let[t,n]of Object.entries(e.responses))if(t.startsWith(`2`)&&n.schema)return n.schema;return null}function d(e,a,o,l){let u=n(e.operationId,a.prefix),d=s(e.method),f=c(e,a.defaultAuth??`user`),p=[];p.push(`import { defineCommand, defineQuery } from '@lssm/lib.contracts';`),(o||l)&&p.push(i([...o?.fields??[],...l?.fields??[]])),p.push(``),o&&o.code&&(p.push(`// Input schema`),p.push(o.code),p.push(``)),l&&l.code&&(p.push(`// Output schema`),p.push(l.code),p.push(``));let m=d===`command`?`defineCommand`:`defineQuery`,h=r(t(e.operationId));return p.push(`/**`),p.push(` * ${e.summary??e.operationId}`),e.description&&(p.push(` *`),p.push(` * ${e.description}`)),p.push(` *`),p.push(` * @source OpenAPI: ${e.method.toUpperCase()} ${e.path}`),p.push(` */`),p.push(`export const ${h}Spec = ${m}({`),p.push(` meta: {`),p.push(` name: '${u}',`),p.push(` version: 1,`),p.push(` stability: '${a.defaultStability??`stable`}',`),p.push(` owners: [${(a.defaultOwners??[]).map(e=>`'${e}'`).join(`, `)}],`),p.push(` tags: [${e.tags.map(e=>`'${e}'`).join(`, `)}],`),p.push(` description: ${JSON.stringify(e.summary??e.operationId)},`),p.push(` goal: ${JSON.stringify(e.description??`Imported from OpenAPI`)},`),p.push(` context: 'Imported from OpenAPI: ${e.method.toUpperCase()} ${e.path}',`),p.push(` },`),p.push(` io: {`),o?p.push(` input: ${o.name},`):p.push(` input: null,`),l?p.push(` output: ${l.name},`):p.push(` output: null, // TODO: Define output schema`),p.push(` },`),p.push(` policy: {`),p.push(` auth: '${f}',`),p.push(` },`),p.push(` transport: {`),p.push(` rest: {`),p.push(` method: '${e.method.toUpperCase()}',`),p.push(` path: '${e.path}',`),p.push(` },`),p.push(` },`),p.push(`});`),p.join(`
|
|
2
|
+
`)}function f(t,r={}){let{tags:i,exclude:o=[],include:s}=r,c=[],f=[],p=[];for(let m of t.operations){if(i&&i.length>0&&!m.tags.some(e=>i.includes(e))){f.push({sourceId:m.operationId,reason:`No matching tags (has: ${m.tags.join(`, `)})`});continue}if(s&&s.length>0){if(!s.includes(m.operationId)){f.push({sourceId:m.operationId,reason:`Not in include list`});continue}}else if(o.includes(m.operationId)){f.push({sourceId:m.operationId,reason:`In exclude list`});continue}if(m.deprecated&&r.defaultStability!==`deprecated`){f.push({sourceId:m.operationId,reason:`Deprecated operation`});continue}try{let{schema:i}=l(m),o=i?a(i,`${m.operationId}Input`):null,s=u(m),f=d(m,r,o,s?a(s,`${m.operationId}Output`):null),p=e(n(m.operationId,r.prefix)),h={rest:{method:m.method.toUpperCase(),path:m.path,params:{path:m.pathParams.map(e=>e.name),query:m.queryParams.map(e=>e.name),header:m.headerParams.map(e=>e.name),cookie:m.cookieParams.map(e=>e.name)}}},g={type:`openapi`,sourceId:m.operationId,operationId:m.operationId,openApiVersion:t.version,importedAt:new Date};c.push({spec:{},code:f,fileName:p,source:g,transportHints:h})}catch(e){p.push({sourceId:m.operationId,error:e instanceof Error?e.message:String(e)})}}return{specs:c,skipped:f,errors:p,summary:{total:t.operations.length,imported:c.length,skipped:f.length,errors:p.length}}}function p(e,t={}){let{schema:n}=l(e),r=n?a(n,`${e.operationId}Input`):null,i=u(e);return d(e,t,r,i?a(i,`${e.operationId}Output`):null)}export{f as importFromOpenApi,p as importOperation};
|
package/dist/openapi/index.js
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { defaultRestPath, openApiForRegistry, openApiToJson, openApiToYaml } from "./exporter.js";
|
|
3
|
-
import { generateImports, generateSchemaModelCode, getScalarType, jsonSchemaToField, jsonSchemaToType } from "./schema-converter.js";
|
|
4
|
-
import { importFromOpenApi, importOperation } from "./importer.js";
|
|
5
|
-
import { createSpecDiff, diffAll, diffSpecVsOperation, diffSpecs, formatDiffChanges } from "./differ.js";
|
|
6
|
-
|
|
7
|
-
export { createSpecDiff, defaultRestPath, detectFormat, detectVersion, diffAll, diffSpecVsOperation, diffSpecs, formatDiffChanges, generateImports, generateSchemaModelCode, getScalarType, importFromOpenApi, importOperation, jsonSchemaToField, jsonSchemaToType, openApiForRegistry, openApiToJson, openApiToYaml, parseOpenApi, parseOpenApiDocument, parseOpenApiString };
|
|
1
|
+
import{detectFormat as e,detectVersion as t,parseOpenApi as n,parseOpenApiDocument as r,parseOpenApiString as i}from"./parser.js";import{defaultRestPath as a,openApiForRegistry as o,openApiToJson as s,openApiToYaml as c}from"./exporter.js";import{generateImports as l,generateSchemaModelCode as u,getScalarType as d,jsonSchemaToField as f,jsonSchemaToType as p}from"./schema-converter.js";import{importFromOpenApi as m,importOperation as h}from"./importer.js";import{createSpecDiff as g,diffAll as _,diffSpecVsOperation as v,diffSpecs as y,formatDiffChanges as b}from"./differ.js";export{g as createSpecDiff,a as defaultRestPath,e as detectFormat,t as detectVersion,_ as diffAll,v as diffSpecVsOperation,y as diffSpecs,b as formatDiffChanges,l as generateImports,u as generateSchemaModelCode,d as getScalarType,m as importFromOpenApi,h as importOperation,f as jsonSchemaToField,p as jsonSchemaToType,o as openApiForRegistry,s as openApiToJson,c as openApiToYaml,n as parseOpenApi,r as parseOpenApiDocument,i as parseOpenApiString};
|
package/dist/openapi/parser.js
CHANGED
|
@@ -1,231 +1 @@
|
|
|
1
|
-
import { parse }
|
|
2
|
-
|
|
3
|
-
//#region src/openapi/parser.ts
|
|
4
|
-
/**
|
|
5
|
-
* OpenAPI document parser.
|
|
6
|
-
* Parses OpenAPI 3.x documents from JSON/YAML files or URLs.
|
|
7
|
-
*/
|
|
8
|
-
const HTTP_METHODS = [
|
|
9
|
-
"get",
|
|
10
|
-
"post",
|
|
11
|
-
"put",
|
|
12
|
-
"delete",
|
|
13
|
-
"patch",
|
|
14
|
-
"head",
|
|
15
|
-
"options",
|
|
16
|
-
"trace"
|
|
17
|
-
];
|
|
18
|
-
/**
|
|
19
|
-
* Parse an OpenAPI document from a string (JSON or YAML).
|
|
20
|
-
*/
|
|
21
|
-
function parseOpenApiString(content, format = "json") {
|
|
22
|
-
if (format === "yaml") return parse(content);
|
|
23
|
-
return JSON.parse(content);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Detect the format of content (JSON or YAML).
|
|
27
|
-
*/
|
|
28
|
-
function detectFormat(content) {
|
|
29
|
-
const trimmed = content.trim();
|
|
30
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
31
|
-
return "yaml";
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Detect OpenAPI version from document.
|
|
35
|
-
*/
|
|
36
|
-
function detectVersion(doc) {
|
|
37
|
-
if (doc.openapi.startsWith("3.1")) return "3.1";
|
|
38
|
-
return "3.0";
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Check if a value is a reference object.
|
|
42
|
-
*/
|
|
43
|
-
function isReference(obj) {
|
|
44
|
-
return typeof obj === "object" && obj !== null && "$ref" in obj;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Resolve a $ref reference in the document.
|
|
48
|
-
*/
|
|
49
|
-
function resolveRef(doc, ref) {
|
|
50
|
-
if (!ref.startsWith("#/")) return;
|
|
51
|
-
const path = ref.slice(2).split("/");
|
|
52
|
-
let current = doc;
|
|
53
|
-
for (const part of path) {
|
|
54
|
-
if (current === null || current === void 0) return void 0;
|
|
55
|
-
if (typeof current !== "object") return void 0;
|
|
56
|
-
current = current[part];
|
|
57
|
-
}
|
|
58
|
-
return current;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Resolve a schema, following $ref if needed.
|
|
62
|
-
*/
|
|
63
|
-
function resolveSchema(doc, schema) {
|
|
64
|
-
if (!schema) return void 0;
|
|
65
|
-
if (isReference(schema)) return resolveRef(doc, schema.$ref) ?? schema;
|
|
66
|
-
return schema;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Parse parameters from an operation.
|
|
70
|
-
*/
|
|
71
|
-
function parseParameters(doc, params) {
|
|
72
|
-
const result = {
|
|
73
|
-
path: [],
|
|
74
|
-
query: [],
|
|
75
|
-
header: [],
|
|
76
|
-
cookie: []
|
|
77
|
-
};
|
|
78
|
-
if (!params) return result;
|
|
79
|
-
for (const param of params) {
|
|
80
|
-
let resolved;
|
|
81
|
-
if (isReference(param)) {
|
|
82
|
-
const ref = resolveRef(doc, param.$ref);
|
|
83
|
-
if (!ref) continue;
|
|
84
|
-
resolved = ref;
|
|
85
|
-
} else resolved = param;
|
|
86
|
-
const parsed = {
|
|
87
|
-
name: resolved.name,
|
|
88
|
-
in: resolved.in,
|
|
89
|
-
required: resolved.required ?? resolved.in === "path",
|
|
90
|
-
description: resolved.description,
|
|
91
|
-
schema: resolved.schema,
|
|
92
|
-
deprecated: resolved.deprecated ?? false
|
|
93
|
-
};
|
|
94
|
-
result[resolved.in]?.push(parsed);
|
|
95
|
-
}
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Generate an operationId if not present.
|
|
100
|
-
*/
|
|
101
|
-
function generateOperationId(method, path) {
|
|
102
|
-
return method + path.split("/").filter(Boolean).map((part) => {
|
|
103
|
-
if (part.startsWith("{") && part.endsWith("}")) return "By" + part.slice(1, -1).charAt(0).toUpperCase() + part.slice(2, -1);
|
|
104
|
-
return part.charAt(0).toUpperCase() + part.slice(1);
|
|
105
|
-
}).join("");
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Parse a single operation.
|
|
109
|
-
*/
|
|
110
|
-
function parseOperation(doc, method, path, operation, pathParams) {
|
|
111
|
-
const params = parseParameters(doc, [...pathParams ?? [], ...operation.parameters ?? []]);
|
|
112
|
-
let requestBody;
|
|
113
|
-
if (operation.requestBody) {
|
|
114
|
-
const body = isReference(operation.requestBody) ? resolveRef(doc, operation.requestBody.$ref) : operation.requestBody;
|
|
115
|
-
if (body) {
|
|
116
|
-
const contentType = Object.keys(body.content ?? {})[0] ?? "application/json";
|
|
117
|
-
const content = body.content?.[contentType];
|
|
118
|
-
if (content?.schema) requestBody = {
|
|
119
|
-
required: body.required ?? false,
|
|
120
|
-
schema: resolveSchema(doc, content.schema),
|
|
121
|
-
contentType
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const responses = {};
|
|
126
|
-
for (const [status, response] of Object.entries(operation.responses ?? {})) {
|
|
127
|
-
const resolved = isReference(response) ? resolveRef(doc, response.$ref) : response;
|
|
128
|
-
if (resolved) {
|
|
129
|
-
const contentType = Object.keys(resolved.content ?? {})[0];
|
|
130
|
-
const content = contentType ? resolved.content?.[contentType] : void 0;
|
|
131
|
-
responses[status] = {
|
|
132
|
-
description: resolved.description,
|
|
133
|
-
schema: content?.schema ? resolveSchema(doc, content.schema) : void 0,
|
|
134
|
-
contentType
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
const contractSpecMeta = operation?.["x-contractspec"];
|
|
139
|
-
return {
|
|
140
|
-
operationId: operation.operationId ?? generateOperationId(method, path),
|
|
141
|
-
method,
|
|
142
|
-
path,
|
|
143
|
-
summary: operation.summary,
|
|
144
|
-
description: operation.description,
|
|
145
|
-
tags: operation.tags ?? [],
|
|
146
|
-
pathParams: params.path,
|
|
147
|
-
queryParams: params.query,
|
|
148
|
-
headerParams: params.header,
|
|
149
|
-
cookieParams: params.cookie,
|
|
150
|
-
requestBody,
|
|
151
|
-
responses,
|
|
152
|
-
deprecated: operation.deprecated ?? false,
|
|
153
|
-
security: operation.security,
|
|
154
|
-
contractSpecMeta
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Parse an OpenAPI document into a structured result.
|
|
159
|
-
*/
|
|
160
|
-
function parseOpenApiDocument(doc, _options = {}) {
|
|
161
|
-
const version = detectVersion(doc);
|
|
162
|
-
const warnings = [];
|
|
163
|
-
const operations = [];
|
|
164
|
-
for (const [path, pathItem] of Object.entries(doc.paths ?? {})) {
|
|
165
|
-
if (!pathItem) continue;
|
|
166
|
-
const pathParams = pathItem.parameters;
|
|
167
|
-
for (const method of HTTP_METHODS) {
|
|
168
|
-
const operation = pathItem[method];
|
|
169
|
-
if (operation) try {
|
|
170
|
-
operations.push(parseOperation(doc, method, path, operation, pathParams));
|
|
171
|
-
} catch (error) {
|
|
172
|
-
warnings.push(`Failed to parse ${method.toUpperCase()} ${path}: ${error}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
const schemas = {};
|
|
177
|
-
const components = doc.components;
|
|
178
|
-
if (components?.schemas) for (const [name, schema] of Object.entries(components.schemas)) schemas[name] = schema;
|
|
179
|
-
const servers = (doc.servers ?? []).map((s) => ({
|
|
180
|
-
url: s.url,
|
|
181
|
-
description: s.description,
|
|
182
|
-
variables: s.variables
|
|
183
|
-
}));
|
|
184
|
-
return {
|
|
185
|
-
document: doc,
|
|
186
|
-
version,
|
|
187
|
-
info: {
|
|
188
|
-
title: doc.info.title,
|
|
189
|
-
version: doc.info.version,
|
|
190
|
-
description: doc.info.description
|
|
191
|
-
},
|
|
192
|
-
operations,
|
|
193
|
-
schemas,
|
|
194
|
-
servers,
|
|
195
|
-
warnings
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Parse OpenAPI from a file path or URL.
|
|
200
|
-
* Note: This is an async function that requires I/O adapters.
|
|
201
|
-
* For pure parsing, use parseOpenApiString or parseOpenApiDocument.
|
|
202
|
-
*/
|
|
203
|
-
async function parseOpenApi(source, options = {}) {
|
|
204
|
-
const { fetch: fetchFn = globalThis.fetch, readFile, timeout = 3e4 } = options;
|
|
205
|
-
let content;
|
|
206
|
-
let format;
|
|
207
|
-
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
208
|
-
const controller = new AbortController();
|
|
209
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
210
|
-
try {
|
|
211
|
-
const response = await fetchFn(source, { signal: controller.signal });
|
|
212
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
213
|
-
content = await response.text();
|
|
214
|
-
} finally {
|
|
215
|
-
clearTimeout(timeoutId);
|
|
216
|
-
}
|
|
217
|
-
if (source.endsWith(".yaml") || source.endsWith(".yml")) format = "yaml";
|
|
218
|
-
else if (source.endsWith(".json")) format = "json";
|
|
219
|
-
else format = detectFormat(content);
|
|
220
|
-
} else {
|
|
221
|
-
if (!readFile) throw new Error("readFile adapter required for file paths");
|
|
222
|
-
content = await readFile(source);
|
|
223
|
-
if (source.endsWith(".yaml") || source.endsWith(".yml")) format = "yaml";
|
|
224
|
-
else if (source.endsWith(".json")) format = "json";
|
|
225
|
-
else format = detectFormat(content);
|
|
226
|
-
}
|
|
227
|
-
return parseOpenApiDocument(parseOpenApiString(content, format), options);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
//#endregion
|
|
231
|
-
export { detectFormat, detectVersion, parseOpenApi, parseOpenApiDocument, parseOpenApiString };
|
|
1
|
+
import{parse as e}from"yaml";const t=[`get`,`post`,`put`,`delete`,`patch`,`head`,`options`,`trace`];function n(t,n=`json`){return n===`yaml`?e(t):JSON.parse(t)}function r(e){let t=e.trim();return t.startsWith(`{`)||t.startsWith(`[`)?`json`:`yaml`}function i(e){return e.openapi.startsWith(`3.1`)?`3.1`:`3.0`}function a(e){return typeof e==`object`&&!!e&&`$ref`in e}function o(e,t){if(!t.startsWith(`#/`))return;let n=t.slice(2).split(`/`),r=e;for(let e of n){if(typeof r!=`object`||!r)return;r=r[e]}return r}function s(e,t){if(t)return a(t)?o(e,t.$ref)??t:t}function c(e,t){let n={path:[],query:[],header:[],cookie:[]};if(!t)return n;for(let r of t){let t;if(a(r)){let n=o(e,r.$ref);if(!n)continue;t=n}else t=r;let i={name:t.name,in:t.in,required:t.required??t.in===`path`,description:t.description,schema:t.schema,deprecated:t.deprecated??!1};n[t.in]?.push(i)}return n}function l(e,t){return e+t.split(`/`).filter(Boolean).map(e=>e.startsWith(`{`)&&e.endsWith(`}`)?`By`+e.slice(1,-1).charAt(0).toUpperCase()+e.slice(2,-1):e.charAt(0).toUpperCase()+e.slice(1)).join(``)}function u(e,t,n,r,i){let u=c(e,[...i??[],...r.parameters??[]]),d;if(r.requestBody){let t=a(r.requestBody)?o(e,r.requestBody.$ref):r.requestBody;if(t){let n=Object.keys(t.content??{})[0]??`application/json`,r=t.content?.[n];r?.schema&&(d={required:t.required??!1,schema:s(e,r.schema),contentType:n})}}let f={};for(let[t,n]of Object.entries(r.responses??{})){let r=a(n)?o(e,n.$ref):n;if(r){let n=Object.keys(r.content??{})[0],i=n?r.content?.[n]:void 0;f[t]={description:r.description,schema:i?.schema?s(e,i.schema):void 0,contentType:n}}}let p=r?.[`x-contractspec`];return{operationId:r.operationId??l(t,n),method:t,path:n,summary:r.summary,description:r.description,tags:r.tags??[],pathParams:u.path,queryParams:u.query,headerParams:u.header,cookieParams:u.cookie,requestBody:d,responses:f,deprecated:r.deprecated??!1,security:r.security,contractSpecMeta:p}}function d(e,n={}){let r=i(e),a=[],o=[];for(let[n,r]of Object.entries(e.paths??{})){if(!r)continue;let i=r.parameters;for(let s of t){let t=r[s];if(t)try{o.push(u(e,s,n,t,i))}catch(e){a.push(`Failed to parse ${s.toUpperCase()} ${n}: ${e}`)}}}let s={},c=e.components;if(c?.schemas)for(let[e,t]of Object.entries(c.schemas))s[e]=t;let l=(e.servers??[]).map(e=>({url:e.url,description:e.description,variables:e.variables}));return{document:e,version:r,info:{title:e.info.title,version:e.info.version,description:e.info.description},operations:o,schemas:s,servers:l,warnings:a}}async function f(e,t={}){let{fetch:i=globalThis.fetch,readFile:a,timeout:o=3e4}=t,s,c;if(e.startsWith(`http://`)||e.startsWith(`https://`)){let t=new AbortController,n=setTimeout(()=>t.abort(),o);try{let n=await i(e,{signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}: ${n.statusText}`);s=await n.text()}finally{clearTimeout(n)}c=e.endsWith(`.yaml`)||e.endsWith(`.yml`)?`yaml`:e.endsWith(`.json`)?`json`:r(s)}else{if(!a)throw Error(`readFile adapter required for file paths`);s=await a(e),c=e.endsWith(`.yaml`)||e.endsWith(`.yml`)?`yaml`:e.endsWith(`.json`)?`json`:r(s)}return d(n(s,c),t)}export{r as detectFormat,i as detectVersion,f as parseOpenApi,d as parseOpenApiDocument,n as parseOpenApiString};
|