1use crate::{ConstraintComponentId, Shape, ShapeType, Target};
22use serde::{Deserialize, Serialize};
23use std::collections::{HashMap, HashSet};
24use std::fmt::Write;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ExportFormat {
29 Dot,
31 Mermaid,
33 JsonSchema,
35 Svg,
37 PlantUml,
39 D3Json,
41 CytoscapeJson,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct VisualizerConfig {
48 pub show_constraints: bool,
50 pub show_targets: bool,
52 pub show_metadata: bool,
54 pub color_scheme: ColorScheme,
56 pub shape_style: ShapeStyle,
58 pub property_style: PropertyStyle,
60 pub layout_direction: LayoutDirection,
62 pub max_label_length: usize,
64 pub use_prefixes: bool,
66 pub prefixes: HashMap<String, String>,
68}
69
70impl Default for VisualizerConfig {
71 fn default() -> Self {
72 let mut prefixes = HashMap::new();
73 prefixes.insert("sh".to_string(), "http://www.w3.org/ns/shacl#".to_string());
74 prefixes.insert(
75 "xsd".to_string(),
76 "http://www.w3.org/2001/XMLSchema#".to_string(),
77 );
78 prefixes.insert(
79 "rdf".to_string(),
80 "http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
81 );
82 prefixes.insert(
83 "rdfs".to_string(),
84 "http://www.w3.org/2000/01/rdf-schema#".to_string(),
85 );
86 prefixes.insert("foaf".to_string(), "http://xmlns.com/foaf/0.1/".to_string());
87 prefixes.insert("schema".to_string(), "http://schema.org/".to_string());
88 prefixes.insert("ex".to_string(), "http://example.org/".to_string());
89
90 Self {
91 show_constraints: true,
92 show_targets: true,
93 show_metadata: false,
94 color_scheme: ColorScheme::default(),
95 shape_style: ShapeStyle::default(),
96 property_style: PropertyStyle::default(),
97 layout_direction: LayoutDirection::TopToBottom,
98 max_label_length: 50,
99 use_prefixes: true,
100 prefixes,
101 }
102 }
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ColorScheme {
108 pub shape_fill: String,
110 pub shape_border: String,
112 pub property_fill: String,
114 pub property_border: String,
116 pub target_fill: String,
118 pub constraint_fill: String,
120 pub edge_color: String,
122 pub text_color: String,
124 pub required_color: String,
126}
127
128impl Default for ColorScheme {
129 fn default() -> Self {
130 Self {
131 shape_fill: "#e8f4fc".to_string(),
132 shape_border: "#3498db".to_string(),
133 property_fill: "#f9f9f9".to_string(),
134 property_border: "#7f8c8d".to_string(),
135 target_fill: "#d5f5e3".to_string(),
136 constraint_fill: "#fef9e7".to_string(),
137 edge_color: "#34495e".to_string(),
138 text_color: "#2c3e50".to_string(),
139 required_color: "#e74c3c".to_string(),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ShapeStyle {
147 pub shape: String,
149 pub font_size: u32,
151 pub font_family: String,
153 pub border_width: u32,
155}
156
157impl Default for ShapeStyle {
158 fn default() -> Self {
159 Self {
160 shape: "box".to_string(),
161 font_size: 12,
162 font_family: "Arial".to_string(),
163 border_width: 2,
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct PropertyStyle {
171 pub shape: String,
173 pub font_size: u32,
175 pub font_family: String,
177 pub border_width: u32,
179}
180
181impl Default for PropertyStyle {
182 fn default() -> Self {
183 Self {
184 shape: "record".to_string(),
185 font_size: 10,
186 font_family: "Arial".to_string(),
187 border_width: 1,
188 }
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194pub enum LayoutDirection {
195 TopToBottom,
196 LeftToRight,
197 BottomToTop,
198 RightToLeft,
199}
200
201impl LayoutDirection {
202 fn as_dot(self) -> &'static str {
203 match self {
204 LayoutDirection::TopToBottom => "TB",
205 LayoutDirection::LeftToRight => "LR",
206 LayoutDirection::BottomToTop => "BT",
207 LayoutDirection::RightToLeft => "RL",
208 }
209 }
210
211 fn as_mermaid(self) -> &'static str {
212 match self {
213 LayoutDirection::TopToBottom => "TD",
214 LayoutDirection::LeftToRight => "LR",
215 LayoutDirection::BottomToTop => "BT",
216 LayoutDirection::RightToLeft => "RL",
217 }
218 }
219}
220
221#[derive(Debug)]
223pub struct ShapeVisualizer {
224 config: VisualizerConfig,
225}
226
227impl ShapeVisualizer {
228 pub fn new() -> Self {
230 Self {
231 config: VisualizerConfig::default(),
232 }
233 }
234
235 pub fn with_config(config: VisualizerConfig) -> Self {
237 Self { config }
238 }
239
240 pub fn export(&self, shape: &Shape, format: ExportFormat) -> String {
242 self.export_multiple(std::slice::from_ref(shape), format)
243 }
244
245 pub fn export_multiple(&self, shapes: &[Shape], format: ExportFormat) -> String {
247 match format {
248 ExportFormat::Dot => self.to_dot(shapes),
249 ExportFormat::Mermaid => self.to_mermaid(shapes),
250 ExportFormat::JsonSchema => self.to_json_schema(shapes),
251 ExportFormat::Svg => self.to_svg(shapes),
252 ExportFormat::PlantUml => self.to_plantuml(shapes),
253 ExportFormat::D3Json => self.to_d3_json(shapes),
254 ExportFormat::CytoscapeJson => self.to_cytoscape_json(shapes),
255 }
256 }
257
258 pub fn to_dot(&self, shapes: &[Shape]) -> String {
260 let mut dot = String::new();
261
262 writeln!(dot, "digraph SHACL_Shapes {{").ok();
263 writeln!(dot, " rankdir={};", self.config.layout_direction.as_dot()).ok();
264 writeln!(
265 dot,
266 " node [fontname=\"{}\"];",
267 self.config.shape_style.font_family
268 )
269 .ok();
270 writeln!(
271 dot,
272 " edge [fontname=\"{}\"];",
273 self.config.shape_style.font_family
274 )
275 .ok();
276 writeln!(dot).ok();
277
278 writeln!(dot, " // Node shape styles").ok();
280 writeln!(
281 dot,
282 " node [shape={}, style=filled, fillcolor=\"{}\", color=\"{}\", fontsize={}];",
283 self.config.shape_style.shape,
284 self.config.color_scheme.shape_fill,
285 self.config.color_scheme.shape_border,
286 self.config.shape_style.font_size
287 )
288 .ok();
289 writeln!(dot).ok();
290
291 for shape in shapes {
292 self.shape_to_dot(&mut dot, shape);
293 }
294
295 let shape_ids: HashSet<_> = shapes.iter().map(|s| s.id.as_str()).collect();
297 for shape in shapes {
298 for (_, constraint) in &shape.constraints {
299 if let crate::constraints::Constraint::Node(node_constraint) = constraint {
301 let ref_id = node_constraint.shape.as_str();
302 if shape_ids.contains(ref_id) {
303 let from_id = self.sanitize_id(&shape.id.0);
304 let to_id = self.sanitize_id(ref_id);
305 writeln!(
306 dot,
307 " {} -> {} [label=\"sh:node\", style=dashed];",
308 from_id, to_id
309 )
310 .ok();
311 }
312 }
313 }
314 }
315
316 writeln!(dot, "}}").ok();
317 dot
318 }
319
320 fn shape_to_dot(&self, dot: &mut String, shape: &Shape) {
321 let shape_id = self.sanitize_id(&shape.id.0);
322 let label = self.compact_iri(&shape.id.0);
323
324 let shape_type_label = match shape.shape_type {
326 ShapeType::NodeShape => "NodeShape",
327 ShapeType::PropertyShape => "PropertyShape",
328 };
329
330 writeln!(dot, " // Shape: {}", label).ok();
331 writeln!(
332 dot,
333 " {} [label=\"{} ({})\\n{}\"];",
334 shape_id,
335 label,
336 shape_type_label,
337 shape.label.as_deref().unwrap_or("")
338 )
339 .ok();
340
341 if self.config.show_targets && !shape.targets.is_empty() {
343 for (idx, target) in shape.targets.iter().enumerate() {
344 let target_id = format!("{}_target_{}", shape_id, idx);
345 let target_label = self.target_label(target);
346
347 writeln!(
348 dot,
349 " {} [shape=ellipse, style=filled, fillcolor=\"{}\", label=\"{}\"];",
350 target_id, self.config.color_scheme.target_fill, target_label
351 )
352 .ok();
353 writeln!(dot, " {} -> {} [label=\"target\"];", shape_id, target_id).ok();
354 }
355 }
356
357 if self.config.show_constraints && !shape.constraints.is_empty() {
359 let constraints_id = format!("{}_constraints", shape_id);
360 let mut constraint_labels = Vec::new();
361
362 for (comp_id, constraint) in &shape.constraints {
363 let constraint_label = self.constraint_label(comp_id, constraint);
364 constraint_labels.push(constraint_label);
365 }
366
367 let constraints_label = constraint_labels.join("\\n");
368 writeln!(
369 dot,
370 " {} [shape=note, style=filled, fillcolor=\"{}\", label=\"{}\", fontsize={}];",
371 constraints_id,
372 self.config.color_scheme.constraint_fill,
373 constraints_label,
374 self.config.property_style.font_size
375 )
376 .ok();
377 writeln!(
378 dot,
379 " {} -> {} [style=dotted, label=\"constraints\"];",
380 shape_id, constraints_id
381 )
382 .ok();
383 }
384
385 writeln!(dot).ok();
386 }
387
388 pub fn to_mermaid(&self, shapes: &[Shape]) -> String {
390 let mut mermaid = String::new();
391
392 writeln!(
393 mermaid,
394 "flowchart {}",
395 self.config.layout_direction.as_mermaid()
396 )
397 .ok();
398
399 for shape in shapes {
400 self.shape_to_mermaid(&mut mermaid, shape);
401 }
402
403 mermaid
404 }
405
406 fn shape_to_mermaid(&self, mermaid: &mut String, shape: &Shape) {
407 let shape_id = self.sanitize_id(&shape.id.0);
408 let label = self.compact_iri(&shape.id.0);
409
410 let shape_type = match shape.shape_type {
411 ShapeType::NodeShape => "NodeShape",
412 ShapeType::PropertyShape => "PropertyShape",
413 };
414
415 writeln!(
417 mermaid,
418 " {}[\"<b>{}</b><br/><small>{}</small>\"]",
419 shape_id, label, shape_type
420 )
421 .ok();
422
423 writeln!(
425 mermaid,
426 " style {} fill:{},stroke:{},stroke-width:{}px",
427 shape_id,
428 self.config.color_scheme.shape_fill,
429 self.config.color_scheme.shape_border,
430 self.config.shape_style.border_width
431 )
432 .ok();
433
434 if self.config.show_targets {
436 for (idx, target) in shape.targets.iter().enumerate() {
437 let target_id = format!("{}_t{}", shape_id, idx);
438 let target_label = self.target_label(target);
439
440 writeln!(mermaid, " {}((\"{}\"))", target_id, target_label).ok();
441 writeln!(
442 mermaid,
443 " style {} fill:{},stroke:#27ae60",
444 target_id, self.config.color_scheme.target_fill
445 )
446 .ok();
447 writeln!(mermaid, " {} -->|target| {}", shape_id, target_id).ok();
448 }
449 }
450
451 if self.config.show_constraints && !shape.constraints.is_empty() {
453 let constraints_id = format!("{}_c", shape_id);
454 let mut labels = Vec::new();
455
456 for (comp_id, constraint) in &shape.constraints {
457 labels.push(self.constraint_label(comp_id, constraint));
458 }
459
460 writeln!(
461 mermaid,
462 " {}[\"{}\"]",
463 constraints_id,
464 labels.join("<br/>")
465 )
466 .ok();
467 writeln!(
468 mermaid,
469 " style {} fill:{},stroke:#f39c12",
470 constraints_id, self.config.color_scheme.constraint_fill
471 )
472 .ok();
473 writeln!(
474 mermaid,
475 " {} -.->|constraints| {}",
476 shape_id, constraints_id
477 )
478 .ok();
479 }
480 }
481
482 pub fn to_json_schema(&self, shapes: &[Shape]) -> String {
484 let mut schema = serde_json::json!({
485 "$schema": "https://json-schema.org/draft/2020-12/schema",
486 "$id": "shacl-shapes-schema",
487 "type": "object",
488 "title": "SHACL Shapes",
489 "definitions": {}
490 });
491
492 let definitions = schema["definitions"].as_object_mut().unwrap();
493
494 for shape in shapes {
495 let shape_schema = self.shape_to_json_schema(shape);
496 let shape_name = self.compact_iri(&shape.id.0).replace(':', "_");
497 definitions.insert(shape_name, shape_schema);
498 }
499
500 serde_json::to_string_pretty(&schema).unwrap_or_default()
501 }
502
503 fn shape_to_json_schema(&self, shape: &Shape) -> serde_json::Value {
504 let mut properties = serde_json::Map::new();
505 let mut required = Vec::new();
506
507 for (comp_id, constraint) in &shape.constraints {
508 if let Some((prop_name, prop_schema, is_required)) =
509 self.constraint_to_json_schema(comp_id, constraint)
510 {
511 properties.insert(prop_name.clone(), prop_schema);
512 if is_required {
513 required.push(prop_name);
514 }
515 }
516 }
517
518 serde_json::json!({
519 "type": "object",
520 "title": shape.label.as_deref().unwrap_or(&self.compact_iri(&shape.id.0)),
521 "description": shape.description.clone().unwrap_or_default(),
522 "properties": properties,
523 "required": required
524 })
525 }
526
527 fn constraint_to_json_schema(
528 &self,
529 comp_id: &ConstraintComponentId,
530 constraint: &crate::constraints::Constraint,
531 ) -> Option<(String, serde_json::Value, bool)> {
532 let comp_name = comp_id.as_str();
534
535 match constraint {
536 crate::constraints::Constraint::Datatype(dt) => {
537 let dt_iri = dt.datatype_iri.as_str();
538 let json_type = if dt_iri.contains("string") || dt_iri.contains("date") {
539 "string"
540 } else if dt_iri.contains("integer") {
541 "integer"
542 } else if dt_iri.contains("decimal") || dt_iri.contains("float") {
543 "number"
544 } else if dt_iri.contains("boolean") {
545 "boolean"
546 } else {
547 "string"
548 };
549
550 let mut schema = serde_json::json!({ "type": json_type });
551 if dt_iri.contains("date") {
552 schema["format"] = serde_json::json!("date");
553 }
554
555 Some((comp_name.to_string(), schema, false))
556 }
557 crate::constraints::Constraint::MinCount(mc) => Some((
558 comp_name.to_string(),
559 serde_json::json!({}),
560 mc.min_count >= 1,
561 )),
562 crate::constraints::Constraint::Pattern(pat) => Some((
563 comp_name.to_string(),
564 serde_json::json!({
565 "type": "string",
566 "pattern": pat.pattern
567 }),
568 false,
569 )),
570 crate::constraints::Constraint::MinLength(ml) => Some((
571 comp_name.to_string(),
572 serde_json::json!({
573 "type": "string",
574 "minLength": ml.min_length
575 }),
576 false,
577 )),
578 crate::constraints::Constraint::MaxLength(ml) => Some((
579 comp_name.to_string(),
580 serde_json::json!({
581 "type": "string",
582 "maxLength": ml.max_length
583 }),
584 false,
585 )),
586 _ => None,
587 }
588 }
589
590 pub fn to_svg(&self, shapes: &[Shape]) -> String {
592 let mut svg = String::new();
593
594 let shape_height = 80;
596 let shape_width = 200;
597 let padding = 20;
598 let total_height = shapes.len() * (shape_height + padding) + padding;
599 let total_width = shape_width + padding * 2;
600
601 writeln!(
602 svg,
603 r#"<?xml version="1.0" encoding="UTF-8"?>
604<svg xmlns="http://www.w3.org/2000/svg"
605 viewBox="0 0 {} {}"
606 width="{}" height="{}">"#,
607 total_width, total_height, total_width, total_height
608 )
609 .ok();
610
611 writeln!(
613 svg,
614 r#"<style>
615 .shape-rect {{ fill: {}; stroke: {}; stroke-width: {}; }}
616 .shape-text {{ font-family: {}; font-size: {}px; fill: {}; }}
617 .constraint-text {{ font-family: {}; font-size: {}px; fill: #666; }}
618</style>"#,
619 self.config.color_scheme.shape_fill,
620 self.config.color_scheme.shape_border,
621 self.config.shape_style.border_width,
622 self.config.shape_style.font_family,
623 self.config.shape_style.font_size,
624 self.config.color_scheme.text_color,
625 self.config.property_style.font_family,
626 self.config.property_style.font_size
627 )
628 .ok();
629
630 for (idx, shape) in shapes.iter().enumerate() {
632 let y = padding + idx * (shape_height + padding);
633 self.shape_to_svg(&mut svg, shape, padding, y, shape_width, shape_height);
634 }
635
636 writeln!(svg, "</svg>").ok();
637 svg
638 }
639
640 fn shape_to_svg(
641 &self,
642 svg: &mut String,
643 shape: &Shape,
644 x: usize,
645 y: usize,
646 width: usize,
647 height: usize,
648 ) {
649 let label = self.compact_iri(&shape.id.0);
650 let shape_type = match shape.shape_type {
651 ShapeType::NodeShape => "NodeShape",
652 ShapeType::PropertyShape => "PropertyShape",
653 };
654
655 writeln!(
657 svg,
658 r#" <rect class="shape-rect" x="{}" y="{}" width="{}" height="{}" rx="5" />"#,
659 x, y, width, height
660 )
661 .ok();
662
663 writeln!(
665 svg,
666 r#" <text class="shape-text" x="{}" y="{}" text-anchor="middle">{}</text>"#,
667 x + width / 2,
668 y + 25,
669 label
670 )
671 .ok();
672
673 writeln!(
675 svg,
676 r#" <text class="constraint-text" x="{}" y="{}" text-anchor="middle">({})</text>"#,
677 x + width / 2,
678 y + 45,
679 shape_type
680 )
681 .ok();
682
683 if !shape.constraints.is_empty() {
685 writeln!(
686 svg,
687 r#" <text class="constraint-text" x="{}" y="{}" text-anchor="middle">{} constraints</text>"#,
688 x + width / 2,
689 y + 65,
690 shape.constraints.len()
691 )
692 .ok();
693 }
694 }
695
696 pub fn to_plantuml(&self, shapes: &[Shape]) -> String {
698 let mut uml = String::new();
699
700 writeln!(uml, "@startuml").ok();
701 writeln!(uml, "skinparam class {{").ok();
702 writeln!(
703 uml,
704 " BackgroundColor {}",
705 self.config.color_scheme.shape_fill
706 )
707 .ok();
708 writeln!(
709 uml,
710 " BorderColor {}",
711 self.config.color_scheme.shape_border
712 )
713 .ok();
714 writeln!(uml, "}}").ok();
715 writeln!(uml).ok();
716
717 for shape in shapes {
718 let label = self.compact_iri(&shape.id.0).replace(':', "_");
719 let shape_type = match shape.shape_type {
720 ShapeType::NodeShape => "<<NodeShape>>",
721 ShapeType::PropertyShape => "<<PropertyShape>>",
722 };
723
724 writeln!(uml, "class \"{}\" {} {{", label, shape_type).ok();
725
726 for (comp_id, constraint) in &shape.constraints {
727 let constraint_label = self.constraint_label(comp_id, constraint);
728 writeln!(uml, " + {}", constraint_label).ok();
729 }
730
731 writeln!(uml, "}}").ok();
732 writeln!(uml).ok();
733 }
734
735 writeln!(uml, "@enduml").ok();
736 uml
737 }
738
739 pub fn to_d3_json(&self, shapes: &[Shape]) -> String {
741 let mut nodes = Vec::new();
742 let mut links = Vec::new();
743
744 for shape in shapes {
745 let node = serde_json::json!({
746 "id": shape.id.as_str(),
747 "label": self.compact_iri(&shape.id.0),
748 "type": match shape.shape_type {
749 ShapeType::NodeShape => "node",
750 ShapeType::PropertyShape => "property"
751 },
752 "constraints": shape.constraints.len(),
753 "targets": shape.targets.len()
754 });
755 nodes.push(node);
756
757 for (_, constraint) in &shape.constraints {
759 if let crate::constraints::Constraint::Node(nc) = constraint {
760 links.push(serde_json::json!({
761 "source": shape.id.as_str(),
762 "target": nc.shape.as_str(),
763 "type": "sh:node"
764 }));
765 }
766 }
767 }
768
769 let result = serde_json::json!({
770 "nodes": nodes,
771 "links": links
772 });
773
774 serde_json::to_string_pretty(&result).unwrap_or_default()
775 }
776
777 pub fn to_cytoscape_json(&self, shapes: &[Shape]) -> String {
779 let mut elements = Vec::new();
780
781 for shape in shapes {
782 let node = serde_json::json!({
784 "data": {
785 "id": shape.id.as_str(),
786 "label": self.compact_iri(&shape.id.0),
787 "type": match shape.shape_type {
788 ShapeType::NodeShape => "nodeShape",
789 ShapeType::PropertyShape => "propertyShape"
790 }
791 },
792 "classes": match shape.shape_type {
793 ShapeType::NodeShape => "nodeShape",
794 ShapeType::PropertyShape => "propertyShape"
795 }
796 });
797 elements.push(node);
798
799 for (_, constraint) in &shape.constraints {
801 if let crate::constraints::Constraint::Node(nc) = constraint {
802 let edge = serde_json::json!({
803 "data": {
804 "source": shape.id.as_str(),
805 "target": nc.shape.as_str(),
806 "label": "sh:node"
807 },
808 "classes": "reference"
809 });
810 elements.push(edge);
811 }
812 }
813 }
814
815 let result = serde_json::json!({
816 "elements": elements,
817 "style": [
818 {
819 "selector": "node.nodeShape",
820 "style": {
821 "background-color": self.config.color_scheme.shape_fill,
822 "border-color": self.config.color_scheme.shape_border,
823 "label": "data(label)"
824 }
825 },
826 {
827 "selector": "node.propertyShape",
828 "style": {
829 "background-color": self.config.color_scheme.property_fill,
830 "border-color": self.config.color_scheme.property_border,
831 "shape": "rectangle",
832 "label": "data(label)"
833 }
834 },
835 {
836 "selector": "edge",
837 "style": {
838 "line-color": self.config.color_scheme.edge_color,
839 "target-arrow-color": self.config.color_scheme.edge_color,
840 "target-arrow-shape": "triangle",
841 "label": "data(label)"
842 }
843 }
844 ]
845 });
846
847 serde_json::to_string_pretty(&result).unwrap_or_default()
848 }
849
850 pub fn to_editor_protocol(&self, shapes: &[Shape]) -> String {
852 let editor_shapes: Vec<_> = shapes
853 .iter()
854 .map(|s| self.shape_to_editor_json(s))
855 .collect();
856
857 let result = serde_json::json!({
858 "version": "1.0",
859 "protocol": "shacl-editor",
860 "shapes": editor_shapes,
861 "config": {
862 "prefixes": self.config.prefixes,
863 "colorScheme": self.config.color_scheme
864 }
865 });
866
867 serde_json::to_string_pretty(&result).unwrap_or_default()
868 }
869
870 fn shape_to_editor_json(&self, shape: &Shape) -> serde_json::Value {
871 let targets: Vec<_> = shape
872 .targets
873 .iter()
874 .map(|t| self.target_to_editor_json(t))
875 .collect();
876
877 let constraints: Vec<_> = shape
878 .constraints
879 .iter()
880 .map(|(comp_id, c)| self.constraint_to_editor_json(comp_id, c))
881 .collect();
882
883 serde_json::json!({
884 "id": shape.id.as_str(),
885 "label": self.compact_iri(&shape.id.0),
886 "humanLabel": shape.label,
887 "description": shape.description,
888 "type": match shape.shape_type {
889 ShapeType::NodeShape => "NodeShape",
890 ShapeType::PropertyShape => "PropertyShape"
891 },
892 "targets": targets,
893 "constraints": constraints,
894 "severity": format!("{:?}", shape.severity),
895 "deactivated": shape.deactivated,
896 "metadata": {
897 "groups": shape.groups,
898 "order": shape.order,
899 "extends": shape.extends.iter().map(|s| s.as_str()).collect::<Vec<_>>()
900 }
901 })
902 }
903
904 fn target_to_editor_json(&self, target: &Target) -> serde_json::Value {
905 match target {
906 Target::Class(node) => serde_json::json!({
907 "type": "class",
908 "value": node.as_str(),
909 "label": self.compact_iri(node.as_str())
910 }),
911 Target::Node(node) => serde_json::json!({
912 "type": "node",
913 "value": node.to_string(),
914 "label": self.compact_iri(&node.to_string())
915 }),
916 Target::SubjectsOf(node) => serde_json::json!({
917 "type": "subjectsOf",
918 "value": node.as_str(),
919 "label": self.compact_iri(node.as_str())
920 }),
921 Target::ObjectsOf(node) => serde_json::json!({
922 "type": "objectsOf",
923 "value": node.as_str(),
924 "label": self.compact_iri(node.as_str())
925 }),
926 Target::Sparql(sparql_target) => serde_json::json!({
927 "type": "sparql",
928 "value": &sparql_target.query,
929 "label": "SPARQL Target"
930 }),
931 Target::Implicit(node) => serde_json::json!({
932 "type": "implicit",
933 "value": node.as_str(),
934 "label": self.compact_iri(node.as_str())
935 }),
936 Target::Union(union_target) => serde_json::json!({
937 "type": "union",
938 "value": format!("Union of {} targets", union_target.targets.len()),
939 "label": "Union Target"
940 }),
941 Target::Intersection(intersection_target) => serde_json::json!({
942 "type": "intersection",
943 "value": format!("Intersection of {} targets", intersection_target.targets.len()),
944 "label": "Intersection Target"
945 }),
946 Target::Difference(_diff_target) => serde_json::json!({
947 "type": "difference",
948 "value": "Difference Target",
949 "label": format!("Difference (primary - exclusion)"),
950 }),
951 Target::Conditional(_cond_target) => serde_json::json!({
952 "type": "conditional",
953 "value": "Conditional Target",
954 "label": format!("Conditional (with condition)")
955 }),
956 Target::Hierarchical(hier_target) => serde_json::json!({
957 "type": "hierarchical",
958 "value": format!("{:?}", hier_target.relationship),
959 "label": format!("Hierarchical (depth: {:?})", hier_target.max_depth)
960 }),
961 Target::PathBased(path_target) => serde_json::json!({
962 "type": "pathBased",
963 "value": "Path-based Target",
964 "label": format!("Path-based (direction: {:?})", path_target.direction)
965 }),
966 }
967 }
968
969 fn constraint_to_editor_json(
970 &self,
971 comp_id: &ConstraintComponentId,
972 constraint: &crate::constraints::Constraint,
973 ) -> serde_json::Value {
974 serde_json::json!({
975 "componentId": comp_id.as_str(),
976 "label": self.compact_iri(comp_id.as_str()),
977 "description": self.constraint_label(comp_id, constraint)
978 })
979 }
980
981 fn sanitize_id(&self, id: &str) -> String {
984 id.chars()
985 .map(|c| if c.is_alphanumeric() { c } else { '_' })
986 .collect()
987 }
988
989 fn compact_iri(&self, iri: &str) -> String {
990 if !self.config.use_prefixes {
991 return iri.to_string();
992 }
993
994 for (prefix, namespace) in &self.config.prefixes {
995 if iri.starts_with(namespace) {
996 let local = &iri[namespace.len()..];
997 return format!("{}:{}", prefix, local);
998 }
999 }
1000
1001 if let Some(pos) = iri.rfind('#') {
1003 return iri[pos + 1..].to_string();
1004 }
1005 if let Some(pos) = iri.rfind('/') {
1006 return iri[pos + 1..].to_string();
1007 }
1008
1009 iri.to_string()
1010 }
1011
1012 fn target_label(&self, target: &Target) -> String {
1013 match target {
1014 Target::Class(node) => format!("class: {}", self.compact_iri(node.as_str())),
1015 Target::Node(node) => format!("node: {}", self.compact_iri(&node.to_string())),
1016 Target::SubjectsOf(node) => {
1017 format!("subjectsOf: {}", self.compact_iri(node.as_str()))
1018 }
1019 Target::ObjectsOf(node) => {
1020 format!("objectsOf: {}", self.compact_iri(node.as_str()))
1021 }
1022 Target::Sparql(_) => "SPARQL target".to_string(),
1023 Target::Implicit(node) => format!("implicit: {}", self.compact_iri(node.as_str())),
1024 Target::Union(union_target) => format!("union: {} targets", union_target.targets.len()),
1025 Target::Intersection(intersection_target) => {
1026 format!(
1027 "intersection: {} targets",
1028 intersection_target.targets.len()
1029 )
1030 }
1031 Target::Difference(_) => "difference target".to_string(),
1032 Target::Conditional(_) => "conditional target".to_string(),
1033 Target::Hierarchical(hier_target) => {
1034 format!("hierarchical: {:?}", hier_target.relationship)
1035 }
1036 Target::PathBased(_) => "path-based target".to_string(),
1037 }
1038 }
1039
1040 fn constraint_label(
1041 &self,
1042 comp_id: &ConstraintComponentId,
1043 constraint: &crate::constraints::Constraint,
1044 ) -> String {
1045 let comp_name = self.compact_iri(comp_id.as_str());
1046
1047 match constraint {
1048 crate::constraints::Constraint::MinCount(mc) => {
1049 format!("{} = {}", comp_name, mc.min_count)
1050 }
1051 crate::constraints::Constraint::MaxCount(mc) => {
1052 format!("{} = {}", comp_name, mc.max_count)
1053 }
1054 crate::constraints::Constraint::Datatype(dt) => {
1055 format!(
1056 "{} = {}",
1057 comp_name,
1058 self.compact_iri(dt.datatype_iri.as_str())
1059 )
1060 }
1061 crate::constraints::Constraint::Pattern(pat) => {
1062 format!("{} = /{}/", comp_name, pat.pattern)
1063 }
1064 crate::constraints::Constraint::MinLength(ml) => {
1065 format!("{} = {}", comp_name, ml.min_length)
1066 }
1067 crate::constraints::Constraint::MaxLength(ml) => {
1068 format!("{} = {}", comp_name, ml.max_length)
1069 }
1070 _ => comp_name,
1071 }
1072 }
1073}
1074
1075impl Default for ShapeVisualizer {
1076 fn default() -> Self {
1077 Self::new()
1078 }
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083 use super::*;
1084 use crate::constraints::cardinality_constraints::MinCountConstraint;
1085 use crate::constraints::value_constraints::DatatypeConstraint;
1086 use crate::ShapeId;
1087 use oxirs_core::NamedNode;
1088
1089 fn create_test_shape() -> Shape {
1090 let mut shape = Shape::new(
1091 ShapeId::new("http://example.org/PersonShape"),
1092 ShapeType::NodeShape,
1093 );
1094 shape.label = Some("Person Shape".to_string());
1095 shape.targets.push(Target::Class(
1096 NamedNode::new("http://xmlns.com/foaf/0.1/Person").unwrap(),
1097 ));
1098 shape.add_constraint(
1099 ConstraintComponentId::new("sh:minCount"),
1100 crate::constraints::Constraint::MinCount(MinCountConstraint { min_count: 1 }),
1101 );
1102 shape.add_constraint(
1103 ConstraintComponentId::new("sh:datatype"),
1104 crate::constraints::Constraint::Datatype(DatatypeConstraint {
1105 datatype_iri: NamedNode::new("http://www.w3.org/2001/XMLSchema#string").unwrap(),
1106 }),
1107 );
1108 shape
1109 }
1110
1111 #[test]
1112 fn test_dot_export() {
1113 let visualizer = ShapeVisualizer::new();
1114 let shape = create_test_shape();
1115
1116 let dot = visualizer.to_dot(&[shape]);
1117 assert!(dot.contains("digraph"));
1118 assert!(dot.contains("PersonShape"));
1119 }
1120
1121 #[test]
1122 fn test_mermaid_export() {
1123 let visualizer = ShapeVisualizer::new();
1124 let shape = create_test_shape();
1125
1126 let mermaid = visualizer.to_mermaid(&[shape]);
1127 assert!(mermaid.contains("flowchart"));
1128 assert!(mermaid.contains("PersonShape"));
1129 }
1130
1131 #[test]
1132 fn test_json_schema_export() {
1133 let visualizer = ShapeVisualizer::new();
1134 let shape = create_test_shape();
1135
1136 let json_schema = visualizer.to_json_schema(&[shape]);
1137 assert!(json_schema.contains("$schema"));
1138 assert!(json_schema.contains("definitions"));
1139 }
1140
1141 #[test]
1142 fn test_svg_export() {
1143 let visualizer = ShapeVisualizer::new();
1144 let shape = create_test_shape();
1145
1146 let svg = visualizer.to_svg(&[shape]);
1147 assert!(svg.contains("<svg"));
1148 assert!(svg.contains("PersonShape"));
1149 }
1150
1151 #[test]
1152 fn test_plantuml_export() {
1153 let visualizer = ShapeVisualizer::new();
1154 let shape = create_test_shape();
1155
1156 let uml = visualizer.to_plantuml(&[shape]);
1157 assert!(uml.contains("@startuml"));
1158 assert!(uml.contains("@enduml"));
1159 }
1160
1161 #[test]
1162 fn test_d3_json_export() {
1163 let visualizer = ShapeVisualizer::new();
1164 let shape = create_test_shape();
1165
1166 let d3 = visualizer.to_d3_json(&[shape]);
1167 assert!(d3.contains("nodes"));
1168 assert!(d3.contains("links"));
1169 }
1170
1171 #[test]
1172 fn test_cytoscape_export() {
1173 let visualizer = ShapeVisualizer::new();
1174 let shape = create_test_shape();
1175
1176 let cytoscape = visualizer.to_cytoscape_json(&[shape]);
1177 assert!(cytoscape.contains("elements"));
1178 assert!(cytoscape.contains("style"));
1179 }
1180
1181 #[test]
1182 fn test_editor_protocol() {
1183 let visualizer = ShapeVisualizer::new();
1184 let shape = create_test_shape();
1185
1186 let protocol = visualizer.to_editor_protocol(&[shape]);
1187 assert!(protocol.contains("shacl-editor"));
1188 assert!(protocol.contains("shapes"));
1189 }
1190
1191 #[test]
1192 fn test_compact_iri() {
1193 let visualizer = ShapeVisualizer::new();
1194
1195 let compacted = visualizer.compact_iri("http://xmlns.com/foaf/0.1/Person");
1196 assert_eq!(compacted, "foaf:Person");
1197
1198 let compacted = visualizer.compact_iri("http://example.org/test");
1199 assert_eq!(compacted, "ex:test");
1200 }
1201}