use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
mod hold_serde {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(negated: &bool, ser: S) -> Result<S::Ok, S::Error> {
debug_assert!(*negated);
ser.serialize_str("never")
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<bool, D::Error> {
let s: Option<String> = Option::deserialize(d)?;
Ok(s.as_deref() == Some("never"))
}
}
fn is_not_negated(negated: &bool) -> bool {
!*negated
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SpytialDecorators {
pub constraints: Vec<Constraint>,
pub directives: Vec<Directive>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Constraint {
Orientation(OrientationConstraint),
Align(AlignConstraint),
Cyclic(CyclicConstraint),
Group(GroupConstraint),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Directive {
AtomStyle(AtomStyleDirective),
Size(SizeDirective),
Icon(IconDirective),
EdgeStyle(EdgeStyleDirective),
Projection(ProjectionDirective),
Attribute(AttributeDirective),
HideField(HideFieldDirective),
HideAtom(HideAtomDirective),
InferredEdge(InferredEdgeDirective),
Tag(TagDirective),
Flag(FlagDirective),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OrientationConstraint {
pub orientation: OrientationParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct OrientationParams {
pub selector: String,
pub directions: Vec<String>,
#[serde(
rename = "hold",
default,
skip_serializing_if = "is_not_negated",
serialize_with = "hold_serde::serialize",
deserialize_with = "hold_serde::deserialize"
)]
pub negated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AlignConstraint {
pub align: AlignParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AlignParams {
pub selector: String,
pub direction: String,
#[serde(
rename = "hold",
default,
skip_serializing_if = "is_not_negated",
serialize_with = "hold_serde::serialize",
deserialize_with = "hold_serde::deserialize"
)]
pub negated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CyclicConstraint {
pub cyclic: CyclicParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CyclicParams {
pub selector: String,
pub direction: String,
#[serde(
rename = "hold",
default,
skip_serializing_if = "is_not_negated",
serialize_with = "hold_serde::serialize",
deserialize_with = "hold_serde::deserialize"
)]
pub negated: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GroupConstraint {
pub group: GroupParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum GroupParams {
FieldBased {
field: String,
#[serde(rename = "groupOn")]
group_on: u32,
#[serde(rename = "addToGroup")]
add_to_group: u32,
#[serde(skip_serializing_if = "Option::is_none")]
selector: Option<String>,
#[serde(
rename = "hold",
default,
skip_serializing_if = "is_not_negated",
serialize_with = "hold_serde::serialize",
deserialize_with = "hold_serde::deserialize"
)]
negated: bool,
},
SelectorBased {
selector: String,
name: String,
#[serde(rename = "addEdge", default, skip_serializing_if = "Option::is_none")]
add_edge: Option<GroupEdgeValue>,
#[serde(rename = "textStyle", default, skip_serializing_if = "Option::is_none")]
text_style: Option<TextStyle>,
#[serde(
rename = "hold",
default,
skip_serializing_if = "is_not_negated",
serialize_with = "hold_serde::serialize",
deserialize_with = "hold_serde::deserialize"
)]
negated: bool,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LinePattern {
Solid,
Dashed,
Dotted,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TextSize {
Small,
Normal,
Large,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum GroupEdgePoints {
None,
Togroup,
Fromgroup,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct LineStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<LinePattern>,
#[serde(skip_serializing_if = "Option::is_none")]
pub weight: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub highlight: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct TextStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<TextSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct BorderStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct FillStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GroupEdge {
pub points: GroupEdgePoints,
#[serde(rename = "lineStyle", skip_serializing_if = "Option::is_none")]
pub line_style: Option<LineStyle>,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum GroupEdgeValue {
Direction(GroupEdgePoints),
Block(GroupEdge),
}
pub(crate) fn normalize_legacy_pattern(style: &str) -> Option<LinePattern> {
match style.trim().to_ascii_lowercase().as_str() {
"solid" => Some(LinePattern::Solid),
"dashed" => Some(LinePattern::Dashed),
"dotted" => Some(LinePattern::Dotted),
_ => {
eprintln!(
"spytial: ignoring invalid edge style {style:?} (expected solid/dashed/dotted); the edge falls back to the default pattern"
);
None
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AtomStyleDirective {
#[serde(rename = "atomStyle")]
pub atom_style: AtomStyleParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct AtomStyleParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(rename = "fillStyle", skip_serializing_if = "Option::is_none")]
pub fill_style: Option<FillStyle>,
#[serde(rename = "borderStyle", skip_serializing_if = "Option::is_none")]
pub border_style: Option<BorderStyle>,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SizeDirective {
pub size: SizeParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SizeParams {
pub selector: String,
pub height: u32,
pub width: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IconDirective {
pub icon: IconParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct IconParams {
pub selector: String,
pub path: String,
#[serde(rename = "showLabels")]
pub show_labels: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EdgeStyleDirective {
#[serde(rename = "edgeStyle")]
pub edge_style: EdgeStyleParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct EdgeStyleParams {
pub field: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(rename = "lineStyle", skip_serializing_if = "Option::is_none")]
pub line_style: Option<LineStyle>,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
#[serde(skip_serializing_if = "Option::is_none", rename = "showLabel")]
pub show_label: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectionDirective {
pub projection: ProjectionParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectionParams {
pub sig: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AttributeDirective {
pub attribute: AttributeParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AttributeParams {
pub field: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HideFieldDirective {
#[serde(rename = "hideField")]
pub hide_field: HideFieldParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HideFieldParams {
pub field: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub selector: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HideAtomDirective {
#[serde(rename = "hideAtom")]
pub hide_atom: HideAtomParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HideAtomParams {
pub selector: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InferredEdgeDirective {
#[serde(rename = "inferredEdge")]
pub inferred_edge: InferredEdgeParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InferredEdgeParams {
pub name: String,
pub selector: String,
#[serde(rename = "lineStyle", skip_serializing_if = "Option::is_none")]
pub line_style: Option<LineStyle>,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TagDirective {
pub tag: TagParams,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TagParams {
#[serde(rename = "toTag")]
pub to_tag: String,
pub name: String,
pub value: String,
#[serde(rename = "textStyle", skip_serializing_if = "Option::is_none")]
pub text_style: Option<TextStyle>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlagDirective {
pub flag: String,
}
pub trait HasSpytialDecorators {
fn decorators() -> SpytialDecorators;
}
impl<T: HasSpytialDecorators + ?Sized> HasSpytialDecorators for &T {
fn decorators() -> SpytialDecorators {
T::decorators()
}
}
pub struct DecoProbe<T>(
pub ::std::marker::PhantomData<T>,
);
impl<T: HasSpytialDecorators> DecoProbe<T> {
pub fn get(self) -> SpytialDecorators {
T::decorators()
}
}
pub trait DefaultDecorators {
fn get(self) -> SpytialDecorators;
}
impl<T> DefaultDecorators for DecoProbe<T> {
fn get(self) -> SpytialDecorators {
SpytialDecorators::default()
}
}
static TYPE_REGISTRY: LazyLock<Mutex<HashMap<String, SpytialDecorators>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
pub fn register_type_decorators(type_name: &str, decorators: SpytialDecorators) {
let mut registry = TYPE_REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry.insert(type_name.to_string(), decorators);
}
pub fn get_type_decorators(type_name: &str) -> Option<SpytialDecorators> {
let registry = TYPE_REGISTRY
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
registry.get(type_name).cloned()
}
pub fn to_yaml(decorators: &SpytialDecorators) -> Result<String, serde_yaml_ng::Error> {
serde_yaml_ng::to_string(decorators)
}
#[derive(Debug)]
pub struct SpytialDecoratorsBuilder {
constraints: Vec<Constraint>,
directives: Vec<Directive>,
}
impl Default for SpytialDecoratorsBuilder {
fn default() -> Self {
Self::new()
}
}
impl SpytialDecoratorsBuilder {
pub fn new() -> Self {
Self {
constraints: Vec::new(),
directives: Vec::new(),
}
}
pub fn orientation(mut self, selector: &str, directions: Vec<&str>, negated: bool) -> Self {
self.constraints
.push(Constraint::Orientation(OrientationConstraint {
orientation: OrientationParams {
selector: selector.to_string(),
directions: directions.iter().map(|s| s.to_string()).collect(),
negated,
},
}));
self
}
pub fn align(mut self, selector: &str, direction: &str, negated: bool) -> Self {
self.constraints.push(Constraint::Align(AlignConstraint {
align: AlignParams {
selector: selector.to_string(),
direction: direction.to_string(),
negated,
},
}));
self
}
pub fn cyclic(mut self, selector: &str, direction: &str, negated: bool) -> Self {
self.constraints.push(Constraint::Cyclic(CyclicConstraint {
cyclic: CyclicParams {
selector: selector.to_string(),
direction: direction.to_string(),
negated,
},
}));
self
}
pub fn group_field_based(
mut self,
field: &str,
group_on: u32,
add_to_group: u32,
selector: Option<&str>,
negated: bool,
) -> Self {
self.constraints.push(Constraint::Group(GroupConstraint {
group: GroupParams::FieldBased {
field: field.to_string(),
group_on,
add_to_group,
selector: selector.map(|s| s.to_string()),
negated,
},
}));
self
}
pub fn group_selector_based(self, selector: &str, name: &str, negated: bool) -> Self {
self.group_selector_based_styled(selector, name, None, None, negated)
}
pub fn group_selector_based_styled(
mut self,
selector: &str,
name: &str,
add_edge: Option<GroupEdgeValue>,
text_style: Option<TextStyle>,
negated: bool,
) -> Self {
self.constraints.push(Constraint::Group(GroupConstraint {
group: GroupParams::SelectorBased {
selector: selector.to_string(),
name: name.to_string(),
add_edge,
text_style,
negated,
},
}));
self
}
pub fn atom_style(
mut self,
selector: Option<&str>,
fill_style: Option<FillStyle>,
border_style: Option<BorderStyle>,
text_style: Option<TextStyle>,
) -> Self {
self.directives
.push(Directive::AtomStyle(AtomStyleDirective {
atom_style: AtomStyleParams {
selector: selector.map(|s| s.to_string()),
fill_style,
border_style,
text_style,
},
}));
self
}
pub fn atom_color(self, selector: &str, value: &str) -> Self {
self.atom_style(
Some(selector),
None,
Some(BorderStyle {
color: Some(value.to_string()),
width: None,
}),
None,
)
}
pub fn size(mut self, selector: &str, height: u32, width: u32) -> Self {
self.directives.push(Directive::Size(SizeDirective {
size: SizeParams {
selector: selector.to_string(),
height,
width,
},
}));
self
}
pub fn icon(mut self, selector: &str, path: &str, show_labels: bool) -> Self {
self.directives.push(Directive::Icon(IconDirective {
icon: IconParams {
selector: selector.to_string(),
path: path.to_string(),
show_labels,
},
}));
self
}
#[allow(clippy::too_many_arguments)]
pub fn edge_style(
mut self,
field: &str,
selector: Option<&str>,
filter: Option<&str>,
line_style: Option<LineStyle>,
text_style: Option<TextStyle>,
show_label: Option<bool>,
hidden: Option<bool>,
) -> Self {
self.directives
.push(Directive::EdgeStyle(EdgeStyleDirective {
edge_style: EdgeStyleParams {
field: field.to_string(),
selector: selector.map(|s| s.to_string()),
filter: filter.map(|s| s.to_string()),
line_style,
text_style,
show_label,
hidden,
},
}));
self
}
#[allow(clippy::too_many_arguments)]
pub fn edge_color(
self,
field: &str,
value: &str,
selector: Option<&str>,
filter: Option<&str>,
style: Option<&str>,
weight: Option<f64>,
show_label: Option<bool>,
hidden: Option<bool>,
) -> Self {
let line_style = LineStyle {
color: Some(value.to_string()),
pattern: style.and_then(normalize_legacy_pattern),
weight: weight.filter(|w| {
let ok = w.is_finite() && *w > 0.0;
if !ok {
eprintln!("spytial: ignoring invalid edge weight {w:?}; the edge falls back to the default thickness");
}
ok
}),
highlight: None,
};
self.edge_style(
field,
selector,
filter,
Some(line_style),
None,
show_label,
hidden,
)
}
pub fn projection(mut self, sig: &str) -> Self {
self.directives
.push(Directive::Projection(ProjectionDirective {
projection: ProjectionParams {
sig: sig.to_string(),
},
}));
self
}
pub fn attribute(self, field: &str, selector: Option<&str>) -> Self {
self.attribute_styled(field, selector, None)
}
pub fn attribute_styled(
mut self,
field: &str,
selector: Option<&str>,
text_style: Option<TextStyle>,
) -> Self {
self.directives
.push(Directive::Attribute(AttributeDirective {
attribute: AttributeParams {
field: field.to_string(),
selector: selector.map(|s| s.to_string()),
text_style,
},
}));
self
}
pub fn hide_field(mut self, field: &str, selector: Option<&str>) -> Self {
self.directives
.push(Directive::HideField(HideFieldDirective {
hide_field: HideFieldParams {
field: field.to_string(),
selector: selector.map(|s| s.to_string()),
},
}));
self
}
pub fn hide_atom(mut self, selector: &str) -> Self {
self.directives.push(Directive::HideAtom(HideAtomDirective {
hide_atom: HideAtomParams {
selector: selector.to_string(),
},
}));
self
}
pub fn inferred_edge(self, name: &str, selector: &str) -> Self {
self.inferred_edge_styled(name, selector, None, None)
}
pub fn inferred_edge_styled(
mut self,
name: &str,
selector: &str,
line_style: Option<LineStyle>,
text_style: Option<TextStyle>,
) -> Self {
self.directives
.push(Directive::InferredEdge(InferredEdgeDirective {
inferred_edge: InferredEdgeParams {
name: name.to_string(),
selector: selector.to_string(),
line_style,
text_style,
},
}));
self
}
pub fn flag(mut self, name: &str) -> Self {
self.directives.push(Directive::Flag(FlagDirective {
flag: name.to_string(),
}));
self
}
pub fn tag(self, to_tag: &str, name: &str, value: &str) -> Self {
self.tag_styled(to_tag, name, value, None)
}
pub fn tag_styled(
mut self,
to_tag: &str,
name: &str,
value: &str,
text_style: Option<TextStyle>,
) -> Self {
self.directives.push(Directive::Tag(TagDirective {
tag: TagParams {
to_tag: to_tag.to_string(),
name: name.to_string(),
value: value.to_string(),
text_style,
},
}));
self
}
pub fn include_decorators_from_type<T: HasSpytialDecorators>(mut self) -> Self {
let other_decorators = T::decorators();
self.constraints.extend(other_decorators.constraints);
self.directives.extend(other_decorators.directives);
self
}
pub fn extend_with(mut self, other: SpytialDecorators) -> Self {
self.constraints.extend(other.constraints);
self.directives.extend(other.directives);
self
}
pub fn build(self) -> SpytialDecorators {
SpytialDecorators {
constraints: self.constraints,
directives: self.directives,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spytial_decorators_default() {
let decorators = SpytialDecorators::default();
assert!(decorators.constraints.is_empty());
assert!(decorators.directives.is_empty());
}
#[test]
fn test_yaml_serialization() {
let decorators = SpytialDecorators {
constraints: vec![
Constraint::Orientation(OrientationConstraint {
orientation: OrientationParams {
selector: "value".to_string(),
directions: vec!["above".to_string()],
negated: false,
},
}),
Constraint::Align(AlignConstraint {
align: AlignParams {
selector: "siblings".to_string(),
direction: "horizontal".to_string(),
negated: false,
},
}),
],
directives: vec![Directive::Flag(FlagDirective {
flag: "test_flag".to_string(),
})],
};
let yaml = to_yaml(&decorators).unwrap();
assert!(yaml.contains("orientation"));
assert!(yaml.contains("align"));
assert!(yaml.contains("flag"));
}
}