identity_credential/domain_linkage/
domain_linkage_credential_builder.rs1use crate::credential::Credential;
5use crate::credential::Issuer;
6use crate::credential::Subject;
7use crate::domain_linkage::DomainLinkageConfiguration;
8use crate::error::Result;
9use crate::Error;
10use identity_core::common::Object;
11use identity_core::common::OneOrMany;
12use identity_core::common::Timestamp;
13use identity_core::common::Url;
14use identity_did::CoreDID;
15use identity_did::DID;
16
17use crate::utils::url_only_includes_origin;
18
19#[derive(Debug, Default)]
26pub struct DomainLinkageCredentialBuilder {
27 pub(crate) issuer: Option<Url>,
28 pub(crate) issuance_date: Option<Timestamp>,
29 pub(crate) expiration_date: Option<Timestamp>,
30 pub(crate) origin: Option<Url>,
31}
32
33impl DomainLinkageCredentialBuilder {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
43 pub fn issuer(mut self, did: CoreDID) -> Self {
44 self.issuer = Some(did.into_url().into());
45 self
46 }
47
48 #[must_use]
50 pub fn issuance_date(mut self, value: Timestamp) -> Self {
51 self.issuance_date = Some(value);
52 self
53 }
54
55 #[must_use]
57 pub fn expiration_date(mut self, value: Timestamp) -> Self {
58 self.expiration_date = Some(value);
59 self
60 }
61
62 #[must_use]
66 pub fn origin(mut self, value: Url) -> Self {
67 self.origin = Some(value);
68 self
69 }
70
71 pub fn build(self) -> Result<Credential<Object>> {
73 let origin: Url = self.origin.ok_or(Error::MissingOrigin)?;
74 if origin.domain().is_none() {
75 return Err(Error::DomainLinkageError(
76 "origin must be a domain with http(s) scheme".into(),
77 ));
78 }
79 if !url_only_includes_origin(&origin) {
80 return Err(Error::DomainLinkageError(
81 "origin must not contain any path, query or fragment".into(),
82 ));
83 }
84
85 let mut properties: Object = Object::new();
86 properties.insert("origin".into(), origin.into_string().into());
87 let issuer: Url = self.issuer.ok_or(Error::MissingIssuer)?;
88
89 Ok(Credential {
90 context: OneOrMany::Many(vec![
91 Credential::<Object>::base_context().clone(),
92 DomainLinkageConfiguration::well_known_context().clone(),
93 ]),
94 id: None,
95 types: OneOrMany::Many(vec![
96 Credential::<Object>::base_type().to_owned(),
97 DomainLinkageConfiguration::domain_linkage_type().to_owned(),
98 ]),
99 credential_subject: OneOrMany::One(Subject::with_id_and_properties(issuer.clone(), properties)),
100 issuer: Issuer::Url(issuer),
101 issuance_date: self.issuance_date.unwrap_or_else(Timestamp::now_utc),
102 expiration_date: Some(self.expiration_date.ok_or(Error::MissingExpirationDate)?),
103 credential_status: None,
104 credential_schema: Vec::new().into(),
105 refresh_service: Vec::new().into(),
106 terms_of_use: Vec::new().into(),
107 evidence: Vec::new().into(),
108 non_transferable: None,
109 properties: Object::new(),
110 proof: None,
111 })
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use crate::credential::Credential;
118 use crate::domain_linkage::DomainLinkageCredentialBuilder;
119 use crate::error::Result;
120 use crate::Error;
121 use identity_core::common::Timestamp;
122 use identity_core::common::Url;
123 use identity_did::CoreDID;
124
125 #[test]
126 fn test_builder_with_all_fields_set_succeeds() {
127 let issuer: CoreDID = "did:example:issuer".parse().unwrap();
128 assert!(DomainLinkageCredentialBuilder::new()
129 .issuance_date(Timestamp::now_utc())
130 .expiration_date(Timestamp::now_utc())
131 .issuer(issuer)
132 .origin(Url::parse("http://www.example.com").unwrap())
133 .build()
134 .is_ok());
135 }
136
137 #[test]
138 fn test_builder_origin_is_not_a_domain() {
139 let issuer: CoreDID = "did:example:issuer".parse().unwrap();
140 let err: Error = DomainLinkageCredentialBuilder::new()
141 .issuance_date(Timestamp::now_utc())
142 .expiration_date(Timestamp::now_utc())
143 .issuer(issuer)
144 .origin(Url::parse("did:example:origin").unwrap())
145 .build()
146 .unwrap_err();
147 assert!(matches!(err, Error::DomainLinkageError(_)));
148 }
149 #[test]
150 fn test_builder_origin_is_a_url() {
151 let urls = [
152 "https://example.com/foo?bar=420#baz",
153 "https://example.com/?bar=420",
154 "https://example.com/#baz",
155 "https://example.com/?bar=420#baz",
156 ];
157 let issuer: CoreDID = "did:example:issuer".parse().unwrap();
158 for url in urls {
159 let err: Error = DomainLinkageCredentialBuilder::new()
160 .issuance_date(Timestamp::now_utc())
161 .expiration_date(Timestamp::now_utc())
162 .issuer(issuer.clone())
163 .origin(Url::parse(url).unwrap())
164 .build()
165 .unwrap_err();
166 assert!(matches!(err, Error::DomainLinkageError(_)));
167 }
168 }
169
170 #[test]
171 fn test_builder_no_issuer() {
172 let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
173 .issuance_date(Timestamp::now_utc())
174 .expiration_date(Timestamp::now_utc())
175 .origin(Url::parse("http://www.example.com").unwrap())
176 .build();
177
178 assert!(matches!(credential, Err(Error::MissingIssuer)));
179 }
180
181 #[test]
182 fn test_builder_no_origin() {
183 let issuer: CoreDID = "did:example:issuer".parse().unwrap();
184 let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
185 .issuance_date(Timestamp::now_utc())
186 .expiration_date(Timestamp::now_utc())
187 .issuer(issuer)
188 .build();
189
190 assert!(matches!(credential, Err(Error::MissingOrigin)));
191 }
192
193 #[test]
194 fn test_builder_no_expiration_date() {
195 let issuer: CoreDID = "did:example:issuer".parse().unwrap();
196 let credential: Result<Credential> = DomainLinkageCredentialBuilder::new()
197 .issuance_date(Timestamp::now_utc())
198 .issuer(issuer)
199 .origin(Url::parse("http://www.example.com").unwrap())
200 .build();
201
202 assert!(matches!(credential, Err(Error::MissingExpirationDate)));
203 }
204}