pub(crate) mod de;
pub mod activity;
pub mod admin;
pub mod ddata;
pub mod devicestatus;
pub mod direction;
pub mod entry;
pub mod food;
pub mod glucose;
pub mod notification;
pub mod profile;
pub mod properties;
pub mod settings;
pub mod summary;
pub mod system;
pub mod time;
pub mod treatment;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub use activity::Activity;
pub use devicestatus::DeviceStatus;
pub use direction::Direction;
pub use entry::{Cal, Entry, Mbg, Sgv};
pub use food::Food;
pub use glucose::{Glucose, Units};
pub use profile::{Profile, ProfileStore, ScheduleEntry};
pub use settings::Setting;
pub use time::Timestamp;
pub use treatment::{EventType, Treatment, TreatmentKind};
pub type Extra = serde_json::Map<String, serde_json::Value>;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Meta {
#[serde(
rename = "_id",
default,
skip_serializing_if = "Option::is_none",
with = "de::text"
)]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none", with = "de::text")]
pub identifier: Option<String>,
#[serde(
rename = "srvCreated",
default,
skip_serializing_if = "Option::is_none",
with = "time::millis"
)]
pub srv_created: Option<Timestamp>,
#[serde(
rename = "srvModified",
default,
skip_serializing_if = "Option::is_none",
with = "time::millis"
)]
pub srv_modified: Option<Timestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(
rename = "modifiedBy",
default,
skip_serializing_if = "Option::is_none"
)]
pub modified_by: Option<String>,
#[serde(
rename = "isValid",
default,
skip_serializing_if = "Option::is_none",
with = "de::flag"
)]
pub is_valid: Option<bool>,
#[serde(
rename = "isReadOnly",
default,
skip_serializing_if = "Option::is_none",
with = "de::flag"
)]
pub is_read_only: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub app: Option<String>,
#[serde(
rename = "utcOffset",
default,
skip_serializing_if = "Option::is_none",
with = "de::int32"
)]
pub utc_offset: Option<i32>,
}
impl Meta {
#[must_use]
pub fn id(&self) -> Option<&str> {
self.identifier.as_deref().or(self.id.as_deref())
}
#[must_use]
pub fn is_deleted(&self) -> bool {
self.is_valid == Some(false)
}
pub(crate) fn strip_server_fields(&mut self) {
self.srv_created = None;
self.srv_modified = None;
self.subject = None;
self.modified_by = None;
self.is_valid = None;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum CollectionName {
Entries,
Treatments,
#[serde(rename = "devicestatus")]
DeviceStatus,
Profile,
Food,
Settings,
Activity,
}
impl CollectionName {
pub const V3: [Self; 6] = [
Self::Entries,
Self::Treatments,
Self::DeviceStatus,
Self::Profile,
Self::Food,
Self::Settings,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Entries => "entries",
Self::Treatments => "treatments",
Self::DeviceStatus => "devicestatus",
Self::Profile => "profile",
Self::Food => "food",
Self::Settings => "settings",
Self::Activity => "activity",
}
}
pub(crate) const fn v1_date_field(self) -> (&'static str, bool) {
match self {
Self::Entries | Self::Settings => ("date", true),
Self::Profile => ("startDate", false),
Self::Treatments | Self::DeviceStatus | Self::Food | Self::Activity => {
("created_at", false)
}
}
}
pub(crate) const fn v3_date_field(self) -> (&'static str, bool) {
match self {
Self::Entries | Self::Settings => ("date", true),
_ => ("created_at", false),
}
}
}
impl std::fmt::Display for CollectionName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.name())
}
}
mod sealed {
pub trait Sealed {}
}
pub trait Document:
Serialize + DeserializeOwned + Clone + Send + Sync + 'static + sealed::Sealed
{
const COLLECTION: CollectionName;
fn meta(&self) -> &Meta;
fn meta_mut(&mut self) -> &mut Meta;
fn timestamp(&self) -> Option<Timestamp>;
fn device(&self) -> Option<&str> {
None
}
fn event_type(&self) -> Option<&str> {
None
}
fn id(&self) -> Option<&str> {
self.meta().id()
}
#[doc(hidden)]
fn prepare_for_upload(&mut self, app: &str);
}
const IDENTIFIER_NAMESPACE: uuid::Uuid = uuid::Uuid::from_bytes(*b"NightscoutRocks!");
#[must_use]
pub fn compute_identifier(
device: Option<&str>,
date: Timestamp,
event_type: Option<&str>,
) -> String {
let mut key = format!("{}_{}", device.unwrap_or("undefined"), date.as_millis());
if let Some(event_type) = event_type.filter(|e| !e.is_empty()) {
key.push('_');
key.push_str(event_type);
}
uuid::Uuid::new_v5(&IDENTIFIER_NAMESPACE, key.as_bytes()).to_string()
}
pub(crate) fn prepare_meta<D: Document>(doc: &mut D, app: &str) {
let date = doc.timestamp();
let identifier = date.map(|d| compute_identifier(doc.device(), d, doc.event_type()));
let meta = doc.meta_mut();
if meta.app.is_none() {
meta.app = Some(app.to_owned());
}
if meta.utc_offset.is_none() {
meta.utc_offset = Some(local_utc_offset_minutes());
}
if meta.identifier.is_none() {
meta.identifier = identifier;
}
}
fn local_utc_offset_minutes() -> i32 {
chrono::Local::now().offset().local_minus_utc() / 60
}
macro_rules! impl_document {
($ty:ty, $collection:expr, timestamp = |$s:ident| $ts:expr, device = |$d:ident| $dev:expr $(, event_type = |$e:ident| $et:expr)? $(,)?) => {
impl crate::model::sealed::Sealed for $ty {}
impl crate::model::Document for $ty {
const COLLECTION: crate::model::CollectionName = $collection;
fn meta(&self) -> &crate::model::Meta {
&self.meta
}
fn meta_mut(&mut self) -> &mut crate::model::Meta {
&mut self.meta
}
fn timestamp(&self) -> Option<crate::model::Timestamp> {
let $s = self;
$ts
}
fn device(&self) -> Option<&str> {
let $d = self;
$dev
}
$(
fn event_type(&self) -> Option<&str> {
let $e = self;
$et
}
)?
fn prepare_for_upload(&mut self, app: &str) {
self.fill_defaults(app);
crate::model::prepare_meta(self, app);
}
}
};
}
pub(crate) use impl_document;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uploads_default_the_device_to_the_app_name() {
let mut carbs = Treatment::carbs(5.0);
carbs.prepare_for_upload("my-app");
assert_eq!(carbs.device.as_deref(), Some("my-app"));
let at = carbs.time().unwrap_or(Timestamp::EPOCH);
assert_eq!(
carbs.meta.identifier,
Some(compute_identifier(
Some("my-app"),
at,
Some("Carb Correction")
))
);
let mut sgv = Sgv::new(100.0, Timestamp::from_millis(1)).with_device("xDrip");
sgv.prepare_for_upload("my-app");
assert_eq!(
sgv.device.as_deref(),
Some("xDrip"),
"an explicit device wins"
);
}
#[test]
fn identifier_matches_the_server() {
assert_eq!(
compute_identifier(
Some("probe"),
Timestamp::from_millis(1_758_790_800_000),
None
),
"fa0f5891-69a8-50c8-b01d-0da5e7000589"
);
}
}