vox-codegen 0.8.1

Language bindings codegen for vox
Documentation
//! Swift code generation for vox services.
//!
//! This module generates Swift client and server code from service definitions.
//! The generated code includes:
//! - Type definitions for all named types (structs, enums)
//! - Caller protocol and client implementation for making RPC calls
//! - Handler protocol for implementing services
//! - Dispatcher for routing incoming calls
//! - Encoding/decoding logic for all types
//! - Runtime schema information for channel binding

pub mod client;
pub mod decode;
pub mod descriptor;
pub mod encode;
pub mod schema;
pub mod server;
pub mod types;
pub mod wire;

use vox_types::{MethodDescriptor, ServiceDescriptor};

pub use client::generate_client;
pub use descriptor::{generate_service_value_descriptors, generate_value_descriptors};
pub use encode::generate_named_type_encode_fns;
pub use schema::generate_schemas;
pub use server::generate_server;
pub use types::{collect_named_types, generate_named_types};

/// Controls which Swift bindings are generated for a service.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SwiftBindings {
    /// Generate client-side bindings (`*Caller`, `*Client`) and shared support code.
    Client,
    /// Generate server-side bindings (`*Handler`, `*Dispatcher`) and shared support code.
    Server,
    /// Generate both client and server bindings (legacy default behavior).
    ClientAndServer,
}

/// Generate method IDs as a Swift enum.
pub fn generate_method_ids(methods: &[&MethodDescriptor]) -> String {
    use crate::render::{fq_name, hex_u64};

    let mut items = methods
        .iter()
        .map(|m| (fq_name(m), m.id.0))
        .collect::<Vec<_>>();
    items.sort_by(|a, b| a.0.cmp(&b.0));

    let mut out = String::new();
    out.push_str("// @generated by vox-codegen\n");
    out.push_str("// This file defines canonical vox method IDs.\n\n");
    out.push_str("public enum VoxMethodId {\n");
    out.push_str("    public static let byName: [String: UInt64] = [\n");
    for (name, id) in items {
        out.push_str(&format!("        \"{name}\": {hex},\n", hex = hex_u64(id)));
    }
    out.push_str("    ]\n");
    out.push_str("}\n");
    out
}

/// Generate a complete Swift module for a service.
///
/// This is the main entry point for Swift code generation.
pub fn generate_service(service: &ServiceDescriptor) -> String {
    generate_service_with_bindings(service, SwiftBindings::ClientAndServer)
}

/// Generate a Swift module for a service with explicit client/server selection.
///
/// Shared sections (method IDs, named types, schemas) are always included.
pub fn generate_service_with_bindings(
    service: &ServiceDescriptor,
    bindings: SwiftBindings,
) -> String {
    generate_service_inner(service, bindings, /* include_types */ true)
}

/// Like [`generate_service_with_bindings`], but emits no named-type
/// declarations or named-type encoders. Use this together with
/// [`generate_common_types`] when several services share types and
/// you want them in a single "common" file rather than duplicated in
/// every per-service file.
pub fn generate_service_without_types(
    service: &ServiceDescriptor,
    bindings: SwiftBindings,
) -> String {
    generate_service_inner(service, bindings, /* include_types */ false)
}

/// Generate a Swift module containing only the named-type
/// declarations + encoders referenced by the given services,
/// deduplicated by type name. Each type is emitted exactly once even
/// if multiple services reference it.
pub fn generate_common_types(services: &[&ServiceDescriptor]) -> String {
    let mut out = String::new();
    out.push_str("// @generated by vox-codegen\n");
    out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
    out.push_str("import Foundation\n");
    out.push_str("@preconcurrency import NIOCore\n");
    out.push_str("import VoxRuntime\n\n");
    out.push_str("// MARK: - Shared Types\n\n");

    let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
    let mut deduped: Vec<(String, &'static facet_core::Shape)> = Vec::new();
    for service in services {
        for (name, shape) in collect_named_types(service) {
            if seen.insert(name.clone()) {
                deduped.push((name, shape));
            }
        }
    }

    out.push_str(&generate_named_types(&deduped));
    out.push_str("// MARK: - Shared Encoders\n\n");
    out.push_str(&generate_named_type_encode_fns(&deduped));
    out.push_str("// MARK: - Shared Decoders\n\n");
    out.push_str(&decode::generate_named_type_decode_fns(&deduped));
    out.push_str(&generate_value_descriptors("shared", &deduped));
    out
}

fn generate_service_inner(
    service: &ServiceDescriptor,
    bindings: SwiftBindings,
    include_types: bool,
) -> String {
    use crate::render::hex_u64;
    use heck::{ToLowerCamelCase, ToUpperCamelCase};

    let mut out = String::new();
    out.push_str("// @generated by vox-codegen\n");
    out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
    out.push_str("import Foundation\n");
    out.push_str("@preconcurrency import NIOCore\n");
    out.push_str("import VoxRuntime\n\n");

    let service_name = service.service_name.to_upper_camel_case();

    // Generate method IDs enum
    out.push_str(&format!("// MARK: - {service_name} Method IDs\n\n"));
    out.push_str(&format!("public enum {service_name}MethodId {{\n"));
    for method in service.methods {
        let method_name = method.method_name.to_lower_camel_case();
        let id = crate::method_id(method);
        out.push_str(&format!(
            "    public static let {method_name}: UInt64 = {hex}\n",
            hex = hex_u64(id)
        ));
    }
    out.push_str("}\n\n");

    if include_types {
        // Generate named types
        out.push_str(&format!("// MARK: - {service_name} Types\n\n"));
        let named_types = collect_named_types(service);
        out.push_str(&generate_named_types(&named_types));

        // Generate named-type encode functions (one per named struct/enum)
        out.push_str(&format!("// MARK: - {service_name} Encoders\n\n"));
        out.push_str(&generate_named_type_encode_fns(&named_types));

        out.push_str(&format!("// MARK: - {service_name} Decoders\n\n"));
        out.push_str(&decode::generate_named_type_decode_fns(&named_types));

        out.push_str(&generate_service_value_descriptors(
            &service.service_name.to_lower_camel_case(),
            &named_types,
            service.methods,
        ));
    }

    match bindings {
        SwiftBindings::Client => {
            out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
            out.push_str(&generate_client(service));
        }
        SwiftBindings::Server => {
            out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
            out.push_str(&generate_server(service));
        }
        SwiftBindings::ClientAndServer => {
            out.push_str(&format!("// MARK: - {service_name} Client\n\n"));
            out.push_str(&generate_client(service));

            out.push_str(&format!("// MARK: - {service_name} Server\n\n"));
            out.push_str(&generate_server(service));
        }
    }

    // Always generate runtime schema info used for channel binding.
    out.push_str(&format!("// MARK: - {service_name} Schemas\n\n"));
    out.push_str(&generate_schemas(service));

    out
}

#[cfg(test)]
mod tests {
    use super::generate_service;
    use vox::{Rx, Tx};
    use vox_types::{MethodDescriptor, RetryPolicy, ServiceDescriptor, method_descriptor};

    #[test]
    fn generated_swift_emits_channel_schemas() {
        let subscribe = method_descriptor::<(Tx<u32>, Rx<u32>), ()>(
            "StreamSvc",
            "subscribe",
            &["output", "input"],
            None,
        );
        let methods = Box::leak(vec![subscribe].into_boxed_slice());
        let service = ServiceDescriptor {
            service_name: "StreamSvc",
            methods,
            doc: None,
        };

        let generated = generate_service(&service);

        assert!(
            generated.contains(".channel(direction: .tx, element:"),
            "generated Swift should emit Tx channel schema:\n{generated}"
        );
        assert!(
            generated.contains(".channel(direction: .rx, element:"),
            "generated Swift should emit Rx channel schema:\n{generated}"
        );
    }

    #[test]
    fn generated_swift_emits_retry_policy_for_client_and_dispatcher() {
        let base = method_descriptor::<(u32,), ()>("RetrySvc", "rerun", &["value"], None);
        let method = Box::leak(Box::new(MethodDescriptor {
            id: base.id,
            service_name: base.service_name,
            method_name: base.method_name,
            args_shape: base.args_shape,
            args: base.args,
            return_shape: base.return_shape,
            args_have_channels: base.args_have_channels,
            retry: RetryPolicy::PERSIST_IDEM,
            doc: None,
        }));
        let methods: &'static [&'static MethodDescriptor] =
            Box::leak(vec![method as &'static MethodDescriptor].into_boxed_slice());
        let service = ServiceDescriptor {
            service_name: "RetrySvc",
            methods,
            doc: None,
        };

        let generated = generate_service(&service);

        assert!(
            generated.contains("retry: .persistIdem"),
            "generated Swift client should pass retry policy:\n{generated}"
        );
        assert!(
            generated.contains("public func retryPolicy(methodId: UInt64) -> RetryPolicy"),
            "generated Swift dispatcher should expose retry policy lookup:\n{generated}"
        );
        assert!(
            generated.contains("return .persistIdem"),
            "generated Swift dispatcher should return the method retry policy:\n{generated}"
        );
    }

    #[test]
    fn generated_swift_emits_value_descriptors_for_named_types() {
        #[allow(dead_code)]
        #[derive(facet::Facet)]
        struct SwiftPoint {
            x: i32,
            label: String,
        }

        #[allow(dead_code)]
        #[repr(u8)]
        #[derive(facet::Facet)]
        enum SwiftChoice {
            Empty,
            Number(i32),
            Pair { left: i32, right: i32 },
        }

        let describe = method_descriptor::<(SwiftPoint, SwiftChoice), SwiftPoint>(
            "DescriptorSvc",
            "describe",
            &["point", "choice"],
            None,
        );
        let methods = Box::leak(vec![describe].into_boxed_slice());
        let service = ServiceDescriptor {
            service_name: "DescriptorSvc",
            methods,
            doc: None,
        };

        let generated = generate_service(&service);

        assert!(
            generated.contains(
                "public let descriptorSvc_swift_value_descriptors: VoxSwiftDescriptorRegistry"
            ),
            "generated Swift should expose a value descriptor registry:\n{generated}"
        );
        assert!(
            generated.contains("MemoryLayout<SwiftPoint>.offset(of: \\SwiftPoint.x)!"),
            "generated Swift should capture struct field offsets:\n{generated}"
        );
        assert!(
            generated.contains("kind: VoxSwiftTypeKindEnum"),
            "generated Swift should emit enum descriptors:\n{generated}"
        );
        assert!(
            generated.contains("enumWitnesses: VoxSwiftEnumWitnesses("),
            "generated Swift should emit enum witness thunks:\n{generated}"
        );
        assert!(
            generated.contains(
                "public let descriptorSvc_swift_method_value_descriptors: [UInt64: VoxSwiftMethodValueDescriptorInfo]"
            ),
            "generated Swift should expose per-method value descriptor roots:\n{generated}"
        );
        assert!(
            generated.contains("registry.defineMethod(methodId:"),
            "generated Swift should bind method IDs to local value descriptor roots:\n{generated}"
        );
    }
}