use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::lexicon::Bytes;
use crate::lexicon::com_atproto_repo::TypedStrongRef;
use crate::typed::{LexiconType, TypedLexicon};
pub const NSID: &str = "community.lexicon.attestation.signature";
#[derive(Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(debug_assertions, derive(Debug))]
#[serde(untagged)]
pub enum SignatureOrRef {
Reference(TypedStrongRef),
Inline(TypedSignature),
}
pub type Signatures = Vec<SignatureOrRef>;
#[derive(Deserialize, Serialize, Clone, PartialEq)]
#[cfg_attr(debug_assertions, derive(Debug))]
pub struct Signature {
pub issuer: String,
pub signature: Bytes,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
impl LexiconType for Signature {
fn lexicon_type() -> &'static str {
NSID
}
fn type_required() -> bool {
false
}
}
pub type TypedSignature = TypedLexicon<Signature>;
pub fn create_typed_signature(issuer: String, signature: Bytes) -> TypedSignature {
TypedLexicon::new(Signature {
issuer,
signature,
extra: HashMap::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lexicon::com_atproto_repo::StrongRef;
use serde_json::json;
#[test]
fn test_real_signature_or_ref_deserialization() {
let json_str = r#"{
"$type": "community.lexicon.attestation.signature",
"issuedAt": "2025-08-19T20:17:17.133Z",
"issuer": "did:web:acudo-dev.smokesignal.tools",
"signature": {
"$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA"
}
}"#;
let typed_sig_result: Result<TypedSignature, _> = serde_json::from_str(json_str);
match &typed_sig_result {
Ok(sig) => {
println!("TypedSignature OK: issuer={}", sig.inner.issuer);
assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools");
}
Err(e) => {
eprintln!("TypedSignature deserialization error: {}", e);
}
}
let sig_or_ref_result: Result<SignatureOrRef, _> = serde_json::from_str(json_str);
match &sig_or_ref_result {
Ok(SignatureOrRef::Inline(sig)) => {
println!("SignatureOrRef OK (Inline): issuer={}", sig.inner.issuer);
assert_eq!(sig.inner.issuer, "did:web:acudo-dev.smokesignal.tools");
}
Ok(SignatureOrRef::Reference(_)) => {
panic!("Expected Inline signature, got Reference");
}
Err(e) => {
eprintln!("SignatureOrRef deserialization error: {}", e);
}
}
let json_no_type = r#"{
"issuedAt": "2025-08-19T20:17:17.133Z",
"issuer": "did:web:acudo-dev.smokesignal.tools",
"signature": {
"$bytes": "mr9c0MCu3g6SXNQ25JFhzfX1ecYgK9k1Kf6OZI2p2AlQRoQu09dOE7J5uaeilIx/UFCjJErO89C/uBBb9ANmUA"
}
}"#;
let no_type_result: Result<Signature, _> = serde_json::from_str(json_no_type);
match &no_type_result {
Ok(sig) => {
println!("Signature (no type) OK: issuer={}", sig.issuer);
assert_eq!(sig.issuer, "did:web:acudo-dev.smokesignal.tools");
assert_eq!(sig.signature.bytes.len(), 64);
let typed = TypedLexicon::new(sig.clone());
let _as_sig_or_ref = SignatureOrRef::Inline(typed);
println!("Successfully created SignatureOrRef from Signature");
}
Err(e) => {
eprintln!("Signature (no type) deserialization error: {}", e);
}
}
assert!(
typed_sig_result.is_ok() || sig_or_ref_result.is_ok() || no_type_result.is_ok(),
"Failed to deserialize signature in any form"
);
}
#[test]
fn test_signature_deserialization() {
let json_str = r#"{
"$type": "community.lexicon.attestation.signature",
"issuer": "did:plc:test123",
"signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="}
}"#;
let signature: Signature = serde_json::from_str(json_str).unwrap();
assert_eq!(signature.issuer, "did:plc:test123");
assert_eq!(signature.signature.bytes, b"test signature");
assert_eq!(signature.extra.len(), 1);
assert!(signature.extra.contains_key("$type"));
}
#[test]
fn test_signature_deserialization_with_extra_fields() {
let json_str = r#"{
"$type": "community.lexicon.attestation.signature",
"issuer": "did:plc:test123",
"signature": {"$bytes": "dGVzdCBzaWduYXR1cmU="},
"issuedAt": "2024-01-01T00:00:00.000Z",
"purpose": "verification"
}"#;
let signature: Signature = serde_json::from_str(json_str).unwrap();
assert_eq!(signature.issuer, "did:plc:test123");
assert_eq!(signature.signature.bytes, b"test signature");
assert_eq!(signature.extra.len(), 3);
assert!(signature.extra.contains_key("$type"));
assert_eq!(
signature.extra.get("issuedAt").unwrap(),
"2024-01-01T00:00:00.000Z"
);
assert_eq!(signature.extra.get("purpose").unwrap(), "verification");
}
#[test]
fn test_signature_serialization() {
let mut extra = HashMap::new();
extra.insert("custom_field".to_string(), json!("custom_value"));
let signature = Signature {
issuer: "did:plc:serializer".to_string(),
signature: Bytes {
bytes: b"hello world".to_vec(),
},
extra,
};
let json = serde_json::to_value(&signature).unwrap();
assert!(!json.as_object().unwrap().contains_key("$type"));
assert_eq!(json["issuer"], "did:plc:serializer");
assert_eq!(json["signature"]["$bytes"], "aGVsbG8gd29ybGQ=");
assert_eq!(json["custom_field"], "custom_value");
}
#[test]
fn test_signature_round_trip() {
let original = Signature {
issuer: "did:plc:roundtrip".to_string(),
signature: Bytes {
bytes: b"round trip test".to_vec(),
},
extra: HashMap::new(),
};
let json = serde_json::to_string(&original).unwrap();
let deserialized: Signature = serde_json::from_str(&json).unwrap();
assert_eq!(original.issuer, deserialized.issuer);
assert_eq!(original.signature.bytes, deserialized.signature.bytes);
assert_eq!(deserialized.extra.len(), 0);
}
#[test]
fn test_signature_with_complex_extra_fields() {
let mut extra = HashMap::new();
extra.insert("timestamp".to_string(), json!(1234567890));
extra.insert(
"metadata".to_string(),
json!({
"version": "1.0",
"algorithm": "ES256"
}),
);
extra.insert("tags".to_string(), json!(["tag1", "tag2", "tag3"]));
let signature = Signature {
issuer: "did:plc:complex".to_string(),
signature: Bytes {
bytes: vec![0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA],
},
extra,
};
let json = serde_json::to_value(&signature).unwrap();
assert!(!json.as_object().unwrap().contains_key("$type"));
assert_eq!(json["issuer"], "did:plc:complex");
assert_eq!(json["timestamp"], 1234567890);
assert_eq!(json["metadata"]["version"], "1.0");
assert_eq!(json["metadata"]["algorithm"], "ES256");
assert_eq!(json["tags"], json!(["tag1", "tag2", "tag3"]));
}
#[test]
fn test_empty_signature() {
let signature = Signature {
issuer: String::new(),
signature: Bytes { bytes: Vec::new() },
extra: HashMap::new(),
};
let json = serde_json::to_value(&signature).unwrap();
assert!(!json.as_object().unwrap().contains_key("$type"));
assert_eq!(json["issuer"], "");
assert_eq!(json["signature"]["$bytes"], ""); }
#[test]
fn test_signatures_vec_serialization() {
let signatures: Vec<Signature> = vec![
Signature {
issuer: "did:plc:first".to_string(),
signature: Bytes {
bytes: b"first".to_vec(),
},
extra: HashMap::new(),
},
Signature {
issuer: "did:plc:second".to_string(),
signature: Bytes {
bytes: b"second".to_vec(),
},
extra: HashMap::new(),
},
];
let json = serde_json::to_value(&signatures).unwrap();
assert!(json.is_array());
assert_eq!(json.as_array().unwrap().len(), 2);
assert_eq!(json[0]["issuer"], "did:plc:first");
assert_eq!(json[1]["issuer"], "did:plc:second");
}
#[test]
fn test_signatures_as_signature_or_ref_vec() {
let signatures: Signatures = vec![
SignatureOrRef::Inline(create_typed_signature(
"did:plc:first".to_string(),
Bytes {
bytes: b"first".to_vec(),
},
)),
SignatureOrRef::Inline(create_typed_signature(
"did:plc:second".to_string(),
Bytes {
bytes: b"second".to_vec(),
},
)),
];
let json = serde_json::to_value(&signatures).unwrap();
assert!(json.is_array());
assert_eq!(json.as_array().unwrap().len(), 2);
assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature");
assert_eq!(json[0]["issuer"], "did:plc:first");
assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature");
assert_eq!(json[1]["issuer"], "did:plc:second");
}
#[test]
fn test_typed_signature_serialization() {
let typed_sig = create_typed_signature(
"did:plc:typed".to_string(),
Bytes {
bytes: b"typed signature".to_vec(),
},
);
let json = serde_json::to_value(&typed_sig).unwrap();
assert_eq!(json["$type"], "community.lexicon.attestation.signature");
assert_eq!(json["issuer"], "did:plc:typed");
assert_eq!(json["signature"]["$bytes"], "dHlwZWQgc2lnbmF0dXJl");
}
#[test]
fn test_typed_signature_deserialization() {
let json = json!({
"$type": "community.lexicon.attestation.signature",
"issuer": "did:plc:typed",
"signature": {"$bytes": "dHlwZWQgc2lnbmF0dXJl"}
});
let typed_sig: TypedSignature = serde_json::from_value(json).unwrap();
assert_eq!(typed_sig.inner.issuer, "did:plc:typed");
assert_eq!(typed_sig.inner.signature.bytes, b"typed signature");
assert!(typed_sig.has_type_field());
assert!(typed_sig.validate().is_ok());
}
#[test]
fn test_typed_signature_without_type_field() {
let json = json!({
"issuer": "did:plc:notype",
"signature": {"$bytes": "bm8gdHlwZQ=="} });
let typed_sig: TypedSignature = serde_json::from_value(json).unwrap();
assert_eq!(typed_sig.inner.issuer, "did:plc:notype");
assert_eq!(typed_sig.inner.signature.bytes, b"no type");
assert!(!typed_sig.has_type_field());
assert!(typed_sig.validate().is_ok());
}
#[test]
fn test_typed_signature_with_extra_fields() {
let mut sig = Signature {
issuer: "did:plc:extra".to_string(),
signature: Bytes {
bytes: b"extra test".to_vec(),
},
extra: HashMap::new(),
};
sig.extra
.insert("customField".to_string(), json!("customValue"));
sig.extra.insert("timestamp".to_string(), json!(1234567890));
let typed_sig = TypedLexicon::new(sig);
let json = serde_json::to_value(&typed_sig).unwrap();
assert_eq!(json["$type"], "community.lexicon.attestation.signature");
assert_eq!(json["issuer"], "did:plc:extra");
assert_eq!(json["customField"], "customValue");
assert_eq!(json["timestamp"], 1234567890);
}
#[test]
fn test_typed_signature_round_trip() {
let original = Signature {
issuer: "did:plc:roundtrip2".to_string(),
signature: Bytes {
bytes: b"round trip typed".to_vec(),
},
extra: HashMap::new(),
};
let typed = TypedLexicon::new(original.clone());
let json = serde_json::to_string(&typed).unwrap();
let deserialized: TypedSignature = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.inner.issuer, original.issuer);
assert_eq!(deserialized.inner.signature.bytes, original.signature.bytes);
assert!(deserialized.has_type_field());
}
#[test]
fn test_typed_signatures_vec() {
let typed_sigs: Vec<TypedSignature> = vec![
create_typed_signature(
"did:plc:first".to_string(),
Bytes {
bytes: b"first".to_vec(),
},
),
create_typed_signature(
"did:plc:second".to_string(),
Bytes {
bytes: b"second".to_vec(),
},
),
];
let json = serde_json::to_value(&typed_sigs).unwrap();
assert!(json.is_array());
assert_eq!(json[0]["$type"], "community.lexicon.attestation.signature");
assert_eq!(json[0]["issuer"], "did:plc:first");
assert_eq!(json[1]["$type"], "community.lexicon.attestation.signature");
assert_eq!(json[1]["issuer"], "did:plc:second");
}
#[test]
fn test_plain_vs_typed_signature() {
let plain_sig = Signature {
issuer: "did:plc:plain".to_string(),
signature: Bytes {
bytes: b"plain sig".to_vec(),
},
extra: HashMap::new(),
};
let plain_json = serde_json::to_value(&plain_sig).unwrap();
assert!(!plain_json.as_object().unwrap().contains_key("$type"));
let typed_sig = TypedLexicon::new(plain_sig.clone());
let typed_json = serde_json::to_value(&typed_sig).unwrap();
assert_eq!(
typed_json["$type"],
"community.lexicon.attestation.signature"
);
assert_eq!(plain_json["issuer"], typed_json["issuer"]);
assert_eq!(plain_json["signature"], typed_json["signature"]);
}
#[test]
fn test_signature_or_ref_inline() {
let inline_sig = create_typed_signature(
"did:plc:inline".to_string(),
Bytes {
bytes: b"inline signature".to_vec(),
},
);
let sig_or_ref = SignatureOrRef::Inline(inline_sig);
let json = serde_json::to_value(&sig_or_ref).unwrap();
assert_eq!(json["$type"], "community.lexicon.attestation.signature");
assert_eq!(json["issuer"], "did:plc:inline");
assert_eq!(json["signature"]["$bytes"], "aW5saW5lIHNpZ25hdHVyZQ==");
let deserialized: SignatureOrRef = serde_json::from_value(json.clone()).unwrap();
match deserialized {
SignatureOrRef::Inline(sig) => {
assert_eq!(sig.inner.issuer, "did:plc:inline");
assert_eq!(sig.inner.signature.bytes, b"inline signature");
}
_ => panic!("Expected inline signature"),
}
}
#[test]
fn test_signature_or_ref_reference() {
let strong_ref = StrongRef {
uri: "at://did:plc:repo/community.lexicon.attestation.signature/abc123".to_string(),
cid: "bafyreisigref123".to_string(),
};
let typed_ref = TypedLexicon::new(strong_ref);
let sig_or_ref = SignatureOrRef::Reference(typed_ref);
let json = serde_json::to_value(&sig_or_ref).unwrap();
assert_eq!(json["$type"], "com.atproto.repo.strongRef");
assert_eq!(
json["uri"],
"at://did:plc:repo/community.lexicon.attestation.signature/abc123"
);
assert_eq!(json["cid"], "bafyreisigref123");
let deserialized: SignatureOrRef = serde_json::from_value(json.clone()).unwrap();
match deserialized {
SignatureOrRef::Reference(ref_) => {
assert_eq!(
ref_.uri,
"at://did:plc:repo/community.lexicon.attestation.signature/abc123"
);
assert_eq!(ref_.cid, "bafyreisigref123");
}
_ => panic!("Expected reference"),
}
}
#[test]
fn test_signatures_mixed_vector() {
let signatures: Signatures = vec![
SignatureOrRef::Inline(create_typed_signature(
"did:plc:signer1".to_string(),
Bytes {
bytes: b"sig1".to_vec(),
},
)),
SignatureOrRef::Reference(TypedLexicon::new(StrongRef {
uri: "at://did:plc:repo/community.lexicon.attestation.signature/sig2".to_string(),
cid: "bafyreisig2".to_string(),
})),
SignatureOrRef::Inline(create_typed_signature(
"did:plc:signer3".to_string(),
Bytes {
bytes: b"sig3".to_vec(),
},
)),
];
let json = serde_json::to_value(&signatures).unwrap();
assert!(json.is_array());
let array = json.as_array().unwrap();
assert_eq!(array.len(), 3);
assert_eq!(array[0]["$type"], "community.lexicon.attestation.signature");
assert_eq!(array[0]["issuer"], "did:plc:signer1");
assert_eq!(array[1]["$type"], "com.atproto.repo.strongRef");
assert_eq!(
array[1]["uri"],
"at://did:plc:repo/community.lexicon.attestation.signature/sig2"
);
assert_eq!(array[2]["$type"], "community.lexicon.attestation.signature");
assert_eq!(array[2]["issuer"], "did:plc:signer3");
let deserialized: Signatures = serde_json::from_value(json).unwrap();
assert_eq!(deserialized.len(), 3);
match &deserialized[0] {
SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.issuer, "did:plc:signer1"),
_ => panic!("Expected inline signature at index 0"),
}
match &deserialized[1] {
SignatureOrRef::Reference(ref_) => {
assert_eq!(
ref_.uri,
"at://did:plc:repo/community.lexicon.attestation.signature/sig2"
)
}
_ => panic!("Expected reference at index 1"),
}
match &deserialized[2] {
SignatureOrRef::Inline(sig) => assert_eq!(sig.inner.issuer, "did:plc:signer3"),
_ => panic!("Expected inline signature at index 2"),
}
}
#[test]
fn test_signature_or_ref_deserialization_from_json() {
let inline_json = r#"{
"$type": "community.lexicon.attestation.signature",
"issuer": "did:plc:testinline",
"signature": {"$bytes": "aGVsbG8="}
}"#;
let inline_deser: SignatureOrRef = serde_json::from_str(inline_json).unwrap();
match inline_deser {
SignatureOrRef::Inline(sig) => {
assert_eq!(sig.inner.issuer, "did:plc:testinline");
assert_eq!(sig.inner.signature.bytes, b"hello");
}
_ => panic!("Expected inline signature"),
}
let ref_json = r#"{
"$type": "com.atproto.repo.strongRef",
"uri": "at://did:plc:test/collection/record",
"cid": "bafyreicid"
}"#;
let ref_deser: SignatureOrRef = serde_json::from_str(ref_json).unwrap();
match ref_deser {
SignatureOrRef::Reference(ref_) => {
assert_eq!(ref_.uri, "at://did:plc:test/collection/record");
assert_eq!(ref_.cid, "bafyreicid");
}
_ => panic!("Expected reference"),
}
}
}