@usearazzo/resolver 1.0.1-alpha.10

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.
@@ -0,0 +1,224 @@
1
+ import { isParseResultElement } from '@speclynx/apidom-datamodel';
2
+ import { url, resolve as resolveURI, resolveApiDOM as resolveApiDOMElement, mergeOptions, UnmatchedResolveStrategyError } from '@speclynx/apidom-reference/configuration/empty';
3
+ import OpenAPI2ResolveStrategy from '@speclynx/apidom-reference/resolve/strategies/openapi-2';
4
+ import OpenAPI3_0ResolveStrategy from '@speclynx/apidom-reference/resolve/strategies/openapi-3-0';
5
+ import OpenAPI3_1ResolveStrategy from '@speclynx/apidom-reference/resolve/strategies/openapi-3-1';
6
+ import { isSwaggerElement, mediaTypes as openApi2MediaTypes } from '@speclynx/apidom-ns-openapi-2';
7
+ import { isOpenApi3_0Element, mediaTypes as openApi3_0MediaTypes } from '@speclynx/apidom-ns-openapi-3-0';
8
+ import { isOpenApi3_1Element, mediaTypes as openApi3_1MediaTypes } from '@speclynx/apidom-ns-openapi-3-1';
9
+ import { defaultOptions as dereferenceDefaultOptions } from "../dereference/openapi.mjs";
10
+ import ResolveError from "../errors/ResolveError.mjs";
11
+ /**
12
+ * Options for resolving OpenAPI Documents.
13
+ * @public
14
+ */
15
+ /**
16
+ * Default reference options for resolving OpenAPI Documents.
17
+ *
18
+ * The resolve strategies delegate to the dereference strategies of the same name,
19
+ * so the dereference defaults are carried over and only the resolve strategies are added.
20
+ * @public
21
+ */
22
+ export const defaultOptions = {
23
+ resolve: {
24
+ resolvers: [...dereferenceDefaultOptions.resolve.resolvers],
25
+ strategies: [new OpenAPI2ResolveStrategy(), new OpenAPI3_0ResolveStrategy(), new OpenAPI3_1ResolveStrategy()]
26
+ },
27
+ parse: {
28
+ parsers: [...dereferenceDefaultOptions.parse.parsers],
29
+ parserOpts: {
30
+ ...dereferenceDefaultOptions.parse.parserOpts
31
+ }
32
+ },
33
+ dereference: {
34
+ strategies: [...dereferenceDefaultOptions.dereference.strategies],
35
+ strategyOpts: {
36
+ ...dereferenceDefaultOptions.dereference.strategyOpts
37
+ }
38
+ }
39
+ };
40
+
41
+ /**
42
+ * Resolves an OpenAPI Document from a file system path or HTTP(S) URL.
43
+ *
44
+ * This function collects the OpenAPI Document and every external document its
45
+ * JSON References ($ref) reach into a ReferenceSet. Nothing is dereferenced;
46
+ * the root reference of the set holds the parsed entry document.
47
+ *
48
+ * Supports OpenAPI 2.0 (Swagger), OpenAPI 3.0.x, and OpenAPI 3.1.x.
49
+ *
50
+ * The `dereference.refSet` option is ignored: resolving always builds a fresh ReferenceSet.
51
+ *
52
+ * @param uri - A file system path or HTTP(S) URL to the OpenAPI Document
53
+ * @param options - Reference options (uses defaultOptions when not provided)
54
+ * @returns A promise that resolves to the ReferenceSet of the OpenAPI Document and its external references
55
+ * @throws ResolveError - When resolving fails or document is not an OpenAPI specification. The original error is available via the `cause` property.
56
+ *
57
+ * @example
58
+ * // Resolve from file
59
+ * const refSet = await resolveOpenAPI('/path/to/openapi.json');
60
+ *
61
+ * @example
62
+ * // Resolve from URL
63
+ * const refSet = await resolveOpenAPI('https://example.com/openapi.yaml');
64
+ *
65
+ * @example
66
+ * // Resolve with custom options
67
+ * const refSet = await resolveOpenAPI('/path/to/openapi.json', customReferenceOptions);
68
+ * @public
69
+ */
70
+ export async function resolve(uri, options = {}) {
71
+ const mergedOptions = mergeOptions(defaultOptions, options);
72
+
73
+ // a relative file system path resolves against the current working directory; the string
74
+ // doubles as the base URI for relative references, where a relative base resolves against
75
+ // the filesystem root instead of the document. A leading slash or backslash (UNC,
76
+ // drive-rooted) is already rooted and passes through untouched. `uri` itself is left as
77
+ // is so error messages below quote what the caller passed in.
78
+ const retrievalURI = !url.isHttpUrl(uri) && url.getProtocol(uri) !== 'file' && !/^[\\/]/.test(uri) ? url.resolve(url.fromFileSystemPath(url.cwd()), uri) : uri;
79
+ try {
80
+ const refSet = await resolveURI(retrievalURI, withoutSeededRefSet(mergedOptions));
81
+ const parseResult = refSet.rootRef?.value;
82
+
83
+ // validate that the resolved document is an OpenAPI specification
84
+ if (!isParseResultElement(parseResult) || !isOpenApiElement(parseResult.api)) {
85
+ throw new UnmatchedResolveStrategyError(`Could not find a resolve strategy that can resolve "${uri}" as an OpenAPI specification`);
86
+ }
87
+ parseResult.meta.set('retrievalURI', retrievalURI);
88
+ return refSet;
89
+ } catch (error) {
90
+ throw new ResolveError(`Failed to resolve OpenAPI Document at "${uri}"`, {
91
+ cause: error
92
+ });
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolves an ApiDOM element representing an OpenAPI Document.
98
+ *
99
+ * This function collects the element and every external document its
100
+ * JSON References ($ref) reach into a ReferenceSet. Nothing is dereferenced
101
+ * and the element itself is left untouched.
102
+ *
103
+ * Supported scenarios:
104
+ * - ParseResultElement with retrievalURI metadata: baseURI derived automatically
105
+ * - ParseResultElement without retrievalURI: requires `options.resolve.baseURI`
106
+ * - Child element (e.g., PathItemElement) with parseResult in strategyOpts:
107
+ * requires `options.dereference.strategyOpts.parseResult`,
108
+ * and `options.resolve.baseURI` if parseResult lacks retrievalURI metadata
109
+ * - Child element without parseResult: requires `options.resolve.baseURI` and
110
+ * `options.parse.mediaType`, since the element alone cannot identify the document kind
111
+ *
112
+ * For a child element the root reference of the returned set is keyed by the root document
113
+ * URI but holds a ParseResultElement wrapping a copy of the child, not the root document.
114
+ * The set describes the references reachable from the child and is not a whole-document set.
115
+ *
116
+ * The `dereference.refSet` option is ignored: resolving always builds a fresh ReferenceSet.
117
+ *
118
+ * @param element - An ApiDOM element (ParseResultElement or child element like PathItemElement)
119
+ * @param options - Reference options (uses defaultOptions when not provided)
120
+ * @returns A promise that resolves to the ReferenceSet of the element and its external references
121
+ * @throws ResolveError - When baseURI is required but not provided, when the document is not an OpenAPI specification, or when resolving fails
122
+ *
123
+ * @example
124
+ * Resolve ParseResultElement with retrievalURI (from file parsing)
125
+ * ```typescript
126
+ * import { parseOpenAPI } from '@usearazzo/parser';
127
+ *
128
+ * const parseResult = await parseOpenAPI('/path/to/openapi.json');
129
+ * const refSet = await resolveOpenAPIElement(parseResult);
130
+ * ```
131
+ *
132
+ * @example
133
+ * Resolve ParseResultElement without retrievalURI (from inline parsing)
134
+ * ```typescript
135
+ * const parseResult = await parseOpenAPI({ openapi: '3.1.0', ... });
136
+ * const refSet = await resolveOpenAPIElement(parseResult, {
137
+ * resolve: { baseURI: 'https://example.com/openapi.json' },
138
+ * });
139
+ * ```
140
+ *
141
+ * @example
142
+ * Resolve child element (e.g., PathItemElement)
143
+ * ```typescript
144
+ * const parseResult = await parseOpenAPI('/path/to/openapi.json');
145
+ * const pathItem = parseResult.api.paths.get('/users');
146
+ * const refSet = await resolveOpenAPIElement(pathItem, {
147
+ * dereference: { strategyOpts: { parseResult } },
148
+ * });
149
+ * ```
150
+ * @public
151
+ */
152
+ export async function resolveElement(element, options = {}) {
153
+ const mergedOptions = mergeOptions(defaultOptions, options);
154
+ const subject = isParseResultElement(element) ? 'a ParseResultElement' : 'a child element';
155
+ // a child element resolves against the URI of its root document
156
+ const root = isParseResultElement(element) ? element : mergedOptions.dereference?.strategyOpts?.parseResult;
157
+ let baseURI = mergedOptions.resolve?.baseURI;
158
+ let mediaType = mergedOptions.parse?.mediaType ?? 'text/plain';
159
+ if (isParseResultElement(root)) {
160
+ if (!isOpenApiElement(root.api)) {
161
+ throw new ResolveError('Failed to resolve OpenAPI Document', {
162
+ cause: new UnmatchedResolveStrategyError(`Could not find a resolve strategy that can resolve ${subject} as an OpenAPI specification`)
163
+ });
164
+ }
165
+ mediaType = mergedOptions.parse?.mediaType ?? inferOpenApiMediaType(root.api);
166
+ if (root.hasMetaProperty('retrievalURI')) {
167
+ baseURI = root.meta.get('retrievalURI');
168
+ } else if (!baseURI) {
169
+ throw new ResolveError(`baseURI option is required when resolving ${subject} without retrievalURI metadata`);
170
+ }
171
+ } else if (!baseURI) {
172
+ throw new ResolveError('baseURI option is required when resolving a child element without parseResult in strategyOpts');
173
+ }
174
+ try {
175
+ return await resolveApiDOMElement(element, withoutSeededRefSet(mergeOptions(mergedOptions, {
176
+ resolve: {
177
+ baseURI
178
+ },
179
+ parse: {
180
+ mediaType
181
+ }
182
+ })));
183
+ } catch (error) {
184
+ throw new ResolveError('Failed to resolve OpenAPI Document', {
185
+ cause: error
186
+ });
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Drops a seeded `dereference.refSet`. The resolve strategies always build their own
192
+ * ReferenceSet and deep-merge it over the option, which strips the prototype of a
193
+ * caller-supplied instance and crashes the resolution.
194
+ */
195
+ function withoutSeededRefSet(options) {
196
+ return mergeOptions(options, {
197
+ dereference: {
198
+ refSet: null
199
+ }
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Checks if the element is a valid OpenAPI specification element.
205
+ */
206
+ function isOpenApiElement(element) {
207
+ return isSwaggerElement(element) || isOpenApi3_0Element(element) || isOpenApi3_1Element(element);
208
+ }
209
+
210
+ /**
211
+ * Gets the appropriate mediaType for an OpenAPI element.
212
+ */
213
+ function inferOpenApiMediaType(element) {
214
+ if (isSwaggerElement(element)) {
215
+ return openApi2MediaTypes.latest();
216
+ }
217
+ if (isOpenApi3_0Element(element)) {
218
+ return openApi3_0MediaTypes.latest();
219
+ }
220
+ if (isOpenApi3_1Element(element)) {
221
+ return openApi3_1MediaTypes.latest();
222
+ }
223
+ return 'text/plain';
224
+ }