1use super::{Sha256Hex, TextSalt};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21
22pub const WITHHELD_TEXT: &str = "[text withheld]";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
29#[cfg_attr(feature = "schemars", schemars(inline))]
30#[serde(deny_unknown_fields)]
31pub struct TextCommitment {
32 pub commitment: Sha256Hex,
33}
34
35impl TextCommitment {
36 pub fn of(salt: &TextSalt, text: &str) -> Self {
37 let mut hasher = Sha256::new();
38 hasher.update(salt.as_bytes());
39 hasher.update(text.as_bytes());
40 Self {
41 commitment: Sha256Hex::from(<[u8; 32]>::from(hasher.finalize())),
42 }
43 }
44
45 pub fn matches(&self, text: &CommittedText) -> bool {
46 *self == Self::of(&text.salt, &text.text)
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
54#[cfg_attr(feature = "schemars", schemars(inline))]
55#[serde(deny_unknown_fields)]
56pub struct CommittedText {
57 pub salt: TextSalt,
58 pub text: String,
59}
60
61impl CommittedText {
62 pub fn new(text: impl Into<String>) -> Self {
64 Self::with_salt(TextSalt::random(), text)
65 }
66
67 pub fn with_salt(salt: TextSalt, text: impl Into<String>) -> Self {
69 Self {
70 salt,
71 text: text.into(),
72 }
73 }
74
75 pub fn commitment(&self) -> TextCommitment {
76 TextCommitment::of(&self.salt, &self.text)
77 }
78}
79
80impl From<&str> for CommittedText {
81 fn from(text: &str) -> Self {
82 Self::new(text)
83 }
84}
85
86impl From<String> for CommittedText {
87 fn from(text: String) -> Self {
88 Self::new(text)
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
96#[cfg_attr(feature = "schemars", schemars(inline))]
97#[serde(untagged)]
98pub enum AmendmentText {
99 Plain(String),
100 Committed(TextCommitment),
101}
102
103impl AmendmentText {
104 pub fn is_plain(&self) -> bool {
105 matches!(self, Self::Plain(_))
106 }
107
108 pub fn resolve<'a>(&'a self, beside: Option<&'a CommittedText>) -> &'a str {
111 match (self, beside) {
112 (Self::Plain(text), _) => text,
113 (Self::Committed(c), Some(t)) if c.matches(t) => &t.text,
114 (Self::Committed(_), _) => WITHHELD_TEXT,
115 }
116 }
117
118 pub fn status(&self, beside: Option<&CommittedText>) -> Option<TextStatus> {
121 match (self, beside) {
122 (Self::Plain(_), _) => None,
123 (Self::Committed(_), None) => Some(TextStatus::Withheld),
124 (Self::Committed(c), Some(t)) if c.matches(t) => {
125 Some(TextStatus::Present)
126 }
127 (Self::Committed(_), Some(_)) => Some(TextStatus::Mismatch),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
135#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
136#[cfg_attr(feature = "schemars", schemars(inline))]
137#[serde(deny_unknown_fields)]
138pub struct AmendmentTexts {
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub basis: Option<CommittedText>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub note: Option<CommittedText>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub rationale: Option<CommittedText>,
145}
146
147impl AmendmentTexts {
148 pub fn is_empty(&self) -> bool {
149 self.basis.is_none() && self.note.is_none() && self.rationale.is_none()
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
156#[cfg_attr(feature = "schemars", schemars(inline))]
157#[serde(rename_all = "snake_case")]
158pub enum TextStatus {
159 Present,
161 Withheld,
163 Mismatch,
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
169#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
170#[cfg_attr(feature = "schemars", schemars(inline))]
171pub struct AmendmentTextStatus {
172 pub basis: TextStatus,
173 pub note: TextStatus,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub rationale: Option<TextStatus>,
177}