identity_document/service/
service.rs

1// Copyright 2020-2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use core::fmt::Display;
5use core::fmt::Formatter;
6
7use serde::de;
8use serde::Deserialize;
9use serde::Serialize;
10
11use identity_core::common::KeyComparable;
12use identity_core::common::Object;
13use identity_core::common::OneOrSet;
14use identity_core::convert::FmtJson;
15
16use crate::error::Error;
17use crate::error::Result;
18use crate::service::ServiceBuilder;
19use crate::service::ServiceEndpoint;
20use identity_did::CoreDID;
21use identity_did::DIDUrl;
22
23/// A DID Document Service used to enable trusted interactions associated with a DID subject.
24///
25/// [Specification](https://www.w3.org/TR/did-core/#services)
26#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
27pub struct Service {
28  #[serde(deserialize_with = "deserialize_id_with_fragment")]
29  pub(crate) id: DIDUrl,
30  #[serde(rename = "type")]
31  pub(crate) type_: OneOrSet<String>,
32  #[serde(rename = "serviceEndpoint")]
33  pub(crate) service_endpoint: ServiceEndpoint,
34  #[serde(flatten)]
35  pub(crate) properties: Object,
36}
37
38/// Deserializes an [`DIDUrl`] while enforcing that its fragment is non-empty.
39fn deserialize_id_with_fragment<'de, D>(deserializer: D) -> Result<DIDUrl, D::Error>
40where
41  D: de::Deserializer<'de>,
42{
43  let did_url: DIDUrl = DIDUrl::deserialize(deserializer)?;
44  if did_url.fragment().unwrap_or_default().is_empty() {
45    return Err(de::Error::custom(Error::InvalidService("empty id fragment")));
46  }
47  Ok(did_url)
48}
49
50impl Service {
51  /// Creates a `ServiceBuilder` to configure a new `Service`.
52  ///
53  /// This is the same as `ServiceBuilder::new()`.
54  pub fn builder(properties: Object) -> ServiceBuilder {
55    ServiceBuilder::new(properties)
56  }
57
58  /// Returns a new `Service` based on the `ServiceBuilder` configuration.
59  pub fn from_builder(builder: ServiceBuilder) -> Result<Self> {
60    let id: DIDUrl = builder.id.ok_or(Error::InvalidService("missing id"))?;
61    if id.fragment().unwrap_or_default().is_empty() {
62      return Err(Error::InvalidService("empty id fragment"));
63    }
64
65    Ok(Self {
66      id,
67      type_: OneOrSet::try_from(builder.type_).map_err(|_| Error::InvalidService("invalid type"))?,
68      service_endpoint: builder
69        .service_endpoint
70        .ok_or(Error::InvalidService("missing endpoint"))?,
71      properties: builder.properties,
72    })
73  }
74
75  /// Returns a reference to the `Service` id.
76  pub fn id(&self) -> &DIDUrl {
77    &self.id
78  }
79
80  /// Sets the `Service`'s id.
81  ///
82  /// # Errors
83  ///
84  /// [`Error::MissingIdFragment`] if there is no fragment on the [`DIDUrl`].
85  pub fn set_id(&mut self, id: DIDUrl) -> Result<()> {
86    if id.fragment().unwrap_or_default().is_empty() {
87      return Err(Error::MissingIdFragment);
88    }
89    self.id = id;
90    Ok(())
91  }
92
93  /// Returns a reference to the `Service` type.
94  pub fn type_(&self) -> &OneOrSet<String> {
95    &self.type_
96  }
97
98  /// Returns a mutable reference to the `Service` type.
99  pub fn type_mut(&mut self) -> &mut OneOrSet<String> {
100    &mut self.type_
101  }
102
103  /// Returns a reference to the `Service` endpoint.
104  pub fn service_endpoint(&self) -> &ServiceEndpoint {
105    &self.service_endpoint
106  }
107
108  /// Returns a mutable reference to the `Service` endpoint.
109  pub fn service_endpoint_mut(&mut self) -> &mut ServiceEndpoint {
110    &mut self.service_endpoint
111  }
112
113  /// Returns a reference to the custom `Service` properties.
114  pub fn properties(&self) -> &Object {
115    &self.properties
116  }
117
118  /// Returns a mutable reference to the custom `Service` properties.
119  pub fn properties_mut(&mut self) -> &mut Object {
120    &mut self.properties
121  }
122
123  /// Maps `Service` by applying a function `f` to
124  /// the id. This is useful when working with DID methods
125  /// where the DID is not known prior to publishing.  
126  pub fn map<F>(self, f: F) -> Service
127  where
128    F: FnMut(CoreDID) -> CoreDID,
129  {
130    Service {
131      id: self.id.map(f),
132      type_: self.type_,
133      service_endpoint: self.service_endpoint,
134      properties: self.properties,
135    }
136  }
137
138  /// Fallible version of [`Service::map`].
139  pub fn try_map<F, E>(self, f: F) -> Result<Service, E>
140  where
141    F: FnMut(CoreDID) -> Result<CoreDID, E>,
142  {
143    Ok(Service {
144      id: self.id.try_map(f)?,
145      type_: self.type_,
146      service_endpoint: self.service_endpoint,
147      properties: self.properties,
148    })
149  }
150}
151
152impl AsRef<DIDUrl> for Service {
153  fn as_ref(&self) -> &DIDUrl {
154    self.id()
155  }
156}
157
158impl Display for Service {
159  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
160    self.fmt_json(f)
161  }
162}
163
164impl KeyComparable for Service {
165  type Key = DIDUrl;
166
167  #[inline]
168  fn key(&self) -> &Self::Key {
169    self.as_ref()
170  }
171}
172
173#[cfg(test)]
174mod tests {
175  use super::*;
176  use identity_core::common::OrderedSet;
177  use identity_core::common::Url;
178  use identity_core::convert::FromJson;
179  use identity_core::convert::ToJson;
180
181  #[test]
182  fn test_service_types_serde() {
183    // Single type.
184    let service1: Service = Service::builder(Object::new())
185      .id(DIDUrl::parse("did:example:123#service").unwrap())
186      .type_("LinkedDomains")
187      .service_endpoint(Url::parse("https://iota.org/").unwrap())
188      .build()
189      .unwrap();
190    assert_eq!(service1.type_, OneOrSet::new_one("LinkedDomains".to_owned()));
191    assert!(service1.type_.contains("LinkedDomains"));
192    assert!(!service1.type_.contains("NotEntry"));
193    assert!(!service1.type_.contains(""));
194
195    let expected1 = r#"{"id":"did:example:123#service","type":"LinkedDomains","serviceEndpoint":"https://iota.org/"}"#;
196    assert_eq!(service1.to_json().unwrap(), expected1);
197    assert_eq!(Service::from_json(expected1).unwrap(), service1);
198
199    // Set of types.
200    let service2: Service = Service::builder(Object::new())
201      .id(DIDUrl::parse("did:example:123#service").unwrap())
202      .types(["LinkedDomains".to_owned(), "OtherService2022".to_owned()])
203      .service_endpoint(Url::parse("https://iota.org/").unwrap())
204      .build()
205      .unwrap();
206    assert_eq!(
207      service2.type_,
208      OneOrSet::try_from(vec!["LinkedDomains".to_owned(), "OtherService2022".to_owned()]).unwrap()
209    );
210    assert!(service2.type_.contains("LinkedDomains"));
211    assert!(service2.type_.contains("OtherService2022"));
212    assert!(!service2.type_.contains("NotEntry"));
213    assert!(!service2.type_.contains(""));
214
215    let expected2 = r#"{"id":"did:example:123#service","type":["LinkedDomains","OtherService2022"],"serviceEndpoint":"https://iota.org/"}"#;
216    assert_eq!(service2.to_json().unwrap(), expected2);
217    assert_eq!(Service::from_json(expected2).unwrap(), service2)
218  }
219
220  #[test]
221  fn test_service_endpoint_serde() {
222    // Single endpoint.
223    {
224      let service: Service = Service::builder(Object::new())
225        .id(DIDUrl::parse("did:example:123#service").unwrap())
226        .type_("LinkedDomains")
227        .service_endpoint(Url::parse("https://iota.org/").unwrap())
228        .build()
229        .unwrap();
230      let expected = r#"{"id":"did:example:123#service","type":"LinkedDomains","serviceEndpoint":"https://iota.org/"}"#;
231      assert_eq!(service.to_json().unwrap(), expected);
232      assert_eq!(Service::from_json(expected).unwrap(), service);
233    }
234
235    // Set of endpoints.
236    {
237      let endpoint: ServiceEndpoint = ServiceEndpoint::Set(
238        OrderedSet::try_from(vec![
239          Url::parse("https://iota.org/").unwrap(),
240          Url::parse("wss://www.example.com/socketserver/").unwrap(),
241          Url::parse("did:abc:123#service").unwrap(),
242        ])
243        .unwrap(),
244      );
245      let service: Service = Service::builder(Object::new())
246        .id(DIDUrl::parse("did:example:123#service").unwrap())
247        .type_("LinkedDomains")
248        .service_endpoint(endpoint)
249        .build()
250        .unwrap();
251      let expected = r#"{"id":"did:example:123#service","type":"LinkedDomains","serviceEndpoint":["https://iota.org/","wss://www.example.com/socketserver/","did:abc:123#service"]}"#;
252      assert_eq!(service.to_json().unwrap(), expected);
253      assert_eq!(Service::from_json(expected).unwrap(), service)
254    }
255  }
256
257  #[test]
258  fn test_service_deserialize_empty_fragment_fails() {
259    let empty_id_fragment: &str =
260      r#"{"id":"did:example:123","type":"LinkedDomains","serviceEndpoint":"https://iota.org/"}"#;
261    let result: Result<Service, identity_core::Error> = Service::from_json(empty_id_fragment);
262    assert!(result.is_err());
263  }
264}