Skip to content

Commit

Permalink
Add ServiceEndpoint sets and maps (#485)
Browse files Browse the repository at this point in the history
* Add ServiceEndpoint

* [WIP] Update Service to use ServiceEndpoint

* Fix clippy lints, formatting

* Add ServiceEndpoint::Map variant

* Update history example with ServiceEndpoint map

* Fix doc comment

* Fix ServiceEndpoint OrderedSet for Url
  • Loading branch information
cycraig authored Nov 9, 2021
1 parent 7978f5f commit e05ca5b
Show file tree
Hide file tree
Showing 14 changed files with 441 additions and 39 deletions.
4 changes: 3 additions & 1 deletion bindings/wasm/examples/src/resolve_history.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ async function resolveHistory(clientConfig) {
let serviceJSON2 = {
id: diffDoc2.id + "#linked-domain-2",
type: "LinkedDomains",
serviceEndpoint: "https://example.com",
serviceEndpoint: {
"origins": ["https://iota.org/", "https://example.com/"]
},
};
diffDoc2.insertService(Service.fromJSON(serviceJSON2));
diffDoc2.updated = Timestamp.nowUTC();
Expand Down
6 changes: 4 additions & 2 deletions examples/low-level-api/resolve_history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async fn main() -> Result<()> {
let service: Service = Service::from_json_value(json!({
"id": diff_doc_1.id().to_url().join("#linked-domain-1")?,
"type": "LinkedDomains",
"serviceEndpoint": "https://iota.org"
"serviceEndpoint": "https://iota.org/"
}))?;
assert!(diff_doc_1.insert_service(service));
diff_doc_1.set_updated(Timestamp::now_utc());
Expand Down Expand Up @@ -120,7 +120,9 @@ async fn main() -> Result<()> {
let service: Service = Service::from_json_value(json!({
"id": diff_doc_2.id().to_url().join("#linked-domain-2")?,
"type": "LinkedDomains",
"serviceEndpoint": "https://example.com"
"serviceEndpoint": {
"origins": ["https://iota.org/", "https://example.com/"]
}
}))?;
diff_doc_2.insert_service(service);
diff_doc_2.set_updated(Timestamp::now_utc());
Expand Down
8 changes: 4 additions & 4 deletions identity-account/src/events/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use crypto::signatures::ed25519;

use identity_core::common::Fragment;
use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::crypto::PublicKey;
use identity_did::service::ServiceEndpoint;
use identity_did::verification::MethodData;
use identity_did::verification::MethodScope;
use identity_did::verification::MethodType;
Expand Down Expand Up @@ -60,7 +60,7 @@ pub(crate) enum Command {
CreateService {
fragment: String,
type_: String,
endpoint: Url,
endpoint: ServiceEndpoint,
properties: Option<Object>,
},
DeleteService {
Expand Down Expand Up @@ -358,12 +358,12 @@ impl_command_builder!(
/// # Parameters
/// - `type_`: the type of the service, e.g. `"LinkedDomains"`, required.
/// - `fragment`: the identifier of the service in the document, required.
/// - `endpoint`: the url of the service, required.
/// - `endpoint`: the `ServiceEndpoint` of the service, required.
/// - `properties`: additional properties of the service, optional.
CreateService {
@required fragment String,
@required type_ String,
@required endpoint Url,
@required endpoint ServiceEndpoint,
@optional properties Object,
});

Expand Down
5 changes: 3 additions & 2 deletions identity-account/src/identity/identity_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use identity_did::did::DID;
use identity_did::document::CoreDocument;
use identity_did::document::DocumentBuilder;
use identity_did::service::Service as CoreService;
use identity_did::service::ServiceEndpoint;
use identity_did::verifiable::Properties as VerifiableProperties;
use identity_did::verification::MethodData;
use identity_did::verification::MethodRef as CoreMethodRef;
Expand Down Expand Up @@ -569,14 +570,14 @@ pub struct TinyService {
#[serde(rename = "2")]
type_: String,
#[serde(rename = "3")]
endpoint: Url,
endpoint: ServiceEndpoint,
#[serde(rename = "4")]
properties: Option<Object>,
}

impl TinyService {
/// Creates a new `TinyService`.
pub fn new(fragment: String, type_: String, endpoint: Url, properties: Option<Object>) -> Self {
pub fn new(fragment: String, type_: String, endpoint: ServiceEndpoint, properties: Option<Object>) -> Self {
Self {
fragment: Fragment::new(fragment),
type_,
Expand Down
1 change: 1 addition & 0 deletions identity-did/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ async-trait = { version = "0.1", default-features = false }
did_url = { version = "0.1", default-features = false, features = ["std", "serde"] }
form_urlencoded = { version = "1.0.1", default-features = false }
identity-core = { version = "=0.4.0", path = "../identity-core" }
indexmap = { version = "1.7", default-features = false, features = ["std", "serde-1"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
strum = { version = "0.21", features = ["derive"] }
thiserror = { version = "1.0", default-features = false }
Expand Down
3 changes: 2 additions & 1 deletion identity-did/src/diff/diff_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ mod test {
use identity_core::common::Value;

use crate::service::ServiceBuilder;
use crate::service::ServiceEndpoint;
use crate::verification::MethodBuilder;
use crate::verification::MethodData;
use crate::verification::MethodType;
Expand All @@ -340,7 +341,7 @@ mod test {
fn service(did_url: CoreDIDUrl) -> Service {
ServiceBuilder::default()
.id(did_url)
.service_endpoint(Url::parse("did:service:1234").unwrap())
.service_endpoint(ServiceEndpoint::One(Url::parse("did:service:1234").unwrap()))
.type_("test_service")
.build()
.unwrap()
Expand Down
117 changes: 105 additions & 12 deletions identity-did/src/diff/diff_service.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
// Copyright 2020-2021 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

use serde::Deserialize;
use serde::Serialize;

use identity_core::common::Object;
use identity_core::common::Url;
use identity_core::convert::FromJson;
use identity_core::convert::ToJson;
use identity_core::diff::Diff;
use identity_core::diff::DiffString;
use identity_core::diff::Error;
use identity_core::diff::Result;
use serde::Deserialize;
use serde::Serialize;

use crate::did::CoreDIDUrl;
use crate::service::Service;
use crate::service::ServiceEndpoint;

#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct DiffService<T = Object>
Expand Down Expand Up @@ -72,7 +75,7 @@ where
.transpose()?
.unwrap_or_else(|| self.type_().to_string());

let service_endpoint: Url = diff
let service_endpoint: ServiceEndpoint = diff
.service_endpoint
.map(|value| self.service_endpoint().merge(value))
.transpose()?
Expand Down Expand Up @@ -105,9 +108,9 @@ where
.transpose()?
.ok_or_else(|| Error::convert("Missing field `service.type_`"))?;

let service_endpoint: Url = diff
let service_endpoint: ServiceEndpoint = diff
.service_endpoint
.map(Url::from_diff)
.map(ServiceEndpoint::from_diff)
.transpose()?
.ok_or_else(|| Error::convert("Missing field `service.service_endpoint`"))?;

Expand All @@ -131,10 +134,41 @@ where
}
}

impl Diff for ServiceEndpoint {
type Type = DiffString;

fn diff(&self, other: &Self) -> identity_core::diff::Result<Self::Type> {
self
.to_json()
.map_err(identity_core::diff::Error::diff)?
.diff(&other.to_string())
}

fn merge(&self, diff: Self::Type) -> identity_core::diff::Result<Self> {
self
.to_json()
.map_err(identity_core::diff::Error::diff)?
.merge(diff)
.and_then(|this| Self::from_json(&this).map_err(identity_core::diff::Error::merge))
}

fn from_diff(diff: Self::Type) -> identity_core::diff::Result<Self> {
String::from_diff(diff).and_then(|this| Self::from_json(&this).map_err(identity_core::diff::Error::convert))
}

fn into_diff(self) -> identity_core::diff::Result<Self::Type> {
self.to_json().map_err(identity_core::diff::Error::diff)?.into_diff()
}
}

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

use identity_core::common::Object;
use identity_core::common::Url;

use super::*;

fn controller() -> CoreDIDUrl {
"did:example:1234".parse().unwrap()
Expand All @@ -146,7 +180,7 @@ mod test {
properties.insert("key1".to_string(), "value1".into());
Service::builder(properties)
.id(controller)
.service_endpoint(Url::parse("did:service:1234").unwrap())
.service_endpoint(Url::parse("did:service:1234").unwrap().into())
.type_("test_service")
.build()
.unwrap()
Expand Down Expand Up @@ -183,17 +217,76 @@ mod test {
}

#[test]
fn test_service_endpoint() {
fn test_service_endpoint_one() {
let service = service();
let mut new = service.clone();
let new_url = "did:test:1234".to_string();
*new.service_endpoint_mut() = Url::parse(new_url.clone()).unwrap();
let new_url = "did:test:1234#service".to_string();
*new.service_endpoint_mut() = Url::parse(new_url).unwrap().into();

let diff = service.diff(&new).unwrap();
assert!(diff.id.is_none());
assert!(diff.properties.is_none());
assert!(diff.type_.is_none());
assert_eq!(
diff.service_endpoint,
Some(DiffString(Some("\"did:test:1234#service\"".to_owned())))
);
let merge = service.merge(diff).unwrap();
assert_eq!(merge, new);
}

#[test]
fn test_service_endpoint_set() {
let service = service();

let mut new = service.clone();
let new_url_set = vec![
Url::parse("https://example.com/").unwrap(),
Url::parse("did:test:1234#service").unwrap(),
];
*new.service_endpoint_mut() = ServiceEndpoint::Set(new_url_set.try_into().unwrap());

let diff = service.diff(&new).unwrap();
assert!(diff.id.is_none());
assert!(diff.properties.is_none());
assert!(diff.type_.is_none());
assert_eq!(
diff.service_endpoint,
Some(DiffString(Some(
r#"["https://example.com/","did:test:1234#service"]"#.to_owned()
)))
);
let merge = service.merge(diff).unwrap();
assert_eq!(merge, new);
}

#[test]
fn test_service_endpoint_map() {
let service = service();

let mut new = service.clone();
let mut new_url_map = IndexMap::new();
new_url_map.insert(
"origins".to_owned(),
vec![
Url::parse("https://example.com/").unwrap(),
Url::parse("did:test:1234#service").unwrap(),
]
.try_into()
.unwrap(),
);
*new.service_endpoint_mut() = ServiceEndpoint::Map(new_url_map);

let diff = service.diff(&new).unwrap();
assert!(diff.id.is_none());
assert!(diff.properties.is_none());
assert!(diff.type_.is_none());
assert_eq!(diff.service_endpoint, Some(DiffString(Some(new_url))));
assert_eq!(
diff.service_endpoint,
Some(DiffString(Some(
r#"{"origins":["https://example.com/","did:test:1234#service"]}"#.to_owned()
)))
);
let merge = service.merge(diff).unwrap();
assert_eq!(merge, new);
}
Expand Down
2 changes: 1 addition & 1 deletion identity-did/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ pub enum Error {
#[error("Invalid DID Resolution Fragment")]
InvalidDIDFragment,
#[error("Invalid DID Resolution Service")]
InvalidServiceProtocol,
InvalidResolutionService,
}
12 changes: 9 additions & 3 deletions identity-did/src/resolution/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::resolution::Resolution;
use crate::resolution::ResolverMethod;
use crate::resolution::Resource;
use crate::resolution::SecondaryResource;
use crate::service::ServiceEndpoint;
use crate::utils::OrderedSet;

/// Resolves a DID into a DID Document by using the "Read" operation of the DID method.
Expand Down Expand Up @@ -229,7 +230,12 @@ fn dereference_primary(document: CoreDocument, mut did_url: CoreDIDUrl) -> Resul
.find(|service| matches!(service.id().fragment(), Some(fragment) if fragment == target))
.map(|service| service.service_endpoint())
// 1.2. Execute the Service Endpoint Construction algorithm.
.map(|url| service_endpoint_ctor(did_url, url))
.map(|endpoint| match endpoint {
ServiceEndpoint::One(url) => service_endpoint_ctor(did_url, url),
// TODO: support service endpoint sets and map? Dereferencing spec does not address them.
ServiceEndpoint::Set(_) => Err(Error::InvalidResolutionService),
ServiceEndpoint::Map(_) => Err(Error::InvalidResolutionService),
})
.transpose()?
// 1.3. Return the output service endpoint URL.
.map(Into::into)
Expand Down Expand Up @@ -321,7 +327,7 @@ fn service_endpoint_ctor(did: CoreDIDUrl, url: &Url) -> Result<Url> {

// The input service endpoint URL MUST be an HTTP(S) URL.
if url.scheme() != "https" {
return Err(Error::InvalidServiceProtocol);
return Err(Error::InvalidResolutionService);
}

// 1. Initialize a string output service endpoint URL to the value of
Expand Down Expand Up @@ -455,7 +461,7 @@ mod test {
fn generate_service(did: &CoreDID, fragment: &str, url: &str) -> Service {
Service::builder(Default::default())
.id(did.to_url().join(fragment).unwrap())
.service_endpoint(Url::parse(url).unwrap())
.service_endpoint(Url::parse(url).unwrap().into())
.type_("LinkedDomains")
.build()
.unwrap()
Expand Down
12 changes: 7 additions & 5 deletions identity-did/src/service/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
// SPDX-License-Identifier: Apache-2.0

use identity_core::common::Object;
use identity_core::common::Url;

use crate::did::CoreDIDUrl;
use crate::error::Result;
use crate::service::Service;
use crate::service::ServiceEndpoint;

/// A `ServiceBuilder` is used to generate a customized `Service`.
#[derive(Clone, Debug, Default)]
pub struct ServiceBuilder<T = Object> {
pub(crate) id: Option<CoreDIDUrl>,
pub(crate) type_: Option<String>,
pub(crate) service_endpoint: Option<Url>,
pub(crate) service_endpoint: Option<ServiceEndpoint>,
pub(crate) properties: T,
}

Expand Down Expand Up @@ -44,7 +44,7 @@ impl<T> ServiceBuilder<T> {

/// Sets the `serviceEndpoint` value of the generated `Service`.
#[must_use]
pub fn service_endpoint(mut self, value: Url) -> Self {
pub fn service_endpoint(mut self, value: ServiceEndpoint) -> Self {
self.service_endpoint = Some(value);
self
}
Expand All @@ -57,14 +57,16 @@ impl<T> ServiceBuilder<T> {

#[cfg(test)]
mod tests {
use identity_core::common::Url;

use super::*;

#[test]
#[should_panic = "InvalidServiceId"]
fn test_missing_id() {
let _: Service = ServiceBuilder::default()
.type_("ServiceType")
.service_endpoint("https://example.com".parse().unwrap())
.service_endpoint(Url::parse("https://example.com").unwrap().into())
.build()
.unwrap();
}
Expand All @@ -74,7 +76,7 @@ mod tests {
fn test_missing_type_() {
let _: Service = ServiceBuilder::default()
.id("did:example:123".parse().unwrap())
.service_endpoint("https://example.com".parse().unwrap())
.service_endpoint(Url::parse("https://example.com").unwrap().into())
.build()
.unwrap();
}
Expand Down
Loading

0 comments on commit e05ca5b

Please sign in to comment.