#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
use itertools::Itertools;
use melodium_common::descriptor::{
Collection, CollectionTree, Context, Data, DescribedType, Entry, Flow, Function, Identified,
Identifier, Input, Model, Output, Parameter, Treatment,
};
use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub enum DocumentationSubject {
All,
One(String),
Multiple(Vec<String>),
}
#[derive(Clone, Debug)]
pub struct Documentation {
collection: Collection,
_subject: DocumentationSubject,
tree: CollectionTree,
output: PathBuf,
}
impl Documentation {
pub fn new(output: PathBuf, collection: Collection, subject: DocumentationSubject) -> Self {
let mut tree = collection.get_tree();
match &subject {
DocumentationSubject::All => {}
DocumentationSubject::One(name) => tree.areas.retain(|k, _| k == name),
DocumentationSubject::Multiple(names) => tree.areas.retain(|k, _| names.contains(k)),
}
Self {
tree,
collection,
_subject: subject,
output,
}
}
pub fn make_documentation(&self) -> Result<(), Box<dyn Error>> {
self.write("book.toml", Self::default_mdbook_config().as_bytes())?;
self.make_summary()?;
self.make_areas()?;
for id in &self.collection.identifiers() {
self.make_entry(self.collection.get(&id.into()).unwrap())?;
}
Ok(())
}
fn write(&self, file: &str, content: &[u8]) -> Result<(), std::io::Error> {
let mut path = self.output.clone();
path.push(file);
std::fs::create_dir_all(path.parent().unwrap())?;
std::fs::write(path, content)
}
fn make_summary(&self) -> Result<(), Box<dyn Error>> {
let mut md = String::from("# Summary\n\n[Documentation](README.md)\n");
md.push_str(&Self::summary_area(&self.tree, vec![]));
self.write("src/SUMMARY.md", md.as_bytes())?;
Ok(())
}
fn summary_area(area: &CollectionTree, path: Vec<String>) -> String {
let mut content = String::new();
let mut margin = String::new();
(0..path.len()).for_each(|_| margin.push_str(" "));
for name in area.areas.keys().sorted() {
let mut sub_path = path.clone();
sub_path.push(name.clone());
content.push_str(&format!(
"{margin}- [ {name}]({}/index.md)\n",
sub_path.join("/")
));
content.push_str(&Self::summary_area(
area.areas.get(name).as_ref().unwrap(),
sub_path,
));
}
for entry in area.entries.iter().sorted() {
let line = match entry {
Entry::Context(c) => {
format!(
"- [⥱ {}]({})\n",
c.name(),
Self::id_filepath(c.identifier())
)
}
Entry::Function(f) => format!(
"- [𝑓 {}]({})\n",
f.identifier().name(),
Self::id_filepath(f.identifier())
),
Entry::Model(m) => format!(
"- [⬢ {}]({})\n",
m.identifier().name(),
Self::id_filepath(m.identifier())
),
Entry::Data(d) => format!(
"- [◼ {}]({})\n",
d.identifier().name(),
Self::id_filepath(d.identifier())
),
Entry::Treatment(t) => format!(
"- [⤇ {}]({})\n",
t.identifier().name(),
Self::id_filepath(t.identifier())
),
};
content.push_str(&margin);
content.push_str(&line);
}
content
}
fn make_areas(&self) -> Result<(), Box<dyn Error>> {
self.make_area(&self.tree, vec![])
}
fn make_area(&self, area: &CollectionTree, path: Vec<String>) -> Result<(), Box<dyn Error>> {
let is_root = path.is_empty();
let title = if is_root {
Self::get_title()
} else {
format!("Area {}", path.last().unwrap())
};
let mut subs = String::new();
for sub_name in area.areas.keys().sorted() {
let sub_area = area.areas.get(sub_name).unwrap();
if subs.is_empty() {
if is_root {
subs.push_str("## Packages\n\n");
} else {
subs.push_str("## Subareas\n\n");
}
}
subs.push_str(&format!("[{sub_name}]({sub_name}/index.md) \n"));
let mut sub_path = path.clone();
sub_path.push(sub_name.clone());
self.make_area(sub_area, sub_path)?;
}
let mut datas = String::new();
let mut contexts = String::new();
let mut functions = String::new();
let mut models = String::new();
let mut treatments = String::new();
let mut entries = area.entries.clone();
entries.sort();
for entry in entries {
match entry {
Entry::Context(c) => {
if contexts.is_empty() {
contexts.push_str("## Contexts\n\n");
}
contexts.push_str(&format!("⥱ [{name}]({name}.md) \n", name = c.name()));
}
Entry::Function(f) => {
if functions.is_empty() {
functions.push_str("## Functions\n\n");
}
functions.push_str(&format!(
"𝑓 [{name}]({name}.md) \n",
name = f.identifier().name()
));
}
Entry::Model(m) => {
if models.is_empty() {
models.push_str("## Models\n\n");
}
models.push_str(&format!(
"⬢[ {name}]({name}.md) \n",
name = m.identifier().name()
));
}
Entry::Data(d) => {
if datas.is_empty() {
datas.push_str("## Data types\n\n");
}
datas.push_str(&format!(
"◼[ {name}]({name}.md) \n",
name = d.identifier().name()
));
}
Entry::Treatment(t) => {
if treatments.is_empty() {
treatments.push_str("## Treatments\n\n");
}
treatments.push_str(&format!(
"⤇[ {name}]({name}.md) \n",
name = t.identifier().name()
));
}
}
}
let display_path = if !path.is_empty() {
format!("\n\n`{}`", path.join("/"))
} else {
"".to_string()
};
let file = if is_root {
"src/README.md".to_string()
} else {
format!("src/{}/index.md", path.join("/"))
};
let content = format!(
"# {title}{display_path}\n\n---\n\n{subs}{datas}{contexts}{functions}{models}{treatments}"
);
self.write(&file, content.as_bytes())?;
Ok(())
}
fn make_entry(&self, entry: &Entry) -> Result<(), Box<dyn Error>> {
let content = match entry {
Entry::Context(c) => self.context_content(c),
Entry::Function(f) => self.function_content(f),
Entry::Model(m) => self.model_content(m),
Entry::Data(o) => self.data_content(o),
Entry::Treatment(t) => self.treatment_content(t),
};
let file = format!(
"src/{path}/{name}.md",
path = entry.identifier().path().join("/"),
name = entry.identifier().name()
);
self.write(&file, content.as_bytes())?;
Ok(())
}
fn context_content(&self, context: &Arc<dyn Context>) -> String {
let entries = if !context.values().is_empty() {
let mut string = String::new();
for entry_name in context.values().keys().sorted() {
let data_type = context.values().get(entry_name).unwrap();
string.push_str(&format!(
"↪ `{}:` `{}`{type_link} \n",
entry_name,
data_type,
type_link =
if let Some(data) = DescribedType::from(data_type).final_type().data() {
format!(
" _([`{id}`]({link}))_",
id = data.identifier(),
link = self.get_link(context.identifier(), data.identifier())
)
} else {
String::new()
}
));
}
format!("#### Entries\n\n{}", string)
} else {
String::default()
};
format!(
"# Context {name}\n\n`{id}`\n\n---\n\n{entries}\n\n---\n\n{doc}",
name = context.identifier().name(),
id = context.identifier().to_string(),
doc = context.documentation(),
)
}
fn function_content(&self, function: &Arc<dyn Function>) -> String {
let generics = if !function.generics().is_empty() {
let mut string = String::new();
for generic in function.generics().iter() {
if generic.traits.is_empty() {
string.push_str(&format!("◻ `{}` _(any)_ \n", generic.name));
} else {
string.push_str(&format!(
"◻ `{}:` {} \n",
generic.name,
generic
.traits
.iter()
.map(|tr| format!("`{tr}`"))
.sorted()
.collect::<Vec<_>>()
.join(" + ")
));
}
}
format!("#### Generics\n\n{}", string)
} else {
String::default()
};
let parameters = if !function.parameters().is_empty() {
let mut string = String::new();
for param in function.parameters().iter() {
string.push_str(&format!(
"↳ `{}:` `{}`{type_link} \n",
param.name(),
param.described_type(),
type_link = if let Some(data) = param.described_type().final_type().data() {
format!(
" _([`{id}`]({link}))_",
id = data.identifier(),
link = self.get_link(function.identifier(), data.identifier())
)
} else {
String::new()
}
));
}
format!("#### Parameters\n\n{}", string)
} else {
String::default()
};
let call = format!(
"{name}{generics}({params})",
name = function.identifier().name(),
generics = if !function.generics().is_empty() {
format!(
"<{}>",
function
.generics()
.iter()
.map(|g| format!("{}", g.name))
.collect::<Vec<_>>()
.join(", ")
)
} else {
String::new()
},
params = function
.parameters()
.iter()
.map(|p| p.name())
.collect::<Vec<&str>>()
.join(", ")
);
format!("# Function {name}\n\n`{id}`\n\n---\n\n#### Usage\n```\n{call}\n```\n\n{generics}{parameters}\n\n#### Return\n\n↴ `{return}`\n\n---\n\n{doc}",
name = function.identifier().name(),
id = function.identifier().to_string(),
call = call,
return = function.return_type(),
parameters = parameters,
doc = function.documentation(),
)
}
fn model_content(&self, model: &Arc<dyn Model>) -> String {
let parameters = if !model.parameters().is_empty() {
let mut string = String::new();
for param_name in model.parameters().keys().sorted() {
string.push_str(&format!(
"↳ {} \n",
self.parameter(
model.parameters().get(param_name).unwrap(),
model.identifier()
)
));
}
format!("\n\n---\n\n#### Parameters\n\n{}", string)
} else {
String::default()
};
let mut sources = HashMap::new();
for (source_name, contexts) in model.sources() {
let all_ids = self
.collection
.identifiers()
.into_iter()
.filter(|id| id.root() == model.identifier().root())
.collect::<Vec<_>>();
for id in &all_ids {
if let Some(entry) = self.collection.get(&id.into()) {
match entry {
Entry::Treatment(treatment) => {
for (model_name, model_desc) in treatment.models() {
if model_desc.identifier() == model.identifier() {
if let Some(model_sources) =
treatment.source_from().get(model_name)
{
if model_sources.contains(source_name) {
sources.insert(
treatment.identifier().clone(),
(Arc::clone(treatment), contexts.clone()),
);
}
}
}
}
}
_ => {}
}
}
}
}
let sources = if !sources.is_empty() {
let mut string = String::new();
for id in sources.keys().sorted() {
let (treatment, contexts) = sources.get(id).unwrap();
let mut contexts = contexts.clone();
contexts.sort_by(|a, b| a.identifier().cmp(b.identifier()));
let contexts = if !contexts.is_empty() {
format!(
" with {contexts}",
contexts = contexts
.iter()
.map(|c| format!(
"[`{name}`]({link})",
name = c.name(),
link = self.get_link(model.identifier(), c.identifier())
))
.collect::<Vec<_>>()
.join(", ")
)
} else {
String::default()
};
string.push_str(&format!(
"⤇ `{name}:` [`{id}`]({link}){contexts} \n",
name = id.name(),
link = self.get_link(model.identifier(), treatment.identifier()),
));
}
format!("\n\n---\n\n#### Sources\n\n{}", string)
} else {
String::default()
};
let base = if let Some(base_model) = model.base_model() {
format!(
"Based on [`{id}`]({link})\n\n",
id = base_model.identifier(),
link = self.get_link(model.identifier(), base_model.identifier())
)
} else {
String::new()
};
format!(
"# Model {name}\n\n`{id}`\n\n{base}{parameters}{sources}\n\n---\n\n{doc}",
name = model.identifier().name(),
id = model.identifier().to_string(),
base = base,
parameters = parameters,
doc = model.documentation(),
)
}
fn data_content(&self, data: &Arc<dyn Data>) -> String {
let traits = if !data.implements().is_empty() {
let mut string = String::new();
for name in data.implements().iter().map(|t| t.to_string()).sorted() {
string.push_str(&format!("∈ `{name}` \n",));
}
format!("#### Traits\n\n{}", string)
} else {
String::from("_This data type do not implement any trait_")
};
format!(
"# Data {name}\n\n`{id}`\n\n---\n\n{traits}\n\n---\n\n{doc}",
name = data.identifier().name(),
id = data.identifier().to_string(),
doc = data.documentation(),
)
}
fn treatment_content(&self, treatment: &Arc<dyn Treatment>) -> String {
let generics = if !treatment.generics().is_empty() {
let mut string = String::new();
for generic in treatment.generics().iter() {
if generic.traits.is_empty() {
string.push_str(&format!("◻ `{}` _(any)_ \n", generic.name));
} else {
string.push_str(&format!(
"◻ `{}:` {} \n",
generic.name,
generic
.traits
.iter()
.map(|tr| format!("`{tr}`"))
.sorted()
.collect::<Vec<_>>()
.join(" + ")
));
}
}
format!("#### Generics\n\n{}", string)
} else {
String::default()
};
let models = if !treatment.models().is_empty() {
let mut string = String::new();
for name in treatment.models().keys().sorted() {
string.push_str(&format!("⬡ `{name}:` [`{type}`]({link}) \n",
type = treatment.models().get(name).unwrap().identifier(),
link = self.get_link(treatment.identifier(), treatment.models().get(name).unwrap().identifier()),
));
}
format!("#### Configuration\n\n{}", string)
} else {
String::default()
};
let mut provided_contexts = HashMap::new();
for (model_name, sources) in treatment.source_from() {
for (model_source, model_contexts) in
treatment.models().get(model_name).unwrap().sources()
{
if sources.contains(model_source) {
model_contexts.iter().for_each(|c| {
provided_contexts.insert(c.name(), (Arc::clone(c), model_name));
});
}
}
}
let provided = if !provided_contexts.is_empty() {
let mut string = String::new();
for context_name in provided_contexts.keys().sorted() {
let (context, model_name) = provided_contexts.get(context_name).unwrap();
let model = treatment.models().get(*model_name).unwrap();
string.push_str(&format!("⥱ `{context_name}:` [`{id}`]({link}) from `{model_name}:` [`{id_model}`]({link_model}) \n",
id = context.identifier(),
link = self.get_link(treatment.identifier(), context.identifier()),
id_model = model.identifier(),
link_model = self.get_link(treatment.identifier(), model.identifier()),
));
}
format!("#### Provide contexts\n\n{}", string)
} else {
String::default()
};
let parameters = if !treatment.parameters().is_empty() {
let mut string = String::new();
for param_name in treatment.parameters().keys().sorted() {
string.push_str(&format!(
"↳ {} \n",
self.parameter(
treatment.parameters().get(param_name).unwrap(),
treatment.identifier()
)
));
}
format!("#### Parameters\n\n{}", string)
} else {
String::default()
};
let requirements = if !treatment.contexts().is_empty() {
let mut string = String::new();
for name in treatment.contexts().keys().sorted() {
string.push_str(&format!("⥱ `{name}:` [`{type}`]({link}) \n",
type = treatment.contexts().get(name).unwrap().identifier(),
link = self.get_link(treatment.identifier(), treatment.contexts().get(name).unwrap().identifier()),
));
}
format!("#### Required contexts\n\n{}", string)
} else {
String::default()
};
let inputs = if !treatment.inputs().is_empty() {
let mut string = String::new();
for input_name in treatment.inputs().keys().sorted() {
let input = treatment.inputs().get(input_name).unwrap();
string.push_str(&format!(
"⇥ `{}:` `{}`{type_link} \n",
input_name,
self.input(input),
type_link = if let Some(data) = input.described_type().final_type().data() {
format!(
" _([`{id}`]({link}))_",
id = data.identifier(),
link = self.get_link(treatment.identifier(), data.identifier())
)
} else {
String::new()
}
));
}
format!("#### Inputs\n\n{}", string)
} else {
String::default()
};
let outputs = if !treatment.outputs().is_empty() {
let mut string = String::new();
for output_name in treatment.outputs().keys().sorted() {
let output = treatment.outputs().get(output_name).unwrap();
string.push_str(&format!(
"↦ `{}:` `{}`{type_link} \n",
output_name,
self.output(output),
type_link = if let Some(data) = output.described_type().final_type().data() {
format!(
" _([`{id}`]({link}))_",
id = data.identifier(),
link = self.get_link(treatment.identifier(), data.identifier())
)
} else {
String::new()
}
));
}
format!("#### Outputs\n\n{}", string)
} else {
String::default()
};
format!("# Treatment {name}\n\n`{id}`\n\n---\n\n{generics}{models}{provided}{parameters}{requirements}{inputs}{outputs}\n\n---\n\n{doc}",
name = treatment.identifier().name(),
id = treatment.identifier().to_string(),
doc = treatment.documentation(),
)
}
fn get_link(&self, current_id: &Identifier, to_id: &Identifier) -> String {
let mut path = String::new();
(0..current_id.path().len()).for_each(|_| path.push_str("../"));
path.push_str(&to_id.path().join("/"));
path.push_str(&format!("/{}.md", to_id.name()));
path
}
fn id_filepath(id: &Identifier) -> String {
format!("{}/{}.md", id.path().join("/"), id.name())
}
fn parameter(&self, parameter: &Parameter, current_id: &Identifier) -> String {
format!("`{var} {name}:` `{type}{val}`{type_link}",
var = parameter.variability(),
name = parameter.name(),
type = parameter.described_type(),
val = parameter.default().as_ref().map(|v| format!(" = {v}")).unwrap_or_default(),
type_link = if let Some(data) = parameter.described_type().final_type().data() {
format!(" _([`{id}`]({link}))_", id = data.identifier(), link = self.get_link(current_id, data.identifier()))
} else {
String::new()
}
)
}
fn input(&self, input: &Input) -> String {
let flow = match input.flow() {
Flow::Block => "Block",
Flow::Stream => "Stream",
};
format!("{}<{}>", flow, input.described_type(),)
}
fn output(&self, output: &Output) -> String {
let flow = match output.flow() {
Flow::Block => "Block",
Flow::Stream => "Stream",
};
format!("{}<{}>", flow, output.described_type())
}
fn get_title() -> String {
std::env::var("MELODIUM_DOC_TITLE").unwrap_or("Documentation".to_string())
}
fn get_author() -> String {
std::env::var("MELODIUM_DOC_AUTHOR").unwrap_or("The Author".to_string())
}
fn default_mdbook_config() -> String {
let title = Self::get_title();
let author = Self::get_author();
format!(
r#"[book]
authors = ["{}"]
language = "en"
multilingual = false
src = "src"
title = "{}"
[output.html]
no-section-label = true
[output.html.fold]
enable = true
level = 0
[output.html.print]
enable = false
"#,
author, title
)
}
}