use serde::{Deserialize, Serialize};
use serde_json::Value;
use trust_tasks_rs::Payload as _;
use trust_tasks_rs::specs::vtc::join_requests::{manifest, submit};
#[must_use]
pub fn is_version_refusal(code: &str) -> bool {
matches!(code, "unsupportedType" | "unsupportedVersion")
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum JoinProtocol {
#[default]
V0_3,
V0_2,
}
impl JoinProtocol {
pub fn manifest_type(self) -> &'static str {
match self {
Self::V0_3 => manifest::v0_3::Payload::TYPE_URI,
Self::V0_2 => manifest::v0_2::Payload::TYPE_URI,
}
}
pub fn manifest_response_type(self) -> &'static str {
match self {
Self::V0_3 => manifest::v0_3::Response::TYPE_URI,
Self::V0_2 => manifest::v0_2::Response::TYPE_URI,
}
}
pub fn submit_type(self) -> &'static str {
match self {
Self::V0_3 => submit::v0_3::Payload::TYPE_URI,
Self::V0_2 => submit::v0_2::Payload::TYPE_URI,
}
}
pub fn submit_response_type(self) -> &'static str {
match self {
Self::V0_3 => submit::v0_3::Response::TYPE_URI,
Self::V0_2 => submit::v0_2::Response::TYPE_URI,
}
}
pub fn from_manifest_response(typ: &str) -> Option<Self> {
[Self::V0_3, Self::V0_2]
.into_iter()
.find(|p| p.manifest_response_type() == typ)
}
pub fn from_manifest_request(typ: &str) -> Option<Self> {
[Self::V0_3, Self::V0_2]
.into_iter()
.find(|p| p.manifest_type() == typ)
}
pub fn is_submit_response(typ: &str) -> bool {
[Self::V0_3, Self::V0_2]
.into_iter()
.any(|p| p.submit_response_type() == typ)
}
pub fn fallback(self) -> Option<Self> {
match self {
Self::V0_3 => Some(Self::V0_2),
Self::V0_2 => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Admission {
Automatic,
Review,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CriterionMeta {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub admission: Option<Admission>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invitation_required: Option<bool>,
}
const V0_3_ONLY_CRITERION_MEMBERS: [&str; 3] =
["admission", "credentialIssuers", "invitationRequired"];
pub fn read_manifest(
protocol: JoinProtocol,
payload: &Value,
) -> Result<(manifest::v0_2::Response, Vec<CriterionMeta>), serde_json::Error> {
match protocol {
JoinProtocol::V0_2 => Ok((serde_json::from_value(payload.clone())?, Vec::new())),
JoinProtocol::V0_3 => {
let v3: manifest::v0_3::Response = serde_json::from_value(payload.clone())?;
let meta = v3
.criteria
.iter()
.map(|c| CriterionMeta {
id: c.id.to_string(),
admission: Some(match c.admission {
manifest::v0_3::CriterionAdmission::Automatic => Admission::Automatic,
_ => Admission::Review,
}),
invitation_required: c.invitation_required,
})
.collect();
let mut as_v2 = payload.clone();
if let Some(criteria) = as_v2.get_mut("criteria").and_then(Value::as_array_mut) {
for criterion in criteria.iter_mut().filter_map(Value::as_object_mut) {
for member in V0_3_ONLY_CRITERION_MEMBERS {
criterion.remove(member);
}
criterion
.entry("presentationDefinition")
.or_insert_with(|| Value::Object(Default::default()));
}
}
Ok((serde_json::from_value(as_v2)?, meta))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const DIGEST: &str = "zQmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
fn v3_manifest() -> Value {
json!({
"communityDid": "did:webvh:example.com:community",
"criteria": [
{
"id": "invited",
"admission": "automatic",
"invitationRequired": true,
"requirementsDigest": DIGEST,
},
{
"id": "review",
"admission": "review",
"credentialIssuers": "any",
"presentationDefinition": { "credentials": [] },
"requirementsDigest": DIGEST,
},
]
})
}
#[test]
fn a_v0_3_manifest_is_read_into_the_v0_2_shape_with_its_admission() {
let (manifest, meta) = read_manifest(JoinProtocol::V0_3, &v3_manifest()).unwrap();
assert_eq!(manifest.criteria.len(), 2);
assert_eq!(meta.len(), 2);
assert_eq!(meta[0].id, "invited");
assert_eq!(meta[0].admission, Some(Admission::Automatic));
assert_eq!(meta[0].invitation_required, Some(true));
assert_eq!(meta[1].admission, Some(Admission::Review));
assert!(
manifest.criteria[0]
.requirements_digest
.as_ref()
.is_some_and(|d| d.to_string() == DIGEST)
);
}
#[test]
fn a_malformed_v0_3_manifest_is_refused() {
let mut m = v3_manifest();
m["criteria"][0]
.as_object_mut()
.unwrap()
.remove("admission");
assert!(read_manifest(JoinProtocol::V0_3, &m).is_err());
}
#[test]
fn the_versions_name_their_types_and_fall_back_once() {
assert!(
JoinProtocol::V0_3
.manifest_type()
.ends_with("/manifest/0.3")
);
assert!(JoinProtocol::V0_2.submit_type().ends_with("/submit/0.2"));
assert_eq!(
JoinProtocol::from_manifest_response(JoinProtocol::V0_3.manifest_response_type()),
Some(JoinProtocol::V0_3)
);
assert!(JoinProtocol::is_submit_response(
JoinProtocol::V0_2.submit_response_type()
));
assert_eq!(JoinProtocol::V0_3.fallback(), Some(JoinProtocol::V0_2));
assert_eq!(JoinProtocol::V0_2.fallback(), None);
assert!(is_version_refusal("unsupportedVersion"));
assert!(is_version_refusal("unsupportedType"));
assert!(!is_version_refusal("permissionDenied"));
}
}