use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::error::BoxDynError;
use sqlx::postgres::{PgTypeInfo, PgValueRef};
use uuid::Uuid;
use crate::types::error::{HorizonError, Result as HorizonResult};
const POINT_BYTE_LEN: usize = 16;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Position(pub f64, pub f64);
impl sqlx::Type<sqlx::Postgres> for Position {
fn compatible(ty: &PgTypeInfo) -> bool {
*ty == Self::type_info()
}
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("POINT")
}
}
#[allow(
clippy::as_conversions,
clippy::indexing_slicing,
clippy::std_instead_of_core,
clippy::big_endian_bytes,
clippy::float_arithmetic,
reason = "byte slicing, Box cast, and BE f64 decode required for PostgreSQL POINT wire format"
)]
impl<'de> sqlx::Decode<'de, sqlx::Postgres> for Position {
fn decode(value: PgValueRef<'de>) -> Result<Self, BoxDynError> {
let buf = <&[u8] as sqlx::Decode<sqlx::Postgres>>::decode(value)?;
if buf.len() < POINT_BYTE_LEN {
return Err("POINT requires 16 bytes".into());
}
let (x_bytes, rest) = buf.split_at(8_usize);
let x = f64::from_be_bytes(
x_bytes
.try_into()
.map_err(|err| Box::new(err) as BoxDynError)?,
);
let y = f64::from_be_bytes(
rest[..8]
.try_into()
.map_err(|err| Box::new(err) as BoxDynError)?,
);
Ok(Self(x, y))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Annotation {
pub bearing_time_record_specification_id: Option<Uuid>,
pub confidence: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Option<Uuid>,
pub duration_seconds: Option<i32>,
pub feed_context: Option<serde_json::Value>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub notes: Option<String>,
pub ontology_class_id: Option<Uuid>,
pub organization_id: Option<Uuid>,
pub parent_annotation_id: Option<Uuid>,
pub platform_id: Uuid,
pub spectrogram_specification_id: Option<Uuid>,
pub time_coordinates: Vec<DateTime<Utc>>,
pub value_coordinates: Vec<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AudioSpecification {
pub bit_depth: i64,
pub channel_count: i64,
pub channel_index: i64,
pub created_datetime: Option<DateTime<Utc>>,
pub encoding: String,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub sample_rate: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BeamgramSpecification {
pub center_bearing: Option<f64>,
pub center_bin_width: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub elevation_increment: Option<f64>,
pub fft_sample_count: Option<i32>,
pub id: Option<Uuid>,
pub lower_elevation: Option<f64>,
pub max_frequency: Option<f64>,
pub min_frequency: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub normalizer: Option<String>,
pub organization_id: Option<Uuid>,
pub update_rate_ms: Option<i64>,
pub upper_elevation: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BearingResolutionAlias {
pub bearing_bin_count: Option<i32>,
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BearingTimeRecordSpecification {
pub amplitude_unit_mode: Option<String>,
pub baffle_bearing_list: Option<Vec<f64>>,
pub bearing_bin_count: Option<i32>,
pub created_datetime: Option<DateTime<Utc>>,
pub elevation_increment: Option<f64>,
pub fft_sample_count: Option<i32>,
pub focus_range: Option<String>,
pub frequency_spacing: Option<f64>,
pub heading_data_type: Option<String>,
pub heading_vector_index: Option<i32>,
pub id: Option<Uuid>,
pub lower_elevation: Option<f64>,
pub max_frequency: Option<f64>,
pub max_pixel: Option<i32>,
pub min_frequency: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub normalizer: Option<String>,
pub organization_id: Option<Uuid>,
pub update_rate_ms: Option<i64>,
pub upper_elevation: Option<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum DataRowVariant {
Float32,
Float32Array,
Float64,
Float64Array,
Int,
IntArray,
String,
Struct,
Vector,
}
impl DataRowVariant {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Float32 => "float32",
Self::Float32Array => "float32_array",
Self::Float64 => "float64",
Self::Float64Array => "float64_array",
Self::Int => "int",
Self::IntArray => "int_array",
Self::String => "string",
Self::Struct => "struct",
Self::Vector => "vector",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value {
"float32" => Some(Self::Float32),
"float32_array" => Some(Self::Float32Array),
"float64" => Some(Self::Float64),
"float64_array" => Some(Self::Float64Array),
"int" => Some(Self::Int),
"int_array" => Some(Self::IntArray),
"string" => Some(Self::String),
"struct" => Some(Self::Struct),
"vector" => Some(Self::Vector),
_ => None,
}
}
#[must_use]
pub const fn payload_arm(self) -> &'static str {
match self {
Self::Float32 => "payload_float32",
Self::Float32Array => "payload_float32_array",
Self::Float64 => "payload_float64",
Self::Float64Array => "payload_float64_array",
Self::Int => "payload_int",
Self::IntArray => "payload_int_array",
Self::String => "payload_string",
Self::Struct => "payload_struct",
Self::Vector => "vector",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DataRowPayload {
Float32(f32),
Float32Array(Vec<f32>),
Float64(f64),
Float64Array(Vec<f64>),
Int(i32),
IntArray(Vec<i32>),
String(String),
Struct(serde_json::Value),
Vector {
vector: Vec<f64>,
vector_end_bound: f64,
vector_start_bound: f64,
},
}
impl DataRowPayload {
#[must_use]
pub const fn variant(&self) -> DataRowVariant {
match self {
Self::Float32(_) => DataRowVariant::Float32,
Self::Float32Array(_) => DataRowVariant::Float32Array,
Self::Float64(_) => DataRowVariant::Float64,
Self::Float64Array(_) => DataRowVariant::Float64Array,
Self::Int(_) => DataRowVariant::Int,
Self::IntArray(_) => DataRowVariant::IntArray,
Self::String(_) => DataRowVariant::String,
Self::Struct(_) => DataRowVariant::Struct,
Self::Vector { .. } => DataRowVariant::Vector,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DataRow {
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Uuid,
pub data_type: String,
pub datetime: DateTime<Utc>,
pub modified_datetime: Option<DateTime<Utc>>,
pub specification_id: Uuid,
pub vector: Vec<f64>,
pub vector_end_bound: f64,
pub vector_start_bound: f64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VariantDataRow {
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Uuid,
pub data_type: String,
pub datetime: DateTime<Utc>,
pub modified_datetime: Option<DateTime<Utc>>,
pub payload_float32: Option<f32>,
pub payload_float32_array: Option<Vec<f32>>,
pub payload_float64: Option<f64>,
pub payload_float64_array: Option<Vec<f64>>,
pub payload_int: Option<i32>,
pub payload_int_array: Option<Vec<i32>>,
pub payload_string: Option<String>,
pub payload_struct: Option<serde_json::Value>,
pub specification_id: Uuid,
pub variant: Option<String>,
pub vector: Option<Vec<f64>>,
pub vector_end_bound: Option<f64>,
pub vector_start_bound: Option<f64>,
}
impl VariantDataRow {
#[must_use]
pub fn from_payload(
data_stream_id: Uuid,
datetime: DateTime<Utc>,
data_type: String,
specification_id: Uuid,
payload: DataRowPayload,
) -> Self {
let mut row = Self {
created_datetime: None,
data_stream_id,
data_type,
datetime,
modified_datetime: None,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id,
variant: Some(payload.variant().as_str().to_owned()),
vector: None,
vector_end_bound: None,
vector_start_bound: None,
};
match payload {
DataRowPayload::Float32(value) => row.payload_float32 = Some(value),
DataRowPayload::Float32Array(value_list) => {
row.payload_float32_array = Some(value_list);
}
DataRowPayload::Float64(value) => row.payload_float64 = Some(value),
DataRowPayload::Float64Array(value_list) => {
row.payload_float64_array = Some(value_list);
}
DataRowPayload::Int(value) => row.payload_int = Some(value),
DataRowPayload::IntArray(value_list) => row.payload_int_array = Some(value_list),
DataRowPayload::String(value) => row.payload_string = Some(value),
DataRowPayload::Struct(value) => row.payload_struct = Some(value),
DataRowPayload::Vector {
vector,
vector_end_bound,
vector_start_bound,
} => {
row.vector = Some(vector);
row.vector_end_bound = Some(vector_end_bound);
row.vector_start_bound = Some(vector_start_bound);
}
}
row
}
pub fn payload(&self) -> Option<DataRowPayload> {
let variant = match self.variant.as_deref() {
None if self.vector.is_some() => DataRowVariant::Vector,
None => return None,
Some(variant_text) => DataRowVariant::parse(variant_text)?,
};
match variant {
DataRowVariant::Float32 => self.payload_float32.map(DataRowPayload::Float32),
DataRowVariant::Float32Array => self
.payload_float32_array
.clone()
.map(DataRowPayload::Float32Array),
DataRowVariant::Float64 => self.payload_float64.map(DataRowPayload::Float64),
DataRowVariant::Float64Array => self
.payload_float64_array
.clone()
.map(DataRowPayload::Float64Array),
DataRowVariant::Int => self.payload_int.map(DataRowPayload::Int),
DataRowVariant::IntArray => {
self.payload_int_array.clone().map(DataRowPayload::IntArray)
}
DataRowVariant::String => self.payload_string.clone().map(DataRowPayload::String),
DataRowVariant::Struct => self.payload_struct.clone().map(DataRowPayload::Struct),
DataRowVariant::Vector => {
let vector = self.vector.clone()?;
let vector_end_bound = self.vector_end_bound?;
let vector_start_bound = self.vector_start_bound?;
Some(DataRowPayload::Vector {
vector,
vector_end_bound,
vector_start_bound,
})
}
}
}
pub fn validate_payload(&self) -> HorizonResult<()> {
let populated_arm_list: Vec<&'static str> = [
("payload_float32", self.payload_float32.is_some()),
(
"payload_float32_array",
self.payload_float32_array.is_some(),
),
("payload_float64", self.payload_float64.is_some()),
(
"payload_float64_array",
self.payload_float64_array.is_some(),
),
("payload_int", self.payload_int.is_some()),
("payload_int_array", self.payload_int_array.is_some()),
("payload_string", self.payload_string.is_some()),
("payload_struct", self.payload_struct.is_some()),
("vector", self.vector.is_some()),
]
.into_iter()
.filter_map(|(field_name, is_set)| is_set.then_some(field_name))
.collect();
let active_arm = if let [arm] = populated_arm_list.as_slice() {
*arm
} else {
let message = "exactly one of vector or payload_* must be set".to_owned();
return Err(HorizonError::InvalidDataRowPayload { message });
};
if active_arm == "vector"
&& (self.vector_start_bound.is_none() || self.vector_end_bound.is_none())
{
let message =
"vector payload requires vector_start_bound and vector_end_bound".to_owned();
return Err(HorizonError::InvalidDataRowPayload { message });
}
match self.variant.as_deref() {
None => {
if active_arm != "vector" {
let message =
"variant is required when the payload is not a legacy vector".to_owned();
return Err(HorizonError::InvalidDataRowPayload { message });
}
}
Some(variant_text) => {
let Some(variant) = DataRowVariant::parse(variant_text) else {
let message = format!("unknown variant: {variant_text}");
return Err(HorizonError::InvalidDataRowPayload { message });
};
let expected_arm = variant.payload_arm();
if expected_arm != active_arm {
let message = format!(
"variant '{variant_text}' does not match populated payload arm '{active_arm}'"
);
return Err(HorizonError::InvalidDataRowPayload { message });
}
}
}
Ok(())
}
}
impl From<&DataRow> for VariantDataRow {
fn from(row: &DataRow) -> Self {
Self {
created_datetime: row.created_datetime,
data_stream_id: row.data_stream_id,
data_type: row.data_type.clone(),
datetime: row.datetime,
modified_datetime: row.modified_datetime,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id: row.specification_id,
variant: Some(DataRowVariant::Vector.as_str().to_owned()),
vector: Some(row.vector.clone()),
vector_end_bound: Some(row.vector_end_bound),
vector_start_bound: Some(row.vector_start_bound),
}
}
}
impl TryFrom<VariantDataRow> for DataRow {
type Error = HorizonError;
fn try_from(row: VariantDataRow) -> Result<Self, Self::Error> {
let Some(DataRowPayload::Vector {
vector,
vector_end_bound,
vector_start_bound,
}) = row.payload()
else {
let message = "a non-vector variant cannot be converted to DataRow".to_owned();
return Err(HorizonError::InvalidDataRowPayload { message });
};
Ok(Self {
created_datetime: row.created_datetime,
data_stream_id: row.data_stream_id,
data_type: row.data_type,
datetime: row.datetime,
modified_datetime: row.modified_datetime,
specification_id: row.specification_id,
vector,
vector_end_bound,
vector_start_bound,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataCollection {
pub collected_secs: Option<i32>,
pub collected_size: Option<i64>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub ingest_pipeline_version: Option<String>,
pub ingested_datetime: Option<DateTime<Utc>>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: String,
pub organization_id: Option<Uuid>,
pub platform_id: Uuid,
pub platform_software_version: Option<String>,
pub s3_prefix: Option<String>,
pub status: Option<DataCollectionStatus>,
pub upload_complete_datetime: Option<DateTime<Utc>>,
pub xml_sensor_config: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataCollectionInterval {
pub collected_secs: Option<i32>,
pub collected_size: Option<i64>,
pub created_datetime: Option<DateTime<Utc>>,
pub data_collection_id: Uuid,
pub data_stream_id: Uuid,
pub end_datetime: DateTime<Utc>,
pub group_hash_id: Option<String>,
pub has_classification_override: Option<bool>,
pub has_detection_override: Option<bool>,
pub has_track_override: Option<bool>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub processing_end_datetime: Option<DateTime<Utc>>,
pub processing_start_datetime: Option<DateTime<Utc>>,
pub start_datetime: DateTime<Utc>,
pub status: Option<DataCollectionStatus>,
}
#[allow(
clippy::exhaustive_enums,
reason = "variants mirror a closed Postgres ENUM, so exhaustive matching downstream is desirable"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DataCollectionStatus {
Failed,
Ingested,
Pending,
Processing,
Registered,
}
#[allow(
clippy::exhaustive_enums,
reason = "variants mirror a closed Postgres ENUM, so exhaustive matching downstream is desirable"
)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
#[sqlx(
type_name = "horizon_public.data_stream_query_source",
rename_all = "lowercase"
)]
#[serde(rename_all = "lowercase")]
pub enum DataStreamQuerySource {
#[default]
Postgres,
Trino,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DataStream {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
#[serde(default)]
pub query_source: DataStreamQuerySource,
}
impl DataStream {
#[must_use]
pub fn from_platform_and_name(platform_id: Uuid, data_stream_name: &str) -> Self {
let name = capitalize(data_stream_name);
Self {
created_datetime: None,
id: Some(data_stream_uuid(platform_id, data_stream_name)),
modified_datetime: None,
name: Some(name),
organization_id: None,
platform_id: Some(platform_id),
query_source: DataStreamQuerySource::Postgres,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ElevationAlias {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub elevation_increment: Option<f64>,
pub id: Option<Uuid>,
pub lower_elevation: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub upper_elevation: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FocusRange {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FrequencyBandAlias {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub lower_frequency: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub upper_frequency: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FrequencyResolutionAlias {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub frequency_bin_count: Option<i32>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetadataRow {
pub altitude: Option<f64>,
pub created_datetime: Option<DateTime<Utc>>,
pub data_stream_id: Uuid,
pub datetime: DateTime<Utc>,
pub heading: Option<f64>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub modified_datetime: Option<DateTime<Utc>>,
pub pitch: Option<f64>,
pub roll: Option<f64>,
pub speed: Option<f64>,
pub speed_over_ground: Option<f64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Mission {
pub created_datetime: Option<DateTime<Utc>>,
pub end_datetime: Option<DateTime<Utc>>,
pub free_text: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub position: Option<Position>,
pub start_datetime: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MissionPlatform {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub mission_id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Normalizer {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub is_archived: Option<bool>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Ontology {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OntologyClass {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub ontology_id: Uuid,
pub order: Option<i32>,
pub organization_id: Option<Uuid>,
pub parent_id: Option<Uuid>,
pub relationship_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Platform {
pub created_datetime: Option<DateTime<Utc>>,
pub end_datetime: Option<DateTime<Utc>>,
pub free_text: Option<String>,
pub id: Option<Uuid>,
pub kind_id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub position: Option<Position>,
pub start_datetime: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformAudioSpecification {
pub audio_specification_id: Uuid,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Uuid,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformBeamgramSpecification {
pub beamgram_specification_id: Option<Uuid>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformBearingTimeRecordSpecification {
pub bearing_time_record_specification_id: Option<Uuid>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformInformation {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Uuid,
pub properties: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformKind {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub image_url: Option<String>,
pub long_description: Option<String>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub short_description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlatformSpectrogramSpecification {
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub platform_id: Option<Uuid>,
pub spectrogram_specification_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectionalSpectrogramSpecification {
pub bearing_lagging_rows: Option<i64>,
pub bearing_leading_rows: Option<i64>,
pub created_datetime: Option<DateTime<Utc>>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub organization_id: Option<Uuid>,
pub spectrogram_specification_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SpectrogramSpecification {
pub amplitude_unit_mode: Option<String>,
pub channel: Option<i32>,
pub channel_role: Option<String>,
pub created_datetime: Option<DateTime<Utc>>,
pub fft_sample_count: Option<i32>,
pub fft_sample_overlap_count: Option<i32>,
pub frequency_bin_count: Option<i32>,
pub frequency_spacing: Option<f64>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub normalizer: Option<String>,
pub organization_id: Option<Uuid>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateRateAlias {
pub created_datetime: Option<DateTime<Utc>>,
pub description: Option<String>,
pub id: Option<Uuid>,
pub modified_datetime: Option<DateTime<Utc>>,
pub name: Option<String>,
pub organization_id: Option<Uuid>,
pub update_rate_ms: Option<i64>,
}
#[must_use]
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
chars.next().map_or_else(String::new, |first| {
let upper: String = first.to_uppercase().collect();
format!("{upper}{}", chars.as_str().to_lowercase())
})
}
#[must_use]
pub fn data_stream_uuid(platform_id: Uuid, data_stream_name: &str) -> Uuid {
let name = capitalize(data_stream_name);
name_to_uuid(&format!("{platform_id}:{name}"))
}
#[must_use]
pub fn name_to_uuid(name: &str) -> Uuid {
const NAMESPACE: Uuid = Uuid::from_bytes([
0x13, 0xdb, 0x1b, 0xb5, 0x27, 0xac, 0x4b, 0xf7, 0x81, 0x86, 0x10, 0x43, 0x34, 0x7c, 0x42,
0x47,
]);
let normalized = name.trim().to_lowercase();
Uuid::new_v5(&NAMESPACE, normalized.as_bytes())
}
#[cfg(test)]
#[allow(
clippy::default_numeric_fallback,
clippy::unwrap_used,
reason = "tests use numeric literals and unwrap for brevity"
)]
mod tests {
use super::*;
#[test]
fn capitalize_matches_python() {
assert_eq!(capitalize("hydrophone"), "Hydrophone");
assert_eq!(capitalize("Hydrophone"), "Hydrophone");
assert_eq!(capitalize("hYDROPHONE"), "Hydrophone");
assert_eq!(capitalize("HYDROPHONE"), "Hydrophone");
assert_eq!(capitalize(""), "");
assert_eq!(capitalize("a"), "A");
}
#[test]
fn data_stream_from_platform_and_name() {
let platform_id = Uuid::new_v4();
let ds = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.platform_id, Some(platform_id));
assert_eq!(ds.name.as_deref(), Some("Audio"));
assert!(ds.id.is_some());
let ds2 = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.id, ds2.id);
}
#[test]
fn data_stream_uuid_is_deterministic() {
let platform_id = Uuid::new_v4();
let a = data_stream_uuid(platform_id, "audio");
let b = data_stream_uuid(platform_id, "audio");
assert_eq!(a, b);
}
#[test]
fn data_stream_uuid_matches_from_platform_and_name() {
let platform_id = Uuid::new_v4();
let ds = DataStream::from_platform_and_name(platform_id, "audio");
assert_eq!(ds.id, Some(data_stream_uuid(platform_id, "audio")));
}
#[test]
fn mission_serde_round_trip() {
let mission = Mission {
created_datetime: None,
end_datetime: None,
free_text: None,
id: Some(Uuid::new_v4()),
modified_datetime: None,
name: Some("Test Mission".to_owned()),
organization_id: None,
position: Some(Position(1.5_f64, 2.5_f64)),
start_datetime: Some(Utc::now()),
};
let json = serde_json::to_string(&mission).unwrap();
let deserialized: Mission = serde_json::from_str(&json).unwrap();
assert_eq!(mission, deserialized);
}
#[test]
fn name_to_uuid_is_deterministic() {
let a = name_to_uuid("test");
let b = name_to_uuid("test");
assert_eq!(a, b);
}
#[test]
fn name_to_uuid_matches_python_sdk() {
let expected = Uuid::parse_str("e32be881-c656-5b8e-8dd7-889066ac1c83").unwrap();
assert_eq!(name_to_uuid("Test Platform"), expected);
let expected2 = Uuid::parse_str("eaeebe45-0eaa-59e4-9b79-7ceda69789e2").unwrap();
assert_eq!(name_to_uuid("hello world"), expected2);
}
#[test]
fn name_to_uuid_normalizes_whitespace_and_case() {
let a = name_to_uuid(" Hello World ");
let b = name_to_uuid("hello world");
assert_eq!(a, b);
}
#[test]
fn platform_information_serde_with_json_properties() {
let info = PlatformInformation {
created_datetime: None,
id: None,
modified_datetime: None,
organization_id: None,
platform_id: Uuid::new_v4(),
properties: serde_json::json!({"key": "value", "nested": {"a": 1_i32}}),
};
let json = serde_json::to_string(&info).unwrap();
let deserialized: PlatformInformation = serde_json::from_str(&json).unwrap();
assert_eq!(info, deserialized);
}
#[test]
fn data_row_from_payload_passes_validation() {
let row = VariantDataRow::from_payload(
Uuid::nil(),
Utc::now(),
"cot".to_owned(),
Uuid::nil(),
DataRowPayload::Struct(serde_json::json!({"callsign": "ALPHA"})),
);
row.validate_payload().unwrap();
}
#[test]
fn data_row_allows_legacy_vector_without_variant() {
let row = VariantDataRow {
created_datetime: None,
data_stream_id: Uuid::nil(),
data_type: "audio".to_owned(),
datetime: Utc::now(),
modified_datetime: None,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id: Uuid::nil(),
variant: None,
vector: Some(vec![1.0, 2.0]),
vector_end_bound: Some(1.0),
vector_start_bound: Some(0.0),
};
row.validate_payload().unwrap();
}
#[test]
fn data_row_rejects_vector_without_bounds() {
let row = VariantDataRow {
created_datetime: None,
data_stream_id: Uuid::nil(),
data_type: "audio".to_owned(),
datetime: Utc::now(),
modified_datetime: None,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id: Uuid::nil(),
variant: None,
vector: Some(vec![1.0, 2.0]),
vector_end_bound: None,
vector_start_bound: Some(0.0),
};
let error = row.validate_payload().unwrap_err();
assert!(
error
.to_string()
.contains("vector payload requires vector_start_bound and vector_end_bound"),
"got: {error}"
);
assert!(row.payload().is_none());
}
#[test]
fn data_row_rejects_mixed_payload_fields() {
let mut row = VariantDataRow::from_payload(
Uuid::nil(),
Utc::now(),
"cot".to_owned(),
Uuid::nil(),
DataRowPayload::Struct(serde_json::json!({})),
);
row.payload_float64 = Some(22.5);
let error = row.validate_payload().unwrap_err();
assert!(
error
.to_string()
.contains("exactly one of vector or payload_* must be set"),
"got: {error}"
);
}
#[test]
fn data_row_rejects_mismatched_variant() {
let mut row = VariantDataRow::from_payload(
Uuid::nil(),
Utc::now(),
"cot".to_owned(),
Uuid::nil(),
DataRowPayload::Struct(serde_json::json!({})),
);
row.variant = Some("float64".to_owned());
let error = row.validate_payload().unwrap_err();
assert!(
error
.to_string()
.contains("does not match populated payload arm"),
"got: {error}"
);
}
#[test]
fn data_row_payload_resolves_legacy_vector() {
let row = VariantDataRow {
created_datetime: None,
data_stream_id: Uuid::nil(),
data_type: "audio".to_owned(),
datetime: Utc::now(),
modified_datetime: None,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id: Uuid::nil(),
variant: None,
vector: Some(vec![1.0, 2.0]),
vector_end_bound: Some(1.0),
vector_start_bound: Some(0.0),
};
assert!(matches!(row.payload(), Some(DataRowPayload::Vector { .. })));
}
#[test]
fn data_row_payload_rejects_unknown_variant_even_with_vector() {
let row = VariantDataRow {
created_datetime: None,
data_stream_id: Uuid::nil(),
data_type: "audio".to_owned(),
datetime: Utc::now(),
modified_datetime: None,
payload_float32: None,
payload_float32_array: None,
payload_float64: None,
payload_float64_array: None,
payload_int: None,
payload_int_array: None,
payload_string: None,
payload_struct: None,
specification_id: Uuid::nil(),
variant: Some("not-a-variant".to_owned()),
vector: Some(vec![1.0, 2.0]),
vector_end_bound: Some(1.0),
vector_start_bound: Some(0.0),
};
assert!(row.payload().is_none());
}
}