Skip to content

Add codegen support for rpc_service declarations#362

Open
FredrikNoren wants to merge 4 commits into
planus-org:mainfrom
FredrikNoren:add-rpc-service-codegen
Open

Add codegen support for rpc_service declarations#362
FredrikNoren wants to merge 4 commits into
planus-org:mainfrom
FredrikNoren:add-rpc-service-codegen

Conversation

@FredrikNoren

@FredrikNoren FredrikNoren commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Closes #56

What

rpc_service declarations are now supported instead of being rejected with "Rpc services are not currently supported". Each service generates a module-level name constant, transport-agnostic Rust traits — a sync one and an async one — plus a method enum with numeric ids for routing:

/// The fully-qualified name of the `Greeter` service in the flatbuffers schema.
pub const GREETER_NAME: &str = "example.Greeter";

/// A simple greeting service.
pub trait Greeter {
    /// Also available without an implementing type as [`GREETER_NAME`].
    const NAME: &'static str = GREETER_NAME;

    /// The error type returned by the methods of this service.
    type Error;

    fn say_hello(&self, request: HelloRequestRef<'_>)
        -> Result<HelloReply, Self::Error>;
}

/// A simple greeting service.
#[allow(async_fn_in_trait)]
pub trait GreeterAsync {
    const NAME: &'static str = GREETER_NAME;
    type Error;

    async fn say_hello(&self, request: HelloRequestRef<'_>)
        -> Result<HelloReply, Self::Error>;
}

/// The methods of the rpc service `example.Greeter`.
#[repr(u32)]
pub enum GreeterMethod {
    SayHello = 0,
}

impl GreeterMethod {
    pub const SERVICE_NAME: &'static str = GREETER_NAME;
    pub const ENUM_VALUES: [Self; 1];
    pub fn id(self) -> u32;           // zero-based declaration index
    pub fn name(self) -> &'static str; // "SayHello"
}
// + TryFrom<u32> (UnknownEnumTagKind on bad ids) and From<GreeterMethod> for u32

Requests are received as the zero-copy Ref types and responses are returned as the owned types, so the traits work as server-side handler interfaces over any transport. Schema docstrings are carried through, and idempotent methods get a doc note. The async trait uses native async-fn-in-trait (stable since 1.75, below the 1.88 MSRV), so there are no new dependencies; the #[allow(async_fn_in_trait)] leaves the Send-bound decision to implementors. The method enum gives transports a stable numeric handle for dispatching/relaying messages without hardcoding ids; ids follow declaration order (documented on the type), mirroring the conventions of the schema-enum codegen (ENUM_VALUES, TryFrom, serde derives). The free name constant exists because an associated const on a trait cannot be read without an implementing type — clients and relays that never implement the handler traits would otherwise need a dummy probe impl just to learn the service name.

Since flatc has no Rust RPC output to stay compatible with, this deliberately starts with the smallest useful surface (no generated client/server stubs, no dependency on any specific RPC framework). Happy to adjust the design based on feedback — that's also why streaming is rejected rather than given a made-up signature.

Changes by layer

  • planus-types / planus-translation: translate_rpc_service now actually translates methods (it previously returned an empty map), intermediate::RpcMethod gains idempotent + docstrings. Argument/return types are validated to be tables (same rule as flatc). streaming: "none" and idempotent are accepted; streaming: "client"/"server"/"bidi" get a dedicated "not currently supported" error.
  • planus-codegen: added generate_rpc_service/generate_rpc_method to the Backend trait and replaced the three todo!()s in backend_translation.rs, so services flow through the same pipeline as the other declarations.
  • Rust backend: generates the constant, traits and method enum above, using the existing name-reservation machinery.
  • Dot backend: services render as nodes with one row per method and edges to argument/return tables.
  • README/CHANGELOG: rpc_service removed from the unsupported list (with a note that streaming methods are still unsupported).

Tests

  • test/rust-test-20xx/api_files/rpc_service.{fbs,rs}: implements both generated traits and does a full serialize → read-as-ref → handle → assert round-trip; the async future is driven via Waker::noop() so no executor dependency is needed; the name constants and the method enum's ids, names, ENUM_VALUES and TryFrom conversions are asserted.
  • test/files/valid/rpc_service.fbs: accepted-schema fixture (namespaces, docstrings, idempotent, streaming: "none").
  • test/files/invalid/rpc_bad_types.{fbs,stderr} and rpc_streaming.{fbs,stderr}: golden stderr for non-table method types and for all streaming variants + an invalid streaming value.
  • Regenerated rpc_service_as_type.stderr and defined_twice.stderr, whose expected output changed.

🤖 Generated with Claude Code

FredrikNoren and others added 4 commits June 10, 2026 15:09
Each rpc_service now generates a transport-agnostic Rust trait with one
method per (unary) rpc method, taking the zero-copy Ref type as request
and returning the owned type. Streaming methods and non-table
argument/return types are rejected with proper errors instead of the
previous blanket "not supported" error. The dot backend renders services
as nodes with edges to their argument/return tables.

Closes planus-org#56

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each service now additionally generates a GreeterAsync-style trait using
native async-fn-in-trait (MSRV 1.88 > 1.75), so the service can be
implemented in async code without any extra dependencies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each service now also generates a GreeterMethod-style enum where every
method gets a numeric id from its zero-based declaration index, plus
name()/id()/TryFrom<u32> conversions and an ENUM_VALUES list, so
transports can route and relay messages without hardcoding ids.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The service name was only reachable through the traits' associated NAME
const, which requires an implementing type — consumers that just relay
or route messages had to write a dummy probe impl to read it. Generate
a free `GREETER_NAME`-style constant as the canonical source, reference
it from the traits' NAME defaults, and expose it on the method enum as
SERVICE_NAME.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handling of rpc_service

1 participant