diff --git a/.travis.yml b/.travis.yml index 3bb7065..7624ef7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,12 +36,13 @@ script: - cargo test --features trace - cargo test --features chrono - cargo test --features serde_json + - cargo test --features url - cargo test --features bundled - cargo test --features sqlcipher - cargo test --features i128_blob - cargo test --features "unlock_notify bundled" - cargo test --features "array bundled csvtab vtab" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab buildtime_bindgen" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab bundled" - - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace vtab bundled buildtime_bindgen" + - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url vtab" + - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url vtab buildtime_bindgen" + - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url vtab bundled" + - cargo test --features "backup blob chrono csvtab functions hooks limits load_extension serde_json trace url vtab bundled buildtime_bindgen" diff --git a/Cargo.toml b/Cargo.toml index 861c0f7..38bd0c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ lru-cache = "0.1" chrono = { version = "0.4", optional = true } serde_json = { version = "1.0", optional = true } csv = { version = "1.0", optional = true } +url = { version = "1.7", optional = true } lazy_static = { version = "1.0", optional = true } byteorder = { version = "1.2", features = ["i128"], optional = true } fallible-streaming-iterator = { version = "0.1", optional = true } @@ -81,7 +82,7 @@ name = "deny_single_threaded_sqlite_config" name = "vtab" [package.metadata.docs.rs] -features = [ "backup", "blob", "chrono", "functions", "limits", "load_extension", "serde_json", "trace", "vtab" ] +features = [ "backup", "blob", "chrono", "functions", "limits", "load_extension", "serde_json", "trace", "url", "vtab" ] all-features = false no-default-features = true default-target = "x86_64-unknown-linux-gnu" diff --git a/README.md b/README.md index 268f23f..e31a692 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ features](https://doc.rust-lang.org/cargo/reference/manifest.html#the-features-s * `serde_json` implements [`FromSql`](https://docs.rs/rusqlite/0.16.0/rusqlite/types/trait.FromSql.html) and [`ToSql`](https://docs.rs/rusqlite/0.16.0/rusqlite/types/trait.ToSql.html) for the `Value` type from the [`serde_json` crate](https://crates.io/crates/serde_json). +* `url` implements [`FromSql`](https://docs.rs/rusqlite/0.16.0/rusqlite/types/trait.FromSql.html) + and [`ToSql`](https://docs.rs/rusqlite/0.16.0/rusqlite/types/trait.ToSql.html) for the + `Url` type from the [`url` crate](https://crates.io/crates/url). * `bundled` uses a bundled version of sqlite3. This is a good option for cases where linking to sqlite3 is complicated, such as Windows. * `sqlcipher` looks for the SQLCipher library to link against instead of SQLite. This feature is mutually exclusive with `bundled`. * `hooks` for [Commit, Rollback](http://sqlite.org/c3ref/commit_hook.html) and [Data Change](http://sqlite.org/c3ref/update_hook.html) notification callbacks. diff --git a/src/types/mod.rs b/src/types/mod.rs index 48aa60a..34db37a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -66,6 +66,8 @@ mod from_sql; mod serde_json; mod time; mod to_sql; +#[cfg(feature = "url")] +mod url; mod value; mod value_ref; diff --git a/src/types/url.rs b/src/types/url.rs new file mode 100644 index 0000000..3028a04 --- /dev/null +++ b/src/types/url.rs @@ -0,0 +1,83 @@ +//! `ToSql` and `FromSql` implementation for [`url::Url`]. +use url::Url; +use crate::Result; +use crate::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef}; + +/// Serialize `Url` to text. +impl ToSql for Url { + fn to_sql(&self) -> Result> { + Ok(ToSqlOutput::from(self.as_str())) + } +} + +/// Deserialize text to `Url`. +impl FromSql for Url { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + match value { + ValueRef::Text(s) => Url::parse(s), + _ => return Err(FromSqlError::InvalidType), + } + .map_err(|err| FromSqlError::Other(Box::new(err))) + } +} + +#[cfg(test)] +mod test { + use url::{Url, ParseError}; + use crate::{Connection, params, Error, Result}; + + fn checked_memory_handle() -> Connection { + let db = Connection::open_in_memory().unwrap(); + db.execute_batch("CREATE TABLE urls (i INTEGER, v TEXT)") + .unwrap(); + db + } + + fn get_url(db: &Connection, id: i64) -> Result { + db.query_row( + "SELECT v FROM urls WHERE i = ?", + params![id], + |r| r.get(0), + ) + } + + #[test] + fn test_sql_url() { + let db = &checked_memory_handle(); + + let url0 = Url::parse("http://www.example1.com").unwrap(); + let url1 = Url::parse("http://www.example1.com/👌").unwrap(); + let url2 = "http://www.example2.com/👌"; + + db.execute( + "INSERT INTO urls (i, v) VALUES (0, ?), (1, ?), (2, ?), (3, ?)", + // also insert a non-hex encoded url (which might be present if it was + // inserted separately) + params![url0, url1, url2, "illegal"], + ) + .unwrap(); + + assert_eq!(get_url(db, 0).unwrap(), url0); + + assert_eq!(get_url(db, 1).unwrap(), url1); + + // Should successfully read it, even though it wasn't inserted as an + // escaped url. + let out_url2: Url = get_url(db, 2).unwrap(); + assert_eq!(out_url2, Url::parse(url2).unwrap()); + + // Make sure the conversion error comes through correctly. + let err = get_url(db, 3).unwrap_err(); + match err { + Error::FromSqlConversionFailure(_, _, e) => { + assert_eq!( + *e.downcast::().unwrap(), + ParseError::RelativeUrlWithoutBase, + ); + } + e => { + panic!("Expected conversion failure, got {}", e); + } + } + } +}