diff --git a/bin/configs/typescript-angular-v22-with-http-resource.yaml b/bin/configs/typescript-angular-v22-with-http-resource.yaml new file mode 100644 index 000000000000..2f7e10270400 --- /dev/null +++ b/bin/configs/typescript-angular-v22-with-http-resource.yaml @@ -0,0 +1,8 @@ +generatorName: typescript-angular +outputDir: samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-angular/http-resource.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-angular +additionalProperties: + ngVersion: 22.0.0 + supportsES6: true + withHttpResource: true diff --git a/docs/generators/typescript-angular.md b/docs/generators/typescript-angular.md index 7270a23e5e1f..0f401e3de1b5 100644 --- a/docs/generators/typescript-angular.md +++ b/docs/generators/typescript-angular.md @@ -56,6 +56,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |useDotNotationForFormObjects|Use parameter-prefixed dot notation for exploded form query objects instead of the OpenAPI standard unprefixed property names. Enable only for servers requiring this convention.| |false| |useSingleRequestParameter|Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.| |false| |useSquareBracketsInArrayNames|Setting this property to true will add brackets to array attribute names, e.g. my_values[].| |false| +|withHttpResource|Setting this property to true will generate, next to the Observable method of each GET operation without a request body, a method returning a signal-based Angular httpResource. Requires Angular v20+.| |false| |withInterfaces|Setting this property to true will generate interfaces next to the default class implementations.| |false| |zonejsVersion|The version of zone.js compatible with Angular (see ngVersion option).| |null| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java index 41e88007c077..8d331f308d71 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptAngularClientCodegen.java @@ -38,6 +38,7 @@ import java.io.File; import java.util.*; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -59,6 +60,9 @@ public static enum QUERY_PARAM_OBJECT_FORMAT_TYPE {dot, json, key} public static enum PROVIDED_IN_LEVEL {none, root, any, platform} + // same expression as Configuration.isJsonMime in configuration.mustache + private static final Pattern JSON_MIME_PATTERN = Pattern.compile("^(application/json|[^;/ \\t]+/[^;/ \\t]+[+]json)[ \\t]*(;.*)?$", Pattern.CASE_INSENSITIVE); + private static final String DEFAULT_IMPORT_PREFIX = "./"; private static final String DEFAULT_MODEL_IMPORT_DIRECTORY_PREFIX = "../"; @@ -87,6 +91,13 @@ public static enum PROVIDED_IN_LEVEL {none, root, any, platform} public static final String RXJS_VERSION = "rxjsVersion"; public static final String NGPACKAGR_VERSION = "ngPackagrVersion"; public static final String ZONEJS_VERSION = "zonejsVersion"; + public static final String WITH_HTTP_RESOURCE = "withHttpResource"; + + public static final String X_ANGULAR_HTTP_RESOURCE = "x-angular-http-resource"; + public static final String X_ANGULAR_HTTP_RESOURCE_FACTORY = "x-angular-http-resource-factory"; + public static final String X_ANGULAR_HTTP_RESOURCE_TYPE = "x-angular-http-resource-type"; + public static final String X_ANGULAR_HTTP_RESOURCE_ACCEPT = "x-angular-http-resource-accept"; + public static final String X_ANGULAR_REQUEST_PARAMS_INTERFACE = "x-angular-request-params-interface"; protected String ngVersion = "22.0.0"; @Getter @Setter @@ -102,6 +113,7 @@ public static enum PROVIDED_IN_LEVEL {none, root, any, platform} protected PROVIDED_IN_LEVEL providedIn = PROVIDED_IN_LEVEL.root; private boolean taggedUnions = false; + private boolean withHttpResource = false; public TypeScriptAngularClientCodegen() { super(); @@ -132,6 +144,10 @@ public TypeScriptAngularClientCodegen() { this.cliOptions.add(CliOption.newBoolean(USE_SINGLE_REQUEST_PARAMETER, "Setting this property to true will generate functions with a single argument containing all API endpoint parameters instead of one argument per parameter.", false)); + this.cliOptions.add(CliOption.newBoolean(WITH_HTTP_RESOURCE, + "Setting this property to true will generate, next to the Observable method of each GET operation without a request body, " + + "a method returning a signal-based Angular httpResource. Requires Angular v20+.", + false)); this.cliOptions.add(CliOption.newBoolean(TAGGED_UNIONS, "Use discriminators to create tagged unions instead of extending interfaces.", this.taggedUnions)); @@ -223,6 +239,17 @@ public void processOpts() { throw new IllegalArgumentException("Invalid ngVersion: " + ngVersion + ". Only Angular v9+ is supported."); } + if (additionalProperties.containsKey(WITH_HTTP_RESOURCE)) { + withHttpResource = convertPropertyToBoolean(WITH_HTTP_RESOURCE); + } + if (withHttpResource && !ngVersion.atLeast("20.0.0")) { + throw new IllegalArgumentException("Invalid ngVersion: " + ngVersion + ". " + WITH_HTTP_RESOURCE + " requires Angular v20+."); + } + writePropertyBack(WITH_HTTP_RESOURCE, withHttpResource); + if (withHttpResource) { + supportingFiles.add(new SupportingFile("httpResourceOptions.mustache", getIndexDirectory(), "http.resource.options.ts")); + } + if (additionalProperties.containsKey(NPM_NAME)) { addNpmPackageGeneration(ngVersion); } @@ -428,13 +455,24 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L objs.put("apiFilename", getApiFilenameFromClassname(objs.getClassname())); List ops = objs.getOperation(); + Set nicknames = ops.stream().map(op -> op.nickname).collect(Collectors.toSet()); boolean hasSomeFormParams = false; boolean hasSomeEncodableParams = false; + boolean hasSomeHttpResource = false; for (CodegenOperation op : ops) { if (op.getHasFormParams()) { hasSomeFormParams = true; } op.httpMethod = op.httpMethod.toLowerCase(Locale.ENGLISH); + if (markHttpResource(op, nicknames)) { + hasSomeHttpResource = true; + } + // the RequestParams interface: for every operation with useSingleRequestParameter, else for the resource methods only + if (op.getHasParams() && (getUseSingleRequestParameter() || op.vendorExtensions.containsKey(X_ANGULAR_HTTP_RESOURCE))) { + op.vendorExtensions.put(X_ANGULAR_REQUEST_PARAMS_INTERFACE, true); + } else { + op.vendorExtensions.remove(X_ANGULAR_REQUEST_PARAMS_INTERFACE); + } // deduplicate auth methods by name (as they will lead to duplicate code): op.authMethods = op.authMethods != null ? op.authMethods.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(x -> x.name))), ArrayList::new)) @@ -479,6 +517,7 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L operations.put("hasSomeFormParams", hasSomeFormParams); operations.put("hasSomeEncodableParams", hasSomeEncodableParams); + operations.put("hasSomeHttpResource", hasSomeHttpResource); // Add additional filename information for model imports in the services List> imports = operations.getImports(); @@ -491,6 +530,61 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L return operations; } + /** + * Marks a GET operation without a request body for an additional httpResource method. + * The factory mirrors the responseType the Observable method picks at runtime for the default + * Accept header (Configuration.selectHeaderAccept): JSON (or no produces) uses httpResource, + * text uses httpResource.text, anything else, and file responses, httpResource.blob. + * + * @param op the operation, with its http method already lower-cased + * @param nicknames the nicknames of all operations of the same API class + * @return true if the operation gets a resource method + */ + private boolean markHttpResource(CodegenOperation op, Set nicknames) { + op.vendorExtensions.remove(X_ANGULAR_HTTP_RESOURCE); + if (!withHttpResource || !"get".equals(op.httpMethod) || op.getHasBodyOrFormParams()) { + return false; + } + String resourceName = op.nickname + "Resource"; + if (nicknames.contains(resourceName) || nicknames.contains(resourceName + "Request")) { + LOGGER.warn("Not generating {} for operation {}: the name is taken by another operation.", resourceName, op.operationId); + return false; + } + + List produces = op.produces == null ? Collections.emptyList() : op.produces.stream() + .map(mediaType -> mediaType.get("mediaType")) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + String accept = produces.stream() + .filter(mediaType -> JSON_MIME_PATTERN.matcher(mediaType).matches()) + .findFirst() + .orElse(produces.isEmpty() ? null : produces.get(0)); + + String factory; + String type; + if (op.isResponseFile) { + factory = ".blob"; + type = "Blob"; + } else if (accept != null && accept.startsWith("text")) { + factory = ".text"; + type = "string"; + } else if (accept == null || JSON_MIME_PATTERN.matcher(accept).matches()) { + factory = ""; + type = op.returnType != null ? op.returnType : "any"; + } else { + factory = ".blob"; + type = "Blob"; + } + + op.vendorExtensions.put(X_ANGULAR_HTTP_RESOURCE, true); + op.vendorExtensions.put(X_ANGULAR_HTTP_RESOURCE_FACTORY, factory); + op.vendorExtensions.put(X_ANGULAR_HTTP_RESOURCE_TYPE, type); + if (accept != null) { + op.vendorExtensions.put(X_ANGULAR_HTTP_RESOURCE_ACCEPT, accept); + } + return true; + } + @Override public ModelsMap postProcessModels(ModelsMap objs) { ModelsMap result = super.postProcessModels(objs); diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache index ce4a81e940c4..226f6ce148c4 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache @@ -160,6 +160,50 @@ export const appConfig: ApplicationConfig = { }; ``` +{{#withHttpResource}} +### Signal-based resources + +Every `GET` operation without a request body also has a `...Resource` method, next to its Observable method, +which returns an Angular [`HttpResourceRef`](https://angular.dev/api/common/http/HttpResourceRef) +(see [`httpResource`](https://angular.dev/guide/http/http-resource)). The method is skipped, with a warning at +generation time, when its name would collide with another operation: + +```typescript +import { Component, computed, inject, input } from '@angular/core'; +import { PetService } from '{{npmName}}'; + +@Component({ + selector: 'app-pet', + templateUrl: './pet.component.html', +}) +export class PetComponent { + readonly petId = input.required(); + + // requested again whenever petId changes + readonly pet = inject(PetService).getPetByIdResource(() => ({ petId: this.petId() })); + + readonly name = computed(() => this.pet.hasValue() ? this.pet.value().name : ''); +} +``` + +- The first argument returns the request parameters and is called in a reactive context: every signal it reads + sends a new request when it changes. While it returns `undefined`, or a required parameter is `null` or `undefined`, + the resource stays idle. Operations without parameters take no such argument, and it can be left out when all + parameters are optional. +- Call the method in an injection context (a field initializer or a constructor), or pass an `injector` in the options. + The resource belongs to that injector: it is destroyed with it, and it sends its request through the `HttpClient` + of that injector, so the interceptors of the caller apply. +- The credentials of the `{{configurationClassName}}` are read each time the request is built, so a credential + function that reads a signal sends a new request when the signal changes. Other credential changes need `reload()`. +- `transferCache` defaults to `true`, as for the Observable methods. Angular's transfer cache skips requests with an + `Authorization` header unless `includeRequestsWithAuthHeaders` is set in `withHttpTransferCacheOptions`. +- Resources are generated for reads only. A resource cancels its pending request when its parameters change, which + is not safe for a write, so operations that change data keep only their Observable method. + +The `withHttpResource` option requires Angular 20 or later. `httpResource` is stable since Angular 22 and +experimental in Angular 20 and 21. + +{{/withHttpResource}} ### Customizing path parameter encoding Without further customization, only [path-parameters][parameter-locations-url] of [style][style-values-url] 'simple' diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/api.service.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/api.service.mustache index f60df119a660..5b39b37fcafd 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/api.service.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/api.service.mustache @@ -5,6 +5,9 @@ import { Inject, Injectable, Optional } from '@angular/core import { HttpClient, HttpHeaders, HttpParams, HttpResponse, HttpEvent{{#httpContextInOptions}}, HttpContext {{/httpContextInOptions}} } from '@angular/common/http'; +{{#hasSomeHttpResource}} +import { HttpResourceRef, HttpResourceRequest, httpResource } from '@angular/common/http'; +{{/hasSomeHttpResource}} import { Observable } from 'rxjs'; import { OpenApiHttpParams, QueryParamStyle } from '../query.params'; @@ -17,19 +20,21 @@ import { {{ classname }} } from '{{ filename }}'; import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; import { {{configurationClassName}} } from '../configuration'; import { BaseService } from '../api.base.service'; +{{#hasSomeHttpResource}} +import { ApiHttpResourceOptions } from '../http.resource.options'; +{{/hasSomeHttpResource}} {{#withInterfaces}} import { - {{classname}}Interface{{#useSingleRequestParameter}}{{#operations}}{{#operation}}{{#allParams.0}}, - {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams{{/allParams.0}}{{/operation}}{{/operations}}{{/useSingleRequestParameter}} + {{classname}}Interface{{#operations}}{{#operation}}{{#vendorExtensions.x-angular-request-params-interface}}, + {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams{{/vendorExtensions.x-angular-request-params-interface}}{{/operation}}{{/operations}} } from './{{classFilename}}Interface'; {{/withInterfaces}} {{#operations}} {{^withInterfaces}} -{{#useSingleRequestParameter}} {{#operation}} -{{#allParams.0}} +{{#vendorExtensions.x-angular-request-params-interface}} export interface {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams { {{#allParams}} {{#description}}/** {{.}} */ @@ -37,9 +42,8 @@ export interface {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterIn {{/allParams}} } -{{/allParams.0}} +{{/vendorExtensions.x-angular-request-params-interface}} {{/operation}} -{{/useSingleRequestParameter}} {{/withInterfaces}} {{#description}} @@ -107,90 +111,7 @@ export class {{classname}} extends BaseService { {{/required}} {{/allParams}} -{{#hasQueryParamsOrAuth}} - let localVarQueryParameters = new OpenApiHttpParams(this.encoder); -{{#queryParams}} - - localVarQueryParameters = this.addToHttpParams( - localVarQueryParameters, - '{{baseName}}', - {{paramName}}, - {{#isQueryParamObjectFormatJson}} - QueryParamStyle.Json, - {{/isQueryParamObjectFormatJson}} - {{^isQueryParamObjectFormatJson}} - {{^style}} - {{#queryIsJsonMimeType}} - QueryParamStyle.Json, - {{/queryIsJsonMimeType}} - {{^queryIsJsonMimeType}} - QueryParamStyle.Form, - {{/queryIsJsonMimeType}} - {{/style}} - {{#style}} - {{#isDeepObject}} - QueryParamStyle.DeepObject, - {{/isDeepObject}} - {{#isFormStyle}} - QueryParamStyle.Form, - {{/isFormStyle}} - {{#isSpaceDelimited}} - QueryParamStyle.SpaceDelimited, - {{/isSpaceDelimited}} - {{#isPipeDelimited}} - QueryParamStyle.PipeDelimited, - {{/isPipeDelimited}} - {{#queryIsJsonMimeType}} - QueryParamStyle.Json, - {{/queryIsJsonMimeType}} - {{/style}} - {{/isQueryParamObjectFormatJson}} - {{isExplode}}, - ); - -{{/queryParams}} - -{{/hasQueryParamsOrAuth}} - let localVarHeaders = this.defaultHeaders; -{{#headerParams}} - {{#isArray}} - if ({{paramName}}) { - localVarHeaders = localVarHeaders.set('{{baseName}}', [...{{paramName}}].join(COLLECTION_FORMATS['{{collectionFormat}}'])); - } - {{/isArray}} - {{^isArray}} - if ({{paramName}} !== undefined && {{paramName}} !== null) { - localVarHeaders = localVarHeaders.set('{{baseName}}', String({{paramName}})); - } - {{/isArray}} -{{/headerParams}} - -{{#authMethods}} - // authentication ({{name}}) required -{{#isApiKey}} - {{#isKeyInHeader}} - localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', '{{keyParamName}}', localVarHeaders); - {{/isKeyInHeader}} - {{#isKeyInQuery}} - localVarQueryParameters = this.configuration.addCredentialToQuery('{{name}}', '{{keyParamName}}', localVarQueryParameters); - {{/isKeyInQuery}} -{{/isApiKey}} -{{#isBasic}} - {{#isBasicBasic}} - localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Basic '); - {{/isBasicBasic}} - {{#isBasicBearer}} - localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); - {{/isBasicBearer}} -{{/isBasic}} -{{#isOAuth}} - localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); -{{/isOAuth}} -{{#isOpenId}} - localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); -{{/isOpenId}} - -{{/authMethods}} +{{>apiRequestQueryAndHeaders}} const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ {{#produces}} '{{{mediaType}}}'{{^-last}},{{/-last}} @@ -328,5 +249,8 @@ export class {{classname}} extends BaseService { ); } +{{#vendorExtensions.x-angular-http-resource}} +{{>apiHttpResource}} +{{/vendorExtensions.x-angular-http-resource}} {{/operation}}} {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/apiHttpResource.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/apiHttpResource.mustache new file mode 100644 index 000000000000..16bb11f6b42b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-angular/apiHttpResource.mustache @@ -0,0 +1,78 @@ + /** +{{#summary}} + * {{.}} +{{/summary}} +{{#notes}} + * {{.}} +{{/notes}} + * @endpoint {{httpMethod}} {{{vendorExtensions.x-path-from-spec}}} +{{#hasRequiredParams}} + * @param params returns the request parameters. The resource stays idle while it returns undefined or a required parameter is null or undefined. +{{/hasRequiredParams}} +{{^hasRequiredParams}} +{{#hasParams}} + * @param params returns the request parameters, all of them optional. The resource stays idle while it returns undefined. +{{/hasParams}} +{{/hasRequiredParams}} + * @param options resource and request options + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + */ +{{#hasParams}} +{{#hasRequiredParams}} + public {{nickname}}Resource(params: () => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined, options: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}> & { defaultValue: {{{vendorExtensions.x-angular-http-resource-type}}} }): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}}>; + public {{nickname}}Resource(params: () => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined, options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined>; + public {{nickname}}Resource(params: () => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined, options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined> { +{{/hasRequiredParams}} +{{^hasRequiredParams}} + public {{nickname}}Resource(params: (() => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined) | undefined, options: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}> & { defaultValue: {{{vendorExtensions.x-angular-http-resource-type}}} }): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}}>; + public {{nickname}}Resource(params?: () => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined, options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined>; + public {{nickname}}Resource(params: () => {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams | undefined = () => ({}), options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined> { +{{/hasRequiredParams}} + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource{{vendorExtensions.x-angular-http-resource-factory}}<{{{vendorExtensions.x-angular-http-resource-type}}}>(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.{{nickname}}ResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } +{{/hasParams}} +{{^hasParams}} + public {{nickname}}Resource(options: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}> & { defaultValue: {{{vendorExtensions.x-angular-http-resource-type}}} }): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}}>; + public {{nickname}}Resource(options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined>; + public {{nickname}}Resource(options?: ApiHttpResourceOptions<{{{vendorExtensions.x-angular-http-resource-type}}}>): HttpResourceRef<{{{vendorExtensions.x-angular-http-resource-type}}} | undefined> { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource{{vendorExtensions.x-angular-http-resource-factory}}<{{{vendorExtensions.x-angular-http-resource-type}}}>(() => this.{{nickname}}ResourceRequest(context, transferCache), resourceOptions); + } +{{/hasParams}} + + private {{nickname}}ResourceRequest({{#hasParams}}requestParameters: {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams, {{/hasParams}}localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest{{#hasRequiredParams}} | undefined{{/hasRequiredParams}} { +{{#allParams}} + const {{paramName}} = requestParameters.{{paramName}}; +{{#required}} + if ({{paramName}} === null || {{paramName}} === undefined) { + return undefined; + } +{{/required}} +{{/allParams}} + +{{>apiRequestQueryAndHeaders}} +{{#vendorExtensions.x-angular-http-resource-accept}} + localVarHeaders = localVarHeaders.set('Accept', '{{{.}}}'); +{{/vendorExtensions.x-angular-http-resource-accept}} + + let localVarPath = `{{{path}}}`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', +{{#hasQueryParamsOrAuth}} + params: localVarQueryParameters.toHttpParams(), +{{/hasQueryParamsOrAuth}} + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/apiInterface.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/apiInterface.mustache index 6774b0091ebb..0b886abb087f 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/apiInterface.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/apiInterface.mustache @@ -12,18 +12,16 @@ import { {{configurationClassName}} } from ' {{#operations}} -{{#useSingleRequestParameter}} {{#operation}} -{{#allParams.0}} +{{#vendorExtensions.x-angular-request-params-interface}} export interface {{#prefixParameterInterfaces}}{{classname}}{{/prefixParameterInterfaces}}{{operationIdCamelCase}}RequestParams { {{#allParams}} {{paramName}}{{^required}}?{{/required}}: {{{dataType}}}{{#isNullable}} | null{{/isNullable}}; {{/allParams}} } -{{/allParams.0}} +{{/vendorExtensions.x-angular-request-params-interface}} {{/operation}} -{{/useSingleRequestParameter}} {{#description}} /** diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/apiRequestQueryAndHeaders.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/apiRequestQueryAndHeaders.mustache new file mode 100644 index 000000000000..2f94a3a4905c --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-angular/apiRequestQueryAndHeaders.mustache @@ -0,0 +1,85 @@ +{{! query parameters, header parameters and credentials, shared by the Observable methods (api.service) and the resource request builders (apiHttpResource) }} +{{#hasQueryParamsOrAuth}} + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); +{{#queryParams}} + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + '{{baseName}}', + {{paramName}}, + {{#isQueryParamObjectFormatJson}} + QueryParamStyle.Json, + {{/isQueryParamObjectFormatJson}} + {{^isQueryParamObjectFormatJson}} + {{^style}} + {{#queryIsJsonMimeType}} + QueryParamStyle.Json, + {{/queryIsJsonMimeType}} + {{^queryIsJsonMimeType}} + QueryParamStyle.Form, + {{/queryIsJsonMimeType}} + {{/style}} + {{#style}} + {{#isDeepObject}} + QueryParamStyle.DeepObject, + {{/isDeepObject}} + {{#isFormStyle}} + QueryParamStyle.Form, + {{/isFormStyle}} + {{#isSpaceDelimited}} + QueryParamStyle.SpaceDelimited, + {{/isSpaceDelimited}} + {{#isPipeDelimited}} + QueryParamStyle.PipeDelimited, + {{/isPipeDelimited}} + {{#queryIsJsonMimeType}} + QueryParamStyle.Json, + {{/queryIsJsonMimeType}} + {{/style}} + {{/isQueryParamObjectFormatJson}} + {{isExplode}}, + ); + +{{/queryParams}} + +{{/hasQueryParamsOrAuth}} + let localVarHeaders = this.defaultHeaders; +{{#headerParams}} + {{#isArray}} + if ({{paramName}}) { + localVarHeaders = localVarHeaders.set('{{baseName}}', [...{{paramName}}].join(COLLECTION_FORMATS['{{collectionFormat}}'])); + } + {{/isArray}} + {{^isArray}} + if ({{paramName}} !== undefined && {{paramName}} !== null) { + localVarHeaders = localVarHeaders.set('{{baseName}}', String({{paramName}})); + } + {{/isArray}} +{{/headerParams}} + +{{#authMethods}} + // authentication ({{name}}) required +{{#isApiKey}} + {{#isKeyInHeader}} + localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', '{{keyParamName}}', localVarHeaders); + {{/isKeyInHeader}} + {{#isKeyInQuery}} + localVarQueryParameters = this.configuration.addCredentialToQuery('{{name}}', '{{keyParamName}}', localVarQueryParameters); + {{/isKeyInQuery}} +{{/isApiKey}} +{{#isBasic}} + {{#isBasicBasic}} + localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Basic '); + {{/isBasicBasic}} + {{#isBasicBearer}} + localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); + {{/isBasicBearer}} +{{/isBasic}} +{{#isOAuth}} + localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); +{{/isOAuth}} +{{#isOpenId}} + localVarHeaders = this.configuration.addCredentialToHeaders('{{name}}', 'Authorization', localVarHeaders, 'Bearer '); +{{/isOpenId}} + +{{/authMethods}} diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/httpResourceOptions.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/httpResourceOptions.mustache new file mode 100644 index 000000000000..4b2d8a7972c4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/typescript-angular/httpResourceOptions.mustache @@ -0,0 +1,33 @@ +{{>licenseInfo}} +import { Injector, ValueEqualityFn } from '@angular/core'; +import { HttpContext } from '@angular/common/http'; + +/** + * Options of the generated `...Resource` methods, which return an Angular `httpResource`. + */ +export interface ApiHttpResourceOptions { + /** + * Value of the resource while no response is available, for example while it is idle or loading. + */ + defaultValue?: T; + /** + * Injector the resource is created in. Required when the method is called outside an injection context. + */ + injector?: Injector; + /** + * Equality function used to compare the values of the resource. + */ + equal?: ValueEqualityFn; + /** + * Debug name of the resource. Used by Angular 21.1 and later, ignored before. + */ + debugName?: string; + /** + * Context of the request. + */ + context?: HttpContext; + /** + * Whether the SSR transfer cache may be used for the request. Defaults to true. + */ + transferCache?: boolean; +} diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/index.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/index.mustache index b58aade26080..691e9d01210b 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/index.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/index.mustache @@ -8,4 +8,7 @@ export * from './api.module'; {{#ngVersionAtLeast_17}} export * from './provide-api'; {{/ngVersionAtLeast_17}} +{{#withHttpResource}} +export * from './http.resource.options'; +{{/withHttpResource}} export * from './param'; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptAngularClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptAngularClientOptionsProvider.java index d87bd3364651..9985b918cde6 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptAngularClientOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptAngularClientOptionsProvider.java @@ -52,6 +52,7 @@ public Map createOptions() { .put(TypeScriptAngularClientCodegen.STRING_ENUMS, STRING_ENUMS_VALUE) .put(TypeScriptAngularClientCodegen.WITH_INTERFACES, Boolean.FALSE.toString()) .put(TypeScriptAngularClientCodegen.USE_SINGLE_REQUEST_PARAMETER, Boolean.FALSE.toString()) + .put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, Boolean.FALSE.toString()) .put(TypeScriptAngularClientCodegen.PROVIDED_IN, PROVIDED_IN_LEVEL) .put(TypeScriptAngularClientCodegen.TAGGED_UNIONS, Boolean.FALSE.toString()) .put(TypeScriptAngularClientCodegen.NPM_REPOSITORY, NPM_REPOSITORY) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/typescriptangular/TypeScriptAngularClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/typescriptangular/TypeScriptAngularClientCodegenTest.java index 184176e1c177..dfe632184f65 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/typescriptangular/TypeScriptAngularClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/typescriptangular/TypeScriptAngularClientCodegenTest.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -549,4 +550,136 @@ public void testOpenIdCredentialsAreSet() throws IOException { String credentialsSet = "localVarHeaders = this.configuration.addCredentialToHeaders('oidc', 'Authorization', localVarHeaders, 'Bearer ');"; assertThat(fileContents).contains(credentialsSet); } + + private static final String HTTP_RESOURCE_SPEC = "src/test/resources/3_0/typescript-angular/http-resource.yaml"; + + private File generateAngular(String specPath, Map properties) throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("typescript-angular") + .setInputSpec(specPath) + .setAdditionalProperties(properties) + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + return output; + } + + @Test + public void testHttpResourceIsOffByDefault() throws IOException { + File output = generateAngular(HTTP_RESOURCE_SPEC, new HashMap<>()); + + TestUtils.assertFileNotContains(Paths.get(output + "/api/pet.service.ts"), + "HttpResourceRef", "httpResource<", "httpResource.", "Resource(", "RequestParams"); + TestUtils.assertFileNotContains(Paths.get(output + "/index.ts"), "http.resource.options"); + TestUtils.assertFileNotExists(Paths.get(output + "/http.resource.options.ts")); + } + + @Test + public void testHttpResourceOnlyForGetOperationsWithoutBody() throws IOException { + Map properties = new HashMap<>(); + properties.put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, "true"); + File output = generateAngular(HTTP_RESOURCE_SPEC, properties); + + Path petService = Paths.get(output + "/api/pet.service.ts"); + TestUtils.assertFileContains(petService, + "import { HttpResourceRef, HttpResourceRequest, httpResource } from '@angular/common/http';", + "import { ApiHttpResourceOptions } from '../http.resource.options';", + "export interface GetPetByIdRequestParams {", + "public getPetByIdResource(params: () => GetPetByIdRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef;", + "public getPetByIdResource(params: () => GetPetByIdRequestParams | undefined, options: ApiHttpResourceOptions & { defaultValue: Pet }): HttpResourceRef;", + // every parameter optional: the params function may be left out + "public listPetsResource(params?: () => ListPetsRequestParams | undefined, options?: ApiHttpResourceOptions>): HttpResourceRef | undefined>;", + // a missing required parameter keeps the resource idle instead of throwing + "if (petId === null || petId === undefined) { return undefined; }", + "params: localVarQueryParameters.toHttpParams(),", + "transferCache: localVarTransferCache ?? true,"); + // the Observable methods are unchanged, and writes and a GET with a body get no resource + TestUtils.assertFileContains(petService, + "public addPet(pet: Pet, observe?: 'body'", + "public searchPets(pet?: Pet, observe?: 'body'"); + TestUtils.assertFileNotContains(petService, + "addPetResource", "deletePetResource", "searchPetsResource", + "export interface AddPetRequestParams", "export interface SearchPetsRequestParams"); + + // an operation without parameters takes only the options + TestUtils.assertFileContains(Paths.get(output + "/api/store.service.ts"), + "public getInventoryResource(options?: ApiHttpResourceOptions<{ [key: string]: number; }>)", + "localVarQueryParameters = this.configuration.addCredentialToQuery('api_key_query', 'api_key', localVarQueryParameters);"); + + TestUtils.assertFileContains(Paths.get(output + "/http.resource.options.ts"), + "export interface ApiHttpResourceOptions {"); + TestUtils.assertFileContains(Paths.get(output + "/index.ts"), + "export * from './http.resource.options';"); + } + + @Test + public void testHttpResourceFactoryFollowsTheAcceptHeader() throws IOException { + Map properties = new HashMap<>(); + properties.put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, true); + File output = generateAngular(HTTP_RESOURCE_SPEC, properties); + + Path petService = Paths.get(output + "/api/pet.service.ts"); + TestUtils.assertFileContains(petService, + // application/xml and application/json: the JSON type wins, as in Configuration.selectHeaderAccept + "return httpResource>(", + "localVarHeaders = localVarHeaders.set('Accept', 'application/json');", + "return httpResource(", + // text/plain only + "return httpResource.text(", + "localVarHeaders = localVarHeaders.set('Accept', 'text/plain');", + // binary + "return httpResource.blob(", + "localVarHeaders = localVarHeaders.set('Accept', 'application/octet-stream');"); + } + + @Test + public void testHttpResourceRequiresAngular20() { + TypeScriptAngularClientCodegen codegen = new TypeScriptAngularClientCodegen(); + codegen.additionalProperties().put(TypeScriptAngularClientCodegen.NG_VERSION, "19.2.0"); + codegen.additionalProperties().put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, "true"); + IllegalArgumentException error = Assert.expectThrows(IllegalArgumentException.class, codegen::processOpts); + assertThat(error.getMessage()).contains("withHttpResource requires Angular v20+"); + + codegen = new TypeScriptAngularClientCodegen(); + codegen.additionalProperties().put(TypeScriptAngularClientCodegen.NG_VERSION, "20.0.0"); + codegen.additionalProperties().put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, "true"); + codegen.processOpts(); + assertThat(codegen.additionalProperties()).containsEntry(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, true); + + // without the option, older versions are not affected + codegen = new TypeScriptAngularClientCodegen(); + codegen.additionalProperties().put(TypeScriptAngularClientCodegen.NG_VERSION, "19.2.0"); + codegen.processOpts(); + assertThat(codegen.additionalProperties()).containsEntry(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, false); + } + + @Test + public void testHttpResourceSkipsAnOperationWhoseNameIsTaken() throws IOException { + Path spec = Files.createTempFile("http-resource-collision", ".yaml"); + spec.toFile().deleteOnExit(); + Files.writeString(spec, String.join("\n", + "openapi: 3.0.1", + "info: {title: collision, version: 1.0.0}", + "paths:", + " /pet:", + " get:", + " operationId: getPet", + " responses: {'200': {description: ok, content: {application/json: {schema: {type: string}}}}}", + " /pet-resource:", + " get:", + " operationId: getPetResource", + " responses: {'200': {description: ok, content: {application/json: {schema: {type: string}}}}}", + "")); + + Map properties = new HashMap<>(); + properties.put(TypeScriptAngularClientCodegen.WITH_HTTP_RESOURCE, "true"); + File output = generateAngular(spec.toString(), properties); + + Path service = Paths.get(output + "/api/default.service.ts"); + TestUtils.assertFileContains(service, "public getPetResourceResource(options?: ApiHttpResourceOptions)"); + TestUtils.assertFileNotContains(service, "public getPetResource(options?: ApiHttpResourceOptions)"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-angular/http-resource.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-angular/http-resource.yaml new file mode 100644 index 000000000000..76aeeee29b84 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-angular/http-resource.yaml @@ -0,0 +1,247 @@ +openapi: 3.0.1 +info: + title: httpResource petstore + description: Operations covering each shape of the resource methods generated with withHttpResource. + version: 1.0.0 +servers: + - url: http://localhost/api +tags: + - name: pet + - name: store +paths: + /pet: + get: + tags: + - pet + summary: List pets + description: Every parameter is optional. + operationId: listPets + parameters: + - name: limit + in: query + schema: + type: integer + format: int32 + - name: tags + in: query + style: form + explode: true + schema: + type: array + items: + type: string + - name: X-Request-Id + in: header + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + post: + tags: + - pet + summary: Add a pet + operationId: addPet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + /pet/findByStatus: + get: + tags: + - pet + summary: Find pets by status + operationId: findPetsByStatus + parameters: + - name: status + in: query + required: true + style: form + explode: false + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + security: + - bearer_auth: [] + /pet/search: + get: + tags: + - pet + summary: Search pets with a request body + description: A GET with a body keeps only its Observable method. + operationId: searchPets + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID + operationId: getPetById + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + security: + - api_key: [] + delete: + tags: + - pet + summary: Delete a pet + operationId: deletePet + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '204': + description: deleted + /pet/{petId}/notes: + get: + tags: + - pet + summary: Notes about a pet, as plain text + operationId: getPetNotes + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + text/plain: + schema: + type: string + /pet/{petId}/photo: + get: + tags: + - pet + summary: Photo of a pet + operationId: getPetPhoto + parameters: + - name: petId + in: path + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/octet-stream: + schema: + type: string + format: binary + /store/inventory: + get: + tags: + - store + summary: Pet inventories by status + description: No parameters, with the API key sent in the query. + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key_query: [] +components: + securitySchemes: + api_key: + type: apiKey + name: api_key + in: header + api_key_query: + type: apiKey + name: api_key + in: query + bearer_auth: + type: http + scheme: bearer + schemas: + Pet: + type: object + required: + - name + properties: + id: + type: integer + format: int64 + name: + type: string + status: + type: string + enum: + - available + - pending + - sold diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.gitignore b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.gitignore new file mode 100644 index 000000000000..149b57654723 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator-ignore b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/FILES b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/FILES new file mode 100644 index 000000000000..3c805738c685 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/FILES @@ -0,0 +1,18 @@ +.gitignore +README.md +api.base.service.ts +api.module.ts +api/api.ts +api/pet.service.ts +api/store.service.ts +configuration.ts +encoder.ts +git_push.sh +http.resource.options.ts +index.ts +model/models.ts +model/pet.ts +param.ts +provide-api.ts +query.params.ts +variables.ts diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/VERSION b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/VERSION new file mode 100644 index 000000000000..56fe0e8b297f --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.27.0-SNAPSHOT diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/README.md b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/README.md new file mode 100644 index 000000000000..4ad71cc3aeae --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/README.md @@ -0,0 +1,237 @@ +# @ + +Operations covering each shape of the resource methods generated with withHttpResource. + +The version of the OpenAPI document: 1.0.0 + +## Building + +To install the required dependencies and to build the typescript sources run: + +```console +npm install +npm run build +``` + +## Publishing + +First build the package then run `npm publish dist` (don't forget to specify the `dist` folder!) + +## Consuming + +Navigate to the folder of your consuming project and run one of next commands. + +_published:_ + +```console +npm install @ --save +``` + +_without publishing (not recommended):_ + +```console +npm install PATH_TO_GENERATED_PACKAGE/dist.tgz --save +``` + +_It's important to take the tgz file, otherwise you'll get trouble with links on windows_ + +_using `npm link`:_ + +In PATH_TO_GENERATED_PACKAGE/dist: + +```console +npm link +``` + +In your project: + +```console +npm link +``` + +__Note for Windows users:__ The Angular CLI has troubles to use linked npm packages. +Please refer to this issue for a solution / workaround. +Published packages are not effected by this issue. + +### General usage + +In your Angular project: + +```typescript + +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideApi } from ''; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideHttpClient(), + provideApi() + ], +}; +``` + +**NOTE** +If you're still using `AppModule` and haven't [migrated](https://angular.dev/reference/migrations/standalone) yet, you can still import an Angular module: +```typescript +import { ApiModule } from ''; +``` + +If different from the generated base path, during app bootstrap, you can provide the base path to your service. + +```typescript +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideApi } from ''; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideHttpClient(), + provideApi('http://localhost:9999') + ], +}; +``` + +```typescript +// with a custom configuration +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideApi } from ''; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideHttpClient(), + provideApi({ + withCredentials: true, + username: 'user', + password: 'password' + }) + ], +}; +``` + +```typescript +// with factory building a custom configuration +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideApi, Configuration } from ''; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideHttpClient(), + { + provide: Configuration, + useFactory: (authService: AuthService) => new Configuration({ + basePath: 'http://localhost:9999', + withCredentials: true, + username: authService.getUsername(), + password: authService.getPassword(), + }), + deps: [AuthService], + multi: false + } + ], +}; +``` + +### Using multiple OpenAPI files / APIs + +In order to use multiple APIs generated from different OpenAPI files, +you can create an alias name when importing the modules +in order to avoid naming conflicts: + +```typescript +import { provideApi as provideUserApi } from 'my-user-api-path'; +import { provideApi as provideAdminApi } from 'my-admin-api-path'; +import { HttpClientModule } from '@angular/common/http'; +import { environment } from '../environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideHttpClient(), + provideUserApi(environment.basePath), + provideAdminApi(environment.basePath), + ], +}; +``` + +### Signal-based resources + +Every `GET` operation without a request body also has a `...Resource` method, next to its Observable method, +which returns an Angular [`HttpResourceRef`](https://angular.dev/api/common/http/HttpResourceRef) +(see [`httpResource`](https://angular.dev/guide/http/http-resource)). The method is skipped, with a warning at +generation time, when its name would collide with another operation: + +```typescript +import { Component, computed, inject, input } from '@angular/core'; +import { PetService } from ''; + +@Component({ + selector: 'app-pet', + templateUrl: './pet.component.html', +}) +export class PetComponent { + readonly petId = input.required(); + + // requested again whenever petId changes + readonly pet = inject(PetService).getPetByIdResource(() => ({ petId: this.petId() })); + + readonly name = computed(() => this.pet.hasValue() ? this.pet.value().name : ''); +} +``` + +- The first argument returns the request parameters and is called in a reactive context: every signal it reads + sends a new request when it changes. While it returns `undefined`, or a required parameter is `null` or `undefined`, + the resource stays idle. Operations without parameters take no such argument, and it can be left out when all + parameters are optional. +- Call the method in an injection context (a field initializer or a constructor), or pass an `injector` in the options. + The resource belongs to that injector: it is destroyed with it, and it sends its request through the `HttpClient` + of that injector, so the interceptors of the caller apply. +- The credentials of the `Configuration` are read each time the request is built, so a credential + function that reads a signal sends a new request when the signal changes. Other credential changes need `reload()`. +- `transferCache` defaults to `true`, as for the Observable methods. Angular's transfer cache skips requests with an + `Authorization` header unless `includeRequestsWithAuthHeaders` is set in `withHttpTransferCacheOptions`. +- Resources are generated for reads only. A resource cancels its pending request when its parameters change, which + is not safe for a write, so operations that change data keep only their Observable method. + +The `withHttpResource` option requires Angular 20 or later. `httpResource` is stable since Angular 22 and +experimental in Angular 20 and 21. + +### Customizing path parameter encoding + +Without further customization, only [path-parameters][parameter-locations-url] of [style][style-values-url] 'simple' +and Dates for format 'date-time' are encoded correctly. + +Other styles (e.g. "matrix") are not that easy to encode +and thus are best delegated to other libraries (e.g.: [@honoluluhenk/http-param-expander]). + +To implement your own parameter encoding (or call another library), +pass an arrow-function or method-reference to the `encodeParam` property of the Configuration-object +(see [General Usage](#general-usage) above). + +Example value for use in your Configuration-Provider: + +```typescript +new Configuration({ + encodeParam: (param: Param) => myFancyParamEncoder(param), +}) +``` + +[parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations +[style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values +[@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.base.service.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.base.service.ts new file mode 100644 index 000000000000..f1be6d0471fa --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.base.service.ts @@ -0,0 +1,120 @@ +/** + * httpResource petstore + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { HttpHeaders, HttpParams, HttpParameterCodec } from '@angular/common/http'; +import { CustomHttpParameterCodec } from './encoder'; +import { Configuration } from './configuration'; +import { OpenApiHttpParams, QueryParamStyle, concatHttpParamsObject} from './query.params'; + +export class BaseService { + protected basePath = 'http://localhost/api'; + public defaultHeaders = new HttpHeaders(); + public configuration: Configuration; + public encoder: HttpParameterCodec; + + constructor(basePath?: string|string[], configuration?: Configuration) { + this.configuration = configuration || new Configuration(); + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + protected canConsumeForm(consumes: string[]): boolean { + return consumes.indexOf('multipart/form-data') !== -1; + } + + protected addToHttpParams(httpParams: OpenApiHttpParams, key: string, value: any | null | undefined, paramStyle: QueryParamStyle, explode: boolean): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } + + if (paramStyle === QueryParamStyle.DeepObject) { + if (typeof value !== 'object') { + throw Error(`An object must be provided for key ${key} as it is a deep object`); + } + + return Object.keys(value as Record).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } else if (paramStyle === QueryParamStyle.Json) { + return httpParams.append(key, JSON.stringify(value)); + } else { + // Form-style, SpaceDelimited or PipeDelimited + + if (Object(value) !== value) { + // If it is a primitive type, add its string representation + return httpParams.append(key, value.toString()); + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + // Otherwise, if it's an array or set, add each element. + const array = Array.isArray(value) ? value : Array.from(value); + if (paramStyle === QueryParamStyle.Form) { + return httpParams.set(key, array, {explode: explode, delimiter: ','}); + } else if (paramStyle === QueryParamStyle.SpaceDelimited) { + return httpParams.set(key, array, {explode: explode, delimiter: ' '}); + } else { + // PipeDelimited + return httpParams.set(key, array, {explode: explode, delimiter: '|'}); + } + } else { + // Otherwise, if it's an object, add each field. + if (paramStyle === QueryParamStyle.Form) { + if (explode) { + Object.keys(value).forEach(k => { + httpParams = this.addToHttpParams(httpParams, k, value[k], paramStyle, explode); + }); + return httpParams; + } else { + return concatHttpParamsObject(httpParams, key, value, ','); + } + } else if (paramStyle === QueryParamStyle.SpaceDelimited) { + return concatHttpParamsObject(httpParams, key, value, ' '); + } else { + // PipeDelimited + return concatHttpParamsObject(httpParams, key, value, '|'); + } + } + } + } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.module.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.module.ts new file mode 100644 index 000000000000..58d341fbd2e5 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api.module.ts @@ -0,0 +1,30 @@ +import { NgModule, ModuleWithProviders, SkipSelf, Optional } from '@angular/core'; +import { Configuration } from './configuration'; +import { HttpClient } from '@angular/common/http'; + + +@NgModule({ + imports: [], + declarations: [], + exports: [], + providers: [] +}) +export class ApiModule { + public static forRoot(configurationFactory: () => Configuration): ModuleWithProviders { + return { + ngModule: ApiModule, + providers: [ { provide: Configuration, useFactory: configurationFactory } ] + }; + } + + constructor( @Optional() @SkipSelf() parentModule: ApiModule, + @Optional() http: HttpClient) { + if (parentModule) { + throw new Error('ApiModule is already loaded. Import in your base AppModule only.'); + } + if (!http) { + throw new Error('You need to import the HttpClientModule in your AppModule! \n' + + 'See also https://github.com/angular/angular/issues/20575'); + } + } +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/api.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/api.ts new file mode 100644 index 000000000000..aca343ee9981 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/api.ts @@ -0,0 +1,5 @@ +export * from './pet.service'; +import { PetService } from './pet.service'; +export * from './store.service'; +import { StoreService } from './store.service'; +export const APIS = [PetService, StoreService]; diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/pet.service.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/pet.service.ts new file mode 100644 index 000000000000..c2c3e549ff8b --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/pet.service.ts @@ -0,0 +1,791 @@ +/** + * httpResource petstore + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpContext + } from '@angular/common/http'; +import { HttpResourceRef, HttpResourceRequest, httpResource } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { OpenApiHttpParams, QueryParamStyle } from '../query.params'; + +// @ts-ignore +import { Pet } from '../model/pet'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; +import { ApiHttpResourceOptions } from '../http.resource.options'; + + +export interface FindPetsByStatusRequestParams { + status: Array<'available' | 'pending' | 'sold'>; +} + +export interface GetPetByIdRequestParams { + petId: number; +} + +export interface GetPetNotesRequestParams { + petId: number; +} + +export interface GetPetPhotoRequestParams { + petId: number; +} + +export interface ListPetsRequestParams { + limit?: number; + tags?: Array; + xRequestId?: string; +} + + +@Injectable({ + providedIn: 'root' +}) +export class PetService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Add a pet + * @endpoint post /pet + * @param pet + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public addPet(pet: Pet, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public addPet(pet: Pet, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public addPet(pet: Pet, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public addPet(pet: Pet, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (pet === null || pet === undefined) { + throw new Error('Required parameter pet was null or undefined when calling addPet.'); + } + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('post', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: pet, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Delete a pet + * @endpoint delete /pet/{petId} + * @param petId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public deletePet(petId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable; + public deletePet(petId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deletePet(petId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable>; + public deletePet(petId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: undefined, context?: HttpContext, transferCache?: boolean}): Observable { + if (petId === null || petId === undefined) { + throw new Error('Required parameter petId was null or undefined when calling deletePet.'); + } + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('delete', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Find pets by status + * @endpoint get /pet/findByStatus + * @param status + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public findPetsByStatus(status: Array<'available' | 'pending' | 'sold'>, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/xml' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public findPetsByStatus(status: Array<'available' | 'pending' | 'sold'>, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/xml' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public findPetsByStatus(status: Array<'available' | 'pending' | 'sold'>, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/xml' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public findPetsByStatus(status: Array<'available' | 'pending' | 'sold'>, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/xml' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (status === null || status === undefined) { + throw new Error('Required parameter status was null or undefined when calling findPetsByStatus.'); + } + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'status', + status, + QueryParamStyle.Form, + false, + ); + + + let localVarHeaders = this.defaultHeaders; + + // authentication (bearer_auth) required + localVarHeaders = this.configuration.addCredentialToHeaders('bearer_auth', 'Authorization', localVarHeaders, 'Bearer '); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/xml', + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet/findByStatus`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters.toHttpParams(), + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Find pets by status + * @endpoint get /pet/findByStatus + * @param params returns the request parameters. The resource stays idle while it returns undefined or a required parameter is null or undefined. + * @param options resource and request options + */ + public findPetsByStatusResource(params: () => FindPetsByStatusRequestParams | undefined, options: ApiHttpResourceOptions> & { defaultValue: Array }): HttpResourceRef>; + public findPetsByStatusResource(params: () => FindPetsByStatusRequestParams | undefined, options?: ApiHttpResourceOptions>): HttpResourceRef | undefined>; + public findPetsByStatusResource(params: () => FindPetsByStatusRequestParams | undefined, options?: ApiHttpResourceOptions>): HttpResourceRef | undefined> { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource>(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.findPetsByStatusResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } + + private findPetsByStatusResourceRequest(requestParameters: FindPetsByStatusRequestParams, localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest | undefined { + const status = requestParameters.status; + if (status === null || status === undefined) { + return undefined; + } + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'status', + status, + QueryParamStyle.Form, + false, + ); + + + let localVarHeaders = this.defaultHeaders; + + // authentication (bearer_auth) required + localVarHeaders = this.configuration.addCredentialToHeaders('bearer_auth', 'Authorization', localVarHeaders, 'Bearer '); + + localVarHeaders = localVarHeaders.set('Accept', 'application/json'); + + let localVarPath = `/pet/findByStatus`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + params: localVarQueryParameters.toHttpParams(), + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + + /** + * Find pet by ID + * @endpoint get /pet/{petId} + * @param petId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public getPetById(petId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getPetById(petId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetById(petId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetById(petId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (petId === null || petId === undefined) { + throw new Error('Required parameter petId was null or undefined when calling getPetById.'); + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (api_key) required + localVarHeaders = this.configuration.addCredentialToHeaders('api_key', 'api_key', localVarHeaders); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Find pet by ID + * @endpoint get /pet/{petId} + * @param params returns the request parameters. The resource stays idle while it returns undefined or a required parameter is null or undefined. + * @param options resource and request options + */ + public getPetByIdResource(params: () => GetPetByIdRequestParams | undefined, options: ApiHttpResourceOptions & { defaultValue: Pet }): HttpResourceRef; + public getPetByIdResource(params: () => GetPetByIdRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef; + public getPetByIdResource(params: () => GetPetByIdRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.getPetByIdResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } + + private getPetByIdResourceRequest(requestParameters: GetPetByIdRequestParams, localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest | undefined { + const petId = requestParameters.petId; + if (petId === null || petId === undefined) { + return undefined; + } + + let localVarHeaders = this.defaultHeaders; + + // authentication (api_key) required + localVarHeaders = this.configuration.addCredentialToHeaders('api_key', 'api_key', localVarHeaders); + + localVarHeaders = localVarHeaders.set('Accept', 'application/json'); + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + + /** + * Notes about a pet, as plain text + * @endpoint get /pet/{petId}/notes + * @param petId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public getPetNotes(petId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain', context?: HttpContext, transferCache?: boolean}): Observable; + public getPetNotes(petId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetNotes(petId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/plain', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetNotes(petId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/plain', context?: HttpContext, transferCache?: boolean}): Observable { + if (petId === null || petId === undefined) { + throw new Error('Required parameter petId was null or undefined when calling getPetNotes.'); + } + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'text/plain' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/notes`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Notes about a pet, as plain text + * @endpoint get /pet/{petId}/notes + * @param params returns the request parameters. The resource stays idle while it returns undefined or a required parameter is null or undefined. + * @param options resource and request options + */ + public getPetNotesResource(params: () => GetPetNotesRequestParams | undefined, options: ApiHttpResourceOptions & { defaultValue: string }): HttpResourceRef; + public getPetNotesResource(params: () => GetPetNotesRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef; + public getPetNotesResource(params: () => GetPetNotesRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource.text(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.getPetNotesResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } + + private getPetNotesResourceRequest(requestParameters: GetPetNotesRequestParams, localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest | undefined { + const petId = requestParameters.petId; + if (petId === null || petId === undefined) { + return undefined; + } + + let localVarHeaders = this.defaultHeaders; + + localVarHeaders = localVarHeaders.set('Accept', 'text/plain'); + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/notes`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + + /** + * Photo of a pet + * @endpoint get /pet/{petId}/photo + * @param petId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public getPetPhoto(petId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/octet-stream', context?: HttpContext, transferCache?: boolean}): Observable; + public getPetPhoto(petId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/octet-stream', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetPhoto(petId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/octet-stream', context?: HttpContext, transferCache?: boolean}): Observable>; + public getPetPhoto(petId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/octet-stream', context?: HttpContext, transferCache?: boolean}): Observable { + if (petId === null || petId === undefined) { + throw new Error('Required parameter petId was null or undefined when calling getPetPhoto.'); + } + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/octet-stream' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/photo`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: "blob", + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Photo of a pet + * @endpoint get /pet/{petId}/photo + * @param params returns the request parameters. The resource stays idle while it returns undefined or a required parameter is null or undefined. + * @param options resource and request options + */ + public getPetPhotoResource(params: () => GetPetPhotoRequestParams | undefined, options: ApiHttpResourceOptions & { defaultValue: Blob }): HttpResourceRef; + public getPetPhotoResource(params: () => GetPetPhotoRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef; + public getPetPhotoResource(params: () => GetPetPhotoRequestParams | undefined, options?: ApiHttpResourceOptions): HttpResourceRef { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource.blob(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.getPetPhotoResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } + + private getPetPhotoResourceRequest(requestParameters: GetPetPhotoRequestParams, localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest | undefined { + const petId = requestParameters.petId; + if (petId === null || petId === undefined) { + return undefined; + } + + let localVarHeaders = this.defaultHeaders; + + localVarHeaders = localVarHeaders.set('Accept', 'application/octet-stream'); + + let localVarPath = `/pet/${this.configuration.encodeParam({name: "petId", value: petId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: "int64"})}/photo`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + + /** + * List pets + * Every parameter is optional. + * @endpoint get /pet + * @param limit + * @param tags + * @param xRequestId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public listPets(limit?: number, tags?: Array, xRequestId?: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public listPets(limit?: number, tags?: Array, xRequestId?: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listPets(limit?: number, tags?: Array, xRequestId?: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listPets(limit?: number, tags?: Array, xRequestId?: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'limit', + limit, + QueryParamStyle.Form, + true, + ); + + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'tags', + tags, + QueryParamStyle.Form, + true, + ); + + + let localVarHeaders = this.defaultHeaders; + if (xRequestId !== undefined && xRequestId !== null) { + localVarHeaders = localVarHeaders.set('X-Request-Id', String(xRequestId)); + } + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters.toHttpParams(), + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * List pets + * Every parameter is optional. + * @endpoint get /pet + * @param params returns the request parameters, all of them optional. The resource stays idle while it returns undefined. + * @param options resource and request options + */ + public listPetsResource(params: (() => ListPetsRequestParams | undefined) | undefined, options: ApiHttpResourceOptions> & { defaultValue: Array }): HttpResourceRef>; + public listPetsResource(params?: () => ListPetsRequestParams | undefined, options?: ApiHttpResourceOptions>): HttpResourceRef | undefined>; + public listPetsResource(params: () => ListPetsRequestParams | undefined = () => ({}), options?: ApiHttpResourceOptions>): HttpResourceRef | undefined> { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource>(() => { + const requestParameters = params(); + return requestParameters === undefined ? undefined : this.listPetsResourceRequest(requestParameters, context, transferCache); + }, resourceOptions); + } + + private listPetsResourceRequest(requestParameters: ListPetsRequestParams, localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest { + const limit = requestParameters.limit; + const tags = requestParameters.tags; + const xRequestId = requestParameters.xRequestId; + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'limit', + limit, + QueryParamStyle.Form, + true, + ); + + + localVarQueryParameters = this.addToHttpParams( + localVarQueryParameters, + 'tags', + tags, + QueryParamStyle.Form, + true, + ); + + + let localVarHeaders = this.defaultHeaders; + if (xRequestId !== undefined && xRequestId !== null) { + localVarHeaders = localVarHeaders.set('X-Request-Id', String(xRequestId)); + } + + localVarHeaders = localVarHeaders.set('Accept', 'application/json'); + + let localVarPath = `/pet`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + params: localVarQueryParameters.toHttpParams(), + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + + /** + * Search pets with a request body + * A GET with a body keeps only its Observable method. + * @endpoint get /pet/search + * @param pet + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public searchPets(pet?: Pet, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public searchPets(pet?: Pet, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public searchPets(pet?: Pet, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public searchPets(pet?: Pet, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarHeaders = this.defaultHeaders; + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/pet/search`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: pet, + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/store.service.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/store.service.ts new file mode 100644 index 000000000000..dc4240668762 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/api/store.service.ts @@ -0,0 +1,134 @@ +/** + * httpResource petstore + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpContext + } from '@angular/common/http'; +import { HttpResourceRef, HttpResourceRequest, httpResource } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { OpenApiHttpParams, QueryParamStyle } from '../query.params'; + + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; +import { BaseService } from '../api.base.service'; +import { ApiHttpResourceOptions } from '../http.resource.options'; + + + +@Injectable({ + providedIn: 'root' +}) +export class StoreService extends BaseService { + + constructor(protected httpClient: HttpClient, @Optional() @Inject(BASE_PATH) basePath: string|string[], @Optional() configuration?: Configuration) { + super(basePath, configuration); + } + + /** + * Pet inventories by status + * No parameters, with the API key sent in the query. + * @endpoint get /store/inventory + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + * @param options additional options + */ + public getInventory(observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable<{ [key: string]: number; }>; + public getInventory(observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getInventory(observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getInventory(observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + let localVarHeaders = this.defaultHeaders; + + // authentication (api_key_query) required + localVarQueryParameters = this.configuration.addCredentialToQuery('api_key_query', 'api_key', localVarQueryParameters); + + const localVarHttpHeaderAcceptSelected: string | undefined = options?.httpHeaderAccept ?? this.configuration.selectHeaderAccept([ + 'application/json' + ]); + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + const localVarHttpContext: HttpContext = options?.context ?? new HttpContext(); + + const localVarTransferCache: boolean = options?.transferCache ?? true; + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/store/inventory`; + const { basePath, withCredentials } = this.configuration; + return this.httpClient.request<{ [key: string]: number; }>('get', `${basePath}${localVarPath}`, + { + context: localVarHttpContext, + params: localVarQueryParameters.toHttpParams(), + responseType: responseType_, + ...(withCredentials ? { withCredentials } : {}), + headers: localVarHeaders, + observe: observe, + ...(localVarTransferCache !== undefined ? { transferCache: localVarTransferCache } : {}), + reportProgress: reportProgress + } + ); + } + + /** + * Pet inventories by status + * No parameters, with the API key sent in the query. + * @endpoint get /store/inventory + * @param options resource and request options + */ + public getInventoryResource(options: ApiHttpResourceOptions<{ [key: string]: number; }> & { defaultValue: { [key: string]: number; } }): HttpResourceRef<{ [key: string]: number; }>; + public getInventoryResource(options?: ApiHttpResourceOptions<{ [key: string]: number; }>): HttpResourceRef<{ [key: string]: number; } | undefined>; + public getInventoryResource(options?: ApiHttpResourceOptions<{ [key: string]: number; }>): HttpResourceRef<{ [key: string]: number; } | undefined> { + const { context, transferCache, ...resourceOptions } = options ?? {}; + return httpResource<{ [key: string]: number; }>(() => this.getInventoryResourceRequest(context, transferCache), resourceOptions); + } + + private getInventoryResourceRequest(localVarHttpContext: HttpContext | undefined, localVarTransferCache: boolean | undefined): HttpResourceRequest { + + let localVarQueryParameters = new OpenApiHttpParams(this.encoder); + + let localVarHeaders = this.defaultHeaders; + + // authentication (api_key_query) required + localVarQueryParameters = this.configuration.addCredentialToQuery('api_key_query', 'api_key', localVarQueryParameters); + + localVarHeaders = localVarHeaders.set('Accept', 'application/json'); + + let localVarPath = `/store/inventory`; + const { basePath, withCredentials } = this.configuration; + return { + url: `${basePath}${localVarPath}`, + method: 'GET', + params: localVarQueryParameters.toHttpParams(), + headers: localVarHeaders, + context: localVarHttpContext ?? new HttpContext(), + transferCache: localVarTransferCache ?? true, + ...(withCredentials ? { withCredentials } : {}), + }; + } + +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/configuration.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/configuration.ts new file mode 100644 index 000000000000..5647b55b87a3 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/configuration.ts @@ -0,0 +1,216 @@ +import { HttpHeaders, HttpParameterCodec } from '@angular/common/http'; +import { Param } from './param'; +import { OpenApiHttpParams } from './query.params'; + +export interface ConfigurationParameters { + /** + * @deprecated Since 5.0. Use credentials instead + */ + apiKeys?: {[ key: string ]: string}; + username?: string; + password?: string; + /** + * @deprecated Since 5.0. Use credentials instead + */ + accessToken?: string | (() => string); + basePath?: string; + withCredentials?: boolean; + /** + * Takes care of encoding query- and form-parameters. + */ + encoder?: HttpParameterCodec; + /** + * Override the default method for encoding path parameters in various + * styles. + *

+ * See {@link README.md} for more details + *

+ */ + encodeParam?: (param: Param) => string; + /** + * The keys are the names in the securitySchemes section of the OpenAPI + * document. They should map to the value used for authentication + * minus any standard prefixes such as 'Basic' or 'Bearer'. + */ + credentials?: {[ key: string ]: string | (() => string | undefined)}; +} + +export class Configuration { + /** + * @deprecated Since 5.0. Use credentials instead + */ + apiKeys?: {[ key: string ]: string}; + username?: string; + password?: string; + /** + * @deprecated Since 5.0. Use credentials instead + */ + accessToken?: string | (() => string); + basePath?: string; + withCredentials?: boolean; + /** + * Takes care of encoding query- and form-parameters. + */ + encoder?: HttpParameterCodec; + /** + * Encoding of various path parameter + * styles. + *

+ * See {@link README.md} for more details + *

+ */ + encodeParam: (param: Param) => string; + /** + * The keys are the names in the securitySchemes section of the OpenAPI + * document. They should map to the value used for authentication + * minus any standard prefixes such as 'Basic' or 'Bearer'. + */ + credentials: {[ key: string ]: string | (() => string | undefined)}; + +constructor({ accessToken, apiKeys, basePath, credentials, encodeParam, encoder, password, username, withCredentials }: ConfigurationParameters = {}) { + if (apiKeys) { + this.apiKeys = apiKeys; + } + if (username !== undefined) { + this.username = username; + } + if (password !== undefined) { + this.password = password; + } + if (accessToken !== undefined) { + this.accessToken = accessToken; + } + if (basePath !== undefined) { + this.basePath = basePath; + } + if (withCredentials !== undefined) { + this.withCredentials = withCredentials; + } + if (encoder) { + this.encoder = encoder; + } + this.encodeParam = encodeParam ?? (param => this.defaultEncodeParam(param)); + this.credentials = credentials ?? {}; + + // init default api_key credential + if (!this.credentials['api_key']) { + this.credentials['api_key'] = () => { + if (this.apiKeys === null || this.apiKeys === undefined) { + return undefined; + } else { + return this.apiKeys['api_key'] || this.apiKeys['api_key']; + } + }; + } + + // init default api_key_query credential + if (!this.credentials['api_key_query']) { + this.credentials['api_key_query'] = () => { + if (this.apiKeys === null || this.apiKeys === undefined) { + return undefined; + } else { + return this.apiKeys['api_key_query'] || this.apiKeys['api_key']; + } + }; + } + + // init default bearer_auth credential + if (!this.credentials['bearer_auth']) { + this.credentials['bearer_auth'] = () => { + return typeof this.accessToken === 'function' + ? this.accessToken() + : this.accessToken; + }; + } + } + + /** + * Select the correct content-type to use for a request. + * Uses {@link Configuration#isJsonMime} to determine the correct content-type. + * If no content type is found return the first found type if the contentTypes is not empty + * @param contentTypes - the array of content types that are available for selection + * @returns the selected content-type or undefined if no selection could be made. + */ + public selectHeaderContentType (contentTypes: string[]): string | undefined { + if (contentTypes.length === 0) { + return undefined; + } + + const type = contentTypes.find((x: string) => this.isJsonMime(x)); + if (type === undefined) { + return contentTypes[0]; + } + return type; + } + + /** + * Select the correct accept content-type to use for a request. + * Uses {@link Configuration#isJsonMime} to determine the correct accept content-type. + * If no content type is found return the first found type if the contentTypes is not empty + * @param accepts - the array of content types that are available for selection. + * @returns the selected content-type or undefined if no selection could be made. + */ + public selectHeaderAccept(accepts: string[]): string | undefined { + if (accepts.length === 0) { + return undefined; + } + + const type = accepts.find((x: string) => this.isJsonMime(x)); + if (type === undefined) { + return accepts[0]; + } + return type; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = /^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$/i; + return mime !== null && jsonMime.test(mime); + } + + public lookupCredential(key: string): string | undefined { + const value = this.credentials[key]; + return typeof value === 'function' + ? value() + : value; + } + + public addCredentialToHeaders(credentialKey: string, headerName: string, headers: HttpHeaders, prefix?: string): HttpHeaders { + const value = this.lookupCredential(credentialKey); + return value + ? headers.set(headerName, (prefix ?? '') + value) + : headers; + } + + public addCredentialToQuery(credentialKey: string, paramName: string, query: OpenApiHttpParams): OpenApiHttpParams { + const value = this.lookupCredential(credentialKey); + return value + ? query.set(paramName, value) + : query; + } + + private defaultEncodeParam(param: Param): string { + // This implementation exists as fallback for missing configuration + // and for backwards compatibility to older typescript-angular generator versions. + // It only works for the 'simple' parameter style. + // Date-handling only works for the 'date-time' format. + // All other styles and Date-formats are probably handled incorrectly. + // + // But: if that's all you need (i.e.: the most common use-case): no need for customization! + + const value = param.dataFormat === 'date-time' && param.value instanceof Date + ? (param.value as Date).toISOString() + : param.value; + + return encodeURIComponent(String(value)); + } +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/encoder.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/encoder.ts new file mode 100644 index 000000000000..af4523543e6c --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/encoder.ts @@ -0,0 +1,35 @@ +import { HttpParameterCodec } from '@angular/common/http'; + +/** + * Custom HttpParameterCodec + * Workaround for https://github.com/angular/angular/issues/18261 + */ +export class CustomHttpParameterCodec implements HttpParameterCodec { + encodeKey(k: string): string { + return encodeURIComponent(k); + } + encodeValue(v: string): string { + return encodeURIComponent(v); + } + decodeKey(k: string): string { + return decodeURIComponent(k); + } + decodeValue(v: string): string { + return decodeURIComponent(v); + } +} + +export class IdentityHttpParameterCodec implements HttpParameterCodec { + encodeKey(k: string): string { + return k; + } + encodeValue(v: string): string { + return v; + } + decodeKey(k: string): string { + return k; + } + decodeValue(v: string): string { + return v; + } +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/git_push.sh b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/git_push.sh new file mode 100644 index 000000000000..a35991cd51a1 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/git_push.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ -z "${git_host}" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \${git_host} to ${git_host}" +fi + +if [ -z "${git_user_id}" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \${git_user_id} to ${git_user_id}" +fi + +if [ -z "${git_repo_id}" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \${git_repo_id} to ${git_repo_id}" +fi + +if [ -z "${release_note}" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \${release_note} to ${release_note}" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "${release_note}" + +# Sets the new remote +if [ -z "$(git remote)" ]; then # git remote not defined + if [ -z "${GIT_TOKEN:-}" ]; then + echo "[INFO] \${GIT_TOKEN} (environment variable) is not set. Using the git credential in your environment." + git remote add origin "https://${git_host}/${git_user_id}/${git_repo_id}.git" + else + git remote add origin "https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git" + fi +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/http.resource.options.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/http.resource.options.ts new file mode 100644 index 000000000000..0128ade37645 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/http.resource.options.ts @@ -0,0 +1,41 @@ +/** + * httpResource petstore + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ +import { Injector, ValueEqualityFn } from '@angular/core'; +import { HttpContext } from '@angular/common/http'; + +/** + * Options of the generated `...Resource` methods, which return an Angular `httpResource`. + */ +export interface ApiHttpResourceOptions { + /** + * Value of the resource while no response is available, for example while it is idle or loading. + */ + defaultValue?: T; + /** + * Injector the resource is created in. Required when the method is called outside an injection context. + */ + injector?: Injector; + /** + * Equality function used to compare the values of the resource. + */ + equal?: ValueEqualityFn; + /** + * Debug name of the resource. Used by Angular 21.1 and later, ignored before. + */ + debugName?: string; + /** + * Context of the request. + */ + context?: HttpContext; + /** + * Whether the SSR transfer cache may be used for the request. Defaults to true. + */ + transferCache?: boolean; +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/index.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/index.ts new file mode 100644 index 000000000000..c984d12d0848 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/index.ts @@ -0,0 +1,8 @@ +export * from './api/api'; +export * from './model/models'; +export * from './variables'; +export * from './configuration'; +export * from './api.module'; +export * from './provide-api'; +export * from './http.resource.options'; +export * from './param'; diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/models.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/models.ts new file mode 100644 index 000000000000..eccf4ec59cbb --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/models.ts @@ -0,0 +1 @@ +export * from './pet'; diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/pet.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/pet.ts new file mode 100644 index 000000000000..40e3bf53975c --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/model/pet.ts @@ -0,0 +1,26 @@ +/** + * httpResource petstore + * + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface Pet { + id?: number; + name: string; + status?: Pet.StatusEnum; +} +export namespace Pet { + export const StatusEnum = { + Available: 'available', + Pending: 'pending', + Sold: 'sold', + } as const; + export type StatusEnum = typeof StatusEnum[keyof typeof StatusEnum]; +} + + diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/param.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/param.ts new file mode 100644 index 000000000000..78a2d20a6433 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/param.ts @@ -0,0 +1,69 @@ +/** + * Standard parameter styles defined by OpenAPI spec + */ +export type StandardParamStyle = + | 'matrix' + | 'label' + | 'form' + | 'simple' + | 'spaceDelimited' + | 'pipeDelimited' + | 'deepObject' + ; + +/** + * The OpenAPI standard {@link StandardParamStyle}s may be extended by custom styles by the user. + */ +export type ParamStyle = StandardParamStyle | string; + +/** + * Standard parameter locations defined by OpenAPI spec + */ +export type ParamLocation = 'query' | 'header' | 'path' | 'cookie'; + +/** + * Standard types as defined in OpenAPI Specification: Data Types + */ +export type StandardDataType = + | "integer" + | "number" + | "boolean" + | "string" + | "object" + | "array" + ; + +/** + * Standard {@link DataType}s plus your own types/classes. + */ +export type DataType = StandardDataType | string; + +/** + * Standard formats as defined in OpenAPI Specification: Data Types + */ +export type StandardDataFormat = + | "int32" + | "int64" + | "float" + | "double" + | "byte" + | "binary" + | "date" + | "date-time" + | "password" + ; + +export type DataFormat = StandardDataFormat | string; + +/** + * The parameter to encode. + */ +export interface Param { + name: string; + value: unknown; + in: ParamLocation; + style: ParamStyle, + explode: boolean; + dataType: DataType; + dataFormat: DataFormat | undefined; +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/provide-api.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/provide-api.ts new file mode 100644 index 000000000000..19c762acdfe0 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/provide-api.ts @@ -0,0 +1,15 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from "@angular/core"; +import { Configuration, ConfigurationParameters } from './configuration'; +import { BASE_PATH } from './variables'; + +// Returns the service class providers, to be used in the [ApplicationConfig](https://angular.dev/api/core/ApplicationConfig). +export function provideApi(configOrBasePath: string | ConfigurationParameters): EnvironmentProviders { + return makeEnvironmentProviders([ + typeof configOrBasePath === "string" + ? { provide: BASE_PATH, useValue: configOrBasePath } + : { + provide: Configuration, + useValue: new Configuration({ ...configOrBasePath }), + }, + ]); +} \ No newline at end of file diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/query.params.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/query.params.ts new file mode 100644 index 000000000000..4076c1140c83 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/query.params.ts @@ -0,0 +1,160 @@ +import { HttpParams, HttpParameterCodec } from '@angular/common/http'; +import { CustomHttpParameterCodec, IdentityHttpParameterCodec } from './encoder'; + +export enum QueryParamStyle { + Json, + Form, + DeepObject, + SpaceDelimited, + PipeDelimited, +} + +export type Delimiter = "," | " " | "|" | "\t"; + +export interface ParamOptions { + /** When true, serialized as multiple repeated key=value pairs. When false, serialized as a single key with joined values using `delimiter`. */ + explode?: boolean; + /** Delimiter used when explode=false. The delimiter itself is inserted unencoded between encoded values. */ + delimiter?: Delimiter; +} + +interface ParamEntry { + values: string[]; + options: Required; +} + +export class OpenApiHttpParams { + private params: Map = new Map(); + private defaults: Required; + private encoder: HttpParameterCodec; + + /** + * @param encoder Parameter serializer + * @param defaults Global defaults used when a specific parameter has no explicit options. + * By OpenAPI default, explode is true for query params with style=form. + */ + constructor(encoder?: HttpParameterCodec, defaults?: { explode?: boolean; delimiter?: Delimiter }) { + this.encoder = encoder || new CustomHttpParameterCodec(); + this.defaults = { + explode: defaults?.explode ?? true, + delimiter: defaults?.delimiter ?? ",", + }; + } + + private resolveOptions(local?: ParamOptions): Required { + return { + explode: local?.explode ?? this.defaults.explode, + delimiter: local?.delimiter ?? this.defaults.delimiter, + }; + } + + /** + * Replace the parameter's values and (optionally) its options. + * Options are stored per-parameter (not global). + */ + set(key: string, values: string[] | string, options?: ParamOptions): this { + const arr = Array.isArray(values) ? values.slice() : [values]; + const opts = this.resolveOptions(options); + this.params.set(key, {values: arr, options: opts}); + return this; + } + + /** + * Append a single value to the parameter. If the parameter didn't exist it will be created + * and use resolved options (global defaults merged with any provided options). + */ + append(key: string, value: string, options?: ParamOptions): this { + const entry = this.params.get(key); + if (entry) { + // If new options provided, override the stored options for subsequent serialization + if (options) { + entry.options = this.resolveOptions({...entry.options, ...options}); + } + entry.values.push(value); + } else { + this.set(key, [value], options); + } + return this; + } + + /** + * Serialize to a query string according to per-parameter OpenAPI options. + * - If explode=true for that parameter → repeated key=value pairs (each value encoded). + * - If explode=false for that parameter → single key=value where values are individually encoded + * and joined using the configured delimiter. The delimiter character is inserted AS-IS + * (not percent-encoded). + */ + toString(): string { + const records = this.toRecord(); + const parts: string[] = []; + + for (const key in records) { + parts.push(`${key}=${records[key]}`); + } + + return parts.join("&"); + } + + /** + * Return parameters as a plain record. + * - If a parameter has exactly one value, returns that value directly. + * - If a parameter has multiple values, returns a readonly array of values. + */ + toRecord(): Record> { + const parts: Record> = {}; + + for (const [key, entry] of this.params.entries()) { + const encodedKey = this.encoder.encodeKey(key); + + if (entry.options.explode) { + parts[encodedKey] = entry.values.map((v) => this.encoder.encodeValue(v)); + } else { + const encodedValues = entry.values.map((v) => this.encoder.encodeValue(v)); + + // join with the delimiter *unencoded* + parts[encodedKey] = encodedValues.join(entry.options.delimiter); + } + } + + return parts; + } + + /** + * Return an Angular's HttpParams with an identity parameter codec as the parameters are already encoded. + */ + toHttpParams(): HttpParams { + const records = this.toRecord(); + + let httpParams = new HttpParams({encoder: new IdentityHttpParameterCodec()}); + + return httpParams.appendAll(records); + } +} + +export function concatHttpParamsObject(httpParams: OpenApiHttpParams, key: string, item: { + [index: string]: any +}, delimiter: Delimiter): OpenApiHttpParams { + let keyAndValues: string[] = []; + + for (const k in item) { + keyAndValues.push(k); + + const value = item[k]; + + if (Array.isArray(value)) { + keyAndValues.push(...value.map(convertToString)); + } else { + keyAndValues.push(convertToString(value)); + } + } + + return httpParams.set(key, keyAndValues, {explode: false, delimiter: delimiter}); +} + +function convertToString(value: any): string { + if (value instanceof Date) { + return value.toISOString(); + } else { + return value.toString(); + } +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/variables.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/variables.ts new file mode 100644 index 000000000000..6fe58549f395 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/with-http-resource/variables.ts @@ -0,0 +1,9 @@ +import { InjectionToken } from '@angular/core'; + +export const BASE_PATH = new InjectionToken('basePath'); +export const COLLECTION_FORMATS = { + 'csv': ',', + 'tsv': ' ', + 'ssv': ' ', + 'pipes': '|' +} diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/tests/default/src/test/http-resource.spec.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/tests/default/src/test/http-resource.spec.ts new file mode 100644 index 000000000000..468dfd8e7d18 --- /dev/null +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/tests/default/src/test/http-resource.spec.ts @@ -0,0 +1,226 @@ +import { Injector, signal } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { HttpContext, HttpContextToken, provideHttpClient } from '@angular/common/http' +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing' +import { + Configuration, + PetService, + StoreService, + type Pet +} from '@swagger/typescript-angular-http-resource' + +const TOKEN = new HttpContextToken(() => 'none') + +describe('API (httpResource)', () => { + let http: HttpTestingController + let petService: PetService + let storeService: StoreService + const accessToken = signal('first-token') + + // lets the resources react to signal changes and deliver the flushed responses + const settle = async (): Promise => { + TestBed.tick() + await new Promise(resolve => setTimeout(resolve)) + TestBed.tick() + } + + beforeEach(() => { + accessToken.set('first-token') + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + { + provide: Configuration, + useValue: new Configuration({ + basePath: 'http://localhost/api', + withCredentials: true, + credentials: { + api_key: 'secret-key', + api_key_query: 'query-key', + bearer_auth: () => accessToken() + } + }) + } + ] + }) + http = TestBed.inject(HttpTestingController) + petService = TestBed.inject(PetService) + storeService = TestBed.inject(StoreService) + }) + + afterEach(() => { + http.verify() + }) + + it('stays idle while the parameters are undefined', async () => { + const petId = signal(undefined) + const pet = TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => { + const id = petId() + return id === undefined ? undefined : { petId: id } + })) + await settle() + + expect(pet.status()).toBe('idle') + expect(pet.value()).toBeUndefined() + http.expectNone(() => true) + + petId.set(1) + await settle() + http.expectOne('http://localhost/api/pet/1').flush({ id: 1, name: 'Rex' }) + await settle() + expect(pet.value()?.name).toBe('Rex') + }) + + it('stays idle while a required parameter is null or undefined', async () => { + const withNull = TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => ({ petId: null as unknown as number }))) + const withUndefined = TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => ({ petId: undefined as unknown as number }))) + await settle() + + expect(withNull.status()).toBe('idle') + expect(withUndefined.status()).toBe('idle') + http.expectNone(() => true) + }) + + it('builds the request from the parameters, the configuration and the options', async () => { + const context = new HttpContext().set(TOKEN, 'from-options') + const pet = TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => ({ petId: 7 }), { context, transferCache: false })) + await settle() + + const req = http.expectOne('http://localhost/api/pet/7') + expect(req.request.method).toBe('GET') + expect(req.request.headers.get('api_key')).toBe('secret-key') + expect(req.request.headers.get('Accept')).toBe('application/json') + expect(req.request.withCredentials).toBe(true) + expect(req.request.context.get(TOKEN)).toBe('from-options') + expect(req.request.transferCache).toBe(false) + expect(req.request.responseType).toBe('json') + + req.flush({ id: 7, name: 'Rex' }) + await settle() + expect(pet.status()).toBe('resolved') + expect(pet.value()).toEqual({ id: 7, name: 'Rex' }) + expect(pet.statusCode()).toBe(200) + }) + + it('uses the transfer cache by default', async () => { + TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => ({ petId: 7 }))) + await settle() + + const req = http.expectOne('http://localhost/api/pet/7') + expect(req.request.transferCache).toBe(true) + req.flush({ id: 7, name: 'Rex' }) + await settle() + }) + + it('sends a new request when a parameter changes', async () => { + const petId = signal(1) + const pet = TestBed.runInInjectionContext(() => petService.getPetByIdResource(() => ({ petId: petId() }))) + await settle() + + const first = http.expectOne('http://localhost/api/pet/1') + petId.set(2) + await settle() + + expect(first.cancelled).toBe(true) + http.expectOne('http://localhost/api/pet/2').flush({ id: 2, name: 'Max' }) + await settle() + expect(pet.value()?.name).toBe('Max') + }) + + it('encodes query parameters with the configured codec', async () => { + const tags = TestBed.runInInjectionContext(() => petService.listPetsResource(() => ({ tags: ['a b&c', 'd'], xRequestId: 'request-1' }))) + await settle() + + const req = http.expectOne(r => r.url === 'http://localhost/api/pet') + expect(req.request.urlWithParams).toBe('http://localhost/api/pet?tags=a%20b%26c&tags=d') + expect(req.request.headers.get('X-Request-Id')).toBe('request-1') + req.flush([]) + await settle() + expect(tags.value()).toEqual([]) + }) + + it('sends the request without arguments when every parameter is optional', async () => { + TestBed.runInInjectionContext(() => petService.listPetsResource()) + await settle() + + const req = http.expectOne('http://localhost/api/pet') + expect(req.request.params.keys()).toEqual([]) + req.flush([]) + await settle() + }) + + it('sends the API key in the query for an operation without parameters', async () => { + const inventory = TestBed.runInInjectionContext(() => storeService.getInventoryResource()) + await settle() + + const req = http.expectOne(r => r.url === 'http://localhost/api/store/inventory') + expect(req.request.urlWithParams).toBe('http://localhost/api/store/inventory?api_key=query-key') + req.flush({ available: 3 }) + await settle() + expect(inventory.value()).toEqual({ available: 3 }) + }) + + it('reads the credentials when the request is built', async () => { + const byStatus = TestBed.runInInjectionContext(() => petService.findPetsByStatusResource(() => ({ status: ['available', 'sold'] }), { defaultValue: [] })) + const initial: Pet[] = byStatus.value() + expect(initial).toEqual([]) + await settle() + + let req = http.expectOne(r => r.url === 'http://localhost/api/pet/findByStatus') + expect(req.request.urlWithParams).toBe('http://localhost/api/pet/findByStatus?status=available,sold') + expect(req.request.headers.get('Authorization')).toBe('Bearer first-token') + req.flush([{ id: 1, name: 'Rex' }]) + await settle() + expect(byStatus.value().length).toBe(1) + + accessToken.set('second-token') + await settle() + req = http.expectOne(r => r.url === 'http://localhost/api/pet/findByStatus') + expect(req.request.headers.get('Authorization')).toBe('Bearer second-token') + req.flush([]) + await settle() + }) + + it('uses httpResource.text for a text response', async () => { + const notes = TestBed.runInInjectionContext(() => petService.getPetNotesResource(() => ({ petId: 3 }))) + await settle() + + const req = http.expectOne('http://localhost/api/pet/3/notes') + expect(req.request.responseType).toBe('text') + expect(req.request.headers.get('Accept')).toBe('text/plain') + req.flush('likes walks') + await settle() + expect(notes.value()).toBe('likes walks') + }) + + it('uses httpResource.blob for a binary response', async () => { + const photo = TestBed.runInInjectionContext(() => petService.getPetPhotoResource(() => ({ petId: 3 }))) + await settle() + + const req = http.expectOne('http://localhost/api/pet/3/photo') + expect(req.request.responseType).toBe('blob') + expect(req.request.headers.get('Accept')).toBe('application/octet-stream') + req.flush(new Blob(['png'])) + await settle() + expect(photo.value() instanceof Blob).toBe(true) + }) + + it('takes an injector when called outside an injection context', async () => { + const pet = petService.getPetByIdResource(() => ({ petId: 5 }), { injector: TestBed.inject(Injector) }) + await settle() + + http.expectOne('http://localhost/api/pet/5').flush({ id: 5, name: 'Bo' }) + await settle() + expect(pet.value()?.name).toBe('Bo') + pet.destroy() + }) + + it('generates resources for reads only', () => { + const methods = petService as unknown as Record + expect(typeof methods['getPetByIdResource']).toBe('function') + expect(methods['addPetResource']).toBeUndefined() + expect(methods['deletePetResource']).toBeUndefined() + expect(methods['searchPetsResource']).toBeUndefined() + }) +}) diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/tsconfig.json b/samples/client/petstore/typescript-angular-v22-provided-in-root/tsconfig.json index b32274175413..5afb11a6148f 100644 --- a/samples/client/petstore/typescript-angular-v22-provided-in-root/tsconfig.json +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/tsconfig.json @@ -13,7 +13,8 @@ "target": "es2022", "typeRoots": ["node_modules/@types"], "paths": { - "@swagger/typescript-angular-petstore": ["./builds/default"] + "@swagger/typescript-angular-petstore": ["./builds/default"], + "@swagger/typescript-angular-http-resource": ["./builds/with-http-resource"] } } }