use heck::{ToLowerCamelCase, ToUpperCamelCase};
use vox_types::{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_client_arg, swift_type_client_return};
use crate::render::hex_u64;
fn client_arg_ty(a: &vox_types::ArgDescriptor) -> String {
if is_tx(a.shape) {
format!(
"UnboundTx<{}>",
swift_type_base(a.channel_element.expect("tx element"))
)
} else if is_rx(a.shape) {
format!(
"UnboundRx<{}>",
swift_type_base(a.channel_element.expect("rx element"))
)
} else {
swift_type_client_arg(a.shape)
}
}
fn method_signature(method: &vox_types::MethodDescriptor) -> (String, String, String) {
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(), client_arg_ty(a)))
.collect();
let ret = swift_type_client_return(method.return_shape);
(name, args.join(", "), ret)
}
pub fn generate_phon_client(service: &ServiceDescriptor) -> String {
let service_name = service.service_name.to_upper_camel_case();
let svc = service.service_name.to_lower_camel_case();
let mut out = String::new();
if let Some(doc) = &service.doc {
out.push_str(&format_doc(doc, ""));
}
out.push_str(&format!(
"public protocol {service_name}Caller: Sendable {{\n"
));
for method in service.methods {
if let Some(doc) = &method.doc {
out.push_str(&format_doc(doc, " "));
}
let (name, args, ret) = method_signature(method);
if ret == "Void" {
out.push_str(&format!(" func {name}({args}) async throws\n"));
} else {
out.push_str(&format!(" func {name}({args}) async throws -> {ret}\n"));
}
}
out.push_str("}\n\n");
out.push_str(&format!(
"public final class {service_name}Client: {service_name}Caller, Sendable {{\n"
));
out.push_str(" private let connection: VoxLane\n");
out.push_str(" private let timeout: TimeInterval?\n\n");
out.push_str(
" public init(connection: VoxLane, timeout: TimeInterval? = 30.0) {\n self.connection = connection\n self.timeout = timeout\n }\n\n",
);
for method in service.methods {
let (name, args, ret) = method_signature(method);
let method_id = hex_u64(crate::method_id(method));
let prefix = method_global_prefix(service.service_name, method.method_name);
let resp_ty = swift_type_base(method.response_wire_shape);
let has_channels = method.args.iter().any(|a| is_tx(a.shape) || is_rx(a.shape));
let sig = if ret == "Void" {
format!(" public func {name}({args}) async throws {{\n")
} else {
format!(" public func {name}({args}) async throws -> {ret} {{\n")
};
out.push_str(&sig);
let mut arg_exprs: Vec<String> = Vec::new();
let mut finalizers: Vec<String> = Vec::new();
if has_channels {
out.push_str(" var channelIds: [UInt64] = []\n");
}
for (i, a) in method.args.iter().enumerate() {
let an = a.name.to_lower_camel_case();
if is_rx(a.shape) {
let elem_ty = swift_type_base(a.channel_element.expect("rx element"));
let ser =
element_encode_closure(&elem_ty, &format!("{prefix}_{an}_ElementEncoder"));
out.push_str(&format!(" let {an}WireIndex = channelIds.count\n"));
out.push_str(&format!(
" channelIds.append(await connection.bindClientRxArg({an}, serialize: {ser}))\n"
));
arg_exprs.push(format!("Data(channelWireIndexBytes({an}WireIndex))"));
finalizers.push(format!("finalizeChannel({an})"));
} else if is_tx(a.shape) {
let elem_ty = swift_type_base(a.channel_element.expect("tx element"));
let de = element_auxiliary_decode_closure(
&elem_ty,
"self.connection.schemaReceiveTracker",
&method_id,
&format!("channel.arg.{i}.tx.element"),
&format!("{prefix}_{an}_ElementDescriptor"),
&format!("{prefix}_{an}_ElementDescriptorBlocks"),
&format!("{svc}Registry"),
);
out.push_str(&format!(" let {an}WireIndex = channelIds.count\n"));
out.push_str(" // r[impl schema.exchange.channels.tx-args]\n");
out.push_str(&format!(
" channelIds.append(await connection.bindClientTxArg({an}, deserialize: {de}))\n"
));
arg_exprs.push(format!("Data(channelWireIndexBytes({an}WireIndex))"));
finalizers.push(format!("finalizeChannel({an})"));
} else {
arg_exprs.push(an);
}
}
match arg_exprs.len() {
0 => out.push_str(" let payload: [UInt8] = []\n"),
1 => out.push_str(&format!(
" let payload = encodeVoxTyped({}, {prefix}_ArgsEncoder)\n",
arg_exprs[0]
)),
_ => out.push_str(&format!(
" let payload = encodeVoxTyped(({}), {prefix}_ArgsEncoder)\n",
arg_exprs.join(", ")
)),
}
let channels_arg = if has_channels {
"channels: channelIds, "
} else {
""
};
let finalize_arg = if finalizers.is_empty() {
"nil".to_string()
} else {
format!("{{ {} }}", finalizers.join("; "))
};
out.push_str(&format!(
" let response = try await connection.call(\n methodId: {method_id}, metadata: .null, payload: payload, {channels_arg}timeout: timeout,\n finalizeChannels: {finalize_arg},\n schemaInfo: ClientSchemaInfo(methodSchemas: {svc}Methods[{method_id}]!, registry: {svc}Registry))\n"
));
let is_fallible = matches!(
classify_shape(method.return_shape),
ShapeKind::Result { .. }
);
out.push_str(&format!(
" guard let respDecoder = connection.schemaReceiveTracker.buildDecodeFn({method_id}, .response, readerDescriptor: {prefix}_ResponseDescriptor, readerBlocks: {prefix}_ResponseDescriptorBlocks, local: {svc}Registry) else {{\n throw VoxError<Infallible>.invalidPayload(\"no response schema advertised\")\n }}\n"
));
out.push_str(&format!(
" let result: {resp_ty} = try decodeVoxTyped(respDecoder, response)\n switch result {{\n"
));
if is_fallible {
out.push_str(" case .success(let value): return .success(value)\n");
out.push_str(" case .failure(.user(let e)): return .failure(e)\n");
out.push_str(" case .failure(let voxError): throw voxError\n");
} else if ret == "Void" {
out.push_str(" case .success: return\n");
out.push_str(" case .failure(let error): throw error\n");
} else {
out.push_str(" case .success(let value): return value\n");
out.push_str(" case .failure(let error): throw error\n");
}
out.push_str(" }\n }\n\n");
}
out.push_str("}\n\n");
out
}