identity_document/service/
service_endpoint.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 indexmap::map::IndexMap;
8use serde::Serialize;
9
10use identity_core::common::OrderedSet;
11use identity_core::common::Url;
12use identity_core::convert::FmtJson;
13
14/// A single URL, set, or map of endpoints specified in a [`Service`](crate::service::Service).
15///
16/// [Specification](https://www.w3.org/TR/did-core/#dfn-serviceendpoint)
17#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
18#[serde(untagged)]
19pub enum ServiceEndpoint {
20  One(Url),
21  Set(OrderedSet<Url>),
22  Map(IndexMap<String, OrderedSet<Url>>),
23}
24
25impl From<Url> for ServiceEndpoint {
26  fn from(url: Url) -> Self {
27    ServiceEndpoint::One(url)
28  }
29}
30
31impl From<OrderedSet<Url>> for ServiceEndpoint {
32  fn from(set: OrderedSet<Url>) -> Self {
33    ServiceEndpoint::Set(set)
34  }
35}
36
37impl From<IndexMap<String, OrderedSet<Url>>> for ServiceEndpoint {
38  fn from(map: IndexMap<String, OrderedSet<Url>>) -> Self {
39    ServiceEndpoint::Map(map)
40  }
41}
42
43impl Display for ServiceEndpoint {
44  fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
45    self.fmt_json(f)
46  }
47}
48
49#[cfg(test)]
50mod tests {
51  use identity_core::convert::FromJson;
52  use identity_core::convert::ToJson;
53
54  use super::*;
55
56  #[test]
57  fn test_service_endpoint_one() {
58    let url1 = Url::parse("https://iota.org/").unwrap();
59    let url2 = Url::parse("wss://www.example.com/socketserver/").unwrap();
60    let url3 = Url::parse("did:abc:123#service").unwrap();
61
62    // VALID: One.
63    let endpoint1: ServiceEndpoint = ServiceEndpoint::One(url1);
64    let ser_endpoint1: String = endpoint1.to_json().unwrap();
65    assert_eq!(ser_endpoint1, "\"https://iota.org/\"");
66    assert_eq!(endpoint1, ServiceEndpoint::from_json(&ser_endpoint1).unwrap());
67
68    let endpoint2: ServiceEndpoint = ServiceEndpoint::One(url2);
69    let ser_endpoint2: String = endpoint2.to_json().unwrap();
70    assert_eq!(ser_endpoint2, "\"wss://www.example.com/socketserver/\"");
71    assert_eq!(endpoint2, ServiceEndpoint::from_json(&ser_endpoint2).unwrap());
72
73    let endpoint3: ServiceEndpoint = ServiceEndpoint::One(url3);
74    let ser_endpoint3: String = endpoint3.to_json().unwrap();
75    assert_eq!(ser_endpoint3, "\"did:abc:123#service\"");
76    assert_eq!(endpoint3, ServiceEndpoint::from_json(&ser_endpoint3).unwrap());
77  }
78
79  #[test]
80  fn test_service_endpoint_set() {
81    let url1 = Url::parse("https://iota.org/").unwrap();
82    let url2 = Url::parse("wss://www.example.com/socketserver/").unwrap();
83    let url3 = Url::parse("did:abc:123#service").unwrap();
84
85    // VALID: Set.
86    let mut set: OrderedSet<Url> = OrderedSet::new();
87    // One element.
88    assert!(set.append(url1.clone()));
89    let endpoint_set: ServiceEndpoint = ServiceEndpoint::Set(set.clone());
90    let ser_endpoint_set: String = endpoint_set.to_json().unwrap();
91    assert_eq!(ser_endpoint_set, "[\"https://iota.org/\"]");
92    assert_eq!(endpoint_set, ServiceEndpoint::from_json(&ser_endpoint_set).unwrap());
93    // Two elements.
94    assert!(set.append(url2.clone()));
95    let endpoint_set: ServiceEndpoint = ServiceEndpoint::Set(set.clone());
96    let ser_endpoint_set: String = endpoint_set.to_json().unwrap();
97    assert_eq!(
98      ser_endpoint_set,
99      "[\"https://iota.org/\",\"wss://www.example.com/socketserver/\"]"
100    );
101    assert_eq!(endpoint_set, ServiceEndpoint::from_json(&ser_endpoint_set).unwrap());
102    // Three elements.
103    assert!(set.append(url3.clone()));
104    let endpoint_set: ServiceEndpoint = ServiceEndpoint::Set(set.clone());
105    let ser_endpoint_set: String = endpoint_set.to_json().unwrap();
106    assert_eq!(
107      ser_endpoint_set,
108      "[\"https://iota.org/\",\"wss://www.example.com/socketserver/\",\"did:abc:123#service\"]"
109    );
110    assert_eq!(endpoint_set, ServiceEndpoint::from_json(&ser_endpoint_set).unwrap());
111
112    // VALID: Set ignores duplicates.
113    let mut duplicates_set: OrderedSet<Url> = OrderedSet::new();
114    duplicates_set.append(url1.clone());
115    duplicates_set.append(url1.clone());
116    assert_eq!(
117      ServiceEndpoint::Set(duplicates_set.clone()).to_json().unwrap(),
118      "[\"https://iota.org/\"]"
119    );
120    duplicates_set.append(url2.clone());
121    duplicates_set.append(url2.clone());
122    duplicates_set.append(url1.clone());
123    assert_eq!(
124      ServiceEndpoint::Set(duplicates_set.clone()).to_json().unwrap(),
125      "[\"https://iota.org/\",\"wss://www.example.com/socketserver/\"]"
126    );
127    assert!(duplicates_set.append(url3.clone()));
128    duplicates_set.append(url3);
129    duplicates_set.append(url1);
130    duplicates_set.append(url2);
131    assert_eq!(
132      ServiceEndpoint::Set(duplicates_set.clone()).to_json().unwrap(),
133      "[\"https://iota.org/\",\"wss://www.example.com/socketserver/\",\"did:abc:123#service\"]"
134    );
135  }
136
137  #[test]
138  fn test_service_endpoint_map() {
139    let url1 = Url::parse("https://iota.org/").unwrap();
140    let url2 = Url::parse("wss://www.example.com/socketserver/").unwrap();
141    let url3 = Url::parse("did:abc:123#service").unwrap();
142    let url4 = Url::parse("did:xyz:789#link").unwrap();
143
144    // VALID: Map.
145    let mut map: IndexMap<String, OrderedSet<Url>> = IndexMap::new();
146    // One entry.
147    assert!(map
148      .insert("key".to_owned(), OrderedSet::try_from(vec![url1]).unwrap())
149      .is_none());
150    let endpoint_map: ServiceEndpoint = ServiceEndpoint::Map(map.clone());
151    let ser_endpoint_map: String = endpoint_map.to_json().unwrap();
152    assert_eq!(ser_endpoint_map, r#"{"key":["https://iota.org/"]}"#);
153    assert_eq!(endpoint_map, ServiceEndpoint::from_json(&ser_endpoint_map).unwrap());
154    // Two entries.
155    assert!(map
156      .insert("apple".to_owned(), OrderedSet::try_from(vec![url2]).unwrap())
157      .is_none());
158    let endpoint_map: ServiceEndpoint = ServiceEndpoint::Map(map.clone());
159    let ser_endpoint_map: String = endpoint_map.to_json().unwrap();
160    assert_eq!(
161      ser_endpoint_map,
162      r#"{"key":["https://iota.org/"],"apple":["wss://www.example.com/socketserver/"]}"#
163    );
164    assert_eq!(endpoint_map, ServiceEndpoint::from_json(&ser_endpoint_map).unwrap());
165    // Three entries.
166    assert!(map
167      .insert("example".to_owned(), OrderedSet::try_from(vec![url3]).unwrap())
168      .is_none());
169    let endpoint_map: ServiceEndpoint = ServiceEndpoint::Map(map.clone());
170    let ser_endpoint_map: String = endpoint_map.to_json().unwrap();
171    assert_eq!(
172      ser_endpoint_map,
173      r#"{"key":["https://iota.org/"],"apple":["wss://www.example.com/socketserver/"],"example":["did:abc:123#service"]}"#
174    );
175    assert_eq!(endpoint_map, ServiceEndpoint::from_json(&ser_endpoint_map).unwrap());
176
177    // Ensure insertion order is maintained.
178    // Remove first entry and add a new one.
179    map.shift_remove("key"); // N.B: only shift_remove retains order for IndexMap
180    assert!(map
181      .insert("bee".to_owned(), OrderedSet::try_from(vec![url4]).unwrap())
182      .is_none());
183    let endpoint_map: ServiceEndpoint = ServiceEndpoint::Map(map.clone());
184    let ser_endpoint_map: String = endpoint_map.to_json().unwrap();
185    assert_eq!(
186      ser_endpoint_map,
187      r#"{"apple":["wss://www.example.com/socketserver/"],"example":["did:abc:123#service"],"bee":["did:xyz:789#link"]}"#
188    );
189    assert_eq!(endpoint_map, ServiceEndpoint::from_json(&ser_endpoint_map).unwrap());
190  }
191
192  #[test]
193  fn test_service_endpoint_serde_fails() {
194    // INVALID: empty
195    assert!(ServiceEndpoint::from_json("").is_err());
196    assert!(ServiceEndpoint::from_json("\"\"").is_err());
197
198    // INVALID: spaces
199    assert!(ServiceEndpoint::from_json("\" \"").is_err());
200    assert!(ServiceEndpoint::from_json("\"\t\"").is_err());
201    assert!(ServiceEndpoint::from_json(r#""https:// iota.org/""#).is_err());
202    assert!(ServiceEndpoint::from_json(r#"["https://iota.org/","wss://www.example.com /socketserver/"]"#).is_err());
203    assert!(ServiceEndpoint::from_json(r#"{"key":["https:// iota.org/"],"apple":["wss://www.example.com/socketserver/"],"example":["did:abc:123#service"]}"#).is_err());
204
205    // INVALID: set with duplicate keys
206    assert!(ServiceEndpoint::from_json(r#"["https://iota.org/","https://iota.org/"]"#).is_err());
207    // INVALID: set with duplicate keys when normalised
208    assert!(ServiceEndpoint::from_json(r#"["https://iota.org/a/./b/../b/.","https://iota.org/a/b/"]"#).is_err());
209
210    // INVALID: map with no keys
211    assert!(ServiceEndpoint::from_json(r#"{["https://iota.org/"],["wss://www.example.com/socketserver/"]}"#).is_err());
212    assert!(
213      ServiceEndpoint::from_json(r#"{"key1":["https://iota.org/"],["wss://www.example.com/socketserver/"]}"#).is_err()
214    );
215    assert!(
216      ServiceEndpoint::from_json(r#"{["https://iota.org/"],"key2":["wss://www.example.com/socketserver/"]}"#).is_err()
217    );
218  }
219}