use chrono::{DateTime, Utc};
use uuid::Uuid;
use crate::models::annotations::AnnotationUpsert;
use crate::models::replicates::{GroupAudit, ReplicateSpec};
use crate::models::streams::{IngestReading, IngestStatusEvent};
#[derive(Debug, Clone)]
pub struct StreamDescriptor {
pub source_key: String,
pub source_name: String,
pub source_path: String,
pub metadata: serde_json::Value,
pub measurement_type: Option<String>,
pub sensor_id: Option<Uuid>,
pub replicates: Option<ReplicateSpec>,
pub decimal_places: Option<i16>,
}
#[derive(Debug, Clone)]
pub struct StreamFetchRequest {
pub stream_id: Uuid,
pub source_key: String,
pub since: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct SourceWindow {
pub from: DateTime<Utc>,
pub to: DateTime<Utc>,
pub source_rows_read: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dropped_times: Vec<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_digest: Option<String>,
}
#[derive(Debug)]
pub struct StreamReadings {
pub stream_id: Uuid,
pub source_key: String,
pub readings: Vec<IngestReading>,
pub audits: Vec<GroupAudit>,
pub collection: bool,
pub window: Option<SourceWindow>,
pub annotations: Vec<AnnotationUpsert>,
}
impl StreamReadings {
pub fn new(stream_id: Uuid, source_key: String, readings: Vec<IngestReading>) -> Self {
Self {
stream_id,
source_key,
readings,
audits: Vec::new(),
collection: false,
window: None,
annotations: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct StreamStatusEvents {
pub stream_id: Uuid,
pub source_key: String,
pub events: Vec<IngestStatusEvent>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SourceInventory {
pub candidates: Vec<SourceCandidate>,
#[serde(default)]
pub declined: Vec<DeclinedChannel>,
#[serde(default)]
pub groups: Vec<String>,
#[serde(default)]
pub instruments: Vec<crate::models::SensorUpsert>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct SourceCandidate {
pub source_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub group: Option<String>,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct DeclinedChannel {
pub channel: String,
pub reason: String,
}
impl SourceInventory {
#[must_use]
pub fn of_discovered(descriptors: &[StreamDescriptor]) -> Self {
let candidates: Vec<SourceCandidate> = descriptors
.iter()
.map(|d| SourceCandidate {
source_key: d.source_key.clone(),
group: d.source_path.split('/').nth(1).map(ToString::to_string),
})
.collect();
let mut groups: Vec<String> = candidates
.iter()
.filter_map(|c| c.group.clone())
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
groups.sort();
Self {
candidates,
declined: Vec::new(),
groups,
instruments: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_window_round_trips() {
let w = SourceWindow {
from: Utc::now(),
to: Utc::now(),
source_rows_read: 500,
dropped_times: vec![Utc::now()],
content_digest: Some("fnv:1".into()),
};
let back: SourceWindow = serde_json::from_value(serde_json::to_value(&w).unwrap()).unwrap();
assert_eq!(back.source_rows_read, 500);
assert_eq!(back.dropped_times.len(), 1);
assert_eq!(back.content_digest.as_deref(), Some("fnv:1"));
}
}