Skip to main content

auths_keri/
types.rs

1use std::borrow::Borrow;
2use std::fmt;
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize};
6
7use crate::keys::{KeriDecodeError, KeriPublicKey};
8
9// ── KERI Identifier Newtypes ────────────────────────────────────────────────
10
11/// Error when constructing KERI newtypes with invalid values.
12#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
13#[error("Invalid KERI {type_name}: {reason}")]
14pub struct KeriTypeError {
15    /// Which KERI type failed validation.
16    pub type_name: &'static str,
17    /// Why validation failed.
18    pub reason: String,
19}
20
21/// Validate a CESR derivation code for AIDs (Prefix).
22///
23/// Accepts any valid CESR primitive prefix: uppercase letter or digit.
24/// `D` = Ed25519, `E` = Blake3-256, `1` = secp256k1, etc.
25fn validate_prefix_derivation_code(s: &str) -> Result<(), KeriTypeError> {
26    if s.is_empty() {
27        return Err(KeriTypeError {
28            type_name: "Prefix",
29            reason: "must not be empty".into(),
30        });
31    }
32    let first = s.as_bytes()[0];
33    if !first.is_ascii_uppercase() && !first.is_ascii_digit() {
34        return Err(KeriTypeError {
35            type_name: "Prefix",
36            reason: format!(
37                "must start with a CESR derivation code (uppercase letter or digit), got '{}'",
38                &s[..s.len().min(10)]
39            ),
40        });
41    }
42    Ok(())
43}
44
45/// Validate a CESR derivation code for SAIDs (digest only).
46///
47/// SAIDs are always digests — currently only Blake3-256 (`E`).
48fn validate_said_derivation_code(s: &str) -> Result<(), KeriTypeError> {
49    if s.is_empty() {
50        return Err(KeriTypeError {
51            type_name: "Said",
52            reason: "must not be empty".into(),
53        });
54    }
55    if !s.starts_with('E') {
56        return Err(KeriTypeError {
57            type_name: "Said",
58            reason: format!(
59                "must start with 'E' (Blake3 derivation code), got '{}'",
60                &s[..s.len().min(10)]
61            ),
62        });
63    }
64    Ok(())
65}
66
67/// Strongly-typed KERI identifier prefix (e.g., `"ETest123..."`, `"DKey456..."`).
68///
69/// A prefix is the autonomous identifier (AID) for a KERI identity. For
70/// self-addressing AIDs it starts with `E` (Blake3-256 digest of the inception
71/// event); for key-based AIDs it starts with `D` (Ed25519 public key) or
72/// another CESR derivation code.
73///
74/// Args:
75/// * Inner `String` must start with a valid CESR derivation code (uppercase
76///   letter or digit). Enforced by `new()`, not by serde.
77///
78/// Usage:
79/// ```ignore
80/// let prefix = Prefix::new("ETest123abc".to_string())?;
81/// assert_eq!(prefix.as_str(), "ETest123abc");
82/// ```
83#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
84#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
85#[repr(transparent)]
86pub struct Prefix(String);
87
88impl Prefix {
89    /// Validates and wraps a KERI prefix string.
90    ///
91    /// Accepts any valid CESR derivation code (`D` for Ed25519, `E` for Blake3,
92    /// `1` for secp256k1, etc.). See [`validate_prefix_derivation_code`] for details.
93    pub fn new(s: String) -> Result<Self, KeriTypeError> {
94        validate_prefix_derivation_code(&s)?;
95        Ok(Self(s))
96    }
97
98    /// Wraps a prefix string without validation (for trusted internal paths).
99    pub fn new_unchecked(s: String) -> Self {
100        Self(s)
101    }
102
103    /// Returns the inner string slice.
104    pub fn as_str(&self) -> &str {
105        &self.0
106    }
107
108    /// Consumes self and returns the inner String.
109    pub fn into_inner(self) -> String {
110        self.0
111    }
112
113    /// Returns true if the inner string is empty (placeholder during event construction).
114    pub fn is_empty(&self) -> bool {
115        self.0.is_empty()
116    }
117}
118
119// Wire deserialization rejects an empty prefix: a present-but-empty identifier is
120// never valid on the wire, and `#[serde(default)]` removal only catches a *missing*
121// field. Internal placeholders go through `Default`/`new_unchecked`, not this path.
122impl<'de> Deserialize<'de> for Prefix {
123    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
124        let s = String::deserialize(deserializer)?;
125        if s.is_empty() {
126            return Err(serde::de::Error::custom("Prefix must not be empty"));
127        }
128        Ok(Self(s))
129    }
130}
131
132impl fmt::Display for Prefix {
133    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134        f.write_str(&self.0)
135    }
136}
137
138impl AsRef<str> for Prefix {
139    fn as_ref(&self) -> &str {
140        &self.0
141    }
142}
143
144impl Borrow<str> for Prefix {
145    fn borrow(&self) -> &str {
146        &self.0
147    }
148}
149
150impl From<Prefix> for String {
151    fn from(p: Prefix) -> String {
152        p.0
153    }
154}
155
156impl PartialEq<str> for Prefix {
157    fn eq(&self, other: &str) -> bool {
158        self.0 == other
159    }
160}
161
162impl PartialEq<&str> for Prefix {
163    fn eq(&self, other: &&str) -> bool {
164        self.0 == *other
165    }
166}
167
168impl PartialEq<Prefix> for str {
169    fn eq(&self, other: &Prefix) -> bool {
170        self == other.0
171    }
172}
173
174impl PartialEq<Prefix> for &str {
175    fn eq(&self, other: &Prefix) -> bool {
176        *self == other.0
177    }
178}
179
180/// KERI Self-Addressing Identifier (SAID).
181///
182/// A Blake3 hash that uniquely identifies a KERI event. Creates the
183/// hash chain: each event's `p` (previous) field is the prior event's SAID.
184///
185/// Structurally identical to `Prefix` (both start with 'E') but semantically
186/// distinct — a prefix identifies an *identity*, a SAID identifies an *event*.
187///
188/// Args:
189/// * Inner `String` must be non-empty (rejected on deserialize) and should start
190///   with `'E'` (the Blake3 digest derivation code, enforced by `new()`).
191///
192/// Usage:
193/// ```ignore
194/// let said = Said::new("ESAID123".to_string())?;
195/// assert_eq!(said.as_str(), "ESAID123");
196/// ```
197#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize)]
198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
199#[repr(transparent)]
200pub struct Said(String);
201
202impl Said {
203    /// Validates and wraps a KERI SAID string.
204    ///
205    /// Only accepts `E` prefix (digest derivation codes).
206    pub fn new(s: String) -> Result<Self, KeriTypeError> {
207        validate_said_derivation_code(&s)?;
208        Ok(Self(s))
209    }
210
211    /// Wraps a SAID string without validation (for `compute_said()` output and storage loads).
212    pub fn new_unchecked(s: String) -> Self {
213        Self(s)
214    }
215
216    /// Returns the inner string slice.
217    pub fn as_str(&self) -> &str {
218        &self.0
219    }
220
221    /// Consumes self and returns the inner String.
222    pub fn into_inner(self) -> String {
223        self.0
224    }
225
226    /// Returns true if the inner string is empty (placeholder during event construction).
227    pub fn is_empty(&self) -> bool {
228        self.0.is_empty()
229    }
230}
231
232// Wire deserialization rejects an empty SAID: an event must carry a real `d`/`n`
233// on the wire (removing `#[serde(default)]` on `d` only catches a *missing* field).
234// `compute_said`'s placeholder and other internal uses go through `new_unchecked`.
235impl<'de> Deserialize<'de> for Said {
236    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
237        let s = String::deserialize(deserializer)?;
238        if s.is_empty() {
239            return Err(serde::de::Error::custom("Said must not be empty"));
240        }
241        Ok(Self(s))
242    }
243}
244
245impl fmt::Display for Said {
246    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247        f.write_str(&self.0)
248    }
249}
250
251impl AsRef<str> for Said {
252    fn as_ref(&self) -> &str {
253        &self.0
254    }
255}
256
257impl Borrow<str> for Said {
258    fn borrow(&self) -> &str {
259        &self.0
260    }
261}
262
263impl From<Said> for String {
264    fn from(s: Said) -> String {
265        s.0
266    }
267}
268
269impl PartialEq<str> for Said {
270    fn eq(&self, other: &str) -> bool {
271        self.0 == other
272    }
273}
274
275impl PartialEq<&str> for Said {
276    fn eq(&self, other: &&str) -> bool {
277        self.0 == *other
278    }
279}
280
281impl PartialEq<Said> for str {
282    fn eq(&self, other: &Said) -> bool {
283        self == other.0
284    }
285}
286
287impl PartialEq<Said> for &str {
288    fn eq(&self, other: &Said) -> bool {
289        *self == other.0
290    }
291}
292
293// ── Fraction ────────────────────────────────────────────────────────────────
294
295/// Exact rational number for weighted threshold arithmetic.
296///
297/// Uses integer cross-multiplication for comparison — NOT floating point.
298/// This ensures `1/3 + 1/3 + 1/3` equals exactly `1`.
299///
300/// Usage:
301/// ```
302/// use auths_keri::Fraction;
303/// let f: Fraction = "1/3".parse().unwrap();
304/// assert_eq!(f.numerator, 1);
305/// assert_eq!(f.denominator, 3);
306/// ```
307#[derive(Debug, Clone, PartialEq, Eq, Hash)]
308pub struct Fraction {
309    /// Numerator of the fraction.
310    pub numerator: u64,
311    /// Denominator of the fraction (must be > 0).
312    pub denominator: u64,
313}
314
315/// Error when parsing a `Fraction` from a string.
316#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
317pub enum FractionError {
318    /// Missing the `/` separator.
319    #[error("missing '/' separator in fraction: {0:?}")]
320    MissingSeparator(String),
321    /// Numerator or denominator is not a valid integer.
322    #[error("invalid integer in fraction: {0}")]
323    InvalidInt(String),
324    /// Denominator is zero.
325    #[error("fraction denominator must not be zero")]
326    ZeroDenominator,
327}
328
329impl Fraction {
330    /// Check if the sum of the given fractions is >= 1, using exact integer arithmetic.
331    ///
332    /// Uses cross-multiplication to avoid floating-point imprecision.
333    /// `1/3 + 1/3 + 1/3` returns `true` (exactly 1).
334    ///
335    /// Usage:
336    /// ```
337    /// use auths_keri::Fraction;
338    /// let thirds: Vec<Fraction> = vec!["1/3".parse().unwrap(); 3];
339    /// let refs: Vec<&Fraction> = thirds.iter().collect();
340    /// assert!(Fraction::sum_meets_one(&refs));
341    /// ```
342    pub fn sum_meets_one(fractions: &[&Fraction]) -> bool {
343        if fractions.is_empty() {
344            return false;
345        }
346        // Accumulate as num/den using: a/b + c/d = (a*d + c*b) / (b*d)
347        // Use u128 to avoid overflow with u64 numerators/denominators.
348        let mut num: u128 = 0;
349        let mut den: u128 = 1;
350        for f in fractions {
351            // num/den + f.numerator/f.denominator
352            // = (num * f.denominator + f.numerator * den) / (den * f.denominator)
353            num = num * (f.denominator as u128) + (f.numerator as u128) * den;
354            den *= f.denominator as u128;
355        }
356        // Check num/den >= 1, i.e., num >= den
357        num >= den
358    }
359}
360
361impl FromStr for Fraction {
362    type Err = FractionError;
363
364    fn from_str(s: &str) -> Result<Self, Self::Err> {
365        let (num_str, den_str) = s
366            .split_once('/')
367            .ok_or_else(|| FractionError::MissingSeparator(s.to_string()))?;
368        let numerator: u64 = num_str
369            .parse()
370            .map_err(|_| FractionError::InvalidInt(num_str.to_string()))?;
371        let denominator: u64 = den_str
372            .parse()
373            .map_err(|_| FractionError::InvalidInt(den_str.to_string()))?;
374        if denominator == 0 {
375            return Err(FractionError::ZeroDenominator);
376        }
377        Ok(Self {
378            numerator,
379            denominator,
380        })
381    }
382}
383
384impl fmt::Display for Fraction {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        write!(f, "{}/{}", self.numerator, self.denominator)
387    }
388}
389
390impl Serialize for Fraction {
391    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
392        serializer.serialize_str(&self.to_string())
393    }
394}
395
396impl<'de> Deserialize<'de> for Fraction {
397    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
398        let s = String::deserialize(deserializer)?;
399        s.parse().map_err(serde::de::Error::custom)
400    }
401}
402
403// ── Threshold ───────────────────────────────────────────────────────────────
404
405/// KERI signing/backer threshold.
406///
407/// Simple thresholds are hex-encoded integers (`"1"`, `"2"`, `"a"`).
408/// Weighted thresholds are clause lists of fractions (`[["1/2","1/2"]]`).
409/// Clauses are ANDed; each is satisfied when the sum of verified weights >= 1.
410///
411/// Usage:
412/// ```
413/// use auths_keri::Threshold;
414/// let t: Threshold = serde_json::from_str("\"2\"").unwrap();
415/// assert_eq!(t.simple_value(), Some(2));
416/// ```
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum Threshold {
419    /// M-of-N threshold (hex-encoded integer in JSON).
420    Simple(u64),
421    /// Fractionally weighted threshold (list of clause lists in JSON).
422    /// Clauses are ANDed; each is satisfied when sum of verified weights >= 1.
423    Weighted(Vec<Vec<Fraction>>),
424}
425
426impl Threshold {
427    /// Get the simple threshold value, if this is a simple threshold.
428    pub fn simple_value(&self) -> Option<u64> {
429        match self {
430            Threshold::Simple(v) => Some(*v),
431            Threshold::Weighted(_) => None,
432        }
433    }
434
435    /// Check that this threshold is structurally satisfiable against a key
436    /// (or commitment / backer) list of length `count`.
437    ///
438    /// This is a structural guard run at validation entry — it rejects events
439    /// whose threshold can never be met regardless of which signatures arrive:
440    /// a `Simple(n)` with `n > count`, a non-zero `Simple` over an empty list,
441    /// or a `Weighted` clause whose length doesn't match `count`.
442    ///
443    /// Args:
444    /// * `count` - Length of the list the threshold governs (`k`, `n`, or `b`).
445    ///
446    /// Usage:
447    /// ```
448    /// use auths_keri::Threshold;
449    /// assert!(Threshold::Simple(2).validate_satisfiable(3).is_ok());
450    /// assert!(Threshold::Simple(5).validate_satisfiable(1).is_err());
451    /// ```
452    pub fn validate_satisfiable(&self, count: usize) -> Result<(), KeriTypeError> {
453        match self {
454            Threshold::Simple(0) => Ok(()),
455            Threshold::Simple(n) if *n as usize > count => Err(KeriTypeError {
456                type_name: "Threshold",
457                reason: format!("simple threshold {n} exceeds list length {count}"),
458            }),
459            Threshold::Simple(_) => Ok(()),
460            Threshold::Weighted(clauses) => {
461                for clause in clauses {
462                    if clause.len() != count {
463                        return Err(KeriTypeError {
464                            type_name: "Threshold",
465                            reason: format!(
466                                "weighted clause length {} != list length {count}",
467                                clause.len()
468                            ),
469                        });
470                    }
471                }
472                Ok(())
473            }
474        }
475    }
476
477    /// Check if the threshold is satisfied by the given set of verified key indices.
478    ///
479    /// For `Simple(n)`: at least `n` unique indices must be verified.
480    /// For `Weighted(clauses)`: ALL clauses must be independently satisfied.
481    /// A clause is satisfied when the sum of weights at verified indices >= 1.
482    ///
483    /// Args:
484    /// * `verified_indices` - Indices of keys whose signatures have been verified.
485    /// * `key_count` - Total number of keys in the key list (for bounds checking).
486    ///
487    /// Usage:
488    /// ```
489    /// use auths_keri::Threshold;
490    /// let t = Threshold::Simple(2);
491    /// assert!(t.is_satisfied(&[0, 1], 3));
492    /// assert!(!t.is_satisfied(&[0], 3));
493    /// ```
494    pub fn is_satisfied(&self, verified_indices: &[u32], key_count: usize) -> bool {
495        // Deduplicate indices and filter out-of-range
496        let mut unique: std::collections::HashSet<u32> = std::collections::HashSet::new();
497        for &idx in verified_indices {
498            if (idx as usize) < key_count {
499                unique.insert(idx);
500            }
501        }
502
503        match self {
504            Threshold::Simple(required) => unique.len() as u64 >= *required,
505            Threshold::Weighted(clauses) => {
506                // ALL clauses must be satisfied (ANDed)
507                for clause in clauses {
508                    let verified_fractions: Vec<&Fraction> = clause
509                        .iter()
510                        .enumerate()
511                        .filter(|(i, _)| unique.contains(&(*i as u32)))
512                        .map(|(_, f)| f)
513                        .collect();
514                    if !Fraction::sum_meets_one(&verified_fractions) {
515                        return false;
516                    }
517                }
518                true
519            }
520        }
521    }
522}
523
524impl Serialize for Threshold {
525    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
526        match self {
527            Threshold::Simple(v) => serializer.serialize_str(&format!("{v:x}")),
528            Threshold::Weighted(clauses) => clauses.serialize(serializer),
529        }
530    }
531}
532
533impl<'de> Deserialize<'de> for Threshold {
534    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
535        let value = serde_json::Value::deserialize(deserializer)?;
536        match value {
537            serde_json::Value::String(s) => {
538                let v = u64::from_str_radix(&s, 16).map_err(|_| {
539                    serde::de::Error::custom(format!("invalid hex threshold: {s:?}"))
540                })?;
541                Ok(Threshold::Simple(v))
542            }
543            serde_json::Value::Array(arr) => {
544                let clauses: Vec<Vec<Fraction>> = arr
545                    .into_iter()
546                    .map(|clause| match clause {
547                        serde_json::Value::Array(weights) => weights
548                            .into_iter()
549                            .map(|w| match w {
550                                serde_json::Value::String(s) => {
551                                    s.parse().map_err(serde::de::Error::custom)
552                                }
553                                _ => Err(serde::de::Error::custom("weight must be a string")),
554                            })
555                            .collect::<Result<Vec<_>, _>>(),
556                        _ => Err(serde::de::Error::custom("clause must be an array")),
557                    })
558                    .collect::<Result<Vec<_>, _>>()?;
559                Ok(Threshold::Weighted(clauses))
560            }
561            _ => Err(serde::de::Error::custom(
562                "threshold must be a hex string or array of clause arrays",
563            )),
564        }
565    }
566}
567
568impl Default for Threshold {
569    fn default() -> Self {
570        Threshold::Simple(0)
571    }
572}
573
574// ── CesrKey ─────────────────────────────────────────────────────────────────
575
576/// A CESR-encoded public key (e.g., `D` + base64url Ed25519, `1AAI` + base64url P-256).
577///
578/// Wraps the qualified string form. Use `parse()` to extract
579/// the curve-tagged `KeriPublicKey` for cryptographic operations.
580///
581/// Usage:
582/// ```
583/// use auths_keri::CesrKey;
584/// let key = CesrKey::new_unchecked("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".into());
585/// assert!(key.parse().is_ok());
586/// ```
587#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
588#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
589#[repr(transparent)]
590pub struct CesrKey(String);
591
592impl CesrKey {
593    /// Wrap a qualified key string without validation.
594    pub fn new_unchecked(s: String) -> Self {
595        Self(s)
596    }
597
598    /// Parse the inner CESR string, dispatching on the derivation code prefix.
599    ///
600    /// Supports Ed25519 (`D` prefix) and P-256 (`1AAJ`/`1AAI` prefix).
601    /// Returns `Err(UnsupportedKeyType)` for unknown prefixes.
602    pub fn parse(&self) -> Result<KeriPublicKey, KeriDecodeError> {
603        KeriPublicKey::parse(&self.0)
604    }
605
606    /// Get the raw CESR-qualified string.
607    pub fn as_str(&self) -> &str {
608        &self.0
609    }
610
611    /// Consumes self and returns the inner String.
612    pub fn into_inner(self) -> String {
613        self.0
614    }
615}
616
617impl fmt::Display for CesrKey {
618    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
619        f.write_str(&self.0)
620    }
621}
622
623impl AsRef<str> for CesrKey {
624    fn as_ref(&self) -> &str {
625        &self.0
626    }
627}
628
629// ── ConfigTrait ─────────────────────────────────────────────────────────────
630
631/// KERI configuration trait codes.
632///
633/// These control identity behavior at inception and may be updated at rotation
634/// (for `RB`/`NRB` only). If two conflicting traits appear, the latter supersedes.
635///
636/// Usage:
637/// ```
638/// use auths_keri::ConfigTrait;
639/// let traits: Vec<ConfigTrait> = serde_json::from_str(r#"["EO","DND"]"#).unwrap();
640/// assert!(traits.contains(&ConfigTrait::EstablishmentOnly));
641/// ```
642#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644pub enum ConfigTrait {
645    /// Establishment-Only: only establishment events in KEL.
646    #[serde(rename = "EO")]
647    EstablishmentOnly,
648    /// Do-Not-Delegate: cannot act as delegator.
649    #[serde(rename = "DND")]
650    DoNotDelegate,
651    /// Registrar Backers: backer list provides registrar backer AIDs.
652    #[serde(rename = "RB")]
653    RegistrarBackers,
654    /// No Registrar Backers: switch back to witnesses.
655    #[serde(rename = "NRB")]
656    NoRegistrarBackers,
657}
658
659// ── VersionString ───────────────────────────────────────────────────────────
660
661/// KERI v1.x version string: `KERI10JSON{hhhhhh}_` (17 chars).
662///
663/// The size field is the total serialized byte count of the event.
664///
665/// Usage:
666/// ```
667/// use auths_keri::VersionString;
668/// let vs = VersionString::json(256);
669/// assert_eq!(vs.to_string(), "KERI10JSON000100_");
670/// ```
671#[derive(Debug, Clone, PartialEq, Eq)]
672pub struct VersionString {
673    /// Serialization kind (e.g., "JSON", "CBOR").
674    pub kind: String,
675    /// Serialized byte count.
676    pub size: u32,
677}
678
679impl VersionString {
680    /// Create a version string for JSON serialization with the given byte count.
681    pub fn json(size: u32) -> Self {
682        Self {
683            kind: "JSON".to_string(),
684            size,
685        }
686    }
687
688    /// Create a placeholder version string (size = 0, to be updated after serialization).
689    pub fn placeholder() -> Self {
690        Self {
691            kind: "JSON".to_string(),
692            size: 0,
693        }
694    }
695}
696
697impl Default for VersionString {
698    fn default() -> Self {
699        Self::placeholder()
700    }
701}
702
703impl fmt::Display for VersionString {
704    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
705        write!(f, "KERI10{}{:06x}_", self.kind, self.size)
706    }
707}
708
709impl Serialize for VersionString {
710    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
711        serializer.serialize_str(&self.to_string())
712    }
713}
714
715impl<'de> Deserialize<'de> for VersionString {
716    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
717        let s = String::deserialize(deserializer)?;
718        // Full 17-char format: "KERI10JSON000100_". The version string is all-ASCII; the
719        // `is_ascii` guard makes the byte slices below char-boundary-safe, so a multi-byte
720        // UTF-8 char that passes the byte-length check is rejected rather than panicking.
721        if s.len() >= 17 && s.is_ascii() && s.ends_with('_') {
722            let size_hex = &s[10..16];
723            let size = u32::from_str_radix(size_hex, 16).map_err(|_| {
724                serde::de::Error::custom(format!("invalid version string size: {size_hex:?}"))
725            })?;
726            let kind = s[6..10].to_string();
727            Ok(Self { kind, size })
728        } else {
729            // KERI v1.1 mandates the full 17-char form (`KERI10JSON{size:06x}_`).
730            // Short/legacy strings are rejected — they are wire-incompatible
731            // with spec verifiers, which parse the declared size from `v`.
732            Err(serde::de::Error::custom(format!(
733                "invalid KERI version string: {s:?}"
734            )))
735        }
736    }
737}
738
739// ── JsonSchema impls for custom serde types ─────────────────────────────────
740
741#[cfg(feature = "schema")]
742mod schema_impls {
743    use super::*;
744
745    impl schemars::JsonSchema for Fraction {
746        fn schema_name() -> String {
747            "Fraction".to_string()
748        }
749        fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
750            schemars::schema::SchemaObject {
751                instance_type: Some(schemars::schema::InstanceType::String.into()),
752                ..Default::default()
753            }
754            .into()
755        }
756    }
757
758    impl schemars::JsonSchema for Threshold {
759        fn schema_name() -> String {
760            "Threshold".to_string()
761        }
762        fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
763            // Union: string (hex integer) or array of arrays of strings (weighted)
764            schemars::schema::Schema::Bool(true)
765        }
766    }
767
768    impl schemars::JsonSchema for crate::events::Seal {
769        fn schema_name() -> String {
770            "Seal".to_string()
771        }
772        fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
773            // Untagged union of seal variants — accept any object
774            schemars::schema::Schema::Bool(true)
775        }
776    }
777
778    impl schemars::JsonSchema for VersionString {
779        fn schema_name() -> String {
780            "VersionString".to_string()
781        }
782        fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
783            schemars::schema::SchemaObject {
784                instance_type: Some(schemars::schema::InstanceType::String.into()),
785                ..Default::default()
786            }
787            .into()
788        }
789    }
790}
791
792#[cfg(test)]
793#[allow(clippy::unwrap_used)]
794mod tests {
795    use super::*;
796
797    // ── Fraction ────────────────────────────────────────────────────────
798
799    #[test]
800    fn fraction_parse_valid() {
801        let f: Fraction = "1/3".parse().unwrap();
802        assert_eq!(f.numerator, 1);
803        assert_eq!(f.denominator, 3);
804    }
805
806    #[test]
807    fn fraction_parse_rejects_zero_denominator() {
808        let err = "1/0".parse::<Fraction>().unwrap_err();
809        assert_eq!(err, FractionError::ZeroDenominator);
810    }
811
812    #[test]
813    fn fraction_parse_rejects_missing_separator() {
814        assert!("42".parse::<Fraction>().is_err());
815    }
816
817    #[test]
818    fn fraction_serde_roundtrip() {
819        let f = Fraction {
820            numerator: 1,
821            denominator: 2,
822        };
823        let json = serde_json::to_string(&f).unwrap();
824        assert_eq!(json, "\"1/2\"");
825        let parsed: Fraction = serde_json::from_str(&json).unwrap();
826        assert_eq!(parsed, f);
827    }
828
829    #[test]
830    fn fraction_display() {
831        let f = Fraction {
832            numerator: 3,
833            denominator: 4,
834        };
835        assert_eq!(f.to_string(), "3/4");
836    }
837
838    // ── Threshold ───────────────────────────────────────────────────────
839
840    #[test]
841    fn threshold_simple_from_hex() {
842        let t: Threshold = serde_json::from_str("\"a\"").unwrap();
843        assert_eq!(t, Threshold::Simple(10));
844        assert_eq!(t.simple_value(), Some(10));
845    }
846
847    #[test]
848    fn threshold_simple_serialize_as_hex() {
849        let t = Threshold::Simple(16);
850        let json = serde_json::to_string(&t).unwrap();
851        assert_eq!(json, "\"10\""); // 16 decimal = 10 hex
852    }
853
854    #[test]
855    fn threshold_simple_roundtrip() {
856        let t = Threshold::Simple(2);
857        let json = serde_json::to_string(&t).unwrap();
858        let parsed: Threshold = serde_json::from_str(&json).unwrap();
859        assert_eq!(parsed, t);
860    }
861
862    #[test]
863    fn threshold_weighted_roundtrip() {
864        let json = r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#;
865        let t: Threshold = serde_json::from_str(json).unwrap();
866        assert!(t.simple_value().is_none());
867        if let Threshold::Weighted(clauses) = &t {
868            assert_eq!(clauses.len(), 2);
869            assert_eq!(clauses[0].len(), 2);
870            assert_eq!(clauses[1].len(), 3);
871            assert_eq!(clauses[0][0].numerator, 1);
872            assert_eq!(clauses[0][0].denominator, 2);
873        } else {
874            panic!("expected Weighted");
875        }
876        let reserialized = serde_json::to_string(&t).unwrap();
877        let reparsed: Threshold = serde_json::from_str(&reserialized).unwrap();
878        assert_eq!(reparsed, t);
879    }
880
881    #[test]
882    fn threshold_rejects_invalid_hex() {
883        let result = serde_json::from_str::<Threshold>("\"xyz\"");
884        assert!(result.is_err());
885    }
886
887    // ── Fraction arithmetic ─────────────────────────────────────────────
888
889    #[test]
890    fn fraction_sum_one_third_times_three() {
891        let f: Fraction = "1/3".parse().unwrap();
892        assert!(Fraction::sum_meets_one(&[&f, &f, &f]));
893    }
894
895    #[test]
896    fn fraction_sum_two_thirds_not_enough() {
897        let f: Fraction = "1/3".parse().unwrap();
898        assert!(!Fraction::sum_meets_one(&[&f, &f]));
899    }
900
901    #[test]
902    fn fraction_sum_halves() {
903        let f: Fraction = "1/2".parse().unwrap();
904        assert!(Fraction::sum_meets_one(&[&f, &f]));
905        assert!(!Fraction::sum_meets_one(&[&f]));
906    }
907
908    #[test]
909    fn fraction_sum_empty_is_false() {
910        assert!(!Fraction::sum_meets_one(&[]));
911    }
912
913    // ── Threshold::is_satisfied ─────────────────────────────────────────
914
915    #[test]
916    fn threshold_simple_satisfied() {
917        let t = Threshold::Simple(2);
918        assert!(t.is_satisfied(&[0, 1], 3));
919        assert!(t.is_satisfied(&[0, 1, 2], 3));
920        assert!(!t.is_satisfied(&[0], 3));
921    }
922
923    #[test]
924    fn threshold_simple_zero_always_satisfied() {
925        let t = Threshold::Simple(0);
926        assert!(t.is_satisfied(&[], 3));
927    }
928
929    #[test]
930    fn threshold_simple_deduplicates_indices() {
931        let t = Threshold::Simple(2);
932        // Same index twice doesn't count as 2
933        assert!(!t.is_satisfied(&[0, 0], 3));
934    }
935
936    #[test]
937    fn threshold_simple_rejects_out_of_range() {
938        let t = Threshold::Simple(1);
939        // Index 5 is out of range for 3 keys
940        assert!(!t.is_satisfied(&[5], 3));
941    }
942
943    #[test]
944    fn threshold_weighted_two_of_three() {
945        // [["1/2","1/2","1/2"]] — any 2 of 3
946        let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2"]]"#).unwrap();
947        assert!(t.is_satisfied(&[0, 1], 3));
948        assert!(t.is_satisfied(&[1, 2], 3));
949        assert!(!t.is_satisfied(&[0], 3));
950    }
951
952    #[test]
953    fn threshold_weighted_with_reserves() {
954        // [["1/2","1/2","1/2","1/4","1/4"]] — 2 main OR 1 main + 2 reserves
955        let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2","1/4","1/4"]]"#).unwrap();
956        assert!(t.is_satisfied(&[0, 1], 5)); // 2 main
957        assert!(t.is_satisfied(&[0, 3, 4], 5)); // 1 main + 2 reserves
958        assert!(!t.is_satisfied(&[3, 4], 5)); // reserves only: 1/4+1/4=1/2 < 1
959    }
960
961    #[test]
962    fn threshold_weighted_multi_clause_and() {
963        // [["1/2","1/2"],["1/3","1/3","1/3"]] — both clauses must be satisfied
964        let t: Threshold = serde_json::from_str(r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#).unwrap();
965        // Indices 0,1 satisfy clause 1 (1/2+1/2=1), but clause 2 needs 2 of {0,1,2}
966        // Index 0 in clause 2 = 1/3, index 1 in clause 2 = 1/3 → 2/3 < 1
967        assert!(!t.is_satisfied(&[0, 1], 3));
968        // Indices 0,1,2 satisfy both clauses
969        assert!(t.is_satisfied(&[0, 1, 2], 3));
970    }
971
972    // ── CesrKey ─────────────────────────────────────────────────────────
973
974    #[test]
975    fn cesr_key_roundtrip() {
976        let key_str = "DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
977        let key = CesrKey::new_unchecked(key_str.to_string());
978        let json = serde_json::to_string(&key).unwrap();
979        let parsed: CesrKey = serde_json::from_str(&json).unwrap();
980        assert_eq!(parsed.as_str(), key_str);
981    }
982
983    #[test]
984    fn cesr_key_parse_valid() {
985        let key =
986            CesrKey::new_unchecked("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string());
987        assert!(key.parse().is_ok());
988    }
989
990    #[test]
991    fn cesr_key_parse_invalid() {
992        let key = CesrKey::new_unchecked("not-a-valid-key".to_string());
993        assert!(key.parse().is_err());
994    }
995
996    // ── ConfigTrait ─────────────────────────────────────────────────────
997
998    #[test]
999    fn config_trait_serde_roundtrip() {
1000        let traits = vec![ConfigTrait::EstablishmentOnly, ConfigTrait::DoNotDelegate];
1001        let json = serde_json::to_string(&traits).unwrap();
1002        assert_eq!(json, r#"["EO","DND"]"#);
1003        let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
1004        assert_eq!(parsed, traits);
1005    }
1006
1007    #[test]
1008    fn config_trait_all_variants_roundtrip() {
1009        let all = vec![
1010            ConfigTrait::EstablishmentOnly,
1011            ConfigTrait::DoNotDelegate,
1012            ConfigTrait::RegistrarBackers,
1013            ConfigTrait::NoRegistrarBackers,
1014        ];
1015        let json = serde_json::to_string(&all).unwrap();
1016        assert_eq!(json, r#"["EO","DND","RB","NRB"]"#);
1017        let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
1018        assert_eq!(parsed, all);
1019    }
1020
1021    // ── VersionString ───────────────────────────────────────────────────
1022
1023    #[test]
1024    fn version_string_display() {
1025        let vs = VersionString::json(256);
1026        assert_eq!(vs.to_string(), "KERI10JSON000100_");
1027    }
1028
1029    #[test]
1030    fn version_string_placeholder() {
1031        let vs = VersionString::placeholder();
1032        assert_eq!(vs.to_string(), "KERI10JSON000000_");
1033        assert_eq!(vs.size, 0);
1034    }
1035
1036    #[test]
1037    fn version_string_parse_full() {
1038        let vs: VersionString = serde_json::from_str("\"KERI10JSON000100_\"").unwrap();
1039        assert_eq!(vs.kind, "JSON");
1040        assert_eq!(vs.size, 256);
1041    }
1042
1043    #[test]
1044    fn version_string_rejects_legacy_short() {
1045        // KERI v1.1 mandates the 17-char form; the legacy short form
1046        // (`KERI10JSON` with no size/terminator) must be rejected, not
1047        // silently accepted with size 0.
1048        assert!(serde_json::from_str::<VersionString>("\"KERI10JSON\"").is_err());
1049        assert!(serde_json::from_str::<VersionString>("\"KERI10JSON0001\"").is_err());
1050    }
1051
1052    #[test]
1053    fn version_string_roundtrip() {
1054        let vs = VersionString::json(1024);
1055        let json = serde_json::to_string(&vs).unwrap();
1056        let parsed: VersionString = serde_json::from_str(&json).unwrap();
1057        assert_eq!(parsed, vs);
1058    }
1059
1060    #[test]
1061    fn version_string_rejects_invalid() {
1062        assert!(serde_json::from_str::<VersionString>("\"INVALID\"").is_err());
1063    }
1064
1065    #[test]
1066    fn version_string_rejects_multibyte_without_panicking() {
1067        // A 17+ *byte* string whose bytes 6..10 / 10..16 straddle a multi-byte UTF-8
1068        // char must be rejected, not panic on a non-char-boundary slice. The Cyrillic
1069        // `Ь` is two bytes, so this passes a byte-length check while breaking the slice.
1070        assert!(serde_json::from_str::<VersionString>("\"KERI1ЬSON00012b_\"").is_err());
1071        // A multi-byte char inside the size field is rejected too.
1072        assert!(serde_json::from_str::<VersionString>("\"KERI10JSON00Ь1b_\"").is_err());
1073    }
1074}