#![deny(missing_docs)]
pub mod export;
pub mod jsondata;
pub mod reify;
pub mod spytial_annotations;
pub use export::export_json_instance;
pub use reify::{from_datum, from_datum_root, replit, replit_root, ReifyError};
pub use spytial_export_macros::SpytialDecorators;
use serde::Serialize;
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::{self, Command};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::SystemTime;
static DIAGRAM_COUNTER: AtomicU64 = AtomicU64::new(0);
fn diagram_output_path() -> PathBuf {
if let Ok(explicit) = env::var("SPYTIAL_OUTPUT_PATH") {
return PathBuf::from(explicit);
}
let pid = process::id();
let counter = DIAGRAM_COUNTER.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
env::temp_dir().join(format!("spytial-{pid}-{counter}-{nanos}.html"))
}
pub fn diagram<T: spytial_annotations::HasSpytialDecorators + Serialize>(value: &T) {
let spytial_spec = collect_spytial_spec_for_diagram(value);
diagram_impl(value, &spytial_spec);
}
fn collect_spytial_spec_for_diagram<T: spytial_annotations::HasSpytialDecorators + Serialize>(
_value: &T,
) -> String {
let all_decorators = T::decorators();
spytial_annotations::to_yaml(&all_decorators).unwrap_or_default()
}
pub fn diagram_with_spec<T: Serialize>(value: &T, spec: &str) {
diagram_impl(value, spec);
}
#[macro_export]
macro_rules! dbg {
() => {
::std::eprintln!(
"[{}:{}:{}]",
::std::file!(),
::std::line!(),
::std::column!(),
)
};
($val:expr $(,)?) => {
match $val {
tmp => {
::std::eprintln!(
"[{}:{}:{}] {} = {:#?}",
::std::file!(),
::std::line!(),
::std::column!(),
::std::stringify!($val),
&tmp,
);
$crate::diagram(&tmp);
tmp
}
}
};
($($val:expr),+ $(,)?) => {
($($crate::dbg!($val)),+,)
};
}
fn diagram_impl<T: Serialize>(value: &T, spec: &str) {
let json_instance = export_json_instance(value);
let json_data = match serde_json::to_string_pretty(&json_instance) {
Ok(json) => json,
Err(err) => {
eprintln!("spytial: could not encode diagram JSON, skipping: {err}");
return;
}
};
let template = include_str!("../templates/template.html");
let rendered_html = template
.replace(
"/*__SPYTIAL_CORE_CSS__*/",
include_str!("../templates/vendor/spytial-core.css"),
)
.replace(
"/*__REACT_COMPONENTS_CSS__*/",
include_str!("../templates/vendor/react-component-integration.css"),
)
.replace(
"/*__SPYTIAL_CORE_JS__*/",
include_str!("../templates/vendor/spytial-core.global.js"),
)
.replace(
"/*__REACT_COMPONENTS_JS__*/",
include_str!("../templates/vendor/react-component-integration.global.js"),
)
.replace("{{ json_data }}", &json_data)
.replace("{{ spytial_spec }}", spec);
let temp_file_path = diagram_output_path();
if let Err(err) = fs::write(&temp_file_path, rendered_html) {
eprintln!(
"spytial: could not write diagram to {}: {err}",
temp_file_path.display()
);
return;
}
let skip_browser_open = env::var("SPYTIAL_NO_OPEN")
.map(|raw| matches!(raw.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
.unwrap_or(false);
if skip_browser_open {
eprintln!("spytial: diagram written to {}", temp_file_path.display());
return;
}
#[cfg(target_os = "macos")]
let open_cmd: Option<&str> = Some("open");
#[cfg(target_os = "windows")]
let open_cmd: Option<&str> = Some("start");
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly"
))]
let open_cmd: Option<&str> = Some("xdg-open");
#[cfg(not(any(
target_os = "macos",
target_os = "windows",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "dragonfly",
)))]
let open_cmd: Option<&str> = None;
let Some(open_cmd) = open_cmd else {
eprintln!(
"spytial: no known browser-open command for this platform. Open this file manually: {}",
temp_file_path.display()
);
return;
};
if let Err(err) = Command::new(open_cmd).arg(&temp_file_path).spawn() {
eprintln!(
"spytial: failed to open browser ({err}). Open this file manually: {}",
temp_file_path.display()
);
}
}