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