use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Rank {
Endorsed,
Verified,
Other,
}
impl Rank {
pub const fn number(self) -> u8 {
match self {
Rank::Endorsed => 1,
Rank::Verified => 2,
Rank::Other => 3,
}
}
pub const fn is_ranked(self) -> bool {
!matches!(self, Rank::Other)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Group {
Sources,
Verified,
Others,
}
impl Group {
pub fn of(rank: Option<u8>) -> Group {
match rank {
Some(1) => Group::Sources,
Some(2) => Group::Verified,
_ => Group::Others,
}
}
}
pub fn sets(v: &Value) -> bool {
match v {
Value::Null => false,
Value::String(s) => !s.trim().is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(o) => !o.is_empty(),
_ => true,
}
}
pub fn comparable(v: &Value) -> Value {
match v {
Value::String(s) => {
let t = s.trim();
match chrono::DateTime::parse_from_rfc3339(t) {
Ok(at) => json!(at.timestamp_millis()),
Err(_) => json!(t),
}
}
Value::Array(a) => Value::Array(a.iter().map(comparable).collect()),
Value::Object(o) => Value::Object(o.iter().map(|(k, v)| (k.clone(), comparable(v))).collect()),
other => other.clone(),
}
}
pub fn shown_as(field: &str, v: &Value) -> Value {
fn drop_record_uris(v: &Value) -> Value {
match v {
Value::Array(a) => Value::Array(a.iter().map(drop_record_uris).collect()),
Value::Object(o) => Value::Object(
o.iter()
.filter(|(k, x)| !(k.as_str() == "uri" && x.as_str().is_some_and(|s| s.starts_with("at://"))))
.map(|(k, x)| (k.clone(), drop_record_uris(x)))
.collect(),
),
other => other.clone(),
}
}
if field == "locations" {
comparable(&drop_record_uris(v))
} else {
comparable(v)
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetVersions {
pub uri: String,
pub record: Value,
#[serde(default)]
pub composition: Option<Composition>,
#[serde(default)]
pub versions: Vec<Version>,
#[serde(default)]
pub left_out: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Composition {
#[serde(default)]
pub byline: Option<String>,
#[serde(default)]
pub fields: Vec<FieldChoice>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FieldChoice {
pub field: String,
pub from: String,
#[serde(default)]
pub conflicts: Option<u64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Version {
pub uri: String,
pub did: String,
pub record: Value,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub handle: Option<String>,
#[serde(default)]
pub rank: Option<u8>,
#[serde(default)]
pub mirror: bool,
#[serde(default)]
pub verification: Option<Value>,
#[serde(default)]
pub indexed_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionRef {
pub uri: String,
pub did: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rank: Option<u8>,
pub mirror: bool,
pub verified: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct FieldState {
pub value: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<VersionRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conflicts: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionEntry {
#[serde(flatten)]
pub version: VersionRef,
pub group: Group,
pub record: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_at: Option<String>,
pub showing: bool,
pub differs: Vec<String>,
pub same: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EventState {
pub uri: String,
pub fields: BTreeMap<String, FieldState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub byline: Option<VersionRef>,
pub versions: Vec<VersionEntry>,
pub picked: Option<String>,
pub left_out: u64,
}
fn hidden(field: &str) -> bool {
field == "$type" || field == "createdAt"
}
fn ref_of(v: &Version) -> VersionRef {
VersionRef {
uri: v.uri.clone(),
did: v.did.clone(),
name: v.name.clone(),
handle: v.handle.clone(),
rank: v.rank,
mirror: v.mirror,
verified: v.verification.is_some(),
}
}
pub fn event_state(data: &GetVersions, picked: Option<&str>) -> EventState {
let empty = Map::new();
let by_uri = |uri: &str| data.versions.iter().find(|v| v.uri == uri);
let picked = picked.and_then(by_uri);
let shown = picked.map_or(&data.record, |v| &v.record).as_object().unwrap_or(&empty);
let choices = data.composition.as_ref().map_or(&[][..], |c| &c.fields[..]);
let only = (data.versions.len() == 1).then(|| data.versions[0].uri.as_str());
let fields = shown
.iter()
.filter(|(name, value)| !hidden(name) && sets(value))
.map(|(name, value)| {
let choice = choices.iter().find(|f| &f.field == name);
let from = match picked {
Some(v) => Some(v.uri.as_str()),
None => choice.map(|c| c.from.as_str()).or(only),
};
let state = FieldState {
value: value.clone(),
from: from.and_then(by_uri).map(ref_of),
conflicts: choice.and_then(|c| c.conflicts).filter(|n| *n >= 2),
};
(name.clone(), state)
})
.collect();
let byline = picked.map(|v| v.uri.as_str()).or(data.composition.as_ref().and_then(|c| c.byline.as_deref())).or(only);
let mut versions: Vec<VersionEntry> = data
.versions
.iter()
.map(|v| {
let (mut differs, mut same) = (vec![], vec![]);
for (name, value) in v.record.as_object().unwrap_or(&empty) {
if hidden(name) || !sets(value) {
continue;
}
match shown.get(name) {
Some(s) if shown_as(name, s) == shown_as(name, value) => same.push(name.clone()),
_ => differs.push(name.clone()),
}
}
VersionEntry {
version: ref_of(v),
group: Group::of(v.rank),
record: v.record.clone(),
indexed_at: v.indexed_at.clone(),
showing: Some(v.uri.as_str()) == byline,
differs,
same,
}
})
.collect();
versions.sort_by_key(|e| (e.group, !e.showing));
EventState {
uri: data.uri.clone(),
fields,
byline: byline.and_then(by_uri).map(ref_of),
versions,
picked: picked.map(|v| v.uri.clone()),
left_out: data.left_out,
}
}