mod seo_link;
use crate::{Result, SemanticError, SemanticVector};
pub use seo_link::SeoLinkPolicy;
use std::collections::BTreeSet;
use weavatrix_graph::{Edge, Graph, NodeId};
pub trait LinkPolicy {
fn id(&self) -> &str;
fn validate(&self, graph: &Graph, vectors: &[SemanticVector]) -> Result<()>;
fn allows(&self, source: &NodeId, target: &NodeId) -> bool;
fn annotate_edge(&self, edge: Edge, _source: &NodeId, _target: &NodeId) -> Result<Edge> {
Ok(edge.with_attribute("policy", self.id()))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AllowAllPolicy;
impl LinkPolicy for AllowAllPolicy {
fn id(&self) -> &'static str {
"allow_all"
}
fn validate(&self, _graph: &Graph, _vectors: &[SemanticVector]) -> Result<()> {
Ok(())
}
fn allows(&self, source: &NodeId, target: &NodeId) -> bool {
source != target
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeoPage {
node_id: NodeId,
site: String,
canonical: String,
language: Option<String>,
eligibility: SeoEligibility,
existing_targets: BTreeSet<NodeId>,
signals: SeoSignals,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SeoEligibility {
source: bool,
target: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct SeoSignals {
cornerstone: bool,
orphan: bool,
target_priority: u32,
}
impl SeoPage {
pub fn new(
node_id: NodeId,
site: impl Into<String>,
canonical: impl Into<String>,
) -> Result<Self> {
let site = site.into();
let canonical = canonical.into();
validate_page_text(&node_id, "site", &site)?;
validate_page_text(&node_id, "canonical", &canonical)?;
Ok(Self {
node_id,
site,
canonical,
language: None,
eligibility: SeoEligibility {
source: true,
target: true,
},
existing_targets: BTreeSet::new(),
signals: SeoSignals {
cornerstone: false,
orphan: false,
target_priority: 0,
},
})
}
#[must_use]
pub const fn node_id(&self) -> &NodeId {
&self.node_id
}
#[must_use]
pub fn site(&self) -> &str {
&self.site
}
#[must_use]
pub fn canonical(&self) -> &str {
&self.canonical
}
#[must_use]
pub fn language(&self) -> Option<&str> {
self.language.as_deref()
}
#[must_use]
pub const fn source_eligible(&self) -> bool {
self.eligibility.source
}
#[must_use]
pub const fn target_eligible(&self) -> bool {
self.eligibility.target
}
#[must_use]
pub const fn cornerstone(&self) -> bool {
self.signals.cornerstone
}
#[must_use]
pub const fn orphan(&self) -> bool {
self.signals.orphan
}
#[must_use]
pub const fn target_priority(&self) -> u32 {
self.signals.target_priority
}
pub fn with_language(mut self, language: impl Into<String>) -> Result<Self> {
let language = language.into();
if language.is_empty() {
return Err(SemanticError::EmptySeoLanguage {
node: self.node_id.to_string(),
});
}
if language.trim() != language {
return Err(SemanticError::SeoLanguageHasSurroundingWhitespace {
node: self.node_id.to_string(),
});
}
self.language = Some(language);
Ok(self)
}
#[must_use]
pub const fn with_source_eligible(mut self, eligible: bool) -> Self {
self.eligibility.source = eligible;
self
}
#[must_use]
pub const fn with_target_eligible(mut self, eligible: bool) -> Self {
self.eligibility.target = eligible;
self
}
#[must_use]
pub fn with_existing_target(mut self, target: NodeId) -> Self {
self.existing_targets.insert(target);
self
}
#[must_use]
pub const fn with_cornerstone(mut self, cornerstone: bool) -> Self {
self.signals.cornerstone = cornerstone;
self
}
#[must_use]
pub const fn with_orphan(mut self, orphan: bool) -> Self {
self.signals.orphan = orphan;
self
}
#[must_use]
pub const fn with_target_priority(mut self, priority: u32) -> Self {
self.signals.target_priority = priority;
self
}
}
fn validate_page_text(node: &NodeId, field: &str, value: &str) -> Result<()> {
if value.is_empty() {
return Err(match field {
"site" => SemanticError::EmptySeoSite {
node: node.to_string(),
},
_ => SemanticError::EmptySeoCanonical {
node: node.to_string(),
},
});
}
if value.trim() != value {
return Err(match field {
"site" => SemanticError::SeoSiteHasSurroundingWhitespace {
node: node.to_string(),
},
_ => SemanticError::SeoCanonicalHasSurroundingWhitespace {
node: node.to_string(),
},
});
}
Ok(())
}