sqlx-firebird 0.1.0-beta.1

sqlx firebird driver
Documentation
//
// Copyright © 2023, RedSoft
// License: MIT
//

use std::str::FromStr;

use rsfbclient::charset::Charset;
use rsfbclient::{Dialect, FbError};
use sqlx_core::error::Error;
use sqlx_core::percent_encoding::percent_decode_str;
use sqlx_core::Url;

use super::FbConnectOptions;

impl FbConnectOptions {
    pub(crate) fn parse_from_url(url: &Url) -> Result<Self, Error> {
        let settings = parse(url).map_err(Error::config)?;

        let mut options = FbConnectOptions::new();

        if let Some(user) = settings.user {
            options = options.username(user);
        }

        if let Some(pass) = settings.pass {
            options = options.password(pass);
        }

        if let Some(host) = settings.host {
            options = options.host(host);
        }

        if let Some(port) = settings.port {
            options = options.port(port);
        }

        options = options.database(settings.db_name);

        if let Some(charset) = settings.charset {
            options = options.charset(charset);
        }

        if let Some(dialect) = settings.dialect {
            options = options.dialect(dialect);
        }

        if let Some(stmt_cache_size) = settings.stmt_cache_size {
            options = options.stmt_cache_size(stmt_cache_size);
        }

        if let Some(role_name) = settings.role_name {
            options = options.role_name(role_name);
        }

        Ok(options)
    }
}

pub struct ConnStringSettings {
    pub user: Option<String>,
    pub pass: Option<String>,
    pub host: Option<String>,
    pub port: Option<u16>,
    pub db_name: String,
    pub charset: Option<Charset>,
    pub dialect: Option<Dialect>,
    pub stmt_cache_size: Option<usize>,
    pub role_name: Option<String>,
}

fn parse(url: &Url) -> Result<ConnStringSettings, FbError> {
    if url.scheme().to_lowercase() != "firebird" {
        return Err(FbError::from(
            "The string must start with the prefix 'firebird://'",
        ));
    }

    let user = match url.username() {
        "" => None,
        u => Some(percent_decode_str(u).decode_utf8()?.into_owned()),
    };

    let pass = match url.password() {
        Some(p) => Some(percent_decode_str(p).decode_utf8()?.into_owned()),
        _ => None,
    };

    let mut host = match url.host_str() {
        Some(h) => Some(percent_decode_str(h).decode_utf8()?.into_owned()),
        _ => None,
    };

    let port = url.port();

    let mut db_name = match url.path() {
        "" => None,
        db => {
            let db = percent_decode_str(db).decode_utf8()?.into_owned();
            if db.starts_with('/') && url.has_host() {
                Some(db.replacen('/', "", 1))
            } else {
                Some(db)
            }
        },
    };

    match (&host, &db_name) {
        // In the embedded case with a windows path,
        // the lib will return the drive in the host,
        // because of ':' char.
        //
        // Example: firebird://c:/a/b/c.fdb
        // We get:
        //  - host: c
        //  - port: None
        //  - db_name: a/b/c.fdb
        (Some(h), Some(db)) => {
            if h.len() == 1 && user.is_none() && pass.is_none() && port.is_none() {
                db_name = Some(format!("{}:/{}", h, db));
                host = None;
            }
        },
        // When we have an embedded path, but only
        // with the filename. In this cases, the lib
        // will return the db path in the host.
        //
        // Example: firebird://abc.fdb
        // We get:
        //  - host: abc.fdb
        //  - db_name: None
        (Some(h), None) => {
            if user.is_none() && pass.is_none() && port.is_none() {
                db_name = Some(h.to_string());
                host = None;
            }
        },
        _ => {},
    }

    let db_name = db_name.ok_or_else(|| FbError::from("The database name/path is required"))?;

    let mut dialect = None;
    let mut charset = None;
    let mut stmt_cache_size = None;
    let mut role_name = None;

    for (param, val) in url.query_pairs() {
        match param.to_string().as_str() {
            "dialect" => {
                dialect = match Dialect::from_str(&val) {
                    Ok(d) => Some(d),
                    _ => None,
                };
            },
            "charset" => {
                charset = match Charset::from_str(&val) {
                    Ok(d) => Some(d),
                    _ => None,
                };
            },
            "stmt_cache_size" => {
                stmt_cache_size = match val.parse::<usize>() {
                    Ok(v) => Some(v),
                    _ => None,
                };
            },
            "role_name" => {
                if val != "" {
                    role_name = Some(val.to_string());
                }
            },
            _ => {},
        }
    }

    Ok(ConnStringSettings {
        user,
        pass,
        host,
        port,
        db_name,
        charset,
        dialect,
        stmt_cache_size,
        role_name,
    })
}

impl FromStr for FbConnectOptions {
    type Err = Error;

    fn from_str(url: &str) -> Result<Self, Self::Err> {
        let url = Url::parse(url).map_err(Error::config)?;
        Self::parse_from_url(&url)
    }
}