use aion_core::{SortDirection, WorkflowSort, WorkflowSortField, WorkflowStatus};
use chrono::{DateTime, Utc};
use super::VisibilityRecord;
#[must_use]
pub fn page_key(record: &VisibilityRecord, sort: WorkflowSort) -> Vec<u8> {
let mut key = sort_key(record, sort);
key.extend_from_slice(record.workflow_id.as_uuid().as_bytes());
key
}
#[must_use]
pub fn sort_key(record: &VisibilityRecord, sort: WorkflowSort) -> Vec<u8> {
let ascending = match sort.field {
WorkflowSortField::StartedAt => encode_instant(Some(record.started_at)),
WorkflowSortField::UpdatedAt => encode_instant(Some(record.updated_at)),
WorkflowSortField::EndedAt => encode_instant(record.ended_at),
WorkflowSortField::WorkflowType => encode_text(&record.workflow_type),
WorkflowSortField::Status => encode_text(status_name(record.status)),
WorkflowSortField::DisplayName => {
encode_text(&record.display_name.as_deref().unwrap_or("").to_lowercase())
}
};
match sort.direction {
SortDirection::Asc => ascending,
SortDirection::Desc => ascending.into_iter().map(|byte| !byte).collect(),
}
}
#[must_use]
pub fn encode_instant(instant: Option<DateTime<Utc>>) -> Vec<u8> {
let micros = instant.map_or(i64::MIN, |instant| instant.timestamp_micros());
let ordered = u64::from_ne_bytes((micros ^ i64::MIN).to_ne_bytes());
ordered.to_be_bytes().to_vec()
}
#[must_use]
pub fn encode_text(text: &str) -> Vec<u8> {
let mut bytes = Vec::with_capacity(text.len() + 1);
bytes.extend_from_slice(text.as_bytes());
bytes.push(0);
bytes
}
#[must_use]
pub const fn status_name(status: WorkflowStatus) -> &'static str {
match status {
WorkflowStatus::Running => "running",
WorkflowStatus::Completed => "completed",
WorkflowStatus::Failed => "failed",
WorkflowStatus::Cancelled => "cancelled",
WorkflowStatus::TimedOut => "timed_out",
WorkflowStatus::ContinuedAsNew => "continued_as_new",
WorkflowStatus::Paused => "paused",
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use aion_core::{
RunId, SortDirection, WorkflowId, WorkflowSort, WorkflowSortField, WorkflowStatus,
};
use chrono::{DateTime, Utc};
use super::{encode_instant, encode_text, page_key, sort_key};
use crate::visibility::VisibilityRecord;
fn record(id: u128, display_name: Option<&str>, ended: Option<i64>) -> VisibilityRecord {
let started = DateTime::<Utc>::default();
VisibilityRecord {
namespace: String::from("default"),
workflow_id: WorkflowId::new(uuid::Uuid::from_u128(id)),
run_id: RunId::new(uuid::Uuid::from_u128(id + 1)),
workflow_type: String::from("checkout"),
status: WorkflowStatus::Running,
started_at: started,
updated_at: started,
ended_at: ended.map(|seconds| started + chrono::Duration::seconds(seconds)),
parent: None,
display_name: display_name.map(str::to_owned),
kind: None,
failed_step: None,
failure_reason: None,
search_attributes: HashMap::new(),
outstanding_leases: Vec::new(),
package_version: None,
}
}
#[test]
fn instants_order_bytewise_including_negative_and_absent() {
let epoch = DateTime::<Utc>::default();
let before = epoch - chrono::Duration::days(1);
let after = epoch + chrono::Duration::microseconds(1);
assert!(encode_instant(None) < encode_instant(Some(before)));
assert!(encode_instant(Some(before)) < encode_instant(Some(epoch)));
assert!(encode_instant(Some(epoch)) < encode_instant(Some(after)));
}
#[test]
fn a_string_sorts_before_its_extension_in_both_directions() {
assert!(encode_text("a") < encode_text("ab"));
let desc = |text: &str| {
encode_text(text)
.into_iter()
.map(|b| !b)
.collect::<Vec<_>>()
};
assert!(desc("ab") < desc("a"));
}
#[test]
fn descending_key_reverses_ascending_order_and_keeps_id_tiebreak() {
let early = record(1, None, Some(1));
let late = record(2, None, Some(5));
let asc = WorkflowSort {
field: WorkflowSortField::EndedAt,
direction: SortDirection::Asc,
};
let desc = WorkflowSort {
field: WorkflowSortField::EndedAt,
direction: SortDirection::Desc,
};
assert!(page_key(&early, asc) < page_key(&late, asc));
assert!(page_key(&late, desc) < page_key(&early, desc));
let twin_a = record(1, None, Some(1));
let twin_b = record(2, None, Some(1));
assert!(
page_key(&twin_a, desc) < page_key(&twin_b, desc),
"ties break on id ascending even descending"
);
}
#[test]
fn running_sorts_first_ascending_by_ended_at() {
let running = record(9, None, None);
let ended = record(1, None, Some(1));
let asc = WorkflowSort {
field: WorkflowSortField::EndedAt,
direction: SortDirection::Asc,
};
assert!(sort_key(&running, asc) < sort_key(&ended, asc));
}
#[test]
fn display_name_orders_case_insensitively_and_unnamed_first() {
let sort = WorkflowSort {
field: WorkflowSortField::DisplayName,
direction: SortDirection::Asc,
};
assert_eq!(
sort_key(&record(1, Some("Beta"), None), sort),
sort_key(&record(1, Some("beta"), None), sort)
);
assert!(
sort_key(&record(1, None, None), sort)
< sort_key(&record(1, Some("alpha"), None), sort)
);
assert!(
sort_key(&record(1, Some("alpha"), None), sort)
< sort_key(&record(1, Some("Beta"), None), sort)
);
}
}