diff --git a/README.md b/README.md index 6c03451..e5e3032 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Try it without installing anything: [playground](https://docs.openapi-ng.dev/pla - **Two layouts.** Per-tag `@Injectable` services by default, or `--layout operations` for one tree-shakeable constant per operation, callable from any injection context or bound once with `withInjector()`. - **Strict OpenAPI subset.** A focused 3.x slice with clear diagnostics. No silent misgeneration; see [Assumptions & limitations](https://docs.openapi-ng.dev/reference/limitations/) for the accepted shape. - **Configurable naming.** Tune method names and service grouping with template + regex rules, via YAML, JSON, or TypeScript config. -- **Thin, pass-through helpers.** Generated methods just build the request (method, URL, query, body) and forward every `HttpClient.request` / `httpResource` option through unchanged — `withCredentials`, `transferCache`, `reportProgress`, `equal`, `injector`, and the rest. The response reaches you untouched. +- **Thin, pass-through helpers.** Generated methods just build the request (method, URL, query, body) and forward every `HttpClient.request` / `httpResource` option through unchanged — `withCredentials`, `transferCache`, `reportUploadProgress`, `equal`, `injector`, and the rest. The response reaches you untouched. ## Install diff --git a/__test__/angular-consumer/src/service-proof.ts b/__test__/angular-consumer/src/service-proof.ts index 49f42cc..ef3450f 100644 --- a/__test__/angular-consumer/src/service-proof.ts +++ b/__test__/angular-consumer/src/service-proof.ts @@ -1,6 +1,12 @@ import type { PetRest, UpdatePetParams } from '../generated/rest/pet.rest'; -import type { HttpEvent, HttpResourceRef, HttpResponse } from '@angular/common/http'; +import type { + HttpClient, + HttpEvent, + HttpResourceRef, + HttpResponse, +} from '@angular/common/http'; import { Pet, PetList } from '../generated/model.ts'; +import type { ObservableOptions } from '../generated/rest.model'; import { Observable } from 'rxjs'; import type { ResourceParamsContext } from '@angular/core'; import type { @@ -37,7 +43,7 @@ const listPetsObservableResponse = service.listPets.observable({ }); const listPetsObservableEvents = service.listPets.observable({ observe: 'events', - reportProgress: true, + reportDownloadProgress: true, }); expectType(listPetsRequest.url); @@ -99,9 +105,38 @@ const updatePetObservableResponse = service.updatePet.observable(request, { }); const updatePetObservableEvents = service.updatePet.observable(request, { observe: 'events', - reportProgress: true, + reportDownloadProgress: true, +}); + +// Every HttpClient.request option the operation does not supply itself passes +// through, so options added by a later Angular need no regeneration. +type PassThroughOptions = Omit< + NonNullable[2]>, + 'body' | 'params' | 'headers' | 'responseType' +>; +// A non-literal argument skips excess-property checks, so compare keys too. +declare const missingOptionKey: Exclude< + keyof PassThroughOptions, + keyof ObservableOptions +>; +expectType(missingOptionKey); +declare const everyRequestOption: Required; +service.updatePet.observable(request, everyRequestOption); +service.updatePet.observable(request, { + integrity: 'sha256-x', + referrerPolicy: 'no-referrer', + reportUploadProgress: true, }); +// @ts-expect-error — the operation supplies the body +service.updatePet.observable(request, { body: {} }); +// @ts-expect-error — the operation supplies the query params +service.updatePet.observable(request, { params: {} }); +// @ts-expect-error — the operation supplies the headers +service.updatePet.observable(request, { headers: {} }); +// @ts-expect-error — the response type is fixed per operation +service.updatePet.observable(request, { responseType: 'text' }); + expectType(updatePetRequest.url); expectType>(updatePetObservable); expectType>(updatePetResource); @@ -133,6 +168,6 @@ expectType>>( expectType>>( requestVoidFactory.observable( { id: 'x' }, - { observe: 'events', reportProgress: true }, + { observe: 'events', reportDownloadProgress: true }, ), ); diff --git a/__test__/snapshots/generate-native/static-template/rest.model.ts b/__test__/snapshots/generate-native/static-template/rest.model.ts index d36d6d0..57b6275 100644 --- a/__test__/snapshots/generate-native/static-template/rest.model.ts +++ b/__test__/snapshots/generate-native/static-template/rest.model.ts @@ -1,6 +1,6 @@ import type { Injector } from '@angular/core'; import type { - HttpContext, + HttpClient, HttpHeaders, HttpParams, HttpResourceOptions, @@ -65,23 +65,10 @@ export interface OperationRequestOptions { injector?: Injector; } -// Omits body/params/headers (generator supplies them) and responseType -// (fixed per requestFactory variant). Structurally compatible with -// HttpClient.request(method, url, options) so the runtime spread doesn't -// need a Parameters<…>[2] cast. `injector` is consumed by the runtime and -// never reaches HttpClient; when given it wins over a `withInjector()` binding. -export type ObservableOptions = { - injector?: Injector; - context?: HttpContext; - observe?: 'body' | 'response' | 'events'; - reportProgress?: boolean; - transferCache?: { includeHeaders?: string[] } | boolean; - withCredentials?: boolean; - keepalive?: boolean; - redirect?: RequestRedirect; - mode?: RequestMode; - credentials?: RequestCredentials; - priority?: RequestPriority; - cache?: RequestCache; - timeout?: number; -}; +// `Parameters` reads request()'s last overload, its catch-all, so options a later +// Angular adds pass through. The operation supplies body/params/headers and fixes +// responseType; `injector` is consumed by the runtime and never reaches HttpClient. +export type ObservableOptions = Omit< + NonNullable[2]>, + 'body' | 'params' | 'headers' | 'responseType' +> & { injector?: Injector }; diff --git a/templates/angular/rest.model.ts b/templates/angular/rest.model.ts index d36d6d0..57b6275 100644 --- a/templates/angular/rest.model.ts +++ b/templates/angular/rest.model.ts @@ -1,6 +1,6 @@ import type { Injector } from '@angular/core'; import type { - HttpContext, + HttpClient, HttpHeaders, HttpParams, HttpResourceOptions, @@ -65,23 +65,10 @@ export interface OperationRequestOptions { injector?: Injector; } -// Omits body/params/headers (generator supplies them) and responseType -// (fixed per requestFactory variant). Structurally compatible with -// HttpClient.request(method, url, options) so the runtime spread doesn't -// need a Parameters<…>[2] cast. `injector` is consumed by the runtime and -// never reaches HttpClient; when given it wins over a `withInjector()` binding. -export type ObservableOptions = { - injector?: Injector; - context?: HttpContext; - observe?: 'body' | 'response' | 'events'; - reportProgress?: boolean; - transferCache?: { includeHeaders?: string[] } | boolean; - withCredentials?: boolean; - keepalive?: boolean; - redirect?: RequestRedirect; - mode?: RequestMode; - credentials?: RequestCredentials; - priority?: RequestPriority; - cache?: RequestCache; - timeout?: number; -}; +// `Parameters` reads request()'s last overload, its catch-all, so options a later +// Angular adds pass through. The operation supplies body/params/headers and fixes +// responseType; `injector` is consumed by the runtime and never reaches HttpClient. +export type ObservableOptions = Omit< + NonNullable[2]>, + 'body' | 'params' | 'headers' | 'responseType' +> & { injector?: Injector }; diff --git a/website/src/content/docs/guides/angular.md b/website/src/content/docs/guides/angular.md index fd13419..afb0f5c 100644 --- a/website/src/content/docs/guides/angular.md +++ b/website/src/content/docs/guides/angular.md @@ -421,7 +421,10 @@ options object to switch observation modes or to forward extra ```ts this.#petRest.updatePet.observable(req); this.#petRest.updatePet.observable(req, { observe: 'response' }); -this.#petRest.updatePet.observable(req, { observe: 'events', reportProgress: true }); +this.#petRest.updatePet.observable(req, { + observe: 'events', + reportUploadProgress: true, +}); ``` | Options | Return type | @@ -430,13 +433,15 @@ this.#petRest.updatePet.observable(req, { observe: 'events', reportProgress: tru | `{ observe: 'response' }` | `Observable>` | | `{ observe: 'events' }` | `Observable>` | -The options bag mirrors `HttpClient.request`'s options minus the -fields the generator already supplies — `body`, `params`, `headers`, -and `responseType` are baked in from the operation, so the type -rejects them. Everything else is forwarded: `withCredentials`, -`reportProgress`, `transferCache`, `context`, `keepalive`, and the -Fetch-related options (`redirect`, `mode`, `credentials`, `priority`, -`cache`, `timeout`). +The options bag is `HttpClient.request`'s own options type, read from +your installed Angular, minus the fields the generator already +supplies — `body`, `params`, `headers`, and `responseType` are baked +in from the operation, so the type rejects them. Everything else is +forwarded: `withCredentials`, `transferCache`, `context`, the progress +and Fetch-related options, and any option a later Angular adds, +without regenerating. Deprecations carry over the same way: on +Angular 22 your editor marks `reportProgress` as deprecated, as it +does on `HttpClient` itself. For void-response operations (204 No Content), the same overloads still apply — `Observable>` is meaningful when you