use std::collections::HashSet;
use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use super::id::{ChainId, ChainIdFormatError};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ChainIdPattern {
Wildcard {
namespace: String,
},
Exact {
namespace: String,
reference: String,
},
Set {
namespace: String,
references: HashSet<String>,
},
}
impl ChainIdPattern {
pub fn wildcard<S: Into<String>>(namespace: S) -> Self {
Self::Wildcard {
namespace: namespace.into(),
}
}
pub fn exact<N: Into<String>, R: Into<String>>(namespace: N, reference: R) -> Self {
Self::Exact {
namespace: namespace.into(),
reference: reference.into(),
}
}
pub fn set<N: Into<String>>(namespace: N, references: HashSet<String>) -> Self {
Self::Set {
namespace: namespace.into(),
references,
}
}
#[must_use]
pub fn matches(&self, chain_id: &ChainId) -> bool {
match self {
Self::Wildcard { namespace } => chain_id.namespace() == namespace,
Self::Exact {
namespace,
reference,
} => chain_id.namespace() == namespace && chain_id.reference() == reference,
Self::Set {
namespace,
references,
} => chain_id.namespace() == namespace && references.contains(chain_id.reference()),
}
}
#[must_use]
pub fn namespace(&self) -> &str {
match self {
Self::Wildcard { namespace }
| Self::Exact { namespace, .. }
| Self::Set { namespace, .. } => namespace,
}
}
}
impl fmt::Display for ChainIdPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Wildcard { namespace } => write!(f, "{namespace}:*"),
Self::Exact {
namespace,
reference,
} => write!(f, "{namespace}:{reference}"),
Self::Set {
namespace,
references,
} => {
let refs: Vec<&str> = references.iter().map(AsRef::as_ref).collect();
write!(f, "{}:{{{}}}", namespace, refs.join(","))
}
}
}
}
impl FromStr for ChainIdPattern {
type Err = ChainIdFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (namespace, rest) = s
.split_once(':')
.ok_or_else(|| ChainIdFormatError(s.into()))?;
if namespace.is_empty() {
return Err(ChainIdFormatError(s.into()));
}
if rest == "*" {
return Ok(Self::wildcard(namespace));
}
if let Some(inner) = rest.strip_prefix('{').and_then(|r| r.strip_suffix('}')) {
let items: Vec<&str> = inner.split(',').map(str::trim).collect();
if items.is_empty() || items.iter().any(|item| item.is_empty()) {
return Err(ChainIdFormatError(s.into()));
}
let references = items.into_iter().map(Into::into).collect();
return Ok(Self::set(namespace, references));
}
if rest.is_empty() {
return Err(ChainIdFormatError(s.into()));
}
Ok(Self::exact(namespace, rest))
}
}
impl Serialize for ChainIdPattern {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ChainIdPattern {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::from_str(&s).map_err(de::Error::custom)
}
}
impl From<ChainId> for ChainIdPattern {
fn from(chain_id: ChainId) -> Self {
let (namespace, reference) = chain_id.into_parts();
Self::exact(namespace, reference)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wildcard_matches() {
let pattern = ChainIdPattern::wildcard("eip155");
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(pattern.matches(&ChainId::new("eip155", "8453")));
assert!(pattern.matches(&ChainId::new("eip155", "137")));
assert!(!pattern.matches(&ChainId::new("solana", "mainnet")));
}
#[test]
fn exact_matches() {
let pattern = ChainIdPattern::exact("eip155", "1");
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(!pattern.matches(&ChainId::new("eip155", "8453")));
assert!(!pattern.matches(&ChainId::new("solana", "1")));
}
#[test]
fn set_matches() {
let references: HashSet<String> =
["1", "8453", "137"].into_iter().map(String::from).collect();
let pattern = ChainIdPattern::set("eip155", references);
assert!(pattern.matches(&ChainId::new("eip155", "1")));
assert!(pattern.matches(&ChainId::new("eip155", "8453")));
assert!(pattern.matches(&ChainId::new("eip155", "137")));
assert!(!pattern.matches(&ChainId::new("eip155", "42")));
assert!(!pattern.matches(&ChainId::new("solana", "1")));
}
#[test]
fn namespace_accessor() {
let wildcard = ChainIdPattern::wildcard("eip155");
assert_eq!(wildcard.namespace(), "eip155");
let exact = ChainIdPattern::exact("solana", "mainnet");
assert_eq!(exact.namespace(), "solana");
let references: HashSet<String> = std::iter::once("1").map(String::from).collect();
let set = ChainIdPattern::set("eip155", references);
assert_eq!(set.namespace(), "eip155");
}
}