identity_credential/revocation/status_list_2021/
entry.rsuse identity_core::common::Url;
use serde::de::Error;
use serde::de::Visitor;
use serde::Deserialize;
use serde::Serialize;
use crate::credential::Status;
use super::credential::StatusPurpose;
const CREDENTIAL_STATUS_TYPE: &str = "StatusList2021Entry";
fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ExactStrVisitor(&'static str);
impl Visitor<'_> for ExactStrVisitor {
type Value = &'static str;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "the exact string \"{}\"", self.0)
}
fn visit_str<E: Error>(self, str: &str) -> Result<Self::Value, E> {
if str == self.0 {
Ok(self.0)
} else {
Err(E::custom(format!("not \"{}\"", self.0)))
}
}
}
deserializer
.deserialize_str(ExactStrVisitor(CREDENTIAL_STATUS_TYPE))
.map(ToOwned::to_owned)
}
fn serialize_number_as_string<S>(value: &usize, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&value.to_string())
}
#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StatusList2021Entry {
id: Url,
#[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")]
type_: String,
status_purpose: StatusPurpose,
#[serde(
deserialize_with = "serde_aux::prelude::deserialize_number_from_string",
serialize_with = "serialize_number_as_string"
)]
status_list_index: usize,
status_list_credential: Url,
}
impl TryFrom<&Status> for StatusList2021Entry {
type Error = serde_json::Error;
fn try_from(status: &Status) -> Result<Self, Self::Error> {
let json_status = serde_json::to_value(status)?;
serde_json::from_value(json_status)
}
}
impl From<StatusList2021Entry> for Status {
fn from(entry: StatusList2021Entry) -> Self {
let json_status = serde_json::to_value(entry).unwrap(); serde_json::from_value(json_status).unwrap() }
}
impl StatusList2021Entry {
pub fn new(status_list: Url, purpose: StatusPurpose, index: usize, id: Option<Url>) -> Self {
let id = id.unwrap_or_else(|| {
let mut id = status_list.clone();
id.set_fragment(None);
id
});
Self {
id,
type_: CREDENTIAL_STATUS_TYPE.to_owned(),
status_purpose: purpose,
status_list_credential: status_list,
status_list_index: index,
}
}
pub const fn id(&self) -> &Url {
&self.id
}
pub const fn purpose(&self) -> StatusPurpose {
self.status_purpose
}
pub const fn index(&self) -> usize {
self.status_list_index
}
pub const fn status_list_credential(&self) -> &Url {
&self.status_list_credential
}
}
#[cfg(test)]
mod tests {
use super::*;
const STATUS_LIST_ENTRY_SAMPLE: &str = r#"
{
"id": "https://example.com/credentials/status/3#94567",
"type": "StatusList2021Entry",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://example.com/credentials/status/3"
}"#;
#[test]
fn entry_deserialization_works() {
let deserialized =
serde_json::from_str::<StatusList2021Entry>(STATUS_LIST_ENTRY_SAMPLE).expect("Failed to deserialize");
let status = StatusList2021Entry::new(
Url::parse("https://example.com/credentials/status/3").unwrap(),
StatusPurpose::Revocation,
94567,
Url::parse("https://example.com/credentials/status/3#94567").ok(),
);
assert_eq!(status, deserialized);
}
#[test]
#[should_panic]
fn deserializing_wrong_status_type_fails() {
let status = serde_json::json!({
"id": "https://example.com/credentials/status/3#94567",
"type": "Whatever2024",
"statusPurpose": "revocation",
"statusListIndex": "94567",
"statusListCredential": "https://example.com/credentials/status/3"
});
serde_json::from_value::<StatusList2021Entry>(status).expect("wrong type");
}
#[test]
fn test_status_list_index_serialization() {
let base_url = Url::parse("https://example.com/credentials/status/3").unwrap();
let entry1 = StatusList2021Entry::new(base_url.clone(), StatusPurpose::Revocation, 94567, None);
let json1 = serde_json::to_value(&entry1).unwrap();
assert_eq!(json1["statusListIndex"], "94567");
}
}