use crate::deps::fluent_uri::Uri;
use crate::types::crypto::{CryptoError, PublicKey};
use crate::types::string::{AtprotoStr, Did, Handle};
use crate::types::value::Data;
use crate::{Bos, DefaultStr};
use alloc::collections::BTreeMap;
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")]
#[serde(bound(
serialize = "S: Serialize + Bos<str> + AsRef<str>",
deserialize = "S: Deserialize<'de> + Bos<str> + AsRef<str>"
))]
pub struct DidDocument<S: Bos<str> + AsRef<str> = DefaultStr> {
#[serde(rename = "@context")]
#[serde(default = "default_context")]
pub context: Vec<SmolStr>,
pub id: Did<S>,
#[serde(skip_serializing_if = "Option::is_none")]
pub also_known_as: Option<Vec<S>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_method: Option<Vec<VerificationMethod<S>>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub service: Option<Vec<Service<S>>>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<S>>,
}
pub fn default_context() -> Vec<SmolStr> {
vec![
SmolStr::new_static("https://www.w3.org/ns/did/v1"),
SmolStr::new_static("https://w3id.org/security/multikey/v1"),
SmolStr::new_static("https://w3id.org/security/suites/secp256k1-2019/v1"),
]
}
impl<S> crate::IntoStatic for DidDocument<S>
where
S: Bos<str> + AsRef<str> + crate::IntoStatic,
S::Output: AsRef<str> + Bos<str>,
{
type Output = DidDocument<S::Output>;
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<S> DidDocument<S>
where
S: Bos<str> + AsRef<str> + Clone,
{
pub fn handles(&self) -> Vec<Handle<&str>> {
self.also_known_as
.as_ref()
.map(|v| {
v.iter()
.filter_map(|h| {
let s = h.as_ref().strip_prefix("at://").unwrap_or(h.as_ref());
Handle::new(s).ok()
})
.collect()
})
.unwrap_or_default()
}
pub fn atproto_multikey(&self) -> Option<S> {
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())
} else {
None
}
})
})
}
pub fn pds_endpoint(&self) -> Option<Uri<&str>> {
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(AtprotoStr::Uri(u))) => Uri::parse(u.as_ref()).ok(),
Some(Data::String(AtprotoStr::String(s))) => Uri::parse(s.as_ref()).ok(),
_ => 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.as_ref())?;
Ok(Some(pk))
} else {
Ok(None)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
#[builder(start_fn = new)]
#[serde(rename_all = "camelCase")]
#[serde(bound(
serialize = "S: Serialize + Bos<str> + AsRef<str>",
deserialize = "S: Deserialize<'de> + Bos<str> + AsRef<str>"
))]
pub struct VerificationMethod<S: Bos<str> + AsRef<str>> {
pub id: S,
#[serde(rename = "type")]
pub r#type: S,
#[serde(skip_serializing_if = "Option::is_none")]
pub controller: Option<S>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public_key_multibase: Option<S>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<S>>,
}
impl<S> crate::IntoStatic for VerificationMethod<S>
where
S: Bos<str> + AsRef<str> + crate::IntoStatic,
S::Output: AsRef<str> + Bos<str>,
{
type Output = VerificationMethod<S::Output>;
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")]
#[serde(bound(
serialize = "S: Serialize + Bos<str> + AsRef<str>",
deserialize = "S: Deserialize<'de> + Bos<str> + AsRef<str>"
))]
pub struct Service<S: Bos<str> + AsRef<str>> {
pub id: S,
#[serde(rename = "type")]
pub r#type: S,
#[serde(skip_serializing_if = "Option::is_none")]
pub service_endpoint: Option<Data<S>>,
#[serde(flatten)]
pub extra_data: BTreeMap<SmolStr, Data<S>>,
}
impl<S> crate::IntoStatic for Service<S>
where
S: Bos<str> + AsRef<str> + crate::IntoStatic,
S::Output: AsRef<str> + Bos<str>,
{
type Output = Service<S::Output>;
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 extra_data_is_preserved() {
use crate::IntoStatic;
let raw = r##"{
"id": "did:plc:example",
"customTopLevel": "top",
"verificationMethod": [{
"id": "#key-1",
"type": "Multikey",
"publicKeyMultibase": "zExample",
"customVerification": 42
}],
"service": [{
"id": "#pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://pds.example",
"customService": true
}]
}"##;
let doc: DidDocument<&str> = serde_json::from_str(raw).expect("parse doc");
assert!(matches!(
doc.extra_data.get("customTopLevel"),
Some(Data::String(AtprotoStr::String(value))) if *value == "top"
));
assert!(matches!(
doc.verification_method.as_ref().unwrap()[0]
.extra_data
.get("customVerification"),
Some(Data::Integer(42))
));
assert!(matches!(
doc.service.as_ref().unwrap()[0]
.extra_data
.get("customService"),
Some(Data::Boolean(true))
));
let doc = doc.into_static();
assert!(doc.extra_data.contains_key("customTopLevel"));
assert!(
doc.verification_method.unwrap()[0]
.extra_data
.contains_key("customVerification")
);
assert!(
doc.service.unwrap()[0]
.extra_data
.contains_key("customService")
);
}
#[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!(AsRef::<str>::as_ref(&mk).starts_with('z'));
let _ = doc.atproto_public_key().expect("decode ok");
}
}