Skip to content

Commit 8e0e71f

Browse files
committed
feat(truapi): add testing API and versioned wiring
Adds the canonical testing module (api/testing.rs) and its v01/v02/versioned wiring used by the Rust host runtime and generated clients.
1 parent b6291db commit 8e0e71f

15 files changed

Lines changed: 292 additions & 55 deletions

File tree

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,6 @@ describe("generated client transport", () => {
8686
expectedFrame.set(expectedPayload, str.enc("p:1").length + 1);
8787

8888
expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame));
89-
expect(transport.truapiVersion).toBe(1);
90-
expect(transport.codecVersion).toBe(1);
9189
});
9290

9391
it("uses the transport codec version for generated handshake calls", () => {

js/packages/truapi/src/scale.ts

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import {
99
Bytes,
1010
Enum,
1111
Struct,
12-
_void,
1312
createCodec,
1413
createDecoder,
1514
enhanceCodec,
16-
str as scaleStr,
15+
str,
1716
u8,
17+
_void,
1818
type Codec,
1919
} from "scale-ts";
2020
import {
@@ -123,49 +123,23 @@ export function TaggedUnion<O extends TaggedUnionCodecs>(
123123
return Enum(inner) as unknown as Codec<TaggedUnionValue<O>>;
124124
}
125125

126-
/**
127-
* Wire codec for Rust `CallError<D>`, projected to the public domain error `D`.
128-
*
129-
* Generated TypeScript APIs expose only the domain error union in
130-
* `ResultAsync<Ok, D>`. The Rust host still wraps that value in
131-
* `CallError::Domain` on the wire so framework errors can share the response
132-
* channel. Encoding always emits `Domain`; decoding returns the inner domain
133-
* value and throws for framework-level failures that have no public `D` shape.
134-
*/
135-
export function CallError<D>(domain: Codec<D>): Codec<D> {
136-
type WireCallError =
137-
| { tag: "Domain"; value: D }
138-
| { tag: "Denied"; value?: undefined }
139-
| { tag: "Unsupported"; value?: undefined }
140-
| { tag: "MalformedFrame"; value: { reason: string } }
141-
| { tag: "HostFailure"; value: { reason: string } };
126+
/** Public TS value for Rust's derived `CallError<D>` enum. */
127+
export type CallErrorValue<D> =
128+
| { tag: "Domain"; value: D }
129+
| { tag: "Denied"; value?: undefined }
130+
| { tag: "Unsupported"; value?: undefined }
131+
| { tag: "MalformedFrame"; value: { reason: string } }
132+
| { tag: "HostFailure"; value: { reason: string } };
142133

143-
const wire = Enum({
134+
/** SCALE codec for Rust's derived `CallError<D>` enum. */
135+
export function CallError<D>(domain: Codec<D>): Codec<CallErrorValue<D>> {
136+
return TaggedUnion({
144137
Domain: domain,
145138
Denied: _void,
146139
Unsupported: _void,
147-
MalformedFrame: Struct({ reason: scaleStr }),
148-
HostFailure: Struct({ reason: scaleStr }),
149-
}) as unknown as Codec<WireCallError>;
150-
151-
return enhanceCodec(
152-
wire,
153-
(value: D): WireCallError => ({ tag: "Domain", value }),
154-
(value: WireCallError): D => {
155-
switch (value.tag) {
156-
case "Domain":
157-
return value.value;
158-
case "Denied":
159-
throw new Error("Host denied the request");
160-
case "Unsupported":
161-
throw new Error("Host does not support this request");
162-
case "MalformedFrame":
163-
throw new Error(`Malformed request frame: ${value.value.reason}`);
164-
case "HostFailure":
165-
throw new Error(`Host failure: ${value.value.reason}`);
166-
}
167-
},
168-
);
140+
MalformedFrame: Struct({ reason: str }),
141+
HostFailure: Struct({ reason: str }),
142+
}) as Codec<CallErrorValue<D>>;
169143
}
170144

171145
type TaggedUnionCodecs = {

rust/crates/truapi-codegen/src/rustdoc.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,9 @@ pub fn extract_api(krate: &Crate) -> Result<ApiDefinition> {
294294
}
295295

296296
for candidate in candidates {
297+
if should_skip_type_candidate(&name, &candidate) {
298+
continue;
299+
}
297300
let item = krate
298301
.index
299302
.get(&candidate.item_id)
@@ -393,18 +396,24 @@ fn should_skip_type_name(name: &str) -> bool {
393396
| "CancellationToken"
394397
| "FrameworkOnlyError"
395398
| "Infallible"
399+
| "LatestOf"
396400
| "RequestId"
397401
| "RuntimeFailure"
398402
| "RuntimeFailureKind"
399403
)
400404
}
401405

406+
fn should_skip_type_candidate(name: &str, candidate: &ItemCandidate) -> bool {
407+
should_skip_type_name(name) || candidate.path.iter().any(|segment| segment == "latest")
408+
}
409+
402410
fn build_name_context(type_candidates: &BTreeMap<String, Vec<ItemCandidate>>) -> NameContext {
403411
let mut ctx = NameContext::default();
404412
for (simple_name, candidates) in type_candidates {
405-
if should_skip_type_name(simple_name) {
406-
continue;
407-
}
413+
let candidates = candidates
414+
.iter()
415+
.filter(|candidate| !should_skip_type_candidate(simple_name, candidate))
416+
.collect::<Vec<_>>();
408417
let has_conflict = candidates.len() > 1;
409418
for candidate in candidates {
410419
let output_name = if has_conflict {

rust/crates/truapi-codegen/src/ts.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,6 +1901,12 @@ fn codec_expr_mode(
19011901
_ => bail!("Unsupported primitive type `{name}` in TypeScript codec generation"),
19021902
},
19031903
TypeRef::Named { name, args } => {
1904+
if name == "CallError" && args.len() == 1 {
1905+
return Ok(format!(
1906+
"S.CallError({})",
1907+
codec_expr_mode(&args[0], qualified, ctx, mode)?
1908+
));
1909+
}
19041910
let resolved = resolve_named(name, mode);
19051911
let target = if qualified {
19061912
qualify_named(&resolved, mode)
@@ -1975,6 +1981,12 @@ fn ts_type_with_named(ty: &TypeRef, qualified: bool, mode: NameMode) -> Result<S
19751981
_ => bail!("Unsupported primitive type `{name}` in TypeScript type generation"),
19761982
},
19771983
TypeRef::Named { name, args } => {
1984+
if name == "CallError" && args.len() == 1 {
1985+
return Ok(format!(
1986+
"S.CallErrorValue<{}>",
1987+
ts_type_with_named(&args[0], qualified, mode)?
1988+
));
1989+
}
19781990
let resolved = resolve_named(name, mode);
19791991
let target = if qualified {
19801992
qualify_named(&resolved, mode)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ pub mod resource_allocation;
1414
pub mod signing;
1515
pub mod statement_store;
1616
pub mod system;
17+
#[cfg(debug_assertions)]
18+
pub mod testing;
1719
pub mod theme;
1820

1921
pub use account::Account;
@@ -30,9 +32,12 @@ pub use resource_allocation::ResourceAllocation;
3032
pub use signing::Signing;
3133
pub use statement_store::StatementStore;
3234
pub use system::System;
35+
#[cfg(debug_assertions)]
36+
pub use testing::Testing;
3337
pub use theme::Theme;
3438

3539
/// The unified TrUAPI contract.
40+
#[cfg(debug_assertions)]
3641
pub trait TrUApi:
3742
Account
3843
+ Chain
@@ -48,12 +53,59 @@ pub trait TrUApi:
4853
+ Signing
4954
+ StatementStore
5055
+ System
56+
+ Testing
5157
+ Theme
5258
+ Send
5359
+ Sync
5460
{
5561
}
5662

63+
#[cfg(not(debug_assertions))]
64+
pub trait TrUApi:
65+
Account
66+
+ Chain
67+
+ Chat
68+
+ CoinPayment
69+
+ Entropy
70+
+ LocalStorage
71+
+ Notifications
72+
+ Payment
73+
+ Permissions
74+
+ Preimage
75+
+ ResourceAllocation
76+
+ Signing
77+
+ StatementStore
78+
+ System
79+
+ Theme
80+
+ Send
81+
+ Sync
82+
{
83+
}
84+
85+
#[cfg(debug_assertions)]
86+
impl<T> TrUApi for T where
87+
T: Account
88+
+ Chain
89+
+ Chat
90+
+ CoinPayment
91+
+ Entropy
92+
+ LocalStorage
93+
+ Notifications
94+
+ Payment
95+
+ Permissions
96+
+ Preimage
97+
+ ResourceAllocation
98+
+ Signing
99+
+ StatementStore
100+
+ System
101+
+ Testing
102+
+ Theme
103+
+ Send
104+
+ Sync
105+
{
106+
}
107+
108+
#[cfg(not(debug_assertions))]
57109
impl<T> TrUApi for T where
58110
T: Account
59111
+ Chain

rust/crates/truapi/src/api/statement_store.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use crate::versioned::statement_store::{
66
RemoteStatementStoreCreateProofAuthorizedResponse, RemoteStatementStoreCreateProofError,
77
RemoteStatementStoreCreateProofRequest, RemoteStatementStoreCreateProofResponse,
88
RemoteStatementStoreSubmitError, RemoteStatementStoreSubmitRequest,
9-
RemoteStatementStoreSubscribeItem, RemoteStatementStoreSubscribeRequest,
9+
RemoteStatementStoreSubscribeError, RemoteStatementStoreSubscribeItem,
10+
RemoteStatementStoreSubscribeRequest,
1011
};
1112
use crate::wire;
1213
use crate::{CallContext, CallError, Subscription};
@@ -56,8 +57,11 @@ pub trait StatementStore: Send + Sync {
5657
&self,
5758
_cx: &CallContext,
5859
_request: RemoteStatementStoreSubscribeRequest,
59-
) -> Subscription<RemoteStatementStoreSubscribeItem> {
60-
Subscription::empty()
60+
) -> Result<
61+
Subscription<RemoteStatementStoreSubscribeItem>,
62+
CallError<RemoteStatementStoreSubscribeError>,
63+
> {
64+
Err(CallError::unavailable())
6165
}
6266

6367
/// Create a proof for a statement.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//! Debug-only API used to verify wire-version and framework-error handling.
2+
3+
use crate::v01;
4+
use crate::v02;
5+
use crate::versioned::testing::{
6+
TestingVersionProbeError, TestingVersionProbeRequest, TestingVersionProbeResponse,
7+
};
8+
use crate::wire;
9+
use crate::{CallContext, CallError};
10+
11+
/// Development-only probes for generated client/runtime compatibility.
12+
pub trait Testing: Send + Sync {
13+
/// Echo the request version back to the caller.
14+
///
15+
/// ```ts
16+
/// const result = await truapi.testing.versionProbe({
17+
/// message: "hello from V2",
18+
/// marker: 42,
19+
/// });
20+
/// assert(result.isOk(), "testing version probe failed:", result);
21+
/// console.log("testing version probe:", result.value);
22+
/// ```
23+
#[wire(request_id = 164)]
24+
async fn version_probe(
25+
&self,
26+
_cx: &CallContext,
27+
request: TestingVersionProbeRequest,
28+
) -> Result<TestingVersionProbeResponse, CallError<TestingVersionProbeError>> {
29+
match request {
30+
TestingVersionProbeRequest::V1(inner) => Ok(TestingVersionProbeResponse::V1(
31+
v01::TestingVersionProbeResponse {
32+
received_version: 1,
33+
message: inner.message,
34+
},
35+
)),
36+
TestingVersionProbeRequest::V2(inner) => Ok(TestingVersionProbeResponse::V2(
37+
v02::TestingVersionProbeResponse {
38+
received_version: 2,
39+
message: inner.message,
40+
marker: inner.marker,
41+
},
42+
)),
43+
}
44+
}
45+
46+
/// Echo a framework/domain error on the public response channel.
47+
///
48+
/// ```ts
49+
/// const result = await truapi.testing.echoError({
50+
/// error: { tag: "HostFailure", value: { reason: "forced by test" } },
51+
/// });
52+
/// assert(result.isErr(), "expected host failure");
53+
/// console.log("echo error:", result.error);
54+
/// ```
55+
#[wire(request_id = 166)]
56+
async fn echo_error(
57+
&self,
58+
_cx: &CallContext,
59+
request: v01::EchoErrorRequest,
60+
) -> Result<(), CallError<v01::TestingVersionProbeError>> {
61+
Err(request.error)
62+
}
63+
}

0 commit comments

Comments
 (0)