From 8ddbe7808515fb54f5b0b73f216ee294e327a616 Mon Sep 17 00:00:00 2001 From: pgherveou Date: Tue, 30 Jun 2026 09:24:47 +0200 Subject: [PATCH] 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. --- Cargo.lock | 90 + Cargo.toml | 1 + js/packages/truapi/src/client.test.ts | 140 +- rust/crates/truapi-codegen/.gitignore | 2 + rust/crates/truapi-codegen/Cargo.toml | 3 + rust/crates/truapi-codegen/src/main.rs | 63 + rust/crates/truapi-codegen/src/platform.rs | 690 ++++++ rust/crates/truapi-codegen/src/rust.rs | 604 +++++ .../truapi-codegen/src/rust/dispatcher.rs | 740 ++++++ .../truapi-codegen/src/rust/wire_table.rs | 303 +++ rust/crates/truapi-codegen/src/rustdoc.rs | 84 +- rust/crates/truapi-codegen/src/ts.rs | 382 ++- .../truapi-codegen/src/ts/host_callbacks.rs | 1483 +++++++++++ .../truapi-codegen/src/ts/playground.rs | 4 +- .../truapi-codegen/tests/golden/dispatcher.rs | 1913 +++++++++++++++ .../tests/golden/host-callbacks-adapter.ts | 78 + .../tests/golden/host-callbacks.ts | 497 ++++ .../truapi-codegen/tests/golden/wire_table.rs | 746 ++++++ .../tests/golden/worker-callbacks.ts | 117 + .../truapi-codegen/tests/golden_rust_emit.rs | 286 +++ .../truapi-server/src/generated/dispatcher.rs | 2182 +++++++++++++++++ .../crates/truapi-server/src/generated/mod.rs | 4 + .../truapi-server/src/generated/wire_table.rs | 746 ++++++ scripts/codegen.sh | 14 +- 24 files changed, 11097 insertions(+), 75 deletions(-) create mode 100644 rust/crates/truapi-codegen/.gitignore create mode 100644 rust/crates/truapi-codegen/src/platform.rs create mode 100644 rust/crates/truapi-codegen/src/rust.rs create mode 100644 rust/crates/truapi-codegen/src/rust/dispatcher.rs create mode 100644 rust/crates/truapi-codegen/src/rust/wire_table.rs create mode 100644 rust/crates/truapi-codegen/src/ts/host_callbacks.rs create mode 100644 rust/crates/truapi-codegen/tests/golden/dispatcher.rs create mode 100644 rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts create mode 100644 rust/crates/truapi-codegen/tests/golden/host-callbacks.ts create mode 100644 rust/crates/truapi-codegen/tests/golden/wire_table.rs create mode 100644 rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts create mode 100644 rust/crates/truapi-codegen/tests/golden_rust_emit.rs create mode 100644 rust/crates/truapi-server/src/generated/dispatcher.rs create mode 100644 rust/crates/truapi-server/src/generated/mod.rs create mode 100644 rust/crates/truapi-server/src/generated/wire_table.rs diff --git a/Cargo.lock b/Cargo.lock index 7a2c5a54..ac5904b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,6 +75,12 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + [[package]] name = "bitvec" version = "1.0.1" @@ -93,6 +99,12 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "clap" version = "4.6.1" @@ -218,6 +230,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -321,6 +349,17 @@ dependencies = [ "slab", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -499,6 +538,18 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.2" @@ -511,6 +562,12 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -593,6 +650,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -608,6 +671,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -715,6 +791,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -776,6 +865,7 @@ dependencies = [ "indoc", "serde", "serde_json", + "tempfile", "truapi", ] diff --git a/Cargo.toml b/Cargo.toml index 042aff82..d657d267 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = ["rust/crates/*"] +exclude = ["rust/crates/truapi-server"] [workspace.package] edition = "2024" diff --git a/js/packages/truapi/src/client.test.ts b/js/packages/truapi/src/client.test.ts index 3ab213ba..5e1db02d 100644 --- a/js/packages/truapi/src/client.test.ts +++ b/js/packages/truapi/src/client.test.ts @@ -2,12 +2,16 @@ import type { Result } from "neverthrow"; import { describe, expect, it } from "bun:test"; import { createTransport } from "./client.js"; -import { indexedTaggedUnion, Result as ScaleResult, str, _void } from "./scale.js"; +import { CallError, indexedTaggedUnion, Result as ScaleResult, str, _void } from "./scale.js"; +import type { Codec } from "./scale.js"; import { createClient, SubscriptionError } from "./generated/client.js"; import * as T from "./generated/types.js"; import * as W from "./generated/wire-table.js"; import { encodeWireMessage } from "./transport.js"; +/** Wrap a codec in the `{ V1: [0, codec] }` indexed-tagged-union envelope. */ +const versionedV1 = (codec: Codec) => indexedTaggedUnion({ V1: [0, codec] }); + function toHex(u: Uint8Array): string { return Array.from(u) .map((b) => b.toString(16).padStart(2, "0")) @@ -56,9 +60,34 @@ function providerFixture() { /** Encode a V1 host-handshake response result payload. */ function handshakeResponsePayload(value: { success: true; value: undefined }): Uint8Array { - return indexedTaggedUnion({ - V1: [0, ScaleResult(_void, T.HostHandshakeError)], - }).enc({ tag: "V1", value }); + return versionedV1(ScaleResult(_void, CallError(T.VersionedHostHandshakeError))).enc({ + tag: "V1", + value, + }); +} + +function accountGetResponsePayload( + value: + | { + success: true; + value: T.HostAccountGetResponse; + } + | { + success: false; + value: { tag: "Domain"; value: T.VersionedHostAccountGetError }; + }, +): Uint8Array { + return versionedV1( + ScaleResult(T.HostAccountGetResponse, CallError(T.VersionedHostAccountGetError)), + ).enc({ tag: "V1", value }); +} + +/** Encode a raw testing echo error response payload. */ +function testingEchoErrorPayload(reason: string): Uint8Array { + return ScaleResult(_void, CallError(T.V01TestingVersionProbeError)).enc({ + success: false, + value: { tag: "HostFailure", value: { reason } }, + }); } describe("generated client transport", () => { @@ -88,6 +117,29 @@ describe("generated client transport", () => { expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); }); + it("uses the latest generated request version for testing probes", () => { + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + + const request = { + message: "hello from test", + marker: 42, + }; + void client.testing.versionProbe(request); + + const expectedPayload = T.VersionedTestingVersionProbeRequest.enc({ + tag: "V2", + value: request, + }); + const expectedFrame = new Uint8Array(str.enc("p:1").length + 1 + expectedPayload.length); + expectedFrame.set(str.enc("p:1"), 0); + expectedFrame[str.enc("p:1").length] = W.TESTING_VERSION_PROBE.request; + expectedFrame.set(expectedPayload, str.enc("p:1").length + 1); + + expect(toHex(fixture.sent[0])).toBe(toHex(expectedFrame)); + }); + it("uses the transport codec version for generated handshake calls", () => { const fixture = providerFixture(); const transport = createTransport(fixture.provider); @@ -129,6 +181,63 @@ describe("generated client transport", () => { expect(result.isOk()).toBe(true); }); + it("decodes request domain errors from the versioned response envelope", async () => { + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + + const response = client.account.getAccount({ + productAccountId: { dotNsIdentifier: "foo", derivationIndex: 0 }, + }); + const reason = { tag: "V1", value: { tag: "NotConnected", value: undefined } } as const; + const frame = unwrap( + encodeWireMessage({ + requestId: "p:1", + payload: { + id: W.ACCOUNT_GET_ACCOUNT.response, + value: accountGetResponsePayload({ + success: false, + value: { tag: "Domain", value: reason }, + }), + }, + }), + "encode account_get error response", + ); + fixture.receive(frame); + + const result = await response; + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toEqual({ tag: "Domain", value: reason }); + }); + + it("returns framework call errors as typed Err values", async () => { + const fixture = providerFixture(); + const transport = createTransport(fixture.provider); + const client = createClient(transport); + + const response = client.testing.echoError({ + error: { tag: "HostFailure", value: { reason: "forced by test" } }, + }); + const frame = unwrap( + encodeWireMessage({ + requestId: "p:1", + payload: { + id: W.TESTING_ECHO_ERROR.response, + value: testingEchoErrorPayload("forced by test"), + }, + }), + "encode testing framework error response", + ); + fixture.receive(frame); + + const result = await response; + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toEqual({ + tag: "HostFailure", + value: { reason: "forced by test" }, + }); + }); + it("auto-responds to an inbound handshake with the versioned-result shape", () => { const fixture = providerFixture(); createTransport(fixture.provider); @@ -225,14 +334,18 @@ describe("generated client transport", () => { }); const reason = { tag: "PermissionDenied", value: undefined } as const; + const callError = { + tag: "Domain", + value: { tag: "V1", value: reason }, + } as const; const frame = unwrap( encodeWireMessage({ requestId: sub.subscriptionId, payload: { id: W.PAYMENT_BALANCE_SUBSCRIBE.interrupt, - value: T.VersionedHostPaymentBalanceSubscribeError.enc({ + value: versionedV1(CallError(T.VersionedHostPaymentBalanceSubscribeError)).enc({ tag: "V1", - value: reason, + value: callError, }), }, }), @@ -243,7 +356,7 @@ describe("generated client transport", () => { expect(completions).toEqual([]); expect(errors).toHaveLength(1); expect(errors[0]).toBeInstanceOf(SubscriptionError); - expect((errors[0] as SubscriptionError).reason).toEqual(reason); + expect((errors[0] as SubscriptionError).reason).toEqual(callError); expect(fixture.sent).toHaveLength(1); }); @@ -258,15 +371,18 @@ describe("generated client transport", () => { .subscribe({ error: (error) => errors.push(error) }); const reason = "Denied"; + const callError = { + tag: "Domain", + value: { tag: "V1", value: reason }, + } as const; const frame = unwrap( encodeWireMessage({ requestId: sub.subscriptionId, payload: { id: W.COIN_PAYMENT_REBALANCE_PURSE.interrupt, - value: T.VersionedHostCoinPaymentRebalancePurseError.enc({ - tag: "V1", - value: reason, - }), + value: versionedV1( + CallError(T.VersionedHostCoinPaymentRebalancePurseError), + ).enc({ tag: "V1", value: callError }), }, }), "encode typed coin payment interrupt", @@ -275,7 +391,7 @@ describe("generated client transport", () => { expect(errors).toHaveLength(1); expect(errors[0]).toBeInstanceOf(SubscriptionError); - expect((errors[0] as SubscriptionError).reason).toEqual(reason); + expect((errors[0] as SubscriptionError).reason).toEqual(callError); }); it("treats a malformed receive payload as terminal and sends _stop", () => { diff --git a/rust/crates/truapi-codegen/.gitignore b/rust/crates/truapi-codegen/.gitignore new file mode 100644 index 00000000..7a478389 --- /dev/null +++ b/rust/crates/truapi-codegen/.gitignore @@ -0,0 +1,2 @@ +# Mismatch dumps written by tests/golden_rust_emit.rs for local inspection. +tests/golden/*.actual diff --git a/rust/crates/truapi-codegen/Cargo.toml b/rust/crates/truapi-codegen/Cargo.toml index c6d3ef3c..c376346e 100644 --- a/rust/crates/truapi-codegen/Cargo.toml +++ b/rust/crates/truapi-codegen/Cargo.toml @@ -17,3 +17,6 @@ anyhow = "1" clap = { version = "4", features = ["derive"] } indoc = "2" convert_case = "0.6" + +[dev-dependencies] +tempfile = "3" diff --git a/rust/crates/truapi-codegen/src/main.rs b/rust/crates/truapi-codegen/src/main.rs index 0dfa318c..42a5aa1e 100644 --- a/rust/crates/truapi-codegen/src/main.rs +++ b/rust/crates/truapi-codegen/src/main.rs @@ -1,7 +1,10 @@ use anyhow::{Context, Result}; use clap::Parser; +use std::path::PathBuf; use std::str::FromStr; +mod platform; +mod rust; mod rustdoc; mod ts; @@ -43,6 +46,32 @@ struct Cli { #[arg(long)] client_examples_output: Option, + /// Output directory for the generated Rust dispatcher / wire-table (optional). + /// + /// When set, emits `dispatcher.rs` and `wire_table.rs` for the + /// `truapi-server` crate to include. + #[arg(long)] + rust_output: Option, + + /// Path to rustdoc JSON for the `truapi-platform` crate (optional). + /// + /// When provided together with `--platform-ts-output`, walks the + /// platform crate's capability traits and emits the typed TS + /// `HostCallbacks` surface plus the WASM raw callback adapter. + #[arg(long)] + platform_input: Option, + + /// Output directory for the generated typed `HostCallbacks` TypeScript + /// surface (optional). Only honored when `--platform-input` is also set. + #[arg(long)] + platform_ts_output: Option, + + /// Output directory for the generated WASM host-callback adapter + /// (optional). Only honored when `--platform-input` and + /// `--platform-ts-output` are also set. Defaults to `--platform-ts-output`. + #[arg(long)] + platform_wasm_adapter_output: Option, + /// Output directory for generated explorer metadata (optional). When set, /// writes `codegen/types.ts` with the DataType list consumed by the /// explorer site. @@ -111,6 +140,40 @@ fn main() -> Result<()> { .with_context(|| format!("writing client examples to {path}"))?; println!("Generated client examples in {path}"); } + if let Some(path) = &cli.rust_output { + rust::generate(&api, path) + .with_context(|| format!("writing Rust dispatcher to {}", path.display()))?; + println!("Wrote Rust dispatcher to {}", path.display()); + } + if let (Some(input), Some(output)) = (&cli.platform_input, &cli.platform_ts_output) { + let json = std::fs::read_to_string(input) + .with_context(|| format!("reading platform rustdoc JSON from {input}"))?; + let krate = + rustdoc::parse(&json).with_context(|| format!("parsing platform rustdoc {input}"))?; + let definition = platform::extract(&krate) + .with_context(|| format!("extracting platform definition from {input}"))?; + let codec_types = api + .types + .iter() + .filter(|t| !matches!(t.kind, rustdoc::TypeDefKind::Alias(_))) + .map(|t| t.name.clone()) + .collect(); + let adapter_output = cli + .platform_wasm_adapter_output + .as_deref() + .unwrap_or(output.as_str()); + ts::generate_host_callbacks(&definition, &codec_types, output, adapter_output) + .with_context(|| format!("writing host callbacks TS to {output}"))?; + println!("Generated typed HostCallbacks TS surface in {output}"); + println!("Generated WASM HostCallbacks adapter in {adapter_output}"); + } else if cli.platform_input.is_some() != cli.platform_ts_output.is_some() + || cli.platform_wasm_adapter_output.is_some() + { + anyhow::bail!( + "--platform-input and --platform-ts-output must be provided together; \ + --platform-wasm-adapter-output additionally requires both" + ); + } if let Some(path) = &cli.explorer_output { ts::generate_explorer(&api, path, client_version) .with_context(|| format!("writing explorer metadata to {path}"))?; diff --git a/rust/crates/truapi-codegen/src/platform.rs b/rust/crates/truapi-codegen/src/platform.rs new file mode 100644 index 00000000..f37b5e91 --- /dev/null +++ b/rust/crates/truapi-codegen/src/platform.rs @@ -0,0 +1,690 @@ +//! Parse `truapi-platform`-style "plain capability traits" from rustdoc JSON. +//! +//! Unlike the `truapi` API crate, the platform crate has no `#[wire(id = N)]` +//! annotations: it is a set of host-facing capability traits whose methods +//! use `async_trait` (rustdoc exposes those as boxed `Future` trait objects) or +//! plain synchronous functions returning trait objects / `BoxStream`. This +//! module walks the rustdoc index for every public trait in the platform crate +//! and produces a [`PlatformDefinition`] the TS emitter can render directly. + +use std::collections::BTreeSet; + +use anyhow::{Context, Result, bail}; + +use crate::rustdoc::{ + Crate, Item, NameContext, TypeDef, TypeDefKind, TypeRef, VariantFields, clean_docs, + extract_enum, extract_struct, resolve_type, summarize_json, +}; + +/// Top-level extracted shape of a `truapi-platform`-style crate. +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformDefinition { + /// Capability traits sorted alphabetically by name. + pub traits: Vec, + /// Local structs and enums referenced from trait method signatures, + /// sorted alphabetically by name. Emitted alongside the trait interfaces + /// so the generated TS does not have to import them from the API client. + pub types: Vec, + /// Composite super-trait (`Platform: Storage + Navigation + ...`), if any. + pub super_trait: Option, +} + +/// Single capability trait extracted from the platform crate. +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformTrait { + /// Trait name as it appears in source. + pub name: String, + /// Rustdoc comment on the trait. + pub docs: Option, + /// Methods declared on the trait, in declaration order. + pub methods: Vec, +} + +/// A trait method on a capability trait. +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformMethod { + /// Method name as it appears in source. + pub name: String, + /// Rustdoc comment on the method. + pub docs: Option, + /// Parameter list with names preserved (excluding `&self`). + pub params: Vec, + /// Return shape decoded from the method signature. + pub return_shape: PlatformReturn, + /// Whether the trait provides a default body, making the method optional + /// for host implementations. + pub has_default: bool, +} + +/// Method parameter (name + type). +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformParam { + /// Parameter name as written in the trait method signature. + pub name: String, + /// Parameter type expressed as a [`TypeRef`]. + pub type_ref: TypeRef, +} + +/// Return shape after stripping async-trait `Pin>>` +/// / `Box` wrappers. +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformReturn { + /// Whether the method returns an async-trait boxed future (i.e. is async). + pub is_async: bool, + /// Unwrapped inner shape. + pub inner: PlatformInner, +} + +/// Classification of the unwrapped return type. +#[derive(Debug, PartialEq, Eq)] +pub enum PlatformInner { + /// `()` (or no return). + Unit, + /// `Result`. The TS surface returns `Promise` and rejects with `Err`. + Result { ok: TypeRef, err: TypeRef }, + /// `BoxStream<'static, T>`, a stream of `T` items. + Stream(TypeRef), + /// `Box`, a trait object handle to a named trait. + TraitObject(String), + /// Any other concrete type, returned as-is. + Plain(TypeRef), +} + +/// Composite super-trait that aggregates capability traits. +#[derive(Debug, PartialEq, Eq)] +pub struct PlatformSuperTrait { + /// Name of the super-trait (e.g. `Platform`). + pub name: String, + /// Rustdoc comment on the super-trait. + pub docs: Option, + /// Capability trait names this super-trait composes, in source order. + pub composes: Vec, +} + +/// Walk the platform crate and extract every public trait + its methods. +pub fn extract(krate: &Crate) -> Result { + let trait_ids = collect_local_trait_ids(krate); + let names = NameContext::default(); + + let mut traits = Vec::new(); + let mut super_trait = None; + for item_id in &trait_ids { + let item = krate + .index + .get(item_id) + .with_context(|| format!("Missing rustdoc item `{item_id}` for trait"))?; + let name = item + .name + .as_ref() + .cloned() + .with_context(|| format!("Trait `{item_id}` has no name"))?; + let trait_inner = item + .inner + .get("trait") + .with_context(|| format!("Trait `{name}` missing rustdoc trait body"))?; + + if is_super_trait(trait_inner) { + if super_trait.is_some() { + bail!("Multiple super-traits with method-less bodies found; only one is supported"); + } + super_trait = Some(extract_super_trait(&name, item, trait_inner)?); + continue; + } + + traits.push(extract_capability_trait( + &name, + item, + trait_inner, + krate, + &names, + )?); + } + + traits.sort_by(|a, b| a.name.cmp(&b.name)); + let types = collect_referenced_local_types(krate, &traits, &names)?; + + Ok(PlatformDefinition { + traits, + types, + super_trait, + }) +} + +/// Extract every local struct or enum whose name appears in a trait method +/// signature. +fn collect_referenced_local_types( + krate: &Crate, + traits: &[PlatformTrait], + names: &NameContext, +) -> Result> { + let mut referenced = BTreeSet::new(); + for trait_def in traits { + for method in &trait_def.methods { + for param in &method.params { + collect_named_types(¶m.type_ref, &mut referenced); + } + match &method.return_shape.inner { + // Err types never reach the TS signature (errors throw), so + // their names are not emitted either. + PlatformInner::Result { ok, .. } => collect_named_types(ok, &mut referenced), + PlatformInner::Stream(inner) | PlatformInner::Plain(inner) => { + collect_named_types(inner, &mut referenced) + } + PlatformInner::TraitObject(_) | PlatformInner::Unit => {} + } + } + } + + // Local types can reference further local types from their fields or + // variant payloads (e.g. `AuthState::Connected(SessionUiInfo)`), so keep + // extracting until the referenced set stops growing. + let mut types: Vec = Vec::new(); + let mut extracted: BTreeSet = BTreeSet::new(); + loop { + let mut grew = false; + for (item_id, item_path) in &krate.paths { + if item_path.crate_id != 0 || !matches!(item_path.kind.as_str(), "struct" | "enum") { + continue; + } + let Some(name) = item_path.path.last() else { + continue; + }; + if !referenced.contains(name) || extracted.contains(name) { + continue; + } + let item = krate.index.get(item_id).with_context(|| { + format!( + "Missing rustdoc item `{item_id}` for {} `{name}`", + item_path.kind + ) + })?; + let module_path = item_path.path[..item_path.path.len() - 1].to_vec(); + let type_def = if item_path.kind == "struct" { + extract_struct(item_id, item, krate, names, module_path)? + } else { + extract_enum(item_id, item, krate, names, module_path)? + }; + collect_type_def_references(&type_def, &mut referenced); + extracted.insert(name.clone()); + types.push(type_def); + grew = true; + } + if !grew { + break; + } + } + types.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(types) +} + +/// Collect named types referenced from a local type's fields or variants. +fn collect_type_def_references(type_def: &TypeDef, out: &mut BTreeSet) { + match &type_def.kind { + TypeDefKind::Alias(ty) => collect_named_types(ty, out), + TypeDefKind::Struct(fields) => { + for field in fields { + collect_named_types(&field.type_ref, out); + } + } + TypeDefKind::TupleStruct(types) => { + for ty in types { + collect_named_types(ty, out); + } + } + TypeDefKind::Enum(variants) => { + for variant in variants { + match &variant.fields { + VariantFields::Unit => {} + VariantFields::Unnamed(types) => { + for ty in types { + collect_named_types(ty, out); + } + } + VariantFields::Named(fields) => { + for field in fields { + collect_named_types(&field.type_ref, out); + } + } + } + } + } + } +} + +fn collect_named_types(ty: &TypeRef, out: &mut BTreeSet) { + match ty { + TypeRef::Named { name, args } => { + out.insert(name.clone()); + for arg in args { + collect_named_types(arg, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_named_types(inner, out) + } + TypeRef::Tuple(items) => { + for item in items { + collect_named_types(item, out); + } + } + TypeRef::Primitive(_) | TypeRef::Generic(_) | TypeRef::Unit => {} + } +} + +fn collect_local_trait_ids(krate: &Crate) -> BTreeSet { + let mut out = BTreeSet::new(); + for (item_id, item_path) in &krate.paths { + if item_path.crate_id != 0 || item_path.kind != "trait" { + continue; + } + out.insert(item_id.clone()); + } + out +} + +fn is_super_trait(trait_inner: &serde_json::Value) -> bool { + let no_methods = trait_inner + .get("items") + .and_then(|value| value.as_array()) + .map(|arr| arr.is_empty()) + .unwrap_or(true); + + let has_local_trait_bound = trait_inner + .get("bounds") + .and_then(|value| value.as_array()) + .map(|bounds| { + bounds.iter().any(|bound| { + bound + .get("trait_bound") + .and_then(|tb| tb.get("trait")) + .and_then(|t| t.get("path")) + .and_then(|p| p.as_str()) + .map(|name| name != "Send" && name != "Sync") + .unwrap_or(false) + }) + }) + .unwrap_or(false); + + no_methods && has_local_trait_bound +} + +fn extract_super_trait( + name: &str, + item: &Item, + trait_inner: &serde_json::Value, +) -> Result { + let bounds = trait_inner + .get("bounds") + .and_then(|value| value.as_array()) + .with_context(|| format!("Super-trait `{name}` missing rustdoc bounds"))?; + + let mut composes = Vec::new(); + for bound in bounds { + let Some(path) = bound + .get("trait_bound") + .and_then(|tb| tb.get("trait")) + .and_then(|t| t.get("path")) + .and_then(|p| p.as_str()) + else { + continue; + }; + if path == "Send" || path == "Sync" { + continue; + } + composes.push(path.to_string()); + } + + Ok(PlatformSuperTrait { + name: name.to_string(), + docs: clean_docs(item.docs.as_deref()), + composes, + }) +} + +fn extract_capability_trait( + name: &str, + item: &Item, + trait_inner: &serde_json::Value, + krate: &Crate, + names: &NameContext, +) -> Result { + let item_ids = trait_inner + .get("items") + .and_then(|value| value.as_array()) + .with_context(|| format!("Trait `{name}` missing rustdoc items array"))?; + + let mut methods = Vec::new(); + for method_id in item_ids { + let method_id = value_to_id(method_id) + .with_context(|| format!("Trait `{name}` contained a non-item method id"))?; + let method_item = krate + .index + .get(&method_id) + .with_context(|| format!("Trait `{name}` references missing item `{method_id}`"))?; + if let Some(method) = extract_method(method_item, names)? { + methods.push(method); + } + } + + Ok(PlatformTrait { + name: name.to_string(), + docs: clean_docs(item.docs.as_deref()), + methods, + }) +} + +fn extract_method(item: &Item, names: &NameContext) -> Result> { + let Some(fn_inner) = item.inner.get("function") else { + return Ok(None); + }; + let name = item + .name + .as_ref() + .cloned() + .with_context(|| "Method item has no name".to_string())?; + let sig = fn_inner + .get("sig") + .with_context(|| format!("Method `{name}` missing rustdoc signature"))?; + + let mut params = Vec::new(); + if let Some(inputs) = sig.get("inputs").and_then(|value| value.as_array()) { + for input in inputs { + let arr = input + .as_array() + .with_context(|| format!("Method `{name}` has an invalid input entry"))?; + let param_name = arr + .first() + .and_then(|value| value.as_str()) + .with_context(|| format!("Method `{name}` has an unnamed input"))? + .to_string(); + if param_name == "self" { + continue; + } + let ty = arr.get(1).with_context(|| { + format!("Method `{name}` input `{param_name}` is missing a type") + })?; + let type_ref = resolve_type(ty, names).with_context(|| { + format!("Method `{name}` input `{param_name}` has an unsupported type") + })?; + params.push(PlatformParam { + name: param_name, + type_ref, + }); + } + } + + let return_shape = resolve_return(sig.get("output"), names) + .with_context(|| format!("Method `{name}` has an unsupported return type"))?; + let has_default = fn_inner + .get("has_body") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false); + + Ok(Some(PlatformMethod { + name, + docs: clean_docs(item.docs.as_deref()), + params, + return_shape, + has_default, + })) +} + +fn resolve_return( + output: Option<&serde_json::Value>, + names: &NameContext, +) -> Result { + let Some(output) = output else { + return Ok(PlatformReturn { + is_async: false, + inner: PlatformInner::Unit, + }); + }; + if output.is_null() { + return Ok(PlatformReturn { + is_async: false, + inner: PlatformInner::Unit, + }); + } + + if let Some(future_output) = extract_async_trait_future_output(output) { + let inner = resolve_inner_shape(&future_output, names)?; + return Ok(PlatformReturn { + is_async: true, + inner, + }); + } + + let inner = resolve_inner_shape(output, names)?; + Ok(PlatformReturn { + is_async: false, + inner, + }) +} + +fn extract_async_trait_future_output(output: &serde_json::Value) -> Option { + let pin = output.get("resolved_path")?; + if resolved_leaf(pin) != Some("Pin") { + return None; + } + let boxed = generic_arg(pin, 0)?; + let boxed = boxed.get("resolved_path")?; + if resolved_leaf(boxed) != Some("Box") { + return None; + } + let dyn_trait = generic_arg(boxed, 0)?; + let dyn_trait = dyn_trait.get("dyn_trait")?; + let traits = dyn_trait.get("traits")?.as_array()?; + for trait_entry in traits { + let trait_obj = trait_entry.get("trait")?; + if resolved_leaf(trait_obj) != Some("Future") { + continue; + } + let constraints = trait_obj + .get("args")? + .get("angle_bracketed")? + .get("constraints")? + .as_array()?; + for constraint in constraints { + if constraint.get("name")?.as_str()? != "Output" { + continue; + } + let ty = constraint.get("binding")?.get("equality")?.get("type")?; + return Some(ty.clone()); + } + } + None +} + +fn resolve_inner_shape(ty: &serde_json::Value, names: &NameContext) -> Result { + // `()` tuple. + if let Some(arr) = ty.get("tuple").and_then(|v| v.as_array()) + && arr.is_empty() + { + return Ok(PlatformInner::Unit); + } + + if let Some(resolved) = ty.get("resolved_path") { + let path = resolved + .get("path") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let leaf = path.rsplit("::").next().unwrap_or(path); + + match leaf { + "Result" => { + let ok = + generic_arg(resolved, 0).context("Result<...> return type missing ok arg")?; + let err = + generic_arg(resolved, 1).context("Result<...> return type missing err arg")?; + let ok_ref = resolve_inner_type(&ok, names)?; + let err_ref = resolve_inner_type(&err, names)?; + return Ok(PlatformInner::Result { + ok: ok_ref, + err: err_ref, + }); + } + "BoxStream" => { + // `BoxStream<'a, T>`: the lifetime arg is filtered out by + // `generic_arg` (it has no `type` field), so the first + // remaining positional arg is the item type. + let item = + generic_arg(resolved, 0).context("BoxStream<'a, T> missing item type")?; + return Ok(PlatformInner::Stream(resolve_type(&item, names)?)); + } + "Box" => { + // `Box` or `Box`. + if let Some(arg) = generic_arg(resolved, 0) + && let Some(dyn_trait) = arg.get("dyn_trait") + { + return Ok(PlatformInner::TraitObject(dyn_trait_leaf_name(dyn_trait)?)); + } + } + _ => {} + } + } + + let resolved_ref = resolve_type(ty, names) + .with_context(|| format!("Unsupported return shape: {}", summarize_json(ty)))?; + Ok(PlatformInner::Plain(resolved_ref)) +} + +/// Resolve a positional type. Recognises `Box` and folds it +/// into a `TypeRef::Named { name: TraitName, args: [] }` so it survives +/// through to TS emission without `rustdoc.rs` having to model dyn traits. +fn resolve_inner_type(ty: &serde_json::Value, names: &NameContext) -> Result { + if let Some(resolved) = ty.get("resolved_path") + && resolved + .get("path") + .and_then(|v| v.as_str()) + .map(|p| p.rsplit("::").next().unwrap_or(p) == "Box") + .unwrap_or(false) + && let Some(arg) = generic_arg(resolved, 0) + && let Some(dyn_trait) = arg.get("dyn_trait") + { + return Ok(TypeRef::Named { + name: dyn_trait_leaf_name(dyn_trait)?, + args: Vec::new(), + }); + } + resolve_type(ty, names) +} + +/// Extract the leaf trait name from a `Box` rustdoc `dyn_trait` +/// value (the last `::`-segment of the first listed trait path). +fn dyn_trait_leaf_name(dyn_trait: &serde_json::Value) -> Result { + Ok(dyn_trait + .get("traits") + .and_then(|t| t.as_array()) + .and_then(|arr| arr.first()) + .and_then(|first| first.get("trait")) + .and_then(|trait_obj| trait_obj.get("path")) + .and_then(|p| p.as_str()) + .context("Box missing trait path")? + .rsplit("::") + .next() + .unwrap_or_default() + .to_string()) +} + +fn resolved_leaf(resolved: &serde_json::Value) -> Option<&str> { + let path = resolved.get("path")?.as_str()?; + Some(path.rsplit("::").next().unwrap_or(path)) +} + +fn generic_arg(resolved: &serde_json::Value, index: usize) -> Option { + resolved + .get("args")? + .get("angle_bracketed")? + .get("args")? + .as_array()? + .iter() + .filter_map(|entry| entry.get("type").cloned()) + .nth(index) +} + +fn value_to_id(value: &serde_json::Value) -> Result { + if let Some(id) = value.as_str() { + return Ok(id.to_string()); + } + if let Some(id) = value.as_u64() { + return Ok(id.to_string()); + } + bail!("Expected rustdoc item id, got non-id value") +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn extract_async_trait_future_output_from_pin_box_dyn_future() { + let output = json!({ + "resolved_path": { + "path": "::core::pin::Pin", + "args": { + "angle_bracketed": { + "args": [ + { + "type": { + "resolved_path": { + "path": "Box", + "args": { + "angle_bracketed": { + "args": [ + { + "type": { + "dyn_trait": { + "traits": [ + { + "trait": { + "path": "::core::future::Future", + "args": { + "angle_bracketed": { + "args": [], + "constraints": [ + { + "name": "Output", + "binding": { + "equality": { + "type": { "primitive": "u8" } + } + } + } + ] + } + } + } + }, + { + "trait": { + "path": "::core::marker::Send", + "args": null + } + } + ], + "lifetime": "'async_trait" + } + } + } + ], + "constraints": [] + } + } + } + } + } + ], + "constraints": [] + } + } + } + }); + + assert_eq!( + extract_async_trait_future_output(&output), + Some(json!({ "primitive": "u8" })) + ); + } +} diff --git a/rust/crates/truapi-codegen/src/rust.rs b/rust/crates/truapi-codegen/src/rust.rs new file mode 100644 index 00000000..15a2d904 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust.rs @@ -0,0 +1,604 @@ +//! Rust code generation from extracted API definitions. +//! +//! Emits the server-side wire dispatcher (`dispatcher.rs`) and the +//! discriminant lookup table (`wire_table.rs`). The generated files are +//! intended to be included in the `truapi-server` crate. + +use std::fs; +use std::path::Path; + +use anyhow::Result; + +use convert_case::{Case, Casing}; + +use crate::rustdoc::*; + +mod dispatcher; +mod wire_table; + +pub use dispatcher::generate_dispatcher; +pub use wire_table::generate_wire_table; + +/// Generates the Rust wire dispatcher and wire-table sources into `output_dir`. +pub fn generate(api: &ApiDefinition, output_dir: &Path) -> Result<()> { + fs::create_dir_all(output_dir)?; + let dispatcher = generate_dispatcher(api)?; + fs::write(output_dir.join("dispatcher.rs"), dispatcher)?; + let wire_table = generate_wire_table(api)?; + fs::write(output_dir.join("wire_table.rs"), wire_table)?; + Ok(()) +} + +/// Trait -> versioned-module mapping. Trait names are PascalCase +/// (`JsonRpc`, `LocalStorage`); module names are snake_case +/// (`jsonrpc`, `local_storage`). The mapping is irregular enough +/// (e.g. `JsonRpc` -> `jsonrpc`) that it is hardcoded. +const TRAIT_MODULE_MAP: &[(&str, &str)] = &[ + ("Account", "account"), + ("Chain", "chain"), + ("Chat", "chat"), + ("Entropy", "entropy"), + ("JsonRpc", "jsonrpc"), + ("LocalStorage", "local_storage"), + ("Payment", "payment"), + ("Permissions", "permissions"), + ("Preimage", "preimage"), + ("ResourceAllocation", "resource_allocation"), + ("Signing", "signing"), + ("StatementStore", "statement_store"), + ("System", "system"), + ("Theme", "theme"), +]; + +/// Returns the versioned-module name for a trait, falling back to a +/// snake_case conversion of the trait name when no explicit mapping is +/// declared. New traits should be added to [`TRAIT_MODULE_MAP`] so the +/// emission stays deterministic. +fn module_for_trait(trait_name: &str) -> String { + for (name, module) in TRAIT_MODULE_MAP { + if *name == trait_name { + return (*module).to_string(); + } + } + snake_case(trait_name) +} + +/// Returns the wire-protocol method name for a trait/method pair, used both +/// as the dispatcher's registration key and as the prefix of the action tag +/// (`{wire_method}_{request|response|...}`). The form is +/// `{trait_snake}_{method}` so collisions between sibling traits (e.g. +/// `StatementStore::submit` and `Preimage::submit`) become distinct keys +/// (`statement_store_submit`, `preimage_submit`). +pub(crate) fn wire_method_name(trait_name: &str, method_name: &str) -> String { + format!("{}_{}", snake_case(trait_name), method_name) +} + +/// The `SCREAMING_SNAKE_CASE` const name holding a wire method's ids. +/// Routed through [`convert_case::Case::UpperSnake`] so it follows the same +/// casing rules as the TS wire-table emitter (`ts.rs`). +pub(crate) fn const_name(wire_method: &str) -> String { + wire_method.to_case(Case::UpperSnake) +} + +/// Const name for a trait/method pair's wire ids. Both the Rust and TS +/// wire-table emitters apply `Case::UpperSnake`, so for the real +/// (single-capital PascalCase trait, snake_case method) surface the two +/// generated const names agree. +#[cfg(test)] +pub(crate) fn wire_const_name(trait_name: &str, method_name: &str) -> String { + const_name(&wire_method_name(trait_name, method_name)) +} + +/// Convert a PascalCase identifier into snake_case. +fn snake_case(name: &str) -> String { + let mut out = String::with_capacity(name.len() + 4); + for (idx, ch) in name.chars().enumerate() { + if ch.is_ascii_uppercase() { + if idx != 0 { + out.push('_'); + } + out.push(ch.to_ascii_lowercase()); + } else { + out.push(ch); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request_method(name: &str, request_id: u8) -> MethodDef { + MethodDef { + name: name.to_string(), + kind: MethodKind::Request, + params: vec![ParamDef { + name: "request".to_string(), + type_ref: TypeRef::Named { + name: "ReqWrapper".to_string(), + args: vec![], + }, + }], + return_type: ReturnType::Result { + ok: TypeRef::Named { + name: "RespWrapper".to_string(), + args: vec![], + }, + err: TypeRef::Named { + name: "CallError".to_string(), + args: vec![TypeRef::Named { + name: "ErrWrapper".to_string(), + args: vec![], + }], + }, + }, + wire: WireAttrs { + request_id: Some(request_id), + response_id: None, + start_id: None, + stop_id: None, + interrupt_id: None, + receive_id: None, + }, + docs: None, + } + } + + fn make_subscription_method(name: &str, start_id: u8) -> MethodDef { + MethodDef { + name: name.to_string(), + kind: MethodKind::Subscription, + params: vec![], + return_type: ReturnType::Subscription(TypeRef::Named { + name: "ItemWrapper".to_string(), + args: vec![], + }), + wire: WireAttrs { + request_id: None, + response_id: None, + start_id: Some(start_id), + stop_id: None, + interrupt_id: None, + receive_id: None, + }, + docs: None, + } + } + + fn versioned_test_type(name: &str) -> TypeDef { + TypeDef { + name: name.to_string(), + module_path: Vec::new(), + generic_params: Vec::new(), + kind: TypeDefKind::Enum(vec![VariantDef { + name: "V1".to_string(), + fields: VariantFields::Unnamed(vec![TypeRef::Named { + name: format!("V01{name}"), + args: vec![], + }]), + docs: None, + }]), + docs: None, + } + } + + fn versioned_request_test_types() -> Vec { + ["ReqWrapper", "RespWrapper", "ErrWrapper"] + .into_iter() + .map(versioned_test_type) + .collect() + } + + fn parse_entries(src: &str) -> Vec<(u8, String)> { + // Each method's ids are emitted as a named const, e.g. + // pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + // request_id: 68, + // response_id: 69, + // }; + // Reconstruct the `(id, "{method}_{suffix}")` pairs the assertions use. + let mut out = Vec::new(); + let mut lines = src.lines(); + while let Some(line) = lines.next() { + let Some(rest) = line.trim().strip_prefix("pub const ") else { + continue; + }; + let Some(colon) = rest.find(':') else { + continue; + }; + let is_sub = rest.contains("SubscriptionFrameIds"); + // Skip non-id consts (e.g. `WIRE_TABLE: &[WireEntry]`). + if !is_sub && !rest.contains("RequestFrameIds") { + continue; + } + let method = rest[..colon].trim().to_ascii_lowercase(); + + let mut ids: std::collections::BTreeMap<&str, u8> = std::collections::BTreeMap::new(); + for inner in lines.by_ref() { + let t = inner.trim(); + if t.starts_with("};") { + break; + } + if let Some((field, val)) = t.split_once(':') { + let id = val.trim().trim_end_matches(',').parse::().unwrap(); + ids.insert(field.trim(), id); + } + } + + let suffixes: &[(&str, &str)] = if is_sub { + &[ + ("start_id", "start"), + ("stop_id", "stop"), + ("interrupt_id", "interrupt"), + ("receive_id", "receive"), + ] + } else { + &[("request_id", "request"), ("response_id", "response")] + }; + for (field, suffix) in suffixes { + out.push((ids[field], format!("{method}_{suffix}"))); + } + } + out + } + + /// A single subscription method must reserve four consecutive wire + /// ids (start/stop/interrupt/receive) even when no sibling methods + /// exist to mask off-by-one errors. + #[test] + fn wire_table_subscribe_method_reserves_four_ids() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + module_path: Vec::new(), + methods: vec![make_subscription_method("connection_status_subscribe", 18)], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + + let src = generate_wire_table(&api).expect("generate_wire_table"); + let entries = parse_entries(&src); + assert_eq!( + entries, + vec![ + (18, "account_connection_status_subscribe_start".into()), + (19, "account_connection_status_subscribe_stop".into()), + (20, "account_connection_status_subscribe_interrupt".into()), + (21, "account_connection_status_subscribe_receive".into()), + ], + ); + } + + /// Two traits each declaring a method named `submit` must produce two + /// distinct, non-colliding wire method keys; the emitter prefixes by + /// the snake_case trait name (e.g. `statement_store_submit` / + /// `preimage_submit`). + #[test] + fn collision_safe_when_two_traits_share_method_name() { + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "StatementStore".to_string(), + module_path: Vec::new(), + methods: vec![make_request_method("submit", 62)], + docs: None, + }, + TraitDef { + name: "Preimage".to_string(), + module_path: Vec::new(), + methods: vec![make_request_method("submit", 68)], + docs: None, + }, + ], + public_trait_order: vec!["StatementStore".to_string(), "Preimage".to_string()], + types: versioned_request_test_types(), + }; + + let dispatcher = generate_dispatcher(&api).expect("dispatcher"); + assert!( + dispatcher.contains("wire_table::STATEMENT_STORE_SUBMIT"), + "dispatcher missing prefixed StatementStore const:\n{dispatcher}" + ); + assert!( + dispatcher.contains("wire_table::PREIMAGE_SUBMIT"), + "dispatcher missing prefixed Preimage const:\n{dispatcher}" + ); + + let table = generate_wire_table(&api).expect("wire_table"); + let entries = parse_entries(&table); + assert!( + entries + .iter() + .any(|(_, tag)| tag == "statement_store_submit_request"), + "wire_table missing prefixed StatementStore tag:\n{table}" + ); + assert!( + entries + .iter() + .any(|(_, tag)| tag == "preimage_submit_request"), + "wire_table missing prefixed Preimage tag:\n{table}" + ); + } + + /// If a future change ever produces the same wire method key from two + /// different (trait, method) pairs, both emitters must fail loudly + /// rather than silently overwrite a handler. + #[test] + fn wire_table_rejects_method_name_collision() { + // `Foo::bar_baz` and `FooBar::baz` both snake-case to + // `foo_bar_baz`. The emitter must reject the pair. + let api = ApiDefinition { + traits: vec![ + TraitDef { + name: "Foo".to_string(), + module_path: Vec::new(), + methods: vec![make_request_method("bar_baz", 10)], + docs: None, + }, + TraitDef { + name: "FooBar".to_string(), + module_path: Vec::new(), + methods: vec![make_request_method("baz", 12)], + docs: None, + }, + ], + public_trait_order: vec!["Foo".to_string(), "FooBar".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("duplicate wire method name must error"); + let msg = format!("{err}"); + assert!( + msg.contains("wire method name `foo_bar_baz` reused"), + "unexpected error message: {msg}", + ); + + let err = generate_dispatcher(&api).expect_err("duplicate wire method name must error"); + let msg = format!("{err}"); + assert!( + msg.contains("Wire method name `foo_bar_baz` registered twice"), + "unexpected dispatcher error message: {msg}", + ); + } + + /// Emission must be deterministic: running the codegen twice on the + /// same API produces byte-identical output. + #[test] + fn idempotent_emission() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![make_request_method("request_device_permission", 8)], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: versioned_request_test_types(), + }; + + let dispatcher_a = generate_dispatcher(&api).expect("dispatcher a"); + let dispatcher_b = generate_dispatcher(&api).expect("dispatcher b"); + assert_eq!(dispatcher_a, dispatcher_b); + + let table_a = generate_wire_table(&api).expect("wire_table a"); + let table_b = generate_wire_table(&api).expect("wire_table b"); + assert_eq!(table_a, table_b); + } + + /// Methods with a `#[wire(request_id = N)]` annotation get a 2-id + /// slot (request/response). Methods with `#[wire(start_id = N)]` + /// get a 4-id slot (start/stop/interrupt/receive). The emitter + /// must enforce that, and reject collisions. + #[test] + fn wire_table_rejects_collisions() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![ + make_request_method("alpha", 10), + make_request_method("beta", 10), + ], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("duplicate ids must error"); + let msg = format!("{err}"); + assert!( + msg.contains("wire id 10 reused"), + "unexpected error message: {msg}", + ); + } + + /// Pin `wire_const_name`'s `convert_case::Case::UpperSnake` behavior: + /// digits split off (`v2` -> `V_2`) and acronyms split (`HTTPServer` + /// snake-cases to `h_t_t_p_server`, then upper-snakes to + /// `H_T_T_P_SERVER`). Real traits/methods avoid both, so the committed + /// output is unaffected; the pin guards future drift. + #[test] + fn wire_const_name_pins_digits_and_acronyms() { + assert_eq!(wire_const_name("Preimage", "submit"), "PREIMAGE_SUBMIT"); + assert_eq!(wire_const_name("Signing", "sign_v2"), "SIGNING_SIGN_V_2"); + assert_eq!( + wire_const_name("HTTPServer", "serve"), + "H_T_T_P_SERVER_SERVE" + ); + assert_eq!( + wire_const_name("StatementStore", "create_proof"), + "STATEMENT_STORE_CREATE_PROOF" + ); + } + + #[test] + fn module_for_trait_maps_irregular_names() { + assert_eq!(module_for_trait("JsonRpc"), "jsonrpc"); + assert_eq!(module_for_trait("LocalStorage"), "local_storage"); + assert_eq!( + module_for_trait("ResourceAllocation"), + "resource_allocation" + ); + assert_eq!(module_for_trait("Account"), "account"); + } + + /// A request-kind method must not carry subscription wire ids. The + /// emitter rejects `start_id` / `stop_id` / `interrupt_id` / `receive_id` + /// on a `MethodKind::Request`. + #[test] + fn wire_table_request_with_subscription_id_errors() { + let mut method = make_request_method("alpha", 10); + method.wire.start_id = Some(99); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("request kind + start_id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("must not use subscription wire ids"), + "unexpected error message: {msg}", + ); + } + + /// A subscription-kind method must not carry request wire ids. + #[test] + fn wire_table_subscription_with_request_id_errors() { + let mut method = make_subscription_method("connection_status_subscribe", 18); + method.wire.request_id = Some(99); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("subscription kind + request_id must error"); + let msg = format!("{err}"); + assert!( + msg.contains("must not use request wire ids"), + "unexpected error message: {msg}", + ); + } + + /// A request-kind method missing the mandatory `request_id` annotation + /// must fail emission, not silently default to 0. + #[test] + fn wire_table_missing_request_id_errors() { + let mut method = make_request_method("alpha", 10); + method.wire.request_id = None; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("missing request_id annotation must error"); + let msg = format!("{err}"); + assert!( + msg.contains("missing #[wire(request_id"), + "unexpected error message: {msg}", + ); + } + + /// Subscription-kind method missing `start_id` is similarly rejected. + #[test] + fn wire_table_missing_start_id_errors() { + let mut method = make_subscription_method("connection_status_subscribe", 18); + method.wire.start_id = None; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Account".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Account".to_string()], + types: vec![], + }; + let err = generate_wire_table(&api).expect_err("missing start_id annotation must error"); + let msg = format!("{err}"); + assert!( + msg.contains("missing #[wire(start_id"), + "unexpected error message: {msg}", + ); + } + + /// The dispatcher expects each method to take exactly one versioned + /// wrapper parameter (plus `&self` and `&CallContext`, which are + /// elided from `params`). A method with two params errors out. + #[test] + fn dispatcher_multi_param_method_errors() { + let mut method = make_request_method("alpha", 10); + method.params.push(ParamDef { + name: "extra".to_string(), + type_ref: TypeRef::Named { + name: "ExtraWrapper".to_string(), + args: vec![], + }, + }); + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_dispatcher(&api).expect_err("two-param method must error"); + let msg = format!("{err}"); + assert!( + msg.contains("expected at most one request parameter"), + "unexpected error message: {msg}", + ); + } + + /// The response wrapper extraction expects a `TypeRef::Named` with no + /// generic args. Anything else (primitives, tuples, generics) errors. + #[test] + fn dispatcher_non_named_root_response_errors() { + let mut method = make_request_method("alpha", 10); + method.return_type = ReturnType::Result { + ok: TypeRef::Primitive("u32".to_string()), + err: TypeRef::Named { + name: "CallError".to_string(), + args: vec![TypeRef::Named { + name: "ErrWrapper".to_string(), + args: vec![], + }], + }, + }; + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Permissions".to_string(), + module_path: Vec::new(), + methods: vec![method], + docs: None, + }], + public_trait_order: vec!["Permissions".to_string()], + types: vec![], + }; + let err = generate_dispatcher(&api).expect_err("primitive response must error"); + let msg = format!("{err}"); + assert!( + msg.contains("response is not a versioned wrapper"), + "unexpected error message: {msg}", + ); + } +} diff --git a/rust/crates/truapi-codegen/src/rust/dispatcher.rs b/rust/crates/truapi-codegen/src/rust/dispatcher.rs new file mode 100644 index 00000000..5155e99c --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/dispatcher.rs @@ -0,0 +1,740 @@ +//! Emits `dispatcher.rs`: the server-side wire dispatcher that routes +//! incoming frames to the host trait implementation. +//! +//! For each method the emitter produces an `on_request` (or +//! `on_subscription`) registration that: +//! 1. SCALE-decodes the versioned request wrapper from the wire bytes. +//! 2. Calls the host trait method (which receives the wrapper directly +//! and matches `_::V1(inner)` internally). +//! 3. SCALE-encodes the versioned response wrapper back onto the wire. +//! +//! The generated file expects to live inside a `truapi-server` crate +//! and references `crate::dispatcher::Dispatcher`. The codegen itself +//! does not compile the output; string-diff golden tests guard it. + +use std::collections::BTreeMap; +use std::collections::BTreeSet; +use std::fmt::Write; + +use anyhow::{Result, bail}; +use indoc::{formatdoc, indoc, writedoc}; + +use crate::rustdoc::*; + +use super::{const_name, module_for_trait, wire_method_name}; + +/// Emit the contents of `dispatcher.rs`. +pub fn generate_dispatcher(api: &ApiDefinition) -> Result { + let traits = order_traits(api)?; + + // Reject any duplicate wire method name across traits before emission, so + // a future addition can't silently overwrite a handler in the HashMap. + let mut seen: BTreeSet = BTreeSet::new(); + for trait_def in &traits { + for method in &trait_def.methods { + let key = wire_method_name(&trait_def.name, &method.name); + if !seen.insert(key.clone()) { + bail!( + "Wire method name `{key}` registered twice; \ + change `{}::{}` or its sibling trait to disambiguate", + trait_def.name, + method.name + ); + } + } + } + + let mut modules = Vec::with_capacity(traits.len()); + for trait_def in &traits { + modules.push(build_module(api, trait_def)?); + } + + let mut out = String::new(); + write_header(&mut out); + write_imports(&mut out, &traits); + writeln!(out).unwrap(); + write_top_register(&mut out, &traits); + + for module in &modules { + writeln!(out).unwrap(); + out.push_str(module); + } + + Ok(out) +} + +/// Returns the traits to emit, in the order declared by the top-level +/// `TrUApi` super-trait. Falls back to alphabetical order if the +/// extractor did not record a public ordering (e.g. synthetic tests). +fn order_traits(api: &ApiDefinition) -> Result> { + let by_name: BTreeMap<&str, &TraitDef> = + api.traits.iter().map(|t| (t.name.as_str(), t)).collect(); + + if api.public_trait_order.is_empty() { + return Ok(api.traits.iter().collect()); + } + + let mut ordered = Vec::with_capacity(api.public_trait_order.len()); + for name in &api.public_trait_order { + let Some(trait_def) = by_name.get(name.as_str()) else { + bail!("trait `{name}` appears in TrUApi but was not extracted"); + }; + ordered.push(*trait_def); + } + Ok(ordered) +} + +/// Emit the `register_{module}` function for a single trait. +fn build_module(api: &ApiDefinition, trait_def: &TraitDef) -> Result { + let module = module_for_trait(&trait_def.name); + + let mut methods = Vec::with_capacity(trait_def.methods.len()); + for method in &trait_def.methods { + let wire_method = wire_method_name(&trait_def.name, &method.name); + methods.push(MethodEmission::build(api, &module, &wire_method, method)?); + } + + let fn_name = format!("register_{module}"); + let trait_name = &trait_def.name; + let mut code = String::new(); + if trait_name == "Testing" { + writeln!(code, "#[cfg(debug_assertions)]").unwrap(); + } + writedoc!( + code, + r#" + fn {fn_name}

(dispatcher: &mut Dispatcher, host: Arc

) + where + P: {trait_name} + Send + Sync + 'static, + {{ + "# + ) + .unwrap(); + let last = methods.len().saturating_sub(1); + for (idx, method) in methods.iter().enumerate() { + let host_expr = if idx == last { "host" } else { "host.clone()" }; + method.write(&mut code, host_expr); + } + writeln!(code, "}}").unwrap(); + + Ok(code) +} + +struct MethodEmission { + /// Rust method name on the host trait (used for the `host.(...)` call). + name: String, + /// Fully-qualified wire method name (`{trait_snake}_{method}`); uppercased + /// to the `wire_table` const this method registers against. + wire_name: String, + module: String, + kind: MethodKind, + request_payload: Option, + response_wrapper: Option, + error_payload: WirePayload, + item_wrapper: Option, +} + +#[derive(Clone)] +enum WirePayload { + Versioned(String), + Raw(TypeRef), +} + +impl MethodEmission { + fn build( + api: &ApiDefinition, + module: &str, + wire_method: &str, + method: &MethodDef, + ) -> Result { + let versioned_wrappers = versioned_wrapper_names(api); + let request_payload = match method.params.as_slice() { + [] => None, + [param] => match ¶m.type_ref { + TypeRef::Named { name, args } + if args.is_empty() && versioned_wrappers.contains(name) => + { + Some(WirePayload::Versioned(name.clone())) + } + _ => Some(WirePayload::Raw(param.type_ref.clone())), + }, + _ => bail!( + "Method `{}`: expected at most one request parameter (got {})", + method.name, + method.params.len() + ), + }; + + let error_payload = match &method.return_type { + ReturnType::Result { err, .. } | ReturnType::ResultSubscription { err, .. } => { + wire_payload_for_error(&method.name, err, &versioned_wrappers)? + } + ReturnType::Subscription(_) => WirePayload::Raw(TypeRef::Unit), + }; + + let (response_wrapper, item_wrapper) = match &method.return_type { + // `Result<(), _>` returns produce an empty wire payload. + // The trait method is called for its side effects and the + // dispatcher encodes `()` (zero bytes) on success. + ReturnType::Result { + ok: TypeRef::Unit, .. + } => (None, None), + ReturnType::Result { ok, .. } => ( + Some( + versioned_wrapper_root(&method.name, "response", ok, &versioned_wrappers)? + .to_string(), + ), + None, + ), + ReturnType::Subscription(item) => ( + None, + Some( + versioned_wrapper_root( + &method.name, + "subscription item", + item, + &versioned_wrappers, + )? + .to_string(), + ), + ), + ReturnType::ResultSubscription { item, .. } => ( + None, + Some( + versioned_wrapper_root( + &method.name, + "subscription item", + item, + &versioned_wrappers, + )? + .to_string(), + ), + ), + }; + + Ok(MethodEmission { + name: method.name.clone(), + wire_name: wire_method.to_string(), + module: module.to_string(), + kind: method.kind, + request_payload, + response_wrapper, + error_payload, + item_wrapper, + }) + } + + fn write(&self, out: &mut String, host_expr: &str) { + match self.kind { + MethodKind::Request => self.write_request(out, host_expr), + MethodKind::Subscription | MethodKind::ResultSubscription => { + self.write_subscription(out, host_expr) + } + } + } + + fn write_request(&self, out: &mut String, host_expr: &str) { + let module = &self.module; + let method = &self.name; + let ids = const_name(&self.wire_name); + + write_indented( + out, + 4, + &formatdoc! { + r#" + {{ + let host = {host_expr}; + dispatcher.on_request(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + let (call_args, target_version_expr) = match &self.request_payload { + Some(WirePayload::Versioned(request)) => { + let error = self + .error_payload + .versioned_name() + .expect("versioned request methods must use versioned errors"); + write_indented( + out, + 16, + &formatdoc! { + r#" + let request: versioned::{module}::{request} = match Decode::decode(&mut &bytes[..]) {{ + Ok(request) => request, + Err(err) => {{ + let error: truapi::CallError = + truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + }} + }}; + let target_version = request.version(); + "# + }, + ); + ( + "&cx, request".to_string(), + Some("target_version".to_string()), + ) + } + Some(WirePayload::Raw(request)) => { + let request_ty = rust_type_ref(request).expect("raw request type"); + let error_ty = self + .error_payload + .rust_error_type(module) + .expect("raw request methods must have error type"); + write_indented( + out, + 16, + &formatdoc! { + r#" + let request: {request_ty} = match Decode::decode(&mut &bytes[..]) {{ + Ok(request) => request, + Err(err) => {{ + let error: truapi::CallError<{error_ty}> = + truapi::CallError::MalformedFrame {{ reason: err.to_string() }}; + return Ok(encode_raw_err_payload(error)); + }} + }}; + "# + }, + ); + ("&cx, request".to_string(), None) + } + None => { + writeln!(out, " let _ = bytes;").unwrap(); + let target = self + .error_payload + .versioned_name() + .map(|error| format!("::LATEST")); + ("&cx".to_string(), target) + } + }; + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + match &self.response_wrapper { + Some(response) => { + let target_version_expr = target_version_expr + .as_deref() + .expect("versioned responses require a target version"); + write_indented( + out, + 16, + &formatdoc! { + r#" + let response: versioned::{module}::{response} = match host.{method}({call_args}).await {{ + Ok(value) => value, + Err(err) => {{ + return Ok(encode_versioned_err_payload(err, {target_version_expr})); + }} + }}; + Ok(encode_versioned_ok_payload(response)) + "# + }, + ); + } + None => match (&self.error_payload, target_version_expr.as_deref()) { + (WirePayload::Versioned(_), Some(target_version_expr)) => { + write_indented( + out, + 16, + &formatdoc! { + r#" + match host.{method}({call_args}).await {{ + Ok(()) => Ok(encode_versioned_unit_ok_payload({target_version_expr})), + Err(err) => {{ + Ok(encode_versioned_err_payload(err, {target_version_expr})) + }} + }} + "# + }, + ); + } + (WirePayload::Raw(_), _) => { + write_indented( + out, + 16, + &formatdoc! { + r#" + match host.{method}({call_args}).await {{ + Ok(()) => Ok(encode_raw_unit_ok_payload()), + Err(err) => Ok(encode_raw_err_payload(err)), + }} + "# + }, + ); + } + (WirePayload::Versioned(_), None) => unreachable!("missing versioned target"), + }, + } + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + } + + fn write_subscription(&self, out: &mut String, host_expr: &str) { + let module = &self.module; + let method = &self.name; + let ids = const_name(&self.wire_name); + let item = self + .item_wrapper + .as_deref() + .expect("subscription methods must have an item wrapper"); + let error = self.error_payload.versioned_name(); + + let is_result_sub = matches!(self.kind, MethodKind::ResultSubscription); + + write_indented( + out, + 4, + &formatdoc! { + r#" + {{ + let host = {host_expr}; + dispatcher.on_subscription(wire_table::{ids}, move |request_id: String, bytes: Vec| {{ + let host = host.clone(); + Box::pin(async move {{ + "# + }, + ); + let (call_args, target_version_expr) = if let Some(WirePayload::Versioned(request)) = + &self.request_payload + { + let decode_error = match error { + Some(error) => { + let block = formatdoc! { + r#" + Err(err) => {{ + let error: truapi::CallError = + truapi::CallError::MalformedFrame {{ + reason: err.to_string(), + }}; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + }} + "# + }; + block + .lines() + .map(|line| format!(" {line}")) + .collect::>() + .join("\n") + } + None => " Err(_) => return Err(Vec::new()),".to_string(), + }; + write_indented( + out, + 16, + &formatdoc! { + r#" + let request: versioned::{module}::{request} = match Decode::decode(&mut &bytes[..]) {{ + Ok(request) => request, + {decode_error} + }}; + "# + }, + ); + if is_result_sub { + writeln!( + out, + " let target_version = request.version();" + ) + .unwrap(); + } + ("&cx, request".to_string(), "target_version".to_string()) + } else { + writeln!(out, " let _ = bytes;").unwrap(); + let target_version = error + .map(|error| format!("::LATEST")) + .unwrap_or_else(|| "1".to_string()); + ("&cx".to_string(), target_version) + }; + writeln!( + out, + " let cx = CallContext::with_request_id(request_id.clone());" + ) + .unwrap(); + if is_result_sub { + let _ = error.expect("result subscription methods must have an error wrapper"); + write_indented( + out, + 16, + &formatdoc! { + r#" + let stream = match host.{method}({call_args}).await {{ + Ok(sub) => sub, + Err(err) => {{ + return Err(encode_versioned_interrupt_payload(err, {target_version_expr})); + }} + }}; + "# + }, + ); + } else { + writeln!( + out, + " let stream = host.{method}({call_args}).await;" + ) + .unwrap(); + } + writeln!( + out, + " Ok(subscription_stream::(stream))" + ) + .unwrap(); + write_indented( + out, + 4, + indoc! { + r#" + }) + }); + } + "# + }, + ); + } +} + +impl WirePayload { + fn versioned_name(&self) -> Option<&str> { + match self { + Self::Versioned(name) => Some(name), + Self::Raw(_) => None, + } + } + + fn rust_error_type(&self, module: &str) -> Result { + match self { + Self::Versioned(name) => Ok(format!("versioned::{module}::{name}")), + Self::Raw(ty) => rust_type_ref(ty), + } + } +} + +fn wire_payload_for_error( + method: &str, + ty: &TypeRef, + versioned_wrappers: &BTreeSet, +) -> Result { + let inner = call_error_inner(ty).unwrap_or(ty); + match inner { + TypeRef::Named { name, args } if args.is_empty() && versioned_wrappers.contains(name) => { + Ok(WirePayload::Versioned(name.clone())) + } + _ => { + if matches!(inner, TypeRef::Unit) { + bail!("Method `{method}`: error type cannot be unit") + } + Ok(WirePayload::Raw(inner.clone())) + } + } +} + +fn versioned_wrapper_root<'a>( + method: &str, + role: &str, + ty: &'a TypeRef, + versioned_wrappers: &BTreeSet, +) -> Result<&'a str> { + let TypeRef::Named { name, args } = ty else { + bail!("Method `{method}`: {role} is not a versioned wrapper") + }; + if !args.is_empty() || !versioned_wrappers.contains(name) { + bail!("Method `{method}`: {role} is not a versioned wrapper") + } + Ok(name) +} + +fn versioned_wrapper_names(api: &ApiDefinition) -> BTreeSet { + api.types + .iter() + .filter_map(|ty| { + let TypeDefKind::Enum(variants) = &ty.kind else { + return None; + }; + if variants.iter().all(|variant| { + variant + .name + .strip_prefix('V') + .is_some_and(|version| version.parse::().is_ok()) + }) { + Some(ty.name.clone()) + } else { + None + } + }) + .collect() +} + +fn rust_type_ref(ty: &TypeRef) -> Result { + match ty { + TypeRef::Primitive(name) => Ok(match name.as_str() { + "str" => "String".to_string(), + "compact" => "u128".to_string(), + "optionBool" => "parity_scale_codec::OptionBool".to_string(), + other => other.to_string(), + }), + TypeRef::Named { name, args } if name == "CallError" && args.len() == 1 => { + Ok(format!("truapi::CallError<{}>", rust_type_ref(&args[0])?)) + } + TypeRef::Named { name, args } if args.is_empty() => { + if let Some((version, base)) = version_prefixed_type(name) { + Ok(format!("truapi::v{version:02}::{base}")) + } else { + Ok(format!("truapi::v01::{name}")) + } + } + TypeRef::Named { name, args } => { + let args = args + .iter() + .map(rust_type_ref) + .collect::>>()? + .join(", "); + Ok(format!("truapi::v01::{name}<{args}>")) + } + TypeRef::Vec(inner) => Ok(format!("Vec<{}>", rust_type_ref(inner)?)), + TypeRef::Option(inner) => Ok(format!("Option<{}>", rust_type_ref(inner)?)), + TypeRef::Tuple(items) => { + let items = items + .iter() + .map(rust_type_ref) + .collect::>>()? + .join(", "); + Ok(format!("({items})")) + } + TypeRef::Array(inner, len) => Ok(format!("[{}; {len}]", rust_type_ref(inner)?)), + TypeRef::Generic(name) => Ok(name.clone()), + TypeRef::Unit => Ok("()".to_string()), + } +} + +fn version_prefixed_type(name: &str) -> Option<(u32, &str)> { + let rest = name.strip_prefix('V')?; + if rest.len() < 3 { + return None; + } + let (version, base) = rest.split_at(2); + if base.is_empty() { + return None; + } + Some((version.parse().ok()?, base)) +} + +fn call_error_inner(ty: &TypeRef) -> Option<&TypeRef> { + match ty { + TypeRef::Named { name, args } if name == "CallError" && args.len() == 1 => Some(&args[0]), + _ => None, + } +} + +/// Append `block` to `out`, prefixing every non-empty line with `indent` spaces. +fn write_indented(out: &mut String, indent: usize, block: &str) { + let pad = " ".repeat(indent); + for line in block.lines() { + if line.is_empty() { + out.push('\n'); + } else { + writeln!(out, "{pad}{line}").unwrap(); + } + } +} + +fn write_header(out: &mut String) { + writedoc!( + out, + r#" + //! Wire dispatcher for the unified `TrUApi` trait. + //! + //! Auto-generated by truapi-codegen. Do not edit. + + "# + ) + .unwrap(); +} + +fn write_imports(out: &mut String, traits: &[&TraitDef]) { + let has_testing = traits.iter().any(|trait_def| trait_def.name == "Testing"); + writedoc!( + out, + r#" + use std::sync::Arc; + + use parity_scale_codec::Decode; + + use truapi::CallContext; + use truapi::api::{{ + "# + ) + .unwrap(); + for trait_def in traits { + if trait_def.name == "Testing" { + continue; + } + writeln!(out, " {},", trait_def.name).unwrap(); + } + writedoc!( + out, + r#" + }}; + use truapi::versioned::{{self, Versioned}}; + + use crate::dispatcher::Dispatcher; + use crate::frame::encode_raw_err_payload; + use crate::frame::encode_raw_unit_ok_payload; + use crate::frame::encode_versioned_err_payload; + use crate::frame::encode_versioned_interrupt_payload; + use crate::frame::encode_versioned_ok_payload; + use crate::frame::encode_versioned_unit_ok_payload; + use crate::generated::wire_table; + use crate::subscription::subscription_stream; + "# + ) + .unwrap(); + if has_testing { + writeln!(out, "#[cfg(debug_assertions)]").unwrap(); + writeln!(out, "use truapi::api::Testing;").unwrap(); + } +} + +fn write_top_register(out: &mut String, traits: &[&TraitDef]) { + writedoc!( + out, + r#" + /// Register every TrUAPI method with the dispatcher. + pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) + where + P: truapi::api::TrUApi + 'static, + {{ + "# + ) + .unwrap(); + let last = traits.len().saturating_sub(1); + for (idx, trait_def) in traits.iter().enumerate() { + let host_expr = if idx == last { "host" } else { "host.clone()" }; + let module = module_for_trait(&trait_def.name); + if trait_def.name == "Testing" { + writeln!(out, " #[cfg(debug_assertions)]").unwrap(); + } + writeln!(out, " register_{module}(dispatcher, {host_expr});").unwrap(); + } + writeln!(out, "}}").unwrap(); +} diff --git a/rust/crates/truapi-codegen/src/rust/wire_table.rs b/rust/crates/truapi-codegen/src/rust/wire_table.rs new file mode 100644 index 00000000..6c63a4f8 --- /dev/null +++ b/rust/crates/truapi-codegen/src/rust/wire_table.rs @@ -0,0 +1,303 @@ +//! Emits `wire_table.rs`: the (id, tag) lookup table the server uses to +//! pair incoming wire frames with their request, response, or +//! subscription role. +//! +//! Per-method `#[wire(...)]` annotations decide id assignment: +//! - request methods reserve `(request_id, response_id)`. +//! - subscription methods reserve `(start_id, stop_id, interrupt_id, receive_id)`. +//! +//! Missing annotations and collisions both hard-fail codegen. + +use std::collections::BTreeMap; +use std::fmt::Write; + +use anyhow::{Result, bail}; +use indoc::{formatdoc, writedoc}; + +use crate::rustdoc::*; + +use super::{const_name, wire_method_name}; + +#[derive(Debug, Clone, Copy)] +struct WireEntry { + request_id: u8, + response_id: u8, +} + +#[derive(Debug, Clone, Copy)] +struct SubEntry { + start_id: u8, + stop_id: u8, + interrupt_id: u8, + receive_id: u8, +} + +#[derive(Debug, Clone, Copy)] +enum MethodEntry { + Request(WireEntry), + Subscription(SubEntry), +} + +/// Emit the contents of `wire_table.rs`. +pub fn generate_wire_table(api: &ApiDefinition) -> Result { + let mut method_entries: Vec<(String, MethodEntry, bool)> = Vec::new(); + let mut seen: BTreeMap = BTreeMap::new(); + let mut seen_methods: BTreeMap = BTreeMap::new(); + + for trait_def in &api.traits { + for method in &trait_def.methods { + let entry = method_entry(trait_def, method)?; + let wire_method = wire_method_name(&trait_def.name, &method.name); + if let Some(existing) = seen_methods.insert( + wire_method.clone(), + format!("{}::{}", trait_def.name, method.name), + ) { + bail!( + "wire method name `{wire_method}` reused: `{existing}` and `{}::{}` collide", + trait_def.name, + method.name + ); + } + insert_entry(&mut seen, &wire_method, entry)?; + method_entries.push((wire_method, entry, trait_def.name == "Testing")); + } + } + + method_entries.sort_by_key(|(_, entry, _)| match entry { + MethodEntry::Request(WireEntry { request_id, .. }) => *request_id, + MethodEntry::Subscription(SubEntry { start_id, .. }) => *start_id, + }); + + render(&method_entries) +} + +fn method_entry(trait_def: &TraitDef, method: &MethodDef) -> Result { + let wire = &method.wire; + match method.kind { + MethodKind::Request => { + if wire.start_id.is_some() + || wire.stop_id.is_some() + || wire.interrupt_id.is_some() + || wire.receive_id.is_some() + { + bail!( + "method `{}::{}` is a request and must not use subscription wire ids", + trait_def.name, + method.name + ); + } + let request_id = wire.request_id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(request_id = N)] annotation", + trait_def.name, + method.name + ) + })?; + let response_id = infer_id(wire.response_id, request_id, 1, &method.name)?; + Ok(MethodEntry::Request(WireEntry { + request_id, + response_id, + })) + } + MethodKind::Subscription | MethodKind::ResultSubscription => { + if wire.request_id.is_some() || wire.response_id.is_some() { + bail!( + "method `{}::{}` is a subscription and must not use request wire ids", + trait_def.name, + method.name + ); + } + let start_id = wire.start_id.ok_or_else(|| { + anyhow::anyhow!( + "method `{}::{}` is missing #[wire(start_id = N)] annotation", + trait_def.name, + method.name + ) + })?; + let stop_id = infer_id(wire.stop_id, start_id, 1, &method.name)?; + let interrupt_id = infer_id(wire.interrupt_id, start_id, 2, &method.name)?; + let receive_id = infer_id(wire.receive_id, start_id, 3, &method.name)?; + Ok(MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + })) + } + } +} + +fn infer_id(explicit: Option, anchor: u8, offset: u8, method_name: &str) -> Result { + if let Some(id) = explicit { + return Ok(id); + } + anchor + .checked_add(offset) + .ok_or_else(|| anyhow::anyhow!("wire id overflow on `{method_name}` (base {anchor})")) +} + +fn insert_entry( + seen: &mut BTreeMap, + method_name: &str, + entry: MethodEntry, +) -> Result<()> { + let pairs: Vec<(u8, String)> = match entry { + MethodEntry::Request(WireEntry { + request_id, + response_id, + }) => vec![ + (request_id, format!("{method_name}_request")), + (response_id, format!("{method_name}_response")), + ], + MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + }) => vec![ + (start_id, format!("{method_name}_start")), + (stop_id, format!("{method_name}_stop")), + (interrupt_id, format!("{method_name}_interrupt")), + (receive_id, format!("{method_name}_receive")), + ], + }; + for (id, tag) in pairs { + if let Some(existing) = seen.insert(id, tag.clone()) { + bail!("wire id {id} reused: `{existing}` and `{tag}` collide"); + } + } + Ok(()) +} + +fn render(methods: &[(String, MethodEntry, bool)]) -> Result { + let mut out = String::new(); + writedoc!( + out, + r#" + //! Wire-protocol discriminant table. + //! + //! Auto-generated by truapi-codegen. Do not edit. + //! + //! Each method reserves either two ids (request/response) or four + //! (start/stop/interrupt/receive). The ids for each method are exposed + //! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the + //! generated dispatcher both reference those consts so the numbers live + //! in exactly one place. The table is sorted by request/start id. + + /// Request method wire discriminants. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct RequestFrameIds {{ + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, + }} + + /// Subscription method wire discriminants. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct SubscriptionFrameIds {{ + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, + }} + + /// A single wire-table row. + pub struct WireEntry {{ + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, + }} + + /// Wire-slot shape: request/response pair or subscription quartet. + pub enum WireKind {{ + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), + }} + "# + ) + .unwrap(); + + // Per-method consts: the single source of truth for each method's ids. + for (name, entry, debug_only) in methods { + let konst = const_name(name); + if *debug_only { + out.push('\n'); + out.push_str("#[cfg(debug_assertions)]"); + } + let block = match entry { + MethodEntry::Request(WireEntry { + request_id, + response_id, + }) => formatdoc! { + r#" + /// Wire discriminants for `{name}`. + pub const {konst}: RequestFrameIds = RequestFrameIds {{ + request_id: {request_id}, + response_id: {response_id}, + }}; + "# + }, + MethodEntry::Subscription(SubEntry { + start_id, + stop_id, + interrupt_id, + receive_id, + }) => formatdoc! { + r#" + /// Wire discriminants for `{name}`. + pub const {konst}: SubscriptionFrameIds = SubscriptionFrameIds {{ + start_id: {start_id}, + stop_id: {stop_id}, + interrupt_id: {interrupt_id}, + receive_id: {receive_id}, + }}; + "# + }, + }; + out.push('\n'); + out.push_str(&block); + } + + out.push('\n'); + writedoc!( + out, + r#" + /// The full wire table. Ordering is part of the wire protocol; + /// only ever append. Removed methods leave their slot empty. + pub const WIRE_TABLE: &[WireEntry] = &[ + "# + ) + .unwrap(); + for (name, entry, debug_only) in methods { + let konst = const_name(name); + let variant = match entry { + MethodEntry::Request(_) => "Request", + MethodEntry::Subscription(_) => "Subscription", + }; + let block = formatdoc! { + r#" + WireEntry {{ + method: "{name}", + kind: WireKind::{variant}({konst}), + }}, + "# + }; + if *debug_only { + writeln!(out, " #[cfg(debug_assertions)]").unwrap(); + } + for line in block.lines() { + writeln!(out, " {line}").unwrap(); + } + } + writeln!(out, "];").unwrap(); + + Ok(out) +} diff --git a/rust/crates/truapi-codegen/src/rustdoc.rs b/rust/crates/truapi-codegen/src/rustdoc.rs index 5cc8c5a9..0dd136ee 100644 --- a/rust/crates/truapi-codegen/src/rustdoc.rs +++ b/rust/crates/truapi-codegen/src/rustdoc.rs @@ -6,9 +6,17 @@ use std::collections::{BTreeMap, HashMap}; use anyhow::{Context, Result, bail}; use serde::Deserialize; +/// Minimum rustdoc JSON `format_version` the extractors are tested against. +/// Emitted by nightly 2026-02-23 (rustc 1.95.0-nightly); older formats may +/// encode item shapes differently and are rejected outright. +const MIN_FORMAT_VERSION: u32 = 57; + /// Parsed rustdoc crate. IDs are integers but serialized as string keys in JSON maps. #[derive(Debug, Deserialize)] pub struct Crate { + /// rustdoc JSON format version stamped into the document. + #[serde(default)] + pub format_version: Option, pub index: HashMap, #[serde(default)] pub paths: HashMap, @@ -220,7 +228,7 @@ struct ItemCandidate { } #[derive(Debug, Default)] -struct NameContext { +pub(crate) struct NameContext { by_item_id: HashMap, by_path: HashMap, } @@ -242,9 +250,23 @@ impl NameContext { } /// Parses rustdoc JSON output into the minimal crate model used by the code -/// generator. +/// generator. Rejects documents older than [`MIN_FORMAT_VERSION`] because the +/// untyped walkers in this crate assume the format of recent nightlies. pub fn parse(json: &str) -> Result { - serde_json::from_str(json).context("Failed to parse rustdoc JSON") + let krate: Crate = serde_json::from_str(json).context("Failed to parse rustdoc JSON")?; + let Some(version) = krate.format_version else { + bail!( + "rustdoc JSON is missing `format_version`; regenerate it with \ + `cargo +nightly rustdoc --output-format json` (nightly 2026-02-23 or later)" + ); + }; + if version < MIN_FORMAT_VERSION { + bail!( + "rustdoc JSON format_version {version} is older than the tested minimum \ + {MIN_FORMAT_VERSION}; regenerate with nightly 2026-02-23 or later" + ); + } + Ok(krate) } /// Extracts the public traits and types that make up the generated API surface @@ -433,8 +455,11 @@ fn disambiguated_type_name(simple_name: &str, path: &[String]) -> String { if path.iter().any(|segment| segment == "versioned") { return simple_name.to_string(); } - if path.iter().any(|segment| segment == "v01") { - return format!("V01{simple_name}"); + if let Some(version) = path + .iter() + .find_map(|segment| version_module_number(segment)) + { + return format!("V{version:02}{simple_name}"); } let module = path .iter() @@ -445,6 +470,12 @@ fn disambiguated_type_name(simple_name: &str, path: &[String]) -> String { format!("{module}{simple_name}") } +fn version_module_number(segment: &str) -> Option { + segment + .strip_prefix('v') + .and_then(|value| value.parse::().ok()) +} + fn to_pascal_case(value: &str) -> String { value .split('_') @@ -853,7 +884,8 @@ fn extract_generic_arg( resolve_type(&generic, names) } -fn resolve_type(ty: &serde_json::Value, names: &NameContext) -> Result { +/// Resolve a rustdoc JSON type node into the internal type reference model. +pub(crate) fn resolve_type(ty: &serde_json::Value, names: &NameContext) -> Result { if let Some(name) = ty.get("generic").and_then(|value| value.as_str()) { return Ok(TypeRef::Generic(name.to_string())); } @@ -994,7 +1026,8 @@ fn expect_single_arg(type_name: &str, mut args: Vec) -> Result Ok(args.remove(0)) } -fn extract_struct( +/// Extract a struct item, including field docs and generic parameters. +pub(crate) fn extract_struct( item_id: &str, item: &Item, krate: &Crate, @@ -1095,7 +1128,8 @@ fn extract_struct( }) } -fn extract_enum( +/// Extract an enum item, including variant docs and field payloads. +pub(crate) fn extract_enum( item_id: &str, item: &Item, krate: &Crate, @@ -1297,7 +1331,8 @@ fn value_id(value: &serde_json::Value) -> Result { bail!("Expected rustdoc item id, got {}", summarize_json(value)) } -fn summarize_json(value: &serde_json::Value) -> String { +/// Render a bounded JSON snippet for diagnostics. +pub(crate) fn summarize_json(value: &serde_json::Value) -> String { const LIMIT: usize = 200; let mut text = @@ -1319,4 +1354,35 @@ mod tests { assert_eq!(clean_docs(Some(docs)).as_deref(), Some("Trait summary.")); } + + #[test] + fn parse_accepts_tested_format_version() { + let json = format!(r#"{{ "format_version": {MIN_FORMAT_VERSION}, "index": {{}} }}"#); + + assert!(parse(&json).is_ok()); + } + + #[test] + fn parse_rejects_missing_format_version() { + let err = parse(r#"{ "index": {} }"#).expect_err("missing format_version must error"); + + assert!( + format!("{err}").contains("missing `format_version`"), + "unexpected error: {err}" + ); + } + + #[test] + fn parse_rejects_old_format_version() { + let json = format!( + r#"{{ "format_version": {}, "index": {{}} }}"#, + MIN_FORMAT_VERSION - 1 + ); + let err = parse(&json).expect_err("old format_version must error"); + + assert!( + format!("{err}").contains("older than the tested minimum"), + "unexpected error: {err}" + ); + } } diff --git a/rust/crates/truapi-codegen/src/ts.rs b/rust/crates/truapi-codegen/src/ts.rs index d833b542..019c33f7 100644 --- a/rust/crates/truapi-codegen/src/ts.rs +++ b/rust/crates/truapi-codegen/src/ts.rs @@ -13,10 +13,12 @@ use crate::rustdoc::*; mod examples; mod explorer; +mod host_callbacks; mod playground; pub use examples::generate_client_examples; pub use explorer::generate_explorer; +pub use host_callbacks::generate as generate_host_callbacks; pub use playground::generate_playground_services; #[derive(Default)] @@ -30,23 +32,34 @@ struct CodecContext { /// qualifies every named type with `T.*`. Used by the client/playground/ /// examples generators that emit version-aliased public names (e.g. /// `T.HostAccountGetRequest`). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum NameMode { +#[derive(Clone, Copy, Debug, Default)] +enum NameMode<'a> { #[default] Public, + PreserveQualified, + Generated { + aliases: &'a BTreeMap, + }, } -fn resolve_named(name: &str, mode: NameMode) -> String { +fn resolve_named(name: &str, mode: NameMode<'_>) -> String { match mode { NameMode::Public => public_versioned_type_name(name), + NameMode::PreserveQualified => name.to_string(), + NameMode::Generated { aliases } => aliases + .get(name) + .cloned() + .unwrap_or_else(|| name.to_string()), } } /// Decide how to namespace a resolved type name for `qualified` rendering. /// `Public` prefixes every name with `T.*`. -fn qualify_named(resolved: &str, mode: NameMode) -> String { +fn qualify_named(resolved: &str, mode: NameMode<'_>) -> String { match mode { NameMode::Public => format!("T.{resolved}"), + NameMode::PreserveQualified => format!("T.{resolved}"), + NameMode::Generated { .. } => resolved.to_string(), } } @@ -147,6 +160,130 @@ fn selected_public_aliases( .collect() } +fn emitted_version_prefixed_types( + wrappers: &HashMap, + emit_versions: &HashMap>, + aliases: &BTreeMap, +) -> BTreeSet { + let mut names = BTreeSet::new(); + for (wrapper_name, versions) in emit_versions { + let Some(wrapper) = wrappers.get(wrapper_name) else { + continue; + }; + for version in versions { + let Some(variant) = wrapper.variants.get(version) else { + continue; + }; + collect_preserved_version_prefixed_types(&variant.kind, aliases, &mut names); + } + } + names +} + +fn preserve_version_prefixed_types_referenced_by_emitted_types( + api: &ApiDefinition, + aliases: &BTreeMap, + names: &mut BTreeSet, +) { + loop { + let before = names.len(); + for ty in &api.types { + if version_prefixed_type(&ty.name).is_some() + && !aliases.contains_key(&ty.name) + && !names.contains(&ty.name) + { + continue; + } + collect_preserved_version_prefixed_type_refs_from_type(ty, aliases, names); + } + if names.len() == before { + break; + } + } +} + +fn collect_preserved_version_prefixed_type_refs_from_type( + ty: &TypeDef, + aliases: &BTreeMap, + names: &mut BTreeSet, +) { + match &ty.kind { + TypeDefKind::Alias(type_ref) => { + collect_preserved_version_prefixed_type_refs(type_ref, aliases, names); + } + TypeDefKind::Struct(fields) => { + for field in fields { + collect_preserved_version_prefixed_type_refs(&field.type_ref, aliases, names); + } + } + TypeDefKind::TupleStruct(fields) => { + for field in fields { + collect_preserved_version_prefixed_type_refs(field, aliases, names); + } + } + TypeDefKind::Enum(variants) => { + for variant in variants { + match &variant.fields { + VariantFields::Unit => {} + VariantFields::Unnamed(fields) => { + for field in fields { + collect_preserved_version_prefixed_type_refs(field, aliases, names); + } + } + VariantFields::Named(fields) => { + for field in fields { + collect_preserved_version_prefixed_type_refs( + &field.type_ref, + aliases, + names, + ); + } + } + } + } + } + } +} + +fn collect_preserved_version_prefixed_types( + kind: &VersionedKind, + aliases: &BTreeMap, + names: &mut BTreeSet, +) { + match kind { + VersionedKind::Unit => {} + VersionedKind::Tuple(inner) => { + collect_preserved_version_prefixed_type_refs(inner, aliases, names); + } + } +} + +fn collect_preserved_version_prefixed_type_refs( + ty: &TypeRef, + aliases: &BTreeMap, + names: &mut BTreeSet, +) { + match ty { + TypeRef::Named { name, args } => { + if version_prefixed_type(name).is_some() && !aliases.contains_key(name) { + names.insert(name.clone()); + } + for arg in args { + collect_preserved_version_prefixed_type_refs(arg, aliases, names); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_preserved_version_prefixed_type_refs(inner, aliases, names); + } + TypeRef::Tuple(items) => { + for item in items { + collect_preserved_version_prefixed_type_refs(item, aliases, names); + } + } + TypeRef::Primitive(_) | TypeRef::Generic(_) | TypeRef::Unit => {} + } +} + #[derive(Debug, Clone)] enum VersionedKind { Unit, @@ -762,9 +899,19 @@ fn generate_types(api: &ApiDefinition, target_version: u32) -> Result { let wrappers = collect_versioned_wrappers(api); let emit_versions = versioned_wrapper_emit_versions(api, &wrappers, target_version)?; let aliases = selected_public_aliases(api, &wrappers, &emit_versions, target_version); + let mut preserved_version_prefixed_types = + emitted_version_prefixed_types(&wrappers, &emit_versions, &aliases); + preserve_version_prefixed_types_referenced_by_emitted_types( + api, + &aliases, + &mut preserved_version_prefixed_types, + ); for ty in &api.types { - if version_prefixed_type(&ty.name).is_some() && !aliases.contains_key(&ty.name) { + if version_prefixed_type(&ty.name).is_some() + && !aliases.contains_key(&ty.name) + && !preserved_version_prefixed_types.contains(&ty.name) + { continue; } write_type_definition(&mut out, ty, &emit_versions, &aliases)?; @@ -809,7 +956,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) .unwrap(); write_observable_helper(&mut out); - let ctx = CodecContext::default(); + let ctx = codec_context(&[]); let wrappers = collect_versioned_wrappers(api); let services = public_services(api)?; @@ -861,13 +1008,12 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) export type Client = TrUApiClient; - export type GeneratedClientTransport = Omit & - Partial>; + export type GeneratedClientTransport = Omit & + Partial>; - function withGeneratedTransportVersions(transport: GeneratedClientTransport): TrUApiTransport {{ + function withGeneratedCodecVersion(transport: GeneratedClientTransport): TrUApiTransport {{ return {{ ...transport, - truapiVersion: transport.truapiVersion ?? TRUAPI_VERSION, codecVersion: transport.codecVersion ?? TRUAPI_CODEC_VERSION, }}; }} @@ -875,7 +1021,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) /** Creates the generated client facade by binding each service namespace to the * shared transport instance. */ export function createClient(transport: GeneratedClientTransport): TrUApiClient {{ - const versionedTransport = withGeneratedTransportVersions(transport); + const transportWithCodecVersion = withGeneratedCodecVersion(transport); return {{ "# ) @@ -888,7 +1034,7 @@ fn generate_client(api: &ApiDefinition, target_version: u32, codec_version: u8) let field = to_camel_case(&trait_def.name); writeln!( out, - " {}: new {}Client(versionedTransport),", + " {}: new {}Client(transportWithCodecVersion),", field, trait_def.name ) .unwrap(); @@ -1186,27 +1332,47 @@ fn emit_error_response( ctx: &CodecContext, wire_version: Option, ) -> Result { - emit_response( - call_error_inner(ty).unwrap_or(ty), - wrappers, - ctx, - wire_version, - ) -} + let Some(error_wrapper_ty) = call_error_inner(ty) else { + return emit_response(ty, wrappers, ctx, wire_version); + }; -fn versioned_kind_codec_expr( - kind: &VersionedKind, - qualified: bool, - ctx: &CodecContext, -) -> Result { - versioned_kind_codec_expr_mode(kind, qualified, ctx, NameMode::Public) + if let Some((wrapper_name, _wrapper)) = versioned_wrapper_for(error_wrapper_ty, wrappers) { + let version = wire_version.ok_or_else(|| { + anyhow::anyhow!("versioned error wrapper `{wrapper_name}` has no selected wire version") + })?; + let versioned_name = versioned_wrapper_ts_name(wrapper_name); + let inner_type_ts = format!("S.CallErrorValue"); + let inner_codec_expr = format!("S.CallError(T.{versioned_name})"); + let wire_codec_expr = indexed_versioned_codec_expr([(version, inner_codec_expr.clone())])?; + return Ok(ResponseEmission { + inner_type_ts: inner_type_ts.clone(), + wire_type_ts: format!("{{ tag: \"V{version}\"; value: {inner_type_ts} }}"), + wire_codec_expr, + inner_codec_expr, + }); + } + + let inner_type_ts = format!( + "S.CallErrorValue<{}>", + ts_type_qualified_preserve(error_wrapper_ty)? + ); + let inner_codec_expr = format!( + "S.CallError({})", + codec_expr_mode(error_wrapper_ty, true, ctx, NameMode::PreserveQualified)? + ); + Ok(ResponseEmission { + inner_type_ts: inner_type_ts.clone(), + wire_type_ts: inner_type_ts, + wire_codec_expr: inner_codec_expr.clone(), + inner_codec_expr, + }) } fn versioned_kind_codec_expr_mode( kind: &VersionedKind, qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { match kind { VersionedKind::Unit => Ok("S._void".to_string()), @@ -1441,6 +1607,7 @@ fn write_type_definition( emit_versions: &HashMap>, aliases: &BTreeMap, ) -> Result<()> { + let generated_names = NameMode::Generated { aliases }; let generic_decl = generic_param_declaration(&ty.generic_params); let emitted_name = if should_rename_wire_wrapper(ty, emit_versions, aliases) { versioned_wrapper_ts_name(&ty.name) @@ -1456,7 +1623,7 @@ fn write_type_definition( writeln!( out, "export type {emitted_name}{generic_decl} = {};", - ts_type(type_ref)? + ts_type_with_named(type_ref, false, generated_names)? ) .unwrap(); } @@ -1466,9 +1633,19 @@ fn write_type_definition( let (ts_name, optional) = ts_field_name(&field.name, &field.type_ref); write_jsdoc(out, " ", field.docs.as_deref()); if optional { - writeln!(out, " {ts_name}?: {};", ts_inner_option(&field.type_ref)?).unwrap(); + writeln!( + out, + " {ts_name}?: {};", + ts_inner_option_with_named(&field.type_ref, false, generated_names)? + ) + .unwrap(); } else { - writeln!(out, " {ts_name}: {};", ts_type(&field.type_ref)?).unwrap(); + writeln!( + out, + " {ts_name}: {};", + ts_type_with_named(&field.type_ref, false, generated_names)? + ) + .unwrap(); } } writeln!(out, "}}").unwrap(); @@ -1477,7 +1654,7 @@ fn write_type_definition( writeln!( out, "export type {emitted_name}{generic_decl} = {};", - unnamed_fields_type(fields)? + unnamed_fields_type_mode(fields, false, generated_names)? ) .unwrap(); } @@ -1505,7 +1682,12 @@ fn write_type_definition( } } write_jsdoc(out, " ", variant.docs.as_deref()); - writeln!(out, " | {}", enum_variant_ts_type(variant)?).unwrap(); + writeln!( + out, + " | {}", + enum_variant_ts_type_mode(variant, generated_names)? + ) + .unwrap(); } writeln!(out, ";").unwrap(); } @@ -1521,8 +1703,9 @@ fn write_codec_definition( emit_versions: &HashMap>, aliases: &BTreeMap, ) -> Result<()> { + let generated_names = NameMode::Generated { aliases }; if ty.generic_params.is_empty() { - let ctx = CodecContext::default(); + let ctx = codec_context(&[]); if let Some(wrapper) = detect_versioned_wrapper(ty) { let selected = emit_versions.get(&ty.name); let emitted_name = if should_rename_wire_wrapper(ty, emit_versions, aliases) { @@ -1542,7 +1725,12 @@ fn write_codec_definition( .map(|variant| { Ok(( variant.version, - versioned_kind_codec_expr(&variant.kind, false, &ctx)?, + versioned_kind_codec_expr_mode( + &variant.kind, + false, + &ctx, + generated_names, + )?, )) }) .collect::>>()?, @@ -1560,7 +1748,8 @@ fn write_codec_definition( .map(String::as_str) .unwrap_or(&ty.name); let type_name = top_level_type_name(emitted_name, &ty.generic_params); - let codec_expr = type_codec_expr(ty, &type_name, &ctx)?; + let codec_expr = + type_codec_expr_mode_qualified(ty, &type_name, &ctx, generated_names, false)?; writeln!( out, "export const {emitted_name}: S.Codec<{type_name}> = S.lazy((): S.Codec<{type_name}> => {codec_expr});", @@ -1599,7 +1788,7 @@ fn write_codec_definition( .get(&ty.name) .map(String::as_str) .unwrap_or(&ty.name); - let codec_body = type_codec_expr(ty, &type_name, &ctx)?; + let codec_body = type_codec_expr_mode_qualified(ty, &type_name, &ctx, generated_names, false)?; writedoc!( out, " @@ -1622,15 +1811,11 @@ fn should_rename_wire_wrapper( && (emit_versions.contains_key(&ty.name) || aliases.values().any(|alias| alias == &ty.name)) } -fn type_codec_expr(ty: &TypeDef, type_name: &str, ctx: &CodecContext) -> Result { - type_codec_expr_mode_qualified(ty, type_name, ctx, NameMode::Public, false) -} - fn type_codec_expr_mode_qualified( ty: &TypeDef, type_name: &str, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, qualified: bool, ) -> Result { match &ty.kind { @@ -1704,7 +1889,7 @@ fn unit_enum_summary(variants: &[VariantDef]) -> String { ) } -fn variant_value_type_mode(fields: &VariantFields, mode: NameMode) -> Result { +fn variant_value_type_mode(fields: &VariantFields, mode: NameMode<'_>) -> Result { let qualified = false; match fields { VariantFields::Unit => Ok("undefined".to_string()), @@ -1721,7 +1906,7 @@ fn enum_variant_ts_type(variant: &VariantDef) -> Result { enum_variant_ts_type_mode(variant, NameMode::Public) } -fn enum_variant_ts_type_mode(variant: &VariantDef, mode: NameMode) -> Result { +fn enum_variant_ts_type_mode(variant: &VariantDef, mode: NameMode<'_>) -> Result { Ok(match &variant.fields { VariantFields::Unit => format!("{{ tag: \"{}\"; value?: undefined }}", variant.name), fields => format!( @@ -1736,7 +1921,7 @@ fn variant_codec_expr_mode( fields: &VariantFields, qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { match fields { VariantFields::Unit => Ok("S._void".to_string()), @@ -1757,7 +1942,11 @@ fn unnamed_fields_type(types: &[TypeRef]) -> Result { unnamed_fields_type_mode(types, false, NameMode::Public) } -fn unnamed_fields_type_mode(types: &[TypeRef], qualified: bool, mode: NameMode) -> Result { +fn unnamed_fields_type_mode( + types: &[TypeRef], + qualified: bool, + mode: NameMode<'_>, +) -> Result { if types.is_empty() { Ok("undefined".to_string()) } else if types.len() == 1 { @@ -1778,7 +1967,7 @@ fn unnamed_fields_codec_expr_mode( types: &[TypeRef], qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { if types.is_empty() { Ok("S._void".to_string()) @@ -1799,7 +1988,7 @@ fn struct_codec_expr_mode( type_name: &str, qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { let field_specs = fields .iter() @@ -1818,7 +2007,11 @@ fn struct_codec_expr_mode( )) } -fn inline_object_type_mode(fields: &[FieldDef], qualified: bool, mode: NameMode) -> Result { +fn inline_object_type_mode( + fields: &[FieldDef], + qualified: bool, + mode: NameMode<'_>, +) -> Result { Ok(format!( "{{ {} }}", fields @@ -1856,7 +2049,7 @@ fn method_payload_codec_expr_mode( params: &[ParamDef], qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { match params.len() { 0 => Ok("S._void".to_string()), @@ -1880,7 +2073,7 @@ fn codec_expr_mode( ty: &TypeRef, qualified: bool, ctx: &CodecContext, - mode: NameMode, + mode: NameMode<'_>, ) -> Result { match ty { TypeRef::Primitive(name) => match name.as_str() { @@ -1969,7 +2162,7 @@ fn ts_type(ty: &TypeRef) -> Result { ts_type_with_named(ty, false, NameMode::Public) } -fn ts_type_with_named(ty: &TypeRef, qualified: bool, mode: NameMode) -> Result { +fn ts_type_with_named(ty: &TypeRef, qualified: bool, mode: NameMode<'_>) -> Result { match ty { TypeRef::Primitive(name) => match name.as_str() { "bool" => Ok("boolean".to_string()), @@ -2052,7 +2245,7 @@ fn ts_inner_option(ty: &TypeRef) -> Result { ts_inner_option_with_named(ty, false, NameMode::Public) } -fn ts_inner_option_with_named(ty: &TypeRef, qualified: bool, mode: NameMode) -> Result { +fn ts_inner_option_with_named(ty: &TypeRef, qualified: bool, mode: NameMode<'_>) -> Result { match ty { TypeRef::Option(inner) => ts_type_with_named(inner, qualified, mode), other => ts_type_with_named(other, qualified, mode), @@ -2063,6 +2256,10 @@ fn ts_type_qualified(ty: &TypeRef) -> Result { ts_type_with_named(ty, true, NameMode::Public) } +fn ts_type_qualified_preserve(ty: &TypeRef) -> Result { + ts_type_with_named(ty, true, NameMode::PreserveQualified) +} + fn ts_field_name(name: &str, ty: &TypeRef) -> (String, bool) { let camel = to_camel_case(name); let optional = matches!(ty, TypeRef::Option(_)); @@ -2073,7 +2270,7 @@ fn payload_type(params: &[ParamDef]) -> Result { payload_type_mode(params, NameMode::Public) } -fn payload_type_mode(params: &[ParamDef], mode: NameMode) -> Result { +fn payload_type_mode(params: &[ParamDef], mode: NameMode<'_>) -> Result { match params.len() { 0 => Ok("undefined".to_string()), 1 => ts_type_with_named(¶ms[0].type_ref, true, mode), @@ -2294,6 +2491,20 @@ mod tests { versioned_tuple_wrapper_variants(name, &[(1, legacy), (2, latest)]) } + fn single_field_struct(name: &str, field_name: &str, field_type: &str) -> TypeDef { + TypeDef { + name: name.to_string(), + module_path: Vec::new(), + generic_params: Vec::new(), + kind: TypeDefKind::Struct(vec![FieldDef { + name: field_name.to_string(), + type_ref: TypeRef::Primitive(field_type.to_string()), + docs: None, + }]), + docs: None, + } + } + fn named_field_versioned_wrapper(name: &str) -> TypeDef { let fields = vec![ FieldDef { @@ -2663,6 +2874,73 @@ mod tests { assert!(client_source.contains("ResultAsync")); } + #[test] + fn generate_types_preserves_legacy_prefixed_wrapper_variants() { + let api = ApiDefinition { + traits: vec![TraitDef { + name: "Example".to_string(), + module_path: Vec::new(), + methods: vec![ + MethodDef { + name: "legacy_call".to_string(), + kind: MethodKind::Request, + params: vec![ParamDef { + name: "request".to_string(), + type_ref: named_type("LegacyRequest"), + }], + return_type: ReturnType::Result { + ok: TypeRef::Unit, + err: named_type("ExampleError"), + }, + wire: request_wire(Some(2)), + docs: None, + }, + MethodDef { + name: "latest_call".to_string(), + kind: MethodKind::Request, + params: vec![ParamDef { + name: "request".to_string(), + type_ref: named_type("LatestRequest"), + }], + return_type: ReturnType::Result { + ok: TypeRef::Unit, + err: named_type("ExampleError"), + }, + wire: request_wire(Some(4)), + docs: None, + }, + ], + docs: None, + }], + public_trait_order: vec!["Example".to_string()], + types: vec![ + versioned_tuple_wrapper_variants("LegacyRequest", &[(1, "V01LegacyRequest")]), + versioned_tuple_wrapper_variants("LatestRequest", &[(2, "V02LatestRequest")]), + versioned_tuple_wrapper_variants( + "ExampleError", + &[(1, "V01ExampleError"), (2, "V02ExampleError")], + ), + single_field_struct("V01LegacyRequest", "legacy_marker", "u8"), + single_field_struct("V02LatestRequest", "latest_marker", "u32"), + single_field_struct("V01ExampleError", "legacy_code", "u8"), + single_field_struct("V02ExampleError", "latest_code", "u32"), + ], + }; + + let source = generate_types(&api, 2).expect("generate types"); + + assert!(source.contains("export interface V01ExampleError")); + assert!(source.contains("legacyCode: number;")); + assert!(source.contains("export interface ExampleError")); + assert!(source.contains("latestCode: number;")); + assert!(!source.contains("export interface V02ExampleError")); + assert!(source.contains(r#"{ tag: "V1"; value: V01ExampleError }"#)); + assert!(source.contains(r#"{ tag: "V2"; value: ExampleError }"#)); + assert!(source.contains( + "S.indexedTaggedUnion({V1: [0, V01ExampleError] as const, V2: [1, ExampleError] as const})" + )); + } + #[test] fn generate_client_uses_only_existing_wrapper_variant() { let api = ApiDefinition { diff --git a/rust/crates/truapi-codegen/src/ts/host_callbacks.rs b/rust/crates/truapi-codegen/src/ts/host_callbacks.rs new file mode 100644 index 00000000..4d3f1882 --- /dev/null +++ b/rust/crates/truapi-codegen/src/ts/host_callbacks.rs @@ -0,0 +1,1483 @@ +//! Emit the typed `HostCallbacks` TypeScript surface from a parsed +//! `truapi-platform` rustdoc tree. +//! +//! One TS interface per Rust capability trait, plus a composite +//! `HostCallbacks` super-interface that mirrors the `Platform: A + B + ...` +//! super-trait so consumers can implement capabilities piecemeal. Named +//! types in trait signatures (e.g. `HostDevicePermissionRequest`) resolve +//! against `@parity/truapi`, the canonical typed client surface. + +use std::collections::BTreeSet; +use std::fmt::Write; +use std::fs; +use std::path::Path; + +use anyhow::{Result, bail}; +use indoc::{formatdoc, writedoc}; + +use crate::platform::{ + PlatformDefinition, PlatformInner, PlatformMethod, PlatformParam, PlatformReturn, PlatformTrait, +}; +use crate::rustdoc::{FieldDef, TypeDef, TypeDefKind, TypeRef, VariantDef, VariantFields}; +use crate::ts::ts_string_literal; + +/// Write the typed host-callbacks TS file and its WASM adapter/worker bridge. +/// +/// `codec_types` is the set of `@parity/truapi` type names that carry a SCALE +/// codec (structs and enums, not primitive aliases); the adapter crosses those +/// as bytes and passes everything else through unchanged. +pub fn generate( + definition: &PlatformDefinition, + codec_types: &BTreeSet, + callbacks_output_dir: &str, + adapter_output_dir: &str, +) -> Result<()> { + fs::create_dir_all(callbacks_output_dir)?; + fs::create_dir_all(adapter_output_dir)?; + let local_codec_types = collect_local_async_payload_types(definition); + let body = emit_host_callbacks(definition, codec_types, &local_codec_types)?; + fs::write( + Path::new(callbacks_output_dir).join("host-callbacks.ts"), + body, + )?; + let adapter = emit_wasm_adapter(definition, codec_types, &local_codec_types)?; + fs::write( + Path::new(adapter_output_dir).join("host-callbacks-adapter.ts"), + adapter, + )?; + let worker_callbacks = emit_worker_callbacks(definition, codec_types, &local_codec_types)?; + fs::write( + Path::new(adapter_output_dir).join("worker-callbacks.ts"), + worker_callbacks, + )?; + Ok(()) +} + +/// Emit one `import`/`import type` block listing `names` (one per line) from +/// `module`. No-op when `names` is empty. Does not emit a trailing blank line. +fn emit_import_block(out: &mut String, type_only: bool, module: &str, names: &BTreeSet) { + if names.is_empty() { + return; + } + let entries = names + .iter() + .map(|name| format!(" {name},")) + .collect::>() + .join("\n"); + let keyword = if type_only { "import type" } else { "import" }; + writedoc!( + out, + r#" + {keyword} {{ + {entries} + }} from "{module}"; + "#, + ) + .unwrap(); +} + +fn emit_host_callbacks( + definition: &PlatformDefinition, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> Result { + let mut out = String::new(); + writedoc!( + out, + r#" + // Auto-generated by truapi-codegen. Do not edit. + // + // Typed host-callbacks surface derived from the `truapi-platform` + // capability traits. One interface per Rust trait + a composite + // `HostCallbacks` interface that mirrors the `Platform` super-trait. + + "#, + ) + .unwrap(); + + let codec_imports = collect_local_codec_imports(definition, codec_types, local_codec_types); + if !codec_imports.is_empty() || !local_codec_types.is_empty() { + writedoc!( + out, + r#" + import * as S from "@parity/truapi/scale"; + + "#, + ) + .unwrap(); + } + if !codec_imports.is_empty() { + emit_import_block(&mut out, false, "@parity/truapi", &codec_imports); + out.push('\n'); + } + + let imports = collect_named_types(definition) + .into_iter() + .filter(|name| !codec_imports.contains(name)) + .collect::>(); + if !imports.is_empty() { + emit_import_block(&mut out, true, "@parity/truapi", &imports); + out.push('\n'); + } + + for type_def in &definition.types { + let rendered = match &type_def.kind { + TypeDefKind::Enum(_) => emit_enum_type(type_def)?, + _ => emit_struct_interface(type_def)?, + }; + out.push_str(&rendered); + out.push('\n'); + } + for type_def in definition + .types + .iter() + .filter(|ty| local_codec_types.contains(&ty.name)) + { + out.push_str(&emit_local_codec(type_def)?); + out.push('\n'); + } + + for trait_def in &definition.traits { + out.push_str(&emit_trait_interface(trait_def)?); + out.push('\n'); + } + + // The Rust super-trait `Platform` becomes `HostCallbacks` on the TS + // surface: that is the name every host implementer reaches for, and + // it stays stable even if the Rust trait is renamed. + let (composes, docs): (Vec, Option<&str>) = match &definition.super_trait { + Some(s) => (s.composes.clone(), s.docs.as_deref()), + None => ( + definition.traits.iter().map(|t| t.name.clone()).collect(), + None, + ), + }; + out.push_str(&emit_super_interface("HostCallbacks", &composes, docs)); + + Ok(out) +} + +/// Emit the `createWasmRawCallbacks` adapter that maps the typed +/// `HostCallbacks` surface onto the byte-oriented surface the WASM core +/// invokes. Named wire types cross as SCALE bytes (`.enc`/`.dec`); strings, +/// primitives, byte blobs and platform-local types (`AuthState`) pass through +/// unchanged. `ChainProvider` is delegated to the hand-written +/// `chainConnectAdapter` since its connection handle is bespoke. +fn emit_wasm_adapter( + definition: &PlatformDefinition, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> Result { + // Only the capability traits the `Platform` super-trait composes are host + // callbacks; returned handles like `JsonRpcConnection` are not. + let traits = composed_traits(definition); + + // Local types are emitted in `host-callbacks.ts` (e.g. `AuthState`); codec + // types carry SCALE codecs and are imported as values for `.enc`/`.dec`. + // Anything else named in a signature (e.g. the `NotificationId` alias) is a + // non-codec `@parity/truapi` type the `RawCallbacks` interface imports for + // its type only. + let local = local_names(definition); + let mut imports: BTreeSet = BTreeSet::new(); + let mut extra_types: BTreeSet = BTreeSet::new(); + let mut adapter_local_codec_types: BTreeSet = BTreeSet::new(); + for trait_def in &traits { + for method in &trait_def.methods { + for param in &method.params { + collect_codec_imports(¶m.type_ref, codec_types, &mut imports); + collect_local_codec_names( + ¶m.type_ref, + local_codec_types, + &mut adapter_local_codec_types, + ); + collect_extra_named(¶m.type_ref, codec_types, &local, &mut extra_types); + } + match &method.return_shape.inner { + PlatformInner::Result { ok, .. } | PlatformInner::Plain(ok) => { + collect_codec_imports(ok, codec_types, &mut imports); + collect_extra_named(ok, codec_types, &local, &mut extra_types); + } + PlatformInner::Stream(item) => { + collect_codec_imports(stream_item(item), codec_types, &mut imports) + } + PlatformInner::Unit | PlatformInner::TraitObject(_) => {} + } + } + } + + let mut out = String::new(); + writedoc!( + out, + r#" + // Auto-generated by truapi-codegen. Do not edit. + // + // Adapts the typed `HostCallbacks` surface onto the byte-oriented + // callback surface the WASM core invokes. Named wire types cross as + // SCALE bytes (`.enc`/`.dec`); strings, primitives, byte blobs and + // platform-local types pass through unchanged. + + "#, + ) + .unwrap(); + emit_import_block(&mut out, false, "@parity/truapi", &imports); + emit_import_block(&mut out, true, "@parity/truapi", &extra_types); + emit_import_block( + &mut out, + false, + "./host-callbacks.js", + &adapter_local_codec_types, + ); + writedoc!( + out, + r#" + import type {{ AuthState, HostCallbacks }} from "./host-callbacks.js"; + import type {{ ChainConnect }} from "../runtime.js"; + import {{ + chainConnectAdapter, + driveResultStream, + }} from "../adapter-support.js"; + + "#, + ) + .unwrap(); + out.push_str(&emit_raw_callbacks(&traits, codec_types, local_codec_types)); + writedoc!( + out, + r#" + /** Adapt typed host callbacks into the raw SCALE callback surface the + * WASM core invokes. */ + export function createWasmRawCallbacks( + host: Required, + ): RawCallbacks {{ + return {{ + "#, + ) + .unwrap(); + for trait_def in &traits { + if trait_def.name == "ChainProvider" { + writeln!(out, " chainConnect: chainConnectAdapter(host),").unwrap(); + continue; + } + for method in &trait_def.methods { + let entry = emit_adapter_entry(method, codec_types, local_codec_types)?; + writeln!(out, " {entry}").unwrap(); + } + } + out.push_str(" };\n}\n"); + Ok(out) +} + +/// Emit the generated callback metadata/proxy used by the Web Worker bridge. +/// +/// The lifecycle/transport pieces stay hand-written in `worker-runtime.ts` and +/// `create-worker-host-runtime.ts`; this file owns the mechanical callback +/// name, arity, host-hook installation and subscription metadata that already +/// exists in the parsed `truapi-platform` trait surface. +fn emit_worker_callbacks( + definition: &PlatformDefinition, + _codec_types: &BTreeSet, + _local_codec_types: &BTreeSet, +) -> Result { + let traits = composed_traits(definition); + let mut callbacks = Vec::new(); + let mut subscriptions = Vec::new(); + + for trait_def in traits { + if trait_def.name == "ChainProvider" { + continue; + } + for method in &trait_def.methods { + match &method.return_shape.inner { + PlatformInner::Stream(_) => subscriptions.push(method), + _ => callbacks.push(method), + } + } + } + + let mut out = String::new(); + writedoc!( + out, + r#" + // Auto-generated by truapi-codegen. Do not edit. + // + // Worker-side metadata and proxy functions for the raw WASM callback + // surface. The worker transport/lifecycle remains hand-written; this + // file owns the callback names, host-hook arity, and + // subscription payload shape derived from `truapi-platform`. + + import type {{ ChainConnect }} from "../runtime.js"; + import type {{ RawCallbacks }} from "./host-callbacks-adapter.js"; + + "# + ) + .unwrap(); + + out.push_str(&const_name_array("CALLBACK_NAMES", &callbacks)); + out.push_str("export type CallbackName = typeof CALLBACK_NAMES[number];\n\n"); + out.push_str(&const_name_array("SUBSCRIPTION_NAMES", &subscriptions)); + out.push_str("export type SubscriptionName = typeof SUBSCRIPTION_NAMES[number];\n\n"); + + writedoc!( + out, + r#" + export interface WorkerCallbackBridge {{ + callbackRequest(name: CallbackName, args: readonly unknown[]): Promise; + startSubscription( + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (value: T) => void, + ): () => void; + chainConnect: ChainConnect; + }} + + "# + ) + .unwrap(); + + out.push_str(&emit_worker_callback_factory( + "rawCallbacks", + "CallbackName", + &callbacks, + )?); + out.push('\n'); + out.push_str(&emit_worker_subscription_factory(&subscriptions)?); + out.push('\n'); + + writedoc!( + out, + r#" + export function createWorkerRawCallbacks( + bridge: WorkerCallbackBridge, + ): Record {{ + const callbacks: Record = {{ + ...rawCallbacks(bridge), + ...subscriptionRawCallbacks(bridge), + chainConnect: bridge.chainConnect, + }}; + return callbacks; + }} + + export function startRawSubscription( + callbacks: RawCallbacks, + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (value?: unknown) => void, + ): (() => void) | void {{ + "# + ) + .unwrap(); + out.push_str(&emit_start_raw_subscription_switch(&subscriptions)?); + writedoc!( + out, + r#" + }} + "# + ) + .unwrap(); + + Ok(out) +} + +fn composed_traits(definition: &PlatformDefinition) -> Vec<&PlatformTrait> { + let composed: BTreeSet = match &definition.super_trait { + Some(s) => s.composes.iter().cloned().collect(), + None => definition.traits.iter().map(|t| t.name.clone()).collect(), + }; + definition + .traits + .iter() + .filter(|t| composed.contains(&t.name)) + .collect() +} + +/// All names defined locally in the platform crate: capability trait names plus +/// the struct/enum type names emitted into `host-callbacks.ts`. +fn local_names(definition: &PlatformDefinition) -> BTreeSet { + definition + .traits + .iter() + .map(|t| t.name.clone()) + .chain(definition.types.iter().map(|s| s.name.clone())) + .collect() +} + +fn const_name_array(const_name: &str, methods: &[&PlatformMethod]) -> String { + let entries = methods + .iter() + .map(|method| format!(" \"{}\",", to_camel_case(&method.name))) + .collect::>() + .join("\n"); + format!("export const {const_name} = [\n{entries}\n] as const;\n") +} + +fn emit_worker_callback_factory( + function_name: &str, + name_type: &str, + methods: &[&PlatformMethod], +) -> Result { + let mut out = String::new(); + writeln!( + out, + "function {function_name}(bridge: WorkerCallbackBridge): Required> {{" + ) + .unwrap(); + out.push_str(" return {\n"); + for method in methods { + out.push_str(&emit_worker_callback_entry(method)?); + } + out.push_str(" };\n"); + out.push_str("}\n"); + Ok(out) +} + +fn emit_worker_callback_entry(method: &PlatformMethod) -> Result { + let raw = to_camel_case(&method.name); + let args = method + .params + .iter() + .map(|p| to_camel_case(&p.name)) + .collect::>() + .join(", "); + let arg_array = if args.is_empty() { + "[]".to_string() + } else { + format!("[{args}]") + }; + if method.return_shape.is_async { + Ok(format!( + " {raw}: ({args}) =>\n bridge.callbackRequest(\"{raw}\", {arg_array}) as ReturnType,\n" + )) + } else { + match &method.return_shape.inner { + PlatformInner::Unit => Ok(format!( + " {raw}: ({args}) =>\n void bridge.callbackRequest(\"{raw}\", {arg_array}).catch(() => {{}}),\n" + )), + PlatformInner::Plain(_) => Ok(format!( + " {raw}: ({args}) =>\n bridge.callbackRequest(\"{raw}\", {arg_array}) as ReturnType,\n" + )), + PlatformInner::Result { .. } + | PlatformInner::Stream(_) + | PlatformInner::TraitObject(_) => { + bail!("unsupported non-async worker callback return shape for `{raw}`") + } + } + } +} + +fn emit_worker_subscription_factory(methods: &[&PlatformMethod]) -> Result { + let mut out = String::new(); + out.push_str( + "function subscriptionRawCallbacks(bridge: WorkerCallbackBridge): Required> {\n", + ); + out.push_str(" return {\n"); + for method in methods { + let raw = to_camel_case(&method.name); + let payload_param = worker_subscription_payload_param(method)?; + if let Some(param) = payload_param { + out.push_str(&format!( + " {raw}: ({param}, sendItem) =>\n bridge.startSubscription(\"{raw}\", {param}, sendItem),\n" + )); + } else { + out.push_str(&format!( + " {raw}: (sendItem) =>\n bridge.startSubscription(\"{raw}\", null, sendItem),\n" + )); + } + } + out.push_str(" };\n"); + out.push_str("}\n"); + Ok(out) +} + +fn emit_start_raw_subscription_switch(methods: &[&PlatformMethod]) -> Result { + let mut out = String::new(); + out.push_str(" switch (name) {\n"); + for method in methods { + let raw = to_camel_case(&method.name); + if worker_subscription_payload_param(method)?.is_some() { + out.push_str(&format!( + " case \"{raw}\":\n if (payload === null) {{\n console.warn(`[truapi worker] ${{name}} requires payload`);\n return undefined;\n }}\n return callbacks.{raw}(payload, sendItem);\n" + )); + } else { + out.push_str(&format!( + " case \"{raw}\":\n return callbacks.{raw}(sendItem);\n" + )); + } + } + out.push_str(" }\n"); + Ok(out) +} + +fn worker_subscription_payload_param(method: &PlatformMethod) -> Result> { + match method.params.as_slice() { + [] => Ok(None), + [param] => Ok(Some(to_camel_case(¶m.name))), + _ => bail!( + "subscription callback `{}` has more than one payload parameter", + method.name + ), + } +} + +fn collect_local_codec_names( + ty: &TypeRef, + local_codec_types: &BTreeSet, + out: &mut BTreeSet, +) { + match ty { + TypeRef::Named { name, args } => { + if local_codec_types.contains(name) { + out.insert(name.clone()); + } + for arg in args { + collect_local_codec_names(arg, local_codec_types, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_local_codec_names(inner, local_codec_types, out); + } + TypeRef::Tuple(items) => { + for item in items { + collect_local_codec_names(item, local_codec_types, out); + } + } + TypeRef::Primitive(_) | TypeRef::Generic(_) | TypeRef::Unit => {} + } +} + +/// Emit the complete `RawCallbacks` interface: the byte-oriented callback bag +/// produced by the typed host adapter. Codec payloads cross as `Uint8Array`, +/// strings/primitives/blobs pass through, subscriptions take a `sendItem` sink, +/// and `chainConnect` is the bespoke connection handle. +fn emit_raw_callbacks( + traits: &[&PlatformTrait], + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> String { + let mut out = String::new(); + out.push_str("export interface RawCallbacks {\n"); + for trait_def in traits { + if trait_def.name == "ChainProvider" { + continue; + } + for method in &trait_def.methods { + out.push_str(" "); + out.push_str(&raw_member(method, codec_types, local_codec_types)); + out.push('\n'); + } + } + out.push_str(" chainConnect: ChainConnect;\n"); + out.push_str("}\n"); + out +} + +/// One `RawCallbacks` member signature for `method`. +fn raw_member( + method: &PlatformMethod, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> String { + let name = to_camel_case(&method.name); + match &method.return_shape.inner { + PlatformInner::Stream(_) => { + let mut params: Vec = method + .params + .iter() + .map(|p| { + format!( + "{}: {}", + to_camel_case(&p.name), + raw_param_ts(&p.type_ref, codec_types, local_codec_types) + ) + }) + .collect(); + params.push("sendItem: (item?: Uint8Array) => void".to_string()); + format!("{name}({}): (() => void) | void;", params.join(", ")) + } + PlatformInner::TraitObject(_) => String::new(), + inner => { + let params = method + .params + .iter() + .map(|p| { + format!( + "{}: {}", + to_camel_case(&p.name), + raw_param_ts(&p.type_ref, codec_types, local_codec_types) + ) + }) + .collect::>() + .join(", "); + let ok = match inner { + PlatformInner::Result { ok, .. } | PlatformInner::Plain(ok) => { + raw_ok_ts(ok, codec_types) + } + _ => "void".to_string(), + }; + // A synchronous (`!is_async`) callback is a fire-and-forget + // notification (e.g. `authStateChanged`); everything else is async. + if method.return_shape.is_async { + format!("{name}({params}): Promise<{ok}>;") + } else { + format!("{name}({params}): {ok};") + } + } + } +} + +/// TS type for a `RawCallbacks` parameter under the byte boundary. +fn raw_param_ts( + ty: &TypeRef, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> String { + match ty { + TypeRef::Named { name, .. } if codec_types.contains(name) => "Uint8Array".to_string(), + TypeRef::Named { name, .. } if local_codec_types.contains(name) => "Uint8Array".to_string(), + TypeRef::Named { name, .. } => name.clone(), + TypeRef::Vec(inner) | TypeRef::Array(inner, _) if matches!(inner.as_ref(), TypeRef::Primitive(p) if p == "u8") => { + "Uint8Array".to_string() + } + TypeRef::Option(inner) => { + format!( + "{} | null | undefined", + raw_param_ts(inner, codec_types, local_codec_types) + ) + } + TypeRef::Primitive(p) => raw_primitive_ts(p), + _ => "Uint8Array".to_string(), + } +} + +/// TS type for a `RawCallbacks` `Result` ok value under the byte boundary. +fn raw_ok_ts(ty: &TypeRef, codec_types: &BTreeSet) -> String { + match ty { + TypeRef::Named { name, .. } if codec_types.contains(name) => "Uint8Array".to_string(), + TypeRef::Named { name, .. } => name.clone(), + TypeRef::Vec(inner) | TypeRef::Array(inner, _) if matches!(inner.as_ref(), TypeRef::Primitive(p) if p == "u8") => { + "Uint8Array".to_string() + } + TypeRef::Option(inner) => format!("{} | null | undefined", raw_ok_ts(inner, codec_types)), + TypeRef::Primitive(p) => raw_primitive_ts(p), + TypeRef::Unit => "void".to_string(), + TypeRef::Tuple(items) if items.is_empty() => "void".to_string(), + _ => "Uint8Array".to_string(), + } +} + +fn raw_primitive_ts(p: &str) -> String { + match p { + "bool" => "boolean".to_string(), + "str" => "string".to_string(), + _ => "number".to_string(), + } +} + +/// Collect named types referenced by `ty` that are neither codec types nor +/// platform-local (e.g. the `NotificationId` alias), so the `RawCallbacks` +/// interface can import them from `@parity/truapi` for their type only. +fn collect_extra_named( + ty: &TypeRef, + codec_types: &BTreeSet, + local: &BTreeSet, + out: &mut BTreeSet, +) { + match ty { + TypeRef::Named { name, args } => { + if !codec_types.contains(name) && !local.contains(name) { + out.insert(name.clone()); + } + for arg in args { + collect_extra_named(arg, codec_types, local, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_extra_named(inner, codec_types, local, out) + } + _ => {} + } +} + +/// Collect the `@parity/truapi` codec type names referenced by `ty` so the +/// adapter can import their `.enc`/`.dec`. +fn collect_codec_imports(ty: &TypeRef, codec_types: &BTreeSet, out: &mut BTreeSet) { + match ty { + TypeRef::Named { name, args } => { + if codec_types.contains(name) { + out.insert(name.clone()); + } + for arg in args { + collect_codec_imports(arg, codec_types, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_codec_imports(inner, codec_types, out) + } + _ => {} + } +} + +/// Unwrap a `Result` stream item to its `T`; other item types pass +/// through. Streams carry `Result`s on the Rust side but `driveResultStream` +/// already unwraps them, so the adapter encodes the inner item type. +fn stream_item(item: &TypeRef) -> &TypeRef { + if let TypeRef::Named { name, args } = item + && name == "Result" + && let Some(ok) = args.first() + { + return ok; + } + item +} + +/// The call argument expression for one Rust param. Codec types arrive as +/// `Uint8Array` and are decoded; `u64`-family integers arrive as JS numbers and +/// are widened to `bigint`; everything else passes through. Arrow parameter +/// types are left to contextual inference from `RawCallbacks`, so only the +/// argument expression varies. +fn adapter_arg( + param: &PlatformParam, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> String { + let name = to_camel_case(¶m.name); + match ¶m.type_ref { + TypeRef::Named { name: ty, .. } + if codec_types.contains(ty) || local_codec_types.contains(ty) => + { + format!("{ty}.dec({name})") + } + TypeRef::Primitive(p) if matches!(p.as_str(), "u64" | "u128" | "i64" | "i128") => { + format!("BigInt({name})") + } + _ => name, + } +} + +/// Comma-joined arrow parameter names for a method (excluding `sendItem`). +fn param_names(method: &PlatformMethod) -> String { + method + .params + .iter() + .map(|p| to_camel_case(&p.name)) + .collect::>() + .join(", ") +} + +/// Emit one entry of the adapter object literal for `method`: every web worker +/// host provides a complete callback implementation, so missing capability +/// behavior is expressed inside the host callback rather than by omitting it. +fn emit_adapter_entry( + method: &PlatformMethod, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> Result { + let raw = to_camel_case(&method.name); + let impl_expr = match &method.return_shape.inner { + PlatformInner::Stream(item) => { + adapter_stream_impl(&raw, method, item, codec_types, local_codec_types)? + } + PlatformInner::Result { ok, .. } => { + adapter_unary_impl(&raw, method, ok, codec_types, local_codec_types)? + } + PlatformInner::Plain(ok) => { + adapter_unary_impl(&raw, method, ok, codec_types, local_codec_types)? + } + PlatformInner::Unit => { + adapter_unary_impl(&raw, method, &TypeRef::Unit, codec_types, local_codec_types)? + } + PlatformInner::TraitObject(_) => bail!("unexpected trait-object return on `{raw}`"), + }; + Ok(format!("{raw}: {impl_expr},")) +} + +/// The adapter implementation expression for a unary callback: decode codec +/// params, call the typed host method, SCALE-encode a codec result. +fn adapter_unary_impl( + raw: &str, + method: &PlatformMethod, + ok: &TypeRef, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> Result { + let params = param_names(method); + let args = method + .params + .iter() + .map(|p| adapter_arg(p, codec_types, local_codec_types)) + .collect::>() + .join(", "); + let call = format!("host.{raw}({args})"); + let body = match ok { + TypeRef::Named { name: ty, .. } if codec_types.contains(ty) => { + format!("{ty}.enc(await {call})") + } + _ => format!("await {call}"), + }; + Ok(format!("async ({params}) => {body}")) +} + +/// The adapter implementation expression for a subscription callback: drive +/// the host's stream into `sendItem`, SCALE-encoding each codec item. +fn adapter_stream_impl( + raw: &str, + method: &PlatformMethod, + item: &TypeRef, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> Result { + let args = method + .params + .iter() + .map(|p| adapter_arg(p, codec_types, local_codec_types)) + .collect::>() + .join(", "); + let item_ty = stream_item(item); + let is_unit = matches!(item_ty, TypeRef::Unit) + || matches!(item_ty, TypeRef::Tuple(items) if items.is_empty()); + let item_expr = match item_ty { + // Tick subscription: the item carries no value, so ignore it and emit + // a bare tick to the core's sink. + _ if is_unit => "() => sendItem()".to_string(), + TypeRef::Named { name: ty, .. } if codec_types.contains(ty) => { + format!("(item) => sendItem({ty}.enc(item))") + } + _ => "sendItem".to_string(), + }; + let mut names: Vec = method + .params + .iter() + .map(|p| to_camel_case(&p.name)) + .collect(); + names.push("sendItem".to_string()); + let params = names.join(", "); + let call = format!("host.{raw}({args})"); + Ok(format!( + "({params}) => driveResultStream({call}, {item_expr})" + )) +} + +fn collect_local_async_payload_types(definition: &PlatformDefinition) -> BTreeSet { + let local: BTreeSet = definition.types.iter().map(|ty| ty.name.clone()).collect(); + let mut out = BTreeSet::new(); + for trait_def in &definition.traits { + for method in &trait_def.methods { + if !method.return_shape.is_async { + continue; + } + for param in &method.params { + collect_local_from_type(¶m.type_ref, &local, &mut out); + } + } + } + let mut changed = true; + while changed { + changed = false; + let referenced = definition + .types + .iter() + .filter(|ty| out.contains(&ty.name)) + .collect::>(); + for type_def in referenced { + let before = out.len(); + collect_local_from_type_def(type_def, &local, &mut out); + changed |= out.len() != before; + } + } + out +} + +fn collect_local_from_type_def( + type_def: &TypeDef, + local: &BTreeSet, + out: &mut BTreeSet, +) { + walk_type_def(type_def, out, &mut |ty, out| { + collect_local_from_type(ty, local, out) + }); +} + +fn collect_from_type_def(type_def: &TypeDef, out: &mut BTreeSet) { + walk_type_def(type_def, out, &mut |ty, out| collect_from_type(ty, out)); +} + +/// Walk every `TypeRef` reachable from a type definition's fields/variant +/// payloads, applying `leaf` to each. Callers supply the leaf collector that +/// decides which names land in `out`. +fn walk_type_def( + type_def: &TypeDef, + out: &mut BTreeSet, + leaf: &mut dyn FnMut(&TypeRef, &mut BTreeSet), +) { + match &type_def.kind { + TypeDefKind::Alias(type_ref) => leaf(type_ref, out), + TypeDefKind::Struct(fields) => { + for field in fields { + leaf(&field.type_ref, out); + } + } + TypeDefKind::TupleStruct(fields) => { + for field in fields { + leaf(field, out); + } + } + TypeDefKind::Enum(variants) => { + for variant in variants { + match &variant.fields { + VariantFields::Unit => {} + VariantFields::Unnamed(types) => { + for ty in types { + leaf(ty, out); + } + } + VariantFields::Named(fields) => { + for field in fields { + leaf(&field.type_ref, out); + } + } + } + } + } + } +} + +fn collect_local_from_type(ty: &TypeRef, local: &BTreeSet, out: &mut BTreeSet) { + match ty { + TypeRef::Named { name, args } => { + if local.contains(name) { + out.insert(name.clone()); + } + for arg in args { + collect_local_from_type(arg, local, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_local_from_type(inner, local, out); + } + TypeRef::Tuple(items) => { + for item in items { + collect_local_from_type(item, local, out); + } + } + TypeRef::Primitive(_) | TypeRef::Generic(_) | TypeRef::Unit => {} + } +} + +fn collect_local_codec_imports( + definition: &PlatformDefinition, + codec_types: &BTreeSet, + local_codec_types: &BTreeSet, +) -> BTreeSet { + let mut out = BTreeSet::new(); + for type_def in definition + .types + .iter() + .filter(|ty| local_codec_types.contains(&ty.name)) + { + collect_codec_imports_from_type_def(type_def, codec_types, &mut out); + } + out +} + +fn collect_codec_imports_from_type_def( + type_def: &TypeDef, + codec_types: &BTreeSet, + out: &mut BTreeSet, +) { + match &type_def.kind { + TypeDefKind::Struct(fields) => { + for field in fields { + collect_codec_imports(&field.type_ref, codec_types, out); + } + } + TypeDefKind::TupleStruct(fields) => { + for field in fields { + collect_codec_imports(field, codec_types, out); + } + } + TypeDefKind::Enum(variants) => { + for variant in variants { + match &variant.fields { + VariantFields::Unit => {} + VariantFields::Unnamed(types) => { + for ty in types { + collect_codec_imports(ty, codec_types, out); + } + } + VariantFields::Named(fields) => { + for field in fields { + collect_codec_imports(&field.type_ref, codec_types, out); + } + } + } + } + } + TypeDefKind::Alias(type_ref) => collect_codec_imports(type_ref, codec_types, out), + } +} + +fn emit_local_codec(type_def: &TypeDef) -> Result { + let jsdoc = render_jsdoc("", type_def.docs.as_deref()); + let expr = local_codec_expr_for_type(type_def)?; + Ok(format!( + "{jsdoc}export const {name}: S.Codec<{name}> = S.lazy((): S.Codec<{name}> => {expr});\n", + name = type_def.name, + )) +} + +fn local_codec_expr_for_type(type_def: &TypeDef) -> Result { + match &type_def.kind { + TypeDefKind::Alias(type_ref) => local_codec_expr(type_ref), + TypeDefKind::Struct(fields) => local_struct_codec_expr(fields, &type_def.name), + TypeDefKind::TupleStruct(fields) => local_tuple_codec_expr(fields), + TypeDefKind::Enum(variants) => { + if variants + .iter() + .all(|variant| matches!(variant.fields, VariantFields::Unit)) + { + return Ok(format!( + "S.Status({})", + variants + .iter() + .map(|variant| ts_string_literal(&variant.name)) + .collect::>() + .join(", ") + )); + } + let entries = variants + .iter() + .map(|variant| { + Ok(format!( + "{}: {}", + variant.name, + local_variant_codec_expr(&variant.fields)? + )) + }) + .collect::>>()? + .join(", "); + Ok(format!("S.TaggedUnion({{{entries}}})")) + } + } +} + +fn local_variant_codec_expr(fields: &VariantFields) -> Result { + match fields { + VariantFields::Unit => Ok("S._void".to_string()), + VariantFields::Unnamed(types) => local_tuple_codec_expr(types), + VariantFields::Named(fields) => { + let type_name = inline_object_type(fields)?; + local_struct_codec_expr(fields, &type_name) + } + } +} + +fn local_tuple_codec_expr(types: &[TypeRef]) -> Result { + if types.is_empty() { + Ok("S._void".to_string()) + } else if types.len() == 1 { + local_codec_expr(&types[0]) + } else { + Ok(format!( + "S.Tuple({})", + types + .iter() + .map(local_codec_expr) + .collect::>>()? + .join(", ") + )) + } +} + +fn local_struct_codec_expr(fields: &[FieldDef], type_name: &str) -> Result { + let specs = fields + .iter() + .map(|field| { + Ok(format!( + "{}: {}", + to_camel_case(&field.name), + local_codec_expr(&field.type_ref)? + )) + }) + .collect::>>()? + .join(", "); + Ok(format!("S.Struct({{{specs}}}) as S.Codec<{type_name}>")) +} + +fn local_codec_expr(ty: &TypeRef) -> Result { + match ty { + TypeRef::Primitive(name) => match name.as_str() { + "bool" => Ok("S.bool".to_string()), + "u8" => Ok("S.u8".to_string()), + "u16" => Ok("S.u16".to_string()), + "u32" => Ok("S.u32".to_string()), + "u64" => Ok("S.u64".to_string()), + "u128" => Ok("S.u128".to_string()), + "i8" => Ok("S.i8".to_string()), + "i16" => Ok("S.i16".to_string()), + "i32" => Ok("S.i32".to_string()), + "i64" => Ok("S.i64".to_string()), + "i128" => Ok("S.i128".to_string()), + "str" => Ok("S.str".to_string()), + _ => bail!("Unsupported primitive type `{name}` in host callback codec generation"), + }, + TypeRef::Named { name, args } => { + if args.is_empty() { + Ok(name.clone()) + } else { + let codecs = args + .iter() + .map(local_codec_expr) + .collect::>>()? + .join(", "); + Ok(format!("{name}({codecs})")) + } + } + TypeRef::Vec(inner) => match inner.as_ref() { + TypeRef::Primitive(name) if name == "u8" => Ok("S.Bytes()".to_string()), + _ => Ok(format!("S.Vector({})", local_codec_expr(inner)?)), + }, + TypeRef::Option(inner) => Ok(format!("S.Option({})", local_codec_expr(inner)?)), + TypeRef::Tuple(items) => local_tuple_codec_expr(items), + TypeRef::Array(inner, len) => match inner.as_ref() { + TypeRef::Primitive(name) if name == "u8" => Ok(format!("S.Bytes({len})")), + _ => Ok(format!("S.Vector({})", local_codec_expr(inner)?)), + }, + TypeRef::Generic(name) => { + bail!("Generic `{name}` is not supported in host callback codecs") + } + TypeRef::Unit => Ok("S._void".to_string()), + } +} + +fn emit_trait_interface(trait_def: &PlatformTrait) -> Result { + let jsdoc = render_jsdoc("", trait_def.docs.as_deref()); + let body = trait_def + .methods + .iter() + .map(emit_method) + .collect::>>()? + .join("\n\n"); + Ok(formatdoc! { + r#" + {jsdoc}export interface {name} {{ + {body} + }} + "#, + name = trait_def.name.to_string(), + }) +} + +fn emit_method(method: &PlatformMethod) -> Result { + let jsdoc = render_jsdoc(" ", method.docs.as_deref()); + let params = method + .params + .iter() + .map(|p| { + let ts = ts_type(&p.type_ref)?; + Ok(format!("{}: {ts}", to_camel_case(&p.name))) + }) + .collect::>>()? + .join(", "); + let ret = format_return(&method.return_shape)?; + let name = to_camel_case(&method.name); + // A Rust default body makes the method optional for host implementations. + let optional = if method.has_default { "?" } else { "" }; + Ok(format!("{jsdoc} {name}{optional}({params}): {ret};")) +} + +/// Emit a TS interface for a local platform struct. `Option` fields become +/// optional members so hosts receive plain objects with absent-when-`None` +/// properties. +fn emit_struct_interface(struct_def: &TypeDef) -> Result { + let TypeDefKind::Struct(fields) = &struct_def.kind else { + bail!( + "Platform struct `{}` must have named fields", + struct_def.name + ); + }; + if !struct_def.generic_params.is_empty() { + bail!("Platform struct `{}` must not be generic", struct_def.name); + } + let jsdoc = render_jsdoc("", struct_def.docs.as_deref()); + let body = fields + .iter() + .map(|field| { + let jsdoc = render_jsdoc(" ", field.docs.as_deref()); + let name = to_camel_case(&field.name); + match &field.type_ref { + TypeRef::Option(inner) => Ok(format!("{jsdoc} {name}?: {};", ts_type(inner)?)), + other => Ok(format!("{jsdoc} {name}: {};", ts_type(other)?)), + } + }) + .collect::>>()? + .join("\n\n"); + Ok(formatdoc! { + r#" + {jsdoc}export interface {name} {{ + {body} + }} + "#, + name = struct_def.name, + }) +} + +/// Emit a TS type for a local platform enum. Unit-only enums become string +/// literal unions; payload enums become `{ tag, value }` tagged unions +/// matching the `@parity/truapi` client convention. +fn emit_enum_type(enum_def: &TypeDef) -> Result { + let TypeDefKind::Enum(variants) = &enum_def.kind else { + bail!("Platform enum `{}` must have variants", enum_def.name); + }; + if !enum_def.generic_params.is_empty() { + bail!("Platform enum `{}` must not be generic", enum_def.name); + } + let jsdoc = render_jsdoc("", enum_def.docs.as_deref()); + if variants + .iter() + .all(|variant| matches!(variant.fields, VariantFields::Unit)) + { + let union = variants + .iter() + .map(|variant| format!("\"{}\"", variant.name)) + .collect::>() + .join(" | "); + return Ok(format!( + "{jsdoc}export type {name} = {union};\n", + name = enum_def.name, + )); + } + let mut body = String::new(); + for variant in variants { + body.push_str(&render_jsdoc(" ", variant.docs.as_deref())); + writeln!(body, " | {}", enum_variant_type(variant)?).unwrap(); + } + Ok(formatdoc! { + r#" + {jsdoc}export type {name} = + {body}; + "#, + name = enum_def.name, + body = body.trim_end(), + }) +} + +/// Render one enum variant as a `{ tag, value }` member. Unit variants mark +/// `value` optional so consumers can write `{ tag: "X" }`. +fn enum_variant_type(variant: &VariantDef) -> Result { + Ok(match &variant.fields { + VariantFields::Unit => format!("{{ tag: \"{}\"; value?: undefined }}", variant.name), + VariantFields::Unnamed(types) => format!( + "{{ tag: \"{}\"; value: {} }}", + variant.name, + unnamed_variant_value_type(types)? + ), + VariantFields::Named(fields) => format!( + "{{ tag: \"{}\"; value: {} }}", + variant.name, + inline_object_type(fields)? + ), + }) +} + +fn unnamed_variant_value_type(types: &[TypeRef]) -> Result { + match types { + [single] => ts_type(single), + many => { + let rendered = many + .iter() + .map(ts_type) + .collect::>>()? + .join(", "); + Ok(format!("[{rendered}]")) + } + } +} + +fn inline_object_type(fields: &[FieldDef]) -> Result { + let body = fields + .iter() + .map(|field| { + let name = to_camel_case(&field.name); + match &field.type_ref { + TypeRef::Option(inner) => Ok(format!("{name}?: {}", ts_type(inner)?)), + other => Ok(format!("{name}: {}", ts_type(other)?)), + } + }) + .collect::>>()? + .join("; "); + Ok(format!("{{ {body} }}")) +} + +fn emit_super_interface(name: &str, composes: &[String], docs: Option<&str>) -> String { + let jsdoc = render_jsdoc("", docs); + if composes.is_empty() { + return format!("{jsdoc}export interface {name} {{}}\n"); + } + let extends = composes + .iter() + .map(|name| name.to_string()) + .collect::>() + .join(", "); + format!("{jsdoc}export interface {name} extends {extends} {{}}\n") +} + +fn collect_named_types(definition: &PlatformDefinition) -> BTreeSet { + let mut out: BTreeSet = BTreeSet::new(); + for trait_def in &definition.traits { + for method in &trait_def.methods { + for param in &method.params { + collect_from_type(¶m.type_ref, &mut out); + } + match &method.return_shape.inner { + // Err type is not part of the TS signature (errors throw), + // so don't import its name. + PlatformInner::Result { ok, .. } => collect_from_type(ok, &mut out), + PlatformInner::Stream(inner) => collect_from_type(inner, &mut out), + PlatformInner::Plain(inner) => collect_from_type(inner, &mut out), + // A trait object returns its bare trait name in the TS + // signature; collect it so a non-local trait gets imported + // rather than emitted as an undeclared name. Local traits are + // filtered out below since their interfaces live in this file. + PlatformInner::TraitObject(name) => { + out.insert(name.clone()); + } + PlatformInner::Unit => {} + } + } + } + for type_def in &definition.types { + collect_from_type_def(type_def, &mut out); + } + // Filter out names defined locally (the capability trait interfaces and + // the platform struct/enum types emitted into this file). + let local = local_names(definition); + out.into_iter().filter(|n| !local.contains(n)).collect() +} + +fn collect_from_type(ty: &TypeRef, out: &mut BTreeSet) { + match ty { + TypeRef::Named { name, args } => { + out.insert(name.clone()); + for arg in args { + collect_from_type(arg, out); + } + } + TypeRef::Vec(inner) | TypeRef::Option(inner) | TypeRef::Array(inner, _) => { + collect_from_type(inner, out) + } + TypeRef::Tuple(items) => { + for item in items { + collect_from_type(item, out); + } + } + TypeRef::Primitive(_) | TypeRef::Generic(_) | TypeRef::Unit => {} + } +} + +fn format_return(ret: &PlatformReturn) -> Result { + let inner = match &ret.inner { + PlatformInner::Unit => "void".to_string(), + PlatformInner::Result { ok, .. } => ts_type(ok)?, + PlatformInner::Stream(item) => format!("AsyncIterable<{}>", ts_type(item)?), + PlatformInner::TraitObject(name) => name.clone(), + PlatformInner::Plain(ty) => ts_type(ty)?, + }; + if ret.is_async { + Ok(format!("Promise<{inner}>")) + } else { + Ok(inner) + } +} + +fn ts_type(ty: &TypeRef) -> Result { + match ty { + TypeRef::Primitive(name) => match name.as_str() { + "bool" => Ok("boolean".to_string()), + "u8" | "u16" | "u32" | "i8" | "i16" | "i32" | "f32" | "f64" => Ok("number".to_string()), + "u64" | "u128" | "i64" | "i128" => Ok("bigint".to_string()), + "str" => Ok("string".to_string()), + _ => bail!("Unsupported primitive type `{name}` in host callbacks generation"), + }, + TypeRef::Named { name, args } => { + if args.is_empty() { + Ok(name.clone()) + } else { + let rendered = args + .iter() + .map(ts_type) + .collect::>>()? + .join(", "); + Ok(format!("{name}<{rendered}>")) + } + } + TypeRef::Vec(inner) => match inner.as_ref() { + TypeRef::Primitive(name) if name == "u8" => Ok("Uint8Array".to_string()), + _ => Ok(format!("Array<{}>", ts_type(inner)?)), + }, + TypeRef::Option(inner) => Ok(format!("{} | undefined", ts_type(inner)?)), + TypeRef::Tuple(items) => { + if items.is_empty() { + Ok("void".to_string()) + } else { + let rendered = items + .iter() + .map(ts_type) + .collect::>>()? + .join(", "); + Ok(format!("[{rendered}]")) + } + } + TypeRef::Array(inner, _len) => match inner.as_ref() { + TypeRef::Primitive(name) if name == "u8" => Ok("Uint8Array".to_string()), + _ => Ok(format!("Array<{}>", ts_type(inner)?)), + }, + TypeRef::Generic(name) => Ok(name.clone()), + TypeRef::Unit => Ok("void".to_string()), + } +} + +fn render_jsdoc(indent: &str, docs: Option<&str>) -> String { + let Some(docs) = docs else { + return String::new(); + }; + let docs = docs.trim(); + if docs.is_empty() { + return String::new(); + } + let body = docs + .lines() + .map(|line| { + if line.is_empty() { + format!("{indent} *") + } else { + format!("{indent} * {}", render_ts_doc_line(line)) + } + }) + .collect::>() + .join("\n"); + format!("{indent}/**\n{body}\n{indent} */\n") +} + +fn render_ts_doc_line(line: &str) -> String { + line.replace("[`", "`") + .replace("`]", "`") + .replace("Ok(())", "success") + .replace("None", "`undefined`") +} + +fn to_camel_case(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + let mut upper_next = false; + for (idx, ch) in name.chars().enumerate() { + if ch == '_' { + upper_next = idx != 0; + continue; + } + if upper_next { + out.extend(ch.to_uppercase()); + upper_next = false; + } else { + out.push(ch); + } + } + out +} diff --git a/rust/crates/truapi-codegen/src/ts/playground.rs b/rust/crates/truapi-codegen/src/ts/playground.rs index 51de54ff..adfe20ed 100644 --- a/rust/crates/truapi-codegen/src/ts/playground.rs +++ b/rust/crates/truapi-codegen/src/ts/playground.rs @@ -28,7 +28,7 @@ fn generate_playground_services_code( let wrappers = collect_versioned_wrappers(api); let emit_versions = versioned_wrapper_emit_versions(api, &wrappers, target_version)?; let aliases = selected_public_aliases(api, &wrappers, &emit_versions, target_version); - let ctx = CodecContext::default(); + let ctx = codec_context(&[]); let services = public_services(api)?; let explorer_type_ids = explorer_type_id_set(api, &aliases); @@ -160,6 +160,7 @@ pub(super) struct PlaygroundDocs { pub(super) client_example: Option, } +/// Split method docs into playground description text and a TypeScript example. pub(super) fn split_playground_docs(docs: Option<&str>) -> Result { let Some(docs) = docs else { return Ok(PlaygroundDocs { @@ -238,6 +239,7 @@ fn validate_example_docs(trait_name: &str, method_name: &str, docs: Option<&str> Ok(()) } +/// Strip the generated TypeScript namespace prefix used by playground types. pub(super) fn playground_type_name(value: &str) -> String { value.replace("T.", "") } diff --git a/rust/crates/truapi-codegen/tests/golden/dispatcher.rs b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs new file mode 100644 index 00000000..bc2eef63 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/dispatcher.rs @@ -0,0 +1,1913 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::Decode; + +use truapi::CallContext; +use truapi::api::{ + Account, + Chain, + Chat, + CoinPayment, + Entropy, + LocalStorage, + Notifications, + Payment, + Permissions, + Preimage, + ResourceAllocation, + Signing, + StatementStore, + System, + Theme, +}; +use truapi::versioned::{self, Versioned}; + +use crate::dispatcher::Dispatcher; +use crate::frame::encode_raw_err_payload; +use crate::frame::encode_raw_unit_ok_payload; +use crate::frame::encode_versioned_err_payload; +use crate::frame::encode_versioned_interrupt_payload; +use crate::frame::encode_versioned_ok_payload; +use crate::frame::encode_versioned_unit_ok_payload; +use crate::generated::wire_table; +use crate::subscription::subscription_stream; +#[cfg(debug_assertions)] +use truapi::api::Testing; + +/// Register every TrUAPI method with the dispatcher. +pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: truapi::api::TrUApi + 'static, +{ + register_account(dispatcher, host.clone()); + register_chain(dispatcher, host.clone()); + register_chat(dispatcher, host.clone()); + register_coin_payment(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_notifications(dispatcher, host.clone()); + register_payment(dispatcher, host.clone()); + register_permissions(dispatcher, host.clone()); + register_preimage(dispatcher, host.clone()); + register_resource_allocation(dispatcher, host.clone()); + register_signing(dispatcher, host.clone()); + register_statement_store(dispatcher, host.clone()); + register_system(dispatcher, host.clone()); + #[cfg(debug_assertions)] + register_testing(dispatcher, host.clone()); + register_theme(dispatcher, host); +} + +fn register_account

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.connection_status_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetResponse = match host.get_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::ACCOUNT_GET_ACCOUNT_ALIAS, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetAliasRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetAliasResponse = match host.get_account_alias(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::ACCOUNT_CREATE_ACCOUNT_PROOF, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountCreateProofRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountCreateProofResponse = match host.create_account_proof(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::ACCOUNT_GET_LEGACY_ACCOUNTS, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetLegacyAccountsRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetLegacyAccountsResponse = match host.get_legacy_accounts(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::ACCOUNT_GET_USER_ID, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetUserIdRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetUserIdResponse = match host.get_user_id(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::ACCOUNT_REQUEST_LOGIN, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostRequestLoginRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostRequestLoginResponse = match host.request_login(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.follow_head_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_HEAD_HEADER, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadHeaderRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadHeaderResponse = match host.get_head_header(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_HEAD_BODY, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadBodyRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadBodyResponse = match host.get_head_body(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_HEAD_STORAGE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStorageRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStorageResponse = match host.get_head_storage(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_CALL_HEAD, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadCallRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadCallResponse = match host.call_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_UNPIN_HEAD, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadUnpinRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadUnpinResponse = match host.unpin_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_CONTINUE_HEAD, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadContinueRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadContinueResponse = match host.continue_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_STOP_HEAD_OPERATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStopOperationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SPEC_GENESIS_HASH, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SPEC_CHAIN_NAME, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecChainNameRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecChainNameResponse = match host.get_spec_chain_name(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SPEC_PROPERTIES, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecPropertiesRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecPropertiesResponse = match host.get_spec_properties(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_BROADCAST_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionStopRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAT_CREATE_ROOM, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatCreateRoomRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatCreateRoomResponse = match host.create_room(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAT_REGISTER_BOT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatRegisterBotRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatRegisterBotResponse = match host.register_bot(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::CHAT_LIST_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.list_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAT_POST_MESSAGE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatPostMessageRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatPostMessageResponse = match host.post_message(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::CHAT_ACTION_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.action_subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_subscription(wire_table::CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.custom_message_render_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_coin_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: CoinPayment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreatePurseResponse = match host.create_purse(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentQueryPurseResponse = match host.query_purse(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.rebalance_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.delete_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreateReceivableResponse = match host.create_receivable(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreateChequeResponse = match host.create_cheque(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.deposit(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.refund(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.listen_for_payment(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::ENTROPY_DERIVE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::entropy::HostDeriveEntropyResponse = match host.derive(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::LOCAL_STORAGE_WRITE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageWriteRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::LOCAL_STORAGE_CLEAR, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageClearRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_notifications

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Notifications + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::notifications::HostPushNotificationResponse = match host.send_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationCancelRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::notifications::HostPushNotificationCancelResponse = match host.cancel_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.balance_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::PAYMENT_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.status_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::PAYMENT_TOP_UP, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentTopUpRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentTopUpResponse = match host.top_up(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_REMOTE_PERMISSION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::RemotePermissionRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::RemotePermissionResponse = match host.request_remote_permission(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.lookup_subscribe(&cx, request).await; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::PREIMAGE_SUBMIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageSubmitRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::preimage::RemotePreimageSubmitResponse = match host.submit(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionResponse = match host.create_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_SIGN_RAW, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawResponse = match host.sign_raw(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadResponse = match host.sign_payload(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF_AUTHORIZED, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::STATEMENT_STORE_SUBMIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(encode_versioned_unit_ok_payload(target_version)), + Err(err) => { + Ok(encode_versioned_err_payload(err, target_version)) + } + } + }) + }); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::SYSTEM_HANDSHAKE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostHandshakeResponse = match host.handshake(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SYSTEM_FEATURE_SUPPORTED, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostFeatureSupportedRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostFeatureSupportedResponse = match host.feature_supported(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::SYSTEM_NAVIGATE_TO, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostNavigateToRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostNavigateToResponse = match host.navigate_to(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +#[cfg(debug_assertions)] +fn register_testing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Testing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::TESTING_VERSION_PROBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::testing::TestingVersionProbeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::testing::TestingVersionProbeResponse = match host.version_probe(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::TESTING_ECHO_ERROR, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: truapi::v01::EchoErrorRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_raw_err_payload(error)); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + match host.echo_error(&cx, request).await { + Ok(()) => Ok(encode_raw_unit_ok_payload()), + Err(err) => Ok(encode_raw_err_payload(err)), + } + }) + }); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription(wire_table::THEME_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx).await; + Ok(subscription_stream::(stream)) + }) + }); + } +} diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts new file mode 100644 index 00000000..3edef0ec --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks-adapter.ts @@ -0,0 +1,78 @@ +// Auto-generated by truapi-codegen. Do not edit. +// +// Adapts the typed `HostCallbacks` surface onto the byte-oriented +// callback surface the WASM core invokes. Named wire types cross as +// SCALE bytes (`.enc`/`.dec`); strings, primitives, byte blobs and +// platform-local types pass through unchanged. + +import { + HostDevicePermissionRequest, + HostDevicePermissionResponse, + HostFeatureSupportedRequest, + HostFeatureSupportedResponse, + HostPushNotificationRequest, + HostPushNotificationResponse, + RemotePermissionRequest, + RemotePermissionResponse, + ThemeVariant, +} from "@parity/truapi"; +import type { + NotificationId, +} from "@parity/truapi"; +import { + CoreStorageKey, + UserConfirmationReview, +} from "./host-callbacks.js"; +import type { AuthState, HostCallbacks } from "./host-callbacks.js"; +import type { ChainConnect } from "../runtime.js"; +import { + chainConnectAdapter, + driveResultStream, +} from "../adapter-support.js"; + +export interface RawCallbacks { + authStateChanged(state: AuthState): void; + readCoreStorage(key: Uint8Array): Promise; + writeCoreStorage(key: Uint8Array, value: Uint8Array): Promise; + clearCoreStorage(key: Uint8Array): Promise; + featureSupported(request: Uint8Array): Promise; + navigateTo(url: string): Promise; + pushNotification(notification: Uint8Array): Promise; + cancelNotification(id: NotificationId): Promise; + devicePermission(request: Uint8Array): Promise; + remotePermission(request: Uint8Array): Promise; + submitPreimage(value: Uint8Array): Promise; + lookupPreimage(key: Uint8Array, sendItem: (item?: Uint8Array) => void): (() => void) | void; + read(key: string): Promise; + write(key: string, value: Uint8Array): Promise; + clear(key: string): Promise; + subscribeTheme(sendItem: (item?: Uint8Array) => void): (() => void) | void; + confirmUserAction(review: Uint8Array): Promise; + chainConnect: ChainConnect; +} +/** Adapt typed host callbacks into the raw SCALE callback surface the + * WASM core invokes. */ +export function createWasmRawCallbacks( + host: Required, +): RawCallbacks { + return { + authStateChanged: async (state) => await host.authStateChanged(state), + chainConnect: chainConnectAdapter(host), + readCoreStorage: async (key) => await host.readCoreStorage(CoreStorageKey.dec(key)), + writeCoreStorage: async (key, value) => await host.writeCoreStorage(CoreStorageKey.dec(key), value), + clearCoreStorage: async (key) => await host.clearCoreStorage(CoreStorageKey.dec(key)), + featureSupported: async (request) => HostFeatureSupportedResponse.enc(await host.featureSupported(HostFeatureSupportedRequest.dec(request))), + navigateTo: async (url) => await host.navigateTo(url), + pushNotification: async (notification) => HostPushNotificationResponse.enc(await host.pushNotification(HostPushNotificationRequest.dec(notification))), + cancelNotification: async (id) => await host.cancelNotification(id), + devicePermission: async (request) => HostDevicePermissionResponse.enc(await host.devicePermission(HostDevicePermissionRequest.dec(request))), + remotePermission: async (request) => RemotePermissionResponse.enc(await host.remotePermission(RemotePermissionRequest.dec(request))), + submitPreimage: async (value) => await host.submitPreimage(value), + lookupPreimage: (key, sendItem) => driveResultStream(host.lookupPreimage(key), sendItem), + read: async (key) => await host.read(key), + write: async (key, value) => await host.write(key, value), + clear: async (key) => await host.clear(key), + subscribeTheme: (sendItem) => driveResultStream(host.subscribeTheme(), (item) => sendItem(ThemeVariant.enc(item))), + confirmUserAction: async (review) => await host.confirmUserAction(UserConfirmationReview.dec(review)), + }; +} diff --git a/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts new file mode 100644 index 00000000..4aab10b0 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/host-callbacks.ts @@ -0,0 +1,497 @@ +// Auto-generated by truapi-codegen. Do not edit. +// +// Typed host-callbacks surface derived from the `truapi-platform` +// capability traits. One interface per Rust trait + a composite +// `HostCallbacks` interface that mirrors the `Platform` super-trait. + +import * as S from "@parity/truapi/scale"; + +import { + HostDevicePermissionRequest, + HostRequestResourceAllocationRequest, + HostSignPayloadRequest, + HostSignPayloadWithLegacyAccountRequest, + HostSignRawRequest, + HostSignRawWithLegacyAccountRequest, + LegacyAccountTxPayload, + ProductAccountTxPayload, + RemotePermissionRequest, +} from "@parity/truapi"; + +import type { + GenericError, + HostDevicePermissionResponse, + HostFeatureSupportedRequest, + HostFeatureSupportedResponse, + HostPushNotificationRequest, + HostPushNotificationResponse, + NotificationId, + RemotePermissionResponse, + Result, + ThemeVariant, +} from "@parity/truapi"; + +/** + * Review shown before a product asks to alias another product account. + */ +export interface AccountAliasReview { + /** + * Product currently handling the request. + */ + requestingProductId: string; + + /** + * Product whose account is being requested. + */ + targetProductId: string; +} + +/** + * Auth/session lifecycle state the core projects for host UI. The core owns + * every transition and emits states in order; hosts render the current state + * and never derive auth UI from any other signal. + */ +export type AuthState = + /** + * No active session and no login in progress. + */ + | { tag: "Disconnected"; value?: undefined } + /** + * A login is in progress: present the pairing deeplink/QR. Leave this + * state only on a subsequent emission (connected, failed, or + * disconnected after cancellation). + */ + | { tag: "Pairing"; value: { deeplink: string } } + /** + * A session is active. + */ + | { tag: "Connected"; value: SessionUiInfo } + /** + * The last login attempt failed; show the reason and offer a retry. + */ + | { tag: "LoginFailed"; value: { reason: string } }; + +/** + * Core-owned host-private storage slots. Products never address these slots; + * the host chooses the backing store for each slot. + */ +export type CoreStorageKey = + /** + * Opaque SSO/auth session blob. + */ + | { tag: "AuthSession"; value?: undefined } + /** + * Pairing device identity used during SSO flows. + */ + | { tag: "PairingDeviceIdentity"; value?: undefined } + /** + * Persisted authorization for one product-scoped permission request. + */ + | { tag: "PermissionAuthorization"; value: { productId: string; request: PermissionAuthorizationRequest } }; + +/** + * Review shown before a transaction-creation request is sent to the paired wallet. + */ +export type CreateTransactionReview = + /** + * Product-account transaction request. + */ + | { tag: "Product"; value: ProductAccountTxPayload } + /** + * Legacy-account transaction request. + */ + | { tag: "LegacyAccount"; value: LegacyAccountTxPayload }; + +/** + * Permission request whose authorization status can be inspected or updated + * by host administration UI. + */ +export type PermissionAuthorizationRequest = + /** + * Device-level permission such as camera, microphone, or location. + */ + | { tag: "Device"; value: HostDevicePermissionRequest } + /** + * Remote/product-scoped permission such as chain submit or HTTP access. + */ + | { tag: "Remote"; value: RemotePermissionRequest }; + +/** + * Authorization status for a permission request. + * + * `NotDetermined` means the core has no persisted answer and will prompt the + * host the next time the product requests this permission. + */ +export type PermissionAuthorizationStatus = "NotDetermined" | "Denied" | "Authorized"; + +/** + * Review shown before a preimage is submitted. + */ +export interface PreimageSubmitReview { + /** + * Size of the preimage in bytes. + */ + size: bigint; +} + +/** + * Decoded session fields a host shell needs to render account UI without + * parsing the opaque session blob the core persists through `CoreStorage`. + */ +export interface SessionUiInfo { + /** + * 32-byte sr25519 root public key of the active session. + */ + publicKey: Uint8Array; + + /** + * Wallet identity account id used for People-chain username lookup. + */ + identityAccountId?: Uint8Array; + + /** + * Short username from the People-chain identity record. + */ + liteUsername?: string; + + /** + * Fully qualified username from the People-chain identity record. + */ + fullUsername?: string; +} + +/** + * Review shown before a sign-payload request is sent to the paired wallet. + */ +export type SignPayloadReview = + /** + * Product-account signing request. + */ + | { tag: "Product"; value: HostSignPayloadRequest } + /** + * Legacy-account signing request. + */ + | { tag: "LegacyAccount"; value: HostSignPayloadWithLegacyAccountRequest }; + +/** + * Review shown before a sign-raw request is sent to the paired wallet. + */ +export type SignRawReview = + /** + * Product-account raw signing request. + */ + | { tag: "Product"; value: HostSignRawRequest } + /** + * Legacy-account raw signing request. + */ + | { tag: "LegacyAccount"; value: HostSignRawWithLegacyAccountRequest }; + +/** + * Review shown before a user-confirmed core action continues. + */ +export type UserConfirmationReview = + /** + * Sign a SCALE payload with a product or legacy account. + */ + | { tag: "SignPayload"; value: SignPayloadReview } + /** + * Sign raw bytes with a product or legacy account. + */ + | { tag: "SignRaw"; value: SignRawReview } + /** + * Create a transaction with a product or legacy account. + */ + | { tag: "CreateTransaction"; value: CreateTransactionReview } + /** + * Allow a product to request another product account alias. + */ + | { tag: "AccountAlias"; value: AccountAliasReview } + /** + * Allocate resources for the requesting product. + */ + | { tag: "ResourceAllocation"; value: HostRequestResourceAllocationRequest } + /** + * Submit a preimage to the host-selected backend. + */ + | { tag: "PreimageSubmit"; value: PreimageSubmitReview }; + +/** + * Review shown before a product asks to alias another product account. + */ +export const AccountAliasReview: S.Codec = S.lazy((): S.Codec => S.Struct({requestingProductId: S.str, targetProductId: S.str}) as S.Codec); + +/** + * Core-owned host-private storage slots. Products never address these slots; + * the host chooses the backing store for each slot. + */ +export const CoreStorageKey: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({AuthSession: S._void, PairingDeviceIdentity: S._void, PermissionAuthorization: S.Struct({productId: S.str, request: PermissionAuthorizationRequest}) as S.Codec<{ productId: string; request: PermissionAuthorizationRequest }>})); + +/** + * Review shown before a transaction-creation request is sent to the paired wallet. + */ +export const CreateTransactionReview: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({Product: ProductAccountTxPayload, LegacyAccount: LegacyAccountTxPayload})); + +/** + * Permission request whose authorization status can be inspected or updated + * by host administration UI. + */ +export const PermissionAuthorizationRequest: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({Device: HostDevicePermissionRequest, Remote: RemotePermissionRequest})); + +/** + * Authorization status for a permission request. + * + * `NotDetermined` means the core has no persisted answer and will prompt the + * host the next time the product requests this permission. + */ +export const PermissionAuthorizationStatus: S.Codec = S.lazy((): S.Codec => S.Status("NotDetermined", "Denied", "Authorized")); + +/** + * Review shown before a preimage is submitted. + */ +export const PreimageSubmitReview: S.Codec = S.lazy((): S.Codec => S.Struct({size: S.u64}) as S.Codec); + +/** + * Review shown before a sign-payload request is sent to the paired wallet. + */ +export const SignPayloadReview: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({Product: HostSignPayloadRequest, LegacyAccount: HostSignPayloadWithLegacyAccountRequest})); + +/** + * Review shown before a sign-raw request is sent to the paired wallet. + */ +export const SignRawReview: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({Product: HostSignRawRequest, LegacyAccount: HostSignRawWithLegacyAccountRequest})); + +/** + * Review shown before a user-confirmed core action continues. + */ +export const UserConfirmationReview: S.Codec = S.lazy((): S.Codec => S.TaggedUnion({SignPayload: SignPayloadReview, SignRaw: SignRawReview, CreateTransaction: CreateTransactionReview, AccountAlias: AccountAliasReview, ResourceAllocation: HostRequestResourceAllocationRequest, PreimageSubmit: PreimageSubmitReview})); + +/** + * Host auth UI driven by core-owned `AuthState` transitions. + */ +export interface AuthPresenter { + /** + * Observe an auth state change. Emitted only when the state actually + * changes, in transition order. Default is a no-op for hosts that + * render no auth UI. + */ + authStateChanged?(state: AuthState): void; +} + +/** + * JSON-RPC provider factory for chain access. + * + * The platform provides a way to get a JSON-RPC connection for a given chain. + * The server runtime manages the chainHead v1 state machine on top of this. + */ +export interface ChainProvider { + /** + * Open a JSON-RPC connection for the chain identified by `genesis_hash`. + * Drop the returned connection to disconnect. + */ + connect(genesisHash: Uint8Array): Promise; +} + +/** + * Core-owned administration API exposed to host UI. + * + * Hosts call this surface to drive global runtime actions or inspect/update + * core-owned state without going through a product-scoped TrUAPI request. + */ +export interface CoreAdmin { + /** + * Best-effort logout/disconnect. Clears the active session and emits the + * resulting auth state transition. + */ + disconnectSession(): Promise; + + /** + * Cancel any in-flight pairing request. + */ + cancelPairing(): void; + + /** + * Notify the core that the host-global auth session slot may have + * changed. The core re-reads storage and emits any resulting auth state. + */ + notifySessionStoreChanged(): void; + + /** + * Read a stored permission authorization status without prompting. + */ + getPermissionAuthorizationStatus(request: PermissionAuthorizationRequest): Promise; + + /** + * Read stored permission authorization statuses without prompting. + * + * Results are returned in the same order as `requests`. + */ + getPermissionAuthorizationStatuses(requests: Array): Promise>; + + /** + * Update a stored permission authorization status. `NotDetermined` clears + * the stored value so the next product request prompts again. + */ + setPermissionAuthorizationStatus(request: PermissionAuthorizationRequest, status: PermissionAuthorizationStatus): Promise; +} + +/** + * Host-private persistence for core-owned state. + */ +export interface CoreStorage { + /** + * Read a core-owned value by typed slot. + */ + readCoreStorage(key: CoreStorageKey): Promise; + + /** + * Write a core-owned value by typed slot. + */ + writeCoreStorage(key: CoreStorageKey, value: Uint8Array): Promise; + + /** + * Clear a core-owned value by typed slot. + */ + clearCoreStorage(key: CoreStorageKey): Promise; +} + +/** + * Feature-support probing. The host answers whether it can service a given + * capability (currently scoped to per-chain support). + */ +export interface Features { + /** + * Report whether the requested feature is supported. + */ + featureSupported(request: HostFeatureSupportedRequest): Promise; +} + +/** + * A live JSON-RPC connection to a chain. + */ +export interface JsonRpcConnection { + /** + * Send a JSON-RPC request string. + */ + send(request: string): void; + + /** + * Stream of JSON-RPC response strings. + */ + responses(): AsyncIterable; + + /** + * Close the connection lease. + * + * Hosts may keep a shared underlying transport alive, but this handle + * must stop receiving responses and release any per-caller resources. + */ + close(): void; +} + +/** + * Open URLs in the system browser. Input is already trimmed, categorized, + * and (where needed) normalized by the core; the host implementation only + * needs to hand the URL to the OS URL handler. + */ +export interface Navigation { + /** + * Open the given URL in the system browser. + */ + navigateTo(url: string): Promise; +} + +/** + * Deliver push notifications. + */ +export interface Notifications { + /** + * Schedule or immediately display the given notification and return the + * host-assigned id. + */ + pushNotification(notification: HostPushNotificationRequest): Promise; + + /** + * Cancel a notification by id. Idempotent: cancelling an already-fired or + * unknown id still returns `success`. + */ + cancelNotification?(id: NotificationId): Promise; +} + +/** + * Permission prompts. v0.1 keeps device permissions (camera, mic, NFC, ...) + * separate from remote permissions (domain access, chain submit, ...), so the + * platform surface mirrors that split. + */ +export interface Permissions { + /** + * Prompt the user for a device-level permission. + */ + devicePermission(request: HostDevicePermissionRequest): Promise; + + /** + * Prompt the user for a remote (product-scoped) permission bundle. + */ + remotePermission(request: RemotePermissionRequest): Promise; +} + +/** + * Host preimage backend. The core owns wire mapping and subscription + * lifecycle; the host owns the selected backend. + */ +export interface PreimageHost { + /** + * Submit the preimage and return its key. + */ + submitPreimage?(value: Uint8Array): Promise; + + /** + * Emits current value/miss immediately, then future updates. + */ + lookupPreimage(key: Uint8Array): AsyncIterable>; +} + +/** + * Product-scoped key-value storage. The platform namespaces keys so different + * products cannot read each other's data. + */ +export interface ProductStorage { + /** + * Read a value by key. + */ + read(key: string): Promise; + + /** + * Write a value to a key. + */ + write(key: string, value: Uint8Array): Promise; + + /** + * Clear a value at a key. + */ + clear(key: string): Promise; +} + +/** + * Host theme source. + */ +export interface ThemeHost { + /** + * Emits current theme immediately, then future changes. + */ + subscribeTheme(): AsyncIterable>; +} + +/** + * Local user confirmation UI for session-channel operations. + */ +export interface UserConfirmation { + /** + * Confirm a reviewed action before the core asks the SSO peer. + */ + confirmUserAction?(review: UserConfirmationReview): Promise; +} + +/** + * Combined platform interface. A host must provide all capability traits. + */ +export interface HostCallbacks extends Navigation, Notifications, Permissions, Features, ProductStorage, CoreStorage, ChainProvider, AuthPresenter, UserConfirmation, ThemeHost, PreimageHost {} diff --git a/rust/crates/truapi-codegen/tests/golden/wire_table.rs b/rust/crates/truapi-codegen/tests/golden/wire_table.rs new file mode 100644 index 00000000..1d529c9f --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/wire_table.rs @@ -0,0 +1,746 @@ +//! Wire-protocol discriminant table. +//! +//! Auto-generated by truapi-codegen. Do not edit. +//! +//! Each method reserves either two ids (request/response) or four +//! (start/stop/interrupt/receive). The ids for each method are exposed +//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers live +//! in exactly one place. The table is sorted by request/start id. + +/// Request method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestFrameIds { + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, +} + +/// Subscription method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SubscriptionFrameIds { + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, +} + +/// A single wire-table row. +pub struct WireEntry { + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, +} + +/// Wire-slot shape: request/response pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), +} + +/// Wire discriminants for `system_handshake`. +pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `system_feature_supported`. +pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + request_id: 12, + response_id: 13, +}; + +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + request_id: 14, + response_id: 15, +}; + +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + request_id: 16, + response_id: 17, +}; + +/// Wire discriminants for `account_connection_status_subscribe`. +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, +}; + +/// Wire discriminants for `account_get_account`. +pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 22, + response_id: 23, +}; + +/// Wire discriminants for `account_get_account_alias`. +pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { + request_id: 24, + response_id: 25, +}; + +/// Wire discriminants for `account_create_account_proof`. +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 26, + response_id: 27, +}; + +/// Wire discriminants for `account_get_legacy_accounts`. +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { + request_id: 28, + response_id: 29, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 30, + response_id: 31, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 32, + response_id: 33, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 34, + response_id: 35, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 36, + response_id: 37, +}; + +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + request_id: 38, + response_id: 39, +}; + +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + request_id: 40, + response_id: 41, +}; + +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, +}; + +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + request_id: 46, + response_id: 47, +}; + +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, +}; + +/// Wire discriminants for `chat_custom_message_render_subscribe`. +pub const CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 60, + response_id: 61, +}; + +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 62, + response_id: 63, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 68, + response_id: 69, +}; + +/// Wire discriminants for `chain_follow_head_subscribe`. +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, +}; + +/// Wire discriminants for `chain_get_head_header`. +pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { + request_id: 80, + response_id: 81, +}; + +/// Wire discriminants for `chain_get_head_body`. +pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { + request_id: 82, + response_id: 83, +}; + +/// Wire discriminants for `chain_get_head_storage`. +pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { + request_id: 84, + response_id: 85, +}; + +/// Wire discriminants for `chain_call_head`. +pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 86, + response_id: 87, +}; + +/// Wire discriminants for `chain_unpin_head`. +pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 88, + response_id: 89, +}; + +/// Wire discriminants for `chain_continue_head`. +pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 90, + response_id: 91, +}; + +/// Wire discriminants for `chain_stop_head_operation`. +pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { + request_id: 92, + response_id: 93, +}; + +/// Wire discriminants for `chain_get_spec_genesis_hash`. +pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { + request_id: 94, + response_id: 95, +}; + +/// Wire discriminants for `chain_get_spec_chain_name`. +pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { + request_id: 96, + response_id: 97, +}; + +/// Wire discriminants for `chain_get_spec_properties`. +pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { + request_id: 98, + response_id: 99, +}; + +/// Wire discriminants for `chain_broadcast_transaction`. +pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 100, + response_id: 101, +}; + +/// Wire discriminants for `chain_stop_transaction`. +pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 102, + response_id: 103, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, +}; + +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + request_id: 108, + response_id: 109, +}; + +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + request_id: 110, + response_id: 111, +}; + +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + request_id: 112, + response_id: 113, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + request_id: 114, + response_id: 115, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + request_id: 116, + response_id: 117, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + request_id: 122, + response_id: 123, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 124, + response_id: 125, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 130, + response_id: 131, +}; + +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + request_id: 132, + response_id: 133, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 134, + response_id: 135, +}; + +/// Wire discriminants for `coin_payment_create_purse`. +pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 136, + response_id: 137, +}; + +/// Wire discriminants for `coin_payment_query_purse`. +pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 138, + response_id: 139, +}; + +/// Wire discriminants for `coin_payment_rebalance_purse`. +pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 140, + stop_id: 141, + interrupt_id: 142, + receive_id: 143, +}; + +/// Wire discriminants for `coin_payment_delete_purse`. +pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 144, + stop_id: 145, + interrupt_id: 146, + receive_id: 147, +}; + +/// Wire discriminants for `coin_payment_create_receivable`. +pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { + request_id: 148, + response_id: 149, +}; + +/// Wire discriminants for `coin_payment_create_cheque`. +pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { + request_id: 150, + response_id: 151, +}; + +/// Wire discriminants for `coin_payment_deposit`. +pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 152, + stop_id: 153, + interrupt_id: 154, + receive_id: 155, +}; + +/// Wire discriminants for `coin_payment_refund`. +pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 156, + stop_id: 157, + interrupt_id: 158, + receive_id: 159, +}; + +/// Wire discriminants for `coin_payment_listen_for_payment`. +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 160, + stop_id: 161, + interrupt_id: 162, + receive_id: 163, +}; + +#[cfg(debug_assertions)] +/// Wire discriminants for `testing_version_probe`. +pub const TESTING_VERSION_PROBE: RequestFrameIds = RequestFrameIds { + request_id: 164, + response_id: 165, +}; + +#[cfg(debug_assertions)] +/// Wire discriminants for `testing_echo_error`. +pub const TESTING_ECHO_ERROR: RequestFrameIds = RequestFrameIds { + request_id: 166, + response_id: 167, +}; + +/// The full wire table. Ordering is part of the wire protocol; +/// only ever append. Removed methods leave their slot empty. +pub const WIRE_TABLE: &[WireEntry] = &[ + WireEntry { + method: "system_handshake", + kind: WireKind::Request(SYSTEM_HANDSHAKE), + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), + }, + WireEntry { + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request(SYSTEM_NAVIGATE_TO), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription(ACCOUNT_CONNECTION_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT), + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT_ALIAS), + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request(ACCOUNT_CREATE_ACCOUNT_PROOF), + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription(CHAIN_FOLLOW_HEAD_SUBSCRIBE), + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request(CHAIN_GET_HEAD_HEADER), + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request(CHAIN_GET_HEAD_BODY), + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request(CHAIN_GET_HEAD_STORAGE), + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request(CHAIN_CALL_HEAD), + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request(CHAIN_UNPIN_HEAD), + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request(CHAIN_CONTINUE_HEAD), + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request(CHAIN_STOP_HEAD_OPERATION), + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request(CHAIN_GET_SPEC_GENESIS_HASH), + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request(CHAIN_GET_SPEC_CHAIN_NAME), + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request(CHAIN_GET_SPEC_PROPERTIES), + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request(CHAIN_BROADCAST_TRANSACTION), + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request(CHAIN_STOP_TRANSACTION), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + }, + WireEntry { + method: "coin_payment_create_purse", + kind: WireKind::Request(COIN_PAYMENT_CREATE_PURSE), + }, + WireEntry { + method: "coin_payment_query_purse", + kind: WireKind::Request(COIN_PAYMENT_QUERY_PURSE), + }, + WireEntry { + method: "coin_payment_rebalance_purse", + kind: WireKind::Subscription(COIN_PAYMENT_REBALANCE_PURSE), + }, + WireEntry { + method: "coin_payment_delete_purse", + kind: WireKind::Subscription(COIN_PAYMENT_DELETE_PURSE), + }, + WireEntry { + method: "coin_payment_create_receivable", + kind: WireKind::Request(COIN_PAYMENT_CREATE_RECEIVABLE), + }, + WireEntry { + method: "coin_payment_create_cheque", + kind: WireKind::Request(COIN_PAYMENT_CREATE_CHEQUE), + }, + WireEntry { + method: "coin_payment_deposit", + kind: WireKind::Subscription(COIN_PAYMENT_DEPOSIT), + }, + WireEntry { + method: "coin_payment_refund", + kind: WireKind::Subscription(COIN_PAYMENT_REFUND), + }, + WireEntry { + method: "coin_payment_listen_for_payment", + kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), + }, + #[cfg(debug_assertions)] + WireEntry { + method: "testing_version_probe", + kind: WireKind::Request(TESTING_VERSION_PROBE), + }, + #[cfg(debug_assertions)] + WireEntry { + method: "testing_echo_error", + kind: WireKind::Request(TESTING_ECHO_ERROR), + }, +]; diff --git a/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts new file mode 100644 index 00000000..34275dbf --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden/worker-callbacks.ts @@ -0,0 +1,117 @@ +// Auto-generated by truapi-codegen. Do not edit. +// +// Worker-side metadata and proxy functions for the raw WASM callback +// surface. The worker transport/lifecycle remains hand-written; this +// file owns the callback names, host-hook arity, and +// subscription payload shape derived from `truapi-platform`. + +import type { ChainConnect } from "../runtime.js"; +import type { RawCallbacks } from "./host-callbacks-adapter.js"; + +export const CALLBACK_NAMES = [ + "authStateChanged", + "readCoreStorage", + "writeCoreStorage", + "clearCoreStorage", + "featureSupported", + "navigateTo", + "pushNotification", + "cancelNotification", + "devicePermission", + "remotePermission", + "submitPreimage", + "read", + "write", + "clear", + "confirmUserAction", +] as const; +export type CallbackName = typeof CALLBACK_NAMES[number]; + +export const SUBSCRIPTION_NAMES = [ + "lookupPreimage", + "subscribeTheme", +] as const; +export type SubscriptionName = typeof SUBSCRIPTION_NAMES[number]; + +export interface WorkerCallbackBridge { + callbackRequest(name: CallbackName, args: readonly unknown[]): Promise; + startSubscription( + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (value: T) => void, + ): () => void; + chainConnect: ChainConnect; +} + +function rawCallbacks(bridge: WorkerCallbackBridge): Required> { + return { + authStateChanged: (state) => + void bridge.callbackRequest("authStateChanged", [state]).catch(() => {}), + readCoreStorage: (key) => + bridge.callbackRequest("readCoreStorage", [key]) as ReturnType, + writeCoreStorage: (key, value) => + bridge.callbackRequest("writeCoreStorage", [key, value]) as ReturnType, + clearCoreStorage: (key) => + bridge.callbackRequest("clearCoreStorage", [key]) as ReturnType, + featureSupported: (request) => + bridge.callbackRequest("featureSupported", [request]) as ReturnType, + navigateTo: (url) => + bridge.callbackRequest("navigateTo", [url]) as ReturnType, + pushNotification: (notification) => + bridge.callbackRequest("pushNotification", [notification]) as ReturnType, + cancelNotification: (id) => + bridge.callbackRequest("cancelNotification", [id]) as ReturnType, + devicePermission: (request) => + bridge.callbackRequest("devicePermission", [request]) as ReturnType, + remotePermission: (request) => + bridge.callbackRequest("remotePermission", [request]) as ReturnType, + submitPreimage: (value) => + bridge.callbackRequest("submitPreimage", [value]) as ReturnType, + read: (key) => + bridge.callbackRequest("read", [key]) as ReturnType, + write: (key, value) => + bridge.callbackRequest("write", [key, value]) as ReturnType, + clear: (key) => + bridge.callbackRequest("clear", [key]) as ReturnType, + confirmUserAction: (review) => + bridge.callbackRequest("confirmUserAction", [review]) as ReturnType, + }; +} + +function subscriptionRawCallbacks(bridge: WorkerCallbackBridge): Required> { + return { + lookupPreimage: (key, sendItem) => + bridge.startSubscription("lookupPreimage", key, sendItem), + subscribeTheme: (sendItem) => + bridge.startSubscription("subscribeTheme", null, sendItem), + }; +} + +export function createWorkerRawCallbacks( + bridge: WorkerCallbackBridge, +): Record { + const callbacks: Record = { + ...rawCallbacks(bridge), + ...subscriptionRawCallbacks(bridge), + chainConnect: bridge.chainConnect, + }; + return callbacks; +} + +export function startRawSubscription( + callbacks: RawCallbacks, + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (value?: unknown) => void, +): (() => void) | void { + switch (name) { + case "lookupPreimage": + if (payload === null) { + console.warn(`[truapi worker] ${name} requires payload`); + return undefined; + } + return callbacks.lookupPreimage(payload, sendItem); + case "subscribeTheme": + return callbacks.subscribeTheme(sendItem); + } +} diff --git a/rust/crates/truapi-codegen/tests/golden_rust_emit.rs b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs new file mode 100644 index 00000000..98f7a095 --- /dev/null +++ b/rust/crates/truapi-codegen/tests/golden_rust_emit.rs @@ -0,0 +1,286 @@ +//! Golden snapshot test for the Rust dispatcher emitter. +//! +//! Each test runs `cargo +nightly rustdoc -p truapi` into its own +//! `--target-dir` under a per-test tempdir so concurrent test execution +//! cannot race on the shared `target/doc/truapi.json` path. Nightly Rust +//! is required; if it is not available the test panics rather than +//! silently passing (set up rustup with `rustup toolchain install nightly`). + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn quoted_strings_in_const_array(src: &str, const_name: &str) -> Vec { + let marker = format!("export const {const_name} = ["); + let start = src + .find(&marker) + .unwrap_or_else(|| panic!("missing {const_name}")); + let rest = &src[start + marker.len()..]; + let end = rest + .find("] as const") + .unwrap_or_else(|| panic!("unterminated {const_name}")); + rest[..end] + .lines() + .filter_map(|line| { + let trimmed = line.trim().trim_end_matches(','); + trimmed + .strip_prefix('"') + .and_then(|s| s.strip_suffix('"')) + .map(str::to_string) + }) + .collect() +} + +fn wasm_optional_callback_names(workspace: &Path) -> Vec { + let wasm_rs = workspace.join("rust/crates/truapi-server/src/wasm.rs"); + if !wasm_rs.exists() { + return Vec::new(); + } + let src = fs::read_to_string(wasm_rs).expect("read wasm.rs"); + let mut names = src + .lines() + .filter_map(|line| { + let line = line.trim(); + let start = line.find("get_optional_function(callbacks, \"")?; + let quoted = &line[start + "get_optional_function(callbacks, \"".len()..]; + let end = quoted.find('"')?; + let name = "ed[..end]; + match name { + "chainConnect" | "dispose" => None, + _ => Some(name.to_string()), + } + }) + .collect::>(); + names.sort(); + names +} + +/// Run `cargo +nightly rustdoc -p truapi --output-format json` into the +/// given `target_dir` and return the path to the produced JSON file. +/// Panics with a clear message if nightly is unavailable so CI cannot +/// pass vacuously. +fn produce_rustdoc_json(workspace_root: &Path, target_dir: &Path) -> PathBuf { + produce_rustdoc_json_for_package(workspace_root, target_dir, "truapi") +} + +fn produce_rustdoc_json_for_package( + workspace_root: &Path, + target_dir: &Path, + package: &str, +) -> PathBuf { + let output = Command::new("cargo") + .args(["+nightly", "rustdoc", "-p", package, "--target-dir"]) + .arg(target_dir) + .args(["--", "-Z", "unstable-options", "--output-format", "json"]) + .current_dir(workspace_root) + .output() + .expect( + "failed to spawn `cargo +nightly rustdoc`; install nightly via \ + `rustup toolchain install nightly`", + ); + assert!( + output.status.success(), + "`cargo +nightly rustdoc -p {package}` failed (status {}); nightly toolchain is required.\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + let json_name = package.replace('-', "_"); + let json = target_dir.join(format!("doc/{json_name}.json")); + assert!( + json.exists(), + "rustdoc JSON not found at {} after successful rustdoc invocation", + json.display(), + ); + json +} + +fn workspace_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(3) + .expect("workspace root above rust/crates/truapi-codegen") + .to_path_buf() +} + +#[test] +fn golden_dispatcher_and_wire_table() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace = workspace_root(); + + let tempdir = tempfile::tempdir().expect("tempdir"); + let rustdoc_json = produce_rustdoc_json(&workspace, &tempdir.path().join("rustdoc-target")); + + let out = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) + .args([ + "--input", + rustdoc_json.to_str().unwrap(), + "--output", + tempdir.path().join("ts").to_str().unwrap(), + "--rust-output", + tempdir.path().join("rust").to_str().unwrap(), + ]) + .output() + .expect("run truapi-codegen"); + assert!( + out.status.success(), + "codegen failed: stdout=\n{}\nstderr=\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr), + ); + + // Compare both emitted files against the goldens. We assert on + // wire_table.rs first because it's small and the diff is easy to + // read when the wire ids drift. + let golden_dir = manifest_dir.join("tests/golden"); + let cases = [ + ("wire_table.rs", "wire_table.rs"), + ("dispatcher.rs", "dispatcher.rs"), + ]; + for (golden_name, output_name) in cases { + let golden = fs::read_to_string(golden_dir.join(golden_name)) + .unwrap_or_else(|e| panic!("read {golden_name}: {e}")); + let actual = fs::read_to_string(tempdir.path().join("rust").join(output_name)) + .unwrap_or_else(|e| panic!("read generated {output_name}: {e}")); + if golden != actual { + // Dump actual to a sibling file for easy inspection + // when running locally. + let dump = manifest_dir.join(format!("tests/golden/{output_name}.actual")); + let _ = fs::write(&dump, &actual); + panic!( + "golden mismatch for {output_name}; wrote actual to {}", + dump.display() + ); + } + } +} + +/// Idempotence guard at the integration level: running the binary twice +/// against the same input must produce identical output. This catches +/// non-determinism (HashMap iteration order, timestamps, etc.) that the +/// inline unit tests might miss because they exercise smaller APIs. +#[test] +fn binary_emission_is_idempotent() { + let workspace = workspace_root(); + let tempdir = tempfile::tempdir().expect("tempdir"); + let rustdoc_json = produce_rustdoc_json(&workspace, &tempdir.path().join("rustdoc-target")); + + let run_once = || -> (String, String) { + let tmp = tempfile::tempdir().unwrap(); + let status = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) + .args([ + "--input", + rustdoc_json.to_str().unwrap(), + "--output", + tmp.path().join("ts").to_str().unwrap(), + "--rust-output", + tmp.path().join("rust").to_str().unwrap(), + ]) + .status() + .expect("run truapi-codegen"); + assert!(status.success(), "codegen run failed"); + let dispatcher = + fs::read_to_string(tmp.path().join("rust/dispatcher.rs")).expect("read dispatcher"); + let wire_table = + fs::read_to_string(tmp.path().join("rust/wire_table.rs")).expect("read wire_table"); + (dispatcher, wire_table) + }; + + let (a_disp, a_wire) = run_once(); + let (b_disp, b_wire) = run_once(); + assert_eq!(a_disp, b_disp, "dispatcher.rs differs between runs"); + assert_eq!(a_wire, b_wire, "wire_table.rs differs between runs"); +} + +#[test] +fn golden_host_callbacks_ts() { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace = workspace_root(); + + let tempdir = tempfile::tempdir().expect("tempdir"); + let truapi_json = produce_rustdoc_json(&workspace, &tempdir.path().join("rustdoc-target")); + let platform_json = produce_rustdoc_json_for_package( + &workspace, + &tempdir.path().join("rustdoc-platform-target"), + "truapi-platform", + ); + + let out = Command::new(env!("CARGO_BIN_EXE_truapi-codegen")) + .args([ + "--input", + truapi_json.to_str().unwrap(), + "--output", + tempdir.path().join("ts").to_str().unwrap(), + "--platform-input", + platform_json.to_str().unwrap(), + "--platform-ts-output", + tempdir.path().join("host").to_str().unwrap(), + "--platform-wasm-adapter-output", + tempdir.path().join("wasm").to_str().unwrap(), + ]) + .output() + .expect("run truapi-codegen"); + assert!( + out.status.success(), + "codegen failed: stdout=\n{}\nstderr=\n{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr), + ); + + let golden_path = manifest_dir.join("tests/golden/host-callbacks.ts"); + let golden = + fs::read_to_string(&golden_path).unwrap_or_else(|e| panic!("read host-callbacks.ts: {e}")); + let actual = fs::read_to_string(tempdir.path().join("host/host-callbacks.ts")) + .expect("read generated host-callbacks.ts"); + if golden != actual { + let dump = manifest_dir.join("tests/golden/host-callbacks.ts.actual"); + let _ = fs::write(&dump, &actual); + panic!( + "golden mismatch for host-callbacks.ts; wrote actual to {}", + dump.display() + ); + } + + let adapter_golden_path = manifest_dir.join("tests/golden/host-callbacks-adapter.ts"); + let adapter_actual = fs::read_to_string(tempdir.path().join("wasm/host-callbacks-adapter.ts")) + .expect("read generated host-callbacks-adapter.ts"); + let adapter_golden = fs::read_to_string(&adapter_golden_path).unwrap_or_default(); + if adapter_golden != adapter_actual { + let dump = manifest_dir.join("tests/golden/host-callbacks-adapter.ts.actual"); + let _ = fs::write(&dump, &adapter_actual); + panic!( + "golden mismatch for host-callbacks-adapter.ts; wrote actual to {}", + dump.display() + ); + } + + let worker_golden_path = manifest_dir.join("tests/golden/worker-callbacks.ts"); + let worker_actual = fs::read_to_string(tempdir.path().join("wasm/worker-callbacks.ts")) + .expect("read generated worker-callbacks.ts"); + let worker_golden = fs::read_to_string(&worker_golden_path).unwrap_or_default(); + if worker_golden != worker_actual { + let dump = manifest_dir.join("tests/golden/worker-callbacks.ts.actual"); + let _ = fs::write(&dump, &worker_actual); + panic!( + "golden mismatch for worker-callbacks.ts; wrote actual to {}", + dump.display() + ); + } + + assert!( + !worker_actual.contains("OPTIONAL_CALLBACK_NAMES"), + "worker callback generation should not expose an optional callback manifest" + ); + let mut generated_names = quoted_strings_in_const_array(&worker_actual, "CALLBACK_NAMES"); + generated_names.extend(quoted_strings_in_const_array( + &worker_actual, + "SUBSCRIPTION_NAMES", + )); + let wasm_optional = wasm_optional_callback_names(&workspace); + for name in wasm_optional { + assert!( + generated_names.contains(&name), + "generated worker names must include JsBridge optional callback `{name}`" + ); + } +} diff --git a/rust/crates/truapi-server/src/generated/dispatcher.rs b/rust/crates/truapi-server/src/generated/dispatcher.rs new file mode 100644 index 00000000..4a89f611 --- /dev/null +++ b/rust/crates/truapi-server/src/generated/dispatcher.rs @@ -0,0 +1,2182 @@ +//! Wire dispatcher for the unified `TrUApi` trait. +//! +//! Auto-generated by truapi-codegen. Do not edit. + +use std::sync::Arc; + +use parity_scale_codec::Decode; + +use truapi::CallContext; +use truapi::api::{ + Account, Chain, Chat, CoinPayment, Entropy, LocalStorage, Notifications, Payment, Permissions, + Preimage, ResourceAllocation, Signing, StatementStore, System, Theme, +}; +use truapi::versioned::{self, Versioned}; + +use crate::dispatcher::Dispatcher; +use crate::frame::encode_raw_err_payload; +use crate::frame::encode_raw_unit_ok_payload; +use crate::frame::encode_versioned_err_payload; +use crate::frame::encode_versioned_interrupt_payload; +use crate::frame::encode_versioned_ok_payload; +use crate::frame::encode_versioned_unit_ok_payload; +use crate::generated::wire_table; +use crate::subscription::subscription_stream; +#[cfg(debug_assertions)] +use truapi::api::Testing; + +/// Register every TrUAPI method with the dispatcher. +pub fn register

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: truapi::api::TrUApi + 'static, +{ + register_account(dispatcher, host.clone()); + register_chain(dispatcher, host.clone()); + register_chat(dispatcher, host.clone()); + register_coin_payment(dispatcher, host.clone()); + register_entropy(dispatcher, host.clone()); + register_local_storage(dispatcher, host.clone()); + register_notifications(dispatcher, host.clone()); + register_payment(dispatcher, host.clone()); + register_permissions(dispatcher, host.clone()); + register_preimage(dispatcher, host.clone()); + register_resource_allocation(dispatcher, host.clone()); + register_signing(dispatcher, host.clone()); + register_statement_store(dispatcher, host.clone()); + register_system(dispatcher, host.clone()); + #[cfg(debug_assertions)] + register_testing(dispatcher, host.clone()); + register_theme(dispatcher, host); +} + +fn register_account

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Account + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::ACCOUNT_CONNECTION_STATUS_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.connection_status_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::account::HostAccountConnectionStatusSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::ACCOUNT_GET_ACCOUNT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostAccountGetError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetResponse = + match host.get_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::ACCOUNT_GET_ACCOUNT_ALIAS, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountGetAliasRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostAccountGetAliasError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountGetAliasResponse = + match host.get_account_alias(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::ACCOUNT_CREATE_ACCOUNT_PROOF, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostAccountCreateProofRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostAccountCreateProofError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostAccountCreateProofResponse = + match host.create_account_proof(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::ACCOUNT_GET_LEGACY_ACCOUNTS, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetLegacyAccountsRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostGetLegacyAccountsError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetLegacyAccountsResponse = + match host.get_legacy_accounts(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::ACCOUNT_GET_USER_ID, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostGetUserIdRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostGetUserIdError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostGetUserIdResponse = + match host.get_user_id(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::ACCOUNT_REQUEST_LOGIN, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::account::HostRequestLoginRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::account::HostRequestLoginError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::account::HostRequestLoginResponse = + match host.request_login(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_chain

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chain + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::CHAIN_FOLLOW_HEAD_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadFollowRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.follow_head_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::chain::RemoteChainHeadFollowItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_GET_HEAD_HEADER, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadHeaderRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadHeaderError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadHeaderResponse = + match host.get_head_header(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_GET_HEAD_BODY, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadBodyRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadBodyError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadBodyResponse = + match host.get_head_body(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_GET_HEAD_STORAGE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStorageRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadStorageError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStorageResponse = + match host.get_head_storage(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_CALL_HEAD, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadCallRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadCallError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadCallResponse = + match host.call_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_UNPIN_HEAD, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadUnpinRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadUnpinError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadUnpinResponse = + match host.unpin_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_CONTINUE_HEAD, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadContinueRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainHeadContinueError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadContinueResponse = + match host.continue_head(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_STOP_HEAD_OPERATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainHeadStopOperationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainHeadStopOperationResponse = match host.stop_head_operation(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_GET_SPEC_GENESIS_HASH, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecGenesisHashRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecGenesisHashResponse = match host.get_spec_genesis_hash(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_GET_SPEC_CHAIN_NAME, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecChainNameRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainSpecChainNameError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecChainNameResponse = + match host.get_spec_chain_name(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAIN_GET_SPEC_PROPERTIES, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainSpecPropertiesRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chain::RemoteChainSpecPropertiesError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainSpecPropertiesResponse = + match host.get_spec_properties(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::CHAIN_BROADCAST_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionBroadcastRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionBroadcastResponse = match host.broadcast_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::CHAIN_STOP_TRANSACTION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chain::RemoteChainTransactionStopRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chain::RemoteChainTransactionStopResponse = match host.stop_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_chat

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Chat + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAT_CREATE_ROOM, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatCreateRoomRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chat::HostChatCreateRoomError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatCreateRoomResponse = + match host.create_room(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAT_REGISTER_BOT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatRegisterBotRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chat::HostChatRegisterBotError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatRegisterBotResponse = + match host.register_bot(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::CHAT_LIST_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.list_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::chat::HostChatListSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::CHAT_POST_MESSAGE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::HostChatPostMessageRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::chat::HostChatPostMessageError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::chat::HostChatPostMessageResponse = + match host.post_message(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::CHAT_ACTION_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.action_subscribe(&cx).await; + Ok(subscription_stream::< + versioned::chat::HostChatActionSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_subscription( + wire_table::CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::chat::ProductChatCustomMessageRenderSubscribeRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.custom_message_render_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::chat::ProductChatCustomMessageRenderSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} + +fn register_coin_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: CoinPayment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreatePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreatePurseResponse = match host.create_purse(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_QUERY_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentQueryPurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentQueryPurseResponse = match host.query_purse(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REBALANCE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRebalancePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.rebalance_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DELETE_PURSE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDeletePurseRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.delete_purse(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_RECEIVABLE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateReceivableRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreateReceivableResponse = match host.create_receivable(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::COIN_PAYMENT_CREATE_CHEQUE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentCreateChequeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::coin_payment::HostCoinPaymentCreateChequeResponse = match host.create_cheque(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_DEPOSIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentDepositRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.deposit(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::COIN_PAYMENT_REFUND, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentRefundRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.refund(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_subscription(wire_table::COIN_PAYMENT_LISTEN_FOR_PAYMENT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::coin_payment::HostCoinPaymentListenForRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.listen_for_payment(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } +} + +fn register_entropy

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Entropy + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request( + wire_table::ENTROPY_DERIVE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::entropy::HostDeriveEntropyRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::entropy::HostDeriveEntropyError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::entropy::HostDeriveEntropyResponse = + match host.derive(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_local_storage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: LocalStorage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::LOCAL_STORAGE_READ, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageReadRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageReadResponse = match host.read(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::LOCAL_STORAGE_WRITE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageWriteRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageWriteResponse = match host.write(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::LOCAL_STORAGE_CLEAR, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::local_storage::HostLocalStorageClearRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::local_storage::HostLocalStorageClearResponse = match host.clear(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_notifications

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Notifications + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::NOTIFICATIONS_SEND_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::notifications::HostPushNotificationResponse = match host.send_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::notifications::HostPushNotificationCancelRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::notifications::HostPushNotificationCancelResponse = match host.cancel_push_notification(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_payment

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Payment + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_BALANCE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentBalanceSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.balance_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::PAYMENT_REQUEST, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentResponse = + match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::PAYMENT_STATUS_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentStatusSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.status_subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host; + dispatcher.on_request( + wire_table::PAYMENT_TOP_UP, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::payment::HostPaymentTopUpRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::payment::HostPaymentTopUpError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::payment::HostPaymentTopUpResponse = + match host.top_up(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_permissions

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Permissions + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request(wire_table::PERMISSIONS_REQUEST_DEVICE_PERMISSION, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::HostDevicePermissionRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::HostDevicePermissionResponse = match host.request_device_permission(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request( + wire_table::PERMISSIONS_REQUEST_REMOTE_PERMISSION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::permissions::RemotePermissionRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::permissions::RemotePermissionError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::permissions::RemotePermissionResponse = + match host.request_remote_permission(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_preimage

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Preimage + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription( + wire_table::PREIMAGE_LOOKUP_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageLookupSubscribeRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(_) => return Err(Vec::new()), + }; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.lookup_subscribe(&cx, request).await; + Ok(subscription_stream::< + versioned::preimage::RemotePreimageLookupSubscribeItem, + _, + >(stream)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::PREIMAGE_SUBMIT, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::preimage::RemotePreimageSubmitRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::preimage::RemotePreimageSubmitError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::preimage::RemotePreimageSubmitResponse = + match host.submit(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_resource_allocation

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: ResourceAllocation + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_request(wire_table::RESOURCE_ALLOCATION_REQUEST, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::resource_allocation::HostRequestResourceAllocationRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::resource_allocation::HostRequestResourceAllocationResponse = match host.request(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } +} + +fn register_signing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Signing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SIGNING_CREATE_TRANSACTION, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::signing::HostCreateTransactionError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionResponse = + match host.create_transaction(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostCreateTransactionWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostCreateTransactionWithLegacyAccountResponse = match host.create_transaction_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawWithLegacyAccountResponse = match host.sign_raw_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadWithLegacyAccountRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadWithLegacyAccountResponse = match host.sign_payload_with_legacy_account(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SIGNING_SIGN_RAW, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignRawRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignRawResponse = + match host.sign_raw(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::SIGNING_SIGN_PAYLOAD, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::signing::HostSignPayloadRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::signing::HostSignPayloadError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::signing::HostSignPayloadResponse = + match host.sign_payload(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +fn register_statement_store

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: StatementStore + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_subscription(wire_table::STATEMENT_STORE_SUBSCRIBE, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubscribeRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Err(encode_versioned_interrupt_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let stream = match host.subscribe(&cx, request).await { + Ok(sub) => sub, + Err(err) => { + return Err(encode_versioned_interrupt_payload(err, target_version)); + } + }; + Ok(subscription_stream::(stream)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofResponse = match host.create_proof(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host.clone(); + dispatcher.on_request(wire_table::STATEMENT_STORE_CREATE_PROOF_AUTHORIZED, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::statement_store::RemoteStatementStoreCreateProofAuthorizedResponse = match host.create_proof_authorized(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }); + } + { + let host = host; + dispatcher.on_request(wire_table::STATEMENT_STORE_SUBMIT, move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::statement_store::RemoteStatementStoreSubmitRequest = match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError = + truapi::CallError::MalformedFrame { reason: err.to_string() }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + match host.submit(&cx, request).await { + Ok(()) => Ok(encode_versioned_unit_ok_payload(target_version)), + Err(err) => { + Ok(encode_versioned_err_payload(err, target_version)) + } + } + }) + }); + } +} + +fn register_system

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: System + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SYSTEM_HANDSHAKE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostHandshakeRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::system::HostHandshakeError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostHandshakeResponse = + match host.handshake(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host.clone(); + dispatcher.on_request( + wire_table::SYSTEM_FEATURE_SUPPORTED, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostFeatureSupportedRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::system::HostFeatureSupportedError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostFeatureSupportedResponse = + match host.feature_supported(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::SYSTEM_NAVIGATE_TO, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::system::HostNavigateToRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::system::HostNavigateToError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::system::HostNavigateToResponse = + match host.navigate_to(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } +} + +#[cfg(debug_assertions)] +fn register_testing

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Testing + Send + Sync + 'static, +{ + { + let host = host.clone(); + dispatcher.on_request( + wire_table::TESTING_VERSION_PROBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: versioned::testing::TestingVersionProbeRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + versioned::testing::TestingVersionProbeError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_versioned_err_payload( + error, + ::LATEST, + )); + } + }; + let target_version = request.version(); + let cx = CallContext::with_request_id(request_id.clone()); + let response: versioned::testing::TestingVersionProbeResponse = + match host.version_probe(&cx, request).await { + Ok(value) => value, + Err(err) => { + return Ok(encode_versioned_err_payload(err, target_version)); + } + }; + Ok(encode_versioned_ok_payload(response)) + }) + }, + ); + } + { + let host = host; + dispatcher.on_request( + wire_table::TESTING_ECHO_ERROR, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let request: truapi::v01::EchoErrorRequest = + match Decode::decode(&mut &bytes[..]) { + Ok(request) => request, + Err(err) => { + let error: truapi::CallError< + truapi::v01::TestingVersionProbeError, + > = truapi::CallError::MalformedFrame { + reason: err.to_string(), + }; + return Ok(encode_raw_err_payload(error)); + } + }; + let cx = CallContext::with_request_id(request_id.clone()); + match host.echo_error(&cx, request).await { + Ok(()) => Ok(encode_raw_unit_ok_payload()), + Err(err) => Ok(encode_raw_err_payload(err)), + } + }) + }, + ); + } +} + +fn register_theme

(dispatcher: &mut Dispatcher, host: Arc

) +where + P: Theme + Send + Sync + 'static, +{ + { + let host = host; + dispatcher.on_subscription( + wire_table::THEME_SUBSCRIBE, + move |request_id: String, bytes: Vec| { + let host = host.clone(); + Box::pin(async move { + let _ = bytes; + let cx = CallContext::with_request_id(request_id.clone()); + let stream = host.subscribe(&cx).await; + Ok(subscription_stream::< + versioned::theme::HostThemeSubscribeItem, + _, + >(stream)) + }) + }, + ); + } +} diff --git a/rust/crates/truapi-server/src/generated/mod.rs b/rust/crates/truapi-server/src/generated/mod.rs new file mode 100644 index 00000000..770a015d --- /dev/null +++ b/rust/crates/truapi-server/src/generated/mod.rs @@ -0,0 +1,4 @@ +//! Generated by truapi-codegen. Do not edit. + +pub mod dispatcher; +pub mod wire_table; diff --git a/rust/crates/truapi-server/src/generated/wire_table.rs b/rust/crates/truapi-server/src/generated/wire_table.rs new file mode 100644 index 00000000..1d529c9f --- /dev/null +++ b/rust/crates/truapi-server/src/generated/wire_table.rs @@ -0,0 +1,746 @@ +//! Wire-protocol discriminant table. +//! +//! Auto-generated by truapi-codegen. Do not edit. +//! +//! Each method reserves either two ids (request/response) or four +//! (start/stop/interrupt/receive). The ids for each method are exposed +//! as a named const (`PREIMAGE_SUBMIT`, ...); [`WIRE_TABLE`] and the +//! generated dispatcher both reference those consts so the numbers live +//! in exactly one place. The table is sorted by request/start id. + +/// Request method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestFrameIds { + /// Discriminant for the request frame. + pub request_id: u8, + /// Discriminant for the response frame. + pub response_id: u8, +} + +/// Subscription method wire discriminants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SubscriptionFrameIds { + /// Discriminant for the start frame. + pub start_id: u8, + /// Discriminant for the stop frame. + pub stop_id: u8, + /// Discriminant for the interrupt frame (server-initiated termination). + pub interrupt_id: u8, + /// Discriminant for each receive frame (a streamed item). + pub receive_id: u8, +} + +/// A single wire-table row. +pub struct WireEntry { + /// Method name from the Rust trait. + pub method: &'static str, + /// What kind of slot this entry describes. + pub kind: WireKind, +} + +/// Wire-slot shape: request/response pair or subscription quartet. +pub enum WireKind { + /// Request/response method. + Request(RequestFrameIds), + /// Subscription method. + Subscription(SubscriptionFrameIds), +} + +/// Wire discriminants for `system_handshake`. +pub const SYSTEM_HANDSHAKE: RequestFrameIds = RequestFrameIds { + request_id: 0, + response_id: 1, +}; + +/// Wire discriminants for `system_feature_supported`. +pub const SYSTEM_FEATURE_SUPPORTED: RequestFrameIds = RequestFrameIds { + request_id: 2, + response_id: 3, +}; + +/// Wire discriminants for `notifications_send_push_notification`. +pub const NOTIFICATIONS_SEND_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 4, + response_id: 5, +}; + +/// Wire discriminants for `system_navigate_to`. +pub const SYSTEM_NAVIGATE_TO: RequestFrameIds = RequestFrameIds { + request_id: 6, + response_id: 7, +}; + +/// Wire discriminants for `permissions_request_device_permission`. +pub const PERMISSIONS_REQUEST_DEVICE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 8, + response_id: 9, +}; + +/// Wire discriminants for `permissions_request_remote_permission`. +pub const PERMISSIONS_REQUEST_REMOTE_PERMISSION: RequestFrameIds = RequestFrameIds { + request_id: 10, + response_id: 11, +}; + +/// Wire discriminants for `local_storage_read`. +pub const LOCAL_STORAGE_READ: RequestFrameIds = RequestFrameIds { + request_id: 12, + response_id: 13, +}; + +/// Wire discriminants for `local_storage_write`. +pub const LOCAL_STORAGE_WRITE: RequestFrameIds = RequestFrameIds { + request_id: 14, + response_id: 15, +}; + +/// Wire discriminants for `local_storage_clear`. +pub const LOCAL_STORAGE_CLEAR: RequestFrameIds = RequestFrameIds { + request_id: 16, + response_id: 17, +}; + +/// Wire discriminants for `account_connection_status_subscribe`. +pub const ACCOUNT_CONNECTION_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 18, + stop_id: 19, + interrupt_id: 20, + receive_id: 21, +}; + +/// Wire discriminants for `account_get_account`. +pub const ACCOUNT_GET_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 22, + response_id: 23, +}; + +/// Wire discriminants for `account_get_account_alias`. +pub const ACCOUNT_GET_ACCOUNT_ALIAS: RequestFrameIds = RequestFrameIds { + request_id: 24, + response_id: 25, +}; + +/// Wire discriminants for `account_create_account_proof`. +pub const ACCOUNT_CREATE_ACCOUNT_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 26, + response_id: 27, +}; + +/// Wire discriminants for `account_get_legacy_accounts`. +pub const ACCOUNT_GET_LEGACY_ACCOUNTS: RequestFrameIds = RequestFrameIds { + request_id: 28, + response_id: 29, +}; + +/// Wire discriminants for `signing_create_transaction`. +pub const SIGNING_CREATE_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 30, + response_id: 31, +}; + +/// Wire discriminants for `signing_create_transaction_with_legacy_account`. +pub const SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 32, + response_id: 33, +}; + +/// Wire discriminants for `signing_sign_raw_with_legacy_account`. +pub const SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 34, + response_id: 35, +}; + +/// Wire discriminants for `signing_sign_payload_with_legacy_account`. +pub const SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT: RequestFrameIds = RequestFrameIds { + request_id: 36, + response_id: 37, +}; + +/// Wire discriminants for `chat_create_room`. +pub const CHAT_CREATE_ROOM: RequestFrameIds = RequestFrameIds { + request_id: 38, + response_id: 39, +}; + +/// Wire discriminants for `chat_register_bot`. +pub const CHAT_REGISTER_BOT: RequestFrameIds = RequestFrameIds { + request_id: 40, + response_id: 41, +}; + +/// Wire discriminants for `chat_list_subscribe`. +pub const CHAT_LIST_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 42, + stop_id: 43, + interrupt_id: 44, + receive_id: 45, +}; + +/// Wire discriminants for `chat_post_message`. +pub const CHAT_POST_MESSAGE: RequestFrameIds = RequestFrameIds { + request_id: 46, + response_id: 47, +}; + +/// Wire discriminants for `chat_action_subscribe`. +pub const CHAT_ACTION_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 48, + stop_id: 49, + interrupt_id: 50, + receive_id: 51, +}; + +/// Wire discriminants for `chat_custom_message_render_subscribe`. +pub const CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 52, + stop_id: 53, + interrupt_id: 54, + receive_id: 55, +}; + +/// Wire discriminants for `statement_store_subscribe`. +pub const STATEMENT_STORE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 56, + stop_id: 57, + interrupt_id: 58, + receive_id: 59, +}; + +/// Wire discriminants for `statement_store_create_proof`. +pub const STATEMENT_STORE_CREATE_PROOF: RequestFrameIds = RequestFrameIds { + request_id: 60, + response_id: 61, +}; + +/// Wire discriminants for `statement_store_submit`. +pub const STATEMENT_STORE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 62, + response_id: 63, +}; + +/// Wire discriminants for `preimage_lookup_subscribe`. +pub const PREIMAGE_LOOKUP_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 64, + stop_id: 65, + interrupt_id: 66, + receive_id: 67, +}; + +/// Wire discriminants for `preimage_submit`. +pub const PREIMAGE_SUBMIT: RequestFrameIds = RequestFrameIds { + request_id: 68, + response_id: 69, +}; + +/// Wire discriminants for `chain_follow_head_subscribe`. +pub const CHAIN_FOLLOW_HEAD_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 76, + stop_id: 77, + interrupt_id: 78, + receive_id: 79, +}; + +/// Wire discriminants for `chain_get_head_header`. +pub const CHAIN_GET_HEAD_HEADER: RequestFrameIds = RequestFrameIds { + request_id: 80, + response_id: 81, +}; + +/// Wire discriminants for `chain_get_head_body`. +pub const CHAIN_GET_HEAD_BODY: RequestFrameIds = RequestFrameIds { + request_id: 82, + response_id: 83, +}; + +/// Wire discriminants for `chain_get_head_storage`. +pub const CHAIN_GET_HEAD_STORAGE: RequestFrameIds = RequestFrameIds { + request_id: 84, + response_id: 85, +}; + +/// Wire discriminants for `chain_call_head`. +pub const CHAIN_CALL_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 86, + response_id: 87, +}; + +/// Wire discriminants for `chain_unpin_head`. +pub const CHAIN_UNPIN_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 88, + response_id: 89, +}; + +/// Wire discriminants for `chain_continue_head`. +pub const CHAIN_CONTINUE_HEAD: RequestFrameIds = RequestFrameIds { + request_id: 90, + response_id: 91, +}; + +/// Wire discriminants for `chain_stop_head_operation`. +pub const CHAIN_STOP_HEAD_OPERATION: RequestFrameIds = RequestFrameIds { + request_id: 92, + response_id: 93, +}; + +/// Wire discriminants for `chain_get_spec_genesis_hash`. +pub const CHAIN_GET_SPEC_GENESIS_HASH: RequestFrameIds = RequestFrameIds { + request_id: 94, + response_id: 95, +}; + +/// Wire discriminants for `chain_get_spec_chain_name`. +pub const CHAIN_GET_SPEC_CHAIN_NAME: RequestFrameIds = RequestFrameIds { + request_id: 96, + response_id: 97, +}; + +/// Wire discriminants for `chain_get_spec_properties`. +pub const CHAIN_GET_SPEC_PROPERTIES: RequestFrameIds = RequestFrameIds { + request_id: 98, + response_id: 99, +}; + +/// Wire discriminants for `chain_broadcast_transaction`. +pub const CHAIN_BROADCAST_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 100, + response_id: 101, +}; + +/// Wire discriminants for `chain_stop_transaction`. +pub const CHAIN_STOP_TRANSACTION: RequestFrameIds = RequestFrameIds { + request_id: 102, + response_id: 103, +}; + +/// Wire discriminants for `theme_subscribe`. +pub const THEME_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 104, + stop_id: 105, + interrupt_id: 106, + receive_id: 107, +}; + +/// Wire discriminants for `entropy_derive`. +pub const ENTROPY_DERIVE: RequestFrameIds = RequestFrameIds { + request_id: 108, + response_id: 109, +}; + +/// Wire discriminants for `account_get_user_id`. +pub const ACCOUNT_GET_USER_ID: RequestFrameIds = RequestFrameIds { + request_id: 110, + response_id: 111, +}; + +/// Wire discriminants for `account_request_login`. +pub const ACCOUNT_REQUEST_LOGIN: RequestFrameIds = RequestFrameIds { + request_id: 112, + response_id: 113, +}; + +/// Wire discriminants for `signing_sign_raw`. +pub const SIGNING_SIGN_RAW: RequestFrameIds = RequestFrameIds { + request_id: 114, + response_id: 115, +}; + +/// Wire discriminants for `signing_sign_payload`. +pub const SIGNING_SIGN_PAYLOAD: RequestFrameIds = RequestFrameIds { + request_id: 116, + response_id: 117, +}; + +/// Wire discriminants for `payment_balance_subscribe`. +pub const PAYMENT_BALANCE_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 118, + stop_id: 119, + interrupt_id: 120, + receive_id: 121, +}; + +/// Wire discriminants for `payment_top_up`. +pub const PAYMENT_TOP_UP: RequestFrameIds = RequestFrameIds { + request_id: 122, + response_id: 123, +}; + +/// Wire discriminants for `payment_request`. +pub const PAYMENT_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 124, + response_id: 125, +}; + +/// Wire discriminants for `payment_status_subscribe`. +pub const PAYMENT_STATUS_SUBSCRIBE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 126, + stop_id: 127, + interrupt_id: 128, + receive_id: 129, +}; + +/// Wire discriminants for `resource_allocation_request`. +pub const RESOURCE_ALLOCATION_REQUEST: RequestFrameIds = RequestFrameIds { + request_id: 130, + response_id: 131, +}; + +/// Wire discriminants for `statement_store_create_proof_authorized`. +pub const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED: RequestFrameIds = RequestFrameIds { + request_id: 132, + response_id: 133, +}; + +/// Wire discriminants for `notifications_cancel_push_notification`. +pub const NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION: RequestFrameIds = RequestFrameIds { + request_id: 134, + response_id: 135, +}; + +/// Wire discriminants for `coin_payment_create_purse`. +pub const COIN_PAYMENT_CREATE_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 136, + response_id: 137, +}; + +/// Wire discriminants for `coin_payment_query_purse`. +pub const COIN_PAYMENT_QUERY_PURSE: RequestFrameIds = RequestFrameIds { + request_id: 138, + response_id: 139, +}; + +/// Wire discriminants for `coin_payment_rebalance_purse`. +pub const COIN_PAYMENT_REBALANCE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 140, + stop_id: 141, + interrupt_id: 142, + receive_id: 143, +}; + +/// Wire discriminants for `coin_payment_delete_purse`. +pub const COIN_PAYMENT_DELETE_PURSE: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 144, + stop_id: 145, + interrupt_id: 146, + receive_id: 147, +}; + +/// Wire discriminants for `coin_payment_create_receivable`. +pub const COIN_PAYMENT_CREATE_RECEIVABLE: RequestFrameIds = RequestFrameIds { + request_id: 148, + response_id: 149, +}; + +/// Wire discriminants for `coin_payment_create_cheque`. +pub const COIN_PAYMENT_CREATE_CHEQUE: RequestFrameIds = RequestFrameIds { + request_id: 150, + response_id: 151, +}; + +/// Wire discriminants for `coin_payment_deposit`. +pub const COIN_PAYMENT_DEPOSIT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 152, + stop_id: 153, + interrupt_id: 154, + receive_id: 155, +}; + +/// Wire discriminants for `coin_payment_refund`. +pub const COIN_PAYMENT_REFUND: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 156, + stop_id: 157, + interrupt_id: 158, + receive_id: 159, +}; + +/// Wire discriminants for `coin_payment_listen_for_payment`. +pub const COIN_PAYMENT_LISTEN_FOR_PAYMENT: SubscriptionFrameIds = SubscriptionFrameIds { + start_id: 160, + stop_id: 161, + interrupt_id: 162, + receive_id: 163, +}; + +#[cfg(debug_assertions)] +/// Wire discriminants for `testing_version_probe`. +pub const TESTING_VERSION_PROBE: RequestFrameIds = RequestFrameIds { + request_id: 164, + response_id: 165, +}; + +#[cfg(debug_assertions)] +/// Wire discriminants for `testing_echo_error`. +pub const TESTING_ECHO_ERROR: RequestFrameIds = RequestFrameIds { + request_id: 166, + response_id: 167, +}; + +/// The full wire table. Ordering is part of the wire protocol; +/// only ever append. Removed methods leave their slot empty. +pub const WIRE_TABLE: &[WireEntry] = &[ + WireEntry { + method: "system_handshake", + kind: WireKind::Request(SYSTEM_HANDSHAKE), + }, + WireEntry { + method: "system_feature_supported", + kind: WireKind::Request(SYSTEM_FEATURE_SUPPORTED), + }, + WireEntry { + method: "notifications_send_push_notification", + kind: WireKind::Request(NOTIFICATIONS_SEND_PUSH_NOTIFICATION), + }, + WireEntry { + method: "system_navigate_to", + kind: WireKind::Request(SYSTEM_NAVIGATE_TO), + }, + WireEntry { + method: "permissions_request_device_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_DEVICE_PERMISSION), + }, + WireEntry { + method: "permissions_request_remote_permission", + kind: WireKind::Request(PERMISSIONS_REQUEST_REMOTE_PERMISSION), + }, + WireEntry { + method: "local_storage_read", + kind: WireKind::Request(LOCAL_STORAGE_READ), + }, + WireEntry { + method: "local_storage_write", + kind: WireKind::Request(LOCAL_STORAGE_WRITE), + }, + WireEntry { + method: "local_storage_clear", + kind: WireKind::Request(LOCAL_STORAGE_CLEAR), + }, + WireEntry { + method: "account_connection_status_subscribe", + kind: WireKind::Subscription(ACCOUNT_CONNECTION_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "account_get_account", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT), + }, + WireEntry { + method: "account_get_account_alias", + kind: WireKind::Request(ACCOUNT_GET_ACCOUNT_ALIAS), + }, + WireEntry { + method: "account_create_account_proof", + kind: WireKind::Request(ACCOUNT_CREATE_ACCOUNT_PROOF), + }, + WireEntry { + method: "account_get_legacy_accounts", + kind: WireKind::Request(ACCOUNT_GET_LEGACY_ACCOUNTS), + }, + WireEntry { + method: "signing_create_transaction", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION), + }, + WireEntry { + method: "signing_create_transaction_with_legacy_account", + kind: WireKind::Request(SIGNING_CREATE_TRANSACTION_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_raw_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_RAW_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "signing_sign_payload_with_legacy_account", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD_WITH_LEGACY_ACCOUNT), + }, + WireEntry { + method: "chat_create_room", + kind: WireKind::Request(CHAT_CREATE_ROOM), + }, + WireEntry { + method: "chat_register_bot", + kind: WireKind::Request(CHAT_REGISTER_BOT), + }, + WireEntry { + method: "chat_list_subscribe", + kind: WireKind::Subscription(CHAT_LIST_SUBSCRIBE), + }, + WireEntry { + method: "chat_post_message", + kind: WireKind::Request(CHAT_POST_MESSAGE), + }, + WireEntry { + method: "chat_action_subscribe", + kind: WireKind::Subscription(CHAT_ACTION_SUBSCRIBE), + }, + WireEntry { + method: "chat_custom_message_render_subscribe", + kind: WireKind::Subscription(CHAT_CUSTOM_MESSAGE_RENDER_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_subscribe", + kind: WireKind::Subscription(STATEMENT_STORE_SUBSCRIBE), + }, + WireEntry { + method: "statement_store_create_proof", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF), + }, + WireEntry { + method: "statement_store_submit", + kind: WireKind::Request(STATEMENT_STORE_SUBMIT), + }, + WireEntry { + method: "preimage_lookup_subscribe", + kind: WireKind::Subscription(PREIMAGE_LOOKUP_SUBSCRIBE), + }, + WireEntry { + method: "preimage_submit", + kind: WireKind::Request(PREIMAGE_SUBMIT), + }, + WireEntry { + method: "chain_follow_head_subscribe", + kind: WireKind::Subscription(CHAIN_FOLLOW_HEAD_SUBSCRIBE), + }, + WireEntry { + method: "chain_get_head_header", + kind: WireKind::Request(CHAIN_GET_HEAD_HEADER), + }, + WireEntry { + method: "chain_get_head_body", + kind: WireKind::Request(CHAIN_GET_HEAD_BODY), + }, + WireEntry { + method: "chain_get_head_storage", + kind: WireKind::Request(CHAIN_GET_HEAD_STORAGE), + }, + WireEntry { + method: "chain_call_head", + kind: WireKind::Request(CHAIN_CALL_HEAD), + }, + WireEntry { + method: "chain_unpin_head", + kind: WireKind::Request(CHAIN_UNPIN_HEAD), + }, + WireEntry { + method: "chain_continue_head", + kind: WireKind::Request(CHAIN_CONTINUE_HEAD), + }, + WireEntry { + method: "chain_stop_head_operation", + kind: WireKind::Request(CHAIN_STOP_HEAD_OPERATION), + }, + WireEntry { + method: "chain_get_spec_genesis_hash", + kind: WireKind::Request(CHAIN_GET_SPEC_GENESIS_HASH), + }, + WireEntry { + method: "chain_get_spec_chain_name", + kind: WireKind::Request(CHAIN_GET_SPEC_CHAIN_NAME), + }, + WireEntry { + method: "chain_get_spec_properties", + kind: WireKind::Request(CHAIN_GET_SPEC_PROPERTIES), + }, + WireEntry { + method: "chain_broadcast_transaction", + kind: WireKind::Request(CHAIN_BROADCAST_TRANSACTION), + }, + WireEntry { + method: "chain_stop_transaction", + kind: WireKind::Request(CHAIN_STOP_TRANSACTION), + }, + WireEntry { + method: "theme_subscribe", + kind: WireKind::Subscription(THEME_SUBSCRIBE), + }, + WireEntry { + method: "entropy_derive", + kind: WireKind::Request(ENTROPY_DERIVE), + }, + WireEntry { + method: "account_get_user_id", + kind: WireKind::Request(ACCOUNT_GET_USER_ID), + }, + WireEntry { + method: "account_request_login", + kind: WireKind::Request(ACCOUNT_REQUEST_LOGIN), + }, + WireEntry { + method: "signing_sign_raw", + kind: WireKind::Request(SIGNING_SIGN_RAW), + }, + WireEntry { + method: "signing_sign_payload", + kind: WireKind::Request(SIGNING_SIGN_PAYLOAD), + }, + WireEntry { + method: "payment_balance_subscribe", + kind: WireKind::Subscription(PAYMENT_BALANCE_SUBSCRIBE), + }, + WireEntry { + method: "payment_top_up", + kind: WireKind::Request(PAYMENT_TOP_UP), + }, + WireEntry { + method: "payment_request", + kind: WireKind::Request(PAYMENT_REQUEST), + }, + WireEntry { + method: "payment_status_subscribe", + kind: WireKind::Subscription(PAYMENT_STATUS_SUBSCRIBE), + }, + WireEntry { + method: "resource_allocation_request", + kind: WireKind::Request(RESOURCE_ALLOCATION_REQUEST), + }, + WireEntry { + method: "statement_store_create_proof_authorized", + kind: WireKind::Request(STATEMENT_STORE_CREATE_PROOF_AUTHORIZED), + }, + WireEntry { + method: "notifications_cancel_push_notification", + kind: WireKind::Request(NOTIFICATIONS_CANCEL_PUSH_NOTIFICATION), + }, + WireEntry { + method: "coin_payment_create_purse", + kind: WireKind::Request(COIN_PAYMENT_CREATE_PURSE), + }, + WireEntry { + method: "coin_payment_query_purse", + kind: WireKind::Request(COIN_PAYMENT_QUERY_PURSE), + }, + WireEntry { + method: "coin_payment_rebalance_purse", + kind: WireKind::Subscription(COIN_PAYMENT_REBALANCE_PURSE), + }, + WireEntry { + method: "coin_payment_delete_purse", + kind: WireKind::Subscription(COIN_PAYMENT_DELETE_PURSE), + }, + WireEntry { + method: "coin_payment_create_receivable", + kind: WireKind::Request(COIN_PAYMENT_CREATE_RECEIVABLE), + }, + WireEntry { + method: "coin_payment_create_cheque", + kind: WireKind::Request(COIN_PAYMENT_CREATE_CHEQUE), + }, + WireEntry { + method: "coin_payment_deposit", + kind: WireKind::Subscription(COIN_PAYMENT_DEPOSIT), + }, + WireEntry { + method: "coin_payment_refund", + kind: WireKind::Subscription(COIN_PAYMENT_REFUND), + }, + WireEntry { + method: "coin_payment_listen_for_payment", + kind: WireKind::Subscription(COIN_PAYMENT_LISTEN_FOR_PAYMENT), + }, + #[cfg(debug_assertions)] + WireEntry { + method: "testing_version_probe", + kind: WireKind::Request(TESTING_VERSION_PROBE), + }, + #[cfg(debug_assertions)] + WireEntry { + method: "testing_echo_error", + kind: WireKind::Request(TESTING_ECHO_ERROR), + }, +]; diff --git a/scripts/codegen.sh b/scripts/codegen.sh index 98d7006b..86c17f78 100755 --- a/scripts/codegen.sh +++ b/scripts/codegen.sh @@ -7,6 +7,10 @@ # --output js/packages/truapi/src/generated # --playground-output js/packages/truapi/src/playground # --client-examples-output playground/test/generated/examples +# --rust-output rust/crates/truapi-server/src/generated +# --platform-input target/doc/truapi_platform.json +# --platform-ts-output js/packages/truapi-host-wasm/src/generated +# --platform-wasm-adapter-output js/packages/truapi-host-wasm/src/generated # --codec-version 1 # # The client surface defaults to the latest wire version any versioned @@ -20,11 +24,16 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" cargo +nightly rustdoc -p truapi -- -Z unstable-options --output-format json +cargo +nightly rustdoc -p truapi-platform -- -Z unstable-options --output-format json cargo run -p truapi-codegen -- \ --input target/doc/truapi.json \ --output js/packages/truapi/src/generated \ --playground-output js/packages/truapi/src/playground \ --client-examples-output playground/test/generated/examples \ + --rust-output rust/crates/truapi-server/src/generated \ + --platform-input target/doc/truapi_platform.json \ + --platform-ts-output js/packages/truapi-host-wasm/src/generated \ + --platform-wasm-adapter-output js/packages/truapi-host-wasm/src/generated \ --explorer-output js/packages/truapi/src/explorer \ --codec-version 1 @@ -34,7 +43,8 @@ npm exec --yes -- prettier --write \ "js/packages/truapi/src/generated/**/*.ts" \ "js/packages/truapi/src/playground/**/*.ts" \ "js/packages/truapi/src/explorer/**/*.ts" \ - "playground/test/generated/examples/**/*.ts" + "playground/test/generated/examples/**/*.ts" \ + "js/packages/truapi-host-wasm/src/generated/**/*.ts" # Rebuild dist/ so downstream consumers (in particular the playground, # which picks up @parity/truapi via yarn 1.x file: snapshot) see the @@ -58,3 +68,5 @@ fi echo "Generated client at js/packages/truapi/src/generated/" echo "Generated playground metadata at js/packages/truapi/src/playground/codegen/" echo "Generated client examples at playground/test/generated/examples/" +echo "Generated Rust dispatcher at rust/crates/truapi-server/src/generated/" +echo "Generated host-callbacks WASM adapter at js/packages/truapi-host-wasm/src/generated/"