Skip to main content

vox_codegen/targets/swift/
mod.rs

1//! Swift code generation for vox services.
2//!
3//! This module generates Swift client and server code from service definitions.
4//! The generated code includes:
5//! - Type definitions for all named types (structs, enums)
6//! - Caller protocol and client implementation for making RPC calls
7//! - Handler protocol for implementing services
8//! - Dispatcher for routing incoming calls
9//! - Encoding/decoding logic for all types
10//! - Runtime schema information for channel binding
11
12pub 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/// Controls which Swift bindings are generated for a service.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SwiftBindings {
33    /// Generate client-side bindings (`*Caller`, `*Client`) and shared support code.
34    Client,
35    /// Generate server-side bindings (`*Handler`, `*Dispatcher`) and shared support code.
36    Server,
37    /// Generate both client and server bindings (legacy default behavior).
38    ClientAndServer,
39}
40
41/// Generate method IDs as a Swift enum.
42pub 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
64/// Generate a complete Swift module for a service.
65///
66/// This is the main entry point for Swift code generation.
67pub fn generate_service(service: &ServiceDescriptor) -> String {
68    generate_service_with_bindings(service, SwiftBindings::ClientAndServer)
69}
70
71/// Generate a Swift module for a service with explicit client/server selection.
72///
73/// Shared sections (method IDs, named types, schemas) are always included.
74pub fn generate_service_with_bindings(
75    service: &ServiceDescriptor,
76    bindings: SwiftBindings,
77) -> String {
78    generate_service_inner(service, bindings, /* include_types */ true)
79}
80
81/// Like [`generate_service_with_bindings`], but emits no named-type
82/// declarations or named-type encoders. Use this together with
83/// [`generate_common_types`] when several services share types and
84/// you want them in a single "common" file rather than duplicated in
85/// every per-service file.
86pub fn generate_service_without_types(
87    service: &ServiceDescriptor,
88    bindings: SwiftBindings,
89) -> String {
90    generate_service_inner(service, bindings, /* include_types */ false)
91}
92
93/// Generate a Swift module containing only the named-type
94/// declarations + encoders referenced by the given services,
95/// deduplicated by type name. Each type is emitted exactly once even
96/// if multiple services reference it.
97pub 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    // Generate method IDs enum
143    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        // Generate named types
157        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        // Generate named-type encode functions (one per named struct/enum)
162        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    // Always generate runtime schema info used for channel binding.
194    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}