identity_credential/credential/
linked_verifiable_presentation_service.rs

1// Copyright 2020-2024 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use identity_core::common::Object;
5use identity_core::common::OrderedSet;
6use identity_core::common::Url;
7use identity_did::DIDUrl;
8use identity_document::service::Service;
9use identity_document::service::ServiceBuilder;
10use identity_document::service::ServiceEndpoint;
11use serde::Deserialize;
12use serde::Serialize;
13
14use crate::error::Result;
15use crate::Error;
16use crate::Error::LinkedVerifiablePresentationError;
17
18/// A service wrapper for a [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint).
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(try_from = "Service", into = "Service")]
21pub struct LinkedVerifiablePresentationService(Service);
22
23impl TryFrom<Service> for LinkedVerifiablePresentationService {
24  type Error = Error;
25
26  fn try_from(service: Service) -> std::result::Result<Self, Self::Error> {
27    LinkedVerifiablePresentationService::check_structure(&service)?;
28    Ok(LinkedVerifiablePresentationService(service))
29  }
30}
31
32impl From<LinkedVerifiablePresentationService> for Service {
33  fn from(service: LinkedVerifiablePresentationService) -> Self {
34    service.0
35  }
36}
37
38impl LinkedVerifiablePresentationService {
39  pub(crate) fn linked_verifiable_presentation_service_type() -> &'static str {
40    "LinkedVerifiablePresentation"
41  }
42
43  /// Constructs a new `LinkedVerifiablePresentationService` that wraps a spec compliant
44  /// [Linked Verifiable Presentation Service Endpoint](https://identity.foundation/linked-vp/#linked-verifiable-presentation-service-endpoint).
45  pub fn new(
46    did_url: DIDUrl,
47    verifiable_presentation_urls: impl Into<OrderedSet<Url>>,
48    properties: Object,
49  ) -> Result<Self> {
50    let verifiable_presentation_urls: OrderedSet<Url> = verifiable_presentation_urls.into();
51    let builder: ServiceBuilder = Service::builder(properties)
52      .id(did_url)
53      .type_(Self::linked_verifiable_presentation_service_type());
54    if verifiable_presentation_urls.len() == 1 {
55      let vp_url = verifiable_presentation_urls
56        .into_iter()
57        .next()
58        .expect("element 0 exists");
59      let service = builder
60        .service_endpoint(vp_url)
61        .build()
62        .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?;
63      Ok(Self(service))
64    } else {
65      let service = builder
66        .service_endpoint(ServiceEndpoint::Set(verifiable_presentation_urls))
67        .build()
68        .map_err(|err| LinkedVerifiablePresentationError(Box::new(err)))?;
69      Ok(Self(service))
70    }
71  }
72
73  /// Checks the semantic structure of a Linked Verifiable Presentation Service.
74  ///
75  /// Note: `{"type": ["LinkedVerifiablePresentation"]}` might be serialized the same way as `{"type":
76  /// "LinkedVerifiablePresentation"}` which passes the semantic check.
77  pub fn check_structure(service: &Service) -> Result<()> {
78    if service.type_().len() != 1 {
79      return Err(LinkedVerifiablePresentationError("invalid service type".into()));
80    }
81
82    let service_type = service
83      .type_()
84      .get(0)
85      .ok_or_else(|| LinkedVerifiablePresentationError("missing service type".into()))?;
86
87    if service_type != Self::linked_verifiable_presentation_service_type() {
88      return Err(LinkedVerifiablePresentationError(
89        format!(
90          "expected `{}` service type",
91          Self::linked_verifiable_presentation_service_type()
92        )
93        .into(),
94      ));
95    }
96
97    match service.service_endpoint() {
98      ServiceEndpoint::One(_) => Ok(()),
99      ServiceEndpoint::Set(_) => Ok(()),
100      ServiceEndpoint::Map(_) => Err(LinkedVerifiablePresentationError(
101        "service endpoints must be either a string or a set".into(),
102      )),
103    }
104  }
105
106  /// Returns the Verifiable Presentations contained in the Linked Verifiable Presentation Service.
107  pub fn verifiable_presentation_urls(&self) -> &[Url] {
108    match self.0.service_endpoint() {
109      ServiceEndpoint::One(endpoint) => std::slice::from_ref(endpoint),
110      ServiceEndpoint::Set(endpoints) => endpoints.as_slice(),
111      ServiceEndpoint::Map(_) => {
112        unreachable!("the service endpoint is never a map per the `LinkedVerifiablePresentationService` type invariant")
113      }
114    }
115  }
116
117  /// Returns a reference to the `Service` id.
118  pub fn id(&self) -> &DIDUrl {
119    self.0.id()
120  }
121}
122
123#[cfg(test)]
124mod tests {
125  use crate::credential::linked_verifiable_presentation_service::LinkedVerifiablePresentationService;
126  use identity_core::common::Object;
127  use identity_core::common::OrderedSet;
128  use identity_core::common::Url;
129  use identity_core::convert::FromJson;
130  use identity_did::DIDUrl;
131  use identity_document::service::Service;
132  use serde_json::json;
133
134  #[test]
135  fn test_create_service_single_vp() {
136    let mut linked_vps: OrderedSet<Url> = OrderedSet::new();
137    linked_vps.append(Url::parse("https://foo.example-1.com").unwrap());
138
139    let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new(
140      DIDUrl::parse("did:example:123#foo").unwrap(),
141      linked_vps,
142      Object::new(),
143    )
144    .unwrap();
145
146    let service_from_json: Service = Service::from_json_value(json!({
147        "id": "did:example:123#foo",
148        "type": "LinkedVerifiablePresentation",
149        "serviceEndpoint": "https://foo.example-1.com"
150    }))
151    .unwrap();
152    assert_eq!(Service::from(service), service_from_json);
153  }
154
155  #[test]
156  fn test_create_service_multiple_vps() {
157    let url_1 = "https://foo.example-1.com";
158    let url_2 = "https://bar.example-2.com";
159    let mut linked_vps = OrderedSet::new();
160    linked_vps.append(Url::parse(url_1).unwrap());
161    linked_vps.append(Url::parse(url_2).unwrap());
162
163    let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::new(
164      DIDUrl::parse("did:example:123#foo").unwrap(),
165      linked_vps,
166      Object::new(),
167    )
168    .unwrap();
169
170    let service_from_json: Service = Service::from_json_value(json!({
171        "id":"did:example:123#foo",
172        "type": "LinkedVerifiablePresentation",
173        "serviceEndpoint": [url_1, url_2]
174    }))
175    .unwrap();
176    assert_eq!(Service::from(service), service_from_json);
177  }
178
179  #[test]
180  fn test_valid_single_vp() {
181    let service: Service = Service::from_json_value(json!({
182        "id": "did:example:123#foo",
183        "type": "LinkedVerifiablePresentation",
184        "serviceEndpoint": "https://foo.example-1.com"
185    }))
186    .unwrap();
187    let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap();
188    let linked_vps: Vec<Url> = vec![Url::parse("https://foo.example-1.com").unwrap()];
189    assert_eq!(service.verifiable_presentation_urls(), linked_vps);
190  }
191
192  #[test]
193  fn test_valid_multiple_vps() {
194    let service: Service = Service::from_json_value(json!({
195        "id": "did:example:123#foo",
196        "type": "LinkedVerifiablePresentation",
197        "serviceEndpoint": ["https://foo.example-1.com", "https://foo.example-2.com"]
198    }))
199    .unwrap();
200    let service: LinkedVerifiablePresentationService = LinkedVerifiablePresentationService::try_from(service).unwrap();
201    let linked_vps: Vec<Url> = vec![
202      Url::parse("https://foo.example-1.com").unwrap(),
203      Url::parse("https://foo.example-2.com").unwrap(),
204    ];
205    assert_eq!(service.verifiable_presentation_urls(), linked_vps);
206  }
207}