Skip to content

Commit 11580fb

Browse files
committed
Continue implementing OpenAPI 3.2.0 ; [FUTURE.md] Rewrite with new roadmap, with properly constituted next steps and potential next steps
1 parent 00ea319 commit 11580fb

20 files changed

+1272
-710
lines changed

FUTURE.md

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
1-
### Roadmap & Feature Compliance
2-
3-
This project already implements a vast and powerful set of features from the OpenAPI specification, focusing on the most common use cases for modern Angular applications. The table below outlines features from the standard that are not yet implemented, ordered by priority. This can serve as a roadmap for future development.
4-
5-
| Priority | Feature | Description & Justification |
6-
| :----------------------------------------- | :------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
7-
| 🚀 **High** | Multi-Document Support (`$ref` to external files) | **What:** Allow `$ref` to point to external local or remote files (e.g., `./schemas/User.yaml`). <br/> **Why:** This is crucial for large, real-world APIs where the specification is split across multiple files for maintainability. Implementing this would dramatically increase the generator's utility for enterprise-scale projects. |
8-
| 🚀 **High** | Server Configuration (`servers` array & `variables`) | **What:** Support for the top-level `servers` array, including URL variables for templating hosts, ports, or base paths. <br/> **Why:** This is the standard way to define different environments (dev, staging, prod) and is more flexible than a single `basePath` injection token. |
9-
| 🚀 **High** | Advanced Parameter Serialization (`style`/`explode`) | **What:** Fully support the `style` and `explode` keywords on `Parameter Object`s to control how arrays and objects are formatted in query strings and paths. <br/> **Why:** Correctly handling common patterns like `?ids=1&ids=2` (`style: 'form', explode: true`) is a fundamental part of spec compliance. |
10-
| 🚀 **High** | Support for Other Media Types (`application/x-www-form-urlencoded`, `multipart/form-data`) | **What:** Parse schemas for common media types beyond `application/json`. <br/> **Why:** Many APIs use `x-www-form-urlencoded` for simple forms (like OAuth) and `multipart/form-data` for file uploads with associated metadata. This would make the generator much more versatile. |
11-
| 🚀 **High** | Modern Binary Data Representation (`contentMediaType`/`contentEncoding`) | **What:** Support the OpenAPI 3.1+ approach to defining binary data using JSON Schema's `contentMediaType` and `contentEncoding`. <br/> **Why:** While your current `format: 'binary'` works for OAS 3.0, adopting the modern standard ensures future-proofing and better spec alignment. |
12-
| 🔧 **Medium** | Links & Hypermedia (`Link Object`) | **What:** Parse the `links` object in responses to understand HATEOAS relationships between operations. <br/> **Why:** This would enable the generation of "smarter" client libraries with helper methods on models to navigate the API, elevating the developer experience. |
13-
| 🔧 **Medium** | Expanded Security Schemes (`openIdConnect`) | **What:** Add support for the `openIdConnect` security scheme, which includes a `openIdConnectUrl` for auto-discovery. <br/> **Why:** This is a natural and valuable extension of your existing OAuth2 support, common in modern authentication setups. |
14-
| 🔧 **Medium** | Callbacks (`Callback Object`) | **What:** Support the `callbacks` field on operations for defining out-of-band, asynchronous responses to an API call. <br/> **Why:** This is essential for APIs with long-running processes or asynchronous workflows where the server calls back to the client. |
15-
| 🔧 **Medium** | Additional `in` Locations (`cookie`, `querystring`) | **What:** Handle parameters located `in: "cookie"` or the special `in: "querystring"` which treats the entire query string as a single value. <br/> **Why:** Improves completeness of parameter handling, although these are less common than path, query, and header. |
16-
| 📚 **Low** | Webhooks (`webhooks` object) | **What:** Support the top-level `webhooks` field for defining completely out-of-band events sent from the provider to the consumer. <br/> **Why:** This is a different paradigm from a client library making requests. It's a large feature better suited for generating server-side stubs. |
17-
| 📚 **Low** | XML Modeling (`xml` object) | **What:** Parse the `xml` object within schemas to understand how a model should be represented as XML. <br/> **Why:** Your generator is clearly focused on the JSON/TypeScript ecosystem. XML support is a very heavy lift for a different data paradigm and is a low priority. |
18-
| 📚 **Low** | Metadata in `Tag`, `ExternalDocs`, etc. | **What:** Use descriptive metadata from `Tag Object`s or `External Documentation Object`s. <br/> **Why:** This is a "nice-to-have" polishing step to add richer JSDoc comments to the generated services and models, improving developer experience but not affecting runtime. |
1+
Future
2+
======
3+
4+
This project could go in multiple directions.
5+
6+
One idea I've been playing with is to focus on interoperability. This is out-of-scope for cdd-web-_ng_ as the _ng_
7+
stands for Angular… but this repo could be split up into base/`abstract` `class`es whence this repo has the Angular
8+
specific tech… or even rename this repo and this package handles all Angular and non-Angular solutions (in the
9+
TypeScript web-frontend framework space).
10+
11+
## Full OpenAPI 3.2.0 + Swagger 2 compatibility
12+
13+
- [ ] Full OpenAPI 3.2.0 + Swagger 2 compatibility
14+
15+
## HTTP client interoperability
16+
17+
Add support for:
18+
19+
- [ ] [Fetch API (builtin)](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
20+
- [ ] [Axios](https://axios-http.com)
21+
22+
## Framework interoperability
23+
24+
Add support for creating an auto-admin UI for/with:
25+
26+
- [ ] [Web components (builtin)](https://developer.mozilla.org/en-US/docs/Web/API/Web_components)
27+
- [ ] [Qwik](https://qwik.dev)
28+
- [ ] [React](https://react.dev)
29+
- [ ] [Svelte](https://svelte.dev)
30+
- [ ] [Vue](https://vuejs.org)
31+
32+
## Sync within codebase
33+
34+
- [ ] Modify mock can update client can update admin UI.
35+
- [ ] Modify admin UI can update mock can update client.
36+
37+
## FROM codebase TO OpenAPI
38+
39+
- [ ] Bidirectionality is what distinctly makes it _cdd_: **C**ompiler **D**riven **D**evelopment.
40+
41+
## CI/CD
42+
43+
GitHub Actions for:
44+
45+
- [ ] tests
46+
- [ ] linting and other code-quality checks
47+
- [ ] release to npmjs
48+
- [ ] release hosted HTML API docs

src/core/parser.ts

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export class SwaggerParser {
8989
/** A normalized record of all reusable links defined in the entry specification. */
9090
public readonly links: Record<string, LinkObject>;
9191

92-
/** A cache of all loaded specifications, keyed by their absolute URI. */
92+
/** A cache of all loaded specifications (and sub-schemas with $id), keyed by their absolute URI. */
9393
private readonly specCache: Map<string, SwaggerSpec>;
9494

9595
/**
@@ -132,6 +132,11 @@ 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
136+
if (!specCache) {
137+
SwaggerParser.indexSchemaIds(spec, documentUri, this.specCache);
138+
}
139+
135140
this.schemas = Object.entries(this.getDefinitions()).map(([name, definition]) => ({
136141
name: pascalCase(name),
137142
definition
@@ -172,7 +177,8 @@ export class SwaggerParser {
172177
* @private
173178
*/
174179
private static async loadAndCacheSpecRecursive(uri: string, cache: Map<string, SwaggerSpec>, visited: Set<string>): Promise<void> {
175-
if (visited.has(uri)) return;
180+
// If we've visited this URI, or it's already in cache (possibly explicitly added via $id index), skip.
181+
if (visited.has(uri) || cache.has(uri)) return;
176182
visited.add(uri);
177183

178184
const content = await this.loadContent(uri);
@@ -181,16 +187,74 @@ export class SwaggerParser {
181187

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

190+
// Important: Index any `$id` properties within the document immediately.
191+
// This allows internal sub-schemas to be referenced by their global URI (OAS 3.1 / JSON Schema).
192+
this.indexSchemaIds(spec, baseUri, cache);
193+
184194
const refs = this.findRefs(spec);
185195
for (const ref of refs) {
186196
const [filePath] = ref.split('#', 2);
187-
if (filePath) { // It's a reference to another document
188-
const nextUri = new URL(filePath, baseUri).href;
189-
await this.loadAndCacheSpecRecursive(nextUri, cache, visited);
197+
if (filePath) { // It's a reference to another document or an absolute URI ID
198+
try {
199+
const nextUri = new URL(filePath, baseUri).href;
200+
await this.loadAndCacheSpecRecursive(nextUri, cache, visited);
201+
} catch (e) {
202+
console.warn(`[Parser] Failed to resolve referenced URI: ${filePath}. Skipping.`);
203+
}
190204
}
191205
}
192206
}
193207

208+
/**
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.
212+
*
213+
* @param spec The document root or fragment to traverse.
214+
* @param baseUri The current base URI of the scope.
215+
* @param cache The specification cache.
216+
* @private
217+
*/
218+
private static indexSchemaIds(spec: any, baseUri: string, cache: Map<string, SwaggerSpec>) {
219+
if (!spec || typeof spec !== 'object') return;
220+
221+
// Helper function to recursively traverse and update base URI context
222+
const traverse = (obj: any, currentBase: string, visited: Set<any>) => {
223+
if (!obj || typeof obj !== 'object' || visited.has(obj)) return;
224+
visited.add(obj);
225+
226+
let nextBase = currentBase;
227+
228+
// Check for $id (OAS 3.1 / JSON Schema definition)
229+
if ('$id' in obj && typeof obj.$id === 'string') {
230+
try {
231+
// Resolve $id against the current base.
232+
// $id can be relative or absolute.
233+
// If absolute, it resets the base.
234+
nextBase = new URL(obj.$id, currentBase).href;
235+
236+
// Cache this object as a distinct "document" at this URI
237+
// We use cast because cache expects SwaggerSpec, but these are Schema definitions.
238+
// Our resolution logic handles both.
239+
if (!cache.has(nextBase)) {
240+
cache.set(nextBase, obj as SwaggerSpec);
241+
}
242+
} catch (e) {
243+
// Ignore invalid $id values
244+
}
245+
}
246+
247+
// Recurse into children
248+
for (const key in obj) {
249+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
250+
traverse(obj[key], nextBase, visited);
251+
}
252+
}
253+
};
254+
255+
traverse(spec, baseUri, new Set());
256+
}
257+
194258
/**
195259
* Recursively finds all unique `$ref` and `$dynamicRef` values within a given object.
196260
* @param obj The object to search.
@@ -375,17 +439,13 @@ export class SwaggerParser {
375439
// Get the specification for the current document context to determine its logical base URI.
376440
const currentDocSpec = this.specCache.get(currentDocUri);
377441

378-
// This can happen if an invalid URI is somehow passed as the context.
379-
if (!currentDocSpec) {
380-
console.warn(`[Parser] Unresolved document URI in cache: ${currentDocUri}. Cannot resolve reference "${ref}".`);
381-
return undefined;
382-
}
383-
384-
// The base for resolving relative file paths is the document's logical URI, derived from its $self,
385-
// falling back to its physical URI.
386-
const logicalBaseUri = currentDocSpec.$self ? new URL(currentDocSpec.$self, currentDocUri).href : currentDocUri;
442+
// 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.
445+
const logicalBaseUri = currentDocSpec?.$self ? new URL(currentDocSpec.$self, currentDocUri).href : currentDocUri;
387446

388-
// The target file's physical URI is resolved using the logical base. If the ref is local, it's just the current doc's physical URI.
447+
// 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.
389449
const targetFileUri = filePath ? new URL(filePath, logicalBaseUri).href : currentDocUri;
390450

391451
const targetSpec = this.specCache.get(targetFileUri);

src/core/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,11 @@ export interface SwaggerSpec {
528528
export interface GeneratorConfigOptions {
529529
/** The TypeScript type to use for properties with `format: "date"` or `"date-time"`. */
530530
dateType?: 'string' | 'Date';
531+
/**
532+
* The TypeScript type to use for `integer` types with `format: "int64"`.
533+
* Default is 'number', but 'string' is often safer for browser JS due to precision limit (2^53).
534+
*/
535+
int64Type?: 'number' | 'string' | 'bigint';
531536
/** How to generate types for schemas with an `enum` list. */
532537
enumStyle?: 'enum' | 'union';
533538
/** If true, generates Angular services for API operations. */
@@ -540,6 +545,12 @@ export interface GeneratorConfigOptions {
540545
generateAdminTests?: boolean;
541546
/** A record of static headers to be added to every generated service request. */
542547
customHeaders?: Record<string, string>;
548+
/**
549+
* Target runtime platform for the generated code.
550+
* - 'browser': (Default) Assumes standard browser environment. Cookie setting in headers will emit warnings.
551+
* - 'node': Assumes Node.js/SSR environment. Cookie setting is allowed without warnings.
552+
*/
553+
platform?: 'browser' | 'node';
543554
/** A callback to provide a custom method name for an operation. */
544555
customizeMethodName?: ((operationId: string) => string);
545556
}

src/core/utils.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,19 @@ export function getTypeScriptType(schema: SwaggerDefinition | undefined | null,
233233
}
234234
break;
235235
case 'number':
236-
case 'integer':
237236
type = 'number';
238237
break;
238+
case 'integer':
239+
// Enhanced support for OAS 3.2 / Data Types strict format mapping requirement.
240+
// While 'number' is the standard JS type for all integers, large integers (int64)
241+
// can encounter precision loss in JS (> 2^53).
242+
// We now support a configuration option to map `int64` to `string` or `bigint`.
243+
if (schema.format === 'int64') {
244+
type = config.options.int64Type ?? 'number';
245+
} else {
246+
type = 'number'; // Default for int32 and others
247+
}
248+
break;
239249
case 'boolean':
240250
type = 'boolean';
241251
break;
@@ -313,7 +323,7 @@ export function getTypeScriptType(schema: SwaggerDefinition | undefined | null,
313323
* @returns `true` if the type is likely a generated model interface, `false` otherwise.
314324
*/
315325
export function isDataTypeInterface(type: string): boolean {
316-
const primitiveOrBuiltIn = /^(any|File|Blob|string|number|boolean|object|unknown|null|undefined|Date|void)$/;
326+
const primitiveOrBuiltIn = /^(any|File|Blob|string|number|boolean|object|unknown|null|undefined|Date|void|bigint)$/;
317327
const isArray = /\[\]$/;
318328
const isUnion = / \| /;
319329
return !primitiveOrBuiltIn.test(type) && !isArray.test(type) && !isUnion.test(type) && !type.startsWith('{') && !type.startsWith('Record');

0 commit comments

Comments
 (0)