use crate::error::*;
use crate::schema::ADDRESS_COMMON_COLS;
use rusqlite::{Connection, Row, NO_PARAMS};
use serde::Serialize;
use serde_derive::*;
use sync_guid::Guid;
use types::Timestamp;
#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct NewAddressFields {
pub given_name: String,
#[serde(default)]
pub additional_name: String,
pub family_name: String,
#[serde(default)]
pub organization: String,
pub street_address: String,
#[serde(default)]
pub address_level3: String,
#[serde(default)]
pub address_level2: String,
#[serde(default)]
pub address_level1: String,
#[serde(default)]
pub postal_code: String,
#[serde(default)]
pub country: String,
#[serde(default)]
pub tel: String,
#[serde(default)]
pub email: String,
}
#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Default)]
pub struct Address {
pub guid: Guid,
pub fields: NewAddressFields,
#[serde(default)]
#[serde(rename = "timeCreated")]
pub time_created: Timestamp,
#[serde(default)]
#[serde(rename = "timeLastUsed")]
pub time_last_used: Timestamp,
#[serde(default)]
#[serde(rename = "timeLastModified")]
pub time_last_modified: Timestamp,
#[serde(default)]
#[serde(rename = "timesUsed")]
pub times_used: i64,
#[serde(default)]
#[serde(rename = "changeCounter")]
pub(crate) sync_change_counter: i64,
}
impl Address {
pub fn from_row(row: &Row<'_>) -> Result<Address, rusqlite::Error> {
let address_fields = NewAddressFields {
given_name: row.get("given_name")?,
additional_name: row.get("additional_name")?,
family_name: row.get("family_name")?,
organization: row.get("organization")?,
street_address: row.get("street_address")?,
address_level3: row.get("address_level3")?,
address_level2: row.get("address_level2")?,
address_level1: row.get("address_level1")?,
postal_code: row.get("postal_code")?,
country: row.get("country")?,
tel: row.get("tel")?,
email: row.get("email")?,
};
Ok(Address {
guid: Guid::from_string(row.get("guid")?),
fields: address_fields,
time_created: row.get("time_created")?,
time_last_used: row.get("time_last_used")?,
time_last_modified: row.get("time_last_modified")?,
times_used: row.get("times_used")?,
sync_change_counter: row.get("sync_change_counter")?,
})
}
}
#[allow(dead_code)]
pub fn add_address(conn: &mut Connection, new_address: NewAddressFields) -> Result<Address> {
let tx = conn.transaction()?;
let address = Address {
guid: Guid::random(),
fields: new_address,
time_created: Timestamp::now(),
time_last_used: Timestamp { 0: 0 },
time_last_modified: Timestamp::now(),
times_used: 0,
sync_change_counter: 1,
};
tx.execute_named(
&format!(
"INSERT OR IGNORE INTO addresses_data (
{common_cols}
) VALUES (
:guid,
:given_name,
:additional_name,
:family_name,
:organization,
:street_address,
:address_level3,
:address_level2,
:address_level1,
:postal_code,
:country,
:tel,
:email,
:time_created,
:time_last_used,
:time_last_modified,
:times_used,
:sync_change_counter
)",
common_cols = ADDRESS_COMMON_COLS
),
rusqlite::named_params! {
":guid": address.guid,
":given_name": address.fields.given_name,
":additional_name": address.fields.additional_name,
":family_name": address.fields.family_name,
":organization": address.fields.organization,
":street_address": address.fields.street_address,
":address_level3": address.fields.address_level3,
":address_level2": address.fields.address_level2,
":address_level1": address.fields.address_level1,
":postal_code": address.fields.postal_code,
":country": address.fields.country,
":tel": address.fields.tel,
":email": address.fields.email,
":time_created": address.time_created,
":time_last_used": address.time_last_used,
":time_last_modified": address.time_last_modified,
":times_used": address.times_used,
":sync_change_counter": address.sync_change_counter,
},
)?;
tx.commit()?;
Ok(address)
}
#[allow(dead_code)]
pub fn get_address(conn: &mut Connection, guid: &Guid) -> Result<Address> {
let tx = conn.transaction()?;
let sql = format!(
"SELECT
{common_cols}
FROM addresses_data
WHERE guid = :guid",
common_cols = ADDRESS_COMMON_COLS
);
let address = tx.query_row(&sql, &[guid.as_str()], |row| Ok(Address::from_row(row)?))?;
tx.commit()?;
Ok(address)
}
#[allow(dead_code)]
pub fn get_all_addresses(conn: &mut Connection) -> Result<Vec<Address>> {
let tx = conn.transaction()?;
let mut addresses = Vec::new();
let sql = format!(
"SELECT
{common_cols}
FROM addresses_data",
common_cols = ADDRESS_COMMON_COLS
);
{
let mut stmt = tx.prepare(&sql)?;
let addresses_iter = stmt.query_map(NO_PARAMS, |row| Ok(Address::from_row(row)?))?;
for address_result in addresses_iter {
addresses.push(address_result.expect("Should unwrap address"));
}
}
tx.commit()?;
Ok(addresses)
}
#[allow(dead_code)]
pub fn update_address(conn: &mut Connection, address: Address) -> Result<()> {
let tx = conn.transaction()?;
tx.execute_named(
"UPDATE addresses_data
SET given_name = :given_name,
additional_name = :additional_name,
family_name = :family_name,
organization = :organization,
street_address = :street_address,
address_level3 = :address_level3,
address_level2 = :address_level2,
address_level1 = :address_level1,
postal_code = :postal_code,
country = :country,
tel = :tel,
email = :email,
sync_change_counter = sync_change_counter + 1
WHERE guid = :guid",
rusqlite::named_params! {
":given_name": address.fields.given_name,
":additional_name": address.fields.additional_name,
":family_name": address.fields.family_name,
":organization": address.fields.organization,
":street_address": address.fields.street_address,
":address_level3": address.fields.address_level3,
":address_level2": address.fields.address_level2,
":address_level1": address.fields.address_level1,
":postal_code": address.fields.postal_code,
":country": address.fields.country,
":tel": address.fields.tel,
":email": address.fields.email,
":guid": address.guid,
},
)?;
tx.commit()?;
Ok(())
}
pub fn delete_address(conn: &mut Connection, guid: &Guid) -> Result<bool> {
let tx = conn.transaction()?;
let exists = tx.query_row(
"SELECT EXISTS (
SELECT 1
FROM addresses_data d
WHERE guid = :guid
AND NOT EXISTS (
SELECT 1
FROM addresses_tombstones t
WHERE d.guid = t.guid
)
)",
&[guid.as_str()],
|row| row.get(0),
)?;
if exists {
tx.execute_named(
"DELETE FROM addresses_data
WHERE guid = :guid",
rusqlite::named_params! {
":guid": guid.as_str(),
},
)?;
tx.execute_named(
"INSERT OR IGNORE INTO addresses_tombstones (
guid,
time_deleted
) VALUES (
:guid,
:time_deleted
)",
rusqlite::named_params! {
":guid": guid.as_str(),
":time_deleted": Timestamp::now(),
},
)?;
}
tx.commit()?;
Ok(exists)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::test::new_mem_db;
#[test]
fn test_address_create_and_read() {
let mut db = new_mem_db();
let saved_address = add_address(
&mut db,
NewAddressFields {
given_name: "jane".to_string(),
family_name: "doe".to_string(),
street_address: "123 Main Street".to_string(),
address_level2: "Seattle, WA".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
assert_ne!(Guid::default(), saved_address.guid);
assert_eq!(1, saved_address.sync_change_counter);
let retrieved_address = get_address(&mut db, &saved_address.guid)
.expect("should contain optional retrieved address");
assert_eq!(saved_address.guid, retrieved_address.guid);
assert_eq!(
saved_address.fields.given_name,
retrieved_address.fields.given_name
);
assert_eq!(
saved_address.fields.family_name,
retrieved_address.fields.family_name
);
assert_eq!(
saved_address.fields.street_address,
retrieved_address.fields.street_address
);
assert_eq!(
saved_address.fields.address_level2,
retrieved_address.fields.address_level2
);
assert_eq!(
saved_address.fields.country,
retrieved_address.fields.country
);
let delete_result = delete_address(&mut db, &saved_address.guid);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
assert!(get_address(&mut db, &saved_address.guid).is_err());
}
#[test]
fn test_address_read_all() {
let mut db = new_mem_db();
let saved_address = add_address(
&mut db,
NewAddressFields {
given_name: "jane".to_string(),
family_name: "doe".to_string(),
street_address: "123 Second Avenue".to_string(),
address_level2: "Chicago, IL".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
let saved_address2 = add_address(
&mut db,
NewAddressFields {
given_name: "john".to_string(),
family_name: "deer".to_string(),
street_address: "123 First Avenue".to_string(),
address_level2: "Los Angeles, CA".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
let saved_address3 = add_address(
&mut db,
NewAddressFields {
given_name: "abraham".to_string(),
family_name: "lincoln".to_string(),
street_address: "1600 Pennsylvania Ave NW".to_string(),
address_level2: "Washington, DC".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
let delete_result = delete_address(&mut db, &saved_address3.guid);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
let retrieved_addresses =
get_all_addresses(&mut db).expect("Should contain all saved addresses");
assert!(!retrieved_addresses.is_empty());
let expected_number_of_addresses = 2;
assert_eq!(expected_number_of_addresses, retrieved_addresses.len());
let retrieved_address_guids = vec![
retrieved_addresses[0].guid.as_str(),
retrieved_addresses[1].guid.as_str(),
];
assert!(retrieved_address_guids.contains(&saved_address.guid.as_str()));
assert!(retrieved_address_guids.contains(&saved_address2.guid.as_str()));
}
#[test]
fn test_address_update() {
let mut db = new_mem_db();
let saved_address = add_address(
&mut db,
NewAddressFields {
given_name: "john".to_string(),
family_name: "doe".to_string(),
street_address: "1300 Broadway".to_string(),
address_level2: "New York, NY".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
let expected_additional_name = "paul".to_string();
let update_result = update_address(
&mut db,
Address {
guid: saved_address.guid.clone(),
fields: NewAddressFields {
given_name: "john".to_string(),
additional_name: expected_additional_name.clone(),
family_name: "deer".to_string(),
street_address: "123 First Avenue".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
..Address::default()
},
);
assert!(update_result.is_ok());
let updated_address = get_address(&mut db, &saved_address.guid)
.expect("should contain optional updated address");
assert_eq!(saved_address.guid, updated_address.guid);
assert_eq!(
expected_additional_name,
updated_address.fields.additional_name
);
assert_eq!(2, updated_address.sync_change_counter);
}
#[test]
fn test_address_delete() {
let mut db = new_mem_db();
let saved_address = add_address(
&mut db,
NewAddressFields {
given_name: "jane".to_string(),
family_name: "doe".to_string(),
street_address: "123 Second Avenue".to_string(),
address_level2: "Chicago, IL".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
)
.expect("should contain saved address");
let delete_result = delete_address(&mut db, &saved_address.guid);
assert!(delete_result.is_ok());
assert!(delete_result.unwrap());
}
#[test]
fn test_address_trigger_on_create() {
let db = new_mem_db();
let guid = Guid::random();
let tombstone_result = db.execute_named(
"INSERT OR IGNORE INTO addresses_tombstones (
guid,
time_deleted
) VALUES (
:guid,
:time_deleted
)",
rusqlite::named_params! {
":guid": guid.as_str(),
":time_deleted": Timestamp::now(),
},
);
assert!(tombstone_result.is_ok());
let address = Address {
guid,
fields: NewAddressFields {
given_name: "jane".to_string(),
family_name: "doe".to_string(),
street_address: "123 Second Avenue".to_string(),
address_level2: "Chicago, IL".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
..Address::default()
};
let add_address_result = db.execute_named(
&format!(
"INSERT OR IGNORE INTO addresses_data (
{common_cols}
) VALUES (
:guid,
:given_name,
:additional_name,
:family_name,
:organization,
:street_address,
:address_level3,
:address_level2,
:address_level1,
:postal_code,
:country,
:tel,
:email,
:time_created,
:time_last_used,
:time_last_modified,
:times_used,
:sync_change_counter
)",
common_cols = ADDRESS_COMMON_COLS
),
rusqlite::named_params! {
":guid": address.guid,
":given_name": address.fields.given_name,
":additional_name": address.fields.additional_name,
":family_name": address.fields.family_name,
":organization": address.fields.organization,
":street_address": address.fields.street_address,
":address_level3": address.fields.address_level3,
":address_level2": address.fields.address_level2,
":address_level1": address.fields.address_level1,
":postal_code": address.fields.postal_code,
":country": address.fields.country,
":tel": address.fields.tel,
":email": address.fields.email,
":time_created": address.time_created,
":time_last_used": address.time_last_used,
":time_last_modified": address.time_last_modified,
":times_used": address.times_used,
":sync_change_counter": address.sync_change_counter,
},
);
assert!(add_address_result.is_err());
let expected_error_message = "guid exists in `addresses_tombstones`";
assert_eq!(
expected_error_message,
add_address_result.unwrap_err().to_string()
);
}
#[test]
fn test_address_trigger_on_delete() {
let db = new_mem_db();
let guid = Guid::random();
let address = Address {
guid,
fields: NewAddressFields {
given_name: "jane".to_string(),
family_name: "doe".to_string(),
street_address: "123 Second Avenue".to_string(),
address_level2: "Chicago, IL".to_string(),
country: "United States".to_string(),
..NewAddressFields::default()
},
..Address::default()
};
let add_address_result = db.execute_named(
&format!(
"INSERT OR IGNORE INTO addresses_data (
{common_cols}
) VALUES (
:guid,
:given_name,
:additional_name,
:family_name,
:organization,
:street_address,
:address_level3,
:address_level2,
:address_level1,
:postal_code,
:country,
:tel,
:email,
:time_created,
:time_last_used,
:time_last_modified,
:times_used,
:sync_change_counter
)",
common_cols = ADDRESS_COMMON_COLS
),
rusqlite::named_params! {
":guid": address.guid,
":given_name": address.fields.given_name,
":additional_name": address.fields.additional_name,
":family_name": address.fields.family_name,
":organization": address.fields.organization,
":street_address": address.fields.street_address,
":address_level3": address.fields.address_level3,
":address_level2": address.fields.address_level2,
":address_level1": address.fields.address_level1,
":postal_code": address.fields.postal_code,
":country": address.fields.country,
":tel": address.fields.tel,
":email": address.fields.email,
":time_created": address.time_created,
":time_last_used": address.time_last_used,
":time_last_modified": address.time_last_modified,
":times_used": address.times_used,
":sync_change_counter": address.sync_change_counter,
},
);
assert!(add_address_result.is_ok());
let tombstone_result = db.execute_named(
"INSERT OR IGNORE INTO addresses_tombstones (
guid,
time_deleted
) VALUES (
:guid,
:time_deleted
)",
rusqlite::named_params! {
":guid": address.guid.as_str(),
":time_deleted": Timestamp::now(),
},
);
assert!(tombstone_result.is_err());
let expected_error_message = "guid exists in `addresses_data`";
assert_eq!(
expected_error_message,
tombstone_result.unwrap_err().to_string()
);
}
}