vox_codegen/targets/swift/
mod.rs1pub mod client;
13pub mod decode;
14pub mod encode;
15pub mod schema;
16pub mod server;
17pub mod types;
18pub mod wire;
19
20use vox_types::{MethodDescriptor, ServiceDescriptor};
21
22pub use client::generate_client;
23pub use encode::generate_named_type_encode_fns;
24pub use schema::generate_schemas;
25pub use server::generate_server;
26pub use types::{collect_named_types, generate_named_types};
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SwiftBindings {
31 Client,
33 Server,
35 ClientAndServer,
37}
38
39pub fn generate_method_ids(methods: &[&MethodDescriptor]) -> String {
41 use crate::render::{fq_name, hex_u64};
42
43 let mut items = methods
44 .iter()
45 .map(|m| (fq_name(m), m.id.0))
46 .collect::<Vec<_>>();
47 items.sort_by(|a, b| a.0.cmp(&b.0));
48
49 let mut out = String::new();
50 out.push_str("// @generated by vox-codegen\n");
51 out.push_str("// This file defines canonical vox method IDs.\n\n");
52 out.push_str("public enum VoxMethodId {\n");
53 out.push_str(" public static let byName: [String: UInt64] = [\n");
54 for (name, id) in items {
55 out.push_str(&format!(" \"{name}\": {hex},\n", hex = hex_u64(id)));
56 }
57 out.push_str(" ]\n");
58 out.push_str("}\n");
59 out
60}
61
62pub fn generate_service(service: &ServiceDescriptor) -> String {
66 generate_service_with_bindings(service, SwiftBindings::ClientAndServer)
67}
68
69pub fn generate_service_with_bindings(
73 service: &ServiceDescriptor,
74 bindings: SwiftBindings,
75) -> String {
76 use crate::render::hex_u64;
77 use heck::{ToLowerCamelCase, ToUpperCamelCase};
78
79 let mut out = String::new();
80 out.push_str("// @generated by vox-codegen\n");
81 out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
82 out.push_str("import Foundation\n");
83 out.push_str("@preconcurrency import NIOCore\n");
84 out.push_str("import VoxRuntime\n\n");
85
86 let service_name = service.service_name.to_upper_camel_case();
87
88 out.push_str(&format!("// MARK: - {service_name} Method IDs\n\n"));
90 out.push_str(&format!("public enum {service_name}MethodId {{\n"));
91 for method in service.methods {
92 let method_name = method.method_name.to_lower_camel_case();
93 let id = crate::method_id(method);
94 out.push_str(&format!(
95 " public static let {method_name}: UInt64 = {hex}\n",
96 hex = hex_u64(id)
97 ));
98 }
99 out.push_str("}\n\n");
100
101 out.push_str(&format!("// MARK: - {service_name} Types\n\n"));
103 let named_types = collect_named_types(service);
104 out.push_str(&generate_named_types(&named_types));
105
106 out.push_str(&format!("// MARK: - {service_name} Encoders\n\n"));
108 out.push_str(&generate_named_type_encode_fns(&named_types));
109
110 match bindings {
111 SwiftBindings::Client => {
112 out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
113 out.push_str(&generate_client(service));
114 }
115 SwiftBindings::Server => {
116 out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
117 out.push_str(&generate_server(service));
118 }
119 SwiftBindings::ClientAndServer => {
120 out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
121 out.push_str(&generate_client(service));
122
123 out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
124 out.push_str(&generate_server(service));
125 }
126 }
127
128 out.push_str(&format!("// MARK: - {service_name} Schemas\n\n"));
130 out.push_str(&generate_schemas(service));
131
132 out
133}
134
135#[cfg(test)]
136mod tests {
137 use super::generate_service;
138 use vox::{Rx, Tx};
139 use vox_types::{MethodDescriptor, RetryPolicy, ServiceDescriptor, method_descriptor};
140
141 #[test]
142 fn generated_swift_emits_channel_schemas() {
143 let subscribe = method_descriptor::<(Tx<u32>, Rx<u32>), ()>(
144 "StreamSvc",
145 "subscribe",
146 &["output", "input"],
147 None,
148 );
149 let methods = Box::leak(vec![subscribe].into_boxed_slice());
150 let service = ServiceDescriptor {
151 service_name: "StreamSvc",
152 methods,
153 doc: None,
154 };
155
156 let generated = generate_service(&service);
157
158 assert!(
159 generated.contains(".tx(element: .u32)"),
160 "generated Swift should emit Tx channel schema:\n{generated}"
161 );
162 assert!(
163 generated.contains(".rx(element: .u32)"),
164 "generated Swift should emit Rx channel schema:\n{generated}"
165 );
166 }
167
168 #[test]
169 fn generated_swift_emits_retry_policy_for_client_and_dispatcher() {
170 let base = method_descriptor::<(u32,), ()>("RetrySvc", "rerun", &["value"], None);
171 let method = Box::leak(Box::new(MethodDescriptor {
172 id: base.id,
173 service_name: base.service_name,
174 method_name: base.method_name,
175 args_shape: base.args_shape,
176 args: base.args,
177 return_shape: base.return_shape,
178 retry: RetryPolicy::PERSIST_IDEM,
179 doc: None,
180 }));
181 let methods: &'static [&'static MethodDescriptor] =
182 Box::leak(vec![method as &'static MethodDescriptor].into_boxed_slice());
183 let service = ServiceDescriptor {
184 service_name: "RetrySvc",
185 methods,
186 doc: None,
187 };
188
189 let generated = generate_service(&service);
190
191 assert!(
192 generated.contains("retry: .persistIdem"),
193 "generated Swift client should pass retry policy:\n{generated}"
194 );
195 assert!(
196 generated.contains("public func retryPolicy(methodId: UInt64) -> RetryPolicy"),
197 "generated Swift dispatcher should expose retry policy lookup:\n{generated}"
198 );
199 assert!(
200 generated.contains("return .persistIdem"),
201 "generated Swift dispatcher should return the method retry policy:\n{generated}"
202 );
203 }
204}