@rexeus/typeweaver-clients 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,25 @@
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
8
+ import axios, { AxiosError } from "axios";
1
9
  import type {
2
- IHttpQuery,
3
- IHttpParam,
4
10
  IHttpHeader,
11
+ IHttpParam,
12
+ IHttpQuery,
5
13
  IHttpResponse,
6
14
  } from "@rexeus/typeweaver-core";
7
15
  import { RequestCommand } from "./RequestCommand";
8
- import axios, {
9
- AxiosError,
10
- type AxiosInstance,
11
- type AxiosResponse,
12
- } from "axios";
13
- import Case from "case";
16
+ import type { ProcessResponseOptions } from "./RequestCommand";
17
+ import type { AxiosHeaderValue, AxiosInstance, AxiosResponse } from "axios";
18
+
19
+ /**
20
+ * Configuration options for handling unknown responses.
21
+ */
22
+ export type UnknownResponseHandling = "throw" | "passthrough";
14
23
 
15
24
  /**
16
25
  * Configuration options for ApiClient initialization.
@@ -20,6 +29,10 @@ export type ApiClientProps = {
20
29
  axiosInstance?: AxiosInstance;
21
30
  /** Base URL for API requests. If not provided, must be set in axiosInstance */
22
31
  baseUrl?: string;
32
+ /** How to handle unknown responses. Defaults to "throw" */
33
+ unknownResponseHandling?: UnknownResponseHandling;
34
+ /** Predicate to determine if a status code represents success. Defaults to 2xx status codes */
35
+ isSuccessStatusCode?: (statusCode: number) => boolean;
23
36
  };
24
37
 
25
38
  /**
@@ -37,6 +50,10 @@ export abstract class ApiClient {
37
50
  public readonly axiosInstance: AxiosInstance;
38
51
  /** The base URL for all API requests */
39
52
  public readonly baseUrl: string;
53
+ /** How to handle unknown responses */
54
+ public readonly unknownResponseHandling: UnknownResponseHandling;
55
+ /** Predicate to determine if a status code represents success */
56
+ public readonly isSuccessStatusCode: (statusCode: number) => boolean;
40
57
 
41
58
  /**
42
59
  * Creates a new ApiClient instance.
@@ -53,6 +70,24 @@ export abstract class ApiClient {
53
70
  "Base URL must be provided either in axios instance or in constructor"
54
71
  );
55
72
  }
73
+
74
+ this.unknownResponseHandling = props.unknownResponseHandling ?? "throw";
75
+ this.isSuccessStatusCode =
76
+ props.isSuccessStatusCode ??
77
+ ((statusCode: number) => statusCode >= 200 && statusCode < 300);
78
+ }
79
+
80
+ /**
81
+ * Gets the process response options for this client instance.
82
+ *
83
+ * @returns The configuration options for processing responses
84
+ * @protected
85
+ */
86
+ protected get processResponseOptions(): ProcessResponseOptions {
87
+ return {
88
+ unknownResponseHandling: this.unknownResponseHandling,
89
+ isSuccessStatusCode: this.isSuccessStatusCode,
90
+ };
56
91
  }
57
92
 
58
93
  /**
@@ -109,28 +144,17 @@ export abstract class ApiClient {
109
144
  throw new Error("Network error: Unknown error");
110
145
  }
111
146
 
112
- throw new Error(`Network error: ${error instanceof Error ? error.message : String(error)}`);
147
+ throw new Error(
148
+ `Network error: ${error instanceof Error ? error.message : String(error)}`
149
+ );
113
150
  }
114
151
  }
115
152
 
116
153
  private createResponse(response: AxiosResponse): IHttpResponse {
117
- const header: IHttpHeader = Object.entries(response.headers).reduce(
118
- (acc, [key, value]) => {
119
- if (!value) {
120
- return acc;
121
- }
122
-
123
- const lowerCaseKey = key.toLowerCase();
124
- const headerCaseKey = Case.header(lowerCaseKey);
125
-
126
- return {
127
- ...acc,
128
- [headerCaseKey]: value,
129
- [lowerCaseKey]: value,
130
- };
131
- },
132
- {}
133
- );
154
+ const header: IHttpHeader = {};
155
+ Object.entries(response.headers).forEach(([key, value]) => {
156
+ this.addMultiValue(header, key, String(value));
157
+ });
134
158
 
135
159
  return {
136
160
  body: response.data,
@@ -177,13 +201,32 @@ export abstract class ApiClient {
177
201
 
178
202
  private createUrl(path: string, query?: IHttpQuery): string {
179
203
  const url = new URL(path, this.baseUrl);
180
-
181
204
  this.addQuery(url, query);
182
-
183
205
  return url.toString();
184
206
  }
185
207
 
186
208
  private createHeader(header: any): any {
187
209
  return header;
188
210
  }
211
+
212
+ private addMultiValue(
213
+ record: Record<string, string | string[]>,
214
+ key: string,
215
+ value: AxiosHeaderValue
216
+ ): void {
217
+ const existing = record[key];
218
+ const preparedValue = Array.isArray(value)
219
+ ? value.map(String)
220
+ : [String(value)];
221
+ if (existing) {
222
+ if (Array.isArray(existing)) {
223
+ existing.push(...preparedValue);
224
+ } else {
225
+ record[key] = [existing, ...preparedValue];
226
+ }
227
+ } else {
228
+ record[key] =
229
+ preparedValue.length > 1 ? preparedValue : (preparedValue[0] as string);
230
+ }
231
+ }
189
232
  }
@@ -1,13 +1,29 @@
1
- import {
2
- type HttpMethod,
3
- type IHttpRequest,
4
- type IHttpHeader,
5
- type IHttpParam,
6
- type IHttpQuery,
7
- type IHttpBody,
8
- type IHttpResponse,
9
- HttpResponse,
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
8
+ import { HttpResponse } from "@rexeus/typeweaver-core";
9
+ import type {
10
+ HttpMethod,
11
+ IHttpBody,
12
+ IHttpHeader,
13
+ IHttpParam,
14
+ IHttpQuery,
15
+ IHttpRequest,
16
+ IHttpResponse,
10
17
  } from "@rexeus/typeweaver-core";
18
+ import type { UnknownResponseHandling } from "./ApiClient";
19
+
20
+ /**
21
+ * Configuration options for processing HTTP responses.
22
+ */
23
+ export type ProcessResponseOptions = {
24
+ readonly unknownResponseHandling: UnknownResponseHandling;
25
+ readonly isSuccessStatusCode: (statusCode: number) => boolean;
26
+ };
11
27
 
12
28
  /**
13
29
  * Abstract base class for type-safe API request commands.
@@ -55,7 +71,11 @@ export abstract class RequestCommand<
55
71
  * - Error response handling
56
72
  *
57
73
  * @param response - The raw HTTP response from the server
74
+ * @param options - Configuration options for response processing
58
75
  * @returns The processed, type-safe response object
59
76
  */
60
- public abstract processResponse(response: IHttpResponse): HttpResponse;
77
+ public abstract processResponse(
78
+ response: IHttpResponse,
79
+ options: ProcessResponseOptions
80
+ ): HttpResponse;
61
81
  }
package/dist/lib/index.ts CHANGED
@@ -1,2 +1,9 @@
1
+ /**
2
+ * This file was automatically generated by typeweaver.
3
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
4
+ *
5
+ * @generated by @rexeus/typeweaver
6
+ */
7
+
1
8
  export * from "./ApiClient";
2
9
  export * from "./RequestCommand";
@@ -1,6 +1,17 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * This file was automatically generated by typeweaver.
4
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
5
+ *
6
+ * @generated by @rexeus/typeweaver
7
+ */
8
+
1
9
  import { ApiClient, type ApiClientProps } from "../lib/clients";
2
10
  <% for (const operation of operations) { %>
3
- import { <%= operation.operationId %>RequestCommand, type Successful<%= operation.operationId %>Response } from "<%= operation.requestFile %>";
11
+ import { <%= operation.operationId %>RequestCommand } from "./<%= operation.operationId %>RequestCommand";
12
+ import type {
13
+ Successful<%= operation.operationId %>Response
14
+ } from "<%= operation.requestFile %>";
4
15
  <% } %>
5
16
 
6
17
  export type <%= pascalCaseEntityName %>RequestCommands =
@@ -24,16 +35,6 @@ export class <%= pascalCaseEntityName %>Client extends ApiClient {
24
35
  <% } %>
25
36
  public async send(command: <%= pascalCaseEntityName %>RequestCommands): Promise<Successful<%= pascalCaseEntityName %>Responses> {
26
37
  const response = await this.execute(command);
27
-
28
- switch (true) {
29
- <% for (const operation of operations) { %>
30
- case command instanceof <%= operation.operationId %>RequestCommand: {
31
- return command.processResponse(response);
32
- }
33
- <% } %>
34
- default: {
35
- throw new Error("Command is not supported");
36
- }
37
- }
38
+ return command.processResponse(response, this.processResponseOptions);
38
39
  }
39
40
  }
@@ -0,0 +1,90 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * This file was automatically generated by typeweaver.
4
+ * DO NOT EDIT. Instead, modify the source definition file and generate again.
5
+ *
6
+ * @generated by @rexeus/typeweaver
7
+ */
8
+
9
+ import definition from "<%= sourcePath %>";
10
+ import { HttpMethod, type IHttpResponse, ResponseValidationError, UnknownResponse } from "@rexeus/typeweaver-core";
11
+ import { RequestCommand, type ProcessResponseOptions } from "../lib/clients";
12
+ import { <%= pascalCaseOperationId %>ResponseValidator } from "<%= responseValidatorFile %>";
13
+ import type {
14
+ I<%= pascalCaseOperationId %>Request,
15
+ <%= headerTsType ? `I${pascalCaseOperationId}RequestHeader,` : "" %>
16
+ <%= paramTsType ? `I${pascalCaseOperationId}RequestParam,` : "" %>
17
+ <%= queryTsType ? `I${pascalCaseOperationId}RequestQuery,` : "" %>
18
+ <%= bodyTsType ? `I${pascalCaseOperationId}RequestBody,` : "" %>
19
+ Successful<%= pascalCaseOperationId %>Response,
20
+ } from "<%= requestFile %>";
21
+ <% for (const successResponse of [...ownSuccessResponses, ...sharedSuccessResponses]) { %>
22
+ import { <%= successResponse.name %>Response } from "<%= successResponseImportPath(successResponse) %>";
23
+ <% } %>
24
+
25
+ export class <%= pascalCaseOperationId %>RequestCommand extends RequestCommand implements I<%= pascalCaseOperationId %>Request {
26
+ public override readonly method = definition.method as HttpMethod.<%= method %>;
27
+ public override readonly path = definition.path;
28
+
29
+ public <%= headerTsType ? `override` : `declare` %> readonly header: <%= headerTsType ? `I${pascalCaseOperationId}RequestHeader` : `undefined` %>;
30
+ public <%= paramTsType ? `override` : `declare` %> readonly param: <%= paramTsType ? `I${pascalCaseOperationId}RequestParam` : `undefined` %>;
31
+ public <%= queryTsType ? `override` : `declare` %> readonly query: <%= queryTsType ? `I${pascalCaseOperationId}RequestQuery` : `undefined` %>;
32
+ public <%= bodyTsType ? `override` : `declare` %> readonly body: <%= bodyTsType ? `I${pascalCaseOperationId}RequestBody` : `undefined` %>;
33
+
34
+ private readonly responseValidator: <%= pascalCaseOperationId %>ResponseValidator;
35
+
36
+ public constructor(input: Omit<I<%= pascalCaseOperationId %>Request, "method" | "path">) {
37
+ super();
38
+
39
+ <% if (headerTsType) { %>
40
+ this.header = input.header;
41
+ <% } %>
42
+
43
+ <% if (paramTsType) { %>
44
+ this.param = input.param;
45
+ <% } %>
46
+
47
+ <% if (queryTsType) { %>
48
+ this.query = input.query;
49
+ <% } %>
50
+
51
+ <% if (bodyTsType) { %>
52
+ this.body = input.body;
53
+ <% } %>
54
+
55
+ this.responseValidator = new <%= pascalCaseOperationId %>ResponseValidator();
56
+ }
57
+
58
+ public processResponse(
59
+ response: IHttpResponse,
60
+ options: ProcessResponseOptions
61
+ ): Successful<%= pascalCaseOperationId %>Response {
62
+ try {
63
+ const result = this.responseValidator.validate(response);
64
+
65
+ <% for (const successResponse of [...ownSuccessResponses, ...sharedSuccessResponses]) { %>
66
+ if (result instanceof <%= successResponse.name %>Response) {
67
+ return result;
68
+ }
69
+ <% } %>
70
+
71
+ throw result;
72
+ } catch (error) {
73
+ if (error instanceof ResponseValidationError) {
74
+ const unknownResponse = new UnknownResponse(
75
+ response.statusCode,
76
+ response.header,
77
+ response.body,
78
+ error
79
+ );
80
+
81
+ if (options.unknownResponseHandling === "passthrough" && options.isSuccessStatusCode(response.statusCode)) {
82
+ return unknownResponse as any;
83
+ }
84
+
85
+ throw unknownResponse;
86
+ }
87
+ throw error;
88
+ }
89
+ }
90
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rexeus/typeweaver-clients",
3
- "version": "0.0.2",
4
- "description": "HTTP client generator for TypeWeaver API framework",
3
+ "version": "0.0.4",
4
+ "description": "HTTP client generator for typeweaver API framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -14,7 +14,9 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "package.json",
17
- "README.md"
17
+ "README.md",
18
+ "LICENSE",
19
+ "NOTICE"
18
20
  ],
19
21
  "keywords": [
20
22
  "api",
@@ -26,7 +28,7 @@
26
28
  "typeweaver"
27
29
  ],
28
30
  "author": "Dennis Wentzien <dw@rexeus.com>",
29
- "license": "ISC",
31
+ "license": "Apache-2.0",
30
32
  "repository": {
31
33
  "type": "git",
32
34
  "url": "git+https://github.com/rexeus/typeweaver.git",
@@ -37,18 +39,26 @@
37
39
  },
38
40
  "homepage": "https://github.com/rexeus/typeweaver#readme",
39
41
  "peerDependencies": {
40
- "axios": "^1.7.0"
42
+ "axios": "^1.7.0",
43
+ "@rexeus/typeweaver-core": "^0.0.4",
44
+ "@rexeus/typeweaver-gen": "^0.0.4"
41
45
  },
42
46
  "devDependencies": {
47
+ "@hono/node-server": "^1.16.0",
48
+ "axios": "^1.11.0",
49
+ "test-utils": "file:../test-utils",
50
+ "@rexeus/typeweaver-gen": "^0.0.4",
51
+ "@rexeus/typeweaver-core": "^0.0.4"
52
+ },
53
+ "dependencies": {
43
54
  "case": "^1.6.3",
44
- "axios": "^1.9.0",
45
- "@rexeus/typeweaver-core": "^0.0.2",
46
- "@rexeus/typeweaver-gen": "^0.0.2"
55
+ "@rexeus/typeweaver-zod-to-ts": "^0.0.4"
47
56
  },
48
57
  "scripts": {
49
58
  "typecheck": "tsc --noEmit",
50
59
  "format": "prettier --write .",
51
- "build": "pkgroll --clean-dist && cp -r ./src/templates ./dist/templates && cp -r ./src/lib ./dist/lib",
60
+ "build": "pkgroll --clean-dist && cp -r ./src/templates ./dist/templates && cp -r ./src/lib ./dist/lib && cp ../../LICENSE ../../NOTICE ./dist/",
61
+ "test": "vitest --run",
52
62
  "preversion": "npm run build"
53
63
  }
54
64
  }