use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use crate::errors::PluginError;
pub const RESERVED_PLUGIN_IDS: &[&str] = &["builtin", "apoc-core", "custom", "user.legacy"];
#[must_use]
pub fn is_reserved_plugin_id(id: &str) -> bool {
RESERVED_PLUGIN_IDS.contains(&id)
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct QName {
namespace: SmolStr,
local: SmolStr,
}
impl QName {
pub const BUILTIN_NS: &'static str = "builtin";
#[must_use]
pub fn new(namespace: impl Into<SmolStr>, local: impl Into<SmolStr>) -> Self {
let namespace = namespace.into();
let local = local.into();
assert!(!namespace.is_empty(), "QName namespace must not be empty");
assert!(!local.is_empty(), "QName local must not be empty");
Self { namespace, local }
}
#[must_use]
pub fn builtin(local: impl Into<SmolStr>) -> Self {
Self::new(Self::BUILTIN_NS, local)
}
pub fn parse(s: impl AsRef<str>) -> Result<Self, PluginError> {
let s = s.as_ref();
let (ns, local) = s
.rsplit_once('.')
.ok_or_else(|| PluginError::InvalidQName(s.to_owned()))?;
if ns.is_empty() || local.is_empty() {
return Err(PluginError::InvalidQName(s.to_owned()));
}
Ok(Self {
namespace: SmolStr::new(ns),
local: SmolStr::new(local),
})
}
#[must_use]
pub fn namespace(&self) -> &str {
&self.namespace
}
#[must_use]
pub fn local(&self) -> &str {
&self.local
}
#[must_use]
pub fn is_builtin(&self) -> bool {
self.namespace == Self::BUILTIN_NS
}
#[must_use]
pub fn matches_cypher(&self, other: &Self) -> bool {
self.namespace.eq_ignore_ascii_case(&other.namespace)
&& self.local.eq_ignore_ascii_case(&other.local)
}
}
impl fmt::Display for QName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.namespace, self.local)
}
}
impl FromStr for QName {
type Err = PluginError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() {
let q = QName::parse("foo.bar").unwrap();
assert_eq!(q.namespace(), "foo");
assert_eq!(q.local(), "bar");
}
#[test]
fn parse_nested_namespace() {
let q = QName::parse("ai.dragonscale.geo.haversine").unwrap();
assert_eq!(q.namespace(), "ai.dragonscale.geo");
assert_eq!(q.local(), "haversine");
}
#[test]
fn parse_rejects_empty_local() {
assert!(matches!(
QName::parse("foo."),
Err(PluginError::InvalidQName(_))
));
}
#[test]
fn parse_rejects_empty_namespace() {
assert!(matches!(
QName::parse(".bar"),
Err(PluginError::InvalidQName(_))
));
}
#[test]
fn parse_rejects_no_dot() {
assert!(matches!(
QName::parse("nodothere"),
Err(PluginError::InvalidQName(_))
));
}
#[test]
fn builtin_helper() {
let q = QName::builtin("MIN");
assert!(q.is_builtin());
assert_eq!(q.local(), "MIN");
}
#[test]
fn cypher_match_case_insensitive() {
let a = QName::builtin("toUpper");
let b = QName::builtin("TOUPPER");
assert!(a.matches_cypher(&b));
assert_ne!(a, b);
}
#[test]
fn display_round_trip() {
let q = QName::new("foo.bar", "baz");
assert_eq!(q.to_string(), "foo.bar.baz");
let parsed: QName = "foo.bar.baz".parse().unwrap();
assert_eq!(q, parsed);
}
}