use chrono::{DateTime, Utc};
use crate::db::{Database, WriteOpts};
use crate::error::ApiError;
#[cfg(test)]
pub fn write(
db: &Database,
namespace: &str,
value: &str,
when: Option<DateTime<Utc>>,
ttl: Option<u64>,
) -> Result<crate::db::Written, ApiError> {
write_tagged(db, namespace, value, when, ttl, "")
}
pub fn write_tagged(
db: &Database,
namespace: &str,
value: &str,
when: Option<DateTime<Utc>>,
ttl: Option<u64>,
tags: &str,
) -> Result<crate::db::Written, ApiError> {
write_tagged_as(db, None, namespace, value, when, ttl, tags)
}
pub fn write_tagged_as(
db: &Database,
origin: Option<&str>,
namespace: &str,
value: &str,
when: Option<DateTime<Utc>>,
ttl: Option<u64>,
tags: &str,
) -> Result<crate::db::Written, ApiError> {
check(db, namespace, value)?;
let node = origin.unwrap_or_else(|| db.node());
Ok(db.write_tagged_as(
node,
namespace,
value,
when.unwrap_or_else(Utc::now),
WriteOpts {
consensus: true,
ttl,
},
tags,
))
}
pub fn check(db: &Database, namespace: &str, value: &str) -> Result<(), ApiError> {
if value.is_empty() {
return Err(ApiError::EmptyValue);
}
if crate::db::is_internal(namespace) {
return Err(ApiError::InternalNamespace(namespace.to_string()));
}
if !db.holds(namespace) {
return Err(ApiError::NotStored(namespace.to_string()));
}
Ok(())
}
pub fn timestamp_to_instant(timestamp: i64) -> Result<DateTime<Utc>, ApiError> {
DateTime::from_timestamp(timestamp, 0).ok_or(ApiError::InvalidTimestamp(timestamp))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_write_without_a_timestamp_is_recorded_now() {
let db = Database::default();
let before = Utc::now().timestamp();
write(&db, "ns", "v", None, None).unwrap();
let view = db.view("ns", "v", 0, false).unwrap();
assert!(
view.first_seen >= before,
"first_seen was {}, expected >= {before}",
view.first_seen
);
assert_ne!(view.first_seen, 0);
}
#[test]
fn an_explicit_timestamp_is_honoured() {
let db = Database::default();
write(
&db,
"ns",
"v",
Some(timestamp_to_instant(1_566_624_658).unwrap()),
None,
)
.unwrap();
let view = db.view("ns", "v", 0, false).unwrap();
assert_eq!(view.first_seen, 1_566_624_658);
assert_eq!(view.last_seen, 1_566_624_658);
}
#[test]
fn write_returns_the_running_count() {
let db = Database::default();
let first = write(&db, "ns", "v", None, None).unwrap();
assert_eq!(first.count, 1);
assert!(first.new, "the first sighting was not reported as new");
let second = write(&db, "ns", "v", None, None).unwrap();
assert_eq!(second.count, 2);
assert!(!second.new, "a repeat sighting was reported as new");
}
#[test]
fn a_ttl_is_recorded_and_expires_the_value() {
let db = Database::default();
write(&db, "ns", "live", None, Some(3600)).unwrap();
assert_eq!(db.view("ns", "live", 0, false).unwrap().ttl, 3600);
write(
&db,
"ns",
"dead",
Some(timestamp_to_instant(1000).unwrap()),
Some(60),
)
.unwrap();
assert!(db.view("ns", "dead", 0, false).is_none());
}
#[test]
fn empty_values_are_rejected() {
let db = Database::default();
assert_eq!(
write(&db, "ns", "", None, None).unwrap_err(),
ApiError::EmptyValue
);
}
#[test]
fn no_internal_namespace_is_writable() {
for namespace in [
"_all",
"_shadow/feeds/ips",
"_config/acl/apikeys/mine",
"_internal",
"_anything/at/all",
] {
let db = Database::new();
assert_eq!(
write(&db, namespace, "x", None, None).unwrap_err(),
ApiError::InternalNamespace(namespace.to_string()),
"{namespace} was writable"
);
assert!(!db.namespace_exists(namespace), "{namespace} was created");
}
}
#[test]
fn an_underscore_further_in_is_an_ordinary_namespace() {
let db = Database::new();
assert_eq!(
write(&db, "feeds/_private/ips", "x", None, None)
.unwrap()
.count,
1
);
assert!(db.namespace_exists("feeds/_private/ips"));
}
#[test]
fn the_guard_does_not_break_consensus_bookkeeping() {
let db = Database::new();
write(&db, "feeds/a", "1.2.3.4", None, None).unwrap();
assert_eq!(
db.view(
"feeds/a",
"1.2.3.4",
db.count(crate::db::ALL_NAMESPACE, "1.2.3.4"),
false
)
.unwrap()
.consensus,
1
);
write(&db, "feeds/b", "1.2.3.4", None, None).unwrap();
assert_eq!(
db.count(crate::db::ALL_NAMESPACE, "1.2.3.4"),
2,
"the consensus tally stopped being kept"
);
}
#[test]
fn out_of_range_timestamps_are_rejected() {
assert_eq!(
timestamp_to_instant(i64::MAX).unwrap_err(),
ApiError::InvalidTimestamp(i64::MAX)
);
}
}