From f7325b92cbe6d8d53500ae213aec1f3c1d19860e Mon Sep 17 00:00:00 2001 From: Alisue Date: Tue, 23 Dec 2025 13:00:58 +0900 Subject: [PATCH] BREAKING(@probitas/client-http,@probitas/client-graphql,@probitas/client-connectrpc): convert getter methods to properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Response body accessor methods are now properties for cleaner API: - HTTP: text(), json(), arrayBuffer(), blob() → getter properties - HTTP: raw() → readonly property - GraphQL: data(), raw() → readonly properties - ConnectRPC: data(), raw() → readonly properties The json property returns 'unknown' instead of generic T, requiring explicit type assertions where needed. This change eliminates unnecessary function call syntax while preserving the same lazy evaluation behavior. Migration: - response.json() → response.json as User | null - response.text() → response.text - response.data() → response.data as T | null - All other accessors: remove () parentheses --- README.md | 2 +- packages/probitas-client-connectrpc/client.ts | 2 +- .../integration_test.ts | 12 +- packages/probitas-client-connectrpc/mod.ts | 4 +- .../probitas-client-connectrpc/response.ts | 74 +++++------ .../response_test.ts | 28 ++-- packages/probitas-client-graphql/client.ts | 4 +- .../probitas-client-graphql/client_test.ts | 14 +- .../integration_test.ts | 56 ++++---- packages/probitas-client-graphql/mod.ts | 6 +- packages/probitas-client-graphql/response.ts | 70 +++++----- .../probitas-client-graphql/response_test.ts | 22 ++-- .../probitas-client-grpc/integration_test.ts | 12 +- packages/probitas-client-grpc/mod.ts | 8 +- packages/probitas-client-http/body.ts | 25 ++-- packages/probitas-client-http/body_test.ts | 62 ++++----- packages/probitas-client-http/client.ts | 2 +- packages/probitas-client-http/client_test.ts | 12 +- packages/probitas-client-http/errors.ts | 24 ++-- packages/probitas-client-http/errors_test.ts | 44 +++---- .../probitas-client-http/integration_test.ts | 40 +++--- packages/probitas-client-http/mod.ts | 2 +- packages/probitas-client-http/response.ts | 121 ++++++++---------- .../probitas-client-http/response_test.ts | 58 ++++----- 24 files changed, 332 insertions(+), 372 deletions(-) diff --git a/README.md b/README.md index 77b6ac5..40212cd 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ export default scenario("example http request") .step("assert response", (ctx) => { const response = ctx.previous; assertEquals(response.status, 200); - assertEquals(response.json().args.hello, "world"); + assertEquals(response.json.args.hello, "world"); }) .build(); ``` diff --git a/packages/probitas-client-connectrpc/client.ts b/packages/probitas-client-connectrpc/client.ts index 6ed0aed..32dd14a 100644 --- a/packages/probitas-client-connectrpc/client.ts +++ b/packages/probitas-client-connectrpc/client.ts @@ -292,7 +292,7 @@ function toMethodInfo( * "echo", * { message: "Hello!" } * ); - * console.log(response.data()); + * console.log(response.data); * * await client.close(); * ``` diff --git a/packages/probitas-client-connectrpc/integration_test.ts b/packages/probitas-client-connectrpc/integration_test.ts index b30bef2..5fc9833 100644 --- a/packages/probitas-client-connectrpc/integration_test.ts +++ b/packages/probitas-client-connectrpc/integration_test.ts @@ -189,9 +189,9 @@ Deno.test({ assert(response.ok); assertEquals(response.statusCode, 0); - assertExists(response.data()); + assertExists(response.data); - const data = response.data<{ message: string }>(); + const data = response.data as { message: string } | null; assertExists(data); assertEquals(data.message, "Hello Reflection!"); }); @@ -205,8 +205,8 @@ Deno.test({ assert(response.ok); assertEquals(response.statusCode, 0); - assertExists(response.data()); - const data = response.data<{ message: string }>(); + assertExists(response.data); + const data = response.data as { message: string } | null; assertEquals(data?.message, "Test message"); assertLess(response.duration, 5000); }); @@ -377,7 +377,7 @@ Deno.test({ ) ) { assert(response.ok); - messages.push(response.data()); + messages.push(response.data); } assertEquals(messages.length, 3); @@ -507,7 +507,7 @@ Deno.test({ assertFalse(response.ok); assertEquals(response.statusCode !== 0, true); - assertEquals(response.data(), null); + assertEquals(response.data, null); }); }, }); diff --git a/packages/probitas-client-connectrpc/mod.ts b/packages/probitas-client-connectrpc/mod.ts index 3302688..43379ac 100644 --- a/packages/probitas-client-connectrpc/mod.ts +++ b/packages/probitas-client-connectrpc/mod.ts @@ -43,7 +43,7 @@ * "echo", * { message: "Hello!" } * ); - * console.log(response.data()); + * console.log(response.data); * * await client.close(); * ``` @@ -77,7 +77,7 @@ * await using client = createConnectRpcClient({ url: "http://localhost:50051" }); * * const res = await client.call("echo.EchoService", "echo", { message: "test" }); - * console.log(res.data()); + * console.log(res.data); * // Client automatically closed when block exits * ``` * diff --git a/packages/probitas-client-connectrpc/response.ts b/packages/probitas-client-connectrpc/response.ts index 45c7221..04d100c 100644 --- a/packages/probitas-client-connectrpc/response.ts +++ b/packages/probitas-client-connectrpc/response.ts @@ -66,17 +66,17 @@ interface ConnectRpcResponseBase extends ClientResult { readonly duration: number; /** - * Get deserialized response data. - * Returns the response message as-is (already deserialized by Connect). - * Returns null if the response is an error or has no data. + * Deserialized response data. + * The response message as-is (already deserialized by Connect). + * Null if the response is an error or has no data. */ - data(): U | null; + readonly data: T | null; /** - * Get raw response or error. - * Returns null for failure responses. + * Raw response or error. + * Null for failure responses. */ - raw(): unknown | null; + readonly raw: unknown | null; } /** @@ -101,7 +101,11 @@ export interface ConnectRpcResponseSuccess /** Response trailers (sent at end of RPC). */ readonly trailers: Headers; - raw(): unknown; + /** Raw response. */ + readonly raw: unknown; + + /** Response data. */ + readonly data: T | null; } /** @@ -128,7 +132,11 @@ export interface ConnectRpcResponseError /** Response trailers (sent at end of RPC). */ readonly trailers: Headers; - raw(): ConnectError; + /** Raw ConnectError. */ + readonly raw: ConnectError; + + /** No data for error responses. */ + readonly data: null; } /** @@ -155,7 +163,11 @@ export interface ConnectRpcResponseFailure /** Response trailers (null for failures). */ readonly trailers: null; - raw(): null; + /** No raw response (request didn't reach server). */ + readonly raw: null; + + /** No data (request didn't reach server). */ + readonly data: null; } /** @@ -220,25 +232,15 @@ export class ConnectRpcResponseSuccessImpl readonly headers: Headers; readonly trailers: Headers; readonly duration: number; - - readonly #response: T | null; + readonly data: T | null; + readonly raw: T | null; constructor(params: ConnectRpcResponseSuccessParams) { this.headers = params.headers; this.trailers = params.trailers; this.duration = params.duration; - this.#response = params.response; - } - - data(): U | null { - if (this.#response === null || this.#response === undefined) { - return null; - } - return this.#response as U; - } - - raw(): T | null { - return this.#response; + this.data = params.response; + this.raw = params.response; } } @@ -257,26 +259,18 @@ export class ConnectRpcResponseErrorImpl readonly headers: Headers; readonly trailers: Headers; readonly duration: number; - - readonly #connectError: ConnectError; + readonly data = null; + readonly raw: ConnectError; constructor(params: ConnectRpcResponseErrorParams) { this.headers = params.headers; this.trailers = params.trailers; this.duration = params.duration; this.error = params.rpcError; - this.#connectError = params.error; + this.raw = params.error; this.statusCode = params.rpcError.statusCode; this.statusMessage = params.rpcError.statusMessage; } - - data(): U | null { - return null; - } - - raw(): ConnectError { - return this.#connectError; - } } /** @@ -294,17 +288,11 @@ export class ConnectRpcResponseFailureImpl readonly headers = null; readonly trailers = null; readonly duration: number; + readonly data = null; + readonly raw = null; constructor(params: ConnectRpcResponseFailureParams) { this.error = params.error; this.duration = params.duration; } - - data(): null { - return null; - } - - raw(): null { - return null; - } } diff --git a/packages/probitas-client-connectrpc/response_test.ts b/packages/probitas-client-connectrpc/response_test.ts index 68b9dab..41badb1 100644 --- a/packages/probitas-client-connectrpc/response_test.ts +++ b/packages/probitas-client-connectrpc/response_test.ts @@ -31,7 +31,7 @@ Deno.test("ConnectRpcResponseSuccessImpl", async (t) => { assertEquals(response.error, null); assertEquals(response.statusCode, 0); assertEquals(response.statusMessage, null); - assertEquals(response.data(), { user: { id: 1, name: "John" } }); + assertEquals(response.data, { user: { id: 1, name: "John" } }); assertEquals(response.duration, 100); }); @@ -51,7 +51,7 @@ Deno.test("ConnectRpcResponseSuccessImpl", async (t) => { assertEquals(response.trailers.get("grpc-status"), "0"); }); - await t.step("raw() returns response data", () => { + await t.step("raw returns response data", () => { const rawResponse = { nested: { value: 123 } }; const response = new ConnectRpcResponseSuccessImpl({ response: rawResponse, @@ -60,10 +60,10 @@ Deno.test("ConnectRpcResponseSuccessImpl", async (t) => { duration: 10, }); - assertEquals(response.raw(), rawResponse); + assertEquals(response.raw, rawResponse); }); - await t.step("data() method returns typed data", () => { + await t.step("data supports type assertion", () => { interface User { id: number; name: string; @@ -75,7 +75,7 @@ Deno.test("ConnectRpcResponseSuccessImpl", async (t) => { duration: 100, }); - const result = response.data<{ user: User }>(); + const result = response.data as { user: User } | null; assertEquals(result?.user.id, 1); assertEquals(result?.user.name, "John"); }); @@ -88,8 +88,8 @@ Deno.test("ConnectRpcResponseSuccessImpl", async (t) => { duration: 100, }); - assertEquals(response.data(), null); - assertEquals(response.raw(), null); + assertEquals(response.data, null); + assertEquals(response.raw, null); }); }); @@ -111,10 +111,10 @@ Deno.test("ConnectRpcResponseErrorImpl", async (t) => { assertEquals(response.error, rpcError); assertEquals(response.statusCode, 5); assertEquals(response.statusMessage, "Not found"); - assertEquals(response.data(), null); + assertEquals(response.data, null); }); - await t.step("raw() returns ConnectError", () => { + await t.step("raw returns ConnectError", () => { const connectError = new ConnectError("Internal error", 13); const rpcError = fromConnectError(connectError); const response = new ConnectRpcResponseErrorImpl({ @@ -125,7 +125,7 @@ Deno.test("ConnectRpcResponseErrorImpl", async (t) => { duration: 10, }); - assertEquals(response.raw(), connectError); + assertEquals(response.raw, connectError); }); await t.step("includes headers and trailers", () => { @@ -203,23 +203,23 @@ Deno.test("ConnectRpcResponseFailureImpl", async (t) => { assertEquals(response.trailers, null); }); - await t.step("data() returns null", () => { + await t.step("data is null", () => { const error = new ConnectRpcNetworkError("Network error"); const response = new ConnectRpcResponseFailureImpl({ error, duration: 5, }); - assertEquals(response.data(), null); + assertEquals(response.data, null); }); - await t.step("raw() returns null", () => { + await t.step("raw is null", () => { const error = new ConnectRpcNetworkError("Network error"); const response = new ConnectRpcResponseFailureImpl({ error, duration: 5, }); - assertEquals(response.raw(), null); + assertEquals(response.raw, null); }); }); diff --git a/packages/probitas-client-graphql/client.ts b/packages/probitas-client-graphql/client.ts index c5670d7..c3bfd64 100644 --- a/packages/probitas-client-graphql/client.ts +++ b/packages/probitas-client-graphql/client.ts @@ -533,7 +533,7 @@ class GraphqlClientImpl implements GraphqlClient { * } * `, { id: "123" }); * - * console.log(response.data()); + * console.log(response.data); * await client.close(); * ``` * @@ -560,7 +560,7 @@ class GraphqlClientImpl implements GraphqlClient { * `, { input: { name: "Alice", email: "alice@example.com" } }); * * if (response.ok) { - * console.log("Created user:", response.data().createUser.id); + * console.log("Created user:", (response.data as any).createUser.id); * } * * await client.close(); diff --git a/packages/probitas-client-graphql/client_test.ts b/packages/probitas-client-graphql/client_test.ts index d793215..685aa5f 100644 --- a/packages/probitas-client-graphql/client_test.ts +++ b/packages/probitas-client-graphql/client_test.ts @@ -119,7 +119,7 @@ Deno.test("GraphqlClient.query", async (t) => { await client.close(); assert(response.ok); - assertEquals(response.data(), { user: { id: 1, name: "John" } }); + assertEquals(response.data, { user: { id: 1, name: "John" } }); assertEquals(response.error, null); }); @@ -271,7 +271,7 @@ Deno.test("GraphqlClient.execute", async (t) => { const response = await client.execute("query { test }"); await client.close(); - assertEquals(response.data(), { test: true }); + assertEquals(response.data, { test: true }); }); await t.step("works for mutations", async () => { @@ -287,7 +287,7 @@ Deno.test("GraphqlClient.execute", async (t) => { const response = await client.execute("mutation { updateSomething }"); await client.close(); - assertEquals(response.data(), { updated: true }); + assertEquals(response.data, { updated: true }); }); }); @@ -432,7 +432,7 @@ Deno.test("GraphqlClient response", async (t) => { const response = await client.query("query { test }"); await client.close(); - assertInstanceOf(response.raw(), Response); + assertInstanceOf(response.raw, Response); }); }); @@ -458,8 +458,8 @@ Deno.test("GraphqlClient network errors", async (t) => { assertEquals(response.error?.message, "fetch failed"); assertEquals(response.status, null); assertEquals(response.headers, null); - assertEquals(response.data(), null); - assertEquals(response.raw(), null); + assertEquals(response.data, null); + assertEquals(response.raw, null); }, ); @@ -604,7 +604,7 @@ Deno.test("GraphqlClient partial data with errors", async (t) => { await client.close(); assertFalse(response.ok); - assertEquals(response.data(), { user: { id: 1 }, posts: null }); + assertEquals(response.data, { user: { id: 1 }, posts: null }); assertInstanceOf(response.error, GraphqlExecutionError); }, ); diff --git a/packages/probitas-client-graphql/integration_test.ts b/packages/probitas-client-graphql/integration_test.ts index b461337..0d7c2cc 100644 --- a/packages/probitas-client-graphql/integration_test.ts +++ b/packages/probitas-client-graphql/integration_test.ts @@ -48,9 +48,9 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - assertExists(res.data()); + assertExists(res.data); - assertEquals(res.data()?.__typename, "Query"); + assertEquals(res.data?.__typename, "Query"); }); await t.step("introspection query - __schema", async () => { @@ -59,9 +59,9 @@ Deno.test({ }>("{ __schema { queryType { name } } }"); assert(res.ok); - assertExists(res.data()); + assertExists(res.data); - assertEquals(res.data()?.__schema.queryType.name, "Query"); + assertEquals(res.data?.__schema.queryType.name, "Query"); }); await t.step("echo query - basic message", async () => { @@ -75,8 +75,8 @@ Deno.test({ ); assert(res.ok); - assertExists(res.data()); - assertEquals(res.data()?.echo, "Hello, Probitas!"); + assertExists(res.data); + assertEquals(res.data?.echo, "Hello, Probitas!"); }); await t.step("echoWithDelay for latency testing", async () => { @@ -92,8 +92,8 @@ Deno.test({ const elapsed = Date.now() - start; assert(res.ok); - assertExists(res.data()); - assertEquals(res.data()?.echoWithDelay, "Delayed message"); + assertExists(res.data); + assertEquals(res.data?.echoWithDelay, "Delayed message"); // Should have taken at least 500ms assertEquals( @@ -150,8 +150,8 @@ Deno.test({ ); assert(res.ok); - assertExists(res.data()); - const results = res.data()?.echoPartialError; + assertExists(res.data); + const results = res.data?.echoPartialError; // First and third should succeed, second should have error assertEquals(results?.[0]?.message, "success"); assertEquals(results?.[0]?.error, null); @@ -193,7 +193,7 @@ Deno.test({ await t.step("includes raw response", async () => { const res = await client.query("{ __typename }"); - assertInstanceOf(res.raw(), Response); + assertInstanceOf(res.raw, Response); }); await t.step("using await using (AsyncDisposable)", async () => { @@ -301,9 +301,9 @@ Deno.test({ ); assert(res.ok); - assertExists(res.data()); + assertExists(res.data); - const message = res.data()?.createMessage; + const message = res.data?.createMessage; assertEquals(message?.text, "Hello from integration test"); assertEquals(typeof message?.id, "string"); assertEquals(typeof message?.createdAt, "string"); @@ -314,8 +314,8 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - assertExists(res.data()); - assertEquals(res.data()?.__typename, "Query"); + assertExists(res.data); + assertEquals(res.data?.__typename, "Query"); assertLess(res.duration, 5000); }); @@ -334,8 +334,8 @@ Deno.test({ ); assert(res.ok); - assertExists(res.data()); - assertEquals(res.data()?.echoWithExtensions, "Hello"); + assertExists(res.data); + assertEquals(res.data?.echoWithExtensions, "Hello"); // Server includes timing and tracing extensions assertEquals(typeof res.extensions?.timing, "object"); assertEquals(typeof res.extensions?.tracing, "object"); @@ -358,7 +358,7 @@ Deno.test({ { text: "Original text" }, ); - const messageId = createRes.data()?.createMessage.id; + const messageId = createRes.data?.createMessage.id; // Then update it const updateRes = await client.mutation<{ @@ -376,9 +376,9 @@ Deno.test({ ); assert(updateRes.ok); - assertExists(updateRes.data()); - assertEquals(updateRes.data()?.updateMessage.id, messageId); - assertEquals(updateRes.data()?.updateMessage.text, "Updated text"); + assertExists(updateRes.data); + assertEquals(updateRes.data?.updateMessage.id, messageId); + assertEquals(updateRes.data?.updateMessage.text, "Updated text"); }); await t.step("mutation - deleteMessage", async () => { @@ -396,7 +396,7 @@ Deno.test({ { text: "To be deleted" }, ); - const messageId = createRes.data()?.createMessage.id; + const messageId = createRes.data?.createMessage.id; // Then delete it const deleteRes = await client.mutation<{ @@ -411,8 +411,8 @@ Deno.test({ ); assert(deleteRes.ok); - assertExists(deleteRes.data()); - assertEquals(deleteRes.data()?.deleteMessage, true); + assertExists(deleteRes.data); + assertEquals(deleteRes.data?.deleteMessage, true); }); await t.step("subscription - countdown", async () => { @@ -434,8 +434,8 @@ Deno.test({ ) ) { assert(res.ok); - assertExists(res.data()); - numbers.push(res.data()?.countdown ?? -1); + assertExists(res.data); + numbers.push(res.data?.countdown ?? -1); } assertEquals(numbers, [3, 2, 1, 0]); @@ -475,9 +475,9 @@ Deno.test({ ); assert(res.ok); - assertExists(res.data()); + assertExists(res.data); - const headers = res.data()?.echoHeaders; + const headers = res.data?.echoHeaders; // Verify config-level headers were sent assertEquals(headers?.authorization, "Bearer test-token-123"); assertEquals(headers?.custom, "probitas-integration-test"); diff --git a/packages/probitas-client-graphql/mod.ts b/packages/probitas-client-graphql/mod.ts index 79284af..a142925 100644 --- a/packages/probitas-client-graphql/mod.ts +++ b/packages/probitas-client-graphql/mod.ts @@ -31,7 +31,7 @@ * user(id: $id) { id name email } * } * `, { id: "123" }); - * console.log("User:", res.data()); + * console.log("User:", res.data); * * // Mutation * const created = await client.mutation(outdent` @@ -39,7 +39,7 @@ * createUser(input: $input) { id name } * } * `, { input: { name: "Jane", email: "jane@example.com" } }); - * console.log("Created:", created.data()); + * console.log("Created:", created.data); * * await client.close(); * ``` @@ -52,7 +52,7 @@ * await using client = createGraphqlClient({ url: "http://localhost:4000/graphql" }); * * const res = await client.query(`{ __typename }`); - * console.log(res.data()); + * console.log(res.data); * // Client automatically closed when block exits * ``` * diff --git a/packages/probitas-client-graphql/response.ts b/packages/probitas-client-graphql/response.ts index e5df9a5..2d1ec5e 100644 --- a/packages/probitas-client-graphql/response.ts +++ b/packages/probitas-client-graphql/response.ts @@ -57,16 +57,16 @@ interface GraphqlResponseBase extends ClientResult { readonly url: string; /** - * Get response data (null if no data or request failed). + * Response data (null if no data or request failed). * Does not throw even if errors are present. */ - data(): U | null; + readonly data: T | null; /** - * Get the raw Web standard Response object. - * Returns null for failure responses. + * Raw Web standard Response object. + * Null for failure responses. */ - raw(): globalThis.Response | null; + readonly raw: globalThis.Response | null; } /** @@ -88,7 +88,11 @@ export interface GraphqlResponseSuccess /** Response extensions. */ readonly extensions: Record | null; - raw(): globalThis.Response; + /** Raw Web standard Response. */ + readonly raw: globalThis.Response; + + /** Response data (null if no data). */ + readonly data: T | null; } /** @@ -112,7 +116,11 @@ export interface GraphqlResponseError extends GraphqlResponseBase { /** Response extensions. */ readonly extensions: Record | null; - raw(): globalThis.Response; + /** Raw Web standard Response. */ + readonly raw: globalThis.Response; + + /** Response data (null if no data, may be partial data with errors). */ + readonly data: T | null; } /** @@ -136,7 +144,11 @@ export interface GraphqlResponseFailure /** HTTP response headers (null for failures). */ readonly headers: null; - raw(): null; + /** No raw response (request didn't reach server). */ + readonly raw: null; + + /** No data (request didn't reach server). */ + readonly data: null; } /** @@ -206,26 +218,18 @@ export class GraphqlResponseSuccessImpl readonly status: number; readonly headers: Headers; - readonly #data: T | null; - readonly #raw: globalThis.Response; + readonly data: T | null; + readonly raw: globalThis.Response; constructor(params: GraphqlResponseSuccessParams) { this.url = params.url; - this.#data = params.data; - this.#raw = params.raw; + this.data = params.data; + this.raw = params.raw; this.extensions = params.extensions; this.duration = params.duration; this.status = params.status; this.headers = params.raw.headers; } - - data(): U | null { - return this.#data as U | null; - } - - raw(): globalThis.Response { - return this.#raw; - } } /** @@ -243,27 +247,19 @@ export class GraphqlResponseErrorImpl implements GraphqlResponseError { readonly status: number; readonly headers: Headers; - readonly #data: T | null; - readonly #raw: globalThis.Response; + readonly data: T | null; + readonly raw: globalThis.Response; constructor(params: GraphqlResponseErrorParams) { this.url = params.url; - this.#data = params.data; - this.#raw = params.raw; + this.data = params.data; + this.raw = params.raw; this.error = params.error; this.extensions = params.extensions; this.duration = params.duration; this.status = params.status; this.headers = params.raw.headers; } - - data(): U | null { - return this.#data as U | null; - } - - raw(): globalThis.Response { - return this.#raw; - } } /** @@ -281,18 +277,12 @@ export class GraphqlResponseFailureImpl readonly headers = null; readonly duration: number; readonly url: string; + readonly data = null; + readonly raw = null; constructor(params: GraphqlResponseFailureParams) { this.url = params.url; this.error = params.error; this.duration = params.duration; } - - data(): null { - return null; - } - - raw(): null { - return null; - } } diff --git a/packages/probitas-client-graphql/response_test.ts b/packages/probitas-client-graphql/response_test.ts index 0d905a6..3732b98 100644 --- a/packages/probitas-client-graphql/response_test.ts +++ b/packages/probitas-client-graphql/response_test.ts @@ -26,7 +26,7 @@ Deno.test("GraphqlResponseSuccessImpl", async (t) => { assertEquals(response.processed, true); assert(response.ok); assertEquals(response.error, null); - assertEquals(response.data(), { user: { id: 1, name: "John" } }); + assertEquals(response.data, { user: { id: 1, name: "John" } }); assertEquals(response.duration, 100); assertEquals(response.status, 200); assertEquals(response.url, "http://localhost:4000/graphql"); @@ -56,7 +56,7 @@ Deno.test("GraphqlResponseSuccessImpl", async (t) => { raw: rawResponse, }); - assertEquals(response.raw(), rawResponse); + assertEquals(response.raw, rawResponse); }); await t.step("includes headers from raw response", () => { @@ -76,7 +76,7 @@ Deno.test("GraphqlResponseSuccessImpl", async (t) => { assertEquals(response.headers.get("X-Custom-Header"), "test-value"); }); - await t.step("data() method returns typed data", () => { + await t.step("data supports type assertion", () => { interface User { id: number; name: string; @@ -90,7 +90,7 @@ Deno.test("GraphqlResponseSuccessImpl", async (t) => { raw: new Response(), }); - const result = response.data<{ user: User }>(); + const result = response.data as { user: User } | null; assertEquals(result?.user.id, 1); assertEquals(result?.user.name, "John"); }); @@ -115,7 +115,7 @@ Deno.test("GraphqlResponseErrorImpl", async (t) => { assertEquals(response.processed, true); assertFalse(response.ok); assertEquals(response.error, error); - assertEquals(response.data(), null); + assertEquals(response.data, null); assertEquals(response.status, 200); }); @@ -134,7 +134,7 @@ Deno.test("GraphqlResponseErrorImpl", async (t) => { }); assertFalse(response.ok); - assertEquals(response.data(), { user: { id: 1 }, posts: null }); + assertEquals(response.data, { user: { id: 1 }, posts: null }); }); await t.step("includes raw response", () => { @@ -152,7 +152,7 @@ Deno.test("GraphqlResponseErrorImpl", async (t) => { raw: rawResponse, }); - assertEquals(response.raw(), rawResponse); + assertEquals(response.raw, rawResponse); }); }); @@ -195,7 +195,7 @@ Deno.test("GraphqlResponseFailureImpl", async (t) => { assertEquals(response.headers, null); }); - await t.step("data() returns null", () => { + await t.step("data is null", () => { const error = new GraphqlNetworkError("Network error"); const response = new GraphqlResponseFailureImpl({ url: "http://localhost:4000/graphql", @@ -203,10 +203,10 @@ Deno.test("GraphqlResponseFailureImpl", async (t) => { duration: 5, }); - assertEquals(response.data(), null); + assertEquals(response.data, null); }); - await t.step("raw() returns null", () => { + await t.step("raw is null", () => { const error = new GraphqlNetworkError("Network error"); const response = new GraphqlResponseFailureImpl({ url: "http://localhost:4000/graphql", @@ -214,6 +214,6 @@ Deno.test("GraphqlResponseFailureImpl", async (t) => { duration: 5, }); - assertEquals(response.raw(), null); + assertEquals(response.raw, null); }); }); diff --git a/packages/probitas-client-grpc/integration_test.ts b/packages/probitas-client-grpc/integration_test.ts index 4bbd3ec..2999c3b 100644 --- a/packages/probitas-client-grpc/integration_test.ts +++ b/packages/probitas-client-grpc/integration_test.ts @@ -190,9 +190,9 @@ Deno.test({ assertEquals(response.ok, true); assertEquals(response.statusCode, 0); - assertExists(response.data()); + assertExists(response.data); - const data = response.data<{ message: string }>(); + const data = response.data as { message: string } | null; assertExists(data); assertEquals(data.message, "Hello gRPC!"); }); @@ -206,8 +206,8 @@ Deno.test({ assertEquals(response.ok, true); assertEquals(response.statusCode, 0); - assertExists(response.data()); - const data = response.data<{ message: string }>(); + assertExists(response.data); + const data = response.data as { message: string } | null; assertEquals(data?.message, "Test message"); assertLess(response.duration, 5000); }); @@ -374,7 +374,7 @@ Deno.test({ ) ) { assertEquals(response.ok, true); - messages.push(response.data()); + messages.push(response.data); } assertEquals(messages.length, 3); @@ -447,7 +447,7 @@ Deno.test({ assertEquals(response.ok, false); assertEquals(response.statusCode, 3); - assertEquals(response.data(), null); + assertEquals(response.data, null); }); }, }); diff --git a/packages/probitas-client-grpc/mod.ts b/packages/probitas-client-grpc/mod.ts index cc858b7..164c328 100644 --- a/packages/probitas-client-grpc/mod.ts +++ b/packages/probitas-client-grpc/mod.ts @@ -37,7 +37,7 @@ * "echo", * { message: "Hello!" } * ); - * console.log(response.data()); + * console.log(response.data); * * await client.close(); * ``` @@ -68,7 +68,7 @@ * await using client = createGrpcClient({ url: "http://localhost:50051" }); * * const res = await client.call("echo.EchoService", "echo", { message: "test" }); - * console.log(res.data()); + * console.log(res.data); * // Client automatically closed when block exits * ``` * @@ -154,7 +154,7 @@ export interface GrpcClientConfig * "echo", * { message: "Hello!" } * ); - * console.log(response.data()); + * console.log(response.data); * * await client.close(); * ``` @@ -207,7 +207,7 @@ export interface GrpcClientConfig * }); * * const res = await client.call("echo.EchoService", "echo", { message: "test" }); - * console.log(res.data()); + * console.log(res.data); * // Client automatically closed when scope exits * ``` */ diff --git a/packages/probitas-client-http/body.ts b/packages/probitas-client-http/body.ts index 7764790..1f0e8ef 100644 --- a/packages/probitas-client-http/body.ts +++ b/packages/probitas-client-http/body.ts @@ -8,8 +8,7 @@ export class HttpBody { readonly bytes: Uint8Array | null; readonly #headers: Headers | null; readonly #text: string | null; - readonly #json: unknown; - readonly #jsonError: Error | null; + readonly #json: unknown | null; constructor(bytes: Uint8Array | null, headers?: Headers | null) { this.bytes = bytes; @@ -18,21 +17,19 @@ export class HttpBody { // Decode text and parse JSON once during construction const text = bytes ? new TextDecoder().decode(bytes) : null; let json: unknown = null; - let jsonError: Error | null = null; if (text) { try { json = JSON.parse(text); - } catch (e) { - jsonError = e instanceof Error ? e : new Error(String(e)); + } catch { + // Ignore JSON parse errors } } this.#text = text; this.#json = json; - this.#jsonError = jsonError; } /** Get body as ArrayBuffer (null if no body) */ - arrayBuffer(): ArrayBuffer | null { + get arrayBuffer(): ArrayBuffer | null { if (this.bytes === null) { return null; } @@ -42,16 +39,16 @@ export class HttpBody { } /** Get body as Blob (null if no body) */ - blob(): Blob | null { + get blob(): Blob | null { if (this.bytes === null) { return null; } const contentType = this.#headers?.get("content-type") ?? ""; - return new Blob([this.arrayBuffer()!], { type: contentType }); + return new Blob([this.arrayBuffer!], { type: contentType }); } /** Get body as text (null if no body) */ - text(): string | null { + get text(): string | null { return this.#text; } @@ -59,11 +56,7 @@ export class HttpBody { * Get body as parsed JSON (null if no body). * @throws SyntaxError if body is not valid JSON */ - // deno-lint-ignore no-explicit-any - json(): T | null { - if (this.#jsonError) { - throw this.#jsonError; - } - return this.#json as T | null; + get json(): unknown { + return this.#json; } } diff --git a/packages/probitas-client-http/body_test.ts b/packages/probitas-client-http/body_test.ts index 14266a0..7edd558 100644 --- a/packages/probitas-client-http/body_test.ts +++ b/packages/probitas-client-http/body_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertInstanceOf, assertThrows } from "@std/assert"; +import { assertEquals, assertInstanceOf } from "@std/assert"; import { HttpBody } from "./body.ts"; Deno.test("HttpBody", async (t) => { @@ -13,48 +13,48 @@ Deno.test("HttpBody", async (t) => { assertEquals(body.bytes, null); }); - await t.step("text() returns body as string", () => { + await t.step("text returns body as string", () => { const body = new HttpBody(new TextEncoder().encode("hello world")); - assertEquals(body.text(), "hello world"); + assertEquals(body.text, "hello world"); }); - await t.step("text() returns null when no body", () => { + await t.step("text returns null when no body", () => { const body = new HttpBody(null); - assertEquals(body.text(), null); + assertEquals(body.text, null); }); - await t.step("text() can be called multiple times", () => { + await t.step("text can be accessed multiple times", () => { const body = new HttpBody(new TextEncoder().encode("hello")); - assertEquals(body.text(), "hello"); - assertEquals(body.text(), "hello"); - assertEquals(body.text(), "hello"); + assertEquals(body.text, "hello"); + assertEquals(body.text, "hello"); + assertEquals(body.text, "hello"); }); - await t.step("json() returns parsed JSON", () => { + await t.step("json returns parsed JSON", () => { const data = { name: "John", age: 30 }; const body = new HttpBody(new TextEncoder().encode(JSON.stringify(data))); - assertEquals(body.json(), data); + assertEquals(body.json, data); }); - await t.step("json() returns null when no body", () => { + await t.step("json returns null when no body", () => { const body = new HttpBody(null); - assertEquals(body.json(), null); + assertEquals(body.json, null); }); - await t.step("json() throws when body is not valid JSON", () => { + await t.step("json returns null when body is not valid JSON", () => { const body = new HttpBody(new TextEncoder().encode("not json")); - assertThrows(() => body.json(), SyntaxError); + assertEquals(body.json, null); }); - await t.step("json() can be called multiple times", () => { + await t.step("json can be accessed multiple times", () => { const data = { id: 1 }; const body = new HttpBody(new TextEncoder().encode(JSON.stringify(data))); - assertEquals(body.json(), data); - assertEquals(body.json(), data); - assertEquals(body.json(), data); + assertEquals(body.json, data); + assertEquals(body.json, data); + assertEquals(body.json, data); }); - await t.step("json() supports generic type hint", () => { + await t.step("json supports type assertion", () => { interface User { id: number; name: string; @@ -62,47 +62,47 @@ Deno.test("HttpBody", async (t) => { const body = new HttpBody( new TextEncoder().encode(JSON.stringify({ id: 1, name: "Alice" })), ); - const user = body.json(); + const user = body.json as User | null; assertEquals(user?.id, 1); assertEquals(user?.name, "Alice"); }); - await t.step("arrayBuffer() returns ArrayBuffer", () => { + await t.step("arrayBuffer returns ArrayBuffer", () => { const bytes = new Uint8Array([1, 2, 3, 4, 5]); const body = new HttpBody(bytes); - const buffer = body.arrayBuffer(); + const buffer = body.arrayBuffer; assertInstanceOf(buffer, ArrayBuffer); assertEquals(new Uint8Array(buffer!), bytes); }); - await t.step("arrayBuffer() returns null when no body", () => { + await t.step("arrayBuffer returns null when no body", () => { const body = new HttpBody(null); - assertEquals(body.arrayBuffer(), null); + assertEquals(body.arrayBuffer, null); }); - await t.step("blob() returns Blob", async () => { + await t.step("blob returns Blob", async () => { const body = new HttpBody(new TextEncoder().encode("hello")); - const blob = body.blob(); + const blob = body.blob; assertInstanceOf(blob, Blob); assertEquals(await blob!.text(), "hello"); }); await t.step( - "blob() returns Blob with content type from headers", + "blob returns Blob with content type from headers", async () => { const headers = new Headers({ "content-type": "application/json" }); const body = new HttpBody( new TextEncoder().encode('{"a":1}'), headers, ); - const blob = body.blob(); + const blob = body.blob; assertEquals(blob!.type, "application/json"); assertEquals(await blob!.text(), '{"a":1}'); }, ); - await t.step("blob() returns null when no body", () => { + await t.step("blob returns null when no body", () => { const body = new HttpBody(null); - assertEquals(body.blob(), null); + assertEquals(body.blob, null); }); }); diff --git a/packages/probitas-client-http/client.ts b/packages/probitas-client-http/client.ts index ffaea6c..b49315c 100644 --- a/packages/probitas-client-http/client.ts +++ b/packages/probitas-client-http/client.ts @@ -394,7 +394,7 @@ class HttpClientImpl implements HttpClient { * const http = createHttpClient({ url: "http://localhost:3000" }); * * const response = await http.get("/users/123"); - * console.log(response.json()); + * console.log(response.json); * * await http.close(); * ``` diff --git a/packages/probitas-client-http/client_test.ts b/packages/probitas-client-http/client_test.ts index 0979fae..9413c29 100644 --- a/packages/probitas-client-http/client_test.ts +++ b/packages/probitas-client-http/client_test.ts @@ -75,7 +75,7 @@ Deno.test("HttpClient.get", async (t) => { assert(response.ok); assertEquals(response.status, 200); - assertEquals(response.json(), { id: 1, name: "John" }); + assertEquals(response.json, { id: 1, name: "John" }); }); await t.step("includes query parameters", async () => { @@ -938,11 +938,11 @@ Deno.test("HttpClient network failure handling", async (t) => { await client.close(); assert(!response.processed); - assertEquals(response.raw(), null); - assertEquals(response.arrayBuffer(), null); - assertEquals(response.blob(), null); - assertEquals(response.text(), null); - assertEquals(response.json(), null); + assertEquals(response.raw, null); + assertEquals(response.arrayBuffer, null); + assertEquals(response.blob, null); + assertEquals(response.text, null); + assertEquals(response.json, null); }); await t.step( diff --git a/packages/probitas-client-http/errors.ts b/packages/probitas-client-http/errors.ts index 71f7d11..9aba4cc 100644 --- a/packages/probitas-client-http/errors.ts +++ b/packages/probitas-client-http/errors.ts @@ -10,14 +10,14 @@ function formatErrorMessage( statusText: string, body: HttpBody, ): string { - const text = body.text(); + const text = body.text; if (text === null) { return `${status}: ${statusText}`; } // Try to parse as JSON for pretty-printing, otherwise use text as-is let detail: string; try { - const json = body.json(); + const json = body.json; detail = Deno.inspect(json, { compact: false, sorted: true, @@ -59,7 +59,7 @@ export interface HttpErrorOptions extends ErrorOptions { * await http.get("/not-found"); * } catch (error) { * if (error instanceof HttpError && error.status === 404) { - * console.log("Not found:", error.text()); + * console.log("Not found:", error.text); * } * } * ``` @@ -100,28 +100,26 @@ export class HttpError extends ClientError { } /** Get body as ArrayBuffer (null if no body) */ - arrayBuffer(): ArrayBuffer | null { - return this.#body.arrayBuffer(); + get arrayBuffer(): ArrayBuffer | null { + return this.#body.arrayBuffer; } /** Get body as Blob (null if no body) */ - blob(): Blob | null { - return this.#body.blob(); + get blob(): Blob | null { + return this.#body.blob; } /** Get body as text (null if no body) */ - text(): string | null { - return this.#body.text(); + get text(): string | null { + return this.#body.text; } /** * Get body as parsed JSON (null if no body). - * @template T - defaults to any for test convenience * @throws SyntaxError if body is not valid JSON */ - // deno-lint-ignore no-explicit-any - json(): T | null { - return this.#body.json(); + get json(): unknown { + return this.#body.json; } } diff --git a/packages/probitas-client-http/errors_test.ts b/packages/probitas-client-http/errors_test.ts index d8a2d02..e6d1b50 100644 --- a/packages/probitas-client-http/errors_test.ts +++ b/packages/probitas-client-http/errors_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertInstanceOf, assertThrows } from "@std/assert"; +import { assertEquals, assertInstanceOf } from "@std/assert"; import { ClientError } from "@probitas/client"; import { HttpError } from "./errors.ts"; @@ -56,83 +56,83 @@ Deno.test("HttpError", async (t) => { await t.step("text() returns body as string", () => { const body = new TextEncoder().encode("error message"); const error = new HttpError(500, "Internal Server Error", { body }); - assertEquals(error.text(), "error message"); + assertEquals(error.text, "error message"); }); await t.step("text() returns null when no body", () => { const error = new HttpError(500, "Internal Server Error"); - assertEquals(error.text(), null); + assertEquals(error.text, null); }); await t.step("text() can be called multiple times", () => { const body = new TextEncoder().encode("error message"); const error = new HttpError(500, "Internal Server Error", { body }); - assertEquals(error.text(), error.text()); + assertEquals(error.text, error.text); }); - await t.step("json() returns parsed JSON", () => { + await t.step("json returns parsed JSON", () => { const body = new TextEncoder().encode('{"code": "NOT_FOUND", "id": 123}'); const error = new HttpError(404, "Not Found", { body }); - assertEquals(error.json(), { code: "NOT_FOUND", id: 123 }); + assertEquals(error.json, { code: "NOT_FOUND", id: 123 }); }); - await t.step("json() returns null when no body", () => { + await t.step("json returns null when no body", () => { const error = new HttpError(500, "Internal Server Error"); - assertEquals(error.json(), null); + assertEquals(error.json, null); }); - await t.step("json() throws when body is not valid JSON", () => { + await t.step("json returns null when body is not valid JSON", () => { const body = new TextEncoder().encode("not json"); const error = new HttpError(500, "Internal Server Error", { body }); - assertThrows(() => error.json(), SyntaxError); + assertEquals(error.json, null); }); - await t.step("json() can be called multiple times", () => { + await t.step("json can be accessed multiple times", () => { const body = new TextEncoder().encode('{"error": true}'); const error = new HttpError(500, "Internal Server Error", { body }); - assertEquals(error.json(), error.json()); + assertEquals(error.json, error.json); }); - await t.step("json() supports generic type hint", () => { + await t.step("json supports type assertion", () => { interface ErrorResponse { code: string; message: string; } const body = new TextEncoder().encode('{"code": "ERR", "message": "fail"}'); const error = new HttpError(500, "Internal Server Error", { body }); - const data = error.json(); + const data = error.json as ErrorResponse; assertEquals(data?.code, "ERR"); assertEquals(data?.message, "fail"); }); - await t.step("arrayBuffer() returns ArrayBuffer", () => { + await t.step("arrayBuffer returns ArrayBuffer", () => { const body = new TextEncoder().encode("data"); const error = new HttpError(500, "Internal Server Error", { body }); - const buffer = error.arrayBuffer(); + const buffer = error.arrayBuffer; assertInstanceOf(buffer, ArrayBuffer); assertEquals(buffer?.byteLength, 4); }); - await t.step("arrayBuffer() returns null when no body", () => { + await t.step("arrayBuffer returns null when no body", () => { const error = new HttpError(500, "Internal Server Error"); - assertEquals(error.arrayBuffer(), null); + assertEquals(error.arrayBuffer, null); }); - await t.step("blob() returns Blob with content type", () => { + await t.step("blob returns Blob with content type", () => { const body = new TextEncoder().encode('{"test": true}'); const headers = new Headers({ "Content-Type": "application/json" }); const error = new HttpError(500, "Internal Server Error", { body, headers, }); - const blob = error.blob(); + const blob = error.blob; assertInstanceOf(blob, Blob); assertEquals(blob?.type, "application/json"); assertEquals(blob?.size, body.length); }); - await t.step("blob() returns null when no body", () => { + await t.step("blob returns null when no body", () => { const error = new HttpError(500, "Internal Server Error"); - assertEquals(error.blob(), null); + assertEquals(error.blob, null); }); }); diff --git a/packages/probitas-client-http/integration_test.ts b/packages/probitas-client-http/integration_test.ts index 2d3e772..00940c8 100644 --- a/packages/probitas-client-http/integration_test.ts +++ b/packages/probitas-client-http/integration_test.ts @@ -46,11 +46,11 @@ Deno.test({ assertEquals(res.status, 200); assert(res.headers.get("content-type")?.match(/^application\/json/)); - const data = res.json<{ + const data = res.json as { args: Record; headers: Record; url: string; - }>(); + } | null; assertEquals(data?.args.foo, "bar"); assertEquals(data?.args.num, "42"); @@ -66,10 +66,10 @@ Deno.test({ assertEquals(res.status, 200); assert(res.headers.get("content-type")?.match(/^application\/json/)); - const data = res.json<{ + const data = res.json as { json: typeof payload; headers: Record; - }>(); + } | null; assertEquals(data?.json, payload); assertEquals(data?.headers["Content-Type"], "application/json"); @@ -85,7 +85,7 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - const data = res.json<{ form: Record }>(); + const data = res.json as { form: Record } | null; assertEquals(data?.form.username, "alice"); assertEquals(data?.form.password, "secret"); }); @@ -96,7 +96,7 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - const data = res.json<{ json: { updated: boolean } }>(); + const data = res.json as { json: { updated: boolean } }; assertEquals(data?.json.updated, true); }); @@ -106,7 +106,7 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - const data = res.json<{ json: { patched: string } }>(); + const data = res.json as { json: { patched: string } }; assertEquals(data?.json.patched, "value"); }); @@ -151,7 +151,7 @@ Deno.test({ assert(res.ok); - const data = res.json<{ headers: Record }>(); + const data = res.json as { headers: Record } | null; // Verify Accept header was sent (echo-http echoes back headers) assertEquals(data?.headers["Accept"], "application/json"); }); @@ -172,13 +172,13 @@ Deno.test({ const res = await client.get("/get"); // Read as text - const text1 = res.text(); - const text2 = res.text(); + const text1 = res.text; + const text2 = res.text; assertEquals(text1, text2); // Read as JSON - const json1 = res.json(); - const json2 = res.json(); + const json1 = res.json; + const json2 = res.json; assertEquals(json1, json2); // Body bytes are also available @@ -195,7 +195,7 @@ Deno.test({ }); const res = await clientWithHeaders.get("/headers"); - const data = res.json<{ headers: Record }>(); + const data = res.json as { headers: Record } | null; assertEquals(data?.headers["Authorization"], "Bearer token123"); assertEquals(data?.headers["X-Api-Version"], "v1"); @@ -212,7 +212,7 @@ Deno.test({ const res = await clientWithHeaders.get("/headers", { headers: { "X-Header": "from-request" }, }); - const data = res.json<{ headers: Record }>(); + const data = res.json as { headers: Record } | null; assertEquals(data?.headers["X-Header"], "from-request"); @@ -225,7 +225,7 @@ Deno.test({ assert(res.ok); assertEquals(res.status, 200); - const data = res.json<{ redirected: boolean }>(); + const data = res.json as { redirected: boolean }; assertEquals(data?.redirected, true); }); @@ -301,7 +301,7 @@ Deno.test({ const res = await client.get("/cookies"); assert(res.ok); - const data = res.json<{ cookies: Record }>(); + const data = res.json as { cookies: Record } | null; assertEquals(data?.cookies.session, "test123"); assertEquals(data?.cookies.user, "alice"); @@ -343,7 +343,7 @@ Deno.test({ // Second request should send the cookie back const res = await client.get("/cookies"); - const data = res.json<{ cookies: Record }>(); + const data = res.json as { cookies: Record } | null; assertEquals(data?.cookies.auth, "bearer-token"); await client.close(); @@ -357,7 +357,7 @@ Deno.test({ // Verify initial cookie is sent let res = await client.get("/cookies"); - let data = res.json<{ cookies: Record }>(); + let data = res.json as { cookies: Record } | null; assertEquals(data?.cookies.initial, "value"); // Clear cookies @@ -365,7 +365,7 @@ Deno.test({ // Verify no cookies are sent res = await client.get("/cookies"); - data = res.json<{ cookies: Record }>(); + data = res.json as { cookies: Record } | null; assertEquals(data?.cookies.initial, undefined); await client.close(); @@ -381,7 +381,7 @@ Deno.test({ // Verify it's sent const res = await client.get("/cookies"); - const data = res.json<{ cookies: Record }>(); + const data = res.json as { cookies: Record } | null; assertEquals(data?.cookies.manual, "cookie-value"); await client.close(); diff --git a/packages/probitas-client-http/mod.ts b/packages/probitas-client-http/mod.ts index 5f78c01..e39eed4 100644 --- a/packages/probitas-client-http/mod.ts +++ b/packages/probitas-client-http/mod.ts @@ -34,7 +34,7 @@ * console.log("Status:", res.status); * * // Extract typed data - * const user = res.json(); + * const user = res.json as User; * * // POST request * const created = await http.post("/users", { diff --git a/packages/probitas-client-http/response.ts b/packages/probitas-client-http/response.ts index 46f3013..e8fae19 100644 --- a/packages/probitas-client-http/response.ts +++ b/packages/probitas-client-http/response.ts @@ -7,7 +7,8 @@ import { HttpError, type HttpFailureError } from "./errors.ts"; * * Provides common properties and methods shared by Success, Error, and Failure responses. */ -interface HttpResponseBase extends ClientResult { +// deno-lint-ignore no-explicit-any +interface HttpResponseBase extends ClientResult { /** Result kind discriminator. Always `"http"` for HTTP responses. */ readonly kind: "http"; @@ -30,26 +31,24 @@ interface HttpResponseBase extends ClientResult { readonly body: Uint8Array | null; /** Get body as ArrayBuffer (null if no body or failure). */ - arrayBuffer(): ArrayBuffer | null; + readonly arrayBuffer: ArrayBuffer | null; /** Get body as Blob (null if no body or failure). */ - blob(): Blob | null; + readonly blob: Blob | null; /** * Get body as text (null if no body or failure). */ - text(): string | null; + readonly text: string | null; /** * Get body as parsed JSON (null if no body or failure). - * @template T - defaults to any for test convenience * @throws SyntaxError if body is not valid JSON */ - // deno-lint-ignore no-explicit-any - json(): T | null; + readonly json: T | null; - /** Get raw Web standard Response (null for failure). */ - raw(): globalThis.Response | null; + /** Raw Web standard Response (null for failure). */ + readonly raw: globalThis.Response | null; } /** @@ -58,7 +57,8 @@ interface HttpResponseBase extends ClientResult { * Wraps Web standard Response, allowing body to be read synchronously * and multiple times (unlike the streaming-based standard Response). */ -export interface HttpResponseSuccess extends HttpResponseBase { +// deno-lint-ignore no-explicit-any +export interface HttpResponseSuccess extends HttpResponseBase { /** Server processed the request. */ readonly processed: true; @@ -77,8 +77,8 @@ export interface HttpResponseSuccess extends HttpResponseBase { /** Response headers. */ readonly headers: Headers; - /** Get raw Web standard Response. */ - raw(): globalThis.Response; + /** Raw Web standard Response. */ + readonly raw: globalThis.Response; } /** @@ -86,7 +86,8 @@ export interface HttpResponseSuccess extends HttpResponseBase { * * Server received and processed the request, but returned an error status. */ -export interface HttpResponseError extends HttpResponseBase { +// deno-lint-ignore no-explicit-any +export interface HttpResponseError extends HttpResponseBase { /** Server processed the request. */ readonly processed: true; @@ -105,8 +106,8 @@ export interface HttpResponseError extends HttpResponseBase { /** Response headers. */ readonly headers: Headers; - /** Get raw Web standard Response. */ - raw(): globalThis.Response; + /** Raw Web standard Response. */ + readonly raw: globalThis.Response; } /** @@ -115,7 +116,8 @@ export interface HttpResponseError extends HttpResponseBase { * Request could not be processed by the server (network error, DNS failure, * connection refused, timeout, aborted, etc.). */ -export interface HttpResponseFailure extends HttpResponseBase { +// deno-lint-ignore no-explicit-any +export interface HttpResponseFailure extends HttpResponseBase { /** Server did not process the request. */ readonly processed: false; @@ -138,7 +140,7 @@ export interface HttpResponseFailure extends HttpResponseBase { readonly body: null; /** No raw response (request didn't reach server). */ - raw(): null; + readonly raw: null; } /** @@ -178,16 +180,19 @@ export interface HttpResponseFailure extends HttpResponseBase { * } * ``` */ -export type HttpResponse = - | HttpResponseSuccess - | HttpResponseError - | HttpResponseFailure; +// deno-lint-ignore no-explicit-any +export type HttpResponse = + | HttpResponseSuccess + | HttpResponseError + | HttpResponseFailure; /** * Implementation of HttpResponseSuccess. * @internal */ -export class HttpResponseSuccessImpl implements HttpResponseSuccess { +// deno-lint-ignore no-explicit-any +export class HttpResponseSuccessImpl + implements HttpResponseSuccess { readonly kind = "http" as const; readonly processed = true as const; readonly ok = true as const; @@ -219,24 +224,23 @@ export class HttpResponseSuccessImpl implements HttpResponseSuccess { return this.#body.bytes; } - arrayBuffer(): ArrayBuffer | null { - return this.#body.arrayBuffer(); + get arrayBuffer(): ArrayBuffer | null { + return this.#body.arrayBuffer; } - blob(): Blob | null { - return this.#body.blob(); + get blob(): Blob | null { + return this.#body.blob; } - text(): string | null { - return this.#body.text(); + get text(): string | null { + return this.#body.text; } - // deno-lint-ignore no-explicit-any - json(): T | null { - return this.#body.json(); + get json(): T | null { + return this.#body.json as T | null; } - raw(): globalThis.Response { + get raw(): globalThis.Response { return this.#raw; } } @@ -246,7 +250,8 @@ export class HttpResponseSuccessImpl implements HttpResponseSuccess { * Proxies body methods to the HttpError instance. * @internal */ -export class HttpResponseErrorImpl implements HttpResponseError { +// deno-lint-ignore no-explicit-any +export class HttpResponseErrorImpl implements HttpResponseError { readonly kind = "http" as const; readonly processed = true as const; readonly ok = false as const; @@ -278,24 +283,23 @@ export class HttpResponseErrorImpl implements HttpResponseError { return this.error.body; } - arrayBuffer(): ArrayBuffer | null { - return this.error.arrayBuffer(); + get arrayBuffer(): ArrayBuffer | null { + return this.error.arrayBuffer; } - blob(): Blob | null { - return this.error.blob(); + get blob(): Blob | null { + return this.error.blob; } - text(): string | null { - return this.error.text(); + get text(): string | null { + return this.error.text; } - // deno-lint-ignore no-explicit-any - json(): T | null { - return this.error.json(); + get json(): T | null { + return this.error.json as T | null; } - raw(): globalThis.Response { + get raw(): globalThis.Response { return this.#raw; } } @@ -304,7 +308,9 @@ export class HttpResponseErrorImpl implements HttpResponseError { * Implementation of HttpResponseFailure. * @internal */ -export class HttpResponseFailureImpl implements HttpResponseFailure { +// deno-lint-ignore no-explicit-any +export class HttpResponseFailureImpl + implements HttpResponseFailure { readonly kind = "http" as const; readonly processed = false as const; readonly ok = false as const; @@ -316,30 +322,15 @@ export class HttpResponseFailureImpl implements HttpResponseFailure { readonly body = null; readonly duration: number; + readonly arrayBuffer = null; + readonly blob = null; + readonly text = null; + readonly json = null; + readonly raw = null; + constructor(url: string, duration: number, error: HttpFailureError) { this.url = url; this.duration = duration; this.error = error; } - - arrayBuffer(): null { - return null; - } - - blob(): null { - return null; - } - - text(): null { - return null; - } - - // deno-lint-ignore no-explicit-any - json(): T | null { - return null; - } - - raw(): null { - return null; - } } diff --git a/packages/probitas-client-http/response_test.ts b/packages/probitas-client-http/response_test.ts index f58d2e3..df0cd4b 100644 --- a/packages/probitas-client-http/response_test.ts +++ b/packages/probitas-client-http/response_test.ts @@ -88,51 +88,51 @@ Deno.test("HttpResponseSuccessImpl", async (t) => { const raw = new Response("hello world", { status: 200 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.text(), "hello world"); + assertEquals(response.text, "hello world"); }); await t.step("text() returns null when no body", async () => { const raw = new Response(null, { status: 204 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.text(), null); + assertEquals(response.text, null); }); await t.step("text() can be called multiple times", async () => { const raw = new Response("hello", { status: 200 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.text(), "hello"); - assertEquals(response.text(), "hello"); - assertEquals(response.text(), "hello"); + assertEquals(response.text, "hello"); + assertEquals(response.text, "hello"); + assertEquals(response.text, "hello"); }); - await t.step("json() returns parsed JSON", async () => { + await t.step("json returns parsed JSON", async () => { const data = { name: "John", age: 30 }; const raw = new Response(JSON.stringify(data), { status: 200 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.json(), data); + assertEquals(response.json, data); }); - await t.step("json() returns null when no body", async () => { + await t.step("json returns null when no body", async () => { const raw = new Response(null, { status: 204 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.json(), null); + assertEquals(response.json, null); }); - await t.step("json() can be called multiple times", async () => { + await t.step("json can be accessed multiple times", async () => { const data = { id: 1 }; const raw = new Response(JSON.stringify(data), { status: 200 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.json(), data); - assertEquals(response.json(), data); - assertEquals(response.json(), data); + assertEquals(response.json, data); + assertEquals(response.json, data); + assertEquals(response.json, data); }); - await t.step("json() supports generic type hint", async () => { + await t.step("json supports type assertion", async () => { interface User { id: number; name: string; @@ -142,52 +142,52 @@ Deno.test("HttpResponseSuccessImpl", async (t) => { }); const response = await createSuccessResponse(raw, 10); - const user = response.json(); + const user = response.json as User; assertEquals(user?.id, 1); assertEquals(user?.name, "Alice"); }); - await t.step("arrayBuffer() returns ArrayBuffer", async () => { + await t.step("arrayBuffer returns ArrayBuffer", async () => { const bytes = new Uint8Array([1, 2, 3, 4, 5]); const raw = new Response(bytes, { status: 200 }); const response = await createSuccessResponse(raw, 10); - const buffer = response.arrayBuffer(); + const buffer = response.arrayBuffer; assertInstanceOf(buffer, ArrayBuffer); assertEquals(new Uint8Array(buffer!), bytes); }); - await t.step("arrayBuffer() returns null when no body", async () => { + await t.step("arrayBuffer returns null when no body", async () => { const raw = new Response(null, { status: 204 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.arrayBuffer(), null); + assertEquals(response.arrayBuffer, null); }); - await t.step("blob() returns Blob", async () => { + await t.step("blob returns Blob", async () => { const raw = new Response("hello", { status: 200, headers: { "Content-Type": "text/plain" }, }); const response = await createSuccessResponse(raw, 10); - const blob = response.blob(); + const blob = response.blob; assertInstanceOf(blob, Blob); assertEquals(await blob!.text(), "hello"); }); - await t.step("blob() returns null when no body", async () => { + await t.step("blob returns null when no body", async () => { const raw = new Response(null, { status: 204 }); const response = await createSuccessResponse(raw, 10); - assertEquals(response.blob(), null); + assertEquals(response.blob, null); }); - await t.step("raw() method returns original Response", async () => { + await t.step("raw returns original Response", async () => { const original = new Response("test", { status: 200 }); const response = await createSuccessResponse(original, 10); - assertEquals(response.raw(), original); + assertEquals(response.raw, original); }); await t.step("url property reflects Response url", async () => { @@ -235,15 +235,15 @@ Deno.test("HttpResponseErrorImpl", async (t) => { }); const response = await createErrorResponse(raw, 10); - assertEquals(response.text(), errorBody); - assertEquals(response.json(), { error: "not found" }); + assertEquals(response.text, errorBody); + assertEquals(response.json, { error: "not found" }); assertInstanceOf(response.body, Uint8Array); }); - await t.step("raw() returns original Response", async () => { + await t.step("raw returns original Response", async () => { const original = new Response("error", { status: 500 }); const response = await createErrorResponse(original, 10); - assertEquals(response.raw(), original); + assertEquals(response.raw, original); }); });