Skip to main content

ts_gen/
external_map.rs

1//! External type mapping: resolve imported types to Rust paths.
2//!
3//! When an imported type can't be resolved by parsing its source file,
4//! the external map provides the Rust path to use instead.
5//!
6//! ## CLI format
7//!
8//! ```text
9//! --external "node:*=node_sys::*"              # wildcard module mapping
10//! --external "Blob=::web_sys::Blob"            # explicit type mapping
11//! --external "node:buffer=node_buffer_sys"     # specific module mapping
12//! ```
13//!
14//! Multiple mappings can be comma-separated or specified with multiple flags.
15
16use std::collections::HashMap;
17
18/// A resolved Rust path for an external type.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct RustPath {
21    /// The full Rust path (e.g. `::web_sys::Blob`, `node_sys::buffer::Blob`)
22    pub path: String,
23}
24
25/// Maps TypeScript module specifiers and type names to Rust paths.
26#[derive(Clone, Debug, Default)]
27pub struct ExternalMap {
28    /// Explicit type mappings: `"Blob" → "::web_sys::Blob"`
29    type_map: HashMap<String, String>,
30    /// Module mappings: `"node:buffer" → "node_buffer_sys"`
31    module_map: HashMap<String, String>,
32    /// Wildcard module mappings: `"node:" → "node_sys"`
33    /// Stored as (prefix, rust_crate) pairs.
34    wildcard_map: Vec<(String, String)>,
35}
36
37impl ExternalMap {
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    /// Parse a CLI external mapping string.
43    ///
44    /// Format: `"LHS=RHS"` where:
45    /// - `"Blob=::web_sys::Blob"` → explicit type map
46    /// - `"node:buffer=node_buffer_sys"` → module map
47    /// - `"node:*=node_sys::*"` → wildcard module map
48    pub fn add_mapping(&mut self, mapping: &str) {
49        let Some((lhs, rhs)) = mapping.split_once('=') else {
50            return;
51        };
52        let lhs = lhs.trim();
53        let rhs = rhs.trim();
54
55        if lhs.ends_with('*') && rhs.ends_with('*') {
56            // Wildcard: "node:*=node_sys::*"
57            let prefix = lhs.trim_end_matches('*');
58            let rust_prefix = rhs.trim_end_matches('*').trim_end_matches("::");
59            self.wildcard_map
60                .push((prefix.to_string(), rust_prefix.to_string()));
61            // Sort by prefix length descending so longer (more specific) prefixes match first
62            self.wildcard_map
63                .sort_by_key(|b| std::cmp::Reverse(b.0.len()));
64        } else if lhs.contains(':') || lhs.contains('/') {
65            // Module mapping: "node:buffer=node_buffer_sys"
66            self.module_map.insert(lhs.to_string(), rhs.to_string());
67        } else {
68            // Explicit type: "Blob=::web_sys::Blob"
69            self.type_map.insert(lhs.to_string(), rhs.to_string());
70        }
71    }
72
73    /// Parse multiple mappings from a comma-separated string.
74    pub fn add_mappings(&mut self, mappings: &str) {
75        for mapping in mappings.split(',') {
76            let mapping = mapping.trim();
77            if !mapping.is_empty() {
78                self.add_mapping(mapping);
79            }
80        }
81    }
82
83    /// Resolve a type name imported from a module to a Rust path.
84    ///
85    /// Returns `None` if no mapping exists (caller should fall back to JsValue).
86    pub fn resolve(&self, type_name: &str, from_module: &str) -> Option<RustPath> {
87        // 1. Explicit type map (highest priority)
88        if let Some(rust_path) = self.type_map.get(type_name) {
89            return Some(RustPath {
90                path: rust_path.clone(),
91            });
92        }
93
94        // 2. Specific module map
95        if let Some(rust_crate) = self.module_map.get(from_module) {
96            return Some(RustPath {
97                path: format!("{rust_crate}::{type_name}"),
98            });
99        }
100
101        // 3. Wildcard module map
102        for (prefix, rust_crate) in &self.wildcard_map {
103            if from_module.starts_with(prefix) {
104                let module_suffix = &from_module[prefix.len()..];
105                let rust_module = module_suffix.replace('/', "::");
106                if rust_module.is_empty() {
107                    return Some(RustPath {
108                        path: format!("{rust_crate}::{type_name}"),
109                    });
110                } else {
111                    return Some(RustPath {
112                        path: format!("{rust_crate}::{rust_module}::{type_name}"),
113                    });
114                }
115            }
116        }
117
118        None
119    }
120
121    /// Resolve a type name without a known module (explicit type map only).
122    pub fn resolve_type(&self, type_name: &str) -> Option<RustPath> {
123        self.type_map.get(type_name).map(|rust_path| RustPath {
124            path: rust_path.clone(),
125        })
126    }
127
128    /// Check if any mappings have been configured.
129    pub fn is_empty(&self) -> bool {
130        self.type_map.is_empty() && self.module_map.is_empty() && self.wildcard_map.is_empty()
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_explicit_type_mapping() {
140        let mut map = ExternalMap::new();
141        map.add_mapping("Blob=::web_sys::Blob");
142
143        let result = map.resolve("Blob", "node:buffer");
144        assert_eq!(result.unwrap().path, "::web_sys::Blob");
145
146        // Unknown type returns None
147        assert!(map.resolve("Unknown", "node:buffer").is_none());
148    }
149
150    #[test]
151    fn test_module_mapping() {
152        let mut map = ExternalMap::new();
153        map.add_mapping("node:buffer=node_buffer_sys");
154
155        let result = map.resolve("Blob", "node:buffer");
156        assert_eq!(result.unwrap().path, "node_buffer_sys::Blob");
157
158        // Different module not mapped
159        assert!(map.resolve("Foo", "node:http").is_none());
160    }
161
162    #[test]
163    fn test_wildcard_mapping() {
164        let mut map = ExternalMap::new();
165        map.add_mapping("node:*=node_sys::*");
166
167        let result = map.resolve("Blob", "node:buffer");
168        assert_eq!(result.unwrap().path, "node_sys::buffer::Blob");
169
170        let result2 = map.resolve("Server", "node:http");
171        assert_eq!(result2.unwrap().path, "node_sys::http::Server");
172    }
173
174    #[test]
175    fn test_explicit_overrides_wildcard() {
176        let mut map = ExternalMap::new();
177        map.add_mapping("node:*=node_sys::*");
178        map.add_mapping("Blob=::web_sys::Blob");
179
180        // Explicit wins
181        let result = map.resolve("Blob", "node:buffer");
182        assert_eq!(result.unwrap().path, "::web_sys::Blob");
183
184        // Wildcard for non-explicit types
185        let result2 = map.resolve("Buffer", "node:buffer");
186        assert_eq!(result2.unwrap().path, "node_sys::buffer::Buffer");
187    }
188
189    #[test]
190    fn test_comma_separated() {
191        let mut map = ExternalMap::new();
192        map.add_mappings("Blob=::web_sys::Blob, node:*=node_sys::*");
193
194        assert_eq!(
195            map.resolve("Blob", "node:buffer").unwrap().path,
196            "::web_sys::Blob"
197        );
198        assert_eq!(
199            map.resolve("Server", "node:http").unwrap().path,
200            "node_sys::http::Server"
201        );
202    }
203
204    #[test]
205    fn test_subpath_wildcard() {
206        let mut map = ExternalMap::new();
207        map.add_mapping("node:*=node_sys::*");
208
209        let result = map.resolve("ReadableStream", "node:stream/web");
210        assert_eq!(
211            result.unwrap().path,
212            "node_sys::stream::web::ReadableStream"
213        );
214    }
215}