1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[cfg_attr(debug_assertions, derive(Debug))]
14#[derive(Clone, Serialize, Deserialize, PartialEq)]
15#[serde(rename_all = "camelCase")]
16pub struct Service {
17 pub id: String,
19 pub r#type: String,
21 pub service_endpoint: String,
23
24 #[serde(flatten)]
26 pub extra: HashMap<String, Value>,
27}
28
29#[cfg_attr(debug_assertions, derive(Debug))]
32#[derive(Clone, Serialize, Deserialize, PartialEq)]
33#[serde(tag = "type")]
34pub enum VerificationMethod {
35 Multikey {
37 id: String,
39 controller: String,
41
42 #[serde(rename = "publicKeyMultibase")]
44 public_key_multibase: String,
45
46 #[serde(flatten)]
48 extra: HashMap<String, Value>,
49 },
50
51 #[serde(untagged)]
53 Other {
54 #[serde(flatten)]
56 extra: HashMap<String, Value>,
57 },
58}
59
60#[cfg_attr(debug_assertions, derive(Debug))]
63#[derive(Clone, Serialize, Deserialize, PartialEq)]
64#[serde(rename_all = "camelCase")]
65pub struct Document {
66 pub id: String,
68 pub also_known_as: Vec<String>,
70 pub service: Vec<Service>,
72
73 #[serde(alias = "verificationMethod")]
75 pub verification_method: Vec<VerificationMethod>,
76
77 #[serde(flatten)]
79 pub extra: HashMap<String, Value>,
80}
81
82impl Document {
83 pub fn pds_endpoints(&self) -> Vec<&str> {
86 self.service
87 .iter()
88 .filter_map(|service| {
89 if service.r#type == "AtprotoPersonalDataServer" {
90 Some(service.service_endpoint.as_str())
91 } else {
92 None
93 }
94 })
95 .collect()
96 }
97
98 pub fn handles(&self) -> Option<&str> {
101 self.also_known_as.first().map(|handle| {
102 if let Some(trimmed) = handle.strip_prefix("at://") {
103 trimmed
104 } else {
105 handle.as_str()
106 }
107 })
108 }
109
110 pub fn did_keys(&self) -> Vec<&str> {
113 self.verification_method
114 .iter()
115 .filter_map(|verification_method| match verification_method {
116 VerificationMethod::Multikey {
117 public_key_multibase,
118 ..
119 } => Some(public_key_multibase.as_str()),
120 VerificationMethod::Other { extra: _ } => None,
121 })
122 .collect()
123 }
124}
125
126#[cfg_attr(debug_assertions, derive(Debug))]
129#[derive(Clone, Deserialize, Serialize)]
130pub struct Handle {
131 pub did: String,
133 pub handle: String,
135 pub pds: String,
137 pub verification_methods: Vec<String>,
139}
140
141#[cfg(test)]
142mod tests {
143 use crate::model::Document;
144
145 #[test]
146 fn test_deserialize() {
147 let document = serde_json::from_str::<Document>(
148 r##"{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1","https://w3id.org/security/suites/secp256k1-2019/v1"],"id":"did:plc:cbkjy5n7bk3ax2wplmtjofq2","alsoKnownAs":["at://ngerakines.me","at://nick.gerakines.net","at://nick.thegem.city","https://github.com/ngerakines","https://ngerakines.me/","dns:ngerakines.me"],"verificationMethod":[{"id":"did:plc:cbkjy5n7bk3ax2wplmtjofq2#atproto","type":"Multikey","controller":"did:plc:cbkjy5n7bk3ax2wplmtjofq2","publicKeyMultibase":"zQ3shXvCK2RyPrSLYQjBEw5CExZkUhJH3n1K2Mb9sC7JbvRMF"}],"service":[{"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://pds.cauda.cloud"}]}"##,
149 );
150 assert!(document.is_ok());
151
152 let document = document.unwrap();
153 assert_eq!(document.id, "did:plc:cbkjy5n7bk3ax2wplmtjofq2");
154 }
155
156 #[test]
157 fn test_deserialize_unsupported_verification_method() {
158 let documents = vec![
159 r##"{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1","https://w3id.org/security/suites/secp256k1-2019/v1"],"id":"did:plc:cbkjy5n7bk3ax2wplmtjofq2","alsoKnownAs":["at://ngerakines.me","at://nick.gerakines.net","at://nick.thegem.city","https://github.com/ngerakines","https://ngerakines.me/","dns:ngerakines.me"],"verificationMethod":[{"id":"did:plc:cbkjy5n7bk3ax2wplmtjofq2#atproto","type":"Ed25519VerificationKey2020","controller":"did:plc:cbkjy5n7bk3ax2wplmtjofq2","publicKeyMultibase":"zQ3shXvCK2RyPrSLYQjBEw5CExZkUhJH3n1K2Mb9sC7JbvRMF"}],"service":[{"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://pds.cauda.cloud"}]}"##,
160 r##"{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1","https://w3id.org/security/suites/secp256k1-2019/v1"],"id":"did:plc:cbkjy5n7bk3ax2wplmtjofq2","alsoKnownAs":["at://ngerakines.me","at://nick.gerakines.net","at://nick.thegem.city","https://github.com/ngerakines","https://ngerakines.me/","dns:ngerakines.me"],"verificationMethod":[{"id": "did:example:123#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A","type": "JsonWebKey2020","controller": "did:example:123","publicKeyJwk": {"crv": "Ed25519","x": "VCpo2LMLhn6iWku8MKvSLg2ZAoC-nlOyPVQaO3FxVeQ","kty": "OKP","kid": "_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A"}}],"service":[{"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://pds.cauda.cloud"}]}"##,
161 ];
162 for document in documents {
163 let document = serde_json::from_str::<Document>(document);
164 assert!(document.is_ok());
165
166 let document = document.unwrap();
167 assert_eq!(document.id, "did:plc:cbkjy5n7bk3ax2wplmtjofq2");
168 }
169 }
170}