use time::OffsetDateTime;
use crate::SiwxError;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SiwxMessage {
pub domain: String,
pub address: String,
pub statement: Option<String>,
pub uri: String,
pub version: String,
pub chain_id: String,
pub nonce: Option<String>,
#[cfg_attr(
feature = "serde",
serde(default, with = "time::serde::rfc3339::option")
)]
pub issued_at: Option<OffsetDateTime>,
#[cfg_attr(
feature = "serde",
serde(default, with = "time::serde::rfc3339::option")
)]
pub expiration_time: Option<OffsetDateTime>,
#[cfg_attr(
feature = "serde",
serde(default, with = "time::serde::rfc3339::option")
)]
pub not_before: Option<OffsetDateTime>,
pub request_id: Option<String>,
#[cfg_attr(feature = "serde", serde(default))]
pub resources: Vec<String>,
}
impl SiwxMessage {
pub fn new(
domain: impl Into<String>,
address: impl Into<String>,
uri: impl Into<String>,
version: impl Into<String>,
chain_id: impl Into<String>,
) -> Result<Self, SiwxError> {
Ok(Self {
domain: non_empty(domain.into(), "domain")?,
address: non_empty(address.into(), "address")?,
uri: non_empty(uri.into(), "uri")?,
version: non_empty(version.into(), "version")?,
chain_id: non_empty(chain_id.into(), "chain_id")?,
statement: None,
nonce: None,
issued_at: None,
expiration_time: None,
not_before: None,
request_id: None,
resources: Vec::new(),
})
}
#[must_use]
pub fn with_statement(mut self, statement: impl Into<String>) -> Self {
self.statement = Some(statement.into());
self
}
#[must_use]
pub fn with_nonce(mut self, nonce: impl Into<String>) -> Self {
self.nonce = Some(nonce.into());
self
}
#[must_use]
pub const fn with_issued_at(mut self, t: OffsetDateTime) -> Self {
self.issued_at = Some(t);
self
}
#[must_use]
pub const fn with_expiration_time(mut self, t: OffsetDateTime) -> Self {
self.expiration_time = Some(t);
self
}
#[must_use]
pub const fn with_not_before(mut self, t: OffsetDateTime) -> Self {
self.not_before = Some(t);
self
}
#[must_use]
pub fn with_request_id(mut self, rid: impl Into<String>) -> Self {
self.request_id = Some(rid.into());
self
}
#[must_use]
pub fn with_resources(
mut self,
resources: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.resources = resources.into_iter().map(Into::into).collect();
self
}
}
fn non_empty(s: String, field: &str) -> Result<String, SiwxError> {
if s.is_empty() {
return Err(SiwxError::InvalidFormat(format!(
"{field} must not be empty"
)));
}
Ok(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_empty_mandatory_fields() {
assert!(matches!(
SiwxMessage::new("", "a", "https://d.com", "1", "1").unwrap_err(),
SiwxError::InvalidFormat(_)
));
assert!(matches!(
SiwxMessage::new("d.com", "", "https://d.com", "1", "1").unwrap_err(),
SiwxError::InvalidFormat(_)
));
}
#[test]
fn builder_chains_all_setters() {
let msg = SiwxMessage::new("d.com", "a", "https://d.com", "1", "1")
.expect("valid")
.with_statement("hi")
.with_nonce("n")
.with_request_id("rid")
.with_resources(["https://r.com"]);
assert_eq!(msg.statement.as_deref(), Some("hi"));
assert_eq!(msg.nonce.as_deref(), Some("n"));
assert_eq!(msg.request_id.as_deref(), Some("rid"));
assert_eq!(msg.resources, ["https://r.com"]);
}
}