fork of hey-api/openapi-ts because I need some additional things

feat: Implement Angular HTTP Resource and Request plugins

- Added `httpResource` and `httpRequest` companion plugin handlers to generate Angular services and functions for API operations.
- Introduced `PetResources`, `StoreResources`, and `UserResources` classes for managing pet store operations.
- Enhanced configuration options for HTTP resources and requests, allowing customization of class and method names.
- Generated TypeScript definitions for Angular HTTP resource and request options.
- Updated plugin handler to conditionally enable HTTP resource generation based on configuration.

Max Scopp 2f84f752 000eebec

+1158 -337
+5 -2
examples/openapi-ts-angular-resource/openapi-ts.config.ts
··· 14 14 throwOnError: true, 15 15 }, 16 16 { 17 - asClass: true, 18 - name: '@hey-api/angular-resource', 17 + httpResource: { 18 + asClass: true, 19 + enabled: true, 20 + }, 21 + name: '@angular/common', 19 22 }, 20 23 '@hey-api/schemas', 21 24 {
+1 -1
examples/openapi-ts-angular-resource/src/app/demo/demo.html
··· 4 4 @if (pet.error()) { 5 5 <div class="error-message"> 6 6 <div class="error-title">Error occurred:</div> 7 - <div class="error-details">{{ pet.error() }}</div> 7 + <div class="error-details">{{ pet.error()|json }}</div> 8 8 </div> 9 9 } 10 10
+1 -1
examples/openapi-ts-angular-resource/src/app/demo/demo.ts
··· 2 2 import { Component, inject, signal } from '@angular/core'; 3 3 import { RouterOutlet } from '@angular/router'; 4 4 5 - import { PetResources } from '../../client/@hey-api/angular-resource.gen'; 5 + import { PetResources } from '../../client'; 6 6 7 7 @Component({ 8 8 host: { ngSkipHydration: 'true' },
+295
examples/openapi-ts-angular-resource/src/client/@angular/common/http/httpRequests.gen.ts
··· 1 + // This file is auto-generated by @hey-api/openapi-ts 2 + 3 + import type { HttpRequest } from '@angular/common/http'; 4 + 5 + import { client as _heyApiClient } from '../../../client.gen'; 6 + import type { Options } from '../../../sdk.gen'; 7 + import type { 8 + AddPetData, 9 + CreateUserData, 10 + CreateUsersWithListInputData, 11 + DeleteOrderData, 12 + DeletePetData, 13 + DeleteUserData, 14 + FindPetsByStatusData, 15 + FindPetsByTagsData, 16 + GetInventoryData, 17 + GetOrderByIdData, 18 + GetPetByIdData, 19 + GetUserByNameData, 20 + LoginUserData, 21 + LogoutUserData, 22 + PlaceOrderData, 23 + UpdatePetData, 24 + UpdatePetWithFormData, 25 + UpdateUserData, 26 + UploadFileData, 27 + } from '../../../types.gen'; 28 + 29 + /** 30 + * Add a new pet to the store. 31 + * Add a new pet to the store. 32 + */ 33 + export const addPetRequest = <ThrowOnError extends boolean = false>( 34 + options: Options<AddPetData, ThrowOnError>, 35 + ): HttpRequest<unknown> => 36 + (options?.client ? options.client : _heyApiClient).requestOptions({ 37 + method: 'POST', 38 + responseStyle: 'data', 39 + url: '/pet', 40 + ...options, 41 + }); 42 + 43 + /** 44 + * Update an existing pet. 45 + * Update an existing pet by Id. 46 + */ 47 + export const updatePetRequest = <ThrowOnError extends boolean = false>( 48 + options: Options<UpdatePetData, ThrowOnError>, 49 + ): HttpRequest<unknown> => 50 + (options?.client ? options.client : _heyApiClient).requestOptions({ 51 + method: 'PUT', 52 + responseStyle: 'data', 53 + url: '/pet', 54 + ...options, 55 + }); 56 + 57 + /** 58 + * Finds Pets by status. 59 + * Multiple status values can be provided with comma separated strings. 60 + */ 61 + export const findPetsByStatusRequest = <ThrowOnError extends boolean = false>( 62 + options: Options<FindPetsByStatusData, ThrowOnError>, 63 + ): HttpRequest<unknown> => 64 + (options?.client ? options.client : _heyApiClient).requestOptions({ 65 + method: 'GET', 66 + responseStyle: 'data', 67 + url: '/pet/findByStatus', 68 + ...options, 69 + }); 70 + 71 + /** 72 + * Finds Pets by tags. 73 + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. 74 + */ 75 + export const findPetsByTagsRequest = <ThrowOnError extends boolean = false>( 76 + options: Options<FindPetsByTagsData, ThrowOnError>, 77 + ): HttpRequest<unknown> => 78 + (options?.client ? options.client : _heyApiClient).requestOptions({ 79 + method: 'GET', 80 + responseStyle: 'data', 81 + url: '/pet/findByTags', 82 + ...options, 83 + }); 84 + 85 + /** 86 + * Deletes a pet. 87 + * Delete a pet. 88 + */ 89 + export const deletePetRequest = <ThrowOnError extends boolean = false>( 90 + options: Options<DeletePetData, ThrowOnError>, 91 + ): HttpRequest<unknown> => 92 + (options?.client ? options.client : _heyApiClient).requestOptions({ 93 + method: 'DELETE', 94 + responseStyle: 'data', 95 + url: '/pet/{petId}', 96 + ...options, 97 + }); 98 + 99 + /** 100 + * Find pet by ID. 101 + * Returns a single pet. 102 + */ 103 + export const getPetByIdRequest = <ThrowOnError extends boolean = false>( 104 + options: Options<GetPetByIdData, ThrowOnError>, 105 + ): HttpRequest<unknown> => 106 + (options?.client ? options.client : _heyApiClient).requestOptions({ 107 + method: 'GET', 108 + responseStyle: 'data', 109 + url: '/pet/{petId}', 110 + ...options, 111 + }); 112 + 113 + /** 114 + * Updates a pet in the store with form data. 115 + * Updates a pet resource based on the form data. 116 + */ 117 + export const updatePetWithFormRequest = <ThrowOnError extends boolean = false>( 118 + options: Options<UpdatePetWithFormData, ThrowOnError>, 119 + ): HttpRequest<unknown> => 120 + (options?.client ? options.client : _heyApiClient).requestOptions({ 121 + method: 'POST', 122 + responseStyle: 'data', 123 + url: '/pet/{petId}', 124 + ...options, 125 + }); 126 + 127 + /** 128 + * Uploads an image. 129 + * Upload image of the pet. 130 + */ 131 + export const uploadFileRequest = <ThrowOnError extends boolean = false>( 132 + options: Options<UploadFileData, ThrowOnError>, 133 + ): HttpRequest<unknown> => 134 + (options?.client ? options.client : _heyApiClient).requestOptions({ 135 + method: 'POST', 136 + responseStyle: 'data', 137 + url: '/pet/{petId}/uploadImage', 138 + ...options, 139 + }); 140 + 141 + /** 142 + * Returns pet inventories by status. 143 + * Returns a map of status codes to quantities. 144 + */ 145 + export const getInventoryRequest = <ThrowOnError extends boolean = false>( 146 + options?: Options<GetInventoryData, ThrowOnError>, 147 + ): HttpRequest<unknown> => 148 + (options?.client ? options.client : _heyApiClient).requestOptions({ 149 + method: 'GET', 150 + responseStyle: 'data', 151 + url: '/store/inventory', 152 + ...options, 153 + }); 154 + 155 + /** 156 + * Place an order for a pet. 157 + * Place a new order in the store. 158 + */ 159 + export const placeOrderRequest = <ThrowOnError extends boolean = false>( 160 + options?: Options<PlaceOrderData, ThrowOnError>, 161 + ): HttpRequest<unknown> => 162 + (options?.client ? options.client : _heyApiClient).requestOptions({ 163 + method: 'POST', 164 + responseStyle: 'data', 165 + url: '/store/order', 166 + ...options, 167 + }); 168 + 169 + /** 170 + * Delete purchase order by identifier. 171 + * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. 172 + */ 173 + export const deleteOrderRequest = <ThrowOnError extends boolean = false>( 174 + options: Options<DeleteOrderData, ThrowOnError>, 175 + ): HttpRequest<unknown> => 176 + (options?.client ? options.client : _heyApiClient).requestOptions({ 177 + method: 'DELETE', 178 + responseStyle: 'data', 179 + url: '/store/order/{orderId}', 180 + ...options, 181 + }); 182 + 183 + /** 184 + * Find purchase order by ID. 185 + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. 186 + */ 187 + export const getOrderByIdRequest = <ThrowOnError extends boolean = false>( 188 + options: Options<GetOrderByIdData, ThrowOnError>, 189 + ): HttpRequest<unknown> => 190 + (options?.client ? options.client : _heyApiClient).requestOptions({ 191 + method: 'GET', 192 + responseStyle: 'data', 193 + url: '/store/order/{orderId}', 194 + ...options, 195 + }); 196 + 197 + /** 198 + * Create user. 199 + * This can only be done by the logged in user. 200 + */ 201 + export const createUserRequest = <ThrowOnError extends boolean = false>( 202 + options?: Options<CreateUserData, ThrowOnError>, 203 + ): HttpRequest<unknown> => 204 + (options?.client ? options.client : _heyApiClient).requestOptions({ 205 + method: 'POST', 206 + responseStyle: 'data', 207 + url: '/user', 208 + ...options, 209 + }); 210 + 211 + /** 212 + * Creates list of users with given input array. 213 + * Creates list of users with given input array. 214 + */ 215 + export const createUsersWithListInputRequest = < 216 + ThrowOnError extends boolean = false, 217 + >( 218 + options?: Options<CreateUsersWithListInputData, ThrowOnError>, 219 + ): HttpRequest<unknown> => 220 + (options?.client ? options.client : _heyApiClient).requestOptions({ 221 + method: 'POST', 222 + responseStyle: 'data', 223 + url: '/user/createWithList', 224 + ...options, 225 + }); 226 + 227 + /** 228 + * Logs user into the system. 229 + * Log into the system. 230 + */ 231 + export const loginUserRequest = <ThrowOnError extends boolean = false>( 232 + options?: Options<LoginUserData, ThrowOnError>, 233 + ): HttpRequest<unknown> => 234 + (options?.client ? options.client : _heyApiClient).requestOptions({ 235 + method: 'GET', 236 + responseStyle: 'data', 237 + url: '/user/login', 238 + ...options, 239 + }); 240 + 241 + /** 242 + * Logs out current logged in user session. 243 + * Log user out of the system. 244 + */ 245 + export const logoutUserRequest = <ThrowOnError extends boolean = false>( 246 + options?: Options<LogoutUserData, ThrowOnError>, 247 + ): HttpRequest<unknown> => 248 + (options?.client ? options.client : _heyApiClient).requestOptions({ 249 + method: 'GET', 250 + responseStyle: 'data', 251 + url: '/user/logout', 252 + ...options, 253 + }); 254 + 255 + /** 256 + * Delete user resource. 257 + * This can only be done by the logged in user. 258 + */ 259 + export const deleteUserRequest = <ThrowOnError extends boolean = false>( 260 + options: Options<DeleteUserData, ThrowOnError>, 261 + ): HttpRequest<unknown> => 262 + (options?.client ? options.client : _heyApiClient).requestOptions({ 263 + method: 'DELETE', 264 + responseStyle: 'data', 265 + url: '/user/{username}', 266 + ...options, 267 + }); 268 + 269 + /** 270 + * Get user by user name. 271 + * Get user detail based on username. 272 + */ 273 + export const getUserByNameRequest = <ThrowOnError extends boolean = false>( 274 + options: Options<GetUserByNameData, ThrowOnError>, 275 + ): HttpRequest<unknown> => 276 + (options?.client ? options.client : _heyApiClient).requestOptions({ 277 + method: 'GET', 278 + responseStyle: 'data', 279 + url: '/user/{username}', 280 + ...options, 281 + }); 282 + 283 + /** 284 + * Update user resource. 285 + * This can only be done by the logged in user. 286 + */ 287 + export const updateUserRequest = <ThrowOnError extends boolean = false>( 288 + options: Options<UpdateUserData, ThrowOnError>, 289 + ): HttpRequest<unknown> => 290 + (options?.client ? options.client : _heyApiClient).requestOptions({ 291 + method: 'PUT', 292 + responseStyle: 'data', 293 + url: '/user/{username}', 294 + ...options, 295 + });
+80 -100
examples/openapi-ts-angular-resource/src/client/@hey-api/angular-resource.gen.ts examples/openapi-ts-angular-resource/src/client/@angular/common/http/httpResource.gen.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - import { Injectable, resource } from '@angular/core'; 3 + import { httpResource } from '@angular/common/http'; 4 + import { Injectable } from '@angular/core'; 4 5 5 - import type { Options } from '../sdk.gen'; 6 - import { 7 - addPet, 8 - createUser, 9 - createUsersWithListInput, 10 - deleteOrder, 11 - deletePet, 12 - deleteUser, 13 - findPetsByStatus, 14 - findPetsByTags, 15 - getInventory, 16 - getOrderById, 17 - getPetById, 18 - getUserByName, 19 - loginUser, 20 - logoutUser, 21 - placeOrder, 22 - updatePet, 23 - updatePetWithForm, 24 - updateUser, 25 - uploadFile, 26 - } from '../sdk.gen'; 6 + import type { Options } from '../../../sdk.gen'; 27 7 import type { 28 8 AddPetData, 9 + AddPetResponse, 29 10 CreateUserData, 11 + CreateUserResponse, 30 12 CreateUsersWithListInputData, 13 + CreateUsersWithListInputResponse, 31 14 DeleteOrderData, 32 15 DeletePetData, 33 16 DeleteUserData, 34 17 FindPetsByStatusData, 18 + FindPetsByStatusResponse, 35 19 FindPetsByTagsData, 20 + FindPetsByTagsResponse, 36 21 GetInventoryData, 22 + GetInventoryResponse, 37 23 GetOrderByIdData, 24 + GetOrderByIdResponse, 38 25 GetPetByIdData, 26 + GetPetByIdResponse, 39 27 GetUserByNameData, 28 + GetUserByNameResponse, 40 29 LoginUserData, 30 + LoginUserResponse, 41 31 LogoutUserData, 42 32 PlaceOrderData, 33 + PlaceOrderResponse, 43 34 UpdatePetData, 35 + UpdatePetResponse, 44 36 UpdatePetWithFormData, 37 + UpdatePetWithFormResponse, 45 38 UpdateUserData, 46 39 UploadFileData, 47 - } from '../types.gen'; 40 + UploadFileResponse, 41 + } from '../../../types.gen'; 42 + import { 43 + addPetRequest, 44 + createUserRequest, 45 + createUsersWithListInputRequest, 46 + deleteOrderRequest, 47 + deletePetRequest, 48 + deleteUserRequest, 49 + findPetsByStatusRequest, 50 + findPetsByTagsRequest, 51 + getInventoryRequest, 52 + getOrderByIdRequest, 53 + getPetByIdRequest, 54 + getUserByNameRequest, 55 + loginUserRequest, 56 + logoutUserRequest, 57 + placeOrderRequest, 58 + updatePetRequest, 59 + updatePetWithFormRequest, 60 + updateUserRequest, 61 + uploadFileRequest, 62 + } from './httpRequests.gen'; 48 63 49 64 @Injectable({ 50 65 providedIn: 'root', ··· 57 72 public addPet<ThrowOnError extends boolean = false>( 58 73 options: () => Options<AddPetData, ThrowOnError>, 59 74 ) { 60 - return resource({ 61 - loader: async ({ request }) => addPet(request), 62 - request: options, 63 - }); 75 + return httpResource<AddPetResponse>(() => addPetRequest(options())); 64 76 } 65 77 66 78 /** ··· 70 82 public updatePet<ThrowOnError extends boolean = false>( 71 83 options: () => Options<UpdatePetData, ThrowOnError>, 72 84 ) { 73 - return resource({ 74 - loader: async ({ request }) => updatePet(request), 75 - request: options, 76 - }); 85 + return httpResource<UpdatePetResponse>(() => updatePetRequest(options())); 77 86 } 78 87 79 88 /** ··· 83 92 public findPetsByStatus<ThrowOnError extends boolean = false>( 84 93 options: () => Options<FindPetsByStatusData, ThrowOnError>, 85 94 ) { 86 - return resource({ 87 - loader: async ({ request }) => findPetsByStatus(request), 88 - request: options, 89 - }); 95 + return httpResource<FindPetsByStatusResponse>(() => 96 + findPetsByStatusRequest(options()), 97 + ); 90 98 } 91 99 92 100 /** ··· 96 104 public findPetsByTags<ThrowOnError extends boolean = false>( 97 105 options: () => Options<FindPetsByTagsData, ThrowOnError>, 98 106 ) { 99 - return resource({ 100 - loader: async ({ request }) => findPetsByTags(request), 101 - request: options, 102 - }); 107 + return httpResource<FindPetsByTagsResponse>(() => 108 + findPetsByTagsRequest(options()), 109 + ); 103 110 } 104 111 105 112 /** ··· 109 116 public deletePet<ThrowOnError extends boolean = false>( 110 117 options: () => Options<DeletePetData, ThrowOnError>, 111 118 ) { 112 - return resource({ 113 - loader: async ({ request }) => deletePet(request), 114 - request: options, 115 - }); 119 + return httpResource<unknown>(() => deletePetRequest(options())); 116 120 } 117 121 118 122 /** ··· 122 126 public getPetById<ThrowOnError extends boolean = false>( 123 127 options: () => Options<GetPetByIdData, ThrowOnError>, 124 128 ) { 125 - return resource({ 126 - loader: async ({ request }) => getPetById(request), 127 - request: options, 128 - }); 129 + return httpResource<GetPetByIdResponse>(() => getPetByIdRequest(options())); 129 130 } 130 131 131 132 /** ··· 135 136 public updatePetWithForm<ThrowOnError extends boolean = false>( 136 137 options: () => Options<UpdatePetWithFormData, ThrowOnError>, 137 138 ) { 138 - return resource({ 139 - loader: async ({ request }) => updatePetWithForm(request), 140 - request: options, 141 - }); 139 + return httpResource<UpdatePetWithFormResponse>(() => 140 + updatePetWithFormRequest(options()), 141 + ); 142 142 } 143 143 144 144 /** ··· 148 148 public uploadFile<ThrowOnError extends boolean = false>( 149 149 options: () => Options<UploadFileData, ThrowOnError>, 150 150 ) { 151 - return resource({ 152 - loader: async ({ request }) => uploadFile(request), 153 - request: options, 154 - }); 151 + return httpResource<UploadFileResponse>(() => uploadFileRequest(options())); 155 152 } 156 153 } 157 154 ··· 166 163 public getInventory<ThrowOnError extends boolean = false>( 167 164 options?: () => Options<GetInventoryData, ThrowOnError>, 168 165 ) { 169 - return resource({ 170 - loader: async ({ request }) => getInventory(request), 171 - request: options, 172 - }); 166 + return httpResource<GetInventoryResponse>(() => 167 + getInventoryRequest(options ? options() : undefined), 168 + ); 173 169 } 174 170 175 171 /** ··· 179 175 public placeOrder<ThrowOnError extends boolean = false>( 180 176 options?: () => Options<PlaceOrderData, ThrowOnError>, 181 177 ) { 182 - return resource({ 183 - loader: async ({ request }) => placeOrder(request), 184 - request: options, 185 - }); 178 + return httpResource<PlaceOrderResponse>(() => 179 + placeOrderRequest(options ? options() : undefined), 180 + ); 186 181 } 187 182 188 183 /** ··· 192 187 public deleteOrder<ThrowOnError extends boolean = false>( 193 188 options: () => Options<DeleteOrderData, ThrowOnError>, 194 189 ) { 195 - return resource({ 196 - loader: async ({ request }) => deleteOrder(request), 197 - request: options, 198 - }); 190 + return httpResource<unknown>(() => deleteOrderRequest(options())); 199 191 } 200 192 201 193 /** ··· 205 197 public getOrderById<ThrowOnError extends boolean = false>( 206 198 options: () => Options<GetOrderByIdData, ThrowOnError>, 207 199 ) { 208 - return resource({ 209 - loader: async ({ request }) => getOrderById(request), 210 - request: options, 211 - }); 200 + return httpResource<GetOrderByIdResponse>(() => 201 + getOrderByIdRequest(options()), 202 + ); 212 203 } 213 204 } 214 205 ··· 223 214 public createUser<ThrowOnError extends boolean = false>( 224 215 options?: () => Options<CreateUserData, ThrowOnError>, 225 216 ) { 226 - return resource({ 227 - loader: async ({ request }) => createUser(request), 228 - request: options, 229 - }); 217 + return httpResource<CreateUserResponse>(() => 218 + createUserRequest(options ? options() : undefined), 219 + ); 230 220 } 231 221 232 222 /** ··· 236 226 public createUsersWithListInput<ThrowOnError extends boolean = false>( 237 227 options?: () => Options<CreateUsersWithListInputData, ThrowOnError>, 238 228 ) { 239 - return resource({ 240 - loader: async ({ request }) => createUsersWithListInput(request), 241 - request: options, 242 - }); 229 + return httpResource<CreateUsersWithListInputResponse>(() => 230 + createUsersWithListInputRequest(options ? options() : undefined), 231 + ); 243 232 } 244 233 245 234 /** ··· 249 238 public loginUser<ThrowOnError extends boolean = false>( 250 239 options?: () => Options<LoginUserData, ThrowOnError>, 251 240 ) { 252 - return resource({ 253 - loader: async ({ request }) => loginUser(request), 254 - request: options, 255 - }); 241 + return httpResource<LoginUserResponse>(() => 242 + loginUserRequest(options ? options() : undefined), 243 + ); 256 244 } 257 245 258 246 /** ··· 262 250 public logoutUser<ThrowOnError extends boolean = false>( 263 251 options?: () => Options<LogoutUserData, ThrowOnError>, 264 252 ) { 265 - return resource({ 266 - loader: async ({ request }) => logoutUser(request), 267 - request: options, 268 - }); 253 + return httpResource<unknown>(() => 254 + logoutUserRequest(options ? options() : undefined), 255 + ); 269 256 } 270 257 271 258 /** ··· 275 262 public deleteUser<ThrowOnError extends boolean = false>( 276 263 options: () => Options<DeleteUserData, ThrowOnError>, 277 264 ) { 278 - return resource({ 279 - loader: async ({ request }) => deleteUser(request), 280 - request: options, 281 - }); 265 + return httpResource<unknown>(() => deleteUserRequest(options())); 282 266 } 283 267 284 268 /** ··· 288 272 public getUserByName<ThrowOnError extends boolean = false>( 289 273 options: () => Options<GetUserByNameData, ThrowOnError>, 290 274 ) { 291 - return resource({ 292 - loader: async ({ request }) => getUserByName(request), 293 - request: options, 294 - }); 275 + return httpResource<GetUserByNameResponse>(() => 276 + getUserByNameRequest(options()), 277 + ); 295 278 } 296 279 297 280 /** ··· 301 284 public updateUser<ThrowOnError extends boolean = false>( 302 285 options: () => Options<UpdateUserData, ThrowOnError>, 303 286 ) { 304 - return resource({ 305 - loader: async ({ request }) => updateUser(request), 306 - request: options, 307 - }); 287 + return httpResource<unknown>(() => updateUserRequest(options())); 308 288 } 309 289 }
+54 -21
examples/openapi-ts-angular-resource/src/client/client/client.gen.ts
··· 16 16 import { firstValueFrom } from 'rxjs'; 17 17 import { filter } from 'rxjs/operators'; 18 18 19 - import type { Client, Config, ResolvedRequestOptions } from './types.gen'; 19 + import type { 20 + Client, 21 + Config, 22 + RequestOptions, 23 + ResolvedRequestOptions, 24 + ResponseStyle, 25 + } from './types.gen'; 20 26 import { 21 27 buildUrl, 22 28 createConfig, ··· 50 56 ResolvedRequestOptions 51 57 >(); 52 58 53 - const request: Client['request'] = async (options) => { 59 + const requestOptions = < 60 + ThrowOnError extends boolean = false, 61 + TResponseStyle extends ResponseStyle = 'fields', 62 + >( 63 + options: RequestOptions<TResponseStyle, ThrowOnError>, 64 + ) => { 54 65 const opts = { 55 66 ..._config, 56 67 ...options, 57 68 headers: mergeHeaders(_config.headers, options.headers), 58 69 httpClient: options.httpClient ?? _config.httpClient, 70 + method: 'GET', 59 71 serializedBody: options.body as any, 60 72 }; 61 73 ··· 65 77 inject(HttpClient), 66 78 ); 67 79 } else { 68 - assertInInjectionContext(request); 80 + assertInInjectionContext(requestOptions); 69 81 opts.httpClient = inject(HttpClient); 70 82 } 71 83 } 72 84 73 - if (opts.security) { 74 - await setAuthParams({ 75 - ...opts, 76 - security: opts.security, 77 - }); 78 - } 79 - 80 - if (opts.requestValidator) { 81 - await opts.requestValidator(opts); 82 - } 83 - 84 85 if (opts.body && opts.bodySerializer) { 85 86 opts.serializedBody = opts.bodySerializer(opts.body); 86 87 } ··· 90 91 opts.headers.delete('Content-Type'); 91 92 } 92 93 93 - const url = buildUrl(opts); 94 + const url = buildUrl(opts as any); 94 95 95 - let req = new HttpRequest<unknown>( 96 + const req = new HttpRequest<unknown>( 96 97 opts.method, 97 98 url, 98 99 opts.serializedBody || null, ··· 102 103 }, 103 104 ); 104 105 106 + return { opts, req }; 107 + }; 108 + 109 + const request: Client['request'] = async (options) => { 110 + const { opts, req: initialReq } = requestOptions(options); 111 + 112 + if (opts.security) { 113 + await setAuthParams({ 114 + ...opts, 115 + security: opts.security, 116 + }); 117 + } 118 + 119 + if (opts.requestValidator) { 120 + await opts.requestValidator(opts); 121 + } 122 + 123 + let req = initialReq; 124 + 105 125 for (const fn of interceptors.request._fns) { 106 126 if (fn) { 107 - req = await fn(req, opts); 127 + req = await fn(req, opts as any); 108 128 } 109 129 } 110 130 ··· 116 136 117 137 try { 118 138 response = await firstValueFrom( 119 - opts.httpClient 120 - .request(req) 139 + opts 140 + .httpClient!.request(req) 121 141 .pipe(filter((event) => event.type === HttpEventType.Response)), 122 142 ); 123 143 124 144 for (const fn of interceptors.response._fns) { 125 145 if (fn) { 126 - response = await fn(response, req, opts); 146 + response = await fn(response, req, opts as any); 127 147 } 128 148 } 129 149 ··· 153 173 finalError, 154 174 response as HttpResponse<unknown>, 155 175 req, 156 - opts, 176 + opts as any, 157 177 )) as string; 158 178 } 159 179 } ··· 184 204 post: (options) => request({ ...options, method: 'POST' }), 185 205 put: (options) => request({ ...options, method: 'PUT' }), 186 206 request, 207 + requestOptions: (options) => { 208 + if (options.security) { 209 + throw new Error('Security is not supported in requestOptions'); 210 + } 211 + 212 + if (options.requestValidator) { 213 + throw new Error( 214 + 'Request validation is not supported in requestOptions', 215 + ); 216 + } 217 + 218 + return requestOptions(options).req; 219 + }, 187 220 setConfig, 188 221 trace: (options) => request({ ...options, method: 'TRACE' }), 189 222 };
+9
examples/openapi-ts-angular-resource/src/client/client/types.gen.ts
··· 163 163 Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 164 164 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 165 165 166 + type RequestOptionsFn = < 167 + ThrowOnError extends boolean = false, 168 + TResponseStyle extends ResponseStyle = 'fields', 169 + >( 170 + options: RequestOptions<TResponseStyle, ThrowOnError>, 171 + ) => HttpRequest<unknown>; 172 + 166 173 type BuildUrlFn = < 167 174 TData extends { 168 175 body?: unknown; ··· 181 188 unknown, 182 189 ResolvedRequestOptions 183 190 >; 191 + 192 + requestOptions: RequestOptionsFn; 184 193 }; 185 194 186 195 /**
+9 -10
examples/openapi-ts-angular-resource/src/client/client/utils.gen.ts
··· 194 194 return; 195 195 }; 196 196 197 - export const setAuthParams = async ({ 198 - security, 199 - ...options 200 - }: Pick<Required<RequestOptions>, 'security'> & 201 - Pick<RequestOptions, 'auth' | 'query'> & { 202 - headers: HttpHeaders; 203 - }) => { 204 - for (const auth of security) { 197 + export const setAuthParams = async ( 198 + options: Pick<Required<RequestOptions>, 'security'> & 199 + Pick<RequestOptions, 'auth' | 'query'> & { 200 + headers: HttpHeaders; 201 + }, 202 + ) => { 203 + for (const auth of options.security) { 205 204 const token = await getAuthToken(auth, options.auth); 206 205 207 206 if (!token) { ··· 218 217 options.query[name] = token; 219 218 break; 220 219 case 'cookie': 221 - options.headers.append('Cookie', `${name}=${token}`); 220 + options.headers = options.headers.append('Cookie', `${name}=${token}`); 222 221 break; 223 222 case 'header': 224 223 default: 225 - options.headers.set(name, token); 224 + options.headers = options.headers.set(name, token); 226 225 break; 227 226 } 228 227
+2
examples/openapi-ts-angular-resource/src/client/index.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 + export * from './@angular/common/http/httpRequests.gen'; 3 + export * from './@angular/common/http/httpResource.gen'; 2 4 export * from './sdk.gen'; 3 5 export * from './types.gen';
+1 -1
packages/openapi-ts/src/ir/context.ts
··· 142 142 dependencies: plugin.dependencies ?? [], 143 143 handler: plugin.handler, 144 144 name: plugin.name, 145 - output: plugin.output!, 145 + output: plugin.output! as string, 146 146 }); 147 147 this.plugins[instance.name] = instance; 148 148 return instance;
+398
packages/openapi-ts/src/plugins/@angular/common/companions/angularHttpRequestsCompanionPluginHandler.ts
··· 1 + import { tsc } from '../../../../tsc'; 2 + import { stringCase } from '../../../../utils/stringCase'; 3 + import { clientId } from '../../../@hey-api/client-core/utils'; 4 + import { sdkId } from '../../../@hey-api/sdk/constants'; 5 + import { operationClasses } from '../../../@hey-api/sdk/operation'; 6 + import { typesId } from '../../../@hey-api/typescript/ref'; 7 + import { 8 + createOperationComment, 9 + isOperationOptionsRequired, 10 + } from '../../../shared/utils/operation'; 11 + import type { HeyApiAngularCommonPlugin } from '../types'; 12 + 13 + export const REQUEST_APIS_SUFFIX = '/http/httpRequests'; 14 + 15 + export const angularHttpRequestsCompanionPluginHandler: HeyApiAngularCommonPlugin['Handler'] = 16 + ({ plugin }) => { 17 + const sdkPlugin = plugin.getPlugin('@hey-api/sdk'); 18 + 19 + const file = plugin.createFile({ 20 + exportFromIndex: true, 21 + id: plugin.name + REQUEST_APIS_SUFFIX, 22 + path: plugin.output + REQUEST_APIS_SUFFIX, 23 + }); 24 + 25 + if (plugin.config.httpRequest?.asClass) { 26 + file.import({ 27 + module: '@angular/core', 28 + name: 'Injectable', 29 + }); 30 + } 31 + 32 + file.import({ 33 + module: '@angular/common/http', 34 + name: 'HttpRequest', 35 + }); 36 + 37 + file.import({ 38 + module: file.relativePathToFile({ 39 + context: plugin.context, 40 + id: sdkId, 41 + }), 42 + name: 'Options', 43 + }); 44 + 45 + if (plugin.config.httpRequest?.asClass) { 46 + generateAngularClassRequests({ file, plugin, sdkPlugin }); 47 + } else { 48 + generateAngularFunctionRequests({ file, plugin }); 49 + } 50 + }; 51 + 52 + interface AngularRequestClassEntry { 53 + className: string; 54 + classes: Set<string>; 55 + methods: Set<string>; 56 + nodes: Array<any>; 57 + root: boolean; 58 + } 59 + 60 + const generateAngularClassRequests = ({ 61 + file, 62 + plugin, 63 + sdkPlugin, 64 + }: { 65 + file: any; 66 + plugin: HeyApiAngularCommonPlugin['Instance']; 67 + sdkPlugin: any; 68 + }) => { 69 + const requestClasses = new Map<string, AngularRequestClassEntry>(); 70 + const generatedClasses = new Set<string>(); 71 + 72 + // Iterate through operations to build class structure 73 + plugin.forEach('operation', ({ operation }) => { 74 + const isRequiredOptions = isOperationOptionsRequired({ 75 + context: plugin.context, 76 + operation, 77 + }); 78 + 79 + const classes = operationClasses({ 80 + context: plugin.context, 81 + operation, 82 + plugin: sdkPlugin, 83 + }); 84 + 85 + for (const entry of classes.values()) { 86 + entry.path.forEach((currentClassName, index) => { 87 + if (!requestClasses.has(currentClassName)) { 88 + requestClasses.set(currentClassName, { 89 + className: currentClassName, 90 + classes: new Set(), 91 + methods: new Set(), 92 + nodes: [], 93 + root: !index, 94 + }); 95 + } 96 + 97 + const parentClassName = entry.path[index - 1]; 98 + if (parentClassName && parentClassName !== currentClassName) { 99 + const parentClass = requestClasses.get(parentClassName)!; 100 + parentClass.classes.add(currentClassName); 101 + requestClasses.set(parentClassName, parentClass); 102 + } 103 + 104 + const isLast = entry.path.length === index + 1; 105 + if (!isLast) { 106 + return; 107 + } 108 + 109 + const currentClass = requestClasses.get(currentClassName)!; 110 + 111 + // Generate the request method name with "Request" suffix 112 + const requestMethodName = 113 + plugin.config.httpRequest!.methodNameBuilder!(operation); 114 + 115 + // Avoid duplicate methods 116 + if (currentClass.methods.has(requestMethodName)) { 117 + return; 118 + } 119 + 120 + // Generate Angular request method 121 + const methodNode = generateAngularRequestMethod({ 122 + file, 123 + isRequiredOptions, 124 + methodName: requestMethodName, 125 + operation, 126 + plugin, 127 + }); 128 + 129 + if (!currentClass.nodes.length) { 130 + currentClass.nodes.push(methodNode); 131 + } else { 132 + currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); 133 + } 134 + 135 + currentClass.methods.add(requestMethodName); 136 + requestClasses.set(currentClassName, currentClass); 137 + }); 138 + } 139 + }); 140 + 141 + // Generate classes 142 + const generateClass = (currentClass: AngularRequestClassEntry) => { 143 + if (generatedClasses.has(currentClass.className)) { 144 + return; 145 + } 146 + 147 + // Handle child classes 148 + if (currentClass.classes.size) { 149 + for (const childClassName of currentClass.classes) { 150 + const childClass = requestClasses.get(childClassName)!; 151 + generateClass(childClass); 152 + 153 + currentClass.nodes.push( 154 + tsc.propertyDeclaration({ 155 + initializer: tsc.newExpression({ 156 + argumentsArray: [], 157 + expression: tsc.identifier({ 158 + text: plugin.config.httpRequest!.classNameBuilder!( 159 + childClass.className, 160 + ), 161 + }), 162 + }), 163 + name: stringCase({ 164 + case: 'camelCase', 165 + value: childClass.className, 166 + }), 167 + }), 168 + ); 169 + } 170 + } 171 + 172 + const node = tsc.classDeclaration({ 173 + decorator: currentClass.root 174 + ? { 175 + args: [ 176 + { 177 + providedIn: 'root', 178 + }, 179 + ], 180 + name: 'Injectable', 181 + } 182 + : undefined, 183 + exportClass: currentClass.root, 184 + name: plugin.config.httpRequest!.classNameBuilder!( 185 + currentClass.className, 186 + ), 187 + nodes: currentClass.nodes, 188 + }); 189 + 190 + file.add(node); 191 + generatedClasses.add(currentClass.className); 192 + }; 193 + 194 + for (const requestClass of requestClasses.values()) { 195 + generateClass(requestClass); 196 + } 197 + }; 198 + 199 + const generateAngularFunctionRequests = ({ 200 + file, 201 + plugin, 202 + }: { 203 + file: any; 204 + plugin: HeyApiAngularCommonPlugin['Instance']; 205 + }) => { 206 + plugin.forEach('operation', ({ operation }) => { 207 + const isRequiredOptions = isOperationOptionsRequired({ 208 + context: plugin.context, 209 + operation, 210 + }); 211 + 212 + // Generate function name with "Request" suffix 213 + const functionName = 214 + plugin.config.httpRequest!.methodNameBuilder!(operation); 215 + 216 + const node = generateAngularRequestFunction({ 217 + file, 218 + functionName, 219 + isRequiredOptions, 220 + operation, 221 + plugin, 222 + }); 223 + 224 + file.add(node); 225 + }); 226 + }; 227 + 228 + const generateRequestCallExpression = ({ 229 + file, 230 + operation, 231 + plugin, 232 + }: { 233 + file: any; 234 + operation: any; 235 + plugin: any; 236 + }) => { 237 + // Import the client and use requestOptions instead of HTTP methods 238 + const clientImport = file.import({ 239 + alias: '_heyApiClient', 240 + module: file.relativePathToFile({ 241 + context: plugin.context, 242 + id: clientId, 243 + }), 244 + name: 'client', 245 + }); 246 + 247 + return tsc.callExpression({ 248 + functionName: tsc.propertyAccessExpression({ 249 + expression: tsc.conditionalExpression({ 250 + condition: tsc.propertyAccessExpression({ 251 + expression: tsc.identifier({ text: 'options' }), 252 + isOptional: true, 253 + name: 'client', 254 + }), 255 + whenFalse: tsc.identifier({ text: clientImport.name }), 256 + whenTrue: tsc.propertyAccessExpression({ 257 + expression: tsc.identifier({ text: 'options' }), 258 + name: 'client', 259 + }), 260 + }), 261 + name: 'requestOptions', 262 + }), 263 + parameters: [ 264 + tsc.objectExpression({ 265 + obj: [ 266 + { 267 + key: 'responseStyle', 268 + value: tsc.identifier({ text: "'data'" }), 269 + }, 270 + { 271 + key: 'method', 272 + value: tsc.identifier({ 273 + text: `'${operation.method.toUpperCase()}'`, 274 + }), 275 + }, 276 + { 277 + key: 'url', 278 + value: tsc.identifier({ text: `'${operation.path}'` }), 279 + }, 280 + { 281 + spread: 'options', 282 + }, 283 + ], 284 + }), 285 + ], 286 + }); 287 + }; 288 + 289 + const generateAngularRequestMethod = ({ 290 + file, 291 + isRequiredOptions, 292 + methodName, 293 + operation, 294 + plugin, 295 + }: { 296 + file: any; 297 + isRequiredOptions: boolean; 298 + methodName: string; 299 + operation: any; 300 + plugin: any; 301 + }) => { 302 + // Import operation data type 303 + const pluginTypeScript = plugin.getPlugin('@hey-api/typescript')!; 304 + const fileTypeScript = plugin.context.file({ id: typesId })!; 305 + const dataType = file.import({ 306 + asType: true, 307 + module: file.relativePathToFile({ context: plugin.context, id: typesId }), 308 + name: fileTypeScript.getName( 309 + pluginTypeScript.api.getId({ operation, type: 'data' }), 310 + ), 311 + }); 312 + 313 + return tsc.methodDeclaration({ 314 + accessLevel: 'public', 315 + comment: createOperationComment({ operation }), 316 + name: methodName, 317 + parameters: [ 318 + { 319 + isRequired: isRequiredOptions, 320 + name: 'options', 321 + type: `Options<${dataType.name || 'unknown'}, ThrowOnError>`, 322 + }, 323 + ], 324 + returnType: 'HttpRequest<unknown>', 325 + statements: [ 326 + tsc.returnStatement({ 327 + expression: generateRequestCallExpression({ 328 + file, 329 + operation, 330 + plugin, 331 + }), 332 + }), 333 + ], 334 + types: [ 335 + { 336 + default: false, 337 + extends: 'boolean', 338 + name: 'ThrowOnError', 339 + }, 340 + ], 341 + }); 342 + }; 343 + 344 + const generateAngularRequestFunction = ({ 345 + file, 346 + functionName, 347 + isRequiredOptions, 348 + operation, 349 + plugin, 350 + }: { 351 + file: any; 352 + functionName: string; 353 + isRequiredOptions: boolean; 354 + operation: any; 355 + plugin: any; 356 + }) => { 357 + const pluginTypeScript = plugin.getPlugin('@hey-api/typescript')!; 358 + const fileTypeScript = plugin.context.file({ id: typesId })!; 359 + const dataType = file.import({ 360 + asType: true, 361 + module: file.relativePathToFile({ context: plugin.context, id: typesId }), 362 + name: fileTypeScript.getName( 363 + pluginTypeScript.api.getId({ operation, type: 'data' }), 364 + ), 365 + }); 366 + 367 + return tsc.constVariable({ 368 + comment: createOperationComment({ operation }), 369 + exportConst: true, 370 + expression: tsc.arrowFunction({ 371 + parameters: [ 372 + { 373 + isRequired: isRequiredOptions, 374 + name: 'options', 375 + type: `Options<${dataType.name || 'unknown'}, ThrowOnError>`, 376 + }, 377 + ], 378 + returnType: 'HttpRequest<unknown>', 379 + statements: [ 380 + tsc.returnStatement({ 381 + expression: generateRequestCallExpression({ 382 + file, 383 + operation, 384 + plugin, 385 + }), 386 + }), 387 + ], 388 + types: [ 389 + { 390 + default: false, 391 + extends: 'boolean', 392 + name: 'ThrowOnError', 393 + }, 394 + ], 395 + }), 396 + name: functionName, 397 + }); 398 + };
+48
packages/openapi-ts/src/plugins/@angular/common/config.ts
··· 1 + import { definePluginConfig } from '../../shared/utils/config'; 2 + import { handler } from './plugin'; 3 + import type { HeyApiAngularCommonPlugin } from './types'; 4 + 5 + export const defaultConfig: HeyApiAngularCommonPlugin['Config'] = { 6 + config: {}, 7 + dependencies: ['@hey-api/client-angular', '@hey-api/sdk'], 8 + handler, 9 + name: '@angular/common', 10 + output: '@angular/common', 11 + resolveConfig(plugin) { 12 + plugin.config.httpResource = { 13 + asClass: false, 14 + classNameBuilder(className) { 15 + return className + 'Resources'; 16 + }, 17 + enabled: false, 18 + methodNameBuilder(operation) { 19 + if (plugin.config.httpResource?.asClass) { 20 + return String(operation.id); 21 + } 22 + 23 + return String(operation.id) + 'Resource'; 24 + }, 25 + ...plugin.config.httpResource, 26 + }; 27 + 28 + plugin.config.httpRequest = { 29 + asClass: false, 30 + classNameBuilder(className) { 31 + return className + 'Requests'; 32 + }, 33 + methodNameBuilder(operation) { 34 + if (plugin.config.httpRequest?.asClass) { 35 + return String(operation.id); 36 + } 37 + 38 + return String(operation.id) + 'Request'; 39 + }, 40 + ...plugin.config.httpRequest, 41 + }; 42 + }, 43 + }; 44 + 45 + /** 46 + * Type helper for `@angular/common` plugin, returns {@link Plugin.Config} object 47 + */ 48 + export const defineConfig = definePluginConfig(defaultConfig);
+2
packages/openapi-ts/src/plugins/@angular/common/index.ts
··· 1 + export { defaultConfig, defineConfig } from './config'; 2 + export type { HeyApiAngularCommonPlugin as HeyApiAngularResourcePlugin } from './types';
+11
packages/openapi-ts/src/plugins/@angular/common/plugin.ts
··· 1 + import { angularHttpRequestsCompanionPluginHandler } from './companions/angularHttpRequestsCompanionPluginHandler'; 2 + import { angularHttpResourceCompanionPluginHandler } from './companions/angularHttpResourceCompanionPluginHandler'; 3 + import type { HeyApiAngularCommonPlugin } from './types'; 4 + 5 + export const handler: HeyApiAngularCommonPlugin['Handler'] = (args) => { 6 + angularHttpRequestsCompanionPluginHandler(args); 7 + 8 + if (args.plugin.config.httpResource?.enabled) { 9 + angularHttpResourceCompanionPluginHandler(args); 10 + } 11 + };
+61
packages/openapi-ts/src/plugins/@angular/common/types.d.ts
··· 1 + import type { Operation } from '../../../types/client'; 2 + import type { DefinePlugin, Plugin } from '../../types'; 3 + 4 + export type AngularHttpResourceOptions = { 5 + /** 6 + * Whether to generate the resource as a class. 7 + * @default false 8 + */ 9 + asClass?: boolean; 10 + 11 + /** 12 + * Builds the class name for the generated resource. 13 + * By default, the class name is suffixed with "Resources". 14 + */ 15 + classNameBuilder?: (className: string) => string; 16 + 17 + /** 18 + * Wether or not to create http resource APIs. 19 + */ 20 + enabled: boolean; 21 + 22 + /** 23 + * Builds the method name for the generated resource. 24 + * By default, the operation id is used, if `asClass` is false, the method is also suffixed with "Resource". 25 + */ 26 + methodNameBuilder?: (operation: IR.OperationObject | Operation) => string; 27 + }; 28 + 29 + export type AngularHttpRequestOptions = { 30 + /** 31 + * Whether to generate the resource as a class. 32 + * @default false 33 + */ 34 + asClass?: boolean; 35 + 36 + /** 37 + * Builds the class name for the generated resource. 38 + * By default, the class name is suffixed with "Resources". 39 + */ 40 + classNameBuilder?: (className: string) => string; 41 + 42 + /** 43 + * Builds the method name for the generated resource. 44 + * By default, the operation id is used, if `asClass` is false, the method is also suffixed with "Resource". 45 + */ 46 + methodNameBuilder?: (operation: IR.OperationObject | Operation) => string; 47 + }; 48 + 49 + export type UserConfig = Plugin.Name<'@angular/common'> & { 50 + /** 51 + * Options for generating HTTP Request instances. 52 + */ 53 + httpRequest?: AngularHttpRequestOptions; 54 + 55 + /** 56 + * Options for generating HTTP resource APIs. 57 + */ 58 + httpResource?: AngularHttpResourceOptions; 59 + }; 60 + 61 + export type HeyApiAngularCommonPlugin = DefinePlugin<UserConfig, UserConfig>;
-32
packages/openapi-ts/src/plugins/@hey-api/angular-resource/config.ts
··· 1 - import { definePluginConfig } from '../../shared/utils/config'; 2 - import { angularResourcePluginHandler } from './plugin'; 3 - import type { HeyApiAngularResourcePlugin } from './types'; 4 - 5 - export const defaultConfig: HeyApiAngularResourcePlugin['Config'] = { 6 - config: { 7 - asClass: false, 8 - classNameBuilder(className) { 9 - return className + 'Resources'; 10 - }, 11 - methodNameBuilder(operation) { 12 - if (this.asClass) { 13 - return String(operation.id); 14 - } 15 - 16 - return String(operation.id) + 'Resource'; 17 - }, 18 - }, 19 - dependencies: [ 20 - '@hey-api/client-angular', 21 - '@hey-api/sdk', 22 - '@hey-api/typescript', 23 - ], 24 - handler: angularResourcePluginHandler, 25 - name: '@hey-api/angular-resource', 26 - output: '@hey-api/angular-resource', 27 - }; 28 - 29 - /** 30 - * Type helper for `@hey-api/angular-resource` plugin, returns {@link Plugin.Config} object 31 - */ 32 - export const defineConfig = definePluginConfig(defaultConfig);
-2
packages/openapi-ts/src/plugins/@hey-api/angular-resource/index.ts
··· 1 - export { defaultConfig, defineConfig } from './config'; 2 - export type { HeyApiAngularResourcePlugin } from './types';
+113 -92
packages/openapi-ts/src/plugins/@hey-api/angular-resource/plugin.ts packages/openapi-ts/src/plugins/@angular/common/companions/angularHttpResourceCompanionPluginHandler.ts
··· 1 - import { tsc } from '../../../tsc'; 2 - import { stringCase } from '../../../utils/stringCase'; 1 + import { tsc } from '../../../../tsc'; 2 + import { stringCase } from '../../../../utils/stringCase'; 3 + import { sdkId } from '../../../@hey-api/sdk/constants'; 4 + import { operationClasses } from '../../../@hey-api/sdk/operation'; 5 + import { typesId } from '../../../@hey-api/typescript/ref'; 3 6 import { 4 7 createOperationComment, 5 8 isOperationOptionsRequired, 6 - } from '../../shared/utils/operation'; 7 - import { sdkId } from '../sdk/constants'; 8 - import { operationClasses } from '../sdk/operation'; 9 - import { serviceFunctionIdentifier } from '../sdk/plugin-legacy'; 10 - import { typesId } from '../typescript/ref'; 11 - import type { HeyApiAngularResourcePlugin } from './types'; 9 + } from '../../../shared/utils/operation'; 10 + import type { HeyApiAngularCommonPlugin } from '../types'; 11 + import { REQUEST_APIS_SUFFIX } from './angularHttpRequestsCompanionPluginHandler'; 12 12 13 - export const angularResourcePluginHandler: HeyApiAngularResourcePlugin['Handler'] = 13 + export const RESOURCE_APIS_SUFFIX = '/http/httpResource'; 14 + 15 + export const angularHttpResourceCompanionPluginHandler: HeyApiAngularCommonPlugin['Handler'] = 14 16 ({ plugin }) => { 15 - // Check if SDK plugin exists 16 17 const sdkPlugin = plugin.getPlugin('@hey-api/sdk'); 17 18 18 - if (!sdkPlugin) { 19 - throw new Error( 20 - '@hey-api/sdk plugin is required for @hey-api/angular-resource plugin', 21 - ); 22 - } 23 - 24 - // Create the httpResource file 25 19 const file = plugin.createFile({ 26 - id: plugin.name, 27 - path: plugin.output, 20 + exportFromIndex: true, 21 + id: plugin.name + RESOURCE_APIS_SUFFIX, 22 + path: plugin.output + RESOURCE_APIS_SUFFIX, 28 23 }); 29 24 30 - // Import Angular core decorators 31 - if (plugin.config.asClass) { 25 + if (plugin.config.httpResource?.asClass) { 32 26 file.import({ 33 27 module: '@angular/core', 34 28 name: 'Injectable', 35 29 }); 36 30 } 37 31 32 + if (plugin.config.httpRequest?.asClass) { 33 + file.import({ 34 + module: '@angular/core', 35 + name: 'inject', 36 + }); 37 + } 38 + 38 39 file.import({ 39 - module: '@angular/core', 40 - name: 'resource', 40 + module: '@angular/common/http', 41 + name: 'httpResource', 41 42 }); 42 43 43 44 file.import({ ··· 48 49 name: 'Options', 49 50 }); 50 51 51 - if (plugin.config.asClass) { 52 + if (plugin.config.httpResource!.asClass) { 52 53 generateAngularClassServices({ file, plugin, sdkPlugin }); 53 54 } else { 54 55 generateAngularFunctionServices({ file, plugin, sdkPlugin }); ··· 69 70 sdkPlugin, 70 71 }: { 71 72 file: any; 72 - plugin: HeyApiAngularResourcePlugin['Instance']; 73 + plugin: HeyApiAngularCommonPlugin['Instance']; 73 74 sdkPlugin: any; 74 75 }) => { 75 76 const serviceClasses = new Map<string, AngularServiceClassEntry>(); ··· 115 116 const currentClass = serviceClasses.get(currentClassName)!; 116 117 117 118 // Generate the resource method name 118 - const resourceMethodName = plugin.config.methodNameBuilder!.call( 119 - plugin.config, 120 - operation, 121 - ); 119 + const resourceMethodName = 120 + plugin.config.httpResource!.methodNameBuilder!(operation); 122 121 123 122 // Avoid duplicate methods 124 123 if (currentClass.methods.has(resourceMethodName)) { ··· 164 163 initializer: tsc.newExpression({ 165 164 argumentsArray: [], 166 165 expression: tsc.identifier({ 167 - text: plugin.config.classNameBuilder!(childClass.className), 166 + text: plugin.config.httpResource!.classNameBuilder!( 167 + childClass.className, 168 + ), 168 169 }), 169 170 }), 170 171 name: stringCase({ ··· 188 189 } 189 190 : undefined, 190 191 exportClass: currentClass.root, 191 - name: plugin.config.classNameBuilder!(currentClass.className), 192 + name: plugin.config.httpResource!.classNameBuilder!( 193 + currentClass.className, 194 + ), 192 195 nodes: currentClass.nodes, 193 196 }); 194 197 ··· 207 210 sdkPlugin, 208 211 }: { 209 212 file: any; 210 - plugin: HeyApiAngularResourcePlugin['Instance']; 213 + plugin: HeyApiAngularCommonPlugin['Instance']; 211 214 sdkPlugin: any; 212 215 }) => { 213 216 plugin.forEach('operation', ({ operation }) => { ··· 218 221 219 222 const node = generateAngularResourceFunction({ 220 223 file, 221 - functionName: plugin.config.methodNameBuilder!.call( 222 - plugin.config, 223 - operation, 224 - ), 224 + functionName: plugin.config.httpResource!.methodNameBuilder!(operation), 225 225 isRequiredOptions, 226 226 operation, 227 227 plugin, ··· 234 234 235 235 const generateResourceCallExpression = ({ 236 236 file, 237 + isRequiredOptions, 237 238 operation, 238 239 plugin, 240 + responseTypeName, 239 241 sdkPlugin, 240 242 }: { 241 243 file: any; 244 + isRequiredOptions: boolean; 242 245 operation: any; 243 246 plugin: any; 247 + responseTypeName: string; 244 248 sdkPlugin: any; 245 249 }) => { 246 - // Import the SDK function/method instead of recreating the logic 247 - let sdkFunctionCall; 250 + // Check if httpRequest is configured to use classes 251 + const useRequestClasses = plugin.config.httpRequest?.asClass; 252 + let requestFunctionCall; 253 + 254 + // Create the options call expression based on whether options are required 255 + const optionsCallExpression = isRequiredOptions 256 + ? tsc.callExpression({ 257 + functionName: 'options', 258 + parameters: [], 259 + }) 260 + : tsc.conditionalExpression({ 261 + condition: tsc.identifier({ text: 'options' }), 262 + whenFalse: tsc.identifier({ text: 'undefined' }), 263 + whenTrue: tsc.callExpression({ 264 + functionName: 'options', 265 + parameters: [], 266 + }), 267 + }); 248 268 249 - if (sdkPlugin.config.asClass) { 250 - // For class-based SDK, use the class methods 269 + if (useRequestClasses) { 270 + // For class-based request methods, use inject and class hierarchy 251 271 const classes = operationClasses({ 252 272 context: plugin.context, 253 273 operation, 254 274 plugin: sdkPlugin, 255 275 }); 256 276 257 - // Get the first class entry to determine the method path 258 277 const firstEntry = Array.from(classes.values())[0]; 259 278 if (firstEntry) { 260 - // Import the root class from SDK 279 + // Import the root class from HTTP requests 261 280 const rootClassName = firstEntry.path[0]; 262 - const sdkImport = file.import({ 263 - module: file.relativePathToFile({ 264 - context: plugin.context, 265 - id: sdkId, 266 - }), 267 - name: rootClassName, 268 - }); 281 + const requestClassName = 282 + plugin.config.httpRequest!.classNameBuilder!(rootClassName); 269 283 270 - // Build the method access path 271 - let methodAccess: any = tsc.identifier({ text: sdkImport.name }); 284 + // Build the method access path using inject 285 + let methodAccess: any = tsc.callExpression({ 286 + functionName: 'inject', 287 + parameters: [tsc.identifier({ text: requestClassName })], 288 + }); 272 289 273 290 // Navigate through the class hierarchy 274 291 for (let i = 1; i < firstEntry.path.length; i++) { ··· 284 301 } 285 302 } 286 303 287 - // Add the final method name 304 + // Add the final method name with "Request" suffix 305 + const requestMethodName = 306 + plugin.config.httpRequest!.methodNameBuilder!(operation); 288 307 methodAccess = tsc.propertyAccessExpression({ 289 308 expression: methodAccess, 290 - name: firstEntry.methodName, 309 + name: requestMethodName, 291 310 }); 292 311 293 - sdkFunctionCall = tsc.callExpression({ 312 + requestFunctionCall = tsc.callExpression({ 294 313 functionName: methodAccess, 295 - parameters: [tsc.identifier({ text: 'request' })], 314 + parameters: [optionsCallExpression], 296 315 }); 297 316 } 298 317 } else { 299 - // For function-based SDK, import and call the function directly 300 - const functionName = serviceFunctionIdentifier({ 301 - config: plugin.context.config, 302 - handleIllegal: true, 303 - id: operation.id, 304 - operation, 305 - }); 318 + // For function-based request methods, import and call the function directly 319 + const requestFunctionName = 320 + plugin.config.httpRequest!.methodNameBuilder!(operation); 306 321 307 - const sdkImport = file.import({ 322 + const requestImport = file.import({ 308 323 module: file.relativePathToFile({ 309 324 context: plugin.context, 310 - id: sdkId, 325 + id: plugin.name + REQUEST_APIS_SUFFIX, 311 326 }), 312 - name: functionName, 327 + name: requestFunctionName, 313 328 }); 314 329 315 - sdkFunctionCall = tsc.callExpression({ 316 - functionName: sdkImport.name, 317 - parameters: [tsc.identifier({ text: 'request' })], 330 + requestFunctionCall = tsc.callExpression({ 331 + functionName: requestImport.name, 332 + parameters: [optionsCallExpression], 318 333 }); 319 334 } 320 335 321 336 return tsc.callExpression({ 322 - functionName: 'resource', 337 + functionName: 'httpResource', 323 338 parameters: [ 324 - tsc.objectExpression({ 325 - obj: [ 326 - { 327 - key: 'loader', 328 - value: tsc.arrowFunction({ 329 - async: true, 330 - parameters: [ 331 - { 332 - destructure: [{ name: 'request' }], 333 - type: undefined, 334 - }, 335 - ], 336 - statements: [ 337 - tsc.returnStatement({ 338 - expression: sdkFunctionCall, 339 - }), 340 - ], 341 - }), 342 - }, 343 - { 344 - key: 'request', 345 - value: tsc.identifier({ text: 'options' }), 346 - }, 339 + tsc.arrowFunction({ 340 + parameters: [], 341 + statements: [ 342 + tsc.returnStatement({ 343 + expression: requestFunctionCall, 344 + }), 347 345 ], 348 346 }), 349 347 ], 348 + types: [tsc.typeNode(responseTypeName)], 350 349 }); 351 350 }; 352 351 ··· 376 375 ), 377 376 }); 378 377 378 + // Import operation response type 379 + const responseType = file.import({ 380 + asType: true, 381 + module: file.relativePathToFile({ context: plugin.context, id: typesId }), 382 + name: fileTypeScript.getName( 383 + pluginTypeScript.api.getId({ operation, type: 'response' }), 384 + ), 385 + }); 386 + 379 387 return tsc.methodDeclaration({ 380 388 accessLevel: 'public', 381 389 comment: createOperationComment({ operation }), ··· 393 401 tsc.returnStatement({ 394 402 expression: generateResourceCallExpression({ 395 403 file, 404 + isRequiredOptions, 396 405 operation, 397 406 plugin, 407 + responseTypeName: responseType.name || 'unknown', 398 408 sdkPlugin, 399 409 }), 400 410 }), ··· 434 444 ), 435 445 }); 436 446 447 + // Import operation response type 448 + const responseType = file.import({ 449 + asType: true, 450 + module: file.relativePathToFile({ context: plugin.context, id: typesId }), 451 + name: fileTypeScript.getName( 452 + pluginTypeScript.api.getId({ operation, type: 'response' }), 453 + ), 454 + }); 455 + 437 456 return tsc.constVariable({ 438 457 comment: createOperationComment({ operation }), 439 458 exportConst: true, ··· 449 468 tsc.returnStatement({ 450 469 expression: generateResourceCallExpression({ 451 470 file, 471 + isRequiredOptions, 452 472 operation, 453 473 plugin, 474 + responseTypeName: responseType.name || 'unknown', 454 475 sdkPlugin, 455 476 }), 456 477 }),
-49
packages/openapi-ts/src/plugins/@hey-api/angular-resource/types.d.ts
··· 1 - import type { Operation } from '../../../types/client'; 2 - import type { DefinePlugin, Plugin } from '../../types'; 3 - 4 - export type UserConfig = Plugin.Name<'@hey-api/angular-resource'> & { 5 - /** 6 - * Whether to generate the resource as a class. 7 - * @default false 8 - */ 9 - asClass?: boolean; 10 - 11 - /** 12 - * Builds the class name for the generated resource. 13 - * By default, the class name is suffixed with "Resources". 14 - */ 15 - classNameBuilder?: (className: string) => string; 16 - 17 - /** 18 - * Builds the method name for the generated resource. 19 - * By default, the operation id is used, if `asClass` is false, the method is also suffixed with "Resource". 20 - */ 21 - methodNameBuilder?: (operation: IR.OperationObject | Operation) => string; 22 - 23 - /** 24 - * Name of the generated file. 25 - * 26 - * @default 'httpResource' 27 - */ 28 - output?: string; 29 - }; 30 - 31 - export type Config = Plugin.Name<'@hey-api/angular-resource'> & { 32 - /** 33 - * Whether to generate the resource as a class. 34 - * @default false 35 - */ 36 - asClass: boolean; 37 - 38 - classNameBuilder?: (className: string) => string; 39 - methodNameBuilder?: (operation: IR.OperationObject | Operation) => string; 40 - 41 - /** 42 - * Name of the generated file. 43 - * 44 - * @default 'httpResource' 45 - */ 46 - output?: string; 47 - }; 48 - 49 - export type HeyApiAngularResourcePlugin = DefinePlugin<UserConfig, Config>;
+54 -21
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/client.ts
··· 14 14 import { firstValueFrom } from 'rxjs'; 15 15 import { filter } from 'rxjs/operators'; 16 16 17 - import type { Client, Config, ResolvedRequestOptions } from './types'; 17 + import type { 18 + Client, 19 + Config, 20 + RequestOptions, 21 + ResolvedRequestOptions, 22 + ResponseStyle, 23 + } from './types'; 18 24 import { 19 25 buildUrl, 20 26 createConfig, ··· 48 54 ResolvedRequestOptions 49 55 >(); 50 56 51 - const request: Client['request'] = async (options) => { 57 + const requestOptions = < 58 + ThrowOnError extends boolean = false, 59 + TResponseStyle extends ResponseStyle = 'fields', 60 + >( 61 + options: RequestOptions<TResponseStyle, ThrowOnError>, 62 + ) => { 52 63 const opts = { 53 64 ..._config, 54 65 ...options, 55 66 headers: mergeHeaders(_config.headers, options.headers), 56 67 httpClient: options.httpClient ?? _config.httpClient, 68 + method: 'GET', 57 69 serializedBody: options.body as any, 58 70 }; 59 71 ··· 63 75 inject(HttpClient), 64 76 ); 65 77 } else { 66 - assertInInjectionContext(request); 78 + assertInInjectionContext(requestOptions); 67 79 opts.httpClient = inject(HttpClient); 68 80 } 69 81 } 70 82 71 - if (opts.security) { 72 - await setAuthParams({ 73 - ...opts, 74 - security: opts.security, 75 - }); 76 - } 77 - 78 - if (opts.requestValidator) { 79 - await opts.requestValidator(opts); 80 - } 81 - 82 83 if (opts.body && opts.bodySerializer) { 83 84 opts.serializedBody = opts.bodySerializer(opts.body); 84 85 } ··· 88 89 opts.headers.delete('Content-Type'); 89 90 } 90 91 91 - const url = buildUrl(opts); 92 + const url = buildUrl(opts as any); 92 93 93 - let req = new HttpRequest<unknown>( 94 + const req = new HttpRequest<unknown>( 94 95 opts.method, 95 96 url, 96 97 opts.serializedBody || null, ··· 100 101 }, 101 102 ); 102 103 104 + return { opts, req }; 105 + }; 106 + 107 + const request: Client['request'] = async (options) => { 108 + const { opts, req: initialReq } = requestOptions(options); 109 + 110 + if (opts.security) { 111 + await setAuthParams({ 112 + ...opts, 113 + security: opts.security, 114 + }); 115 + } 116 + 117 + if (opts.requestValidator) { 118 + await opts.requestValidator(opts); 119 + } 120 + 121 + let req = initialReq; 122 + 103 123 for (const fn of interceptors.request._fns) { 104 124 if (fn) { 105 - req = await fn(req, opts); 125 + req = await fn(req, opts as any); 106 126 } 107 127 } 108 128 ··· 114 134 115 135 try { 116 136 response = await firstValueFrom( 117 - opts.httpClient 118 - .request(req) 137 + opts 138 + .httpClient!.request(req) 119 139 .pipe(filter((event) => event.type === HttpEventType.Response)), 120 140 ); 121 141 122 142 for (const fn of interceptors.response._fns) { 123 143 if (fn) { 124 - response = await fn(response, req, opts); 144 + response = await fn(response, req, opts as any); 125 145 } 126 146 } 127 147 ··· 151 171 finalError, 152 172 response as HttpResponse<unknown>, 153 173 req, 154 - opts, 174 + opts as any, 155 175 )) as string; 156 176 } 157 177 } ··· 182 202 post: (options) => request({ ...options, method: 'POST' }), 183 203 put: (options) => request({ ...options, method: 'PUT' }), 184 204 request, 205 + requestOptions: (options) => { 206 + if (options.security) { 207 + throw new Error('Security is not supported in requestOptions'); 208 + } 209 + 210 + if (options.requestValidator) { 211 + throw new Error( 212 + 'Request validation is not supported in requestOptions', 213 + ); 214 + } 215 + 216 + return requestOptions(options).req; 217 + }, 185 218 setConfig, 186 219 trace: (options) => request({ ...options, method: 'TRACE' }), 187 220 };
+9
packages/openapi-ts/src/plugins/@hey-api/client-angular/bundle/types.ts
··· 161 161 Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>, 162 162 ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>; 163 163 164 + type RequestOptionsFn = < 165 + ThrowOnError extends boolean = false, 166 + TResponseStyle extends ResponseStyle = 'fields', 167 + >( 168 + options: RequestOptions<TResponseStyle, ThrowOnError>, 169 + ) => HttpRequest<unknown>; 170 + 164 171 type BuildUrlFn = < 165 172 TData extends { 166 173 body?: unknown; ··· 179 186 unknown, 180 187 ResolvedRequestOptions 181 188 >; 189 + 190 + requestOptions: RequestOptionsFn; 182 191 }; 183 192 184 193 /**
+4 -4
packages/openapi-ts/src/plugins/config.ts
··· 1 - import type { HeyApiAngularResourcePlugin } from './@hey-api/angular-resource'; 2 - import { defaultConfig as heyApiAngularResource } from './@hey-api/angular-resource'; 1 + import type { HeyApiAngularResourcePlugin } from './@angular/common'; 2 + import { defaultConfig as heyApiAngularResource } from './@angular/common'; 3 3 import type { HeyApiClientAngularPlugin } from './@hey-api/client-angular'; 4 4 import { defaultConfig as heyApiClientAngular } from './@hey-api/client-angular'; 5 5 import type { HeyApiClientAxiosPlugin } from './@hey-api/client-axios'; ··· 47 47 import { defaultConfig as zod } from './zod'; 48 48 49 49 export interface PluginConfigMap { 50 - '@hey-api/angular-resource': HeyApiAngularResourcePlugin['Types']; 50 + '@angular/common': HeyApiAngularResourcePlugin['Types']; 51 51 '@hey-api/client-angular': HeyApiClientAngularPlugin['Types']; 52 52 '@hey-api/client-axios': HeyApiClientAxiosPlugin['Types']; 53 53 '@hey-api/client-fetch': HeyApiClientFetchPlugin['Types']; ··· 75 75 export const defaultPluginConfigs: { 76 76 [K in PluginNames]: Plugin.Config<PluginConfigMap[K]>; 77 77 } = { 78 - '@hey-api/angular-resource': heyApiAngularResource, 78 + '@angular/common': heyApiAngularResource, 79 79 '@hey-api/client-angular': heyApiClientAngular, 80 80 '@hey-api/client-axios': heyApiClientAxios, 81 81 '@hey-api/client-fetch': heyApiClientFetch,
+1 -1
packages/openapi-ts/src/plugins/types.d.ts
··· 21 21 22 22 export type PluginNames = 23 23 | PluginClientNames 24 - | '@hey-api/angular-resource' 24 + | '@angular/common' 25 25 | '@hey-api/schemas' 26 26 | '@hey-api/sdk' 27 27 | '@hey-api/transformers'