//! Swift phon server emitter: the `{Service}Handler` protocol (the user implements
//! it) + a `{Service}Dispatcher: ServiceDispatcher` that decodes args via the typed
//! path (reconciling the peer's writer schema), calls the handler, wraps the result
//! into `Result<T, VoxError<E>>`, encodes + advertises it, and replies. Replaces the
//! postcard `server.rs`.
use heck::{ToLowerCamelCase, ToUpperCamelCase};
use vox_types::{MethodDescriptor, ServiceDescriptor, ShapeKind, classify_shape, is_rx, is_tx};
use super::phon_service::{
element_auxiliary_decode_closure, element_encode_closure, method_global_prefix,
};
use super::types::{format_doc, swift_type_base, swift_type_server_arg, swift_type_server_return};
use crate::render::hex_u64;
fn has_channels(method: &MethodDescriptor) -> bool {
method.args.iter().any(|a| is_tx(a.shape) || is_rx(a.shape))
}
/// The server-side (handler) Swift type of an argument — channel args become
/// `Tx`/`Rx` over the `channel_element` (the arg `shape` is an opaque adapter).
fn server_arg_ty(a: &vox_types::ArgDescriptor) -> String {
if is_tx(a.shape) {
format!(
"Tx<{}>",
swift_type_base(a.channel_element.expect("tx element"))
)
} else if is_rx(a.shape) {
format!(
"Rx<{}>",
swift_type_base(a.channel_element.expect("rx element"))
)
} else {
swift_type_server_arg(a.shape)
}
}
/// `(ret_ty, response_wire_ty, user_error_ty?)`. `user_error_ty` is `Some` only for a
/// fallible method (`Result<T, E>` return) — its `E` is the handler's thrown error.
fn method_types(method: &MethodDescriptor) -> (String, String, Option<String>) {
let ret = swift_type_server_return(method.return_shape);
let resp = swift_type_base(method.response_wire_shape);
let user_err = match classify_shape(method.return_shape) {
ShapeKind::Result { err, .. } => Some(swift_type_base(err)),
_ => None,
};
(ret, resp, user_err)
}
pub fn generate_phon_server(service: &ServiceDescriptor) -> String {
let service_name = service.service_name.to_upper_camel_case();
let mut out = String::new();
// r[impl rpc.service]
// r[impl rpc.service.methods]
// Handler protocol (implemented by the user).
if let Some(doc) = &service.doc {
out.push_str(&format_doc(doc, ""));
}
out.push_str(&format!(
"public protocol {service_name}Handler: Sendable {{\n"
));
for method in service.methods {
if let Some(doc) = &method.doc {
out.push_str(&format_doc(doc, " "));
}
let name = method.method_name.to_lower_camel_case();
let args: Vec<String> = method
.args
.iter()
.map(|a| format!("{}: {}", a.name.to_lower_camel_case(), server_arg_ty(a)))
.collect();
let ret = swift_type_server_return(method.return_shape);
if ret == "Void" {
out.push_str(&format!(
" func {name}({}) async throws\n",
args.join(", ")
));
} else {
out.push_str(&format!(
" func {name}({}) async throws -> {ret}\n",
args.join(", ")
));
}
}
out.push_str("}\n\n");
// r[impl rpc.handler]
// Dispatcher.
out.push_str(&format!(
"public final class {service_name}Dispatcher: ServiceDispatcher {{\n"
));
out.push_str(&format!(" private let handler: {service_name}Handler\n"));
out.push_str(&format!(
" public init(handler: {service_name}Handler) {{ self.handler = handler }}\n\n"
));
// encodeVoxError — encode a runtime error through any method's response type (the
// non-User Err arms are independent of `T`/`E` on the wire, so the first method's
// response program suffices). A method-less service has no response program, so it
// returns empty bytes.
match service.methods.first() {
None => out.push_str(
" public func encodeVoxError(_ error: VoxRuntimeError) -> [UInt8] { [] }\n\n",
),
Some(m0) => {
let prefix0 = method_global_prefix(service.service_name, m0.method_name);
let resp0 = swift_type_base(m0.response_wire_shape);
let wire0 = match classify_shape(m0.response_wire_shape) {
ShapeKind::Result { err, .. } => swift_type_base(err),
_ => "VoxError<Infallible>".to_string(),
};
out.push_str(&format!(
" public func encodeVoxError(_ error: VoxRuntimeError) -> [UInt8] {{\n let wire: {wire0}\n switch error {{\n case .unknownMethod, .notImplemented: wire = .unknownMethod\n case .invalidPayload(let s), .decodeError(let s), .encodeError(let s): wire = .invalidPayload(s)\n case .cancelled: wire = .cancelled\n case .connectionClosed: wire = .connectionClosed\n case .timeout: wire = .timedOut\n case .indeterminate: wire = .indeterminate\n }}\n let r: {resp0} = .failure(wire)\n return encodeVoxTyped(r, {prefix0}_ResponseEncoder)\n }}\n\n"
));
}
}
// r[impl rpc.channel.discovery]
// preregister — mark the call's out-of-band channel ids known so incoming Data on
// them buffers (instead of being rejected as unknown) before dispatch binds them.
out.push_str(" public func preregister(methodId: UInt64, payload: [UInt8], channels: [UInt64], registry: ChannelRegistry) async {\n for id in channels { await registry.markKnown(id) }\n }\n\n");
// r[impl rpc.service.methods]
// r[impl rpc.unknown-method]
// dispatch — route to per-method helpers.
out.push_str(" public func dispatch(methodId: UInt64, payload: [UInt8], requestId: UInt64, channels: [UInt64], registry: ChannelRegistry, schemaSendTracker: SchemaSendTracker, schemaReceiveTracker: SchemaTracker, context: RequestContext, taskTx: @escaping @Sendable (TaskMessage) -> Void) async {\n switch methodId {\n");
for m in service.methods {
// r[impl rpc.method-id]
let id = hex_u64(crate::method_id(m));
let name = m.method_name.to_lower_camel_case();
let extra = if has_channels(m) {
"channels: channels, registry: registry, "
} else {
""
};
out.push_str(&format!(
" case {id}: await dispatch_{name}(payload: payload, requestId: requestId, {extra}schemaSendTracker: schemaSendTracker, schemaReceiveTracker: schemaReceiveTracker, taskTx: taskTx)\n"
));
}
out.push_str(" default: taskTx(.response(requestId: requestId, payload: encodeVoxError(.unknownMethod), methodId: methodId))\n }\n }\n\n");
// Per-method dispatch helpers.
for m in service.methods {
out.push_str(&generate_dispatch_method(service, m));
}
out.push_str("}\n\n");
out
}
fn generate_dispatch_method(service: &ServiceDescriptor, m: &MethodDescriptor) -> String {
let id = hex_u64(crate::method_id(m));
let name = m.method_name.to_lower_camel_case();
let prefix = method_global_prefix(service.service_name, m.method_name);
let svc = service.service_name.to_lower_camel_case();
let (ret_ty, resp_ty, user_err) = method_types(m);
let args_ty = swift_type_base(m.args_shape);
let arity = m.args.len();
let channels = has_channels(m);
let mut out = String::new();
let response_schema_closure = format!("{svc}Methods[{id}]!.responseSchemaClosure");
let mut server_tx_closes = Vec::new();
let extra_params = if channels {
"channels: [UInt64], registry: ChannelRegistry, "
} else {
""
};
out.push_str(&format!(
" private func dispatch_{name}(payload: [UInt8], requestId: UInt64, {extra_params}schemaSendTracker: SchemaSendTracker, schemaReceiveTracker: SchemaTracker, taskTx: @escaping @Sendable (TaskMessage) -> Void) async {{\n"
));
// Decode args (reconciling the peer's writer schema when advertised).
// r[impl schema.errors.call-level.callee]
if arity == 0 {
// No args to decode.
} else {
// Reconcile the caller's advertised (writer) args schema against this reader —
// the only decode path. No same-schema fallback: a missing writer schema is a
// protocol error (the caller advertises it on the first call).
out.push_str(&format!(
" guard let argsDecoder = schemaReceiveTracker.buildDecodeFn({id}, .args, readerDescriptor: {prefix}_ArgsDescriptor, readerBlocks: {prefix}_ArgsDescriptorBlocks, local: {svc}Registry) else {{\n taskTx(.response(requestId: requestId, payload: encodeVoxError(.invalidPayload(\"no args schema advertised\")), methodId: {id}, responseSchemaClosure: {response_schema_closure}))\n return\n }}\n"
));
out.push_str(&format!(
" let args: {args_ty}\n do {{ args = try decodeVoxTyped(argsDecoder, payload) }} catch {{\n taskTx(.response(requestId: requestId, payload: encodeVoxError(.invalidPayload(\"decode args\")), methodId: {id}, responseSchemaClosure: {response_schema_closure}))\n return\n }}\n"
));
}
// r[impl rpc.channel.discovery]
// Bind out-of-band channels: a channel arg in the decoded tuple is the u32 LE wire
// index into `channels`; resolve it to a `ChannelId` and create a server-side
// `Tx`/`Rx`. A `Tx` arg means the handler SENDS (callee→caller); an `Rx` arg means
// the handler RECEIVES (caller→callee). The bound handle replaces the arg slot.
for (i, a) in m.args.iter().enumerate() {
if !(is_tx(a.shape) || is_rx(a.shape)) {
continue;
}
let an = a.name.to_lower_camel_case();
let slot = if arity == 1 {
"args".to_string()
} else {
format!("args.{i}")
};
out.push_str(&format!(
" let {an}WireIndex = channelWireIndex({slot})\n"
));
out.push_str(&format!(
" guard {an}WireIndex < channels.count else {{\n taskTx(.response(requestId: requestId, payload: encodeVoxError(.invalidPayload(\"channel wire index out of range\")), methodId: {id}, responseSchemaClosure: {response_schema_closure}))\n return\n }}\n"
));
let elem_ty = swift_type_base(a.channel_element.expect("channel arg element shape"));
if is_tx(a.shape) {
// Handler SENDS → phon element ENCODE codec.
let ser = element_encode_closure(&elem_ty, &format!("{prefix}_{an}_ElementEncoder"));
out.push_str(" // r[impl schema.exchange.channels.tx-args]\n");
out.push_str(&format!(
" let {an} = await bindServerTx(channelId: channels[{an}WireIndex], registry: registry, taskTx: taskTx, methodId: {id}, argsSchemaClosure: {svc}Methods[{id}]!.argsSchemaClosure, schemaSendTracker: schemaSendTracker, serialize: {ser})\n"
));
server_tx_closes.push(format!(" {an}.close()\n"));
} else {
// Handler RECEIVES → phon element DECODE codec reconciled from the caller's
// advertised auxiliary element root.
let de = element_auxiliary_decode_closure(
&elem_ty,
"schemaReceiveTracker",
&id,
&format!("channel.arg.{i}.rx.element"),
&format!("{prefix}_{an}_ElementDescriptor"),
&format!("{prefix}_{an}_ElementDescriptorBlocks"),
&format!("{svc}Registry"),
);
out.push_str(" // r[impl schema.exchange.channels.rx-args]\n");
out.push_str(&format!(
" let {an} = await bindServerRx(channelId: channels[{an}WireIndex], registry: registry, taskTx: taskTx, deserialize: {de})\n"
));
}
}
// The handler call expression, with labels. Channel args use the bound `Tx`/`Rx`
// local just created; other args use the decoded value (`args` or `args.i`).
let call_args: Vec<String> = m
.args
.iter()
.enumerate()
.map(|(i, a)| {
let label = a.name.to_lower_camel_case();
let value = if is_tx(a.shape) || is_rx(a.shape) {
label.clone()
} else if arity == 1 {
"args".to_string()
} else {
format!("args.{i}")
};
format!("{label}: {value}")
})
.collect();
let call = format!("handler.{name}({})", call_args.join(", "));
// r[impl rpc.fallible]
// r[impl rpc.fallible.vox-error]
// Call + wrap into the wire `Result<T, VoxError<E>>`. A fallible handler returns
// `Result<T, E>` (its `.failure(e)` becomes the wire `User(e)`); an infallible one
// returns `T`/`Void`. An unexpected throw maps to `Indeterminate`.
// `voxResult`/`voxValue` are namespaced so they never collide with a handler arg
// name (e.g. a channel arg literally named `result`, as in `postReplySum`).
out.push_str(&format!(
" let voxResult: {resp_ty}\n do {{\n"
));
if user_err.is_some() {
out.push_str(&format!(" let voxValue = try await {call}\n"));
out.push_str(
" switch voxValue {\n case .success(let v): voxResult = .success(v)\n case .failure(let e): voxResult = .failure(.user(e))\n }\n",
);
} else if ret_ty == "Void" {
out.push_str(&format!(
" try await {call}\n voxResult = .success(())\n"
));
} else {
out.push_str(&format!(
" let voxValue = try await {call}\n voxResult = .success(voxValue)\n"
));
}
out.push_str(
" } catch {\n voxResult = .failure(.indeterminate)\n }\n",
);
if !server_tx_closes.is_empty() {
out.push_str(" // r[impl rpc.request.scope.channels]\n");
for close in server_tx_closes {
out.push_str(&close);
}
}
// r[impl rpc.response]
// Encode the response and reply, carrying the method's response schema closure.
// The driver advertises it idempotently at the sequential send point, so the first
// response written for this method carries the schema even under pipelining (the
// schema decision must NOT be made here in the concurrent dispatch task).
out.push_str(&format!(
" let respPayload = encodeVoxTyped(voxResult, {prefix}_ResponseEncoder)\n taskTx(.response(requestId: requestId, payload: respPayload, methodId: {id}, responseSchemaClosure: {response_schema_closure}))\n }}\n\n"
));
out
}