identity_document/service/
service.rs1use 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#[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
38fn 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 pub fn builder(properties: Object) -> ServiceBuilder {
55 ServiceBuilder::new(properties)
56 }
57
58 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 pub fn id(&self) -> &DIDUrl {
77 &self.id
78 }
79
80 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 pub fn type_(&self) -> &OneOrSet<String> {
95 &self.type_
96 }
97
98 pub fn type_mut(&mut self) -> &mut OneOrSet<String> {
100 &mut self.type_
101 }
102
103 pub fn service_endpoint(&self) -> &ServiceEndpoint {
105 &self.service_endpoint
106 }
107
108 pub fn service_endpoint_mut(&mut self) -> &mut ServiceEndpoint {
110 &mut self.service_endpoint
111 }
112
113 pub fn properties(&self) -> &Object {
115 &self.properties
116 }
117
118 pub fn properties_mut(&mut self) -> &mut Object {
120 &mut self.properties
121 }
122
123 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 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 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 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 {
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 {
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}