Skip to main content

vox_codegen/targets/swift/
mod.rs

1//! Swift code generation for vox services, on the phon codec.
2//!
3//! For a service, [`generate_service`] emits one module: the named type definitions,
4//! the per-service phon schemas/descriptors/lowered programs ([`phon_service`]), the
5//! typed client ([`phon_client`]) and server ([`phon_server`]).
6//!
7//! The Message/Handshake wire envelopes are emitted separately by
8//! [`phon_descriptor::generate_phon_wire`].
9
10pub mod phon_client;
11pub mod phon_descriptor;
12pub mod phon_server;
13pub mod phon_service;
14pub mod types;
15
16use facet_core::Shape;
17use vox_types::ServiceDescriptor;
18
19pub use types::{collect_named_types, generate_named_types};
20
21/// A wire type to generate Swift code for: its Swift name and facet `Shape`. Used to
22/// build the envelope/handshake type lists passed to [`phon_descriptor::generate_phon_wire`].
23pub struct WireType {
24    /// The Swift name for this type (e.g. "Message").
25    pub swift_name: String,
26    /// The facet `Shape` for this type.
27    pub shape: &'static Shape,
28}
29
30/// Generate a complete Swift module for a service on the phon codec, including
31/// named types, per-service phon schemas/descriptors/programs, and the typed
32/// client and server.
33/// This is the main entry point for Swift service code generation.
34/// r[impl service-macro.is-source-of-truth]
35pub fn generate_service(service: &ServiceDescriptor) -> String {
36    validate_channel_rules(service);
37    // r[impl transport.fd.capability]
38    super::validate_no_fd_shapes(service, "Swift");
39
40    let mut out = String::new();
41    out.push_str("// @generated by vox-codegen\n");
42    out.push_str("// DO NOT EDIT - regenerate with `cargo xtask codegen --swift`\n\n");
43    out.push_str("import Foundation\nimport PhonEngine\nimport PhonIR\nimport PhonSchema\nimport VoxRuntime\n\n");
44    let named = collect_named_types(service);
45    // phon types don't need Codable (the descriptor codec encodes/decodes them; it also
46    // can't always synthesize). Drop it like the wire types do.
47    let typedefs = generate_named_types(&named)
48        .replace(": Codable, Sendable, Error", ": Sendable, Error")
49        .replace(": Codable, Sendable", ": Sendable");
50    out.push_str(&typedefs);
51    out.push('\n');
52    out.push_str(&phon_service::generate_phon_service(service));
53    out.push_str(&phon_client::generate_phon_client(service));
54    out.push_str(&phon_server::generate_phon_server(service));
55    out
56}
57
58fn validate_channel_rules(service: &ServiceDescriptor) {
59    for method in service.methods {
60        // r[impl rpc.channel.placement]
61        assert!(
62            !vox_types::shape_contains_channel(method.return_shape),
63            "channels are not allowed in return types: {}.{}",
64            method.service_name,
65            method.method_name
66        );
67        // r[impl rpc.channel.no-collections]
68        assert!(
69            !vox_types::shape_contains_channel_in_collection(method.args_shape),
70            "channels are not allowed inside collections: {}.{}",
71            method.service_name,
72            method.method_name
73        );
74        // r[impl rpc.channel.direct-args]
75        assert!(
76            !vox_types::shape_contains_indirect_channel_arg(method.args_shape),
77            "channels are only allowed as direct method arguments: {}.{}",
78            method.service_name,
79            method.method_name
80        );
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use std::collections::BTreeSet;
87
88    use base64::{Engine, engine::general_purpose::STANDARD};
89    use facet::{Facet, Shape};
90    use vox_types::{
91        MethodDescriptor, MethodDescriptorOptions, MethodId, Rx, ServiceDescriptor, Tx,
92        method_descriptor,
93    };
94
95    use crate::render::hex_u64;
96
97    use super::generate_service;
98
99    fn schema_closure_expr(bytes: &[u8]) -> String {
100        let encoded = STANDARD.encode(bytes);
101        let mut out = String::from("voxSchemaClosure(\"\"\"\n");
102        for chunk in encoded.as_bytes().chunks(100) {
103            out.push_str("          ");
104            out.push_str(std::str::from_utf8(chunk).expect("base64 is utf-8"));
105            out.push('\n');
106        }
107        out.push_str("          \"\"\")");
108        out
109    }
110
111    fn parse_self_describing_unique_bundle(
112        bytes: &[u8],
113        expected_root: u64,
114    ) -> vox_phon::SchemaBundle {
115        let bundle = vox_phon::parse_schema_bytes(bytes).expect("schema closure parses");
116        assert_eq!(bundle.root.as_u64(), expected_root);
117        let mut seen = BTreeSet::new();
118        for schema in &bundle.schemas {
119            assert!(
120                seen.insert(schema.id.as_u64()),
121                "schema closure must not repeat type ID {:#x}",
122                schema.id.as_u64()
123            );
124        }
125        bundle
126    }
127
128    #[derive(Facet)]
129    struct NestedInner {
130        value: u32,
131    }
132
133    #[derive(Facet)]
134    struct NestedOuter {
135        inner: NestedInner,
136    }
137
138    #[derive(Facet)]
139    struct NestedChannels {
140        stream: Tx<u32>,
141    }
142
143    #[derive(Facet)]
144    struct OuterChannels {
145        inner: NestedChannels,
146    }
147
148    #[cfg(unix)]
149    #[derive(Facet)]
150    struct FdBundle {
151        descriptors: Vec<vox_types::Fd>,
152    }
153
154    fn service_with_shapes(
155        args_shape: &'static Shape,
156        return_shape: &'static Shape,
157    ) -> ServiceDescriptor {
158        let method = Box::leak(Box::new(MethodDescriptor {
159            id: MethodId(1),
160            service_name: "BadSvc",
161            method_name: "bad",
162            args_shape,
163            args: &[],
164            return_shape,
165            response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
166            args_have_channels: vox_types::shape_contains_channel(args_shape),
167            doc: None,
168        }));
169        let methods = Box::leak(vec![method as &'static MethodDescriptor].into_boxed_slice());
170        ServiceDescriptor {
171            service_name: "BadSvc",
172            methods,
173            doc: None,
174        }
175    }
176
177    fn panic_message(panic: Box<dyn std::any::Any + Send>) -> String {
178        panic
179            .downcast_ref::<String>()
180            .cloned()
181            .or_else(|| panic.downcast_ref::<&'static str>().map(|s| s.to_string()))
182            .expect("panic payload should be a string")
183    }
184
185    #[test]
186    // r[verify schema.type-id]
187    // r[verify schema.principles.self-describing]
188    // r[verify schema.principles.once-per-type]
189    // r[verify schema.format.self-contained]
190    // r[verify schema.tracking.transitive]
191    // r[verify schema.method-id]
192    // r[verify rpc.method-id]
193    // r[verify rpc.method-id.algorithm]
194    // r[verify service-macro.is-source-of-truth]
195    fn generated_swift_uses_phon_schema_closures_and_canonical_method_ids() {
196        let echo = method_descriptor::<(NestedOuter,), NestedOuter>(
197            "SchemaSvc",
198            "EchoNested",
199            &["value"],
200            &[Some(<NestedOuter as Facet>::SHAPE)],
201            MethodDescriptorOptions {
202                response_wire_shape: <Result<NestedOuter, vox_types::VoxError> as Facet>::SHAPE,
203                doc: None,
204            },
205        );
206        let methods = Box::leak(vec![echo].into_boxed_slice());
207        let service = ServiceDescriptor {
208            service_name: "SchemaSvc",
209            methods,
210            doc: None,
211        };
212
213        let generated = generate_service(&service);
214        let args_root = vox_phon::schema_id_for_shape(echo.args_shape).expect("args schema id");
215        let response_root =
216            vox_phon::schema_id_for_shape(echo.response_wire_shape).expect("response schema id");
217        let args_closure =
218            vox_phon::schema_bytes_for_shape(echo.args_shape).expect("args schema closure");
219        let response_closure = vox_phon::schema_bytes_for_shape(echo.response_wire_shape)
220            .expect("response schema closure");
221        let args_bundle = parse_self_describing_unique_bundle(&args_closure, args_root.as_u64());
222        let _response_bundle =
223            parse_self_describing_unique_bundle(&response_closure, response_root.as_u64());
224        let nested_inner_id =
225            vox_phon::schema_id_for_shape(<NestedInner as Facet>::SHAPE).expect("inner schema id");
226
227        assert_eq!(args_bundle.root, args_root);
228        assert!(
229            args_bundle
230                .schemas
231                .iter()
232                .any(|schema| schema.id == nested_inner_id),
233            "args closure must carry transitively referenced NestedInner schema"
234        );
235        assert!(
236            generated.contains(&format!(
237                "    {}: PhonMethodSchemas(",
238                hex_u64(crate::method_id(echo))
239            )),
240            "generated Swift method table must use the canonical Vox method ID:\n{generated}"
241        );
242        assert!(
243            generated.contains(&format!(
244                "argsRoot: SchemaId({})",
245                hex_u64(args_root.as_u64())
246            )),
247            "generated Swift must use the phon args schema ID:\n{generated}"
248        );
249        assert!(
250            generated.contains(&format!(
251                "responseRoot: SchemaId({})",
252                hex_u64(response_root.as_u64())
253            )),
254            "generated Swift must use the phon response schema ID:\n{generated}"
255        );
256        assert!(
257            generated.contains(&format!(
258                "argsSchemaClosure: {}",
259                schema_closure_expr(&args_closure)
260            )),
261            "generated Swift must embed the self-contained phon args closure:\n{generated}"
262        );
263        assert!(
264            generated.contains(&format!(
265                "responseSchemaClosure: {}",
266                schema_closure_expr(&response_closure)
267            )),
268            "generated Swift must embed the self-contained phon response closure:\n{generated}"
269        );
270    }
271
272    #[test]
273    // r[verify rpc.method-id.no-collisions]
274    fn generated_swift_method_ids_include_service_name() {
275        let alpha = method_descriptor::<(), ()>(
276            "AlphaSvc",
277            "echo",
278            &[],
279            &[],
280            MethodDescriptorOptions {
281                response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
282                doc: None,
283            },
284        );
285        let beta = method_descriptor::<(), ()>(
286            "BetaSvc",
287            "echo",
288            &[],
289            &[],
290            MethodDescriptorOptions {
291                response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
292                doc: None,
293            },
294        );
295
296        assert_ne!(crate::method_id(alpha), crate::method_id(beta));
297
298        let alpha_methods = Box::leak(vec![alpha].into_boxed_slice());
299        let beta_methods = Box::leak(vec![beta].into_boxed_slice());
300        let alpha_service = ServiceDescriptor {
301            service_name: "AlphaSvc",
302            methods: alpha_methods,
303            doc: None,
304        };
305        let beta_service = ServiceDescriptor {
306            service_name: "BetaSvc",
307            methods: beta_methods,
308            doc: None,
309        };
310
311        let alpha_generated = generate_service(&alpha_service);
312        let beta_generated = generate_service(&beta_service);
313        let alpha_entry = format!(
314            "    {}: PhonMethodSchemas(",
315            hex_u64(crate::method_id(alpha))
316        );
317        let beta_entry = format!(
318            "    {}: PhonMethodSchemas(",
319            hex_u64(crate::method_id(beta))
320        );
321
322        assert!(
323            alpha_generated.contains(&alpha_entry),
324            "Alpha service must emit its service-qualified method ID:\n{alpha_generated}"
325        );
326        assert!(
327            !alpha_generated.contains(&beta_entry),
328            "Alpha service must not reuse Beta service's same-method-name ID:\n{alpha_generated}"
329        );
330        assert!(
331            beta_generated.contains(&beta_entry),
332            "Beta service must emit its service-qualified method ID:\n{beta_generated}"
333        );
334    }
335
336    #[test]
337    // r[verify rpc.caller]
338    // r[verify rpc.service]
339    // r[verify rpc.service.methods]
340    // r[verify rpc.handler]
341    // r[verify rpc.fallible]
342    // r[verify rpc.fallible.caller-signature]
343    // r[verify rpc.fallible.vox-error]
344    // r[verify rpc.fallible.vox-error.outcome]
345    // r[verify rpc.unknown-method]
346    fn generated_swift_emits_rpc_caller_handler_and_dispatch_shapes() {
347        let echo = method_descriptor::<(String,), String>(
348            "TestSvc",
349            "echo",
350            &["message"],
351            &[None],
352            MethodDescriptorOptions {
353                response_wire_shape: <Result<String, vox_types::VoxError> as Facet>::SHAPE,
354                doc: None,
355            },
356        );
357        let divide = method_descriptor::<(u64, u64), Result<u64, String>>(
358            "TestSvc",
359            "divide",
360            &["lhs", "rhs"],
361            &[None, None],
362            MethodDescriptorOptions {
363                response_wire_shape: <Result<u64, vox_types::VoxError<String>> as Facet>::SHAPE,
364                doc: None,
365            },
366        );
367        let methods = Box::leak(vec![echo, divide].into_boxed_slice());
368        let service = ServiceDescriptor {
369            service_name: "TestSvc",
370            methods,
371            doc: None,
372        };
373
374        let generated = generate_service(&service);
375        let echo_id = hex_u64(crate::method_id(echo));
376        let divide_id = hex_u64(crate::method_id(divide));
377
378        assert!(
379            generated.contains("public protocol TestSvcCaller: Sendable {")
380                && generated.contains("    func echo(message: String) async throws -> String")
381                && generated.contains(
382                    "    func divide(lhs: UInt64, rhs: UInt64) async throws -> Result<UInt64, String>",
383                ),
384            "generated Swift must expose async caller methods matching the source service:\n{generated}"
385        );
386        assert!(
387            generated.contains("public final class TestSvcClient: TestSvcCaller, Sendable {")
388                && generated.contains("    private let connection: VoxLane")
389                && generated
390                    .contains("    public init(connection: VoxLane, timeout: TimeInterval? = 30.0)")
391                && generated.contains("let payload = encodeVoxTyped(message, testSvc_echo_ArgsEncoder)")
392                && generated.contains(&format!(
393                    "methodId: {echo_id}, metadata: .null, payload: payload, timeout: timeout,"
394                ))
395                && generated.contains(&format!(
396                    "schemaInfo: ClientSchemaInfo(methodSchemas: testSvcMethods[{echo_id}]!, registry: testSvcRegistry)"
397                ))
398                && generated.contains("let result: Result<String, VoxError<Infallible>> = try decodeVoxTyped(respDecoder, response)"),
399            "generated Swift client must delegate request serialization and response handling to VoxLane:\n{generated}"
400        );
401        assert!(
402            generated.contains("    case connectionShutdown")
403                && !generated.contains("    case sessionShutdown"),
404            "generated Swift wire errors must use connection-era vocabulary:\n{generated}"
405        );
406        assert!(
407            generated.contains("public protocol TestSvcHandler: Sendable {")
408                && generated.contains("    func echo(message: String) async throws -> String")
409                && generated.contains(
410                    "    func divide(lhs: UInt64, rhs: UInt64) async throws -> Result<UInt64, String>",
411                ),
412            "generated Swift handler must expose service methods with server-side argument types:\n{generated}"
413        );
414        assert!(
415            generated.contains("public final class TestSvcDispatcher: ServiceDispatcher {")
416                && generated.contains("    private let handler: TestSvcHandler")
417                && generated.contains("    public init(handler: TestSvcHandler) { self.handler = handler }")
418                && generated.contains("public func dispatch(methodId: UInt64, payload: [UInt8], requestId: UInt64, channels: [UInt64], registry: ChannelRegistry, schemaSendTracker: SchemaSendTracker, schemaReceiveTracker: SchemaTracker, context: RequestContext, taskTx: @escaping @Sendable (TaskMessage) -> Void) async")
419                && generated.contains(&format!(
420                    "case {echo_id}: await dispatch_echo(payload: payload, requestId: requestId, schemaSendTracker: schemaSendTracker, schemaReceiveTracker: schemaReceiveTracker, taskTx: taskTx)"
421                ))
422                && generated.contains(&format!(
423                    "case {divide_id}: await dispatch_divide(payload: payload, requestId: requestId, schemaSendTracker: schemaSendTracker, schemaReceiveTracker: schemaReceiveTracker, taskTx: taskTx)"
424                ))
425                && generated.contains("let voxValue = try await handler.echo(message: args)")
426                && generated.contains("let voxValue = try await handler.divide(lhs: args.0, rhs: args.1)")
427                && generated.contains("case .failure(let e): voxResult = .failure(.user(e))"),
428            "generated Swift dispatcher must route decoded args to the handler and reply through TaskMessage:\n{generated}"
429        );
430        assert!(
431            generated.contains(
432                "default: taskTx(.response(requestId: requestId, payload: encodeVoxError(.unknownMethod), methodId: methodId))"
433            ),
434            "generated Swift dispatcher must return a call-level UnknownMethod response for unrecognized method ids:\n{generated}"
435        );
436    }
437
438    #[test]
439    fn generated_swift_has_no_trailing_line_whitespace() {
440        let divide = method_descriptor::<(u64, u64), Result<u64, String>>(
441            "WhitespaceSvc",
442            "divide",
443            &["lhs", "rhs"],
444            &[None, None],
445            MethodDescriptorOptions {
446                response_wire_shape: <Result<u64, vox_types::VoxError<String>> as Facet>::SHAPE,
447                doc: None,
448            },
449        );
450        let methods = Box::leak(vec![divide].into_boxed_slice());
451        let service = ServiceDescriptor {
452            service_name: "WhitespaceSvc",
453            methods,
454            doc: None,
455        };
456
457        let generated = generate_service(&service);
458
459        for (line_idx, line) in generated.lines().enumerate() {
460            assert_eq!(
461                line.trim_end_matches([' ', '\t']),
462                line,
463                "generated Swift line {} has trailing whitespace: {line:?}",
464                line_idx + 1
465            );
466        }
467    }
468
469    #[test]
470    // r[verify schema.errors.call-level.callee]
471    // r[verify schema.errors.call-level.caller]
472    // r[verify schema.errors.same-peer-terminal]
473    fn generated_swift_maps_schema_decode_failures_to_call_errors() {
474        let echo = method_descriptor::<(NestedOuter,), NestedOuter>(
475            "SchemaSvc",
476            "EchoNested",
477            &["value"],
478            &[Some(<NestedOuter as Facet>::SHAPE)],
479            MethodDescriptorOptions {
480                response_wire_shape: <Result<NestedOuter, vox_types::VoxError> as Facet>::SHAPE,
481                doc: None,
482            },
483        );
484        let methods = Box::leak(vec![echo].into_boxed_slice());
485        let service = ServiceDescriptor {
486            service_name: "SchemaSvc",
487            methods,
488            doc: None,
489        };
490
491        let generated = generate_service(&service);
492
493        assert!(
494            generated.contains("guard let argsDecoder = schemaReceiveTracker.buildDecodeFn"),
495            "generated Swift server must build a caller->callee args decode plan:\n{generated}"
496        );
497        assert!(
498            generated.contains("encodeVoxError(.invalidPayload(\"no args schema advertised\"))"),
499            "generated Swift server must reject missing args decode plans as call errors:\n{generated}"
500        );
501        assert!(
502            generated.contains("do { args = try decodeVoxTyped(argsDecoder, payload) } catch {")
503                && generated.contains("encodeVoxError(.invalidPayload(\"decode args\"))"),
504            "generated Swift server must turn args decode failures into InvalidPayload responses:\n{generated}"
505        );
506        assert!(
507            generated
508                .contains("guard let respDecoder = connection.schemaReceiveTracker.buildDecodeFn"),
509            "generated Swift client must build a callee->caller response decode plan:\n{generated}"
510        );
511        assert!(
512            generated.contains(
513                "throw VoxError<Infallible>.invalidPayload(\"no response schema advertised\")",
514            ),
515            "generated Swift client must surface response decode-plan failure on the call:\n{generated}"
516        );
517        assert_eq!(
518            generated.matches("connection.call(").count(),
519            1,
520            "generated Swift client must issue one request attempt and not replay after local decode failure:\n{generated}"
521        );
522    }
523
524    #[test]
525    // r[verify schema.exchange.channels]
526    // r[verify rpc.channel.discovery]
527    fn generated_swift_emits_channel_schemas() {
528        let subscribe = method_descriptor::<(Tx<NestedInner>, Rx<NestedInner>), ()>(
529            "StreamSvc",
530            "subscribe",
531            &["output", "input"],
532            &[
533                Some(<NestedInner as facet::Facet>::SHAPE),
534                Some(<NestedInner as facet::Facet>::SHAPE),
535            ],
536            MethodDescriptorOptions {
537                response_wire_shape: <Result<(), vox_types::VoxError> as Facet>::SHAPE,
538                doc: None,
539            },
540        );
541        let methods = Box::leak(vec![subscribe].into_boxed_slice());
542        let service = ServiceDescriptor {
543            service_name: "StreamSvc",
544            methods,
545            doc: None,
546        };
547
548        let generated = generate_service(&service);
549        let args_closure = vox_phon::schema_bytes_for_shape_with_auxiliary_roots(
550            subscribe.args_shape,
551            &[
552                (
553                    "channel.arg.0.tx.element",
554                    <NestedInner as facet::Facet>::SHAPE,
555                ),
556                (
557                    "channel.arg.1.rx.element",
558                    <NestedInner as facet::Facet>::SHAPE,
559                ),
560            ],
561        )
562        .expect("args schema closure with channel auxiliary roots");
563        assert!(
564            generated.contains("channel.arg.0.tx.element"),
565            "Tx<T> channel receive decode must use the named auxiliary root:\n{generated}"
566        );
567        assert!(
568            generated.contains("channel.arg.1.rx.element"),
569            "Rx<T> channel receive decode must use the named auxiliary root:\n{generated}"
570        );
571        assert!(
572            generated.contains(&format!(
573                "argsSchemaClosure: {}",
574                schema_closure_expr(&args_closure)
575            )),
576            "channel element roots must be carried in argsSchemaClosure auxiliary roots:\n{generated}"
577        );
578        assert!(
579            generated.contains("buildAuxiliaryDecodeFn"),
580            "generated Swift must build channel item decode programs from advertised schemas:\n{generated}"
581        );
582        assert!(
583            !generated.contains("ElementDecodeProgram"),
584            "generated Swift must not emit local same-schema channel item decode programs:\n{generated}"
585        );
586        assert!(
587            generated.contains("public struct NestedInner"),
588            "named channel element types must be emitted as normal generated types:\n{generated}"
589        );
590        assert!(
591            generated.contains("var channelIds: [UInt64] = []")
592                && generated.contains("let outputWireIndex = channelIds.count")
593                && generated.contains("channelIds.append(await connection.bindClientTxArg(output")
594                && generated.contains("let inputWireIndex = channelIds.count")
595                && generated.contains("channelIds.append(await connection.bindClientRxArg(input")
596                && generated.contains("channels: channelIds,"),
597            "generated Swift caller must discover direct channel args left-to-right and send that channel id list:\n{generated}"
598        );
599        assert!(
600            generated.contains("let outputWireIndex = channelWireIndex(args.0)")
601                && generated.contains("channels[outputWireIndex]")
602                && generated.contains("let inputWireIndex = channelWireIndex(args.1)")
603                && generated.contains("channels[inputWireIndex]"),
604            "generated Swift dispatcher must resolve decoded channel wire indexes through Request.channels:\n{generated}"
605        );
606    }
607
608    #[test]
609    // r[verify rpc.channel.placement]
610    fn generated_swift_rejects_channels_in_return_shapes() {
611        let service = service_with_shapes(<() as Facet>::SHAPE, <Tx<u32> as Facet>::SHAPE);
612
613        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
614            generate_service(&service);
615        }));
616        let message = panic_message(result.expect_err("generator should reject channel returns"));
617
618        assert!(
619            message.contains("channels are not allowed in return types: BadSvc.bad"),
620            "unexpected panic message: {message}"
621        );
622    }
623
624    // r[verify rpc.channel.direct-args]
625    #[test]
626    fn generated_swift_rejects_nested_channel_args() {
627        let service = service_with_shapes(<(OuterChannels,) as Facet>::SHAPE, <() as Facet>::SHAPE);
628
629        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
630            generate_service(&service);
631        }));
632        let message = panic_message(result.expect_err("generator should reject nested channels"));
633
634        assert!(
635            message.contains("channels are only allowed as direct method arguments: BadSvc.bad"),
636            "unexpected panic message: {message}"
637        );
638    }
639
640    #[test]
641    // r[verify rpc.channel.no-collections]
642    fn generated_swift_rejects_channels_inside_collections() {
643        let service = service_with_shapes(<(Vec<Tx<u32>>,) as Facet>::SHAPE, <() as Facet>::SHAPE);
644
645        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
646            generate_service(&service);
647        }));
648        let message =
649            panic_message(result.expect_err("generator should reject collection channels"));
650
651        assert!(
652            message.contains("channels are not allowed inside collections: BadSvc.bad"),
653            "unexpected panic message: {message}"
654        );
655    }
656
657    #[cfg(unix)]
658    #[test]
659    // r[verify transport.fd.capability]
660    fn generated_swift_rejects_fd_capability_surfaces() {
661        let service = service_with_shapes(<() as Facet>::SHAPE, <FdBundle as Facet>::SHAPE);
662
663        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
664            generate_service(&service);
665        }));
666        let message =
667            panic_message(result.expect_err("generator should reject fd capability surfaces"));
668
669        assert!(
670            message.contains(
671                "Swift codegen does not support vox::Fd capabilities: BadSvc.bad return type"
672            ),
673            "unexpected panic message: {message}"
674        );
675    }
676}