Compare commits

...

1 Commits

Author SHA1 Message Date
Floris Bruynooghe
9009cfcb64 Create configuration items as structs
This allows to have strongly typed config items.
2019-12-08 17:04:51 +01:00
3 changed files with 255 additions and 1 deletions

12
Cargo.lock generated
View File

@@ -620,6 +620,7 @@ dependencies = [
"chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)",
"debug_stub_derive 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"deltachat_derive 0.1.0",
"derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"encoded-words 0.1.0 (git+https://github.com/async-email/encoded-words)",
"escaper 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -716,6 +717,16 @@ dependencies = [
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_deref"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.13 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.44 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derive_more"
version = "0.14.1"
@@ -3452,6 +3463,7 @@ dependencies = [
"checksum deltachat-provider-database 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "814dba060d9fdc7a989fccdc4810ada9d1c7a1f09131c78e42412bc6c634b93b"
"checksum derive_builder 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ac53fa6a3cda160df823a9346442525dcaf1e171999a1cf23e67067e4fd64d4"
"checksum derive_builder_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0288a23da9333c246bb18c143426074a6ae96747995c5819d2947b64cd942b37"
"checksum derive_deref 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11554fdb0aa42363a442e0c4278f51c9621e20c1ce3bac51d79e60646f3b8b8f"
"checksum derive_more 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6d944ac6003ed268757ef1ee686753b57efc5fcf0ebe7b64c9fc81e7e32ff839"
"checksum des 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "74ba5f1b5aee9772379c2670ba81306e65a93c0ee3caade7a1d22b188d88a3af"
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"

View File

@@ -55,6 +55,7 @@ webpki-roots = "0.18.0"
webpki = "0.21.0"
mailparse = "0.10.1"
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
derive_deref = "1.1.0"
[dev-dependencies]
tempfile = "3.0"

View File

@@ -1,5 +1,6 @@
//! # Key-value configuration management
use derive_deref::{Deref, DerefMut};
use strum::{EnumProperty, IntoEnumIterator};
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
@@ -8,6 +9,7 @@ use crate::constants::DC_VERSION_STR;
use crate::context::Context;
use crate::dc_tools::*;
use crate::job::*;
use crate::sql;
use crate::stock::StockMessage;
/// The available configuration keys.
@@ -88,6 +90,164 @@ pub enum Config {
SysConfigKeys,
}
/// A trait defining a [Context]-wide configuration item.
///
/// Configuration items are stored in database of a [Context]. Most
/// configuration items are newtypes which implement [std::ops::Deref]
/// and [std::ops::DerefMut] though this is not required. However
/// what **is required** for the struct to implement
/// [rusqlite::ToSql] and [rusqlite::types::FromSql].
pub trait ConfigItem {
/// Returns the name of the key used in the SQLite database.
fn keyname() -> &'static str;
/// Loads the configuration item from the [Context]'s database.
///
/// If the configuration item is not available in the database,
/// `None` will be returned.
fn load(context: &Context) -> Result<Option<Self>, sql::Error>
where
Self: std::marker::Sized + rusqlite::types::FromSql,
{
context
.sql
.query_row(
"SELECT value FROM config WHERE keyname=?;",
params!(Self::keyname()),
|row| row.get(0),
)
.or_else(|err| match err {
sql::Error::Sql(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
e => Err(e),
})
}
/// Stores the configuration item in the [Context]'s database.
fn store(&self, context: &Context) -> Result<(), sql::Error>
where
Self: rusqlite::ToSql,
{
if context.sql.exists(
"select value FROM config WHERE keyname=?;",
params!(Self::keyname()),
)? {
context.sql.execute(
"UPDATE config SET value=? WHERE keyname=?",
params![&self, Self::keyname()],
)?;
} else {
context.sql.execute(
"INSERT INTO config (keyname, value) VALUES (?, ?)",
params![Self::keyname(), &self],
)?;
}
Ok(())
}
/// Removes the configuration item from the [Context]'s database.
fn delete(context: &Context) -> Result<(), sql::Error> {
context
.sql
.execute(
"DELETE FROM config WHERE keyname=?",
params![Self::keyname()],
)
.and(Ok(()))
}
}
/// Configuration item: display address for this account.
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct Addr(pub String);
impl rusqlite::ToSql for Addr {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
self.0.to_sql()
}
}
impl rusqlite::types::FromSql for Addr {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
value.as_str().map(|v| Addr(v.to_string()))
}
}
impl ConfigItem for Addr {
fn keyname() -> &'static str {
"addr"
}
}
/// Configuration item:
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct MailServer(pub String);
impl rusqlite::ToSql for MailServer {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
self.0.to_sql()
}
}
impl rusqlite::types::FromSql for MailServer {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
value.as_str().map(|v| MailServer(v.to_string()))
}
}
impl ConfigItem for MailServer {
fn keyname() -> &'static str {
"mail_server"
}
}
/// Configuration item:
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct MailUser(pub String);
// XXX TODO
/// Configuration item: whether to watch the INBOX folder for changes.
#[derive(Debug, Clone, PartialEq, Eq, Deref, DerefMut)]
pub struct InboxWatch(pub bool);
impl Default for InboxWatch {
fn default() -> Self {
InboxWatch(true)
}
}
impl rusqlite::ToSql for InboxWatch {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
// Column affinity is "text" so gets stored as string by SQLite.
let obj = rusqlite::types::Value::Integer(self.0 as i64);
Ok(rusqlite::types::ToSqlOutput::Owned(obj))
}
}
impl rusqlite::types::FromSql for InboxWatch {
fn column_result(value: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
let str_to_int = |s: &str| {
s.parse::<i64>()
.map_err(|e| rusqlite::types::FromSqlError::Other(Box::new(e)))
};
let int_to_bool = |i| match i {
0 => Ok(false),
1 => Ok(true),
v => Err(rusqlite::types::FromSqlError::OutOfRange(v)),
};
value
.as_str()
.and_then(str_to_int)
.and_then(int_to_bool)
.map(InboxWatch)
}
}
impl ConfigItem for InboxWatch {
fn keyname() -> &'static str {
"inbox_watch"
}
}
impl Context {
/// Get a configuration key. Returns `None` if no value is set, and no default value found.
pub fn get_config(&self, key: Config) -> Option<String> {
@@ -175,11 +335,16 @@ fn get_config_keys_string() -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use std::str::FromStr;
use std::string::ToString;
use crate::test_utils::*;
use lazy_static::lazy_static;
lazy_static! {
static ref TC: TestContext = dummy_context();
}
#[test]
fn test_to_string() {
@@ -225,4 +390,80 @@ mod tests {
assert_eq!(avatar_cfg, avatar_src.to_str().map(|s| s.to_string()));
Ok(())
}
#[test]
fn test_inbox_watch() {
// Loading from context when it is not in the DB.
let val = InboxWatch::load(&TC.ctx).unwrap();
assert_eq!(val, None);
// Create in-memory from default.
let mut val = InboxWatch::default();
assert_eq!(*val, true);
// Assign using deref.
*val = false;
assert_eq!(*val, false);
// Construct newtype directly.
let val = InboxWatch(false);
assert_eq!(*val, false);
// Helper to query raw DB value.
let query_db_raw = || {
TC.ctx
.sql
.query_row(
"SELECT value FROM config WHERE KEYNAME=?",
params![InboxWatch::keyname()],
|row| row.get::<_, String>(0),
)
.unwrap()
};
// Save (non-default) value to the DB.
InboxWatch(false).store(&TC.ctx).unwrap();
assert_eq!(query_db_raw(), "0");
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
assert_eq!(val, InboxWatch(false));
// Save true (aka default) value to the DB.
InboxWatch(true).store(&TC.ctx).unwrap();
assert_eq!(query_db_raw(), "1");
let val = InboxWatch::load(&TC.ctx).unwrap().unwrap();
assert_eq!(val, InboxWatch(true));
// Delete the value from the DB.
InboxWatch::delete(&TC.ctx).unwrap();
assert!(!TC
.ctx
.sql
.exists(
"SELECT value FROM config WHERE KEYNAME=?",
params![InboxWatch::keyname()],
)
.unwrap());
let val = InboxWatch::load(&TC.ctx).unwrap();
assert_eq!(val, None);
}
#[test]
fn test_addr() {
// In-memory creation
let val = Addr("me@example.com".into());
assert_eq!(*val, "me@example.com");
// Load when DB is empty.
let val = Addr::load(&TC.ctx).unwrap();
assert_eq!(val, None);
// Store and load.
Addr("me@example.com".into()).store(&TC.ctx).unwrap();
let val = Addr::load(&TC.ctx).unwrap();
assert_eq!(val, Some(Addr("me@example.com".into())));
// Delete
Addr::delete(&TC.ctx).unwrap();
assert_eq!(Addr::load(&TC.ctx).unwrap(), None);
}
}