Skip to content

Commit 2cc45b1

Browse files
committed
feat(truapi-codegen): emit Rust dispatcher, wire table, and host callbacks
Extends the rustdoc-JSON code generator to emit the Rust dispatcher and wire table consumed by truapi-server, plus the TS host-callbacks adapter. Golden tests pin the emitted shapes.
1 parent 1a7d349 commit 2cc45b1

24 files changed

Lines changed: 11097 additions & 75 deletions

Cargo.lock

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[workspace]
22
resolver = "2"
33
members = ["rust/crates/*"]
4+
exclude = ["rust/crates/truapi-server"]
45

56
[workspace.package]
67
edition = "2024"

js/packages/truapi/src/client.test.ts

Lines changed: 128 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ import type { Result } from "neverthrow";
22
import { describe, expect, it } from "bun:test";
33

44
import { createTransport } from "./client.js";
5-
import { indexedTaggedUnion, Result as ScaleResult, str, _void } from "./scale.js";
5+
import { CallError, indexedTaggedUnion, Result as ScaleResult, str, _void } from "./scale.js";
6+
import type { Codec } from "./scale.js";
67
import { createClient, SubscriptionError } from "./generated/client.js";
78
import * as T from "./generated/types.js";
89
import * as W from "./generated/wire-table.js";
910
import { encodeWireMessage } from "./transport.js";
1011

12+
/** Wrap a codec in the `{ V1: [0, codec] }` indexed-tagged-union envelope. */
13+
const versionedV1 = <T>(codec: Codec<T>) => indexedTaggedUnion({ V1: [0, codec] });
14+
1115
function toHex(u: Uint8Array): string {
1216
return Array.from(u)
1317
.map((b) => b.toString(16).padStart(2, "0"))
@@ -56,9 +60,34 @@ function providerFixture() {
5660

5761
/** Encode a V1 host-handshake response result payload. */
5862
function handshakeResponsePayload(value: { success: true; value: undefined }): Uint8Array {
59-
return indexedTaggedUnion({
60-
V1: [0, ScaleResult(_void, T.HostHandshakeError)],
61-
}).enc({ tag: "V1", value });
63+
return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({
64+
tag: "V1",
65+
value,
66+
});
67+
}
68+
69+
function accountGetResponsePayload(
70+
value:
71+
| {
72+
success: true;
73+
value: T.HostAccountGetResponse;
74+
}
75+
| {
76+
success: false;
77+
value: { tag: "Domain"; value: T.VersionedHostAccountGetError };
78+
},
79+
): Uint8Array {
80+
return versionedV1(
81+
ScaleResult(T.HostAccountGetResponse, CallError(T.VersionedHostAccountGetError)),
82+
).enc({ tag: "V1", value });
83+
}
84+
85+
/** Encode a raw testing echo error response payload. */
86+
function testingEchoErrorPayload(reason: string): Uint8Array {
87+
return ScaleResult(_void, CallError(T.V01TestingVersionProbeError)).enc({
88+
success: false,
89+
value: { tag: "HostFailure", value: { reason } },
90+
});
6291
}
6392

6493
describe("generated client transport", () => {
@@ -88,6 +117,29 @@ describe("generated client transport", () => {
88117
expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame));
89118
});
90119

120+
it("uses the latest generated request version for testing probes", () => {
121+
const fixture = providerFixture();
122+
const transport = createTransport(fixture.provider);
123+
const client = createClient(transport);
124+
125+
const request = {
126+
message: "hello from test",
127+
marker: 42,
128+
};
129+
void client.testing.versionProbe(request);
130+
131+
const expectedPayload = T.VersionedTestingVersionProbeRequest.enc({
132+
tag: "V2",
133+
value: request,
134+
});
135+
const expectedFrame = new Uint8Array(str.enc("p:1").length + 1 + expectedPayload.length);
136+
expectedFrame.set(str.enc("p:1"), 0);
137+
expectedFrame[str.enc("p:1").length] = W.TESTING_VERSION_PROBE.request;
138+
expectedFrame.set(expectedPayload, str.enc("p:1").length + 1);
139+
140+
expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame));
141+
});
142+
91143
it("uses the transport codec version for generated handshake calls", () => {
92144
const fixture = providerFixture();
93145
const transport = createTransport(fixture.provider);
@@ -129,6 +181,63 @@ describe("generated client transport", () => {
129181
expect(result.isOk()).toBe(true);
130182
});
131183

184+
it("decodes request domain errors from the versioned response envelope", async () => {
185+
const fixture = providerFixture();
186+
const transport = createTransport(fixture.provider);
187+
const client = createClient(transport);
188+
189+
const response = client.account.getAccount({
190+
productAccountId: { dotNsIdentifier: "foo", derivationIndex: 0 },
191+
});
192+
const reason = { tag: "V1", value: { tag: "NotConnected", value: undefined } } as const;
193+
const frame = unwrap(
194+
encodeWireMessage({
195+
requestId: "p:1",
196+
payload: {
197+
id: W.ACCOUNT_GET_ACCOUNT.response,
198+
value: accountGetResponsePayload({
199+
success: false,
200+
value: { tag: "Domain", value: reason },
201+
}),
202+
},
203+
}),
204+
"encode account_get error response",
205+
);
206+
fixture.receive(frame);
207+
208+
const result = await response;
209+
expect(result.isErr()).toBe(true);
210+
expect(result._unsafeUnwrapErr()).toEqual({ tag: "Domain", value: reason });
211+
});
212+
213+
it("returns framework call errors as typed Err values", async () => {
214+
const fixture = providerFixture();
215+
const transport = createTransport(fixture.provider);
216+
const client = createClient(transport);
217+
218+
const response = client.testing.echoError({
219+
error: { tag: "HostFailure", value: { reason: "forced by test" } },
220+
});
221+
const frame = unwrap(
222+
encodeWireMessage({
223+
requestId: "p:1",
224+
payload: {
225+
id: W.TESTING_ECHO_ERROR.response,
226+
value: testingEchoErrorPayload("forced by test"),
227+
},
228+
}),
229+
"encode testing framework error response",
230+
);
231+
fixture.receive(frame);
232+
233+
const result = await response;
234+
expect(result.isErr()).toBe(true);
235+
expect(result._unsafeUnwrapErr()).toEqual({
236+
tag: "HostFailure",
237+
value: { reason: "forced by test" },
238+
});
239+
});
240+
132241
it("auto-responds to an inbound handshake with the versioned-result shape", () => {
133242
const fixture = providerFixture();
134243
createTransport(fixture.provider);
@@ -225,14 +334,18 @@ describe("generated client transport", () => {
225334
});
226335

227336
const reason = { tag: "PermissionDenied", value: undefined } as const;
337+
const callError = {
338+
tag: "Domain",
339+
value: { tag: "V1", value: reason },
340+
} as const;
228341
const frame = unwrap(
229342
encodeWireMessage({
230343
requestId: sub.subscriptionId,
231344
payload: {
232345
id: W.PAYMENT_BALANCE_SUBSCRIBE.interrupt,
233-
value: T.VersionedHostPaymentBalanceSubscribeError.enc({
346+
value: versionedV1(CallError(T.VersionedHostPaymentBalanceSubscribeError)).enc({
234347
tag: "V1",
235-
value: reason,
348+
value: callError,
236349
}),
237350
},
238351
}),
@@ -243,7 +356,7 @@ describe("generated client transport", () => {
243356
expect(completions).toEqual([]);
244357
expect(errors).toHaveLength(1);
245358
expect(errors[0]).toBeInstanceOf(SubscriptionError);
246-
expect((errors[0] as SubscriptionError).reason).toEqual(reason);
359+
expect((errors[0] as SubscriptionError).reason).toEqual(callError);
247360
expect(fixture.sent).toHaveLength(1);
248361
});
249362

@@ -258,15 +371,18 @@ describe("generated client transport", () => {
258371
.subscribe({ error: (error) => errors.push(error) });
259372

260373
const reason = "Denied";
374+
const callError = {
375+
tag: "Domain",
376+
value: { tag: "V1", value: reason },
377+
} as const;
261378
const frame = unwrap(
262379
encodeWireMessage({
263380
requestId: sub.subscriptionId,
264381
payload: {
265382
id: W.COIN_PAYMENT_REBALANCE_PURSE.interrupt,
266-
value: T.VersionedHostCoinPaymentRebalancePurseError.enc({
267-
tag: "V1",
268-
value: reason,
269-
}),
383+
value: versionedV1(
384+
CallError(T.VersionedHostCoinPaymentRebalancePurseError),
385+
).enc({ tag: "V1", value: callError }),
270386
},
271387
}),
272388
"encode typed coin payment interrupt",
@@ -275,7 +391,7 @@ describe("generated client transport", () => {
275391

276392
expect(errors).toHaveLength(1);
277393
expect(errors[0]).toBeInstanceOf(SubscriptionError);
278-
expect((errors[0] as SubscriptionError).reason).toEqual(reason);
394+
expect((errors[0] as SubscriptionError).reason).toEqual(callError);
279395
});
280396

281397
it("treats a malformed receive payload as terminal and sends _stop", () => {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Mismatch dumps written by tests/golden_rust_emit.rs for local inspection.
2+
tests/golden/*.actual

rust/crates/truapi-codegen/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ anyhow = "1"
1717
clap = { version = "4", features = ["derive"] }
1818
indoc = "2"
1919
convert_case = "0.6"
20+
21+
[dev-dependencies]
22+
tempfile = "3"

0 commit comments

Comments
 (0)