use serde_json::Value;
use vta_sdk::client::VtaClient;
use vta_sdk::error::VtaError;
use crate::errors::OpenVTCError;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Severity {
#[default]
Low,
High,
}
impl Severity {
fn from_wire(token: &str) -> Self {
match token {
"high" => Self::High,
_ => Self::Low,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Remedy {
UseDifferentValue,
ReissueCredentialToThisDid,
CorrelateDeliberately,
ProceedAndRecord,
}
impl Remedy {
fn from_wire(token: &str) -> Option<Self> {
match token {
"useDifferentValue" => Some(Self::UseDifferentValue),
"reissueCredentialToThisDid" => Some(Self::ReissueCredentialToThisDid),
"correlateDeliberately" => Some(Self::CorrelateDeliberately),
"proceedAndRecord" => Some(Self::ProceedAndRecord),
_ => None,
}
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::UseDifferentValue => "hold a different value for one of them",
Self::ReissueCredentialToThisDid => {
"have the credential re-issued to the persona that uses it"
}
Self::CorrelateDeliberately => "decide the link is one you want",
Self::ProceedAndRecord => "go ahead, and have it written down",
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Finding {
pub attribute_id: Option<String>,
pub severity: Severity,
pub why: String,
pub remedies: Vec<Remedy>,
pub crosses_worlds: Option<bool>,
pub shared_with: usize,
}
impl Finding {
fn from_wire(value: &Value) -> Self {
Self {
attribute_id: value
.get("attributeId")
.and_then(Value::as_str)
.map(str::to_string),
severity: value
.get("severity")
.and_then(Value::as_str)
.map_or(Severity::Low, Severity::from_wire),
why: value
.get("why")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
remedies: value
.get("remedies")
.and_then(Value::as_array)
.map(|xs| {
xs.iter()
.filter_map(Value::as_str)
.filter_map(Remedy::from_wire)
.collect()
})
.unwrap_or_default(),
crosses_worlds: value.get("crossesWorlds").and_then(Value::as_bool),
shared_with: value
.get("sharedWith")
.and_then(Value::as_array)
.map_or(0, Vec::len),
}
}
#[must_use]
pub fn crosses_a_world(&self) -> bool {
self.crosses_worlds == Some(true)
}
#[must_use]
pub fn headline(&self) -> &'static str {
match (self.crosses_a_world(), self.severity) {
(true, _) => {
"These two worlds share a value — anyone who sees both knows they are \
the same person."
}
(false, Severity::High) => {
"Provable — and the same signature to everyone who sees it, so it links."
}
(false, Severity::Low) => "Same person to anyone who sees both.",
}
}
}
pub async fn analyze(client: &VtaClient) -> Result<Vec<Finding>, OpenVTCError> {
let value = match client.persona_correlation_analyze(None, None, None).await {
Ok(value) => value,
Err(VtaError::UnsupportedTaskType { .. }) => return Ok(Vec::new()),
Err(e) => {
return Err(OpenVTCError::Vta(format!(
"persona correlation analyze failed: {e}"
)));
}
};
Ok(value
.get("findings")
.and_then(Value::as_array)
.map(|rows| rows.iter().map(Finding::from_wire).collect())
.unwrap_or_default())
}
#[must_use]
pub fn for_attribute<'a>(findings: &'a [Finding], attribute_id: &str) -> Option<&'a Finding> {
let mine = || {
findings
.iter()
.filter(|f| f.attribute_id.as_deref() == Some(attribute_id))
};
mine()
.find(|f| f.crosses_a_world())
.or_else(|| mine().next())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn finding(attribute: &str, crosses: Option<bool>, severity: &str) -> Value {
let mut row = json!({
"attributeId": attribute,
"severity": severity,
"why": "the same mobile number is in two faces",
"remedies": ["useDifferentValue", "correlateDeliberately"],
"sharedWith": [{ "profileId": "01P" }, { "profileId": "02P" }],
});
if let Some(c) = crosses {
row["crossesWorlds"] = json!(c);
}
row
}
fn parse(rows: Value) -> Vec<Finding> {
rows.as_array()
.unwrap()
.iter()
.map(Finding::from_wire)
.collect()
}
#[test]
fn a_finding_is_read_whole() {
let f = &parse(json!([finding("01A", Some(true), "high")]))[0];
assert_eq!(f.attribute_id.as_deref(), Some("01A"));
assert_eq!(f.severity, Severity::High);
assert_eq!(f.shared_with, 2);
assert_eq!(
f.remedies,
vec![Remedy::UseDifferentValue, Remedy::CorrelateDeliberately]
);
assert!(f.crosses_a_world());
}
#[test]
fn an_absent_answer_is_not_a_negative_one() {
let f = &parse(json!([finding("01A", None, "low")]))[0];
assert_eq!(f.crosses_worlds, None);
assert!(!f.crosses_a_world());
let explicit = &parse(json!([finding("01A", Some(false), "low")]))[0];
assert_eq!(explicit.crosses_worlds, Some(false));
assert!(!explicit.crosses_a_world());
}
#[test]
fn the_two_axes_are_not_read_off_each_other() {
let inside = &parse(json!([finding("01A", Some(false), "high")]))[0];
let across = &parse(json!([finding("01A", Some(true), "low")]))[0];
assert!(!inside.crosses_a_world());
assert!(across.crosses_a_world());
assert!(inside.headline().contains("same signature"));
assert!(across.headline().contains("These two worlds"));
}
#[test]
fn an_unknown_remedy_is_dropped() {
let mut row = finding("01A", Some(true), "low");
row["remedies"] = json!(["useDifferentValue", "summonALawyer"]);
let f = &parse(json!([row]))[0];
assert_eq!(f.remedies, vec![Remedy::UseDifferentValue]);
}
#[test]
fn the_cross_world_finding_is_the_one_shown() {
let findings = parse(json!([
finding("01A", Some(false), "high"),
finding("01A", Some(true), "low"),
finding("02A", Some(false), "low"),
]));
let chosen = for_attribute(&findings, "01A").expect("a finding for 01A");
assert!(chosen.crosses_a_world());
assert!(!for_attribute(&findings, "02A").unwrap().crosses_a_world());
assert!(for_attribute(&findings, "03A").is_none());
}
}