made_core/value_objects/ceremony/
ceremony_version.rs1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::DomainError;
6
7const DEFAULT_VERSION: &str = "1.0";
8const MAX_VERSION_LEN: usize = 64;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
22#[serde(transparent)]
23pub struct CeremonyVersion(String);
24
25impl CeremonyVersion {
26 pub fn new(raw: impl Into<String>) -> Result<Self, DomainError> {
27 let value = raw.into();
28 let trimmed = value.trim();
29 if trimmed.is_empty() {
30 return Err(DomainError::EmptyField {
31 field: "ceremony_version",
32 });
33 }
34 if trimmed.len() > MAX_VERSION_LEN {
35 return Err(DomainError::FieldTooLong {
36 field: "ceremony_version",
37 actual: trimmed.len(),
38 max: MAX_VERSION_LEN,
39 });
40 }
41 if !trimmed
45 .chars()
46 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
47 {
48 return Err(DomainError::InvalidCharacters {
49 field: "ceremony_version",
50 });
51 }
52 Ok(Self(trimmed.to_owned()))
53 }
54
55 #[must_use]
58 pub fn v1() -> Self {
59 Self(DEFAULT_VERSION.to_owned())
60 }
61
62 #[must_use]
63 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68impl fmt::Display for CeremonyVersion {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str(&self.0)
71 }
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn a_second_version_is_now_expressible() {
80 assert_eq!(CeremonyVersion::new("2.0").unwrap().as_str(), "2.0");
81 assert_eq!(CeremonyVersion::new("v2-rc1").unwrap().as_str(), "v2-rc1");
82 assert_eq!(
83 CeremonyVersion::new("2026-07-30").unwrap().as_str(),
84 "2026-07-30"
85 );
86 }
87
88 #[test]
89 fn the_default_is_what_existing_documents_carry() {
90 assert_eq!(CeremonyVersion::v1().as_str(), "1.0");
91 assert_eq!(CeremonyVersion::new("1.0").unwrap(), CeremonyVersion::v1());
92 }
93
94 #[test]
95 fn characters_that_would_need_escaping_in_a_key_are_rejected() {
96 for rejected in [
97 "",
98 " ",
99 "1.0/2",
100 "1 0",
101 "1.0\u{0}",
102 "a".repeat(65).as_str(),
103 ] {
104 assert!(
105 CeremonyVersion::new(rejected).is_err(),
106 "{rejected:?} was accepted"
107 );
108 }
109 }
110}