vox_codegen/targets/swift/
mod.rs1pub mod client;
13pub mod decode;
14pub mod descriptor;
15pub mod encode;
16pub mod schema;
17pub mod server;
18pub mod types;
19pub mod wire;
20
21use vox_types::{MethodDescriptor, ServiceDescriptor};
22
23pub use client::generate_client;
24pub use descriptor::{generate_service_value_descriptors, generate_value_descriptors};
25pub use encode::generate_named_type_encode_fns;
26pub use schema::generate_schemas;
27pub use server::generate_server;
28pub use types::{collect_named_types, generate_named_types};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SwiftBindings {
33 Client,
35 Server,
37 ClientAndServer,
39}
40
41pub fn generate_method_ids(methods: &[&MethodDescriptor]) -> String {
43 use crate::render::{fq_name, hex_u64};
44
45 let mut items = methods
46 .iter()
47 .map(|m| (fq_name(m), m.id.0))
48 .collect::<Vec<_>>();
49 items.sort_by(|a, b| a.0.cmp(&b.0));
50
51 let mut out = String::new();
52 out.push_str("// @generated by vox-codegen\n");
53 out.push_str("// This file defines canonical vox method IDs.\n\n");
54 out.push_str("public enum VoxMethodId {\n");
55 out.push_str(" public static let byName: [String: UInt64] = [\n");
56 for (name, id) in items {
57 out.push_str(&format!(" \"{name}\": {hex},\n", hex = hex_u64(id)));
58 }
59 out.push_str(" ]\n");
60 out.push_str("}\n");
61 out
62}
63
64pub fn generate_service(service: &ServiceDescriptor) -> String {
68 generate_service_with_bindings(service, SwiftBindings::ClientAndServer)
69}
70
71pub fn generate_service_with_bindings(
75 service: &ServiceDescriptor,
76 bindings: SwiftBindings,
77) -> String {
78 generate_service_inner(service, bindings, true)
79}
80
81pub fn generate_service_without_types(
87 service: &ServiceDescriptor,
88 bindings: SwiftBindings,
89) -> String {
90 generate_service_inner(service, bindings, false)
91}
92
93pub fn generate_common_types(services: &[&ServiceDescriptor]) -> String {
98 let mut out = String::new();
99 out.push_str("// @generated by vox-codegen\n");
100 out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
101 out.push_str("import Foundation\n");
102 out.push_str("@preconcurrency import NIOCore\n");
103 out.push_str("import VoxRuntime\n\n");
104 out.push_str("// MARK: - Shared Types\n\n");
105
106 let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
107 let mut deduped: Vec<(String, &'static facet_core::Shape)> = Vec::new();
108 for service in services {
109 for (name, shape) in collect_named_types(service) {
110 if seen.insert(name.clone()) {
111 deduped.push((name, shape));
112 }
113 }
114 }
115
116 out.push_str(&generate_named_types(&deduped));
117 out.push_str("// MARK: - Shared Encoders\n\n");
118 out.push_str(&generate_named_type_encode_fns(&deduped));
119 out.push_str("// MARK: - Shared Decoders\n\n");
120 out.push_str(&decode::generate_named_type_decode_fns(&deduped));
121 out.push_str(&generate_value_descriptors("shared", &deduped));
122 out
123}
124
125fn generate_service_inner(
126 service: &ServiceDescriptor,
127 bindings: SwiftBindings,
128 include_types: bool,
129) -> String {
130 use crate::render::hex_u64;
131 use heck::{ToLowerCamelCase, ToUpperCamelCase};
132
133 let mut out = String::new();
134 out.push_str("// @generated by vox-codegen\n");
135 out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
136 out.push_str("import Foundation\n");
137 out.push_str("@preconcurrency import NIOCore\n");
138 out.push_str("import VoxRuntime\n\n");
139
140 let service_name = service.service_name.to_upper_camel_case();
141
142 out.push_str(&format!("// MARK: - {service_name} Method IDs\n\n"));
144 out.push_str(&format!("public enum {service_name}MethodId {{\n"));
145 for method in service.methods {
146 let method_name = method.method_name.to_lower_camel_case();
147 let id = crate::method_id(method);
148 out.push_str(&format!(
149 " public static let {method_name}: UInt64 = {hex}\n",
150 hex = hex_u64(id)
151 ));
152 }
153 out.push_str("}\n\n");
154
155 if include_types {
156 out.push_str(&format!("// MARK: - {service_name} Types\n\n"));
158 let named_types = collect_named_types(service);
159 out.push_str(&generate_named_types(&named_types));
160
161 out.push_str(&format!("// MARK: - {service_name} Encoders\n\n"));
163 out.push_str(&generate_named_type_encode_fns(&named_types));
164
165 out.push_str(&format!("// MARK: - {service_name} Decoders\n\n"));
166 out.push_str(&decode::generate_named_type_decode_fns(&named_types));
167
168 out.push_str(&generate_service_value_descriptors(
169 &service.service_name.to_lower_camel_case(),
170 &named_types,
171 service.methods,
172 ));
173 }
174
175 match bindings {
176 SwiftBindings::Client => {
177 out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
178 out.push_str(&generate_client(service));
179 }
180 SwiftBindings::Server => {
181 out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
182 out.push_str(&generate_server(service));
183 }
184 SwiftBindings::ClientAndServer => {
185 out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
186 out.push_str(&generate_client(service));
187
188 out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
189 out.push_str(&generate_server(service));
190 }
191 }
192
193 out.push_str(&format!("// MARK: - {service_name} Schemas\n\n"));
195 out.push_str(&generate_schemas(service));
196
197 out
198}
199
200#[cfg(test)]
201mod tests {
202 use super::generate_service;
203 use vox::{Rx, Tx};
204 use vox_types::{MethodDescriptor, RetryPolicy, ServiceDescriptor, method_descriptor};
205
206 #[test]
207 fn generated_swift_emits_channel_schemas() {
208 let subscribe = method_descriptor::<(Tx<u32>, Rx<u32>), ()>(
209 "StreamSvc",
210 "subscribe",
211 &["output", "input"],
212 None,
213 );
214 let methods = Box::leak(vec![subscribe].into_boxed_slice());
215 let service = ServiceDescriptor {
216 service_name: "StreamSvc",
217 methods,
218 doc: None,
219 };
220
221 let generated = generate_service(&service);
222
223 assert!(
224 generated.contains(".channel(direction: .tx, element:"),
225 "generated Swift should emit Tx channel schema:\n{generated}"
226 );
227 assert!(
228 generated.contains(".channel(direction: .rx, element:"),
229 "generated Swift should emit Rx channel schema:\n{generated}"
230 );
231 }
232
233 #[test]
234 fn generated_swift_emits_retry_policy_for_client_and_dispatcher() {
235 let base = method_descriptor::<(u32,), ()>("RetrySvc", "rerun", &["value"], None);
236 let method = Box::leak(Box::new(MethodDescriptor {
237 id: base.id,
238 service_name: base.service_name,
239 method_name: base.method_name,
240 args_shape: base.args_shape,
241 args: base.args,
242 return_shape: base.return_shape,
243 args_have_channels: base.args_have_channels,
244 retry: RetryPolicy::PERSIST_IDEM,
245 doc: None,
246 }));
247 let methods: &'static [&'static MethodDescriptor] =
248 Box::leak(vec![method as &'static MethodDescriptor].into_boxed_slice());
249 let service = ServiceDescriptor {
250 service_name: "RetrySvc",
251 methods,
252 doc: None,
253 };
254
255 let generated = generate_service(&service);
256
257 assert!(
258 generated.contains("retry: .persistIdem"),
259 "generated Swift client should pass retry policy:\n{generated}"
260 );
261 assert!(
262 generated.contains("public func retryPolicy(methodId: UInt64) -> RetryPolicy"),
263 "generated Swift dispatcher should expose retry policy lookup:\n{generated}"
264 );
265 assert!(
266 generated.contains("return .persistIdem"),
267 "generated Swift dispatcher should return the method retry policy:\n{generated}"
268 );
269 }
270
271 #[test]
272 fn generated_swift_emits_value_descriptors_for_named_types() {
273 #[allow(dead_code)]
274 #[derive(facet::Facet)]
275 struct SwiftPoint {
276 x: i32,
277 label: String,
278 }
279
280 #[allow(dead_code)]
281 #[repr(u8)]
282 #[derive(facet::Facet)]
283 enum SwiftChoice {
284 Empty,
285 Number(i32),
286 Pair { left: i32, right: i32 },
287 }
288
289 let describe = method_descriptor::<(SwiftPoint, SwiftChoice), SwiftPoint>(
290 "DescriptorSvc",
291 "describe",
292 &["point", "choice"],
293 None,
294 );
295 let methods = Box::leak(vec![describe].into_boxed_slice());
296 let service = ServiceDescriptor {
297 service_name: "DescriptorSvc",
298 methods,
299 doc: None,
300 };
301
302 let generated = generate_service(&service);
303
304 assert!(
305 generated.contains(
306 "public let descriptorSvc_swift_value_descriptors: VoxSwiftDescriptorRegistry"
307 ),
308 "generated Swift should expose a value descriptor registry:\n{generated}"
309 );
310 assert!(
311 generated.contains("MemoryLayout<SwiftPoint>.offset(of: \\SwiftPoint.x)!"),
312 "generated Swift should capture struct field offsets:\n{generated}"
313 );
314 assert!(
315 generated.contains("kind: VoxSwiftTypeKindEnum"),
316 "generated Swift should emit enum descriptors:\n{generated}"
317 );
318 assert!(
319 generated.contains("enumWitnesses: VoxSwiftEnumWitnesses("),
320 "generated Swift should emit enum witness thunks:\n{generated}"
321 );
322 assert!(
323 generated.contains(
324 "public let descriptorSvc_swift_method_value_descriptors: [UInt64: VoxSwiftMethodValueDescriptorInfo]"
325 ),
326 "generated Swift should expose per-method value descriptor roots:\n{generated}"
327 );
328 assert!(
329 generated.contains("registry.defineMethod(methodId:"),
330 "generated Swift should bind method IDs to local value descriptor roots:\n{generated}"
331 );
332 }
333}