use super::{Sha256Hex, TextSalt};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const WITHHELD_TEXT: &str = "[text withheld]";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(deny_unknown_fields)]
pub struct TextCommitment {
pub commitment: Sha256Hex,
}
impl TextCommitment {
pub fn of(salt: &TextSalt, text: &str) -> Self {
let mut hasher = Sha256::new();
hasher.update(salt.as_bytes());
hasher.update(text.as_bytes());
Self {
commitment: Sha256Hex::from(<[u8; 32]>::from(hasher.finalize())),
}
}
pub fn matches(&self, text: &CommittedText) -> bool {
*self == Self::of(&text.salt, &text.text)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(deny_unknown_fields)]
pub struct CommittedText {
pub salt: TextSalt,
pub text: String,
}
impl CommittedText {
pub fn new(text: impl Into<String>) -> Self {
Self::with_salt(TextSalt::random(), text)
}
pub fn with_salt(salt: TextSalt, text: impl Into<String>) -> Self {
Self {
salt,
text: text.into(),
}
}
pub fn commitment(&self) -> TextCommitment {
TextCommitment::of(&self.salt, &self.text)
}
}
impl From<&str> for CommittedText {
fn from(text: &str) -> Self {
Self::new(text)
}
}
impl From<String> for CommittedText {
fn from(text: String) -> Self {
Self::new(text)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(untagged)]
pub enum AmendmentText {
Plain(String),
Committed(TextCommitment),
}
impl AmendmentText {
pub fn is_plain(&self) -> bool {
matches!(self, Self::Plain(_))
}
pub fn resolve<'a>(&'a self, beside: Option<&'a CommittedText>) -> &'a str {
match (self, beside) {
(Self::Plain(text), _) => text,
(Self::Committed(c), Some(t)) if c.matches(t) => &t.text,
(Self::Committed(_), _) => WITHHELD_TEXT,
}
}
pub fn status(&self, beside: Option<&CommittedText>) -> Option<TextStatus> {
match (self, beside) {
(Self::Plain(_), _) => None,
(Self::Committed(_), None) => Some(TextStatus::Withheld),
(Self::Committed(c), Some(t)) if c.matches(t) => {
Some(TextStatus::Present)
}
(Self::Committed(_), Some(_)) => Some(TextStatus::Mismatch),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(deny_unknown_fields)]
pub struct AmendmentTexts {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub basis: Option<CommittedText>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<CommittedText>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<CommittedText>,
}
impl AmendmentTexts {
pub fn is_empty(&self) -> bool {
self.basis.is_none() && self.note.is_none() && self.rationale.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
#[serde(rename_all = "snake_case")]
pub enum TextStatus {
Present,
Withheld,
Mismatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "schemars", schemars(inline))]
pub struct AmendmentTextStatus {
pub basis: TextStatus,
pub note: TextStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationale: Option<TextStatus>,
}