Skip to content

Commit

Permalink
Handle multiple service endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
Diane Huxley committed May 22, 2024
1 parent cde7b93 commit 2eb36cc
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 101 deletions.
7 changes: 5 additions & 2 deletions crates/dids/src/document.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -63,9 +65,10 @@ impl KeyIdFragment {
pub struct Service {
pub id: String,
#[serde(rename = "type")]
pub r#type: String,
pub r#type: OneOrMany<String>,
// TODO: Update to allow service endpoint as a map?
#[serde(rename = "serviceEndpoint")]
pub service_endpoint: String,
pub service_endpoint: OneOrMany<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
1 change: 1 addition & 0 deletions crates/dids/src/method/dht/document_packet/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use simple_dns::SimpleDnsError;

mod rdata_encoder;
pub mod service;

const DEFAULT_TTL: u32 = 7200; // seconds
Expand Down
75 changes: 75 additions & 0 deletions crates/dids/src/method/dht/document_packet/rdata_encoder.rs
Original file line number Diff line number Diff line change
@@ -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<HashMap<String, String>, 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<String> {
let split: Vec<String> = 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<String, String>,
key: &str,
) -> Result<String, DocumentPacketError> {
let val = rdata_map
.get(key)
.ok_or(DocumentPacketError::RDataError(format!(
"Could not extract {} from RData",
key
)))?;
Ok(val.to_string())
}
142 changes: 66 additions & 76 deletions crates/dids/src/method/dht/document_packet/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HashMap<String, String>, 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<String>,
pub t: OneOrMany<String>,
}

attributes.insert(k, v);
impl TryFrom<HashMap<String, String>> for ServiceRdata {
fn try_from(rdata_map: HashMap<String, String>) -> Result<Self, Self::Error> {
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<String, String>,
key: &str,
) -> Result<String, DocumentPacketError> {
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 {
Expand All @@ -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();

Expand All @@ -101,15 +70,12 @@ impl Service {
record: ResourceRecord,
) -> Result<Self, DocumentPacketError> {
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,
})
}
}
Expand All @@ -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
Expand All @@ -154,28 +146,26 @@ 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"
let r#type = "some_type";
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
Expand Down
46 changes: 23 additions & 23 deletions crates/dids/src/methods/spruce_mappers/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,41 +150,41 @@ impl VerificationMethod {

impl Service {
pub fn from_spruce(spruce_service: SpruceService) -> Result<Self, String> {
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,
})
}
}

#[cfg(test)]
mod tests {
use ssi_core::one_or_many::OneOrMany;
use ssi_dids::ServiceEndpoint;

use super::*;

#[test]
Expand Down Expand Up @@ -260,18 +260,18 @@ 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,
};
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())
);
}
}

0 comments on commit 2eb36cc

Please sign in to comment.