use antex::{Color, ColorMode, StyledText, Text};
use dsntk_common::{encode_segments, to_rdnn};
use dsntk_model::{Definitions, DmnElement};
use dsntk_model_evaluator::ModelEvaluator;
use std::collections::{HashMap, HashSet};
use std::fmt::Display;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use walkdir::WalkDir;
pub struct WorkspaceBuilder {
cm: ColorMode,
verbose: bool,
file_count: usize,
loaded_count: usize,
failed_loads_count: usize,
failed_deployments_count: usize,
workspace_definitions: HashMap<String, Vec<Definitions>>,
workspace_namespaces: HashMap<String, HashSet<String>>,
workspace_models: HashMap<String, HashMap<String, String>>,
pub(crate) invocables: HashMap<String, (String, String, String, String)>,
pub(crate) evaluators: HashMap<String, Arc<ModelEvaluator>>,
}
impl WorkspaceBuilder {
pub fn new(dirs: &[PathBuf], cm: ColorMode, verbose: bool) -> Self {
let mut builder = Self {
cm,
verbose,
file_count: 0,
loaded_count: 0,
failed_loads_count: 0,
failed_deployments_count: 0,
workspace_definitions: Default::default(),
workspace_namespaces: Default::default(),
workspace_models: Default::default(),
invocables: Default::default(),
evaluators: Default::default(),
};
builder.load(dirs);
builder
}
fn load(&mut self, dirs: &[PathBuf]) {
for dir in dirs {
self.load_from_dir(dir);
}
self.display_summary();
}
fn load_from_dir(&mut self, dir: &Path) {
for entry_result in WalkDir::new(dir).into_iter() {
match entry_result {
Ok(entry) => {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "dmn") {
self.file_count += 1;
let workspace_name = self.workspace_name(dir, path);
self.load_model(&workspace_name, path);
}
}
Err(reason) => self.err_file_operation(reason.to_string()),
}
}
for (workspace_name, loaded_definitions) in &self.workspace_definitions {
match ModelEvaluator::new(loaded_definitions) {
Ok(evaluator) => {
self.evaluators.insert(workspace_name.to_string(), evaluator);
}
Err(reason) => {
self.err_deployment_failure(workspace_name, reason.to_string());
self.failed_deployments_count += 1;
}
}
}
for (workspace_name, evaluator) in &self.evaluators {
for (model_namespace, model_name, invocable_name) in evaluator.invocables().list() {
let invocable_path = format!(
"{}{}/{}/{}",
if !workspace_name.is_empty() { format!("{}/", workspace_name) } else { "".to_string() },
to_rdnn(&model_namespace).unwrap(),
model_name,
invocable_name
);
self
.invocables
.insert(invocable_path, (workspace_name.clone(), model_namespace, model_name, invocable_name));
}
}
}
fn check_namespace_duplicates(&self, file: &Path, workspace_name: &str, namespace: &str) -> bool {
if let Some(namespaces) = self.workspace_namespaces.get(workspace_name) {
if namespaces.contains(namespace) {
let file_name = self.workspace_models.get(workspace_name).unwrap().get(namespace).unwrap();
self.err_duplicated_namespace(workspace_name, file, namespace, file_name);
return false;
}
}
true
}
fn load_model(&mut self, workspace_name: &str, file: &Path) {
match fs::read_to_string(file) {
Ok(xml) => match dsntk_model::parse(&xml) {
Ok(definitions) => {
let namespace = definitions.namespace().to_string();
if to_rdnn(&namespace).is_some() {
if self.check_namespace_duplicates(file, workspace_name, &namespace) {
self
.workspace_definitions
.entry(workspace_name.to_string())
.and_modify(|loaded_definitions| {
loaded_definitions.push(definitions.clone());
})
.or_insert(vec![definitions]);
self
.workspace_namespaces
.entry(workspace_name.to_string())
.and_modify(|loaded_namespaces| {
loaded_namespaces.insert(namespace.clone());
})
.or_insert({
let mut set = HashSet::new();
set.insert(namespace.clone());
set
});
self
.workspace_models
.entry(workspace_name.to_string())
.and_modify(|loaded_models| {
loaded_models.insert(namespace.clone(), file.to_string_lossy().to_string());
})
.or_insert({
let mut map = HashMap::new();
map.insert(namespace.clone(), file.to_string_lossy().to_string());
map
});
self.loaded_count += 1;
}
} else {
self.err_invalid_namespace(workspace_name, file, &namespace);
self.failed_loads_count += 1;
}
}
Err(reason) => {
self.err_file_load(workspace_name, file, reason.to_string());
self.failed_loads_count += 1;
}
},
Err(reason) => {
self.err_file_load(workspace_name, file, reason.to_string());
self.failed_loads_count += 1;
}
}
}
fn display_summary(&self) {
Text::new(self.cm)
.color(if self.file_count > 0 { Color::Green } else { Color::Red })
.s("Found ")
.s(self.file_count)
.plural(" model", self.file_count)
.dot()
.cprintln();
if self.loaded_count > 0 {
Text::new(self.cm)
.green()
.s("Loaded ")
.s(self.loaded_count)
.plural(" model", self.loaded_count)
.dot()
.cprintln();
}
if self.failed_loads_count > 0 {
Text::new(self.cm)
.red()
.s("Failed to load ")
.s(self.failed_loads_count)
.plural(" model", self.failed_loads_count)
.dot()
.cprintln();
}
let deployed_count = self.evaluators.values().map(|evaluator| evaluator.invocables().len()).sum();
Text::new(self.cm)
.color(if deployed_count > 0 { Color::Green } else { Color::Red })
.s("Deployed ")
.s(deployed_count)
.plural(" invocable", deployed_count)
.dot()
.cprintln();
if self.failed_deployments_count > 0 {
Text::new(self.cm)
.red()
.s("Failed to deploy ")
.s(self.failed_deployments_count)
.plural(" workspace", self.failed_deployments_count)
.dot()
.cprintln();
}
if self.verbose {
self.display_deployed_invocables();
}
}
fn display_deployed_invocables(&self) {
let mut invocable_paths = self.invocables.keys().cloned().collect::<Vec<String>>();
invocable_paths.sort();
let invocable_count = invocable_paths.len();
if invocable_count > 0 {
self.text().nl().yellow().s("Deployed ").plural("invocable", invocable_count).colon().cprintln();
}
for key in invocable_paths {
if let Some((workspace_name, model_namespace, model_name, invocable_name)) = self.invocables.get(&key) {
print!(" ");
let segment_1 = encode_segments(workspace_name);
if !segment_1.is_empty() {
self.text().magenta().s(segment_1).clear().slash().print();
}
let rdnn = to_rdnn(model_namespace).unwrap_or_default();
let segment_2 = encode_segments(&rdnn);
if !segment_2.is_empty() {
self.text().cyan().s(segment_2).clear().slash().print();
}
let segment_3 = encode_segments(model_name);
if !segment_3.is_empty() {
self.text().magenta().s(segment_3).clear().slash().print();
}
let segment_4 = encode_segments(invocable_name);
if !segment_4.is_empty() {
self.text().cyan().s(segment_4).clear().print();
}
println!();
}
}
if invocable_count > 0 {
println!();
}
}
fn workspace_name(&self, parent_path: &Path, child_path: &Path) -> String {
let canonical_parent_path = parent_path.canonicalize().unwrap();
let canonical_child_path = child_path.canonicalize().unwrap();
let mut workspace_name = canonical_child_path.parent().unwrap();
workspace_name = workspace_name.strip_prefix(canonical_parent_path).unwrap();
workspace_name
.to_string_lossy()
.replace('\\', "/")
.trim_start_matches('/')
.trim_end_matches('/')
.to_string()
}
fn err_file_load(&self, workspace_name: &str, file: &Path, reason: String) {
self.error_2(self.join(workspace_name, file), reason);
}
fn err_duplicated_namespace(&self, workspace_name: &str, file: &Path, namespace: &str, file_name: &str) {
self.error_2(
self.join(workspace_name, file),
format!("duplicated namespace '{}' in file {}", namespace, self.join(workspace_name, Path::new(file_name))),
);
}
fn err_invalid_namespace(&self, workspace_name: &str, file: &Path, namespace: &str) {
self.error_2(self.join(workspace_name, file), format!("invalid namespace: '{namespace}'"));
}
fn err_deployment_failure(&self, workspace_name: &str, reason: String) {
self.error_2(
if workspace_name.is_empty() { "/" } else { workspace_name },
format!("deployment failed with reason: {}", reason),
);
}
fn err_file_operation(&self, reason: String) {
self.error_1(reason);
}
fn join(&self, workspace_name: &str, file: &Path) -> String {
if workspace_name.is_empty() {
format!("{}", file.file_name().unwrap().to_string_lossy())
} else {
format!("{}/{}", workspace_name, file.file_name().unwrap().to_string_lossy())
}
}
fn error_1<T: Display>(&self, s1: T) {
self.text().s('[').red().s("error").clear().s(']').space().red().s(s1).cprintln();
}
fn error_2<A: Display, B: Display>(&self, s1: A, s2: B) {
self
.text()
.s('[')
.red()
.s("error")
.clear()
.s("][")
.blue()
.s(s1)
.clear()
.s(']')
.space()
.red()
.s(s2)
.cprintln();
}
fn text(&self) -> Text {
Text::new(self.cm)
}
}