use crate::deps::fluent_uri::Uri;
use crate::types::crypto::{CryptoError, PublicKey};
use crate::types::string::{Did, Handle};
use crate::types::value::Data;
use crate::{CowStr, IntoStatic};
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use bon::Builder;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[builder(start_fn = new)]
#[serde(rename_all = "camelCase")]
pub struct DidDocument<'a> {
#[serde(rename = "@context")]
#[serde(default = "default_context")]
pub context: Vec<CowStr<'a>>,
#[serde(borrow)]
pub id: Did<'a>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub also_known_as: Option<Vec<CowStr<'a>>>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_method: Option<Vec<VerificationMethod<'a>>>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<Vec<Service<'a>>>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<'a>>,
}
pub fn default_context() -> Vec<CowStr<'static>> {
vec![
CowStr::new_static("https://www.w3.org/ns/did/v1"),
CowStr::new_static("https://w3id.org/security/multikey/v1"),
CowStr::new_static("https://w3id.org/security/suites/secp256k1-2019/v1"),
]
}
impl crate::IntoStatic for DidDocument<'_> {
type Output = DidDocument<'static>;
fn into_static(self) -> Self::Output {
DidDocument {
context: default_context(),
id: self.id.into_static(),
also_known_as: self.also_known_as.into_static(),
verification_method: self.verification_method.into_static(),
service: self.service.into_static(),
extra_data: self.extra_data.into_static(),
}
}
}
impl<'a> DidDocument<'a> {
pub fn handles(&self) -> Vec<Handle<'static>> {
self.also_known_as
.as_ref()
.map(|v| {
v.iter()
.filter_map(|h| Handle::new(h).ok())
.map(|h| h.into_static())
.collect()
})
.unwrap_or_default()
}
pub fn atproto_multikey(&self) -> Option<CowStr<'static>> {
self.verification_method.as_ref().and_then(|methods| {
methods.iter().find_map(|m| {
if m.r#type.as_ref() == "Multikey" {
m.public_key_multibase
.as_ref()
.map(|k| k.clone().into_static())
} else {
None
}
})
})
}
pub fn pds_endpoint(&self) -> Option<Uri<String>> {
self.service.as_ref().and_then(|services| {
services.iter().find_map(|s| {
if s.r#type.as_ref() == "AtprotoPersonalDataServer" {
match &s.service_endpoint {
Some(Data::String(strv)) => {
Uri::parse(strv.as_ref()).ok().map(|u| u.to_owned())
}
Some(Data::Object(obj)) => {
if let Some(Data::String(urlv)) = obj.0.get("url") {
Uri::parse(urlv.as_ref()).ok().map(|u| u.to_owned())
} else {
None
}
}
_ => None,
}
} else {
None
}
})
})
}
pub fn atproto_public_key(&self) -> Result<Option<PublicKey<'static>>, CryptoError> {
if let Some(multibase) = self.atproto_multikey() {
let pk = PublicKey::decode(&multibase)?;
Ok(Some(pk))
} else {
Ok(None)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[builder(start_fn = new)]
#[serde(rename_all = "camelCase")]
pub struct VerificationMethod<'a> {
#[serde(borrow)]
pub id: CowStr<'a>,
#[serde(borrow, rename = "type")]
pub r#type: CowStr<'a>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<CowStr<'a>>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_multibase: Option<CowStr<'a>>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<'a>>,
}
impl crate::IntoStatic for VerificationMethod<'_> {
type Output = VerificationMethod<'static>;
fn into_static(self) -> Self::Output {
VerificationMethod {
id: self.id.into_static(),
r#type: self.r#type.into_static(),
controller: self.controller.into_static(),
public_key_multibase: self.public_key_multibase.into_static(),
extra_data: self.extra_data.into_static(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[builder(start_fn = new)]
#[serde(rename_all = "camelCase")]
pub struct Service<'a> {
#[serde(borrow)]
pub id: CowStr<'a>,
#[serde(borrow, rename = "type")]
pub r#type: CowStr<'a>,
#[serde(borrow)]
#[serde(skip_serializing_if = "Option::is_none")]
pub service_endpoint: Option<Data<'a>>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<'a>>,
}
impl crate::IntoStatic for Service<'_> {
type Output = Service<'static>;
fn into_static(self) -> Self::Output {
Service {
id: self.id.into_static(),
r#type: self.r#type.into_static(),
service_endpoint: self.service_endpoint.into_static(),
extra_data: self.extra_data.into_static(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::String;
use serde_json::json;
fn encode_uvarint(mut x: u64) -> Vec<u8> {
let mut out = Vec::new();
while x >= 0x80 {
out.push(((x as u8) & 0x7F) | 0x80);
x >>= 7;
}
out.push(x as u8);
out
}
fn multikey(code: u64, key: &[u8]) -> String {
let mut buf = encode_uvarint(code);
buf.extend_from_slice(key);
multibase::encode(multibase::Base::Base58Btc, buf)
}
#[test]
fn public_key_decode() {
let did = "did:plc:example";
let mut k = [0u8; 32];
k[0] = 7;
let mk = multikey(0xED, &k);
let doc_json = json!({
"id": did,
"verificationMethod": [
{
"id": "#key-1",
"type": "Multikey",
"publicKeyMultibase": mk,
}
]
});
let doc_string = serde_json::to_string(&doc_json).unwrap();
let doc: DidDocument<'_> = serde_json::from_str(&doc_string).unwrap();
let pk = doc.atproto_public_key().unwrap().expect("present");
assert!(matches!(pk.codec, crate::types::crypto::KeyCodec::Ed25519));
assert_eq!(pk.bytes.as_ref(), &k);
}
#[test]
fn parse_sample_doc_and_helpers() {
let raw = include_str!("test_did_doc.json");
let doc: DidDocument<'_> = serde_json::from_str(raw).expect("parse doc");
assert_eq!(doc.id.as_str(), "did:plc:yfvwmnlztr4dwkb7hwz55r2g");
let pds = doc.pds_endpoint().expect("pds endpoint");
assert_eq!(pds.as_str(), "https://atproto.systems");
let handles = doc.handles();
assert!(handles.iter().any(|h| h.as_str() == "nonbinary.computer"));
let mk = doc.atproto_multikey().expect("has multikey");
assert!(mk.as_ref().starts_with('z'));
let _ = doc.atproto_public_key().expect("decode ok");
}
}