@@ -1497,6 +1497,215 @@ client_constructor_body.Client = "Self { inner: ::demo::Client::new(api_key, bas
14971497 ) ;
14981498}
14991499
1500+ // ── first-class DTO call-site conversion tests ───────────────────────────────
1501+
1502+ /// When a method on an opaque client class takes a first-class Swift DTO as a
1503+ /// parameter, the generated call site must apply `.intoRust()` to convert the
1504+ /// Swift wrapper into the `RustBridge.T` raw type that the bridge function
1505+ /// expects. Without this conversion the Swift compiler rejects the call with
1506+ /// "cannot convert value of type 'LiterLlm.T' to expected argument type
1507+ /// 'RustBridge.T'".
1508+ ///
1509+ /// The method signature must also carry `throws` because `intoRust()` is itself
1510+ /// a throwing function.
1511+ #[ test]
1512+ fn method_with_first_class_dto_param_calls_into_rust_at_call_site ( ) {
1513+ use alef_core:: ir:: { MethodDef , ReceiverKind } ;
1514+
1515+ // CreateImageRequest: first-class DTO with has_serde + has_default + string fields.
1516+ let mut request_type = make_type (
1517+ "CreateImageRequest" ,
1518+ vec ! [
1519+ make_field( "prompt" , TypeRef :: String , false ) ,
1520+ make_field( "model" , TypeRef :: Optional ( Box :: new( TypeRef :: String ) ) , true ) ,
1521+ ] ,
1522+ ) ;
1523+ request_type. has_serde = true ;
1524+ request_type. has_default = true ;
1525+
1526+ // ImageClient: opaque type with `generateImage(_ request: CreateImageRequest)`.
1527+ let client_type = TypeDef {
1528+ name : "ImageClient" . to_string ( ) ,
1529+ rust_path : "demo::ImageClient" . to_string ( ) ,
1530+ original_rust_path : String :: new ( ) ,
1531+ fields : vec ! [ ] ,
1532+ methods : vec ! [ MethodDef {
1533+ name: "generate_image" . to_string( ) ,
1534+ params: vec![ make_param( "request" , TypeRef :: Named ( "CreateImageRequest" . into( ) ) ) ] ,
1535+ return_type: TypeRef :: String ,
1536+ is_async: true ,
1537+ is_static: false ,
1538+ error_type: Some ( "DemoError" . to_string( ) ) ,
1539+ doc: String :: new( ) ,
1540+ sanitized: false ,
1541+ returns_ref: false ,
1542+ returns_cow: false ,
1543+ return_newtype_wrapper: None ,
1544+ receiver: Some ( ReceiverKind :: Ref ) ,
1545+ trait_source: None ,
1546+ has_default_impl: false ,
1547+ binding_excluded: false ,
1548+ binding_exclusion_reason: None ,
1549+ } ] ,
1550+ is_opaque : true ,
1551+ is_clone : false ,
1552+ is_copy : false ,
1553+ is_trait : false ,
1554+ has_default : false ,
1555+ has_stripped_cfg_fields : false ,
1556+ is_return_type : false ,
1557+ serde_rename_all : None ,
1558+ has_serde : false ,
1559+ super_traits : vec ! [ ] ,
1560+ doc : String :: new ( ) ,
1561+ cfg : None ,
1562+ binding_excluded : false ,
1563+ binding_exclusion_reason : None ,
1564+ } ;
1565+
1566+ let api = ApiSurface {
1567+ crate_name : "demo" . into ( ) ,
1568+ version : "0.1.0" . into ( ) ,
1569+ types : vec ! [ client_type, request_type] ,
1570+ functions : vec ! [ ] ,
1571+ enums : vec ! [ ] ,
1572+ errors : vec ! [ ] ,
1573+ excluded_type_paths : :: std:: collections:: HashMap :: new ( ) ,
1574+ } ;
1575+
1576+ let toml = r#"
1577+ [workspace]
1578+ languages = ["swift"]
1579+
1580+ [[crates]]
1581+ name = "demo"
1582+ sources = ["src/lib.rs"]
1583+
1584+ [crates.swift]
1585+ client_constructor_body.ImageClient = "Self { inner: ::demo::ImageClient::new(api_key, base_url) }"
1586+ "# ;
1587+ let cfg: alef_core:: config:: new_config:: NewAlefConfig = toml:: from_str ( toml) . expect ( "test config must parse" ) ;
1588+ let config = cfg. resolve ( ) . expect ( "test config must resolve" ) . remove ( 0 ) ;
1589+ let files = SwiftBackend . generate_bindings ( & api, & config) . unwrap ( ) ;
1590+ let swift = files
1591+ . iter ( )
1592+ . find ( |f| f. path . to_string_lossy ( ) . ends_with ( ".swift" ) )
1593+ . unwrap ( ) ;
1594+
1595+ // The call site must apply `.intoRust()` to convert the Swift DTO wrapper
1596+ // into the RustBridge raw type.
1597+ assert ! (
1598+ swift. content. contains( "try request.intoRust()" ) ,
1599+ "call site must apply try request.intoRust() for first-class DTO params; got:\n {}" ,
1600+ swift. content
1601+ ) ;
1602+ // The method signature must carry `throws` (both from error_type and intoRust).
1603+ assert ! (
1604+ swift. content. contains( "public func generateImage(_ request: CreateImageRequest) async throws -> String" ) ,
1605+ "method signature must include throws when param is a first-class DTO; got:\n {}" ,
1606+ swift. content
1607+ ) ;
1608+ // Must NOT pass the raw Swift wrapper directly to the bridge.
1609+ assert ! (
1610+ !swift. content. contains( ", request)" ) ,
1611+ "must not forward the Swift wrapper directly to the bridge without .intoRust(); got:\n {}" ,
1612+ swift. content
1613+ ) ;
1614+ }
1615+
1616+ /// When only DTO params are present (no `error_type`), the method must still
1617+ /// emit `throws` on its signature because `intoRust()` is throwing.
1618+ #[ test]
1619+ fn method_with_dto_param_only_adds_throws_even_without_error_type ( ) {
1620+ use alef_core:: ir:: { MethodDef , ReceiverKind } ;
1621+
1622+ let mut req_type = make_type (
1623+ "SpeechRequest" ,
1624+ vec ! [ make_field( "text" , TypeRef :: String , false ) ] ,
1625+ ) ;
1626+ req_type. has_serde = true ;
1627+ req_type. has_default = true ;
1628+
1629+ let client_type = TypeDef {
1630+ name : "SpeechClient" . to_string ( ) ,
1631+ rust_path : "demo::SpeechClient" . to_string ( ) ,
1632+ original_rust_path : String :: new ( ) ,
1633+ fields : vec ! [ ] ,
1634+ methods : vec ! [ MethodDef {
1635+ name: "create_speech" . to_string( ) ,
1636+ params: vec![ make_param( "req" , TypeRef :: Named ( "SpeechRequest" . into( ) ) ) ] ,
1637+ return_type: TypeRef :: Bytes ,
1638+ is_async: false ,
1639+ is_static: false ,
1640+ error_type: None , // no error_type — throws must still come from intoRust()
1641+ doc: String :: new( ) ,
1642+ sanitized: false ,
1643+ returns_ref: false ,
1644+ returns_cow: false ,
1645+ return_newtype_wrapper: None ,
1646+ receiver: Some ( ReceiverKind :: Ref ) ,
1647+ trait_source: None ,
1648+ has_default_impl: false ,
1649+ binding_excluded: false ,
1650+ binding_exclusion_reason: None ,
1651+ } ] ,
1652+ is_opaque : true ,
1653+ is_clone : false ,
1654+ is_copy : false ,
1655+ is_trait : false ,
1656+ has_default : false ,
1657+ has_stripped_cfg_fields : false ,
1658+ is_return_type : false ,
1659+ serde_rename_all : None ,
1660+ has_serde : false ,
1661+ super_traits : vec ! [ ] ,
1662+ doc : String :: new ( ) ,
1663+ cfg : None ,
1664+ binding_excluded : false ,
1665+ binding_exclusion_reason : None ,
1666+ } ;
1667+
1668+ let api = ApiSurface {
1669+ crate_name : "demo" . into ( ) ,
1670+ version : "0.1.0" . into ( ) ,
1671+ types : vec ! [ client_type, req_type] ,
1672+ functions : vec ! [ ] ,
1673+ enums : vec ! [ ] ,
1674+ errors : vec ! [ ] ,
1675+ excluded_type_paths : :: std:: collections:: HashMap :: new ( ) ,
1676+ } ;
1677+
1678+ let toml = r#"
1679+ [workspace]
1680+ languages = ["swift"]
1681+
1682+ [[crates]]
1683+ name = "demo"
1684+ sources = ["src/lib.rs"]
1685+
1686+ [crates.swift]
1687+ client_constructor_body.SpeechClient = "Self { inner: ::demo::SpeechClient::new(api_key, base_url) }"
1688+ "# ;
1689+ let cfg: alef_core:: config:: new_config:: NewAlefConfig = toml:: from_str ( toml) . expect ( "test config must parse" ) ;
1690+ let config = cfg. resolve ( ) . expect ( "test config must resolve" ) . remove ( 0 ) ;
1691+ let files = SwiftBackend . generate_bindings ( & api, & config) . unwrap ( ) ;
1692+ let swift = files
1693+ . iter ( )
1694+ . find ( |f| f. path . to_string_lossy ( ) . ends_with ( ".swift" ) )
1695+ . unwrap ( ) ;
1696+
1697+ assert ! (
1698+ swift. content. contains( "public func createSpeech(_ req: SpeechRequest) throws" ) ,
1699+ "method with only DTO params must still emit throws; got:\n {}" ,
1700+ swift. content
1701+ ) ;
1702+ assert ! (
1703+ swift. content. contains( "try req.intoRust()" ) ,
1704+ "call site must apply try req.intoRust(); got:\n {}" ,
1705+ swift. content
1706+ ) ;
1707+ }
1708+
15001709/// First-class struct fields must emit their rustdoc as `///` lines immediately
15011710/// above the `public let` declaration, mirroring how type/enum docs are surfaced.
15021711#[ test]
0 commit comments