261 lines
8.0 KiB
Rust
261 lines
8.0 KiB
Rust
/*
|
|
Copyright 2020-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
|
|
This file is part of the Luminescent Dreams Tools.
|
|
|
|
Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
|
|
|
Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License along with Lumeto. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
use chrono::NaiveDate;
|
|
use date_time_tz::DateTimeTz;
|
|
use serde::de::DeserializeOwned;
|
|
use serde::ser::Serialize;
|
|
use std::{cmp::Ordering, fmt, io, str};
|
|
use thiserror::Error;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum EmseriesReadError {
|
|
/// Indicates that the UUID specified is invalid and cannot be parsed
|
|
#[error("UUID failed to parse: {0}")]
|
|
UUIDParseError(uuid::Error),
|
|
|
|
/// Indicates an error in the JSON deserialization
|
|
#[error("Error parsing JSON: {0}")]
|
|
JSONParseError(serde_json::error::Error),
|
|
|
|
/// Indicates a general IO error
|
|
#[error("IO Error: {0}")]
|
|
IOError(io::Error),
|
|
}
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum EmseriesWriteError {
|
|
/// Indicates a general IO error
|
|
#[error("IO Error: {0}")]
|
|
IOError(io::Error),
|
|
|
|
/// Indicates an error in the JSON serialization
|
|
#[error("Error generating a JSON string: {0}")]
|
|
JSONWriteError(serde_json::error::Error),
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum Timestamp {
|
|
DateTime(DateTimeTz),
|
|
Date(NaiveDate),
|
|
}
|
|
|
|
impl str::FromStr for Timestamp {
|
|
type Err = chrono::ParseError;
|
|
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
|
DateTimeTz::from_str(line)
|
|
.map(|dtz| Timestamp::DateTime(dtz))
|
|
.or(NaiveDate::from_str(line).map(|d| Timestamp::Date(d)))
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for Timestamp {
|
|
fn partial_cmp(&self, other: &Timestamp) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl Ord for Timestamp {
|
|
fn cmp(&self, other: &Timestamp) -> Ordering {
|
|
match (self, other) {
|
|
(Timestamp::DateTime(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(dt2),
|
|
(Timestamp::DateTime(dt1), Timestamp::Date(dt2)) => dt1.0.date().naive_utc().cmp(&dt2),
|
|
(Timestamp::Date(dt1), Timestamp::DateTime(dt2)) => dt1.cmp(&dt2.0.date().naive_utc()),
|
|
(Timestamp::Date(dt1), Timestamp::Date(dt2)) => dt1.cmp(dt2),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<DateTimeTz> for Timestamp {
|
|
fn from(d: DateTimeTz) -> Self {
|
|
Self::DateTime(d)
|
|
}
|
|
}
|
|
|
|
impl From<NaiveDate> for Timestamp {
|
|
fn from(d: NaiveDate) -> Self {
|
|
Self::Date(d)
|
|
}
|
|
}
|
|
|
|
/// Any element to be put into the database needs to be Recordable. This is the common API that
|
|
/// will aid in searching and later in indexing records.
|
|
pub trait Recordable {
|
|
/// The timestamp for the record.
|
|
fn timestamp(&self) -> Timestamp;
|
|
|
|
/// A list of string tags that can be used for indexing. This list defined per-type.
|
|
fn tags(&self) -> Vec<String>;
|
|
}
|
|
|
|
/// Uniquely identifies a record.
|
|
///
|
|
/// This is a wrapper around a basic uuid with some extra convenience methods.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize)]
|
|
pub struct UniqueId(Uuid);
|
|
|
|
impl UniqueId {
|
|
/// Create a new V4 UUID (this is the most common type in use these days).
|
|
pub fn new() -> UniqueId {
|
|
let id = Uuid::new_v4();
|
|
UniqueId(id)
|
|
}
|
|
}
|
|
|
|
impl str::FromStr for UniqueId {
|
|
type Err = EmseriesReadError;
|
|
|
|
/// Parse a UniqueId from a string. Raise UUIDParseError if the parsing fails.
|
|
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
|
Uuid::parse_str(val)
|
|
.map(UniqueId)
|
|
.map_err(|err| EmseriesReadError::UUIDParseError(err))
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for UniqueId {
|
|
/// Convert to a hyphenated string
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
|
write!(f, "{}", self.0.to_hyphenated().to_string())
|
|
}
|
|
}
|
|
|
|
/// Every record contains a unique ID and then the primary data, which itself must implementd the
|
|
/// Recordable trait.
|
|
#[derive(Clone, Deserialize, Serialize)]
|
|
pub struct Record<T: Clone + Recordable> {
|
|
pub id: UniqueId,
|
|
pub data: Option<T>,
|
|
}
|
|
|
|
impl<T> str::FromStr for Record<T>
|
|
where
|
|
T: Clone + Recordable + DeserializeOwned + Serialize,
|
|
{
|
|
type Err = EmseriesReadError;
|
|
|
|
fn from_str(line: &str) -> Result<Self, Self::Err> {
|
|
serde_json::from_str(&line).map_err(|err| EmseriesReadError::JSONParseError(err))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
extern crate dimensioned;
|
|
extern crate serde_json;
|
|
|
|
use self::dimensioned::si::{Kilogram, KG};
|
|
use super::*;
|
|
use chrono::TimeZone;
|
|
use chrono_tz::{Etc::UTC, US::Central};
|
|
use date_time_tz::DateTimeTz;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
pub struct Weight(Kilogram<f64>);
|
|
|
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
|
pub struct WeightRecord {
|
|
pub date: Timestamp,
|
|
pub weight: Weight,
|
|
}
|
|
|
|
impl Recordable for WeightRecord {
|
|
fn timestamp(&self) -> Timestamp {
|
|
self.date.clone()
|
|
}
|
|
|
|
fn tags(&self) -> Vec<String> {
|
|
Vec::new()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_parses_datetimetz_without_timezone() {
|
|
assert_eq!(
|
|
"2003-11-10T06:00:00Z".parse::<Timestamp>().unwrap(),
|
|
Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn timestamp_parses_date() {
|
|
assert_eq!(
|
|
"2023-11-10".parse::<Timestamp>().unwrap(),
|
|
Timestamp::Date(NaiveDate::from_ymd_opt(2023, 11, 10).unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn v_alpha_serialization() {
|
|
const WEIGHT_ENTRY: &str = "{\"data\":{\"weight\":77.79109,\"date\":\"2003-11-10T06:00:00.000000000000Z\"},\"id\":\"3330c5b0-783f-4919-b2c4-8169c38f65ff\"}";
|
|
|
|
let rec: Record<WeightRecord> = WEIGHT_ENTRY
|
|
.parse()
|
|
.expect("should successfully parse the record");
|
|
assert_eq!(
|
|
rec.id,
|
|
"3330c5b0-783f-4919-b2c4-8169c38f65ff".parse().unwrap()
|
|
);
|
|
assert_eq!(
|
|
rec.data,
|
|
Some(WeightRecord {
|
|
date: Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
|
|
weight: Weight(77.79109 * KG),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn serialization_output() {
|
|
let rec = WeightRecord {
|
|
date: Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0))),
|
|
weight: Weight(77.0 * KG),
|
|
};
|
|
assert_eq!(
|
|
serde_json::to_string(&rec).unwrap(),
|
|
"{\"date\":\"2003-11-10T06:00:00Z\",\"weight\":77.0}"
|
|
);
|
|
|
|
let rec2 = WeightRecord {
|
|
date: Timestamp::DateTime(Central.ymd(2003, 11, 10).and_hms(0, 0, 0).into()),
|
|
weight: Weight(77.0 * KG),
|
|
};
|
|
assert_eq!(
|
|
serde_json::to_string(&rec2).unwrap(),
|
|
"{\"date\":\"2003-11-10T06:00:00Z US/Central\",\"weight\":77.0}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn two_datetimes_can_be_compared() {
|
|
let time1 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)));
|
|
let time2 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 11).and_hms(6, 0, 0)));
|
|
assert!(time1 < time2);
|
|
}
|
|
|
|
#[test]
|
|
fn two_dates_can_be_compared() {
|
|
let time1 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 10));
|
|
let time2 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 11));
|
|
assert!(time1 < time2);
|
|
}
|
|
|
|
#[test]
|
|
fn datetime_and_date_can_be_compared() {
|
|
let time1 = Timestamp::DateTime(DateTimeTz(UTC.ymd(2003, 11, 10).and_hms(6, 0, 0)));
|
|
let time2 = Timestamp::Date(NaiveDate::from_ymd(2003, 11, 11));
|
|
assert!(time1 < time2)
|
|
}
|
|
}
|