Skip to content

Commit

Permalink
DID updates
Browse files Browse the repository at this point in the history
- Update default context
- Allow context used by Universal Resolver
- Make Document properties public
- Allow multiple contexts
- Validate DID Document context property
- Use crate Error type for Document builder validation
- Add helper method for deserializing Document from bytes
  • Loading branch information
clehner committed Sep 21, 2020
1 parent 5117509 commit b2ea30a
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 26 deletions.
113 changes: 87 additions & 26 deletions src/did.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::collections::HashMap as Map;
use std::convert::TryFrom;

use crate::error::Error;
use crate::one_or_many::OneOrMany;

use chrono::prelude::*;
use serde::{Deserialize, Serialize};
Expand All @@ -13,7 +17,10 @@ use serde_json::Value;
// ***********************************************
// @TODO `id` must be URI

pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/2019/did/v1";
pub const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/did/v1";

// v0.11 context used by universal resolver
pub const V0_11_CONTEXT: &str = "https://w3id.org/did/v0.11";

// @TODO parsed data structs for DID and DIDURL
type DID = String;
Expand All @@ -28,22 +35,32 @@ type DIDURL = String;
)]
pub struct Document {
#[serde(rename = "@context")]
context: String,
id: DID,
pub context: Contexts,
pub id: DID,
#[serde(skip_serializing_if = "Option::is_none")]
created: Option<DateTime<Utc>>,
pub created: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
updated: Option<DateTime<Utc>>,
pub updated: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
authentication: Option<Vec<VerificationMethod>>,
pub authentication: Option<Vec<VerificationMethod>>,
#[serde(skip_serializing_if = "Option::is_none")]
service: Option<Vec<Service>>,
pub service: Option<Vec<Service>>,
#[serde(skip_serializing_if = "Option::is_none")]
public_key: Option<PublicKey>,
pub public_key: Option<PublicKey>,
#[serde(skip_serializing_if = "Option::is_none")]
controller: Option<Controller>,
pub controller: Option<Controller>,
#[serde(skip_serializing_if = "Option::is_none")]
proof: Option<Proof>,
pub proof: Option<Proof>,
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
#[serde(try_from = "OneOrMany<String>")]
pub enum Contexts {
One(String),
Many(Vec<String>),
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand All @@ -65,15 +82,15 @@ pub enum PublicKeyEntry {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct PublicKeyObject {
id: String,
pub id: String,
#[serde(rename = "type")]
type_: String,
pub type_: String,
// Note: different than when the DID Document is the subject:
// The value of the controller property, which identifies the
// controller of the corresponding private key, MUST be a valid DID.
controller: DID,
pub controller: DID,
#[serde(flatten)]
property_set: Option<Map<String, Value>>,
pub property_set: Option<Map<String, Value>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand All @@ -95,20 +112,20 @@ pub enum Controller {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Service {
id: String,
pub id: String,
#[serde(rename = "type")]
type_: String,
service_endpoint: String,
pub type_: String,
pub service_endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
property_set: Option<Map<String, Value>>,
pub property_set: Option<Map<String, Value>>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Proof {
#[serde(rename = "type")]
type_: String,
pub type_: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub property_set: Option<Map<String, Value>>,
Expand All @@ -117,7 +134,7 @@ pub struct Proof {
impl Default for Document {
fn default() -> Self {
Document {
context: DEFAULT_CONTEXT.to_string(),
context: Contexts::One(DEFAULT_CONTEXT.to_string()),
id: "".to_string(),
created: None,
updated: None,
Expand All @@ -126,19 +143,58 @@ impl Default for Document {
public_key: None,
controller: None,
proof: None,
property_set: None,
}
}
}

impl TryFrom<OneOrMany<String>> for Contexts {
type Error = Error;
fn try_from(context: OneOrMany<String>) -> Result<Self, Self::Error> {
let first_uri = match context.first() {
None => return Err(Error::MissingContext),
Some(uri) => uri,
};
if first_uri != DEFAULT_CONTEXT && first_uri != V0_11_CONTEXT {
return Err(Error::InvalidContext);
}
Ok(match context {
OneOrMany::One(context) => Contexts::One(context),
OneOrMany::Many(contexts) => Contexts::Many(contexts),
})
}
}

impl From<Contexts> for OneOrMany<String> {
fn from(contexts: Contexts) -> OneOrMany<String> {
match contexts {
Contexts::One(context) => OneOrMany::One(context),
Contexts::Many(contexts) => OneOrMany::Many(contexts),
}
}
}

impl DocumentBuilder {
fn validate(&self) -> Result<(), String> {
fn validate(&self) -> Result<(), Error> {
// validate is called before defaults are assigned.
// None means default will be used.
if self.id == None || self.id == Some("".to_string()) {
return Err("Missing document id".to_string());
return Err(Error::MissingDocumentId);
}
if self.context != None && self.context != Some(DEFAULT_CONTEXT.to_string()) {
return Err("Invalid context".to_string());
if let Some(first_context) = match &self.context {
None => None,
Some(Contexts::One(context)) => Some(context),
Some(Contexts::Many(contexts)) => {
if contexts.len() > 0 {
Some(&contexts[0])
} else {
None
}
}
} {
if first_context != &DEFAULT_CONTEXT && first_context != &V0_11_CONTEXT {
return Err(Error::InvalidContext);
}
}
Ok(())
}
Expand All @@ -147,7 +203,7 @@ impl DocumentBuilder {
impl Document {
pub fn new(id: &str) -> Document {
Document {
context: DEFAULT_CONTEXT.to_string(),
context: Contexts::One(DEFAULT_CONTEXT.to_string()),
id: String::from(id),
created: None,
updated: None,
Expand All @@ -156,12 +212,17 @@ impl Document {
public_key: None,
controller: None,
proof: None,
property_set: None,
}
}

pub fn from_json(json: &str) -> Result<Document, serde_json::Error> {
serde_json::from_str(json)
}

pub fn from_json_bytes(json: &[u8]) -> Result<Document, serde_json::Error> {
serde_json::from_slice(json)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -209,7 +270,7 @@ mod tests {
#[test]
fn document_from_json() {
let doc_str = "{\
\"@context\": \"https://www.w3.org/2019/did/v1\",\
\"@context\": \"https://www.w3.org/ns/did/v1\",\
\"id\": \"did:test:deadbeefcafe\"\
}";
let id = "did:test:deadbeefcafe";
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub enum Error {
URI,
InvalidContext,
MissingContext,
MissingDocumentId,
JWT(JWTError),
Base64(Base64Error),
JSON(JSONError),
Expand All @@ -40,6 +41,7 @@ impl fmt::Display for Error {
Error::TimeError => write!(f, "Unable to convert date/time"),
Error::InvalidContext => write!(f, "Invalid context"),
Error::MissingContext => write!(f, "Missing context"),
Error::MissingDocumentId => write!(f, "Missing document ID"),
Error::URI => write!(f, "Invalid URI"),
Error::Base64(e) => e.fmt(f),
Error::JWT(e) => e.fmt(f),
Expand All @@ -66,3 +68,9 @@ impl From<JSONError> for Error {
Error::JSON(err)
}
}

impl From<Error> for String {
fn from(err: Error) -> String {
format!("{}", err)
}
}

0 comments on commit b2ea30a

Please sign in to comment.