use std::collections::BTreeSet;
use std::iter::FromIterator;
use crate::payload::Payload;
use crate::specs::trust_task_discovery::v0_1 as wire;
use crate::type_uri::TypeUri;
pub fn match_slug(pattern: &str, slug: &str) -> bool {
if pattern == "*" {
return true;
}
if let Some(prefix) = pattern.strip_suffix("/*") {
if prefix.is_empty() {
return false;
}
let mut full_prefix = String::with_capacity(prefix.len() + 1);
full_prefix.push_str(prefix);
full_prefix.push('/');
return slug.starts_with(&full_prefix);
}
pattern == slug
}
pub fn query_matches<S: AsRef<str>>(patterns: &[S], slug: &str) -> bool {
if patterns.is_empty() {
return true;
}
patterns.iter().any(|p| match_slug(p.as_ref(), slug))
}
#[derive(Debug, Clone, Default)]
pub struct DiscoveryRegistry {
type_uris: BTreeSet<String>,
}
impl DiscoveryRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn with<P: Payload>(self) -> Self {
let uri = P::type_uri();
self.with_type_uri(uri)
}
pub fn with_type_uri(mut self, uri: TypeUri) -> Self {
self.type_uris.insert(uri.bare().to_string());
self
}
pub fn register(&mut self, uri: TypeUri) {
self.type_uris.insert(uri.bare().to_string());
}
pub fn register_payload<P: Payload>(&mut self) {
self.register(P::type_uri());
}
pub fn register_str(&mut self, uri: impl Into<String>) {
self.type_uris.insert(uri.into());
}
pub fn with_str(mut self, uri: impl Into<String>) -> Self {
self.register_str(uri);
self
}
pub fn supported_types(&self) -> Vec<&str> {
self.type_uris.iter().map(String::as_str).collect()
}
pub fn respond_to(&self, query: &wire::Payload) -> wire::Response {
let patterns: Vec<&str> = query.patterns.iter().map(|p| p.as_str()).collect();
let supported_types: Vec<String> = self
.type_uris
.iter()
.filter(|uri| match parse_slug(uri) {
Some(slug) => query_matches(&patterns, slug),
None => false,
})
.cloned()
.collect();
wire::Response { supported_types }
}
}
impl<S: Into<String>> FromIterator<S> for DiscoveryRegistry {
fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
let mut registry = Self::new();
for uri in iter {
registry.register_str(uri);
}
registry
}
}
fn parse_slug(uri: &str) -> Option<&str> {
let after_spec = uri.split_once("/spec/")?.1;
let last_slash = after_spec.rfind('/')?;
Some(&after_spec[..last_slash])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn star_matches_anything() {
assert!(match_slug("*", "acl/grant"));
assert!(match_slug("*", "kyc-handoff"));
assert!(match_slug("*", "trust-task-discovery"));
}
#[test]
fn prefix_wildcard_matches_descendants() {
assert!(match_slug("acl/*", "acl/grant"));
assert!(match_slug("acl/*", "acl/revoke"));
assert!(match_slug("acl/*", "acl/grant/sub"));
}
#[test]
fn prefix_wildcard_does_not_match_bare_prefix() {
assert!(!match_slug("acl/*", "acl"));
assert!(!match_slug("acl/*", "aclx"));
assert!(!match_slug("acl/*", "kyc-handoff"));
}
#[test]
fn exact_pattern_matches_exact_slug_only() {
assert!(match_slug("kyc-handoff", "kyc-handoff"));
assert!(!match_slug("kyc-handoff", "kyc-handoff/v2"));
assert!(!match_slug("kyc-handoff", "kyc"));
}
#[test]
fn interior_wildcards_are_treated_literally() {
assert!(!match_slug("acl/*/grant", "acl/grant"));
assert!(!match_slug("a*b", "ab"));
}
#[test]
fn empty_patterns_match_everything() {
let patterns: &[&str] = &[];
assert!(query_matches(patterns, "acl/grant"));
assert!(query_matches(patterns, "anything"));
}
#[test]
fn or_semantics_across_patterns() {
let patterns = ["acl/*", "kyc-handoff"];
assert!(query_matches(&patterns, "acl/grant"));
assert!(query_matches(&patterns, "kyc-handoff"));
assert!(!query_matches(&patterns, "consent/give"));
}
#[test]
fn registry_dedupes_and_sorts() {
let registry = DiscoveryRegistry::new()
.with_type_uri(TypeUri::canonical("acl/revoke", 0, 1).unwrap())
.with_type_uri(TypeUri::canonical("acl/grant", 0, 1).unwrap())
.with_type_uri(TypeUri::canonical("acl/grant", 0, 1).unwrap());
let types = registry.supported_types();
assert_eq!(
types,
vec![
"https://trusttasks.org/spec/acl/grant/0.1",
"https://trusttasks.org/spec/acl/revoke/0.1",
]
);
}
#[test]
fn registry_responds_to_query_with_filtered_subset() {
use crate::specs::acl::{change_role, grant, list, revoke, show};
let registry = DiscoveryRegistry::new()
.with::<grant::v0_1::Payload>()
.with::<revoke::v0_1::Payload>()
.with::<show::v0_1::Payload>()
.with::<list::v0_1::Payload>()
.with::<change_role::v0_1::Payload>();
let acl_only = wire::Payload {
patterns: vec!["acl/*".parse().unwrap()],
};
let response = registry.respond_to(&acl_only);
assert_eq!(response.supported_types.len(), 5);
let only_grant = wire::Payload {
patterns: vec!["acl/grant".parse().unwrap()],
};
let response = registry.respond_to(&only_grant);
assert_eq!(
response.supported_types,
vec!["https://trusttasks.org/spec/acl/grant/0.1"]
);
let everything = wire::Payload { patterns: vec![] };
let response = registry.respond_to(&everything);
assert_eq!(response.supported_types.len(), 5);
let nothing = wire::Payload {
patterns: vec!["does-not-exist/*".parse().unwrap()],
};
let response = registry.respond_to(¬hing);
assert!(response.supported_types.is_empty());
}
}