snapshot of https://github.com/ActivityWatch/aw-server-rust.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled
Some checks failed
Build / ubuntu-latest (push) Failing after 5s
Build / Android (push) Failing after 2s
Build / Code coverage (push) Failing after 2s
Lint / format (push) Failing after 3s
Lint / clippy (push) Failing after 3s
Build / macOS-latest (push) Has been cancelled
Build / windows-latest (push) Has been cancelled
This commit is contained in:
20
aw-models/Cargo.toml
Normal file
20
aw-models/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "aw-models"
|
||||
version = "0.1.0"
|
||||
authors = ["Johan Bjäreholt <johan@bjareho.lt>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
name = "aw_models"
|
||||
crate-type = ["lib", "cdylib"]
|
||||
path = "src/lib.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
schemars = { version = "0.8", features = ["chrono"] }
|
||||
8
aw-models/examples/schema.rs
Normal file
8
aw-models/examples/schema.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
extern crate aw_models;
|
||||
|
||||
use schemars::schema_for;
|
||||
|
||||
fn main() {
|
||||
let schema = schema_for!(aw_models::Bucket);
|
||||
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||
}
|
||||
60
aw-models/src/bucket.rs
Normal file
60
aw-models/src/bucket.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::map::Map;
|
||||
use serde_json::value::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Event;
|
||||
use crate::TryVec;
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)]
|
||||
pub struct Bucket {
|
||||
#[serde(skip)]
|
||||
pub bid: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub id: String,
|
||||
#[serde(rename = "type")] /* type is a reserved Rust keyword */ pub _type: String,
|
||||
pub client: String,
|
||||
pub hostname: String,
|
||||
pub created: Option<DateTime<Utc>>,
|
||||
#[serde(default)]
|
||||
pub data: Map<String, Value>,
|
||||
#[serde(default, skip_deserializing)]
|
||||
pub metadata: BucketMetadata,
|
||||
// Events should only be "Some" during import/export
|
||||
// It's using a TryVec to discard only the events which were failed to be serialized so only a
|
||||
// few events are being dropped during import instead of failing the whole import
|
||||
pub events: Option<TryVec<Event>>,
|
||||
pub last_updated: Option<DateTime<Utc>>, // TODO: Should probably be moved into metadata field
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug, Default)]
|
||||
pub struct BucketMetadata {
|
||||
#[serde(default)]
|
||||
pub start: Option<DateTime<Utc>>,
|
||||
pub end: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct BucketsExport {
|
||||
pub buckets: HashMap<String, Bucket>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bucket() {
|
||||
let b = Bucket {
|
||||
bid: None,
|
||||
id: "id".to_string(),
|
||||
_type: "type".to_string(),
|
||||
client: "client".to_string(),
|
||||
hostname: "hostname".into(),
|
||||
created: None,
|
||||
data: json_map! {},
|
||||
metadata: BucketMetadata::default(),
|
||||
events: None,
|
||||
last_updated: None,
|
||||
};
|
||||
debug!("bucket: {:?}", b);
|
||||
}
|
||||
19
aw-models/src/duration.rs
Normal file
19
aw-models/src/duration.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Max duration of a i64 nanosecond is 2562047.7880152157 hours
|
||||
// ((2**64)/2)/1000000000/60/60
|
||||
|
||||
fn get_nanos(duration: &chrono::Duration) -> f64 {
|
||||
(duration.num_nanoseconds().unwrap() as f64) / 1_000_000_000.0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "chrono::Duration")]
|
||||
pub struct DurationSerialization(#[serde(getter = "get_nanos")] f64);
|
||||
|
||||
// Provide a conversion to construct the remote type.
|
||||
impl From<DurationSerialization> for chrono::Duration {
|
||||
fn from(def: DurationSerialization) -> chrono::Duration {
|
||||
chrono::Duration::nanoseconds((def.0 * 1_000_000_000.0) as i64)
|
||||
}
|
||||
}
|
||||
101
aw-models/src/event.rs
Normal file
101
aw-models/src/event.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration;
|
||||
use chrono::Utc;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::duration::DurationSerialization;
|
||||
use crate::TimeInterval;
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Debug)]
|
||||
pub struct Event {
|
||||
/// An unique id for this event.
|
||||
/// Will be assigned once the event has reached the servers datastore.
|
||||
///
|
||||
/// **WARNING:** If you set the ID and insert the event to the server it will replace the previous
|
||||
/// event with that ID. Only do this if you are completely sure what you are doing.
|
||||
pub id: Option<i64>,
|
||||
/// An rfc3339 timestamp which represents the start of the event
|
||||
pub timestamp: DateTime<Utc>,
|
||||
/// Duration of the event as a floating point number in seconds.
|
||||
/// Appended to the timestamp it can represent the end of the event
|
||||
/// Maximum precision is nanoseconds.
|
||||
#[serde(with = "DurationSerialization", default = "default_duration")]
|
||||
#[schemars(with = "f64")]
|
||||
pub duration: Duration,
|
||||
/// Can contain any arbitrary JSON data that represents the value of the event.
|
||||
/// All events in a bucket should follow the format of it's respective bucket-type.
|
||||
pub data: Map<String, Value>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn new(timestamp: DateTime<Utc>, duration: Duration, data: Map<String, Value>) -> Self {
|
||||
Event {
|
||||
id: None,
|
||||
timestamp,
|
||||
duration,
|
||||
data,
|
||||
}
|
||||
}
|
||||
pub fn calculate_endtime(&self) -> DateTime<Utc> {
|
||||
self.timestamp + chrono::Duration::nanoseconds(self.duration.num_nanoseconds().unwrap())
|
||||
}
|
||||
pub fn interval(&self) -> TimeInterval {
|
||||
TimeInterval::new(self.timestamp, self.calculate_endtime())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Event {
|
||||
fn eq(&self, other: &Event) -> bool {
|
||||
!(self.timestamp != other.timestamp
|
||||
|| self.duration != other.duration
|
||||
|| self.data != other.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Event {
|
||||
fn default() -> Self {
|
||||
Event {
|
||||
id: None,
|
||||
timestamp: Utc::now(),
|
||||
duration: Duration::seconds(0),
|
||||
data: serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_duration() -> Duration {
|
||||
Duration::seconds(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event() {
|
||||
use serde_json::json;
|
||||
|
||||
let e = Event {
|
||||
id: None,
|
||||
timestamp: Utc::now(),
|
||||
duration: Duration::seconds(1),
|
||||
data: json_map! {"test": json!(1)},
|
||||
};
|
||||
debug!("event: {:?}", e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_default_duration() {
|
||||
// Watchers may omit duration when posting an event, in which case it
|
||||
// defaults to zero rather than failing to deserialize.
|
||||
let e: Event =
|
||||
serde_json::from_str(r#"{"timestamp": "2000-01-01T00:00:00Z", "data": {"test": 1}}"#)
|
||||
.unwrap();
|
||||
assert_eq!(e.duration, Duration::seconds(0));
|
||||
|
||||
// An explicit duration is still honoured.
|
||||
let e: Event = serde_json::from_str(
|
||||
r#"{"timestamp": "2000-01-01T00:00:00Z", "duration": 1.5, "data": {}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(e.duration, Duration::milliseconds(1500));
|
||||
}
|
||||
4
aw-models/src/export.rs
Normal file
4
aw-models/src/export.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct BucketsExport {
|
||||
pub buckets: HashMap<String, Bucket>,
|
||||
}
|
||||
19
aw-models/src/info.rs
Normal file
19
aw-models/src/info.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Info {
|
||||
pub hostname: String,
|
||||
pub version: String,
|
||||
pub testing: bool,
|
||||
/// Name of the running instance profile ("default" unless `--profile` was
|
||||
/// given). Lets clients tell concurrent instances apart. Defaults on
|
||||
/// deserialization so a new client can still talk to an older server.
|
||||
#[serde(default = "default_profile")]
|
||||
pub profile: String,
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
fn default_profile() -> String {
|
||||
"default".to_string()
|
||||
}
|
||||
39
aw-models/src/lib.rs
Normal file
39
aw-models/src/lib.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
// TODO: Move me to an appropriate place
|
||||
#[cfg(test)] // Only macro use for tests
|
||||
macro_rules! json_map {
|
||||
{ $( $key:literal : $value:expr),* } => {{
|
||||
use serde_json::{Value};
|
||||
use serde_json::map::Map;
|
||||
#[allow(unused_mut)]
|
||||
let mut map : Map<String, Value> = Map::new();
|
||||
$(
|
||||
map.insert( $key.to_string(), json!($value) );
|
||||
)*
|
||||
map
|
||||
}};
|
||||
}
|
||||
|
||||
mod bucket;
|
||||
mod duration;
|
||||
mod event;
|
||||
mod info;
|
||||
mod query;
|
||||
mod settings;
|
||||
mod timeinterval;
|
||||
mod tryvec;
|
||||
|
||||
pub use self::bucket::Bucket;
|
||||
pub use self::bucket::BucketMetadata;
|
||||
pub use self::bucket::BucketsExport;
|
||||
pub use self::event::Event;
|
||||
pub use self::info::Info;
|
||||
pub use self::query::Query;
|
||||
pub use self::settings::Settings;
|
||||
pub use self::settings::{
|
||||
Class, ClassData, ClassRule, NewReleaseCheckData, UserSatisfactionPollData, View, ViewElement,
|
||||
};
|
||||
pub use self::timeinterval::TimeInterval;
|
||||
pub use self::tryvec::TryVec;
|
||||
38
aw-models/src/query.rs
Normal file
38
aw-models/src/query.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::TimeInterval;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Query {
|
||||
//#[serde(with = "DurationSerialization")]
|
||||
pub timeperiods: Vec<TimeInterval>,
|
||||
pub query: Vec<String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_query_serialization_roundtrip() {
|
||||
for payload in [
|
||||
serde_json::json!({
|
||||
"timeperiods": [
|
||||
"2000-01-01T00:00:00+00:00/2000-01-02T00:00:00+00:00",
|
||||
"2000-01-02T00:00:00.123456789+00:00/2000-01-03T00:00:00+00:00"
|
||||
],
|
||||
"query": ["events = query_bucket(\"test\");\nRETURN = events;", "RETURN = [];" ]
|
||||
}),
|
||||
serde_json::json!({"timeperiods": [], "query": []}),
|
||||
] {
|
||||
let query: Query = serde_json::from_value(payload.clone()).unwrap();
|
||||
let encoded = serde_json::to_string(&query).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&encoded).unwrap(),
|
||||
payload
|
||||
);
|
||||
let decoded: Query = serde_json::from_str(&encoded).unwrap();
|
||||
assert_eq!(decoded.query, query.query);
|
||||
assert_eq!(decoded.timeperiods.len(), query.timeperiods.len());
|
||||
for (actual, expected) in decoded.timeperiods.iter().zip(&query.timeperiods) {
|
||||
assert_eq!(actual.start(), expected.start());
|
||||
assert_eq!(actual.end(), expected.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
76
aw-models/src/settings.rs
Normal file
76
aw-models/src/settings.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct NewReleaseCheckData {
|
||||
pub how_often_to_check: i64,
|
||||
pub is_enabled: bool,
|
||||
pub next_check_time: String,
|
||||
pub times_checked: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct UserSatisfactionPollData {
|
||||
pub is_enabled: bool,
|
||||
pub next_poll_time: String,
|
||||
pub times_poll_is_shown: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct ViewElement {
|
||||
pub size: i32,
|
||||
#[serde(rename = "type")]
|
||||
pub element_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct View {
|
||||
pub elements: Vec<ViewElement>,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct ClassData {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub score: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct ClassRule {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ignore_case: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub regex: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub rule_type: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone)]
|
||||
pub struct Class {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<ClassData>,
|
||||
pub id: i32,
|
||||
pub name: Vec<String>,
|
||||
pub rule: ClassRule,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema)]
|
||||
pub struct Settings {
|
||||
pub landing_page: String,
|
||||
pub start_of_day: String,
|
||||
pub always_active_pattern: String,
|
||||
pub new_release_check_data: NewReleaseCheckData,
|
||||
pub start_of_week: String,
|
||||
pub use_color_fallback: bool,
|
||||
pub user_satisfaction_poll_data: UserSatisfactionPollData,
|
||||
pub request_timeout: i32,
|
||||
pub devmode: bool,
|
||||
pub theme: String,
|
||||
pub views: Vec<View>,
|
||||
pub duration_default: i32,
|
||||
pub classes: Vec<Class>,
|
||||
pub initial_timestamp: String,
|
||||
}
|
||||
211
aw-models/src/timeinterval.rs
Normal file
211
aw-models/src/timeinterval.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use crate::Event;
|
||||
use std::cmp::{max, min};
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, Deserialize, Deserializer, Visitor};
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Duration;
|
||||
use chrono::Utc;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TimeInterval {
|
||||
start: DateTime<Utc>,
|
||||
end: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TimeIntervalError {
|
||||
ParseError(),
|
||||
}
|
||||
|
||||
/// Python versions of many of these functions can be found at https://github.com/ErikBjare/timeslot
|
||||
impl TimeInterval {
|
||||
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> TimeInterval {
|
||||
TimeInterval { start, end }
|
||||
}
|
||||
|
||||
pub fn new_from_string(period: &str) -> Result<TimeInterval, TimeIntervalError> {
|
||||
let splits = period.split('/').collect::<Vec<&str>>();
|
||||
if splits.len() != 2 {
|
||||
return Err(TimeIntervalError::ParseError());
|
||||
}
|
||||
let start = match DateTime::parse_from_rfc3339(splits[0]) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_e) => return Err(TimeIntervalError::ParseError()),
|
||||
};
|
||||
let end = match DateTime::parse_from_rfc3339(splits[1]) {
|
||||
Ok(dt) => dt.with_timezone(&Utc),
|
||||
Err(_e) => return Err(TimeIntervalError::ParseError()),
|
||||
};
|
||||
|
||||
Ok(TimeInterval::new(start, end))
|
||||
}
|
||||
|
||||
pub fn start(&self) -> &DateTime<Utc> {
|
||||
&self.start
|
||||
}
|
||||
|
||||
pub fn end(&self) -> &DateTime<Utc> {
|
||||
&self.end
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.end - self.start
|
||||
}
|
||||
|
||||
/// If intervals are separated by a non-zero gap, return the gap as a new TimeInterval, else None
|
||||
pub fn gap(&self, other: &TimeInterval) -> Option<TimeInterval> {
|
||||
if self.end < other.start {
|
||||
Some(TimeInterval::new(self.end, other.start))
|
||||
} else if other.end < self.start {
|
||||
Some(TimeInterval::new(other.end, self.start))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins two intervals together if they don't have a gap, else None
|
||||
pub fn union(&self, other: &TimeInterval) -> Option<TimeInterval> {
|
||||
match self.gap(other) {
|
||||
Some(_) => None,
|
||||
None => Some(TimeInterval::new(
|
||||
min(self.start, other.start),
|
||||
max(self.end, other.end),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The intersection of two intervals
|
||||
pub fn intersection(&self, other: &TimeInterval) -> Option<TimeInterval> {
|
||||
let last_start = max(self.start, other.start);
|
||||
let first_end = min(self.end, other.end);
|
||||
if last_start < first_end {
|
||||
Some(TimeInterval::new(last_start, first_end))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A boolean whether the two intervals intersect
|
||||
pub fn intersects(&self, other: &TimeInterval) -> bool {
|
||||
self.intersection(other).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Event> for TimeInterval {
|
||||
fn from(e: &Event) -> TimeInterval {
|
||||
TimeInterval::new(e.timestamp, e.timestamp + e.duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TimeInterval {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.start.to_rfc3339(), self.end.to_rfc3339())
|
||||
}
|
||||
}
|
||||
|
||||
struct TimeIntervalVisitor;
|
||||
use serde::de::Unexpected;
|
||||
|
||||
impl<'de> Visitor<'de> for TimeIntervalVisitor {
|
||||
type Value = TimeInterval;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an string in ISO timeinterval format (such as 2000-01-01T00:00:00+01:00/2001-02-02T01:01:01+01:00)")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match TimeInterval::new_from_string(value) {
|
||||
Ok(ti) => Ok(ti),
|
||||
Err(e) => {
|
||||
warn!("{:?}", e);
|
||||
Err(de::Error::invalid_value(Unexpected::Str(value), &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TimeInterval {
|
||||
fn deserialize<D>(deserializer: D) -> Result<TimeInterval, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(TimeIntervalVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for TimeInterval {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeinterval_serialization() {
|
||||
for (input, expected) in [
|
||||
(
|
||||
"2000-01-01T00:00:00Z/2000-01-02T00:00:00Z",
|
||||
"2000-01-01T00:00:00+00:00/2000-01-02T00:00:00+00:00",
|
||||
),
|
||||
(
|
||||
"2000-01-01T03:00:00.123456789+03:00/2000-01-01T01:00:00.987654321-02:00",
|
||||
"2000-01-01T00:00:00.123456789+00:00/2000-01-01T03:00:00.987654321+00:00",
|
||||
),
|
||||
(
|
||||
"2000-01-01T00:00:00Z/2000-01-01T00:00:00Z",
|
||||
"2000-01-01T00:00:00+00:00/2000-01-01T00:00:00+00:00",
|
||||
),
|
||||
] {
|
||||
let interval = TimeInterval::new_from_string(input).unwrap();
|
||||
let encoded = serde_json::to_value(&interval).unwrap();
|
||||
assert_eq!(encoded, serde_json::json!(expected));
|
||||
let decoded: TimeInterval = serde_json::from_value(encoded).unwrap();
|
||||
assert_eq!(decoded.start(), interval.start());
|
||||
assert_eq!(decoded.end(), interval.end());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeinterval() {
|
||||
use std::str::FromStr;
|
||||
|
||||
let start = DateTime::from_str("2000-01-01T00:00:00Z").unwrap();
|
||||
let end = DateTime::from_str("2000-01-02T00:00:00Z").unwrap();
|
||||
let period_str = "2000-01-01T00:00:00+00:00/2000-01-02T00:00:00+00:00";
|
||||
let duration = end - start;
|
||||
let tp = TimeInterval::new(start, end);
|
||||
assert_eq!(tp.start(), &start);
|
||||
assert_eq!(tp.end(), &end);
|
||||
assert_eq!(tp.duration(), duration);
|
||||
assert_eq!(tp.to_string(), period_str);
|
||||
|
||||
let tp = TimeInterval::new_from_string(period_str).unwrap();
|
||||
assert_eq!(tp.start(), &start);
|
||||
assert_eq!(tp.end(), &end);
|
||||
assert_eq!(tp.duration(), duration);
|
||||
assert_eq!(tp.to_string(), period_str);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timeinterval_intersection() {
|
||||
use std::str::FromStr;
|
||||
|
||||
// Check that two exactly adjacent events don't intersect
|
||||
let tp1 = TimeInterval::new(
|
||||
DateTime::from_str("2000-01-01T00:00:00Z").unwrap(),
|
||||
DateTime::from_str("2000-01-01T00:01:00Z").unwrap(),
|
||||
);
|
||||
let tp2 = TimeInterval::new(
|
||||
DateTime::from_str("2000-01-01T00:01:00Z").unwrap(),
|
||||
DateTime::from_str("2000-01-01T00:02:00Z").unwrap(),
|
||||
);
|
||||
assert!(!tp1.intersects(&tp2));
|
||||
}
|
||||
208
aw-models/src/tryvec.rs
Normal file
208
aw-models/src/tryvec.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use core::marker::PhantomData;
|
||||
use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema};
|
||||
use serde::de::{DeserializeOwned, SeqAccess, Visitor};
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::Value;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TryParse<T: JsonSchema> {
|
||||
Parsed(T),
|
||||
#[allow(dead_code)]
|
||||
Unparsed(Value),
|
||||
NotPresent,
|
||||
}
|
||||
|
||||
impl<T: JsonSchema> JsonSchema for TryParse<T> {
|
||||
fn schema_name() -> String {
|
||||
format!("Try<{}>", std::any::type_name::<T>())
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
|
||||
gen.subschema_for::<T>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: DeserializeOwned + JsonSchema> Deserialize<'de> for TryParse<T> {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
match Option::<Value>::deserialize(deserializer)? {
|
||||
None => Ok(TryParse::NotPresent),
|
||||
Some(value) => match T::deserialize(&value) {
|
||||
Ok(t) => Ok(TryParse::Parsed(t)),
|
||||
Err(_) => Ok(TryParse::Unparsed(value)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
pub struct TryVec<T: JsonSchema> {
|
||||
inner: Vec<TryParse<T>>,
|
||||
}
|
||||
|
||||
impl<T: JsonSchema> TryVec<T> {
|
||||
pub fn new(mut vec: Vec<T>) -> Self {
|
||||
let mut vec_marked: Vec<TryParse<T>> = Vec::new();
|
||||
for item in vec.drain(..) {
|
||||
vec_marked.push(TryParse::Parsed(item));
|
||||
}
|
||||
TryVec { inner: vec_marked }
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
TryVec { inner: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn take_inner(self) -> Vec<T> {
|
||||
let mut vec: Vec<T> = Vec::new();
|
||||
for item in self.inner {
|
||||
match item {
|
||||
TryParse::Parsed(i) => vec.push(i),
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
vec
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: JsonSchema> Serialize for TryVec<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.inner.len()))?;
|
||||
for element in &self.inner {
|
||||
match element {
|
||||
TryParse::Parsed(t) => seq.serialize_element(t)?,
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
struct TryVecVisitor<T: JsonSchema> {
|
||||
marker: PhantomData<fn() -> TryVec<T>>,
|
||||
}
|
||||
|
||||
impl<T: JsonSchema> TryVecVisitor<T> {
|
||||
fn new() -> Self {
|
||||
TryVecVisitor {
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T> Visitor<'de> for TryVecVisitor<T>
|
||||
where
|
||||
T: DeserializeOwned + JsonSchema,
|
||||
{
|
||||
type Value = TryVec<T>;
|
||||
|
||||
// Format a message stating what data this Visitor expects to receive.
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("a sequence")
|
||||
}
|
||||
|
||||
fn visit_seq<M>(self, mut access: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: SeqAccess<'de>,
|
||||
{
|
||||
let mut vec = Vec::new();
|
||||
|
||||
loop {
|
||||
let res = match access.next_element() {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
println!("Failed to parse event because '{err}', the event will be discarded");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match res {
|
||||
Some(item) => vec.push(item),
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(TryVec { inner: vec })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: JsonSchema> Deserialize<'de> for TryVec<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(TryVecVisitor::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::TryVec;
|
||||
|
||||
#[derive(Deserialize, Serialize, JsonSchema, Debug)]
|
||||
struct TestEvent {
|
||||
data: String,
|
||||
}
|
||||
|
||||
fn assert_serialized_deserialized_eq(data: &str, eq: &str) {
|
||||
let deserialized = serde_json::from_str::<TryVec<TestEvent>>(data).unwrap();
|
||||
let serialized = serde_json::to_string(&deserialized).unwrap();
|
||||
assert_eq!(serialized, eq);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize_deserialize() {
|
||||
println!("test empty array");
|
||||
assert_serialized_deserialized_eq(r#"[]"#, r#"[]"#);
|
||||
|
||||
println!("test one valid event");
|
||||
assert_serialized_deserialized_eq(r#"[{"data":"test"}]"#, r#"[{"data":"test"}]"#);
|
||||
|
||||
println!("test invalid type int, skip event");
|
||||
assert_serialized_deserialized_eq(r#"[{ "data": 1 }]"#, r#"[]"#);
|
||||
|
||||
println!("test invalid type dict, skip event");
|
||||
assert_serialized_deserialized_eq(r#"[{"data":{}}]"#, r#"[]"#);
|
||||
|
||||
println!("test invalid type arr, skip event");
|
||||
assert_serialized_deserialized_eq(r#"[{"data":[]}]"#, r#"[]"#);
|
||||
|
||||
println!("test multiple valid events");
|
||||
assert_serialized_deserialized_eq(
|
||||
r#"[{"data":"test"},{"data":"test2"},{"data":"test3"}]"#,
|
||||
r#"[{"data":"test"},{"data":"test2"},{"data":"test3"}]"#,
|
||||
);
|
||||
|
||||
println!("test invalid event in middle of sequence, skip one event");
|
||||
assert_serialized_deserialized_eq(
|
||||
r#"[{"data":"test"},{"data":2},{"data":"test3"}]"#,
|
||||
r#"[{"data":"test"},{"data":"test3"}]"#,
|
||||
);
|
||||
|
||||
println!("test utf-16 character");
|
||||
assert_serialized_deserialized_eq(r#"[{"data":"\ud835\udc47"}]"#, r#"[{"data":"𝑇"}]"#);
|
||||
|
||||
println!("test invalid utf-8/16, skip event");
|
||||
assert_serialized_deserialized_eq(r#"[{"data":"\ud835"}]"#, r#"[]"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_methods() {
|
||||
let tryvec = TryVec::<TestEvent>::new_empty();
|
||||
assert_eq!(tryvec.take_inner().len(), Vec::<TestEvent>::new().len());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user