Skip to main content

dsn/
lib.rs

1//!DSN (Data Source Name) parser
2//!
3//!DSN format:
4//!```text
5//!<driver>://<username>:<password>@<protocol>(<address>)/<database>?param1=value1&...&paramN=valueN
6//!```
7//!
8//!A DSN in its fullest form:
9//!
10//!```text
11//!driver://username:password@protocol(address)/dbname?param=value
12//!```
13//!
14//!The address changes depending on the protocol
15//!
16//!For `TCP/UDP` address have the form `host:port`, example:
17//!
18//!```text
19//!postgresql://user:pass@tcp(localhost:5432)/dbname
20//!```
21//!
22//!For protocol `unix` (Unix domain sockets) the address is the absolute path to the socket, for example:
23//!
24//!```text
25//!mysql://user@unix(/path/to/socket)/database
26//!```
27//!
28//!For protocol `file` (sqlite) use the absolute path as the address, example:
29//!
30//!```text
31//!sqlite://@file(/full/unix/path/to/file.db)
32//!```
33//!# percent-encode
34//!
35//!Percent-encode username and password with characters like `@`, for example if password is:
36//!
37//!```text
38//!sop@s
39//!
40//!!A4T@hh'cUj7LXXvk"
41//!```
42//!
43//!From the command line you can encode it with:
44//!
45//!```text
46//!echo -n "sop@s" | jq -s -R -r @uri
47//!```
48//!or
49//!
50//!```text
51//!echo -n "\!A4T@hh'cUj7LXXvk\"" | xxd -p |sed 's/../%&/g'
52//!```
53//!
54//!Then you can build the dsn:
55//!
56//!```text
57//!mysql://root:sop%40s@tcp(10.0.0.1:3306)/test
58//!```
59//!or
60//!
61//!```text
62//!mysql://root:%21%41%34%54%40%68%68%27%63%55%6a%37%4c%58%58%76%6b%22@tcp(10.0.0.1:3306)/test
63//!```
64
65use core::str::Utf8Error;
66use percent_encoding::percent_decode;
67use std::{collections::BTreeMap, error::Error, fmt, str::Chars};
68
69/// Errors that can occur during DSN parsing
70#[derive(Debug)]
71pub enum ParseError {
72    /// Driver name is invalid or missing
73    InvalidDriver,
74    /// Query parameters are malformed
75    InvalidParams,
76    /// File path is not absolute
77    InvalidPath,
78    /// Port number is invalid or out of range
79    InvalidPort,
80    /// Protocol is invalid or missing
81    InvalidProtocol,
82    /// Unix socket path is invalid
83    InvalidSocket,
84    /// Address is missing after protocol
85    MissingAddress,
86    /// Host is missing in address
87    MissingHost,
88    /// Protocol is missing
89    MissingProtocol,
90    /// Unix socket path is missing
91    MissingSocket,
92    /// UTF-8 decoding error
93    Utf8Error(Utf8Error),
94}
95
96impl From<Utf8Error> for ParseError {
97    fn from(err: Utf8Error) -> Self {
98        Self::Utf8Error(err)
99    }
100}
101
102impl fmt::Display for ParseError {
103    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104        match *self {
105            Self::InvalidDriver => write!(f, "invalid driver"),
106            Self::InvalidParams => write!(f, "invalid params"),
107            Self::InvalidPath => write!(f, "invalid absolute path"),
108            Self::InvalidPort => write!(f, "invalid port number"),
109            Self::InvalidProtocol => write!(f, "invalid protocol"),
110            Self::InvalidSocket => write!(f, "invalid socket"),
111            Self::MissingAddress => write!(f, "missing address"),
112            Self::MissingHost => write!(f, "missing host"),
113            Self::MissingProtocol => write!(f, "missing protocol"),
114            Self::MissingSocket => write!(f, "missing unix domain socket"),
115            Self::Utf8Error(ref err) => write!(f, "UTF-8 error: {err}"),
116        }
117    }
118}
119
120impl Error for ParseError {}
121
122impl fmt::Display for DSN {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
125
126        write!(f, "{}://", self.driver)?;
127
128        // Add credentials
129        if let Some(ref username) = self.username {
130            let encoded_user = utf8_percent_encode(username, NON_ALPHANUMERIC);
131            write!(f, "{encoded_user}")?;
132
133            if let Some(ref password) = self.password {
134                let encoded_pass = utf8_percent_encode(password, NON_ALPHANUMERIC);
135                write!(f, ":{encoded_pass}")?;
136            }
137            write!(f, "@")?;
138        }
139
140        // Add protocol and address
141        write!(f, "{}({})", self.protocol, self.address)?;
142
143        // Add database
144        if let Some(ref database) = self.database {
145            write!(f, "/{database}")?;
146        }
147
148        // Add parameters
149        if !self.params.is_empty() {
150            write!(f, "?")?;
151            let params: Vec<String> = self
152                .params
153                .iter()
154                .map(|(k, v)| format!("{k}={v}"))
155                .collect();
156            write!(f, "{}", params.join("&"))?;
157        }
158
159        Ok(())
160    }
161}
162
163/// Parsed Data Source Name (DSN) structure
164///
165/// DSN format: `driver://username:password@protocol(address)/dbname?param=value`
166///
167/// # Examples
168///
169/// ```
170/// use dsn::parse;
171///
172/// let dsn = parse("mysql://user:pass@tcp(localhost:3306)/mydb").unwrap();
173/// assert_eq!(dsn.driver, "mysql");
174/// assert_eq!(dsn.host.unwrap(), "localhost");
175/// assert_eq!(dsn.port.unwrap(), 3306);
176/// ```
177#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
178pub struct DSN {
179    /// Database driver name (e.g., "mysql", "postgres", "sqlite")
180    pub driver: String,
181    /// Optional username for authentication
182    pub username: Option<String>,
183    /// Optional password for authentication (percent-decoded)
184    pub password: Option<String>,
185    /// Connection protocol (e.g., "tcp", "unix", "file")
186    pub protocol: String,
187    /// Full address string (host:port, socket path, or file path)
188    pub address: String,
189    /// Hostname (only for TCP/UDP protocols)
190    pub host: Option<String>,
191    /// Port number (only for TCP/UDP protocols)
192    pub port: Option<u16>,
193    /// Database name
194    pub database: Option<String>,
195    /// Unix socket path (only for unix protocol)
196    pub socket: Option<String>,
197    /// Query string parameters as key-value pairs
198    pub params: BTreeMap<String, String>,
199}
200
201/// Parse a DSN string into a structured `DSN` object
202///
203/// This function parses a Data Source Name (DSN) string and extracts all components
204/// including driver, credentials, protocol, address, database name, and parameters.
205///
206/// # Arguments
207///
208/// * `input` - A DSN string in the format:
209///   `driver://username:password@protocol(address)/database?param=value`
210///
211/// # Returns
212///
213/// Returns a `Result` containing the parsed `DSN` struct on success, or a
214/// `ParseError` if the DSN string is malformed.
215///
216/// # Errors
217///
218/// Returns `ParseError` in the following cases:
219/// - `InvalidDriver` - Missing or invalid driver name
220/// - `InvalidProtocol` - Missing or invalid protocol
221/// - `InvalidSocket` - Unix socket path doesn't start with `/`
222/// - `InvalidPath` - File path is not absolute
223/// - `InvalidPort` - Port number is invalid or out of range (0-65535)
224/// - `MissingAddress` - Address is missing after protocol
225/// - `MissingHost` - Host is missing in TCP/UDP address
226/// - `InvalidParams` - Query parameters are malformed
227/// - `Utf8Error` - Percent-encoded credentials contain invalid UTF-8
228///
229/// # Examples
230///
231/// Basic TCP connection:
232/// ```
233/// use dsn::parse;
234///
235/// let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database?charset=utf8"#).unwrap();
236/// assert_eq!(dsn.driver, "mysql");
237/// assert_eq!(dsn.username.unwrap(), "user");
238/// assert_eq!(dsn.password.unwrap(), "o:o");
239/// assert_eq!(dsn.protocol, "tcp");
240/// assert_eq!(dsn.address, "localhost:3306");
241/// assert_eq!(dsn.host.unwrap(), "localhost");
242/// assert_eq!(dsn.port.unwrap(), 3306);
243/// assert_eq!(dsn.database.unwrap(), "database");
244/// assert_eq!(dsn.socket, None);
245/// assert!(!dsn.params.is_empty());
246/// assert_eq!(dsn.params.get("charset").unwrap(), "utf8");
247/// ```
248///
249/// Unix socket connection:
250/// ```
251/// use dsn::parse;
252///
253/// let dsn = parse(r"mysql://user@unix(/var/run/mysql.sock)/mydb").unwrap();
254/// assert_eq!(dsn.protocol, "unix");
255/// assert_eq!(dsn.socket.unwrap(), "/var/run/mysql.sock");
256/// ```
257pub fn parse(input: &str) -> Result<DSN, ParseError> {
258    // create an empty DSN
259    let mut dsn = DSN::default();
260
261    // create an iterator for input
262    let chars = &mut input.chars();
263
264    // <driver>://
265    dsn.driver = get_driver(chars)?;
266
267    // <username>:<password>@
268    let (user, pass) = get_username_password(chars)?;
269    if !user.is_empty() {
270        dsn.username = Some(user);
271    }
272    if !pass.is_empty() {
273        dsn.password = Some(pass);
274    }
275
276    // protocol(
277    dsn.protocol = get_protocol(chars)?;
278
279    // address) <host:port|/path/to/socket>
280    dsn.address = get_address(chars)?;
281
282    match dsn.protocol.as_str() {
283        "unix" => {
284            if !dsn.address.starts_with('/') {
285                return Err(ParseError::InvalidSocket);
286            }
287            dsn.socket = Some(dsn.address.clone());
288        }
289        "file" => {
290            if !dsn.address.starts_with('/') {
291                return Err(ParseError::InvalidPath);
292            }
293        }
294        _ => {
295            let (host, port) = get_host_port(&dsn.address)?;
296            dsn.host = Some(host);
297
298            if !port.is_empty() {
299                dsn.port = Some(port.parse::<u16>().map_err(|_| ParseError::InvalidPort)?);
300            }
301        }
302    }
303
304    // /<database>?
305    let database = get_database(chars);
306    if !database.is_empty() {
307        dsn.database = Some(database);
308    }
309
310    let params = chars.as_str();
311    if !params.is_empty() {
312        dsn.params = get_params(chars.as_str())?;
313    }
314
315    Ok(dsn)
316}
317
318/// Example:
319///
320///```
321///use dsn::parse;
322///
323///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
324///assert_eq!(dsn.driver, "mysql");
325///```
326fn get_driver(chars: &mut Chars) -> Result<String, ParseError> {
327    let mut driver = String::new();
328    while let Some(c) = chars.next() {
329        if c == ':' {
330            if chars.next() == Some('/') && chars.next() == Some('/') {
331                break;
332            }
333            return Err(ParseError::InvalidDriver);
334        }
335        driver.push(c);
336    }
337    Ok(driver)
338}
339
340/// Example:
341///
342///```
343///use dsn::parse;
344///
345///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
346///assert_eq!(dsn.username.unwrap(), "user");
347///assert_eq!(dsn.password.unwrap(), "o:o");
348///```
349fn get_username_password(chars: &mut Chars) -> Result<(String, String), ParseError> {
350    let mut username = String::new();
351    let mut password = String::new();
352    let mut has_password = true;
353
354    // username
355    for c in chars.by_ref() {
356        match c {
357            '@' => {
358                has_password = false;
359                break;
360            }
361            ':' => {
362                break;
363            }
364            _ => username.push(c),
365        }
366    }
367
368    username = percent_decode(username.as_bytes()).decode_utf8()?.into();
369
370    // password
371    if has_password {
372        for c in chars {
373            match c {
374                '@' => break,
375                _ => password.push(c),
376            }
377        }
378        password = percent_decode(password.as_bytes()).decode_utf8()?.into();
379    }
380
381    Ok((username, password))
382}
383
384/// Example:
385///
386///```
387///use dsn::parse;
388///
389///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
390///assert_eq!(dsn.protocol, "tcp");
391///```
392fn get_protocol(chars: &mut Chars) -> Result<String, ParseError> {
393    let mut protocol = String::new();
394    for c in chars {
395        match c {
396            '(' => {
397                if protocol.is_empty() {
398                    return Err(ParseError::MissingProtocol);
399                }
400                break;
401            }
402            _ => protocol.push(c),
403        }
404    }
405    Ok(protocol)
406}
407
408/// Example:
409///
410///```
411///use dsn::parse;
412///
413///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
414///assert_eq!(dsn.address, "localhost:3306");
415///```
416fn get_address(chars: &mut Chars) -> Result<String, ParseError> {
417    let mut address = String::new();
418    for c in chars {
419        match c {
420            ')' => {
421                if address.is_empty() {
422                    return Err(ParseError::MissingAddress);
423                }
424                break;
425            }
426            _ => address.push(c),
427        }
428    }
429    Ok(address)
430}
431
432/// Example:
433///
434///```
435///use dsn::parse;
436///
437///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
438///assert_eq!(dsn.host.unwrap(), "localhost");
439///assert_eq!(dsn.port.unwrap(), 3306);
440///```
441fn get_host_port(address: &str) -> Result<(String, String), ParseError> {
442    let mut host = String::new();
443    let mut chars = address.chars();
444
445    // host
446    for c in chars.by_ref() {
447        match c {
448            ':' => {
449                if host.is_empty() {
450                    return Err(ParseError::MissingHost);
451                }
452                break;
453            }
454            _ => host.push(c),
455        }
456    }
457
458    // port
459    let port = chars.as_str();
460
461    Ok((host, port.into()))
462}
463
464/// Example:
465///
466///```
467///use dsn::parse;
468///
469///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database"#).unwrap();
470///assert_eq!(dsn.database.unwrap(), "database");
471///```
472fn get_database(chars: &mut Chars) -> String {
473    let mut database = String::new();
474    for c in chars {
475        match c {
476            '/' if database.is_empty() => {}
477            '?' => break,
478            _ => database.push(c),
479        }
480    }
481    database
482}
483
484/// Example:
485///
486///```
487///use dsn::parse;
488///
489///let dsn = parse(r#"mysql://user:o%3Ao@tcp(localhost:3306)/database?param1=value1&param2=value2"#).unwrap();
490///assert!(!dsn.params.is_empty());
491///assert_eq!(dsn.params.get("param1"), Some(&String::from("value1")));
492///assert_eq!(dsn.params.get("param2").unwrap(), "value2");
493///assert_eq!(dsn.params.get("param3"), None);
494///```
495fn get_params(params_string: &str) -> Result<BTreeMap<String, String>, ParseError> {
496    params_string
497        .split('&')
498        .map(|kv| {
499            let mut parts = kv.splitn(2, '=');
500            match (parts.next(), parts.next()) {
501                (Some(key), Some(value)) => Ok((key.to_string(), value.to_string())),
502                _ => Err(ParseError::InvalidParams),
503            }
504        })
505        .collect()
506}
507
508impl DSN {
509    /// Create a new DSN builder
510    ///
511    /// # Examples
512    ///
513    /// ```
514    /// use dsn::DSN;
515    ///
516    /// let dsn = DSN::builder()
517    ///     .driver("mysql")
518    ///     .username("root")
519    ///     .password("secret")
520    ///     .host("localhost")
521    ///     .port(3306)
522    ///     .database("mydb")
523    ///     .build();
524    ///
525    /// assert_eq!(dsn.to_string(), "mysql://root:secret@tcp(localhost:3306)/mydb");
526    /// ```
527    #[must_use]
528    pub fn builder() -> DSNBuilder {
529        DSNBuilder::default()
530    }
531}
532
533/// Builder for constructing DSN strings
534///
535/// # Examples
536///
537/// ```
538/// use dsn::DSN;
539///
540/// // MySQL with TCP
541/// let mysql = DSN::builder()
542///     .driver("mysql")
543///     .username("root")
544///     .password("secret")
545///     .host("localhost")
546///     .port(3306)
547///     .database("mydb")
548///     .param("charset", "utf8mb4")
549///     .build();
550///
551/// // PostgreSQL
552/// let postgres = DSN::builder()
553///     .driver("postgres")
554///     .username("postgres")
555///     .password("pass")
556///     .host("db.example.com")
557///     .port(5432)
558///     .database("production")
559///     .param("sslmode", "require")
560///     .build();
561///
562/// // Redis
563/// let redis = DSN::builder()
564///     .driver("redis")
565///     .host("localhost")
566///     .port(6379)
567///     .database("0")
568///     .build();
569///
570/// // MySQL with Unix socket
571/// let mysql_sock = DSN::builder()
572///     .driver("mysql")
573///     .username("app")
574///     .socket("/var/run/mysqld/mysqld.sock")
575///     .database("appdb")
576///     .build();
577/// ```
578#[derive(Clone, Debug, Default)]
579pub struct DSNBuilder {
580    driver: String,
581    username: Option<String>,
582    password: Option<String>,
583    protocol: Option<String>,
584    host: Option<String>,
585    port: Option<u16>,
586    socket: Option<String>,
587    database: Option<String>,
588    params: BTreeMap<String, String>,
589}
590
591impl DSNBuilder {
592    /// Set the database driver (e.g., "mysql", "postgres", "redis")
593    #[must_use]
594    pub fn driver(mut self, driver: impl Into<String>) -> Self {
595        self.driver = driver.into();
596        self
597    }
598
599    /// Set the username for authentication
600    #[must_use]
601    pub fn username(mut self, username: impl Into<String>) -> Self {
602        self.username = Some(username.into());
603        self
604    }
605
606    /// Set the password for authentication
607    #[must_use]
608    pub fn password(mut self, password: impl Into<String>) -> Self {
609        self.password = Some(password.into());
610        self
611    }
612
613    /// Set the host for TCP connection
614    #[must_use]
615    pub fn host(mut self, host: impl Into<String>) -> Self {
616        self.host = Some(host.into());
617        self.protocol = Some("tcp".to_string());
618        self
619    }
620
621    /// Set the port for TCP connection
622    #[must_use]
623    pub const fn port(mut self, port: u16) -> Self {
624        self.port = Some(port);
625        self
626    }
627
628    /// Set a Unix socket path
629    #[must_use]
630    pub fn socket(mut self, socket: impl Into<String>) -> Self {
631        self.socket = Some(socket.into());
632        self.protocol = Some("unix".to_string());
633        self
634    }
635
636    /// Set the database name
637    #[must_use]
638    pub fn database(mut self, database: impl Into<String>) -> Self {
639        self.database = Some(database.into());
640        self
641    }
642
643    /// Add a query parameter
644    #[must_use]
645    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
646        self.params.insert(key.into(), value.into());
647        self
648    }
649
650    /// Build the DSN
651    #[must_use]
652    pub fn build(self) -> DSN {
653        let protocol = self.protocol.unwrap_or_else(|| "tcp".to_string());
654
655        let (address, host, socket) = if let Some(socket_path) = self.socket {
656            // Unix socket
657            (socket_path.clone(), None, Some(socket_path))
658        } else {
659            // TCP/UDP
660            let host_name = self.host.clone().unwrap_or_else(|| "localhost".to_string());
661            let addr = self
662                .port
663                .map_or_else(|| host_name.clone(), |port| format!("{host_name}:{port}"));
664            (addr, Some(host_name), None)
665        };
666
667        DSN {
668            driver: self.driver,
669            username: self.username,
670            password: self.password,
671            protocol,
672            address,
673            host,
674            port: self.port,
675            database: self.database,
676            socket,
677            params: self.params,
678        }
679    }
680}
681
682impl DSNBuilder {
683    /// Create a MySQL/MariaDB DSN builder with common defaults
684    ///
685    /// # Examples
686    ///
687    /// ```
688    /// use dsn::DSNBuilder;
689    ///
690    /// let dsn = DSNBuilder::mysql()
691    ///     .username("root")
692    ///     .password("secret")
693    ///     .host("localhost")
694    ///     .database("mydb")
695    ///     .build();
696    ///
697    /// assert_eq!(dsn.driver, "mysql");
698    /// assert_eq!(dsn.port, Some(3306));
699    /// ```
700    #[must_use]
701    pub fn mysql() -> Self {
702        Self {
703            driver: "mysql".to_string(),
704            protocol: Some("tcp".to_string()),
705            port: Some(3306),
706            ..Default::default()
707        }
708    }
709
710    /// Create a `PostgreSQL` DSN builder with common defaults
711    ///
712    /// # Examples
713    ///
714    /// ```
715    /// use dsn::DSNBuilder;
716    ///
717    /// let dsn = DSNBuilder::postgres()
718    ///     .username("postgres")
719    ///     .password("pass")
720    ///     .host("localhost")
721    ///     .database("mydb")
722    ///     .build();
723    ///
724    /// assert_eq!(dsn.driver, "postgres");
725    /// assert_eq!(dsn.port, Some(5432));
726    /// ```
727    #[must_use]
728    pub fn postgres() -> Self {
729        Self {
730            driver: "postgres".to_string(),
731            protocol: Some("tcp".to_string()),
732            port: Some(5432),
733            ..Default::default()
734        }
735    }
736
737    /// Create a Redis DSN builder with common defaults
738    ///
739    /// # Examples
740    ///
741    /// ```
742    /// use dsn::DSNBuilder;
743    ///
744    /// let dsn = DSNBuilder::redis()
745    ///     .host("localhost")
746    ///     .password("secret")
747    ///     .database("0")
748    ///     .build();
749    ///
750    /// assert_eq!(dsn.driver, "redis");
751    /// assert_eq!(dsn.port, Some(6379));
752    /// ```
753    #[must_use]
754    pub fn redis() -> Self {
755        Self {
756            driver: "redis".to_string(),
757            protocol: Some("tcp".to_string()),
758            port: Some(6379),
759            ..Default::default()
760        }
761    }
762
763    /// Create a `MariaDB` DSN builder (alias for `MySQL`)
764    ///
765    /// # Examples
766    ///
767    /// ```
768    /// use dsn::DSNBuilder;
769    ///
770    /// let dsn = DSNBuilder::mariadb()
771    ///     .username("root")
772    ///     .host("localhost")
773    ///     .database("mydb")
774    ///     .build();
775    ///
776    /// assert_eq!(dsn.driver, "mariadb");
777    /// assert_eq!(dsn.port, Some(3306));
778    /// ```
779    #[must_use]
780    pub fn mariadb() -> Self {
781        Self {
782            driver: "mariadb".to_string(),
783            protocol: Some("tcp".to_string()),
784            port: Some(3306),
785            ..Default::default()
786        }
787    }
788}
789
790#[cfg(test)]
791#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
792mod tests {
793    use super::{DSN, DSNBuilder, ParseError, parse};
794
795    #[test]
796    fn test_parse_password() {
797        let dsn = parse(r#"mysql://user:pas':"'sword44444@host:port/database"#).unwrap();
798        assert_eq!(dsn.password.unwrap(), r#"pas':"'sword44444"#);
799    }
800
801    #[test]
802    fn test_parse_driver() {
803        let dsn = parse(r"mysql://user:pass@host:port/database").unwrap();
804        assert_eq!(dsn.driver, "mysql");
805    }
806
807    #[test]
808    fn test_parse_driver_postgres() {
809        let dsn = parse(r"postgres://user:pass@host:port/database").unwrap();
810        assert_eq!(dsn.driver, "postgres");
811    }
812
813    #[test]
814    fn test_parse_username() {
815        let dsn = parse(r"mysql://user:pass@host:port/database").unwrap();
816        assert_eq!(dsn.username.unwrap(), "user");
817    }
818
819    #[test]
820    fn test_parse_protocol() {
821        let dsn = parse(r"mysql://user:pass@tcp(host:3306)/database").unwrap();
822        assert_eq!(dsn.protocol, "tcp");
823    }
824
825    #[test]
826    fn test_parse_address() {
827        let dsn = parse(r"mysql://user:pass@tcp(host:3306)/database").unwrap();
828        assert_eq!(dsn.address, "host:3306");
829    }
830
831    #[test]
832    fn test_parse_host() {
833        let dsn = parse(r"mysql://user:pass@tcp(host:3306)/database").unwrap();
834        assert_eq!(dsn.host.unwrap(), "host");
835    }
836
837    #[test]
838    fn test_parse_port() {
839        let dsn = parse(r"mysql://user:pass@tcp(host:3306)/database").unwrap();
840        assert_eq!(dsn.port.unwrap(), 3306);
841    }
842
843    #[test]
844    fn test_builder_mysql() {
845        let dsn = DSNBuilder::mysql()
846            .username("root")
847            .password("secret")
848            .host("localhost")
849            .database("mydb")
850            .param("charset", "utf8mb4")
851            .build();
852
853        assert_eq!(dsn.driver, "mysql");
854        assert_eq!(dsn.username.as_deref(), Some("root"));
855        assert_eq!(dsn.password.as_deref(), Some("secret"));
856        assert_eq!(dsn.host.as_deref(), Some("localhost"));
857        assert_eq!(dsn.port, Some(3306));
858        assert_eq!(dsn.database.as_deref(), Some("mydb"));
859        assert_eq!(dsn.params.get("charset"), Some(&"utf8mb4".to_string()));
860    }
861
862    #[test]
863    fn test_builder_postgres() {
864        let dsn = DSNBuilder::postgres()
865            .username("postgres")
866            .password("pass")
867            .host("db.example.com")
868            .database("production")
869            .param("sslmode", "require")
870            .build();
871
872        assert_eq!(dsn.driver, "postgres");
873        assert_eq!(dsn.port, Some(5432));
874        assert_eq!(dsn.params.get("sslmode"), Some(&"require".to_string()));
875    }
876
877    #[test]
878    fn test_builder_redis() {
879        let dsn = DSNBuilder::redis()
880            .host("localhost")
881            .password("secret")
882            .database("0")
883            .build();
884
885        assert_eq!(dsn.driver, "redis");
886        assert_eq!(dsn.port, Some(6379));
887        assert_eq!(dsn.database.as_deref(), Some("0"));
888    }
889
890    #[test]
891    fn test_builder_unix_socket() {
892        let dsn = DSNBuilder::mysql()
893            .username("app")
894            .socket("/var/run/mysqld/mysqld.sock")
895            .database("appdb")
896            .build();
897
898        assert_eq!(dsn.protocol, "unix");
899        assert_eq!(dsn.socket.as_deref(), Some("/var/run/mysqld/mysqld.sock"));
900        assert_eq!(dsn.address, "/var/run/mysqld/mysqld.sock");
901    }
902
903    #[test]
904    fn test_to_string_basic() {
905        let dsn = DSNBuilder::mysql()
906            .username("root")
907            .password("secret")
908            .host("localhost")
909            .database("mydb")
910            .build();
911
912        let dsn_string = dsn.to_string();
913        assert!(dsn_string.contains("mysql://"));
914        assert!(dsn_string.contains("root"));
915        assert!(dsn_string.contains("secret"));
916        assert!(dsn_string.contains("localhost:3306"));
917        assert!(dsn_string.contains("/mydb"));
918    }
919
920    #[test]
921    fn test_to_string_with_params() {
922        let dsn = DSNBuilder::postgres()
923            .username("user")
924            .password("pass")
925            .host("localhost")
926            .database("db")
927            .param("sslmode", "require")
928            .param("connect_timeout", "10")
929            .build();
930
931        let dsn_string = dsn.to_string();
932        assert!(dsn_string.contains('?'));
933        assert!(dsn_string.contains("sslmode=require"));
934        assert!(dsn_string.contains("connect_timeout=10"));
935    }
936
937    #[test]
938    fn test_to_string_special_chars() {
939        let dsn = DSNBuilder::mysql()
940            .username("user@host")
941            .password("p@ss:word!")
942            .host("localhost")
943            .database("mydb")
944            .build();
945
946        let dsn_string = dsn.to_string();
947        // Should be percent-encoded
948        assert!(dsn_string.contains("%40")); // @
949        assert!(!dsn_string.contains("user@host"));
950    }
951
952    #[test]
953    fn test_roundtrip() {
954        let original = "mysql://root:secret@tcp(localhost:3306)/mydb?charset=utf8mb4";
955        let parsed = parse(original).unwrap();
956        let rebuilt = parsed.to_string();
957
958        // Parse the rebuilt string to verify it's valid
959        let reparsed = parse(&rebuilt).unwrap();
960        assert_eq!(parsed.driver, reparsed.driver);
961        assert_eq!(parsed.username, reparsed.username);
962        assert_eq!(parsed.host, reparsed.host);
963        assert_eq!(parsed.port, reparsed.port);
964        assert_eq!(parsed.database, reparsed.database);
965    }
966
967    #[test]
968    fn test_builder_mariadb() {
969        let dsn = DSNBuilder::mariadb()
970            .username("root")
971            .host("localhost")
972            .database("mydb")
973            .build();
974
975        assert_eq!(dsn.driver, "mariadb");
976        assert_eq!(dsn.port, Some(3306));
977    }
978
979    #[test]
980    fn test_error_display() {
981        // Test all error display messages
982        assert_eq!(format!("{}", ParseError::InvalidDriver), "invalid driver");
983        assert_eq!(format!("{}", ParseError::InvalidParams), "invalid params");
984        assert_eq!(
985            format!("{}", ParseError::InvalidPath),
986            "invalid absolute path"
987        );
988        assert_eq!(
989            format!("{}", ParseError::InvalidPort),
990            "invalid port number"
991        );
992        assert_eq!(
993            format!("{}", ParseError::InvalidProtocol),
994            "invalid protocol"
995        );
996        assert_eq!(format!("{}", ParseError::InvalidSocket), "invalid socket");
997        assert_eq!(format!("{}", ParseError::MissingAddress), "missing address");
998        assert_eq!(format!("{}", ParseError::MissingHost), "missing host");
999        assert_eq!(
1000            format!("{}", ParseError::MissingProtocol),
1001            "missing protocol"
1002        );
1003        assert_eq!(
1004            format!("{}", ParseError::MissingSocket),
1005            "missing unix domain socket"
1006        );
1007    }
1008
1009    #[test]
1010    #[allow(invalid_from_utf8)]
1011    fn test_utf8_error_from() {
1012        // Test Utf8Error conversion
1013        let bad_bytes: &[u8] = &[0xFF, 0xFF];
1014        let utf8_err = std::str::from_utf8(bad_bytes).unwrap_err();
1015        let parse_err = ParseError::from(utf8_err);
1016        match parse_err {
1017            ParseError::Utf8Error(_) => {
1018                assert!(format!("{parse_err}").contains("UTF-8 error"));
1019            }
1020            _ => panic!("Expected Utf8Error variant"),
1021        }
1022    }
1023
1024    #[test]
1025    fn test_to_string_no_credentials() {
1026        // Test DSN without username/password
1027        let dsn = DSNBuilder::mysql().host("localhost").database("db").build();
1028
1029        let dsn_string = dsn.to_string();
1030        assert!(dsn_string.contains("mysql://"));
1031        assert!(!dsn_string.contains('@')); // No @ if no credentials
1032        assert!(dsn_string.contains("tcp(localhost:3306)"));
1033    }
1034
1035    #[test]
1036    fn test_to_string_no_database() {
1037        // Test DSN without database
1038        let dsn = DSNBuilder::mysql()
1039            .username("root")
1040            .password("pass")
1041            .host("localhost")
1042            .build();
1043
1044        let dsn_string = dsn.to_string();
1045        assert!(dsn_string.contains("mysql://"));
1046        assert!(dsn_string.ends_with("tcp(localhost:3306)")); // No trailing /
1047    }
1048
1049    #[test]
1050    fn test_to_string_username_only() {
1051        // Test DSN with username but no password
1052        let dsn = DSNBuilder::mysql()
1053            .username("root")
1054            .host("localhost")
1055            .database("db")
1056            .build();
1057
1058        let dsn_string = dsn.to_string();
1059        assert!(dsn_string.contains("mysql://root@"));
1060        assert!(!dsn_string.contains(":@")); // No colon before @
1061    }
1062
1063    #[test]
1064    fn test_builder_default() {
1065        // Test default builder
1066        let dsn = DSNBuilder::default()
1067            .driver("custom")
1068            .host("localhost")
1069            .port(9999)
1070            .build();
1071
1072        assert_eq!(dsn.driver, "custom");
1073        assert_eq!(dsn.port, Some(9999));
1074    }
1075
1076    #[test]
1077    fn test_builder_const_port() {
1078        // Test const port function
1079        let dsn = DSNBuilder::mysql().port(3307).host("localhost").build();
1080
1081        assert_eq!(dsn.port, Some(3307));
1082    }
1083
1084    #[test]
1085    fn test_parse_errors() {
1086        // Test various parse errors
1087        assert!(parse("mysql://user@tcp(host:99999)/db").is_err()); // Port out of range
1088        assert!(parse("mysql://user@unix(relative/path)/db").is_err()); // Unix socket must be absolute
1089        assert!(parse("mysql://user@file(relative/path)/db").is_err()); // File path must be absolute
1090        assert!(parse("mysql://user@tcp()/db").is_err()); // Missing address
1091        assert!(parse("mysql://user@tcp(:3306)/db").is_err()); // Missing host
1092        assert!(parse("mysql://user@tcp(host:port)/db").is_err()); // Invalid port (not a number)
1093    }
1094
1095    #[test]
1096    fn test_parse_edge_cases() {
1097        // These should parse but have empty driver
1098        let dsn = parse("://user@tcp(host)/db").unwrap();
1099        assert_eq!(dsn.driver, "");
1100
1101        // Test protocol variations work
1102        let dsn = parse("mysql://user@udp(host:9999)/db").unwrap();
1103        assert_eq!(dsn.protocol, "udp");
1104    }
1105
1106    #[test]
1107    fn test_parse_missing_protocol() {
1108        // Test missing protocol before (
1109        assert!(parse("mysql://user@(host)/db").is_err());
1110    }
1111
1112    #[test]
1113    fn test_dsn_builder_method() {
1114        // Test DSN::builder() method
1115        let dsn = DSN::builder().driver("mysql").host("localhost").build();
1116
1117        assert_eq!(dsn.driver, "mysql");
1118    }
1119
1120    #[test]
1121    fn test_dsn_clone() {
1122        let original = parse("mysql://user:pass@tcp(localhost:3306)/mydb?charset=utf8").unwrap();
1123        let cloned = original.clone();
1124
1125        assert_eq!(original.driver, cloned.driver);
1126        assert_eq!(original.username, cloned.username);
1127        assert_eq!(original.password, cloned.password);
1128        assert_eq!(original.protocol, cloned.protocol);
1129        assert_eq!(original.address, cloned.address);
1130        assert_eq!(original.host, cloned.host);
1131        assert_eq!(original.port, cloned.port);
1132        assert_eq!(original.database, cloned.database);
1133        assert_eq!(original.params, cloned.params);
1134    }
1135
1136    #[test]
1137    fn test_dsn_eq() {
1138        let dsn1 = parse("mysql://user:pass@tcp(localhost:3306)/mydb").unwrap();
1139        let dsn2 = parse("mysql://user:pass@tcp(localhost:3306)/mydb").unwrap();
1140        let dsn3 = parse("mysql://user:pass@tcp(localhost:3307)/mydb").unwrap();
1141
1142        assert_eq!(dsn1, dsn2);
1143        assert_ne!(dsn1, dsn3);
1144    }
1145
1146    #[test]
1147    fn test_dsn_hash() {
1148        use std::collections::HashSet;
1149
1150        let dsn1 = parse("mysql://user:pass@tcp(localhost:3306)/mydb").unwrap();
1151        let dsn2 = parse("mysql://user:pass@tcp(localhost:3306)/mydb").unwrap();
1152
1153        let mut set = HashSet::new();
1154        set.insert(dsn1.clone());
1155        set.insert(dsn2);
1156
1157        assert_eq!(set.len(), 1);
1158        assert!(set.contains(&dsn1));
1159    }
1160
1161    #[test]
1162    fn test_dsn_builder_clone() {
1163        let builder1 = DSNBuilder::mysql().username("root").host("localhost");
1164
1165        let builder2 = builder1.clone().database("db1").build();
1166        let builder3 = builder1.database("db2").build();
1167
1168        assert_eq!(builder2.database.as_deref(), Some("db1"));
1169        assert_eq!(builder3.database.as_deref(), Some("db2"));
1170    }
1171}