Skip to content

Commit 4e0b538

Browse files
committed
Continue implementing OpenAPI 3.2.0
1 parent 11580fb commit 4e0b538

21 files changed

+1517
-676
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/parser.ts

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ import {
1212
ServerObject,
1313
SwaggerDefinition,
1414
SwaggerSpec
15-
} from './types.js';
15+
} from './types/index.js';
1616
import * as fs from 'node:fs';
1717
import * as path from 'node:path';
1818
import { pathToFileURL } from 'node:url';
1919
import yaml from 'js-yaml';
20-
import { extractPaths, isUrl, pascalCase } from './utils.js';
20+
import { extractPaths, isUrl, pascalCase } from './utils/index.js';
2121
import { validateSpec } from './validator.js';
2222
import { JSON_SCHEMA_2020_12_DIALECT, OAS_3_1_DIALECT } from './constants.js';
2323

@@ -132,9 +132,15 @@ export class SwaggerParser {
132132
// If a cache isn't provided, create one with just the entry spec.
133133
this.specCache = specCache || new Map<string, SwaggerSpec>([[this.documentUri, spec]]);
134134

135-
// Ensure the entry spec's internal $ids are indexed if constructed manually without the factory
135+
// Ensure the entry spec's internal $ids and $anchors are indexed if constructed manually without the factory
136136
if (!specCache) {
137-
SwaggerParser.indexSchemaIds(spec, documentUri, this.specCache);
137+
const baseUri = spec.$self ? new URL(spec.$self, documentUri).href : documentUri;
138+
// Aliasing: if $self differs from the retrieval URI, we map the baseUri to the spec too
139+
// so it can be resolved by its logical ID.
140+
if (baseUri !== documentUri) {
141+
this.specCache.set(baseUri, spec);
142+
}
143+
SwaggerParser.indexSchemaIds(spec, baseUri, this.specCache);
138144
}
139145

140146
this.schemas = Object.entries(this.getDefinitions()).map(([name, definition]) => ({
@@ -187,7 +193,13 @@ export class SwaggerParser {
187193

188194
const baseUri = spec.$self ? new URL(spec.$self, uri).href : uri;
189195

190-
// Important: Index any `$id` properties within the document immediately.
196+
// Aliasing: map the logical base URI to the spec as well, if different.
197+
// This ensures lookups via $ref using the ID/Self URI find the cached object.
198+
if (baseUri !== uri) {
199+
cache.set(baseUri, spec);
200+
}
201+
202+
// Important: Index any `$id` and `$anchor` properties within the document immediately.
191203
// This allows internal sub-schemas to be referenced by their global URI (OAS 3.1 / JSON Schema).
192204
this.indexSchemaIds(spec, baseUri, cache);
193205

@@ -206,9 +218,9 @@ export class SwaggerParser {
206218
}
207219

208220
/**
209-
* Traverses a document structure to find JSON Schema `$id` keywords.
210-
* Maps the resolved absolute URI of the ID to the schema configuration object in the cache.
211-
* This creates "virtual" documents in the cache, supporting `$ref` resolution by ID.
221+
* Traverses a document structure to find JSON Schema `$id`, `$anchor` and `$dynamicAnchor` keywords.
222+
* Maps the resolved absolute URI of the ID or anchor to the schema object in the cache.
223+
* This creates "virtual" endpoints in the cache, supporting `$ref` resolution by ID or anchor fragment.
212224
*
213225
* @param spec The document root or fragment to traverse.
214226
* @param baseUri The current base URI of the scope.
@@ -226,21 +238,47 @@ export class SwaggerParser {
226238
let nextBase = currentBase;
227239

228240
// Check for $id (OAS 3.1 / JSON Schema definition)
241+
// A schema with an `$id` changes the resolution scope for itself and its children.
229242
if ('$id' in obj && typeof obj.$id === 'string') {
230243
try {
231244
// Resolve $id against the current base.
232-
// $id can be relative or absolute.
233-
// If absolute, it resets the base.
245+
// $id can be relative (rare) or absolute.
234246
nextBase = new URL(obj.$id, currentBase).href;
235247

236-
// Cache this object as a distinct "document" at this URI
248+
// Cache this object as a distinct "document" at this URI.
237249
// We use cast because cache expects SwaggerSpec, but these are Schema definitions.
238-
// Our resolution logic handles both.
250+
// Our resolution logic handles both types.
239251
if (!cache.has(nextBase)) {
240252
cache.set(nextBase, obj as SwaggerSpec);
241253
}
242254
} catch (e) {
243-
// Ignore invalid $id values
255+
// Ignore invalid $id values, proceed with current scope
256+
}
257+
}
258+
259+
// Check for $anchor (OAS 3.1 / JSON Schema 2020-12)
260+
// An anchor creates a unique identifier for the schema within the *current* base scope.
261+
// Syntax: The URI ref is `currentBase#anchorName`.
262+
if ('$anchor' in obj && typeof obj.$anchor === 'string') {
263+
// Anchors strictly must not contain '#'.
264+
const anchor = obj.$anchor;
265+
const anchorUri = `${nextBase}#${anchor}`;
266+
267+
// Cache this specific object at the fully resolved anchor URI
268+
if (!cache.has(anchorUri)) {
269+
cache.set(anchorUri, obj as SwaggerSpec);
270+
}
271+
}
272+
273+
// Check for $dynamicAnchor (OAS 3.1 / JSON Schema 2020-12)
274+
// Similar to $anchor, but primarily for dynamic referencing.
275+
// For static resolution purposes, we treat it like a standard anchor target.
276+
if ('$dynamicAnchor' in obj && typeof obj.$dynamicAnchor === 'string') {
277+
const anchor = obj.$dynamicAnchor;
278+
const anchorUri = `${nextBase}#${anchor}`;
279+
280+
if (!cache.has(anchorUri)) {
281+
cache.set(anchorUri, obj as SwaggerSpec);
244282
}
245283
}
246284

@@ -440,20 +478,34 @@ export class SwaggerParser {
440478
const currentDocSpec = this.specCache.get(currentDocUri);
441479

442480
// logicalBaseUri calculation: checks $self first, then falls back to current physical URI.
443-
// NOTE: For $id resolution, the cache key IS the $id (resolved), so looking up by URI works
444-
// naturally if `indexSchemaIds` has populated the cache.
445481
const logicalBaseUri = currentDocSpec?.$self ? new URL(currentDocSpec.$self, currentDocUri).href : currentDocUri;
446482

447483
// The target file's physical URI is resolved using the logical base.
448-
// If ref is absolute (e.g. based on $id), URL construction handles it correctly.
449-
const targetFileUri = filePath ? new URL(filePath, logicalBaseUri).href : currentDocUri;
484+
// If ref is absolute (e.g. based on $id or http URI), URL construction handles it correctly.
485+
const targetUri = filePath ? new URL(filePath, logicalBaseUri).href : logicalBaseUri;
486+
487+
// 1. Direct Cache Lookup (Supports $id and $anchor lookups)
488+
// If the ref points to a known $id or $anchor (e.g. http://full.uri#anchor),
489+
// `targetUri` will contain the base and `jsonPointer` will contain the anchor name.
490+
// We construct the potential key and check the cache.
491+
// Note: jsonPointer here might be an actual pointer path OR an anchor name.
492+
// Parser's indexSchemaIds puts anchors in the cache as "baseUri#anchor".
493+
const fullUriKey = jsonPointer ? `${targetUri}#${jsonPointer}` : targetUri;
494+
495+
if (this.specCache.has(fullUriKey)) {
496+
return this.specCache.get(fullUriKey) as unknown as T;
497+
}
450498

451-
const targetSpec = this.specCache.get(targetFileUri);
499+
// 2. Spec File Cache Lookup - Fallback to traversing the document
500+
const targetSpec = this.specCache.get(targetUri);
452501
if (!targetSpec) {
453-
console.warn(`[Parser] Unresolved external file reference: ${targetFileUri}. File was not pre-loaded.`);
502+
console.warn(`[Parser] Unresolved external file reference: ${targetUri}. File was not pre-loaded.`);
454503
return undefined;
455504
}
456505

506+
// 3. JSON Pointer Traversal
507+
// If we are here, it means `ref` was not a direct ID or Anchor match.
508+
// We treat `jsonPointer` as a standard JSON pointer traversing the `targetSpec`.
457509
let result: any = targetSpec;
458510
if (jsonPointer) {
459511
// Gracefully handle pointers that are just "/" or empty
@@ -463,20 +515,22 @@ export class SwaggerParser {
463515
if (typeof result === 'object' && result !== null && Object.prototype.hasOwnProperty.call(result, decodedPart)) {
464516
result = result[decodedPart];
465517
} else {
466-
console.warn(`[Parser] Failed to resolve reference part "${decodedPart}" in path "${ref}" within file ${targetFileUri}`);
518+
// It's common to fail here if `jsonPointer` was actually an anchor name that wasn't indexed.
519+
// But if indexSchemaIds ran correctly, we should have hit Step 1.
520+
console.warn(`[Parser] Failed to resolve reference part "${decodedPart}" in path "${ref}" within file ${targetUri}`);
467521
return undefined;
468522
}
469523
}
470524
}
471525

472526
// Handle nested $refs recursively, passing the physical URI of the new document context.
473527
if (isRefObject(result)) {
474-
return this.resolveReference(result.$ref, targetFileUri);
528+
return this.resolveReference(result.$ref, targetUri);
475529
}
476530

477531
// Handle nested $dynamicRefs recursively
478532
if (isDynamicRefObject(result)) {
479-
return this.resolveReference(result.$dynamicRef, targetFileUri);
533+
return this.resolveReference(result.$dynamicRef, targetUri);
480534
}
481535

482536
return result as T;

src/core/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,16 @@ export interface SwaggerDefinition {
384384
not?: SwaggerDefinition;
385385
contentEncoding?: string;
386386
contentMediaType?: string;
387+
/**
388+
* The schema of the content-encoded string. (OAS 3.1 / JSON Schema 2019-09)
389+
* Used when `contentMediaType` indicates a format that can be further described (e.g. application/json).
390+
*/
391+
contentSchema?: SwaggerDefinition;
392+
/**
393+
* strict validation for properties not defined in schema.
394+
* If false, no additional properties are allowed (stricter than additionalProperties: false in some contexts with inheritance).
395+
*/
396+
unevaluatedProperties?: SwaggerDefinition | boolean;
387397

388398
$ref?: string;
389399
/** Dynamic Reference used in OpenAPI 3.1 (JSON Schema 2020-12) */

src/core/types/analysis.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
ExternalDocumentationObject,
3+
HeaderObject,
4+
LinkObject,
5+
Parameter,
6+
PathItem,
7+
ServerObject,
8+
SwaggerDefinition
9+
} from "./openapi.js";
10+
11+
// ===================================================================================
12+
// SECTION: Derived Types for Admin UI Generation & Parsing
13+
// ===================================================================================
14+
15+
/** A single encoding definition for a multipart property. */
16+
export interface EncodingProperty {
17+
contentType?: string;
18+
headers?: Record<string, any>;
19+
style?: string;
20+
explode?: boolean;
21+
allowReserved?: boolean;
22+
[key: string]: any;
23+
}
24+
25+
/** Represents the request body of an operation. */
26+
export interface RequestBody {
27+
required?: boolean;
28+
content?: Record<string, {
29+
schema?: SwaggerDefinition | { $ref: string };
30+
encoding?: Record<string, EncodingProperty>;
31+
}>;
32+
[key: string]: any;
33+
}
34+
35+
/** Represents a single response from an API Operation. */
36+
export interface SwaggerResponse {
37+
description?: string;
38+
content?: Record<string, { schema?: SwaggerDefinition | { $ref: string } }>;
39+
links?: Record<string, LinkObject | { $ref: string }>;
40+
headers?: Record<string, HeaderObject | { $ref: string }>;
41+
[key: string]: any;
42+
}
43+
44+
/** A processed, unified representation of a single API operation (e.g., GET /users/{id}). */
45+
export interface PathInfo {
46+
path: string;
47+
method: string;
48+
operationId?: string;
49+
summary?: string;
50+
description?: string;
51+
deprecated?: boolean;
52+
externalDocs?: ExternalDocumentationObject;
53+
tags?: string[];
54+
consumes?: string[];
55+
parameters?: Parameter[];
56+
requestBody?: RequestBody;
57+
responses?: Record<string, SwaggerResponse>;
58+
methodName?: string;
59+
security?: { [key: string]: string[] }[];
60+
servers?: ServerObject[] | undefined;
61+
callbacks?: Record<string, PathItem | { $ref: string }>;
62+
[key: string]: any;
63+
}
64+
65+
/** A processed representation of an API operation, classified for UI generation. */
66+
export interface ResourceOperation {
67+
action: 'list' | 'create' | 'getById' | 'update' | 'delete' | string;
68+
path: string;
69+
method: string;
70+
operationId?: string;
71+
methodName?: string;
72+
methodParameters?: Parameter[];
73+
isCustomItemAction?: boolean;
74+
isCustomCollectionAction?: boolean;
75+
}
76+
77+
/** Represents a logical API resource (e.g., "Users"), derived by grouping related paths. */
78+
export interface Resource {
79+
name: string;
80+
modelName: string;
81+
operations: ResourceOperation[];
82+
isEditable: boolean;
83+
formProperties: FormProperty[];
84+
listProperties: FormProperty[];
85+
}
86+
87+
/** A simple wrapper for a property's name and its underlying schema, used for templating. */
88+
export interface FormProperty {
89+
name: string;
90+
schema: SwaggerDefinition;
91+
}

src/core/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './openapi.js';
2+
export * from './analysis.js';

0 commit comments

Comments
 (0)