Skip to main content

techne_sdk/
versions.rs

1//! One commons event or place as its versions (§24 versions and trust): the one definition of how
2//! versions rank and compare, used by the rules curator when it composes a curated record and by every
3//! app (through the SDK's WebAssembly) when it shows one.
4//!
5//! A version's rank comes from its author ([`Rank`]: 1 a source the commons root endorses, 2 a verified
6//! account or a member of an endorsed collective, 3 everyone else). A field "says something" when
7//! [`sets`] holds, and two values say the same thing when their [`comparable`] forms are equal (strings
8//! trimmed, a datetime as its instant, recursively).
9//!
10//! [`event_state`] turns a `pub.techne.draft.commons.getVersions` answer into what a screen shows: each
11//! field's value, the version it is from and its conflict count, the byline, and the versions grouped
12//! Sources / Verified / Others with the fields where each differs from what is shown. A person may pick
13//! another version: the fields then show that version's own values.
14
15use std::collections::BTreeMap;
16
17use serde::{Deserialize, Serialize};
18use serde_json::{json, Map, Value};
19
20/// A version's rank (§24 VT1), from its author. Lower speaks first; the order is the derive's.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum Rank {
23    /// A source the root endorses for the record's collection (a mirror, a rule, a collective).
24    Endorsed,
25    /// A verified account (a `verification` by an issuer the root endorses for it), or a member of a
26    /// collective the root endorses to write membership claims.
27    Verified,
28    /// Everyone else. Fills blanks or stands alone, never raises a conflict, never merges two
29    /// clusters (VT6).
30    Other,
31}
32
33impl Rank {
34    /// The number the provenance's `composition.ranks` and `getVersions` carry (1, 2, 3).
35    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    /// Rank 1 or 2: it can raise a conflict and merge clusters.
44    pub const fn is_ranked(self) -> bool {
45        !matches!(self, Rank::Other)
46    }
47}
48
49/// How a version is listed, in this order.
50#[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    /// A version's group from the rank `getVersions` carries: 1 Sources, 2 Verified, else Others.
60    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
69/// Whether a field value says something (blank strings and empty containers do not).
70pub 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
80/// A value as compared: strings trimmed and a datetime as its instant, recursively, so two versions
81/// saying the same thing in other words (`18:00:00Z`, `18:00:00.000Z`) are one value.
82pub 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
97/// A shown field as a reader compares it: [`comparable`], with an event's places compared by what they
98/// say, never by which copy of a place they point at (the curated record names the curated place, a
99/// version names its own copy; the curator maps both to the curated place before comparing).
100pub 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// ── the getVersions answer ───────────────────────────────────────────────────────────────────────
121
122/// `pub.techne.draft.commons.getVersions`'s output, as far as [`event_state`] reads it.
123#[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/// The provenance's `composition` (VT2).
137#[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/// Which version gave one field, and the ranked versions' distinct values for it.
146#[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/// One version as `getVersions` answers it.
155#[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// ── what a screen draws ──────────────────────────────────────────────────────────────────────────
176
177/// Who a value or the byline is from.
178#[derive(Debug, Clone, PartialEq, Serialize)]
179#[serde(rename_all = "camelCase")]
180pub struct VersionRef {
181    pub uri: String,
182    pub did: String,
183    /// Who to show it as (a mirror's source credit, a person's display name).
184    #[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    /// A verification record by an issuer the root endorses is about its author.
192    pub verified: bool,
193}
194
195/// One field as shown: its value, the version it is from, and the ranked versions' conflict count.
196#[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    /// Distinct values among the ranked versions, when 2 or more (the field-conflict badge's count).
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub conflicts: Option<u64>,
204}
205
206/// One version as the versions sheet lists it.
207#[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    /// The version being shown (the byline of the composition, or the one picked).
217    pub showing: bool,
218    /// Fields this version sets to something other than what is shown, in field-name order.
219    pub differs: Vec<String>,
220    /// Fields this version sets to what is shown, in field-name order.
221    pub same: Vec<String>,
222}
223
224/// What a screen draws.
225#[derive(Debug, Clone, PartialEq, Serialize)]
226#[serde(rename_all = "camelCase")]
227pub struct EventState {
228    pub uri: String,
229    /// Every field shown (the composition, or the picked version), without `$type` and `createdAt`.
230    pub fields: BTreeMap<String, FieldState>,
231    /// Whose version speaks first: the composition's byline, or the version picked.
232    #[serde(skip_serializing_if = "Option::is_none")]
233    pub byline: Option<VersionRef>,
234    pub versions: Vec<VersionEntry>,
235    /// The version picked, or `None` for the default composition.
236    pub picked: Option<String>,
237    /// Versions the server left out for a verdict (spam, obscene).
238    pub left_out: u64,
239}
240
241/// Neither shown nor compared: the record's type and its own creation time.
242fn 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
258/// The state of `data` with `picked` (a version's URI) shown, or the default composition. A pick whose
259/// version is not in `data` shows the composition.
260pub 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    // With one version and no composition, everything is from that version.
267    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    // Stable: within a group, the version shown first, then the server's order.
314    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}