Skip to content

Commit

Permalink
did and did url parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
kayhhh committed Oct 21, 2024
1 parent 5d0fe37 commit a5879f1
Show file tree
Hide file tree
Showing 8 changed files with 843 additions and 6 deletions.
440 changes: 439 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions crates/xdid-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ edition.workspace = true
repository.workspace = true
license.workspace = true
keywords.workspace = true

[dependencies]
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.132"
serde_with = "3.11.0"
thiserror = "1.0.64"
129 changes: 129 additions & 0 deletions crates/xdid-core/src/did.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
use std::{fmt::Display, str::FromStr};

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Did {
pub method_name: MethodName,
pub method_id: MethodId,
}

impl Serialize for Did {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let did_string = format!("did:{}:{}", self.method_name.0, self.method_id.0);
serializer.serialize_str(&did_string)
}
}

impl<'de> Deserialize<'de> for Did {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let mut parts = s.splitn(3, ':');

if parts.next() != Some("did") {
return Err(serde::de::Error::custom("DID must start with 'did:'"));
}

let method_name = parts
.next()
.ok_or_else(|| serde::de::Error::custom("Missing method name"))?;
let method_specific_id = parts
.next()
.ok_or_else(|| serde::de::Error::custom("Missing method-specific ID"))?;

let method_name = MethodName::from_str(method_name).map_err(serde::de::Error::custom)?;
let method_id = MethodId::from_str(method_specific_id).map_err(serde::de::Error::custom)?;

Ok(Did {
method_name,
method_id,
})
}
}

impl FromStr for Did {
type Err = serde_json::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(Value::String(s.to_string()))
}
}

impl Display for Did {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = serde_json::to_value(self).map_err(|_| std::fmt::Error)?;
match value {
Value::String(s) => write!(f, "{}", s),
_ => Err(std::fmt::Error),
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MethodName(pub String);

impl FromStr for MethodName {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
{
Ok(MethodName(s.to_string()))
} else {
Err("Method name must contain only lowercase letters and digits".into())
}
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MethodId(pub String);

impl FromStr for MethodId {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.split(':').all(is_valid_idchar) {
Ok(MethodId(s.to_string()))
} else {
Err("Method-specific ID contains invalid characters".into())
}
}
}

fn is_valid_idchar(s: &str) -> bool {
s.chars().all(|c| {
c.is_ascii_alphanumeric()
|| c == '.'
|| c == '-'
|| c == '_'
|| c == '%'
|| c.is_ascii_hexdigit()
})
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_did_example() {
let did = Did {
method_name: MethodName("example".to_string()),
method_id: MethodId("1234-5678-abcdef".to_string()),
};

let serialized = did.to_string();
assert_eq!(serialized, "did:example:1234-5678-abcdef");

let deserialized = Did::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did);
}
}
197 changes: 197 additions & 0 deletions crates/xdid-core/src/did_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
use std::{fmt::Display, str::FromStr};

use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;

use crate::did::Did;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DidUrl {
pub did: Did,
pub path_abempty: String,
pub query: Option<String>,
pub fragment: Option<String>,
}

impl Serialize for DidUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut url = format!("{}{}", self.did, self.path_abempty);

if let Some(ref query) = self.query {
url.push('?');
url.push_str(query);
}

if let Some(ref fragment) = self.fragment {
url.push('#');
url.push_str(fragment);
}

serializer.serialize_str(&url)
}
}

impl<'de> Deserialize<'de> for DidUrl {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct DidUrlVisitor;

impl<'de> Visitor<'de> for DidUrlVisitor {
type Value = DidUrl;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a valid DID URL")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let (did_str, _) = value.split_once('/').unwrap_or_else(|| {
value
.split_once('?')
.unwrap_or_else(|| value.split_once('#').unwrap_or((value, "")))
});

let did = Did::from_str(did_str).map_err(serde::de::Error::custom)?;

let mut path_abempty = String::new();
let mut query = None;
let mut fragment = None;

let mut rest = value.strip_prefix(did_str).unwrap();
if let Some((before_fragment, frag)) = rest.split_once('#') {
fragment = Some(frag.to_string());
rest = before_fragment;
}

if let Some((before_query, qry)) = rest.split_once('?') {
query = Some(qry.to_string());
rest = before_query;
}

path_abempty.push_str(rest);

Ok(DidUrl {
did,
path_abempty,
query,
fragment,
})
}
}

deserializer.deserialize_str(DidUrlVisitor)
}
}

impl FromStr for DidUrl {
type Err = serde_json::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_value(Value::String(s.to_string()))
}
}

impl Display for DidUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = serde_json::to_value(self).map_err(|_| std::fmt::Error)?;
match value {
Value::String(s) => write!(f, "{}", s),
_ => Err(std::fmt::Error),
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_did_url_full() {
let did_url = DidUrl {
did: Did::from_str("did:example:123").unwrap(),
path_abempty: "/path/to/resource".to_string(),
query: Some("key=value".to_string()),
fragment: Some("section".to_string()),
};

let serialized = did_url.to_string();
assert_eq!(
serialized,
"did:example:123/path/to/resource?key=value#section"
);

let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did_url);
}

#[test]
fn test_did_url_no_path() {
let did_url = DidUrl {
did: Did::from_str("did:example:123").unwrap(),
path_abempty: "".to_string(),
query: Some("key=value".to_string()),
fragment: Some("section".to_string()),
};

let serialized = did_url.to_string();
assert_eq!(serialized, "did:example:123?key=value#section");

let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did_url);
}

#[test]
fn test_did_url_no_query() {
let did_url = DidUrl {
did: Did::from_str("did:example:123").unwrap(),
path_abempty: "/path/to/resource".to_string(),
query: None,
fragment: Some("section".to_string()),
};

let serialized = did_url.to_string();
assert_eq!(serialized, "did:example:123/path/to/resource#section");

let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did_url);
}

#[test]
fn test_did_url_no_fragment() {
let did_url = DidUrl {
did: Did::from_str("did:example:123").unwrap(),
path_abempty: "/path/to/resource".to_string(),
query: Some("key=value".to_string()),
fragment: None,
};

let serialized = did_url.to_string();
assert_eq!(serialized, "did:example:123/path/to/resource?key=value");

let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did_url);
}

#[test]
fn test_did_url_none() {
let did_url = DidUrl {
did: Did::from_str("did:example:123").unwrap(),
path_abempty: "".to_string(),
query: None,
fragment: None,
};

let serialized = did_url.to_string();
assert_eq!(serialized, "did:example:123");

let deserialized = DidUrl::from_str(&serialized).expect("deserialize failed");
assert_eq!(deserialized, did_url);
}
}
52 changes: 52 additions & 0 deletions crates/xdid-core/src/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, skip_serializing_none};

use crate::{did::Did, did_url::DidUrl};

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
#[serde_as]
#[skip_serializing_none]
pub struct Document {
pub id: Did,
pub also_known_as: Option<Vec<String>>,
#[serde_as(as = "Option<OneOrMany<_>>")]
pub controller: Option<Vec<Did>>,
pub verification_method: Option<Vec<VerificationMethodMap>>,
pub authentication: Option<Vec<VerificationMethod>>,
pub assertion_method: Option<Vec<VerificationMethod>>,
pub key_agreement: Option<Vec<VerificationMethod>>,
pub capability_invocation: Option<Vec<VerificationMethod>>,
pub capability_delegation: Option<Vec<VerificationMethod>>,
pub service: Option<Vec<ServiceEndpoint>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum VerificationMethod {
Map(VerificationMethodMap),
URL(String),
}

#[derive(Serialize, Deserialize, Debug)]
#[skip_serializing_none]
pub struct VerificationMethodMap {
pub id: DidUrl,
pub controller: Did,
#[serde(rename = "type")]
pub typ: String,
pub public_key_jwk: Option<Jwk>,
/// Multibase encoded public key.
pub public_key_multibase: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
#[serde_as]
pub struct ServiceEndpoint {
pub id: String,
#[serde(rename = "type")]
#[serde_as(as = "OneOrMany<_>")]
pub typ: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Jwk {}
Loading

0 comments on commit a5879f1

Please sign in to comment.