From 2eb36ccf1f93608b4cb6ccfad073f5aed9a6720a Mon Sep 17 00:00:00 2001 From: Diane Huxley Date: Tue, 21 May 2024 13:40:09 -0700 Subject: [PATCH] Handle multiple service endpoints --- crates/dids/src/document.rs | 7 +- .../src/method/dht/document_packet/mod.rs | 1 + .../dht/document_packet/rdata_encoder.rs | 75 +++++++++ .../src/method/dht/document_packet/service.rs | 142 ++++++++---------- .../src/methods/spruce_mappers/document.rs | 46 +++--- 5 files changed, 170 insertions(+), 101 deletions(-) create mode 100644 crates/dids/src/method/dht/document_packet/rdata_encoder.rs diff --git a/crates/dids/src/document.rs b/crates/dids/src/document.rs index 5f2d9c0a..d32f5ae4 100644 --- a/crates/dids/src/document.rs +++ b/crates/dids/src/document.rs @@ -1,5 +1,7 @@ use jwk::Jwk; use serde::{Deserialize, Serialize}; +pub use ssi_core::one_or_many::OneOrMany; +pub use ssi_dids::ServiceEndpoint; #[derive(Debug, Default, PartialEq, Serialize, Deserialize)] pub struct Document { @@ -63,9 +65,10 @@ impl KeyIdFragment { pub struct Service { pub id: String, #[serde(rename = "type")] - pub r#type: String, + pub r#type: OneOrMany, + // TODO: Update to allow service endpoint as a map? #[serde(rename = "serviceEndpoint")] - pub service_endpoint: String, + pub service_endpoint: OneOrMany, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/crates/dids/src/method/dht/document_packet/mod.rs b/crates/dids/src/method/dht/document_packet/mod.rs index 4040c29c..d31ee560 100644 --- a/crates/dids/src/method/dht/document_packet/mod.rs +++ b/crates/dids/src/method/dht/document_packet/mod.rs @@ -1,5 +1,6 @@ use simple_dns::SimpleDnsError; +mod rdata_encoder; pub mod service; const DEFAULT_TTL: u32 = 7200; // seconds diff --git a/crates/dids/src/method/dht/document_packet/rdata_encoder.rs b/crates/dids/src/method/dht/document_packet/rdata_encoder.rs new file mode 100644 index 00000000..2354a654 --- /dev/null +++ b/crates/dids/src/method/dht/document_packet/rdata_encoder.rs @@ -0,0 +1,75 @@ +use std::collections::HashMap; + +use simple_dns::{rdata::RData, ResourceRecord}; +use ssi_core::one_or_many::OneOrMany; + +use super::DocumentPacketError; + +/// Gets the RData from the record. If RData is RData::TXT, get the text as a string. +/// Convert strings like "id=foo;t=bar;se=baz" into a hash map like { 'id': 'foo', 't': 'bar', 'se': 'baz' } +/// If there is any issue, return DocumentPacketError::RDataError +pub fn record_rdata_to_hash_map( + record: ResourceRecord, +) -> Result, DocumentPacketError> { + // Get RData text as String + let rdata_txt = match record.rdata { + RData::TXT(txt) => txt, + _ => { + return Err(DocumentPacketError::RDataError( + "RData must have type TXT".to_owned(), + )) + } + }; + let text = match String::try_from(rdata_txt) { + Ok(text) => text, + Err(_) => { + return Err(DocumentPacketError::RDataError( + "Failed to convert to string".to_owned(), + )) + } + }; + + // Parse key-value pairs: + // Split string by ";" to get entries + // Split each entry by "=" to get key and value + let mut attributes = HashMap::new(); + for entry in text.split(';') { + let k_v: Vec<&str> = entry.split('=').collect(); + if k_v.len() != 2 { + return Err(DocumentPacketError::RDataError( + "Could not get values from RData text".to_owned(), + )); + } + + let k = k_v[0].trim().to_string(); + let v = k_v[1].trim().to_string(); + + attributes.insert(k, v); + } + + Ok(attributes) +} + +pub fn to_one_or_many(value: String) -> OneOrMany { + let split: Vec = value.split(',').map(|s| s.to_string()).collect(); + if split.len() == 1 { + OneOrMany::One(value) + } else { + OneOrMany::Many(split) + } +} + +/// Get value from the RData HashMap created by record_rdata_to_hash_map(). +/// Convert `None` into DocumentPacketError +pub fn get_rdata_txt_value( + rdata_map: &HashMap, + key: &str, +) -> Result { + let val = rdata_map + .get(key) + .ok_or(DocumentPacketError::RDataError(format!( + "Could not extract {} from RData", + key + )))?; + Ok(val.to_string()) +} diff --git a/crates/dids/src/method/dht/document_packet/service.rs b/crates/dids/src/method/dht/document_packet/service.rs index 99eb8a50..6eac6b83 100644 --- a/crates/dids/src/method/dht/document_packet/service.rs +++ b/crates/dids/src/method/dht/document_packet/service.rs @@ -6,68 +6,31 @@ use simple_dns::{ rdata::{RData, TXT}, Name, ResourceRecord, }; +use ssi_core::one_or_many::OneOrMany; use url::Url; -use super::{DocumentPacketError, DEFAULT_TTL}; - -/// Gets the RData from the record. If RData is RData::TXT, get the text as a string. -/// Convert strings like "id=foo;t=bar;se=baz" into a map like { 'id': 'foo', 't': 'bar', 'se': 'baz' } -/// If there is any issue, return DocumentPacketError::RDataError -fn record_rdata_to_hash_map( - record: ResourceRecord, -) -> Result, DocumentPacketError> { - // Get RData text as String - let rdata_txt = match record.rdata { - RData::TXT(txt) => txt, - _ => { - return Err(DocumentPacketError::RDataError( - "RData must have type TXT".to_owned(), - )) - } - }; - let text = match String::try_from(rdata_txt) { - Ok(text) => text, - Err(_) => { - return Err(DocumentPacketError::RDataError( - "Failed to convert to string".to_owned(), - )) - } - }; - - // Parse key-value pairs: - // Split string by ";" to get entries - // Split each entry by "=" to get key and value - let mut attributes = HashMap::new(); - for entry in text.split(';') { - let k_v: Vec<&str> = entry.split('=').collect(); - if k_v.len() != 2 { - return Err(DocumentPacketError::RDataError( - "Could not get values from RData text".to_owned(), - )); - } +use super::{ + rdata_encoder::{get_rdata_txt_value, record_rdata_to_hash_map, to_one_or_many}, + DocumentPacketError, DEFAULT_TTL, +}; - let k = k_v[0].trim().to_string(); - let v = k_v[1].trim().to_string(); +#[derive(Debug, PartialEq)] +struct ServiceRdata { + pub id: String, + pub se: OneOrMany, + pub t: OneOrMany, +} - attributes.insert(k, v); +impl TryFrom> for ServiceRdata { + fn try_from(rdata_map: HashMap) -> Result { + Ok(ServiceRdata { + id: get_rdata_txt_value(&rdata_map, "id")?, + se: to_one_or_many(get_rdata_txt_value(&rdata_map, "se")?), + t: to_one_or_many(get_rdata_txt_value(&rdata_map, "t")?), + }) } - Ok(attributes) -} - -/// Get value from the RData HashMap created by record_rdata_to_hash_map(). -/// Convert `None` into DocumentPacketError -fn get_rdata_txt_value( - rdata_map: &HashMap, - key: &str, -) -> Result { - let val = rdata_map - .get(key) - .ok_or(DocumentPacketError::RDataError(format!( - "Could not extract {} from RData", - key - )))?; - Ok(val.to_string()) + type Error = DocumentPacketError; } impl Service { @@ -81,10 +44,16 @@ impl Service { .fragment() .ok_or(DocumentPacketError::MissingFragment(self.id.clone()))?; - let parts = format!( - "id={};t={};se={}", - service_id_fragment, self.r#type, self.service_endpoint - ); + let t = match &self.r#type { + OneOrMany::One(r#type) => r#type.clone(), + OneOrMany::Many(r#types) => r#types.join(","), + }; + let se = match &self.service_endpoint { + OneOrMany::One(service_endpoint) => service_endpoint.clone(), + OneOrMany::Many(service_endpoints) => service_endpoints.join(","), + }; + + let parts = format!("id={};t={};se={}", service_id_fragment, t, se); let name = Name::new_unchecked(&format!("_s{}._did", idx)).into_owned(); let txt_record = TXT::new().with_string(&parts)?.into_owned(); @@ -101,15 +70,12 @@ impl Service { record: ResourceRecord, ) -> Result { let rdata_map = record_rdata_to_hash_map(record)?; - - let id = get_rdata_txt_value(&rdata_map, "id")?; - let t = get_rdata_txt_value(&rdata_map, "t")?; - let se = get_rdata_txt_value(&rdata_map, "se")?; + let service_rdata: ServiceRdata = rdata_map.try_into()?; Ok(Service { - id: format!("{}#{}", did_uri, id), - r#type: t, - service_endpoint: se, // TODO: support service endpoints as array or map + id: format!("{}#{}", did_uri, service_rdata.id), + r#type: service_rdata.t, + service_endpoint: service_rdata.se, }) } } @@ -129,8 +95,34 @@ mod tests { let service_endpoint = "foo.tbd.website"; let service = Service { id: id.to_string(), - r#type: r#type.to_string(), - service_endpoint: service_endpoint.to_string(), + r#type: OneOrMany::One(r#type.to_string()), + service_endpoint: OneOrMany::One(service_endpoint.to_string()), + }; + + let resource_record = service + .to_resource_record(0) + .expect("Failed to convert Service to ResourceRecord"); + + let service2 = Service::from_resource_record(did_uri, resource_record) + .expect("Failed to convert ResourceRecord to Service"); + + assert_eq!(service, service2); + } + + #[test] + fn test_to_and_from_resource_record_many_service_endpoints() { + let did_uri = "did:dht:123"; + let id = "did:dht:123#0"; + + let r#type = "some_type"; + let service_endpoint = "foo.tbd.website"; + let service = Service { + id: id.to_string(), + r#type: OneOrMany::Many(vec![r#type.to_string(), r#type.to_string()]), + service_endpoint: OneOrMany::Many(vec![ + service_endpoint.to_string(), + service_endpoint.to_string(), + ]), }; let resource_record = service @@ -154,19 +146,17 @@ mod tests { let service_endpoint = "foo.tbd.website"; let service = Service { id: id.to_string(), - r#type: r#type.to_string(), - service_endpoint: service_endpoint.to_string(), + r#type: OneOrMany::One(r#type.to_string()), + service_endpoint: OneOrMany::One(service_endpoint.to_string()), }; let resource_record = service .to_resource_record(0) .expect("Expected to create resource record from service"); - let service2 = Service::from_resource_record(did_uri, resource_record) - .expect("msg"); + let service2 = Service::from_resource_record(did_uri, resource_record).expect("msg"); assert_eq!(service, service2); } - #[test] fn test_to_record_resource_missing_fragment() { let did_uri = "did:dht:123"; // missing "#0" @@ -174,8 +164,8 @@ mod tests { let service_endpoint = "foo.tbd.website"; let service = Service { id: did_uri.to_string(), - r#type: r#type.to_string(), - service_endpoint: service_endpoint.to_string(), + r#type: OneOrMany::One(r#type.to_string()), + service_endpoint: OneOrMany::One(service_endpoint.to_string()), }; let resource_record = service diff --git a/crates/dids/src/methods/spruce_mappers/document.rs b/crates/dids/src/methods/spruce_mappers/document.rs index 1e8e7bf8..89d95b51 100644 --- a/crates/dids/src/methods/spruce_mappers/document.rs +++ b/crates/dids/src/methods/spruce_mappers/document.rs @@ -150,34 +150,31 @@ impl VerificationMethod { impl Service { pub fn from_spruce(spruce_service: SpruceService) -> Result { - let r#type = match spruce_service.type_ { - OneOrMany::One(t) => t, - OneOrMany::Many(mut t) => t - .pop() - .ok_or_else(|| "Service type array was empty".to_string())?, - }; - let service_endpoint = match spruce_service.service_endpoint { Some(OneOrMany::One(endpoint)) => match endpoint { - SpruceServiceEndpoint::URI(uri) => uri, - SpruceServiceEndpoint::Map(map) => serde_json::to_string(&map).unwrap_or_default(), + SpruceServiceEndpoint::URI(uri) => OneOrMany::One(uri), + SpruceServiceEndpoint::Map(map) => { + OneOrMany::One(serde_json::to_string(&map).unwrap_or_default()) + } }, - Some(OneOrMany::Many(endpoints)) => endpoints - .into_iter() - .last() - .map(|endpoint| match endpoint { - SpruceServiceEndpoint::URI(uri) => uri, - SpruceServiceEndpoint::Map(map) => { - serde_json::to_string(&map).unwrap_or_default() - } - }) - .ok_or_else(|| "Service endpoint array was empty".to_string())?, + Some(OneOrMany::Many(endpoints)) => { + let endpoints = endpoints + .into_iter() + .map(|endpoint| match endpoint { + SpruceServiceEndpoint::URI(uri) => uri, + SpruceServiceEndpoint::Map(map) => { + serde_json::to_string(&map).unwrap_or_default() + } + }) + .collect(); + OneOrMany::Many(endpoints) + } None => return Err("Service endpoint is missing".to_string()), }; Ok(Service { id: spruce_service.id, - r#type, + r#type: spruce_service.type_, service_endpoint, }) } @@ -185,6 +182,9 @@ impl Service { #[cfg(test)] mod tests { + use ssi_core::one_or_many::OneOrMany; + use ssi_dids::ServiceEndpoint; + use super::*; #[test] @@ -260,7 +260,7 @@ mod tests { let spruce_service = SpruceService { id: "did:example:123#service1".to_string(), type_: OneOrMany::One("Example".to_string()), - service_endpoint: Some(OneOrMany::One(SpruceServiceEndpoint::URI( + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI( "https://example.com/service1".to_string(), ))), property_set: None, @@ -268,10 +268,10 @@ mod tests { let service = Service::from_spruce(spruce_service).unwrap(); assert_eq!(service.id, "did:example:123#service1".to_string()); - assert_eq!(service.r#type, "Example".to_string()); + assert_eq!(service.r#type, OneOrMany::One("Example".to_string())); assert_eq!( service.service_endpoint, - "https://example.com/service1".to_string() + OneOrMany::One("https://example.com/service1".to_string()) ); } }