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 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/// Controls which Swift bindings are generated for a service.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum SwiftBindings {
31    /// Generate client-side bindings (`*Caller`, `*Client`) and shared support code.
32    Client,
33    /// Generate server-side bindings (`*Handler`, `*Dispatcher`) and shared support code.
34    Server,
35    /// Generate both client and server bindings (legacy default behavior).
36    ClientAndServer,
37}
38
39/// Generate method IDs as a Swift enum.
40pub 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
62/// Generate a complete Swift module for a service.
63///
64/// This is the main entry point for Swift code generation.
65pub fn generate_service(service: &ServiceDescriptor) -> String {
66    generate_service_with_bindings(service, SwiftBindings::ClientAndServer)
67}
68
69/// Generate a Swift module for a service with explicit client/server selection.
70///
71/// Shared sections (method IDs, named types, schemas) are always included.
72pub 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    // Generate method IDs enum
89    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    // Generate named types
102    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    // Generate named-type encode functions (one per named struct/enum)
107    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    // Always generate runtime schema info used for channel binding.
129    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}