Skip to main content

identity_kit/vc/
model.rs

1//! Building blocks shared by credentials and presentations: a [`Party`] (the issuer or the
2//! holder), a [`Subject`] and the [`TypedObject`] of every extension point.
3
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::did::{Context, Did, DidUrl, OneOrMany, Uri};
9
10use super::CredentialError;
11
12/// The base context of the Verifiable Credentials Data Model 1.1.
13pub const CONTEXT_V1: &str = "https://www.w3.org/2018/credentials/v1";
14
15/// The base context of the Verifiable Credentials Data Model 2.0.
16pub const CONTEXT_V2: &str = "https://www.w3.org/ns/credentials/v2";
17
18/// The issuer of a credential or the holder of a presentation: a URI, or an object whose `id`
19/// is that URI and that carries further properties, a name say.
20///
21/// # Examples
22///
23/// ```
24/// use identity_kit::vc::Party;
25/// use serde_json::json;
26///
27/// let issuer: Party = serde_json::from_value(json!("https://example.edu/issuers/565049"))?;
28/// assert_eq!(issuer.id().as_str(), "https://example.edu/issuers/565049");
29///
30/// let issuer: Party = serde_json::from_value(json!({
31///     "id": "https://example.edu/issuers/565049",
32///     "name": "Example University"
33/// }))?;
34/// assert_eq!(issuer.id().as_str(), "https://example.edu/issuers/565049");
35/// assert_eq!(issuer.properties().and_then(|p| p.get("name")), Some(&json!("Example University")));
36/// # Ok::<(), serde_json::Error>(())
37/// ```
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(untagged)]
40#[non_exhaustive]
41pub enum Party {
42    /// Just the identifier.
43    Uri(Uri),
44    /// The identifier with further properties.
45    Object {
46        /// The identifier.
47        id: Uri,
48        /// Every other property, in insertion order.
49        #[serde(flatten)]
50        properties: IndexMap<String, Value>,
51    },
52}
53
54impl Party {
55    /// A party with `properties` besides its `id`.
56    pub fn object(id: impl Into<Uri>, properties: IndexMap<String, Value>) -> Self {
57        Self::Object {
58            id: id.into(),
59            properties,
60        }
61    }
62
63    /// The identifier.
64    pub fn id(&self) -> &Uri {
65        match self {
66            Self::Uri(id) | Self::Object { id, .. } => id,
67        }
68    }
69
70    /// The properties besides `id`, when the party is an object.
71    pub fn properties(&self) -> Option<&IndexMap<String, Value>> {
72        match self {
73            Self::Uri(_) => None,
74            Self::Object { properties, .. } => Some(properties),
75        }
76    }
77}
78
79impl From<Uri> for Party {
80    fn from(id: Uri) -> Self {
81        Self::Uri(id)
82    }
83}
84
85impl From<Did> for Party {
86    fn from(did: Did) -> Self {
87        Self::Uri(did.into())
88    }
89}
90
91impl From<&Did> for Party {
92    fn from(did: &Did) -> Self {
93        Self::Uri(did.into())
94    }
95}
96
97impl From<DidUrl> for Party {
98    fn from(url: DidUrl) -> Self {
99        Self::Uri(url.into())
100    }
101}
102
103/// A `credentialSubject`: the optional `id` of the subject and the claims about it.
104///
105/// A subject without `id` makes a bearer credential.
106///
107/// # Examples
108///
109/// ```
110/// use identity_kit::did::Uri;
111/// use identity_kit::vc::Subject;
112/// use serde_json::json;
113///
114/// let subject = Subject::new(Uri::parse("did:example:ebfeb1f712ebc6f1c276e12ec21")?)
115///     .with_property("alumniOf", json!({ "name": "Example University" }));
116/// assert_eq!(
117///     serde_json::to_string(&subject)?,
118///     r#"{"id":"did:example:ebfeb1f712ebc6f1c276e12ec21","alumniOf":{"name":"Example University"}}"#
119/// );
120/// # Ok::<(), Box<dyn std::error::Error>>(())
121/// ```
122#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
123#[non_exhaustive]
124pub struct Subject {
125    /// The identifier of the subject, absent in a bearer credential.
126    #[serde(default, skip_serializing_if = "Option::is_none")]
127    pub id: Option<Uri>,
128    /// The claims about the subject, in insertion order.
129    #[serde(flatten)]
130    pub properties: IndexMap<String, Value>,
131}
132
133impl Subject {
134    /// A subject identified by `id`, without claims yet.
135    pub fn new(id: impl Into<Uri>) -> Self {
136        Self {
137            id: Some(id.into()),
138            properties: IndexMap::new(),
139        }
140    }
141
142    /// This subject with the claim `name` set to `value`.
143    #[must_use]
144    pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
145        self.properties.insert(name.into(), value.into());
146        self
147    }
148}
149
150/// An object with a `type` and usually an `id`: a `credentialStatus`, `credentialSchema`,
151/// `refreshService`, `termsOfUse` or `evidence` entry, whose meaning the type defines.
152///
153/// # Examples
154///
155/// ```
156/// use identity_kit::did::Uri;
157/// use identity_kit::vc::TypedObject;
158/// use serde_json::json;
159///
160/// let status = TypedObject::new("StatusList2021Entry")
161///     .with_id(Uri::parse("https://example.edu/status/24#94567")?)
162///     .with_property("statusPurpose", "revocation")
163///     .with_property("statusListIndex", "94567");
164/// assert!(status.has_type("StatusList2021Entry"));
165/// assert_eq!(
166///     serde_json::to_string(&status)?,
167///     r#"{"id":"https://example.edu/status/24#94567","type":"StatusList2021Entry","statusPurpose":"revocation","statusListIndex":"94567"}"#
168/// );
169/// # Ok::<(), Box<dyn std::error::Error>>(())
170/// ```
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[non_exhaustive]
173pub struct TypedObject {
174    /// The identifier, which most extension points require.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub id: Option<Uri>,
177    /// The `type`, which selects the meaning of the other properties.
178    #[serde(rename = "type")]
179    pub types: OneOrMany<String>,
180    /// Every other property, in insertion order.
181    #[serde(flatten)]
182    pub properties: IndexMap<String, Value>,
183}
184
185impl TypedObject {
186    /// An object of type `object_type`, without `id`.
187    pub fn new(object_type: impl Into<String>) -> Self {
188        Self {
189            id: None,
190            types: OneOrMany::One(object_type.into()),
191            properties: IndexMap::new(),
192        }
193    }
194
195    /// This object with its `id` set.
196    #[must_use]
197    pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
198        self.id = Some(id.into());
199        self
200    }
201
202    /// This object with the property `name` set to `value`.
203    #[must_use]
204    pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
205        self.properties.insert(name.into(), value.into());
206        self
207    }
208
209    /// Whether `object_type` is among the types.
210    pub fn has_type(&self, object_type: &str) -> bool {
211        self.types.iter().any(|t| t == object_type)
212    }
213
214    /// The first type.
215    pub fn object_type(&self) -> &str {
216        self.types.first().map_or("", String::as_str)
217    }
218}
219
220/// The first URI of `context`, which the data model requires to be its base context.
221pub(super) fn first_context(context: &Context) -> Option<&str> {
222    match context.as_value() {
223        Value::String(uri) => Some(uri),
224        Value::Array(items) => items.first().and_then(Value::as_str),
225        _ => None,
226    }
227}
228
229/// Checks that `context` starts with `base` and `types` include `required`.
230pub(super) fn check_context_and_type(
231    context: &Context,
232    base: &'static str,
233    types: &OneOrMany<String>,
234    required: &'static str,
235) -> Result<(), CredentialError> {
236    if first_context(context) != Some(base) {
237        return Err(CredentialError::InvalidContext { expected: base });
238    }
239    if !types.iter().any(|t| t == required) {
240        return Err(CredentialError::MissingType(required));
241    }
242    Ok(())
243}
244
245/// `context` with `uri` appended.
246pub(super) fn append_context(context: Context, uri: String) -> Context {
247    let mut uris = match context.into_value() {
248        Value::Array(items) => items,
249        single => vec![single],
250    };
251    uris.push(Value::String(uri));
252    Context::new(Value::Array(uris)).expect("a non-empty array")
253}
254
255/// `types` with `added` appended, as a list.
256pub(super) fn push_type(types: &mut OneOrMany<String>, added: String) {
257    let mut list = core::mem::replace(types, OneOrMany::Many(Vec::new())).into_vec();
258    list.push(added);
259    *types = OneOrMany::Many(list);
260}