Skip to content

Commit

Permalink
Merge pull request #91 from fernandobatels/connstr
Browse files Browse the repository at this point in the history
Connection string support
  • Loading branch information
fernandobatels authored Dec 4, 2020
2 parents 0749e8e + 953f893 commit 706e2ac
Show file tree
Hide file tree
Showing 13 changed files with 899 additions and 21 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ rsfbclient-core = { version = "0.12.0", path = "rsfbclient-core" }
rsfbclient-native = { version = "0.12.0", path = "rsfbclient-native", optional = true }
rsfbclient-rust = { version = "0.12.0", path = "rsfbclient-rust", optional = true }
rsfbclient-derive = { version = "0.12.0", path = "rsfbclient-derive" }
url = "2.2.0"
percent-encoding = "2.1.0"

[dev-dependencies]
rand = "0.7.3"
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ let mut conn = rsfbclient::builder_native()
.connect()?
```

You also can choose a string connection configuration
```rust
// Using the native Firebird client
rsfbclient::builder_native()
.from_string("firebird://SYSDBA:[email protected]:3050/awesome.fdb?charset=ascii")
// Or using the pure rust implementation
rsfbclient::builder_pure_rust()
.from_string("firebird://SYSDBA:[email protected]:3050/awesome.fdb?charset=ascii")
```

3. Now you can use the lib
```rust
let rows = conn.query_iter("select col_a, col_b, col_c from test", ())?;
Expand Down
40 changes: 40 additions & 0 deletions examples/string_conn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//!
//! Rust Firebird Client
//!
//! Example of string connection configuration
//!
//! How to use: `DATABASE_URL=firebird://SYSDBA:masterkey@localhost:3050/test.fdb?charset=ascii cargo run --example string_conn`
#![allow(unused_variables, unused_mut)]

use rsfbclient::{prelude::*, FbError};
use std::env;

fn main() -> Result<(), FbError> {
let string_conf =
env::var("DATABASE_URL").map_err(|e| FbError::from("DATABASE_URL env var is empty"))?;

#[cfg(feature = "native_client")]
let mut conn = rsfbclient::builder_native()
.from_string(&string_conf)?
.connect()?;

#[cfg(feature = "pure_rust")]
let mut conn = rsfbclient::builder_pure_rust()
.from_string(&string_conf)?
.connect()?;

let rows = conn.query_iter("SELECT a.RDB$RELATION_NAME FROM RDB$RELATIONS a WHERE COALESCE(RDB$SYSTEM_FLAG, 0) = 0 AND RDB$RELATION_TYPE = 0", ())?;

println!("-------------");
println!("Table name");
println!("-------------");

for row in rows {
let (r_name,): (String,) = row?;

println!("{:^10}", r_name);
}

Ok(())
}
60 changes: 57 additions & 3 deletions rsfbclient-core/src/charset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! [Reference](http://www.destructor.de/firebird/charsets.htm)
use encoding::{all, types::EncodingRef, DecoderTrap, EncoderTrap};
use std::{borrow::Cow, str};
use std::{borrow::Cow, fmt, str, str::FromStr};

use crate::FbError;

Expand Down Expand Up @@ -35,8 +35,7 @@ impl Charset {
.into()
})
} else {
String::from_utf8(bytes.into_owned())
.map_err(|e| format!("Found column with an invalid UTF-8 string: {}", e).into())
String::from_utf8(bytes.into_owned()).map_err(|e| e.into())
}
}

Expand Down Expand Up @@ -75,6 +74,61 @@ impl Clone for Charset {
}
}

impl FromStr for Charset {
type Err = FbError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s
.to_lowercase()
.trim()
.replace("_", "")
.replace("-", "")
.as_str()
{
"utf8" => Ok(UTF_8),
"iso88591" => Ok(ISO_8859_1),
"iso88592" => Ok(ISO_8859_2),
"iso88593" => Ok(ISO_8859_3),
"iso88594" => Ok(ISO_8859_4),
"iso88595" => Ok(ISO_8859_5),
"iso88596" => Ok(ISO_8859_6),
"iso88597" => Ok(ISO_8859_7),
"iso885913" => Ok(ISO_8859_13),
"win1250" => Ok(WIN_1250),
"win1251" => Ok(WIN_1251),
"win1252" => Ok(WIN_1252),
"win1253" => Ok(WIN_1253),
"win1254" => Ok(WIN_1254),
"win1256" => Ok(WIN_1256),
"win1257" => Ok(WIN_1257),
"win1258" => Ok(WIN_1258),
"ascii" => Ok(ASCII),
"koi8r" => Ok(KOI8_R),
"koi8u" => Ok(KOI8_U),
"eucjp" => Ok(EUC_JP),
"big52003" => Ok(BIG5_2003),
_ => Err(FbError::from(format!(
"'{}' doesn't represent any charset",
s
))),
}
}
}

impl fmt::Debug for Charset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Charset")
.field("on_firebird", &self.on_firebird)
.finish()
}
}

impl PartialEq for Charset {
fn eq(&self, other: &Self) -> bool {
self.on_firebird == other.on_firebird
}
}

/// The default charset. Works in most cases
pub const UTF_8: Charset = Charset {
on_firebird: "UTF8",
Expand Down
17 changes: 17 additions & 0 deletions rsfbclient-core/src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Traits to abstract over firebird client implementations
use num_enum::TryFromPrimitive;
use std::str::FromStr;

use crate::*;

Expand Down Expand Up @@ -134,6 +135,22 @@ pub enum Dialect {
D3 = 3,
}

impl FromStr for Dialect {
type Err = FbError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1" => Ok(Dialect::D1),
"2" => Ok(Dialect::D2),
"3" => Ok(Dialect::D3),
_ => Err(FbError::from(format!(
"'{}' doesn't represent any dialect",
s
))),
}
}
}

#[repr(u8)]
/// Transaction isolation level
pub enum TrIsolationLevel {
Expand Down
14 changes: 14 additions & 0 deletions rsfbclient-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Error type for the connection
use std::str::Utf8Error;
use std::string::FromUtf8Error;
use thiserror::Error;

use crate::SqlType;
Expand Down Expand Up @@ -28,6 +30,18 @@ impl From<&str> for FbError {
}
}

impl From<FromUtf8Error> for FbError {
fn from(e: FromUtf8Error) -> Self {
Self::Other(format!("Found column with an invalid UTF-8 string: {}", e))
}
}

impl From<Utf8Error> for FbError {
fn from(e: Utf8Error) -> Self {
Self::Other(format!("Found column with an invalid UTF-8 string: {}", e))
}
}

pub fn err_column_null(type_name: &str) -> FbError {
FbError::Other(format!(
"This is a null value. Use the Option<{}> to safe access this column and avoid errors",
Expand Down
1 change: 0 additions & 1 deletion rsfbclient-native/src/row.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ impl ColumnBuffer {
.collect::<Vec<u8>>();

String::from_utf8(bname)
.map_err(|_| FbError::from("Found a column name with an invalid utf-8 string"))
}?;

Ok(ColumnBuffer {
Expand Down
97 changes: 86 additions & 11 deletions src/connection/builders/builder_native.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use crate::connection::conn_string;
use std::marker::PhantomData;

#[doc(hidden)]
Expand All @@ -15,10 +16,18 @@ pub struct LinkageNotConfigured;
pub struct ConnTypeNotConfigured;
#[doc(hidden)]
#[derive(Clone)]
pub struct Embedded;
pub struct ConnEmbedded;
#[doc(hidden)]
#[derive(Clone)]
pub struct Remote;
pub struct ConnRemote;
#[doc(hidden)]
#[derive(Clone)]
pub struct ConnByString;
#[doc(hidden)]
#[cfg(feature = "linking")]
pub type DynByString = DynLink;
#[cfg(not(feature = "linking"))]
pub type DynByString = DynLoad;

//These traits are used to avoid duplicating some impl blocks
//while at the same time statically disallowing certain methods for
Expand All @@ -27,9 +36,11 @@ pub struct Remote;
#[doc(hidden)]
pub trait ConfiguredConnType: Send + Sync {}
#[doc(hidden)]
impl ConfiguredConnType for Embedded {}
impl ConfiguredConnType for ConnEmbedded {}
#[doc(hidden)]
impl ConfiguredConnType for ConnRemote {}
#[doc(hidden)]
impl ConfiguredConnType for Remote {}
impl ConfiguredConnType for ConnByString {}

#[doc(hidden)]
//note that there is also LinkageMarker implemented for DynLink and
Expand Down Expand Up @@ -87,9 +98,10 @@ where
type C = NativeFbClient<rsfbclient_native::DynLoad>;

fn new_instance(&self) -> Result<Self::C, FbError> {
//not ideal, but the types should
//guarantee this is ok
let path = self.lib_path.as_ref().unwrap();
let path = self
.lib_path
.as_ref()
.ok_or_else(|| FbError::from("The lib path is required to use the dynload loading"))?;

rsfbclient_native::DynLoad {
charset: self.charset.clone(),
Expand Down Expand Up @@ -157,7 +169,7 @@ where
self
}

/// SQL Dialect. Default: 3
/// Connection charset. Default: UTF-8
pub fn charset(&mut self, charset: Charset) -> &mut Self {
self.charset = charset;
self
Expand Down Expand Up @@ -204,7 +216,7 @@ impl Default for NativeConnectionBuilder<LinkageNotConfigured, ConnTypeNotConfig
}

//can only use these methods on a remote builder
impl<A> NativeConnectionBuilder<A, Remote> {
impl<A> NativeConnectionBuilder<A, ConnRemote> {
//private helper accessor for the Option<RemoteConfig> buried inside
//the configuration
fn get_initialized_remote(&mut self) -> &mut RemoteConfig {
Expand Down Expand Up @@ -236,7 +248,7 @@ impl<A> NativeConnectionBuilder<A, Remote> {
impl<A> NativeConnectionBuilder<A, ConnTypeNotConfigured> {
/// Configure the native client for remote connections.
/// This will allow configuration via the 'host', 'port' and 'pass' methods.
pub fn with_remote(mut self) -> NativeConnectionBuilder<A, Remote> {
pub fn with_remote(mut self) -> NativeConnectionBuilder<A, ConnRemote> {
let mut remote: RemoteConfig = Default::default();
remote.host = "localhost".to_string();
remote.port = 3050;
Expand All @@ -258,7 +270,7 @@ impl<A> NativeConnectionBuilder<A, ConnTypeNotConfigured> {
/// On firebird 3.0 and above this may be restricted via the `Providers`
/// config parameter of `firebird.conf` see official firebird documentation
/// for more information.
pub fn with_embedded(self) -> NativeConnectionBuilder<A, Embedded> {
pub fn with_embedded(self) -> NativeConnectionBuilder<A, ConnEmbedded> {
self.safe_transmute()
}
}
Expand Down Expand Up @@ -306,4 +318,67 @@ impl<A> NativeConnectionBuilder<LinkageNotConfigured, A> {
self.lib_path = Some(lib_path.into());
self.safe_transmute()
}

/// Setup the connection using the string
/// pattern.
///
/// Basic string syntax: `firebird://{user}:{pass}@{host}:{port}/{db_name}?charset={charset}&lib={fbclient}&dialect={dialect}`
///
/// Some considerations:
/// - If you not provide the host, we will consider this a embedded connection.
/// - The port, user and pass parameters only will be accepted if you provide the host.
/// - If you not provide the `lib={fbclient}`, we will consider this a dynamic linked connection. The default, by the way.
#[allow(clippy::wrong_self_convention)]
pub fn from_string(
self,
s_conn: &str,
) -> Result<NativeConnectionBuilder<DynByString, ConnByString>, FbError> {
let settings = conn_string::parse(s_conn)?;

let mut cb: NativeConnectionBuilder<DynByString, ConnRemote> = {
// When we have a host, we will consider
// this a remote connection
if let Some(host) = settings.host {
let mut cb = self.safe_transmute().with_remote();

cb.host(host);

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

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

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

cb
} else {
self.safe_transmute()
}
};

cb.db_name(settings.db_name);

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

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

if let Some(lib_path) = settings.lib_path {
cb.lib_path = Some(lib_path);
}

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

Ok(cb.safe_transmute())
}
}
Loading

0 comments on commit 706e2ac

Please sign in to comment.