1use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::HashMap;
10
11#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
14#[serde(rename_all = "camelCase")]
15pub struct Service {
16 pub id: String,
18 pub r#type: String,
20 pub service_endpoint: String,
22
23 #[serde(flatten)]
25 pub extra: HashMap<String, Value>,
26}
27
28#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
31#[serde(tag = "type")]
32pub enum VerificationMethod {
33 Multikey {
35 id: String,
37 controller: String,
39
40 #[serde(rename = "publicKeyMultibase")]
42 public_key_multibase: String,
43
44 #[serde(flatten)]
46 extra: HashMap<String, Value>,
47 },
48
49 #[serde(untagged)]
51 Other {
52 #[serde(flatten)]
54 extra: HashMap<String, Value>,
55 },
56}
57
58#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
61#[serde(rename_all = "camelCase")]
62pub struct Document {
63 pub id: String,
65 pub also_known_as: Vec<String>,
67 pub service: Vec<Service>,
69
70 #[serde(alias = "verificationMethod")]
72 pub verification_method: Vec<VerificationMethod>,
73
74 #[serde(flatten)]
76 pub extra: HashMap<String, Value>,
77}
78
79impl Document {
80 pub fn pds_endpoints(&self) -> Vec<&str> {
83 self.service
84 .iter()
85 .filter_map(|service| {
86 if service.r#type == "AtprotoPersonalDataServer" {
87 Some(service.service_endpoint.as_str())
88 } else {
89 None
90 }
91 })
92 .collect()
93 }
94
95 pub fn handles(&self) -> Option<&str> {
98 self.also_known_as.first().map(|handle| {
99 if let Some(trimmed) = handle.strip_prefix("at://") {
100 trimmed
101 } else {
102 handle.as_str()
103 }
104 })
105 }
106
107 pub fn did_keys(&self) -> Vec<&str> {
110 self.verification_method
111 .iter()
112 .filter_map(|verification_method| match verification_method {
113 VerificationMethod::Multikey {
114 public_key_multibase,
115 ..
116 } => Some(public_key_multibase.as_str()),
117 VerificationMethod::Other { extra: _ } => None,
118 })
119 .collect()
120 }
121}
122
123#[derive(Clone, Deserialize, Serialize, Debug)]
126pub struct Handle {
127 pub did: String,
129 pub handle: String,
131 pub pds: String,
133 pub verification_methods: Vec<String>,
135}
136
137#[cfg(test)]
138mod tests {
139 use crate::model::Document;
140
141 #[test]
142 fn test_deserialize() {
143 let document = serde_json::from_str::<Document>(
144 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"}]}"##,
145 );
146 assert!(document.is_ok());
147
148 let document = document.unwrap();
149 assert_eq!(document.id, "did:plc:cbkjy5n7bk3ax2wplmtjofq2");
150 }
151
152 #[test]
153 fn test_deserialize_unsupported_verification_method() {
154 let documents = vec![
155 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"}]}"##,
156 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"}]}"##,
157 ];
158 for document in documents {
159 let document = serde_json::from_str::<Document>(document);
160 assert!(document.is_ok());
161
162 let document = document.unwrap();
163 assert_eq!(document.id, "did:plc:cbkjy5n7bk3ax2wplmtjofq2");
164 }
165 }
166}