identity_credential/revocation/status_list_2021/
entry.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use identity_core::common::Url;
5use serde::de::Error;
6use serde::de::Visitor;
7use serde::Deserialize;
8use serde::Serialize;
9
10use crate::credential::Status;
11
12use super::credential::StatusPurpose;
13
14const CREDENTIAL_STATUS_TYPE: &str = "StatusList2021Entry";
15
16fn deserialize_status_entry_type<'de, D>(deserializer: D) -> Result<String, D::Error>
17where
18  D: serde::Deserializer<'de>,
19{
20  struct ExactStrVisitor(&'static str);
21  impl Visitor<'_> for ExactStrVisitor {
22    type Value = &'static str;
23    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24      write!(formatter, "the exact string \"{}\"", self.0)
25    }
26    fn visit_str<E: Error>(self, str: &str) -> Result<Self::Value, E> {
27      if str == self.0 {
28        Ok(self.0)
29      } else {
30        Err(E::custom(format!("not \"{}\"", self.0)))
31      }
32    }
33  }
34
35  deserializer
36    .deserialize_str(ExactStrVisitor(CREDENTIAL_STATUS_TYPE))
37    .map(ToOwned::to_owned)
38}
39
40/// Serialize usize as string.
41fn serialize_number_as_string<S>(value: &usize, serializer: S) -> Result<S::Ok, S::Error>
42where
43  S: serde::Serializer,
44{
45  serializer.serialize_str(&value.to_string())
46}
47
48/// [StatusList2021Entry](https://www.w3.org/TR/2023/WD-vc-status-list-20230427/#statuslist2021entry) implementation.
49#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)]
50#[serde(rename_all = "camelCase")]
51pub struct StatusList2021Entry {
52  id: Url,
53  #[serde(rename = "type", deserialize_with = "deserialize_status_entry_type")]
54  type_: String,
55  status_purpose: StatusPurpose,
56  #[serde(
57    deserialize_with = "serde_aux::prelude::deserialize_number_from_string",
58    serialize_with = "serialize_number_as_string"
59  )]
60  status_list_index: usize,
61  status_list_credential: Url,
62}
63
64impl TryFrom<&Status> for StatusList2021Entry {
65  type Error = serde_json::Error;
66  fn try_from(status: &Status) -> Result<Self, Self::Error> {
67    let json_status = serde_json::to_value(status)?;
68    serde_json::from_value(json_status)
69  }
70}
71
72impl From<StatusList2021Entry> for Status {
73  fn from(entry: StatusList2021Entry) -> Self {
74    let json_status = serde_json::to_value(entry).unwrap(); // Safety: shouldn't go out of memory
75    serde_json::from_value(json_status).unwrap() // Safety: `StatusList2021Entry` is a credential status
76  }
77}
78
79impl StatusList2021Entry {
80  /// Creates a new [`StatusList2021Entry`].
81  pub fn new(status_list: Url, purpose: StatusPurpose, index: usize, id: Option<Url>) -> Self {
82    let id = id.unwrap_or_else(|| {
83      let mut id = status_list.clone();
84      id.set_fragment(None);
85      id
86    });
87
88    Self {
89      id,
90      type_: CREDENTIAL_STATUS_TYPE.to_owned(),
91      status_purpose: purpose,
92      status_list_credential: status_list,
93      status_list_index: index,
94    }
95  }
96
97  /// Returns this `credentialStatus`'s `id`.
98  pub const fn id(&self) -> &Url {
99    &self.id
100  }
101
102  /// Returns the purpose of this entry.
103  pub const fn purpose(&self) -> StatusPurpose {
104    self.status_purpose
105  }
106
107  /// Returns the index of this entry.
108  pub const fn index(&self) -> usize {
109    self.status_list_index
110  }
111
112  /// Returns the referenced [`StatusList2021Credential`]'s [`Url`].
113  pub const fn status_list_credential(&self) -> &Url {
114    &self.status_list_credential
115  }
116}
117
118#[cfg(test)]
119mod tests {
120  use super::*;
121
122  const STATUS_LIST_ENTRY_SAMPLE: &str = r#"
123{
124    "id": "https://example.com/credentials/status/3#94567",
125    "type": "StatusList2021Entry",
126    "statusPurpose": "revocation",
127    "statusListIndex": "94567",
128    "statusListCredential": "https://example.com/credentials/status/3"
129}"#;
130
131  #[test]
132  fn entry_deserialization_works() {
133    let deserialized =
134      serde_json::from_str::<StatusList2021Entry>(STATUS_LIST_ENTRY_SAMPLE).expect("Failed to deserialize");
135    let status = StatusList2021Entry::new(
136      Url::parse("https://example.com/credentials/status/3").unwrap(),
137      StatusPurpose::Revocation,
138      94567,
139      Url::parse("https://example.com/credentials/status/3#94567").ok(),
140    );
141    assert_eq!(status, deserialized);
142  }
143
144  #[test]
145  #[should_panic]
146  fn deserializing_wrong_status_type_fails() {
147    let status = serde_json::json!({
148      "id": "https://example.com/credentials/status/3#94567",
149      "type": "Whatever2024",
150      "statusPurpose": "revocation",
151      "statusListIndex": "94567",
152      "statusListCredential": "https://example.com/credentials/status/3"
153    });
154    serde_json::from_value::<StatusList2021Entry>(status).expect("wrong type");
155  }
156
157  #[test]
158  fn test_status_list_index_serialization() {
159    let base_url = Url::parse("https://example.com/credentials/status/3").unwrap();
160
161    let entry1 = StatusList2021Entry::new(base_url.clone(), StatusPurpose::Revocation, 94567, None);
162    let json1 = serde_json::to_value(&entry1).unwrap();
163    assert_eq!(json1["statusListIndex"], "94567");
164  }
165}