@apidiff/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (249) hide show
  1. package/dist/ast/hash.d.ts +2 -0
  2. package/dist/ast/hash.d.ts.map +1 -0
  3. package/dist/ast/index.d.ts +3 -0
  4. package/dist/ast/index.d.ts.map +1 -0
  5. package/dist/ast/traverse.d.ts +13 -0
  6. package/dist/ast/traverse.d.ts.map +1 -0
  7. package/dist/config/index.d.ts +9 -0
  8. package/dist/config/index.d.ts.map +1 -0
  9. package/dist/diff/auth-differ.d.ts +4 -0
  10. package/dist/diff/auth-differ.d.ts.map +1 -0
  11. package/dist/diff/endpoint-differ.d.ts +3 -0
  12. package/dist/diff/endpoint-differ.d.ts.map +1 -0
  13. package/dist/diff/index.d.ts +3 -0
  14. package/dist/diff/index.d.ts.map +1 -0
  15. package/dist/diff/schema-differ.d.ts +3 -0
  16. package/dist/diff/schema-differ.d.ts.map +1 -0
  17. package/dist/diff/server-differ.d.ts +3 -0
  18. package/dist/diff/server-differ.d.ts.map +1 -0
  19. package/dist/index.cjs +2902 -0
  20. package/dist/index.d.ts +15 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +2855 -0
  23. package/dist/loader/file-loader.d.ts +5 -0
  24. package/dist/loader/file-loader.d.ts.map +1 -0
  25. package/dist/loader/git-loader.d.ts +5 -0
  26. package/dist/loader/git-loader.d.ts.map +1 -0
  27. package/dist/loader/index.d.ts +7 -0
  28. package/dist/loader/index.d.ts.map +1 -0
  29. package/dist/loader/url-loader.d.ts +5 -0
  30. package/dist/loader/url-loader.d.ts.map +1 -0
  31. package/dist/output/html.d.ts +3 -0
  32. package/dist/output/html.d.ts.map +1 -0
  33. package/dist/output/index.d.ts +3 -0
  34. package/dist/output/index.d.ts.map +1 -0
  35. package/dist/output/json.d.ts +3 -0
  36. package/dist/output/json.d.ts.map +1 -0
  37. package/dist/output/markdown.d.ts +3 -0
  38. package/dist/output/markdown.d.ts.map +1 -0
  39. package/dist/output/terminal.d.ts +3 -0
  40. package/dist/output/terminal.d.ts.map +1 -0
  41. package/dist/parsers/base.d.ts +8 -0
  42. package/dist/parsers/base.d.ts.map +1 -0
  43. package/dist/parsers/graphql/index.d.ts +13 -0
  44. package/dist/parsers/graphql/index.d.ts.map +1 -0
  45. package/dist/parsers/index.d.ts +7 -0
  46. package/dist/parsers/index.d.ts.map +1 -0
  47. package/dist/parsers/openapi2/index.d.ts +9 -0
  48. package/dist/parsers/openapi2/index.d.ts.map +1 -0
  49. package/dist/parsers/openapi3/index.d.ts +9 -0
  50. package/dist/parsers/openapi3/index.d.ts.map +1 -0
  51. package/dist/parsers/openapi3/ref-resolver.d.ts +2 -0
  52. package/dist/parsers/openapi3/ref-resolver.d.ts.map +1 -0
  53. package/dist/parsers/openapi3/schema-normalizer.d.ts +3 -0
  54. package/dist/parsers/openapi3/schema-normalizer.d.ts.map +1 -0
  55. package/dist/parsers/openapi3/security-normalizer.d.ts +3 -0
  56. package/dist/parsers/openapi3/security-normalizer.d.ts.map +1 -0
  57. package/dist/parsers/protobuf/index.d.ts +14 -0
  58. package/dist/parsers/protobuf/index.d.ts.map +1 -0
  59. package/dist/rules/auth/oauth-scope-removed.d.ts +9 -0
  60. package/dist/rules/auth/oauth-scope-removed.d.ts.map +1 -0
  61. package/dist/rules/auth/security-added.d.ts +9 -0
  62. package/dist/rules/auth/security-added.d.ts.map +1 -0
  63. package/dist/rules/auth/security-removed.d.ts +9 -0
  64. package/dist/rules/auth/security-removed.d.ts.map +1 -0
  65. package/dist/rules/auth/security-scheme-type-changed.d.ts +9 -0
  66. package/dist/rules/auth/security-scheme-type-changed.d.ts.map +1 -0
  67. package/dist/rules/base.d.ts +19 -0
  68. package/dist/rules/base.d.ts.map +1 -0
  69. package/dist/rules/endpoint/endpoint-added.d.ts +9 -0
  70. package/dist/rules/endpoint/endpoint-added.d.ts.map +1 -0
  71. package/dist/rules/endpoint/endpoint-deprecated.d.ts +9 -0
  72. package/dist/rules/endpoint/endpoint-deprecated.d.ts.map +1 -0
  73. package/dist/rules/endpoint/endpoint-removed.d.ts +9 -0
  74. package/dist/rules/endpoint/endpoint-removed.d.ts.map +1 -0
  75. package/dist/rules/endpoint/http-method-changed.d.ts +9 -0
  76. package/dist/rules/endpoint/http-method-changed.d.ts.map +1 -0
  77. package/dist/rules/endpoint/path-changed.d.ts +9 -0
  78. package/dist/rules/endpoint/path-changed.d.ts.map +1 -0
  79. package/dist/rules/index.d.ts +4 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/meta/server-removed.d.ts +9 -0
  82. package/dist/rules/meta/server-removed.d.ts.map +1 -0
  83. package/dist/rules/param/param-added.d.ts +9 -0
  84. package/dist/rules/param/param-added.d.ts.map +1 -0
  85. package/dist/rules/param/param-deprecated.d.ts +9 -0
  86. package/dist/rules/param/param-deprecated.d.ts.map +1 -0
  87. package/dist/rules/param/param-enum-value-added.d.ts +9 -0
  88. package/dist/rules/param/param-enum-value-added.d.ts.map +1 -0
  89. package/dist/rules/param/param-enum-value-removed.d.ts +9 -0
  90. package/dist/rules/param/param-enum-value-removed.d.ts.map +1 -0
  91. package/dist/rules/param/param-location-changed.d.ts +9 -0
  92. package/dist/rules/param/param-location-changed.d.ts.map +1 -0
  93. package/dist/rules/param/param-removed.d.ts +9 -0
  94. package/dist/rules/param/param-removed.d.ts.map +1 -0
  95. package/dist/rules/param/param-required-added.d.ts +9 -0
  96. package/dist/rules/param/param-required-added.d.ts.map +1 -0
  97. package/dist/rules/param/param-required-true-to-false.d.ts +9 -0
  98. package/dist/rules/param/param-required-true-to-false.d.ts.map +1 -0
  99. package/dist/rules/param/param-type-changed.d.ts +9 -0
  100. package/dist/rules/param/param-type-changed.d.ts.map +1 -0
  101. package/dist/rules/request/request-body-added-required.d.ts +9 -0
  102. package/dist/rules/request/request-body-added-required.d.ts.map +1 -0
  103. package/dist/rules/request/request-body-removed.d.ts +9 -0
  104. package/dist/rules/request/request-body-removed.d.ts.map +1 -0
  105. package/dist/rules/request/request-content-type-added.d.ts +9 -0
  106. package/dist/rules/request/request-content-type-added.d.ts.map +1 -0
  107. package/dist/rules/request/request-content-type-removed.d.ts +9 -0
  108. package/dist/rules/request/request-content-type-removed.d.ts.map +1 -0
  109. package/dist/rules/request/request-field-added-required.d.ts +9 -0
  110. package/dist/rules/request/request-field-added-required.d.ts.map +1 -0
  111. package/dist/rules/request/request-field-removed.d.ts +9 -0
  112. package/dist/rules/request/request-field-removed.d.ts.map +1 -0
  113. package/dist/rules/request/request-field-type-changed.d.ts +9 -0
  114. package/dist/rules/request/request-field-type-changed.d.ts.map +1 -0
  115. package/dist/rules/request/request-required-false-to-true.d.ts +9 -0
  116. package/dist/rules/request/request-required-false-to-true.d.ts.map +1 -0
  117. package/dist/rules/response/response-field-added.d.ts +9 -0
  118. package/dist/rules/response/response-field-added.d.ts.map +1 -0
  119. package/dist/rules/response/response-field-removed.d.ts +9 -0
  120. package/dist/rules/response/response-field-removed.d.ts.map +1 -0
  121. package/dist/rules/response/response-field-type-changed.d.ts +9 -0
  122. package/dist/rules/response/response-field-type-changed.d.ts.map +1 -0
  123. package/dist/rules/response/response-header-added-required.d.ts +9 -0
  124. package/dist/rules/response/response-header-added-required.d.ts.map +1 -0
  125. package/dist/rules/response/response-header-removed.d.ts +9 -0
  126. package/dist/rules/response/response-header-removed.d.ts.map +1 -0
  127. package/dist/rules/response/response-media-type-added.d.ts +9 -0
  128. package/dist/rules/response/response-media-type-added.d.ts.map +1 -0
  129. package/dist/rules/response/response-media-type-removed.d.ts +9 -0
  130. package/dist/rules/response/response-media-type-removed.d.ts.map +1 -0
  131. package/dist/rules/response/response-status-added.d.ts +9 -0
  132. package/dist/rules/response/response-status-added.d.ts.map +1 -0
  133. package/dist/rules/response/response-status-removed.d.ts +9 -0
  134. package/dist/rules/response/response-status-removed.d.ts.map +1 -0
  135. package/dist/types/ast.d.ts +140 -0
  136. package/dist/types/ast.d.ts.map +1 -0
  137. package/dist/types/config.d.ts +17 -0
  138. package/dist/types/config.d.ts.map +1 -0
  139. package/dist/types/diff.d.ts +43 -0
  140. package/dist/types/diff.d.ts.map +1 -0
  141. package/dist/types/errors.d.ts +21 -0
  142. package/dist/types/errors.d.ts.map +1 -0
  143. package/dist/types/index.d.ts +6 -0
  144. package/dist/types/index.d.ts.map +1 -0
  145. package/dist/types/semantic.d.ts +45 -0
  146. package/dist/types/semantic.d.ts.map +1 -0
  147. package/package.json +31 -0
  148. package/src/ast/hash.ts +26 -0
  149. package/src/ast/index.ts +2 -0
  150. package/src/ast/traverse.ts +59 -0
  151. package/src/config/index.ts +44 -0
  152. package/src/diff/auth-differ.ts +72 -0
  153. package/src/diff/endpoint-differ.ts +144 -0
  154. package/src/diff/index.ts +13 -0
  155. package/src/diff/schema-differ.ts +70 -0
  156. package/src/diff/server-differ.ts +22 -0
  157. package/src/index.ts +92 -0
  158. package/src/loader/file-loader.ts +19 -0
  159. package/src/loader/git-loader.ts +22 -0
  160. package/src/loader/index.ts +32 -0
  161. package/src/loader/url-loader.ts +25 -0
  162. package/src/output/html.ts +177 -0
  163. package/src/output/index.ts +16 -0
  164. package/src/output/json.ts +5 -0
  165. package/src/output/markdown.ts +29 -0
  166. package/src/output/terminal.ts +37 -0
  167. package/src/parsers/base.ts +8 -0
  168. package/src/parsers/graphql/index.ts +181 -0
  169. package/src/parsers/index.ts +61 -0
  170. package/src/parsers/openapi2/index.ts +218 -0
  171. package/src/parsers/openapi3/index.ts +223 -0
  172. package/src/parsers/openapi3/ref-resolver.ts +101 -0
  173. package/src/parsers/openapi3/schema-normalizer.ts +52 -0
  174. package/src/parsers/openapi3/security-normalizer.ts +18 -0
  175. package/src/parsers/protobuf/index.ts +208 -0
  176. package/src/rules/auth/oauth-scope-removed.ts +34 -0
  177. package/src/rules/auth/security-added.ts +32 -0
  178. package/src/rules/auth/security-removed.ts +47 -0
  179. package/src/rules/auth/security-scheme-type-changed.ts +30 -0
  180. package/src/rules/base.ts +29 -0
  181. package/src/rules/endpoint/endpoint-added.ts +26 -0
  182. package/src/rules/endpoint/endpoint-deprecated.ts +29 -0
  183. package/src/rules/endpoint/endpoint-removed.ts +26 -0
  184. package/src/rules/endpoint/http-method-changed.ts +29 -0
  185. package/src/rules/endpoint/path-changed.ts +29 -0
  186. package/src/rules/index.ts +83 -0
  187. package/src/rules/meta/server-removed.ts +26 -0
  188. package/src/rules/param/param-added.ts +52 -0
  189. package/src/rules/param/param-deprecated.ts +34 -0
  190. package/src/rules/param/param-enum-value-added.ts +32 -0
  191. package/src/rules/param/param-enum-value-removed.ts +32 -0
  192. package/src/rules/param/param-location-changed.ts +43 -0
  193. package/src/rules/param/param-removed.ts +52 -0
  194. package/src/rules/param/param-required-added.ts +34 -0
  195. package/src/rules/param/param-required-true-to-false.ts +34 -0
  196. package/src/rules/param/param-type-changed.ts +32 -0
  197. package/src/rules/request/request-body-added-required.ts +33 -0
  198. package/src/rules/request/request-body-removed.ts +30 -0
  199. package/src/rules/request/request-content-type-added.ts +31 -0
  200. package/src/rules/request/request-content-type-removed.ts +31 -0
  201. package/src/rules/request/request-field-added-required.ts +41 -0
  202. package/src/rules/request/request-field-removed.ts +35 -0
  203. package/src/rules/request/request-field-type-changed.ts +37 -0
  204. package/src/rules/request/request-required-false-to-true.ts +56 -0
  205. package/src/rules/response/response-field-added.ts +34 -0
  206. package/src/rules/response/response-field-removed.ts +34 -0
  207. package/src/rules/response/response-field-type-changed.ts +37 -0
  208. package/src/rules/response/response-header-added-required.ts +47 -0
  209. package/src/rules/response/response-header-removed.ts +32 -0
  210. package/src/rules/response/response-media-type-added.ts +32 -0
  211. package/src/rules/response/response-media-type-removed.ts +32 -0
  212. package/src/rules/response/response-status-added.ts +31 -0
  213. package/src/rules/response/response-status-removed.ts +31 -0
  214. package/src/types/ast.ts +164 -0
  215. package/src/types/config.ts +31 -0
  216. package/src/types/diff.ts +49 -0
  217. package/src/types/errors.ts +26 -0
  218. package/src/types/index.ts +5 -0
  219. package/src/types/semantic.ts +60 -0
  220. package/test/integration/stripe-v1.yaml +36 -0
  221. package/test/integration/stripe-v2.yaml +35 -0
  222. package/tests/ast.test.ts +60 -0
  223. package/tests/config.test.ts +43 -0
  224. package/tests/diff/auth-differ.test.ts +55 -0
  225. package/tests/diff/endpoint-differ.test.ts +42 -0
  226. package/tests/diff/schema-differ.test.ts +46 -0
  227. package/tests/diff/server-differ.test.ts +23 -0
  228. package/tests/diff.test.ts +116 -0
  229. package/tests/fixtures/openapi3/auth-added/v1.yaml +11 -0
  230. package/tests/fixtures/openapi3/auth-added/v2.yaml +18 -0
  231. package/tests/integration.test.ts +21 -0
  232. package/tests/loader-more.test.ts +75 -0
  233. package/tests/loader.test.ts +61 -0
  234. package/tests/output.test.ts +58 -0
  235. package/tests/parsers/openapi3-coverage.test.ts +77 -0
  236. package/tests/parsers/openapi3.test.ts +41 -0
  237. package/tests/parsers/parsers-other.test.ts +135 -0
  238. package/tests/parsers/ref-resolver.test.ts +78 -0
  239. package/tests/parsers/schema-normalizer.test.ts +30 -0
  240. package/tests/rules/auth-meta.test.ts +87 -0
  241. package/tests/rules/base.test.ts +44 -0
  242. package/tests/rules/endpoint.test.ts +122 -0
  243. package/tests/rules/param.test.ts +242 -0
  244. package/tests/rules/request.test.ts +147 -0
  245. package/tests/rules/response.test.ts +161 -0
  246. package/tests/rules/rules-coverage.test.ts +64 -0
  247. package/tests/types-errors.test.ts +22 -0
  248. package/tsconfig.json +8 -0
  249. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,218 @@
1
+ import type { NormalizedAST, Endpoint, Parameter, RequestBody, ResponseDef, ComponentMap, HttpMethod, Server } from '../../types/index.js';
2
+ import type { RawSpec } from '../../loader/index.js';
3
+ import type { ISpecParser } from '../base.js';
4
+ import { ParseError } from '../../types/errors.js';
5
+ import { normalizeSchema } from '../openapi3/schema-normalizer.js';
6
+ import yaml from 'js-yaml';
7
+
8
+ export class OpenApi2Parser implements ISpecParser {
9
+ readonly format = 'openapi2';
10
+
11
+ canParse(raw: RawSpec): boolean {
12
+ return raw.format === 'openapi2';
13
+ }
14
+
15
+ async parse(raw: RawSpec): Promise<NormalizedAST> {
16
+ let parsed: any;
17
+ try {
18
+ parsed = JSON.parse(raw.content);
19
+ } catch {
20
+ try {
21
+ parsed = yaml.load(raw.content);
22
+ } catch (err: any) {
23
+ throw new ParseError('Failed to parse OpenAPI 2.x spec', undefined, undefined, raw.sourcePath, err);
24
+ }
25
+ }
26
+
27
+ if (!parsed || typeof parsed !== 'object' || !parsed.swagger || !parsed.swagger.startsWith('2.')) {
28
+ throw new ParseError('Not a valid OpenAPI 2.x spec', undefined, undefined, raw.sourcePath);
29
+ }
30
+
31
+ // In a real implementation we would resolve refs, but for now we'll assume they are resolved or handle them simply.
32
+ // We should use the same ref-resolver used in openapi3 if possible, but Swagger 2.0 has different structure.
33
+ // For this implementation, we will use the raw parsed object.
34
+ const resolved = parsed; // TODO: proper ref resolution
35
+
36
+ const meta = {
37
+ title: resolved.info?.title || 'Unknown API',
38
+ version: resolved.info?.version || '1.0.0',
39
+ format: 'openapi2' as const,
40
+ rawVersion: resolved.swagger
41
+ };
42
+
43
+ const servers: Server[] = [];
44
+ if (resolved.host) {
45
+ const schemes = Array.isArray(resolved.schemes) && resolved.schemes.length > 0 ? resolved.schemes : ['https'];
46
+ const basePath = resolved.basePath || '';
47
+ for (const scheme of schemes) {
48
+ servers.push({ url: `${scheme}://${resolved.host}${basePath}` });
49
+ }
50
+ }
51
+
52
+ const components: ComponentMap = {
53
+ schemas: {},
54
+ securitySchemes: {},
55
+ parameters: {},
56
+ responses: {},
57
+ headers: {},
58
+ requestBodies: {}
59
+ };
60
+
61
+ if (resolved.definitions) {
62
+ for (const [k, v] of Object.entries(resolved.definitions)) {
63
+ components.schemas[k] = normalizeSchema(v as any);
64
+ }
65
+ }
66
+
67
+ const securitySchemes = resolved.securityDefinitions || {};
68
+ for (const [k, v] of Object.entries(securitySchemes)) {
69
+ components.securitySchemes[k] = v as any;
70
+ }
71
+
72
+ const endpoints: Endpoint[] = [];
73
+ const paths = resolved.paths || {};
74
+ const globalSecurity = resolved.security || [];
75
+
76
+ const allowedMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
77
+
78
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
79
+ if (!pathItem || typeof pathItem !== 'object') continue;
80
+
81
+ const pathParams = Array.isArray((pathItem as any).parameters) ? (pathItem as any).parameters : [];
82
+
83
+ for (const methodStr of Object.keys(pathItem)) {
84
+ if (!allowedMethods.includes(methodStr)) continue;
85
+ const op = (pathItem as any)[methodStr];
86
+ if (!op || typeof op !== 'object') continue;
87
+
88
+ const method = methodStr.toUpperCase() as HttpMethod;
89
+ const id = `${method}:${normalizePathForId(pathStr)}`;
90
+
91
+ const opParams = Array.isArray(op.parameters) ? op.parameters : [];
92
+ const { parameters, requestBody } = buildParametersAndBody(pathParams, opParams, resolved.consumes || op.consumes || ['application/json']);
93
+
94
+ const endpoint: Endpoint = {
95
+ id,
96
+ path: pathStr,
97
+ method,
98
+ summary: op.summary,
99
+ description: op.description,
100
+ operationId: op.operationId,
101
+ tags: Array.isArray(op.tags) ? op.tags : [],
102
+ deprecated: !!op.deprecated,
103
+ security: op.security || globalSecurity,
104
+ parameters,
105
+ requestBody,
106
+ responses: buildResponses(op.responses, resolved.produces || op.produces || ['application/json']),
107
+ extensions: extractExtensions(op)
108
+ };
109
+
110
+ endpoints.push(endpoint);
111
+ }
112
+ }
113
+
114
+ return {
115
+ meta,
116
+ servers,
117
+ endpoints,
118
+ components,
119
+ security: Object.values(components.securitySchemes)
120
+ } as unknown as NormalizedAST;
121
+ }
122
+ }
123
+
124
+ function normalizePathForId(path: string): string {
125
+ return path
126
+ .toLowerCase()
127
+ .replace(/\/$/, '')
128
+ .replace(/\{[^}]+\}/g, '{*}');
129
+ }
130
+
131
+ function buildParametersAndBody(pathParams: any[], opParams: any[], consumes: string[]): { parameters: Parameter[], requestBody?: RequestBody } {
132
+ const paramMap = new Map<string, Parameter>();
133
+ let requestBody: RequestBody | undefined = undefined;
134
+
135
+ const allParams = [...pathParams, ...opParams];
136
+
137
+ for (const p of allParams) {
138
+ if (p.in === 'body') {
139
+ const content: Record<string, any> = {};
140
+ for (const mime of consumes) {
141
+ content[mime] = { schema: normalizeSchema(p.schema || {}) };
142
+ }
143
+ requestBody = {
144
+ required: !!p.required,
145
+ description: p.description,
146
+ content
147
+ };
148
+ } else if (p.in === 'formData') {
149
+ // In OpenAPI 3, formData becomes requestBody with application/x-www-form-urlencoded or multipart/form-data
150
+ if (!requestBody) {
151
+ requestBody = { required: false, content: {} };
152
+ }
153
+ const mime = consumes.includes('multipart/form-data') ? 'multipart/form-data' : 'application/x-www-form-urlencoded';
154
+ if (!requestBody.content[mime]) {
155
+ requestBody.content[mime] = { schema: { type: 'object', properties: {} } };
156
+ }
157
+ const schema = requestBody.content[mime].schema;
158
+ if (!schema.properties) schema.properties = {};
159
+ schema.properties[p.name] = normalizeSchema(p);
160
+ if (p.required) {
161
+ if (!schema.required) schema.required = [];
162
+ schema.required.push(p.name);
163
+ }
164
+ } else {
165
+ paramMap.set(`${p.name}:${p.in}`, {
166
+ name: p.name,
167
+ in: p.in as any,
168
+ required: !!p.required,
169
+ deprecated: false,
170
+ description: p.description,
171
+ schema: normalizeSchema(p)
172
+ });
173
+ }
174
+ }
175
+
176
+ return { parameters: Array.from(paramMap.values()), requestBody };
177
+ }
178
+
179
+ function buildResponses(responses: any, produces: string[]): ResponseDef[] {
180
+ if (!responses || typeof responses !== 'object') return [];
181
+ const res: ResponseDef[] = [];
182
+ for (const [code, r] of Object.entries<any>(responses)) {
183
+ const content: Record<string, any> = {};
184
+ if (r.schema) {
185
+ for (const mime of produces) {
186
+ content[mime] = { schema: normalizeSchema(r.schema) };
187
+ }
188
+ }
189
+ const headers: Record<string, any> = {};
190
+ if (r.headers) {
191
+ for (const [k, v] of Object.entries<any>(r.headers)) {
192
+ headers[k] = {
193
+ schema: normalizeSchema(v as any),
194
+ description: (v as any).description
195
+ };
196
+ }
197
+ }
198
+ res.push({
199
+ statusCode: code,
200
+ description: r.description,
201
+ content,
202
+ headers
203
+ });
204
+ }
205
+ return res;
206
+ }
207
+
208
+ function extractExtensions(obj: any): Record<string, unknown> {
209
+ const ext: Record<string, unknown> = {};
210
+ if (obj && typeof obj === 'object') {
211
+ for (const k of Object.keys(obj)) {
212
+ if (k.startsWith('x-')) {
213
+ ext[k] = obj[k];
214
+ }
215
+ }
216
+ }
217
+ return ext;
218
+ }
@@ -0,0 +1,223 @@
1
+ import { resolveRefs } from './ref-resolver.js';
2
+ import { normalizeSchema } from './schema-normalizer.js';
3
+ import { normalizeSecurity } from './security-normalizer.js';
4
+ import type { NormalizedAST, Endpoint, Parameter, RequestBody, ResponseDef, ComponentMap, HttpMethod } from '../../types/index.js';
5
+ import type { RawSpec } from '../../loader/index.js';
6
+ import type { ISpecParser } from '../base.js';
7
+ import { ParseError } from '../../types/errors.js';
8
+ import yaml from 'js-yaml';
9
+
10
+ export class OpenApi3Parser implements ISpecParser {
11
+ readonly format = 'openapi3';
12
+
13
+ canParse(raw: RawSpec): boolean {
14
+ return raw.format === 'openapi3';
15
+ }
16
+
17
+ async parse(raw: RawSpec): Promise<NormalizedAST> {
18
+ let parsed: any;
19
+ try {
20
+ parsed = JSON.parse(raw.content);
21
+ } catch {
22
+ try {
23
+ parsed = yaml.load(raw.content);
24
+ } catch (err: any) {
25
+ throw new ParseError('Failed to parse OpenAPI 3.x spec', undefined, undefined, raw.sourcePath, err);
26
+ }
27
+ }
28
+
29
+ if (!parsed || typeof parsed !== 'object' || !parsed.openapi || !parsed.openapi.startsWith('3.')) {
30
+ throw new ParseError('Not a valid OpenAPI 3.x spec', undefined, undefined, raw.sourcePath);
31
+ }
32
+
33
+ const resolved = resolveRefs(parsed, parsed, raw.sourcePath);
34
+
35
+ const meta = {
36
+ title: resolved.info?.title || 'Unknown API',
37
+ version: resolved.info?.version || '1.0.0',
38
+ format: 'openapi3' as const,
39
+ rawVersion: resolved.openapi
40
+ };
41
+
42
+ const servers = (resolved.servers || []).map((s: any) => ({
43
+ url: s.url,
44
+ description: s.description,
45
+ variables: s.variables
46
+ }));
47
+
48
+ const components: ComponentMap = {
49
+ schemas: {},
50
+ securitySchemes: {},
51
+ parameters: {},
52
+ responses: {},
53
+ headers: {},
54
+ requestBodies: {}
55
+ };
56
+
57
+ if (resolved.components?.schemas) {
58
+ for (const [k, v] of Object.entries(resolved.components.schemas)) {
59
+ components.schemas[k] = normalizeSchema(v);
60
+ }
61
+ }
62
+
63
+ const securitySchemes = resolved.components?.securitySchemes || {};
64
+ for (const [k, v] of Object.entries(securitySchemes)) {
65
+ components.securitySchemes[k] = v as any;
66
+ }
67
+
68
+ const endpoints: Endpoint[] = [];
69
+ const paths = resolved.paths || {};
70
+
71
+ const globalSecurity = resolved.security;
72
+
73
+ const allowedMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
74
+
75
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
76
+ if (!pathItem || typeof pathItem !== 'object') continue;
77
+
78
+ const pathParams = Array.isArray((pathItem as any).parameters) ? (pathItem as any).parameters : [];
79
+
80
+ for (const methodStr of Object.keys(pathItem)) {
81
+ if (!allowedMethods.includes(methodStr)) continue;
82
+ const op = pathItem[methodStr as keyof typeof pathItem] as any;
83
+ if (!op || typeof op !== 'object') continue;
84
+
85
+ const method = methodStr.toUpperCase() as HttpMethod;
86
+ const id = `${method}:${normalizePathForId(pathStr)}`;
87
+
88
+ const opParams = Array.isArray(op.parameters) ? op.parameters : [];
89
+ const parameters = buildParameters(pathParams, opParams);
90
+
91
+ const security = op.security !== undefined
92
+ ? normalizeSecurity(op.security)
93
+ : normalizeSecurity(globalSecurity);
94
+
95
+ const endpoint: Endpoint = {
96
+ id,
97
+ path: pathStr,
98
+ method,
99
+ summary: op.summary,
100
+ description: op.description,
101
+ operationId: op.operationId,
102
+ tags: Array.isArray(op.tags) ? op.tags : [],
103
+ deprecated: !!op.deprecated,
104
+ security,
105
+ parameters,
106
+ requestBody: op.requestBody ? buildRequestBody(op.requestBody) : undefined,
107
+ responses: buildResponses(op.responses),
108
+ extensions: extractExtensions(op)
109
+ };
110
+
111
+ endpoints.push(endpoint);
112
+ }
113
+ }
114
+
115
+ return {
116
+ meta,
117
+ servers,
118
+ endpoints,
119
+ components,
120
+ security: Object.values(components.securitySchemes)
121
+ };
122
+ }
123
+ }
124
+
125
+ function normalizePathForId(path: string): string {
126
+ return path
127
+ .toLowerCase()
128
+ .replace(/\/$/, '')
129
+ .replace(/\{[^}]+\}/g, '{*}');
130
+ }
131
+
132
+ function buildParameters(pathParams: any[], opParams: any[]): Parameter[] {
133
+ const paramMap = new Map<string, Parameter>();
134
+
135
+ for (const p of pathParams) {
136
+ paramMap.set(`${p.name}:${p.in}`, {
137
+ name: p.name,
138
+ in: p.in,
139
+ required: !!p.required,
140
+ deprecated: !!p.deprecated,
141
+ description: p.description,
142
+ schema: normalizeSchema(p.schema || {}),
143
+ example: p.example
144
+ });
145
+ }
146
+
147
+ for (const p of opParams) {
148
+ paramMap.set(`${p.name}:${p.in}`, {
149
+ name: p.name,
150
+ in: p.in,
151
+ required: !!p.required,
152
+ deprecated: !!p.deprecated,
153
+ description: p.description,
154
+ schema: normalizeSchema(p.schema || {}),
155
+ example: p.example
156
+ });
157
+ }
158
+
159
+ return Array.from(paramMap.values());
160
+ }
161
+
162
+ function buildRequestBody(rb: any): RequestBody {
163
+ const content: Record<string, any> = {};
164
+ if (rb.content) {
165
+ for (const [k, v] of Object.entries<any>(rb.content)) {
166
+ content[k] = {
167
+ schema: normalizeSchema(v.schema || {}),
168
+ example: v.example
169
+ };
170
+ }
171
+ }
172
+ return {
173
+ required: !!rb.required,
174
+ description: rb.description,
175
+ content
176
+ };
177
+ }
178
+
179
+ function buildResponses(responses: any): ResponseDef[] {
180
+ if (!responses || typeof responses !== 'object') return [];
181
+ const res: ResponseDef[] = [];
182
+ for (const [code, r] of Object.entries<any>(responses)) {
183
+ const content: Record<string, any> = {};
184
+ if (r.content) {
185
+ for (const [k, v] of Object.entries<any>(r.content)) {
186
+ content[k] = {
187
+ schema: normalizeSchema(v.schema || {}),
188
+ example: v.example
189
+ };
190
+ }
191
+ }
192
+ const headers: Record<string, any> = {};
193
+ if (r.headers) {
194
+ for (const [k, v] of Object.entries<any>(r.headers)) {
195
+ headers[k] = {
196
+ required: !!v.required,
197
+ deprecated: !!v.deprecated,
198
+ schema: normalizeSchema(v.schema || {}),
199
+ description: v.description
200
+ };
201
+ }
202
+ }
203
+ res.push({
204
+ statusCode: code,
205
+ description: r.description,
206
+ content,
207
+ headers
208
+ });
209
+ }
210
+ return res;
211
+ }
212
+
213
+ function extractExtensions(obj: any): Record<string, unknown> {
214
+ const ext: Record<string, unknown> = {};
215
+ if (obj && typeof obj === 'object') {
216
+ for (const k of Object.keys(obj)) {
217
+ if (k.startsWith('x-')) {
218
+ ext[k] = obj[k];
219
+ }
220
+ }
221
+ }
222
+ return ext;
223
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve, dirname } from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ import { RefError } from '../../types/errors.js';
5
+
6
+ const fileCache = new Map<string, any>();
7
+
8
+ export function resolveRefs(obj: any, root: any, sourcePath?: string): any {
9
+ if (!obj || typeof obj !== 'object') return obj;
10
+
11
+ const stack: Array<{ node: any; parent: any | null; key: string | null }> = [
12
+ { node: obj, parent: null, key: null }
13
+ ];
14
+ const visitedRefs = new Set<string>();
15
+ const visitedNodes = new WeakSet<any>();
16
+
17
+ while (stack.length > 0) {
18
+ const current = stack.pop()!;
19
+ const { node, parent, key } = current;
20
+
21
+ if (!node || typeof node !== 'object') continue;
22
+
23
+ if (visitedNodes.has(node)) continue;
24
+ visitedNodes.add(node);
25
+
26
+ if (Array.isArray(node)) {
27
+ for (let i = node.length - 1; i >= 0; i--) {
28
+ stack.push({ node: node[i], parent: node, key: i.toString() });
29
+ }
30
+ continue;
31
+ }
32
+
33
+ if ('$ref' in node && typeof node.$ref === 'string') {
34
+ const ref = node.$ref;
35
+
36
+ if (visitedRefs.has(ref)) {
37
+ if (parent && key !== null) {
38
+ parent[key] = { ...node, $circular: true };
39
+ }
40
+ continue;
41
+ }
42
+ visitedRefs.add(ref);
43
+
44
+ let resolved: any;
45
+ if (ref.startsWith('#')) {
46
+ resolved = resolveLocalRef(ref, root);
47
+ } else if (ref.startsWith('./') || ref.startsWith('../')) {
48
+ if (!sourcePath) {
49
+ throw new RefError(`Cannot resolve relative ref without sourcePath`, ref);
50
+ }
51
+ const refPath = resolve(dirname(sourcePath), ref.split('#')[0]);
52
+ try {
53
+ let parsed = fileCache.get(refPath);
54
+ if (!parsed) {
55
+ const content = readFileSync(refPath, 'utf8');
56
+ parsed = refPath.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
57
+ fileCache.set(refPath, parsed);
58
+ }
59
+ resolved = ref.includes('#') ? resolveLocalRef('#' + ref.split('#')[1], parsed) : parsed;
60
+ } catch (err: any) {
61
+ throw new RefError(`Failed to load external ref`, ref, err);
62
+ }
63
+ } else if (ref.startsWith('http://') || ref.startsWith('https://')) {
64
+ throw new RefError(`URL refs not implemented synchronously`, ref);
65
+ } else {
66
+ throw new RefError(`Unsupported ref format`, ref);
67
+ }
68
+
69
+ if (parent && key !== null) {
70
+ parent[key] = resolved;
71
+ stack.push({ node: resolved, parent, key });
72
+ }
73
+ continue;
74
+ }
75
+
76
+ const keys = Object.keys(node);
77
+ for (let i = keys.length - 1; i >= 0; i--) {
78
+ const k = keys[i];
79
+ if (k !== '$ref' && typeof node[k] === 'object' && node[k] !== null) {
80
+ stack.push({ node: node[k], parent: node, key: k });
81
+ }
82
+ }
83
+ }
84
+
85
+ return obj;
86
+ }
87
+
88
+ function resolveLocalRef(ref: string, root: any): any {
89
+ const parts = ref.replace(/^#\/?/, '').split('/');
90
+ let current = root;
91
+ for (const part of parts) {
92
+ if (!part) continue;
93
+ const key = part.replace(/~1/g, '/').replace(/~0/g, '~');
94
+ if (current && typeof current === 'object' && key in current) {
95
+ current = current[key];
96
+ } else {
97
+ throw new RefError(`Local ref not found`, ref);
98
+ }
99
+ }
100
+ return current;
101
+ }
@@ -0,0 +1,52 @@
1
+ import type { Schema } from '../../types/index.js';
2
+
3
+ export function normalizeSchema(schema: any): Schema {
4
+ if (!schema || typeof schema !== 'object') return schema;
5
+
6
+ if (Array.isArray(schema.allOf)) {
7
+ const merged = flattenAllOf(schema.allOf);
8
+ schema = { ...schema, ...merged };
9
+ delete schema.allOf;
10
+ }
11
+
12
+ if (schema.nullable === true) {
13
+ if (typeof schema.type === 'string') {
14
+ schema.type = [schema.type, 'null'];
15
+ } else if (Array.isArray(schema.type) && !schema.type.includes('null')) {
16
+ schema.type = [...schema.type, 'null'];
17
+ }
18
+ delete schema.nullable;
19
+ }
20
+
21
+ if (schema.properties) {
22
+ for (const key of Object.keys(schema.properties)) {
23
+ schema.properties[key] = normalizeSchema(schema.properties[key]);
24
+ }
25
+ }
26
+ if (schema.items) {
27
+ schema.items = normalizeSchema(schema.items);
28
+ }
29
+ if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
30
+ schema.additionalProperties = normalizeSchema(schema.additionalProperties);
31
+ }
32
+ if (Array.isArray(schema.oneOf)) {
33
+ schema.oneOf = schema.oneOf.map(normalizeSchema);
34
+ }
35
+ if (Array.isArray(schema.anyOf)) {
36
+ schema.anyOf = schema.anyOf.map(normalizeSchema);
37
+ }
38
+
39
+ return schema as Schema;
40
+ }
41
+
42
+ function flattenAllOf(schemas: any[]): any {
43
+ return schemas.reduce((merged, subSchema) => {
44
+ const normalized = normalizeSchema(subSchema);
45
+ return {
46
+ ...merged,
47
+ ...normalized,
48
+ properties: { ...(merged.properties || {}), ...(normalized.properties || {}) },
49
+ required: [...new Set([...(merged.required || []), ...(normalized.required || [])])]
50
+ };
51
+ }, {});
52
+ }
@@ -0,0 +1,18 @@
1
+ import type { SecurityRequirement } from '../../types/index.js';
2
+
3
+ export function normalizeSecurity(securityArray?: any[]): SecurityRequirement[] {
4
+ if (!securityArray || !Array.isArray(securityArray)) {
5
+ return [];
6
+ }
7
+
8
+ const result: SecurityRequirement[] = [];
9
+ for (const req of securityArray) {
10
+ for (const [schemeId, scopes] of Object.entries(req)) {
11
+ result.push({
12
+ schemeId,
13
+ scopes: Array.isArray(scopes) ? scopes.map(String) : []
14
+ });
15
+ }
16
+ }
17
+ return result;
18
+ }