use crate::color;
use crate::color::rdf::format_url;
use crate::color::rdf::Separator;
use crate::draw::OutputFormat;
use crate::draw::DOT_PROGRAM;
use crate::exec::exec_with_temp_input;
use crate::Generator;
use nu_ansi_term::Style;
use sdml_core::error::Error;
use sdml_core::model::identifiers::Identifier;
use sdml_core::model::modules::HeaderValue;
use sdml_core::model::modules::Module;
use sdml_core::model::HasName;
use sdml_core::{stdlib::is_library_module, store::ModuleStore};
use std::collections::HashSet;
use std::io::Write;
use std::path::PathBuf;
use text_trees::{FormatCharacters, TreeFormatting, TreeNode};
use url::Url;
#[derive(Clone, Copy, Debug, Default)]
pub struct DependencyViewGenerator {}
#[derive(Clone, Copy, Debug, Default)]
pub struct DependencyViewOptions {
depth: usize,
representation: DependencyViewRepresentation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DependencyViewRepresentation {
TextTree,
DotGraph(OutputFormat),
RdfImports,
}
#[derive(Debug)]
struct Node<'a> {
name: &'a Identifier,
base_uri: Option<&'a HeaderValue<Url>>,
version_uri: Option<&'a HeaderValue<Url>>,
children: Option<Vec<Node<'a>>>,
}
impl Default for DependencyViewRepresentation {
fn default() -> Self {
Self::TextTree
}
}
impl DependencyViewOptions {
pub fn with_depth(self, depth: usize) -> Self {
Self { depth, ..self }
}
pub fn with_representation(self, representation: DependencyViewRepresentation) -> Self {
Self {
representation,
..self
}
}
pub fn as_text_tree(self) -> Self {
Self::with_representation(self, DependencyViewRepresentation::TextTree)
}
pub fn as_dot_graph(self, output_format: OutputFormat) -> Self {
Self::with_representation(self, DependencyViewRepresentation::DotGraph(output_format))
}
pub fn as_rdf_imports(self) -> Self {
Self::with_representation(self, DependencyViewRepresentation::RdfImports)
}
}
impl Generator for DependencyViewGenerator {
type Options = DependencyViewOptions;
fn generate_with_options<W>(
&mut self,
module: &Module,
cache: &impl ModuleStore,
options: Self::Options,
_: Option<PathBuf>,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
match options.representation {
DependencyViewRepresentation::TextTree => {
self.write_text_tree(module, cache, options.depth, writer)
}
DependencyViewRepresentation::DotGraph(inner_format) => {
let mut buffer = Vec::new();
self.write_dot_graph(module, cache, options.depth, &mut buffer)?;
if inner_format == OutputFormat::Source {
writer.write_all(&buffer)?;
} else {
let source = String::from_utf8(buffer).unwrap();
match exec_with_temp_input(DOT_PROGRAM, vec![inner_format.into()], source) {
Ok(result) => {
writer.write_all(result.as_bytes())?;
}
Err(e) => {
panic!("exec_with_input failed: {:?}", e);
}
}
}
Ok(())
}
DependencyViewRepresentation::RdfImports => {
self.write_rdf_imports(module, cache, options.depth, writer)
}
}
}
}
impl DependencyViewGenerator {
fn write_text_tree<W>(
&self,
module: &Module,
cache: &impl ModuleStore,
depth: usize,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
let depth = if depth == 0 { usize::MAX } else { depth };
let mut seen = Default::default();
let tree = Node::from_module(module, None, &mut seen, cache, depth);
let new_tree = tree.make_text_tree(true);
new_tree.write_with_format(
writer,
&TreeFormatting::dir_tree(FormatCharacters::box_chars()),
)?;
Ok(())
}
fn write_dot_graph<W>(
&self,
module: &Module,
cache: &impl ModuleStore,
depth: usize,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
let depth = if depth == 0 { usize::MAX } else { depth };
let mut seen = Default::default();
let tree = Node::from_module(module, None, &mut seen, cache, depth);
writer.write_all(
r#"digraph G {
bgcolor="transparent";
rankdir="TB";
fontname="Helvetica,Arial,sans-serif";
node [
shape="tab";
fontname="Helvetica,Arial,sans-serif"; fontsize=11
];
edge [
style="dashed"; arrowhead="open";
fontname="Helvetica,Arial,sans-serif"; fontsize=9; fontcolor="dimgrey";
labelfontcolor="blue"; labeldistance=2.0
];
"#
.as_bytes(),
)?;
if !seen.contains(module.name()) {
writer.write_all(
self.write_gv_node(module.name(), true, is_library_module(module.name()))
.as_bytes(),
)?;
}
for module_name in seen {
writer.write_all(
self.write_gv_node(
module_name,
module_name == module.name(),
is_library_module(module_name),
)
.as_bytes(),
)?;
}
writer.write_all(b"\n")?;
self.write_graph_node(&tree, writer)?;
writer.write_all(b"}\n")?;
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn write_graph_node<W>(&self, node: &Node<'_>, writer: &mut W) -> Result<(), Error>
where
W: Write + Sized,
{
if let Some(children) = &node.children {
for child in children {
if let Some(version_uri) = child.version_uri {
writer.write_all(
format!(
" {} -> {} [label=\"{}\"];\n",
node.name, child.name, version_uri
)
.as_bytes(),
)?;
} else {
writer.write_all(format!(" {} -> {};\n", node.name, child.name).as_bytes())?;
}
self.write_graph_node(child, writer)?;
}
}
Ok(())
}
fn write_gv_node(&self, name: &Identifier, is_subject: bool, is_library: bool) -> String {
const MODULE_STEREOTYPE: &str = "<FONT POINT-SIZE=\"9\">«module»</FONT><BR/>";
match (is_subject, is_library) {
(true, true) => format!(
" {} [label=<{}<B><I>{}</I></B>>];\n",
name, MODULE_STEREOTYPE, name
),
(true, false) => format!(
" {} [label=<{}<B>{}</B>>];\n",
name, MODULE_STEREOTYPE, name
),
(false, true) => format!(
" {} [label=<{}<I>{}</I>>];\n",
name, MODULE_STEREOTYPE, name
),
(false, false) => format!(" {}[label=<{}{}>];\n", name, MODULE_STEREOTYPE, name),
}
}
fn write_rdf_imports<W>(
&self,
module: &Module,
cache: &impl ModuleStore,
depth: usize,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized,
{
const OWL_IMPORTS: &str = "http://www.w3.org/2002/07/owl#imports";
let depth = if depth == 0 { usize::MAX } else { depth };
let mut seen = Default::default();
let tree = Node::from_module(module, None, &mut seen, cache, depth);
let mut list = Vec::default();
self.tree_to_rdf_list(&tree, &mut list);
for (subj, obj) in list {
writer.write_all(
format!(
"{} {} {}{}",
format_url(subj.as_ref()),
format_url(OWL_IMPORTS),
format_url(obj.as_ref()),
Separator::Statement,
)
.as_bytes(),
)?;
}
Ok(())
}
#[allow(clippy::only_used_in_recursion)]
fn tree_to_rdf_list<'a>(
&self,
node: &'a Node<'_>,
list: &mut Vec<(&'a HeaderValue<Url>, &'a HeaderValue<Url>)>,
) {
if node.base_uri.is_some() {
if let Some(children) = &node.children {
for child in children {
if let Some(child_base_uri) = child.base_uri {
if let Some(child_version_uri) = child.version_uri {
list.push((node.base_uri.unwrap(), child_version_uri));
} else {
list.push((node.base_uri.unwrap(), child_base_uri));
}
}
}
for child in children {
if child.base_uri.is_some() {
self.tree_to_rdf_list(child, list);
}
}
}
}
}
}
impl<'a> Node<'a> {
fn from_module(
module: &'a Module,
version_uri: Option<&'a HeaderValue<Url>>,
seen: &mut HashSet<&'a Identifier>,
cache: &'a impl ModuleStore,
depth: usize,
) -> Self {
let mut children: Vec<Node<'a>> = Default::default();
let import_map = module.imported_module_versions();
let mut modules = import_map.keys().collect::<Vec<_>>();
modules.sort();
for imported in modules {
#[allow(clippy::map_clone)]
let imported_version_uri = import_map.get(imported).map(|v| *v).unwrap_or_default();
if depth == 1 || seen.contains(imported) {
if let Some(cached) = cache.get(imported) {
children.push(Self::from_name(
imported,
cached.base_uri(),
imported_version_uri,
));
} else {
children.push(Self::from_name_only(imported, imported_version_uri));
}
} else {
seen.insert(imported);
if let Some(cached) = cache.get(imported) {
children.push(Self::from_module(
cached,
imported_version_uri,
seen,
cache,
depth - 1,
));
} else {
children.push(Self::from_name_only(imported, imported_version_uri));
}
}
}
Self {
name: module.name(),
base_uri: module.base_uri(),
version_uri,
children: Some(children),
}
}
fn from_name_only(module: &'a Identifier, version_uri: Option<&'a HeaderValue<Url>>) -> Self {
Self::from_name(module, None, version_uri)
}
fn from_name(
module: &'a Identifier,
base_uri: Option<&'a HeaderValue<Url>>,
version_uri: Option<&'a HeaderValue<Url>>,
) -> Self {
Self {
name: module,
base_uri,
version_uri,
children: None,
}
}
fn make_text_tree(&'a self, is_root: bool) -> TreeNode<String> {
let children = if let Some(children) = &self.children {
children
.iter()
.map(|node| node.make_text_tree(false))
.collect::<Vec<TreeNode<_>>>()
} else {
Default::default()
};
TreeNode::with_child_nodes(self.make_node_string(is_root), children.into_iter())
}
fn make_node_string(&self, is_root: bool) -> String {
let node_string = format!(
"{}{}",
self.name,
if let Some(version_uri) = self.version_uri {
format!("@<{version_uri}>")
} else {
String::new()
}
);
if color::colorize().use_color() {
let mut style = Style::new();
if is_root {
style = style.bold();
}
if is_library_module(self.name) {
style = style.dimmed().italic();
}
style.paint(node_string).to_string()
} else {
node_string
}
}
}