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#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
13#[error("Invalid KERI {type_name}: {reason}")]
14pub struct KeriTypeError {
15 pub type_name: &'static str,
17 pub reason: String,
19}
20
21fn 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
45fn 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#[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 pub fn new(s: String) -> Result<Self, KeriTypeError> {
94 validate_prefix_derivation_code(&s)?;
95 Ok(Self(s))
96 }
97
98 pub fn new_unchecked(s: String) -> Self {
100 Self(s)
101 }
102
103 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107
108 pub fn into_inner(self) -> String {
110 self.0
111 }
112
113 pub fn is_empty(&self) -> bool {
115 self.0.is_empty()
116 }
117}
118
119impl<'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#[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 pub fn new(s: String) -> Result<Self, KeriTypeError> {
207 validate_said_derivation_code(&s)?;
208 Ok(Self(s))
209 }
210
211 pub fn new_unchecked(s: String) -> Self {
213 Self(s)
214 }
215
216 pub fn as_str(&self) -> &str {
218 &self.0
219 }
220
221 pub fn into_inner(self) -> String {
223 self.0
224 }
225
226 pub fn is_empty(&self) -> bool {
228 self.0.is_empty()
229 }
230}
231
232impl<'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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
308pub struct Fraction {
309 pub numerator: u64,
311 pub denominator: u64,
313}
314
315#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
317pub enum FractionError {
318 #[error("missing '/' separator in fraction: {0:?}")]
320 MissingSeparator(String),
321 #[error("invalid integer in fraction: {0}")]
323 InvalidInt(String),
324 #[error("fraction denominator must not be zero")]
326 ZeroDenominator,
327}
328
329impl Fraction {
330 pub fn sum_meets_one(fractions: &[&Fraction]) -> bool {
343 if fractions.is_empty() {
344 return false;
345 }
346 let mut num: u128 = 0;
349 let mut den: u128 = 1;
350 for f in fractions {
351 num = num * (f.denominator as u128) + (f.numerator as u128) * den;
354 den *= f.denominator as u128;
355 }
356 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#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum Threshold {
419 Simple(u64),
421 Weighted(Vec<Vec<Fraction>>),
424}
425
426impl Threshold {
427 pub fn simple_value(&self) -> Option<u64> {
429 match self {
430 Threshold::Simple(v) => Some(*v),
431 Threshold::Weighted(_) => None,
432 }
433 }
434
435 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 pub fn is_satisfied(&self, verified_indices: &[u32], key_count: usize) -> bool {
495 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 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#[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 pub fn new_unchecked(s: String) -> Self {
595 Self(s)
596 }
597
598 pub fn parse(&self) -> Result<KeriPublicKey, KeriDecodeError> {
603 KeriPublicKey::parse(&self.0)
604 }
605
606 pub fn as_str(&self) -> &str {
608 &self.0
609 }
610
611 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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
644pub enum ConfigTrait {
645 #[serde(rename = "EO")]
647 EstablishmentOnly,
648 #[serde(rename = "DND")]
650 DoNotDelegate,
651 #[serde(rename = "RB")]
653 RegistrarBackers,
654 #[serde(rename = "NRB")]
656 NoRegistrarBackers,
657}
658
659#[derive(Debug, Clone, PartialEq, Eq)]
672pub struct VersionString {
673 pub kind: String,
675 pub size: u32,
677}
678
679impl VersionString {
680 pub fn json(size: u32) -> Self {
682 Self {
683 kind: "JSON".to_string(),
684 size,
685 }
686 }
687
688 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 if s.len() >= 17 && s.ends_with('_') {
720 let size_hex = &s[10..16];
721 let size = u32::from_str_radix(size_hex, 16).map_err(|_| {
722 serde::de::Error::custom(format!("invalid version string size: {size_hex:?}"))
723 })?;
724 let kind = s[6..10].to_string();
725 Ok(Self { kind, size })
726 } else {
727 Err(serde::de::Error::custom(format!(
731 "invalid KERI version string: {s:?}"
732 )))
733 }
734 }
735}
736
737#[cfg(feature = "schema")]
740mod schema_impls {
741 use super::*;
742
743 impl schemars::JsonSchema for Fraction {
744 fn schema_name() -> String {
745 "Fraction".to_string()
746 }
747 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
748 schemars::schema::SchemaObject {
749 instance_type: Some(schemars::schema::InstanceType::String.into()),
750 ..Default::default()
751 }
752 .into()
753 }
754 }
755
756 impl schemars::JsonSchema for Threshold {
757 fn schema_name() -> String {
758 "Threshold".to_string()
759 }
760 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
761 schemars::schema::Schema::Bool(true)
763 }
764 }
765
766 impl schemars::JsonSchema for crate::events::Seal {
767 fn schema_name() -> String {
768 "Seal".to_string()
769 }
770 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
771 schemars::schema::Schema::Bool(true)
773 }
774 }
775
776 impl schemars::JsonSchema for VersionString {
777 fn schema_name() -> String {
778 "VersionString".to_string()
779 }
780 fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
781 schemars::schema::SchemaObject {
782 instance_type: Some(schemars::schema::InstanceType::String.into()),
783 ..Default::default()
784 }
785 .into()
786 }
787 }
788}
789
790#[cfg(test)]
791#[allow(clippy::unwrap_used)]
792mod tests {
793 use super::*;
794
795 #[test]
798 fn fraction_parse_valid() {
799 let f: Fraction = "1/3".parse().unwrap();
800 assert_eq!(f.numerator, 1);
801 assert_eq!(f.denominator, 3);
802 }
803
804 #[test]
805 fn fraction_parse_rejects_zero_denominator() {
806 let err = "1/0".parse::<Fraction>().unwrap_err();
807 assert_eq!(err, FractionError::ZeroDenominator);
808 }
809
810 #[test]
811 fn fraction_parse_rejects_missing_separator() {
812 assert!("42".parse::<Fraction>().is_err());
813 }
814
815 #[test]
816 fn fraction_serde_roundtrip() {
817 let f = Fraction {
818 numerator: 1,
819 denominator: 2,
820 };
821 let json = serde_json::to_string(&f).unwrap();
822 assert_eq!(json, "\"1/2\"");
823 let parsed: Fraction = serde_json::from_str(&json).unwrap();
824 assert_eq!(parsed, f);
825 }
826
827 #[test]
828 fn fraction_display() {
829 let f = Fraction {
830 numerator: 3,
831 denominator: 4,
832 };
833 assert_eq!(f.to_string(), "3/4");
834 }
835
836 #[test]
839 fn threshold_simple_from_hex() {
840 let t: Threshold = serde_json::from_str("\"a\"").unwrap();
841 assert_eq!(t, Threshold::Simple(10));
842 assert_eq!(t.simple_value(), Some(10));
843 }
844
845 #[test]
846 fn threshold_simple_serialize_as_hex() {
847 let t = Threshold::Simple(16);
848 let json = serde_json::to_string(&t).unwrap();
849 assert_eq!(json, "\"10\""); }
851
852 #[test]
853 fn threshold_simple_roundtrip() {
854 let t = Threshold::Simple(2);
855 let json = serde_json::to_string(&t).unwrap();
856 let parsed: Threshold = serde_json::from_str(&json).unwrap();
857 assert_eq!(parsed, t);
858 }
859
860 #[test]
861 fn threshold_weighted_roundtrip() {
862 let json = r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#;
863 let t: Threshold = serde_json::from_str(json).unwrap();
864 assert!(t.simple_value().is_none());
865 if let Threshold::Weighted(clauses) = &t {
866 assert_eq!(clauses.len(), 2);
867 assert_eq!(clauses[0].len(), 2);
868 assert_eq!(clauses[1].len(), 3);
869 assert_eq!(clauses[0][0].numerator, 1);
870 assert_eq!(clauses[0][0].denominator, 2);
871 } else {
872 panic!("expected Weighted");
873 }
874 let reserialized = serde_json::to_string(&t).unwrap();
875 let reparsed: Threshold = serde_json::from_str(&reserialized).unwrap();
876 assert_eq!(reparsed, t);
877 }
878
879 #[test]
880 fn threshold_rejects_invalid_hex() {
881 let result = serde_json::from_str::<Threshold>("\"xyz\"");
882 assert!(result.is_err());
883 }
884
885 #[test]
888 fn fraction_sum_one_third_times_three() {
889 let f: Fraction = "1/3".parse().unwrap();
890 assert!(Fraction::sum_meets_one(&[&f, &f, &f]));
891 }
892
893 #[test]
894 fn fraction_sum_two_thirds_not_enough() {
895 let f: Fraction = "1/3".parse().unwrap();
896 assert!(!Fraction::sum_meets_one(&[&f, &f]));
897 }
898
899 #[test]
900 fn fraction_sum_halves() {
901 let f: Fraction = "1/2".parse().unwrap();
902 assert!(Fraction::sum_meets_one(&[&f, &f]));
903 assert!(!Fraction::sum_meets_one(&[&f]));
904 }
905
906 #[test]
907 fn fraction_sum_empty_is_false() {
908 assert!(!Fraction::sum_meets_one(&[]));
909 }
910
911 #[test]
914 fn threshold_simple_satisfied() {
915 let t = Threshold::Simple(2);
916 assert!(t.is_satisfied(&[0, 1], 3));
917 assert!(t.is_satisfied(&[0, 1, 2], 3));
918 assert!(!t.is_satisfied(&[0], 3));
919 }
920
921 #[test]
922 fn threshold_simple_zero_always_satisfied() {
923 let t = Threshold::Simple(0);
924 assert!(t.is_satisfied(&[], 3));
925 }
926
927 #[test]
928 fn threshold_simple_deduplicates_indices() {
929 let t = Threshold::Simple(2);
930 assert!(!t.is_satisfied(&[0, 0], 3));
932 }
933
934 #[test]
935 fn threshold_simple_rejects_out_of_range() {
936 let t = Threshold::Simple(1);
937 assert!(!t.is_satisfied(&[5], 3));
939 }
940
941 #[test]
942 fn threshold_weighted_two_of_three() {
943 let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2"]]"#).unwrap();
945 assert!(t.is_satisfied(&[0, 1], 3));
946 assert!(t.is_satisfied(&[1, 2], 3));
947 assert!(!t.is_satisfied(&[0], 3));
948 }
949
950 #[test]
951 fn threshold_weighted_with_reserves() {
952 let t: Threshold = serde_json::from_str(r#"[["1/2","1/2","1/2","1/4","1/4"]]"#).unwrap();
954 assert!(t.is_satisfied(&[0, 1], 5)); assert!(t.is_satisfied(&[0, 3, 4], 5)); assert!(!t.is_satisfied(&[3, 4], 5)); }
958
959 #[test]
960 fn threshold_weighted_multi_clause_and() {
961 let t: Threshold = serde_json::from_str(r#"[["1/2","1/2"],["1/3","1/3","1/3"]]"#).unwrap();
963 assert!(!t.is_satisfied(&[0, 1], 3));
966 assert!(t.is_satisfied(&[0, 1, 2], 3));
968 }
969
970 #[test]
973 fn cesr_key_roundtrip() {
974 let key_str = "DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
975 let key = CesrKey::new_unchecked(key_str.to_string());
976 let json = serde_json::to_string(&key).unwrap();
977 let parsed: CesrKey = serde_json::from_str(&json).unwrap();
978 assert_eq!(parsed.as_str(), key_str);
979 }
980
981 #[test]
982 fn cesr_key_parse_valid() {
983 let key =
984 CesrKey::new_unchecked("DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string());
985 assert!(key.parse().is_ok());
986 }
987
988 #[test]
989 fn cesr_key_parse_invalid() {
990 let key = CesrKey::new_unchecked("not-a-valid-key".to_string());
991 assert!(key.parse().is_err());
992 }
993
994 #[test]
997 fn config_trait_serde_roundtrip() {
998 let traits = vec![ConfigTrait::EstablishmentOnly, ConfigTrait::DoNotDelegate];
999 let json = serde_json::to_string(&traits).unwrap();
1000 assert_eq!(json, r#"["EO","DND"]"#);
1001 let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
1002 assert_eq!(parsed, traits);
1003 }
1004
1005 #[test]
1006 fn config_trait_all_variants_roundtrip() {
1007 let all = vec![
1008 ConfigTrait::EstablishmentOnly,
1009 ConfigTrait::DoNotDelegate,
1010 ConfigTrait::RegistrarBackers,
1011 ConfigTrait::NoRegistrarBackers,
1012 ];
1013 let json = serde_json::to_string(&all).unwrap();
1014 assert_eq!(json, r#"["EO","DND","RB","NRB"]"#);
1015 let parsed: Vec<ConfigTrait> = serde_json::from_str(&json).unwrap();
1016 assert_eq!(parsed, all);
1017 }
1018
1019 #[test]
1022 fn version_string_display() {
1023 let vs = VersionString::json(256);
1024 assert_eq!(vs.to_string(), "KERI10JSON000100_");
1025 }
1026
1027 #[test]
1028 fn version_string_placeholder() {
1029 let vs = VersionString::placeholder();
1030 assert_eq!(vs.to_string(), "KERI10JSON000000_");
1031 assert_eq!(vs.size, 0);
1032 }
1033
1034 #[test]
1035 fn version_string_parse_full() {
1036 let vs: VersionString = serde_json::from_str("\"KERI10JSON000100_\"").unwrap();
1037 assert_eq!(vs.kind, "JSON");
1038 assert_eq!(vs.size, 256);
1039 }
1040
1041 #[test]
1042 fn version_string_rejects_legacy_short() {
1043 assert!(serde_json::from_str::<VersionString>("\"KERI10JSON\"").is_err());
1047 assert!(serde_json::from_str::<VersionString>("\"KERI10JSON0001\"").is_err());
1048 }
1049
1050 #[test]
1051 fn version_string_roundtrip() {
1052 let vs = VersionString::json(1024);
1053 let json = serde_json::to_string(&vs).unwrap();
1054 let parsed: VersionString = serde_json::from_str(&json).unwrap();
1055 assert_eq!(parsed, vs);
1056 }
1057
1058 #[test]
1059 fn version_string_rejects_invalid() {
1060 assert!(serde_json::from_str::<VersionString>("\"INVALID\"").is_err());
1061 }
1062}