1use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18use serde_json::{json, Map, Value};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum Rank {
23 Endorsed,
25 Verified,
28 Other,
31}
32
33impl Rank {
34 pub const fn number(self) -> u8 {
36 match self {
37 Rank::Endorsed => 1,
38 Rank::Verified => 2,
39 Rank::Other => 3,
40 }
41 }
42
43 pub const fn is_ranked(self) -> bool {
45 !matches!(self, Rank::Other)
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
51#[serde(rename_all = "lowercase")]
52pub enum Group {
53 Sources,
54 Verified,
55 Others,
56}
57
58impl Group {
59 pub fn of(rank: Option<u8>) -> Group {
61 match rank {
62 Some(1) => Group::Sources,
63 Some(2) => Group::Verified,
64 _ => Group::Others,
65 }
66 }
67}
68
69pub fn sets(v: &Value) -> bool {
71 match v {
72 Value::Null => false,
73 Value::String(s) => !s.trim().is_empty(),
74 Value::Array(a) => !a.is_empty(),
75 Value::Object(o) => !o.is_empty(),
76 _ => true,
77 }
78}
79
80pub fn comparable(v: &Value) -> Value {
83 match v {
84 Value::String(s) => {
85 let t = s.trim();
86 match chrono::DateTime::parse_from_rfc3339(t) {
87 Ok(at) => json!(at.timestamp_millis()),
88 Err(_) => json!(t),
89 }
90 }
91 Value::Array(a) => Value::Array(a.iter().map(comparable).collect()),
92 Value::Object(o) => Value::Object(o.iter().map(|(k, v)| (k.clone(), comparable(v))).collect()),
93 other => other.clone(),
94 }
95}
96
97pub fn shown_as(field: &str, v: &Value) -> Value {
101 fn drop_record_uris(v: &Value) -> Value {
102 match v {
103 Value::Array(a) => Value::Array(a.iter().map(drop_record_uris).collect()),
104 Value::Object(o) => Value::Object(
105 o.iter()
106 .filter(|(k, x)| !(k.as_str() == "uri" && x.as_str().is_some_and(|s| s.starts_with("at://"))))
107 .map(|(k, x)| (k.clone(), drop_record_uris(x)))
108 .collect(),
109 ),
110 other => other.clone(),
111 }
112 }
113 if field == "locations" {
114 comparable(&drop_record_uris(v))
115 } else {
116 comparable(v)
117 }
118}
119
120#[derive(Debug, Clone, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct GetVersions {
126 pub uri: String,
127 pub record: Value,
128 #[serde(default)]
129 pub composition: Option<Composition>,
130 #[serde(default)]
131 pub versions: Vec<Version>,
132 #[serde(default)]
133 pub left_out: u64,
134}
135
136#[derive(Debug, Clone, Deserialize)]
138pub struct Composition {
139 #[serde(default)]
140 pub byline: Option<String>,
141 #[serde(default)]
142 pub fields: Vec<FieldChoice>,
143}
144
145#[derive(Debug, Clone, Deserialize)]
147pub struct FieldChoice {
148 pub field: String,
149 pub from: String,
150 #[serde(default)]
151 pub conflicts: Option<u64>,
152}
153
154#[derive(Debug, Clone, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct Version {
158 pub uri: String,
159 pub did: String,
160 pub record: Value,
161 #[serde(default)]
162 pub name: Option<String>,
163 #[serde(default)]
164 pub handle: Option<String>,
165 #[serde(default)]
166 pub rank: Option<u8>,
167 #[serde(default)]
168 pub mirror: bool,
169 #[serde(default)]
170 pub verification: Option<Value>,
171 #[serde(default)]
172 pub indexed_at: Option<String>,
173}
174
175#[derive(Debug, Clone, PartialEq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct VersionRef {
181 pub uri: String,
182 pub did: String,
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub name: Option<String>,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub handle: Option<String>,
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub rank: Option<u8>,
190 pub mirror: bool,
191 pub verified: bool,
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize)]
197pub struct FieldState {
198 pub value: Value,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub from: Option<VersionRef>,
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub conflicts: Option<u64>,
204}
205
206#[derive(Debug, Clone, PartialEq, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct VersionEntry {
210 #[serde(flatten)]
211 pub version: VersionRef,
212 pub group: Group,
213 pub record: Value,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub indexed_at: Option<String>,
216 pub showing: bool,
218 pub differs: Vec<String>,
220 pub same: Vec<String>,
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct EventState {
228 pub uri: String,
229 pub fields: BTreeMap<String, FieldState>,
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub byline: Option<VersionRef>,
234 pub versions: Vec<VersionEntry>,
235 pub picked: Option<String>,
237 pub left_out: u64,
239}
240
241fn hidden(field: &str) -> bool {
243 field == "$type" || field == "createdAt"
244}
245
246fn ref_of(v: &Version) -> VersionRef {
247 VersionRef {
248 uri: v.uri.clone(),
249 did: v.did.clone(),
250 name: v.name.clone(),
251 handle: v.handle.clone(),
252 rank: v.rank,
253 mirror: v.mirror,
254 verified: v.verification.is_some(),
255 }
256}
257
258pub fn event_state(data: &GetVersions, picked: Option<&str>) -> EventState {
261 let empty = Map::new();
262 let by_uri = |uri: &str| data.versions.iter().find(|v| v.uri == uri);
263 let picked = picked.and_then(by_uri);
264 let shown = picked.map_or(&data.record, |v| &v.record).as_object().unwrap_or(&empty);
265 let choices = data.composition.as_ref().map_or(&[][..], |c| &c.fields[..]);
266 let only = (data.versions.len() == 1).then(|| data.versions[0].uri.as_str());
268
269 let fields = shown
270 .iter()
271 .filter(|(name, value)| !hidden(name) && sets(value))
272 .map(|(name, value)| {
273 let choice = choices.iter().find(|f| &f.field == name);
274 let from = match picked {
275 Some(v) => Some(v.uri.as_str()),
276 None => choice.map(|c| c.from.as_str()).or(only),
277 };
278 let state = FieldState {
279 value: value.clone(),
280 from: from.and_then(by_uri).map(ref_of),
281 conflicts: choice.and_then(|c| c.conflicts).filter(|n| *n >= 2),
282 };
283 (name.clone(), state)
284 })
285 .collect();
286
287 let byline = picked.map(|v| v.uri.as_str()).or(data.composition.as_ref().and_then(|c| c.byline.as_deref())).or(only);
288 let mut versions: Vec<VersionEntry> = data
289 .versions
290 .iter()
291 .map(|v| {
292 let (mut differs, mut same) = (vec![], vec![]);
293 for (name, value) in v.record.as_object().unwrap_or(&empty) {
294 if hidden(name) || !sets(value) {
295 continue;
296 }
297 match shown.get(name) {
298 Some(s) if shown_as(name, s) == shown_as(name, value) => same.push(name.clone()),
299 _ => differs.push(name.clone()),
300 }
301 }
302 VersionEntry {
303 version: ref_of(v),
304 group: Group::of(v.rank),
305 record: v.record.clone(),
306 indexed_at: v.indexed_at.clone(),
307 showing: Some(v.uri.as_str()) == byline,
308 differs,
309 same,
310 }
311 })
312 .collect();
313 versions.sort_by_key(|e| (e.group, !e.showing));
315
316 EventState {
317 uri: data.uri.clone(),
318 fields,
319 byline: byline.and_then(by_uri).map(ref_of),
320 versions,
321 picked: picked.map(|v| v.uri.clone()),
322 left_out: data.left_out,
323 }
324}