1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3
4use itertools::Itertools;
5use melodium_common::descriptor::{
6 Collection, CollectionTree, Context, Data, DescribedType, Entry, Flow, Function, Identified,
7 Identifier, Input, Model, Output, Parameter, Treatment,
8};
9use std::collections::HashMap;
10use std::error::Error;
11use std::path::PathBuf;
12use std::sync::Arc;
13
14#[derive(Clone, Debug)]
15pub enum DocumentationSubject {
16 All,
17 One(String),
18 Multiple(Vec<String>),
19}
20
21#[derive(Clone, Debug)]
22pub struct Documentation {
23 collection: Collection,
24 _subject: DocumentationSubject,
25 tree: CollectionTree,
26 output: PathBuf,
27}
28
29impl Documentation {
30 pub fn new(output: PathBuf, collection: Collection, subject: DocumentationSubject) -> Self {
31 let mut tree = collection.get_tree();
32
33 match &subject {
34 DocumentationSubject::All => {}
35 DocumentationSubject::One(name) => tree.areas.retain(|k, _| k == name),
36 DocumentationSubject::Multiple(names) => tree.areas.retain(|k, _| names.contains(k)),
37 }
38
39 Self {
40 tree,
41 collection,
42 _subject: subject,
43 output,
44 }
45 }
46
47 pub fn make_documentation(&self) -> Result<(), Box<dyn Error>> {
48 self.write("book.toml", Self::default_mdbook_config().as_bytes())?;
49 self.make_summary()?;
50 self.make_areas()?;
51 for id in &self.collection.identifiers() {
52 self.make_entry(self.collection.get(&id.into()).unwrap())?;
53 }
54
55 Ok(())
56 }
57
58 fn write(&self, file: &str, content: &[u8]) -> Result<(), std::io::Error> {
59 let mut path = self.output.clone();
60 path.push(file);
61 std::fs::create_dir_all(path.parent().unwrap())?;
62 std::fs::write(path, content)
63 }
64
65 fn make_summary(&self) -> Result<(), Box<dyn Error>> {
66 let mut md = String::from("# Summary\n\n[Documentation](README.md)\n");
67
68 md.push_str(&Self::summary_area(&self.tree, vec![]));
69
70 self.write("src/SUMMARY.md", md.as_bytes())?;
71
72 Ok(())
73 }
74
75 fn summary_area(area: &CollectionTree, path: Vec<String>) -> String {
76 let mut content = String::new();
77
78 let mut margin = String::new();
79 (0..path.len()).for_each(|_| margin.push_str(" "));
80
81 for name in area.areas.keys().sorted() {
82 let mut sub_path = path.clone();
83 sub_path.push(name.clone());
84
85 content.push_str(&format!(
86 "{margin}- [ {name}]({}/index.md)\n",
87 sub_path.join("/")
88 ));
89 content.push_str(&Self::summary_area(
90 area.areas.get(name).as_ref().unwrap(),
91 sub_path,
92 ));
93 }
94
95 for entry in area.entries.iter().sorted() {
96 let line = match entry {
97 Entry::Context(c) => {
98 format!(
99 "- [⥱ {}]({})\n",
100 c.name(),
101 Self::id_filepath(c.identifier())
102 )
103 }
104 Entry::Function(f) => format!(
105 "- [𝑓 {}]({})\n",
106 f.identifier().name(),
107 Self::id_filepath(f.identifier())
108 ),
109 Entry::Model(m) => format!(
110 "- [⬢ {}]({})\n",
111 m.identifier().name(),
112 Self::id_filepath(m.identifier())
113 ),
114 Entry::Data(d) => format!(
115 "- [◼ {}]({})\n",
116 d.identifier().name(),
117 Self::id_filepath(d.identifier())
118 ),
119 Entry::Treatment(t) => format!(
120 "- [⤇ {}]({})\n",
121 t.identifier().name(),
122 Self::id_filepath(t.identifier())
123 ),
124 };
125
126 content.push_str(&margin);
127 content.push_str(&line);
128 }
129
130 content
131 }
132
133 fn make_areas(&self) -> Result<(), Box<dyn Error>> {
134 self.make_area(&self.tree, vec![])
135 }
136
137 fn make_area(&self, area: &CollectionTree, path: Vec<String>) -> Result<(), Box<dyn Error>> {
138 let is_root = path.is_empty();
139
140 let title = if is_root {
141 Self::get_title()
142 } else {
143 format!("Area {}", path.last().unwrap())
144 };
145
146 let mut subs = String::new();
147 for sub_name in area.areas.keys().sorted() {
148 let sub_area = area.areas.get(sub_name).unwrap();
149 if subs.is_empty() {
150 if is_root {
151 subs.push_str("## Packages\n\n");
152 } else {
153 subs.push_str("## Subareas\n\n");
154 }
155 }
156
157 subs.push_str(&format!("[{sub_name}]({sub_name}/index.md) \n"));
158
159 let mut sub_path = path.clone();
160 sub_path.push(sub_name.clone());
161 self.make_area(sub_area, sub_path)?;
162 }
163
164 let mut datas = String::new();
165 let mut contexts = String::new();
166 let mut functions = String::new();
167 let mut models = String::new();
168 let mut treatments = String::new();
169
170 let mut entries = area.entries.clone();
171 entries.sort();
172 for entry in entries {
173 match entry {
174 Entry::Context(c) => {
175 if contexts.is_empty() {
176 contexts.push_str("## Contexts\n\n");
177 }
178
179 contexts.push_str(&format!("⥱ [{name}]({name}.md) \n", name = c.name()));
180 }
181 Entry::Function(f) => {
182 if functions.is_empty() {
183 functions.push_str("## Functions\n\n");
184 }
185
186 functions.push_str(&format!(
187 "𝑓 [{name}]({name}.md) \n",
188 name = f.identifier().name()
189 ));
190 }
191 Entry::Model(m) => {
192 if models.is_empty() {
193 models.push_str("## Models\n\n");
194 }
195
196 models.push_str(&format!(
197 "⬢[ {name}]({name}.md) \n",
198 name = m.identifier().name()
199 ));
200 }
201 Entry::Data(d) => {
202 if datas.is_empty() {
203 datas.push_str("## Data types\n\n");
204 }
205
206 datas.push_str(&format!(
207 "◼[ {name}]({name}.md) \n",
208 name = d.identifier().name()
209 ));
210 }
211 Entry::Treatment(t) => {
212 if treatments.is_empty() {
213 treatments.push_str("## Treatments\n\n");
214 }
215
216 treatments.push_str(&format!(
217 "⤇[ {name}]({name}.md) \n",
218 name = t.identifier().name()
219 ));
220 }
221 }
222 }
223
224 let display_path = if !path.is_empty() {
225 format!("\n\n`{}`", path.join("/"))
226 } else {
227 "".to_string()
228 };
229
230 let file = if is_root {
231 "src/README.md".to_string()
232 } else {
233 format!("src/{}/index.md", path.join("/"))
234 };
235 let content = format!(
236 "# {title}{display_path}\n\n---\n\n{subs}{datas}{contexts}{functions}{models}{treatments}"
237 );
238
239 self.write(&file, content.as_bytes())?;
240
241 Ok(())
242 }
243
244 fn make_entry(&self, entry: &Entry) -> Result<(), Box<dyn Error>> {
245 let content = match entry {
246 Entry::Context(c) => self.context_content(c),
247 Entry::Function(f) => self.function_content(f),
248 Entry::Model(m) => self.model_content(m),
249 Entry::Data(o) => self.data_content(o),
250 Entry::Treatment(t) => self.treatment_content(t),
251 };
252
253 let file = format!(
254 "src/{path}/{name}.md",
255 path = entry.identifier().path().join("/"),
256 name = entry.identifier().name()
257 );
258
259 self.write(&file, content.as_bytes())?;
260
261 Ok(())
262 }
263
264 fn context_content(&self, context: &Arc<dyn Context>) -> String {
265 let entries = if !context.values().is_empty() {
266 let mut string = String::new();
267
268 for entry_name in context.values().keys().sorted() {
269 let data_type = context.values().get(entry_name).unwrap();
270 string.push_str(&format!(
271 "↪ `{}:` `{}`{type_link} \n",
272 entry_name,
273 data_type,
274 type_link =
275 if let Some(data) = DescribedType::from(data_type).final_type().data() {
276 format!(
277 " _([`{id}`]({link}))_",
278 id = data.identifier(),
279 link = self.get_link(context.identifier(), data.identifier())
280 )
281 } else {
282 String::new()
283 }
284 ));
285 }
286
287 format!("#### Entries\n\n{}", string)
288 } else {
289 String::default()
290 };
291
292 format!(
293 "# Context {name}\n\n`{id}`\n\n---\n\n{entries}\n\n---\n\n{doc}",
294 name = context.identifier().name(),
295 id = context.identifier().to_string(),
296 doc = context.documentation(),
297 )
298 }
299
300 fn function_content(&self, function: &Arc<dyn Function>) -> String {
301 let generics = if !function.generics().is_empty() {
302 let mut string = String::new();
303
304 for generic in function.generics().iter() {
305 if generic.traits.is_empty() {
306 string.push_str(&format!("◻ `{}` _(any)_ \n", generic.name));
307 } else {
308 string.push_str(&format!(
309 "◻ `{}:` {} \n",
310 generic.name,
311 generic
312 .traits
313 .iter()
314 .map(|tr| format!("`{tr}`"))
315 .sorted()
316 .collect::<Vec<_>>()
317 .join(" + ")
318 ));
319 }
320 }
321
322 format!("#### Generics\n\n{}", string)
323 } else {
324 String::default()
325 };
326
327 let parameters = if !function.parameters().is_empty() {
328 let mut string = String::new();
329
330 for param in function.parameters().iter() {
331 string.push_str(&format!(
332 "↳ `{}:` `{}`{type_link} \n",
333 param.name(),
334 param.described_type(),
335 type_link = if let Some(data) = param.described_type().final_type().data() {
336 format!(
337 " _([`{id}`]({link}))_",
338 id = data.identifier(),
339 link = self.get_link(function.identifier(), data.identifier())
340 )
341 } else {
342 String::new()
343 }
344 ));
345 }
346
347 format!("#### Parameters\n\n{}", string)
348 } else {
349 String::default()
350 };
351
352 let call = format!(
353 "{name}{generics}({params})",
354 name = function.identifier().name(),
355 generics = if !function.generics().is_empty() {
356 format!(
357 "<{}>",
358 function
359 .generics()
360 .iter()
361 .map(|g| format!("{}", g.name))
362 .collect::<Vec<_>>()
363 .join(", ")
364 )
365 } else {
366 String::new()
367 },
368 params = function
369 .parameters()
370 .iter()
371 .map(|p| p.name())
372 .collect::<Vec<&str>>()
373 .join(", ")
374 );
375
376 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}",
377 name = function.identifier().name(),
378 id = function.identifier().to_string(),
379 call = call,
380 return = function.return_type(),
381 parameters = parameters,
382 doc = function.documentation(),
383 )
384 }
385
386 fn model_content(&self, model: &Arc<dyn Model>) -> String {
387 let parameters = if !model.parameters().is_empty() {
388 let mut string = String::new();
389
390 for param_name in model.parameters().keys().sorted() {
391 string.push_str(&format!(
392 "↳ {} \n",
393 self.parameter(
394 model.parameters().get(param_name).unwrap(),
395 model.identifier()
396 )
397 ));
398 }
399
400 format!("\n\n---\n\n#### Parameters\n\n{}", string)
401 } else {
402 String::default()
403 };
404
405 let mut sources = HashMap::new();
406 for (source_name, contexts) in model.sources() {
407 let all_ids = self
408 .collection
409 .identifiers()
410 .into_iter()
411 .filter(|id| id.root() == model.identifier().root())
412 .collect::<Vec<_>>();
413
414 for id in &all_ids {
415 if let Some(entry) = self.collection.get(&id.into()) {
416 match entry {
417 Entry::Treatment(treatment) => {
418 for (model_name, model_desc) in treatment.models() {
419 if model_desc.identifier() == model.identifier() {
420 if let Some(model_sources) =
421 treatment.source_from().get(model_name)
422 {
423 if model_sources.contains(source_name) {
424 sources.insert(
425 treatment.identifier().clone(),
426 (Arc::clone(treatment), contexts.clone()),
427 );
428 }
429 }
430 }
431 }
432 }
433 _ => {}
434 }
435 }
436 }
437 }
438 let sources = if !sources.is_empty() {
439 let mut string = String::new();
440
441 for id in sources.keys().sorted() {
442 let (treatment, contexts) = sources.get(id).unwrap();
443
444 let mut contexts = contexts.clone();
445 contexts.sort_by(|a, b| a.identifier().cmp(b.identifier()));
446
447 let contexts = if !contexts.is_empty() {
448 format!(
449 " with {contexts}",
450 contexts = contexts
451 .iter()
452 .map(|c| format!(
453 "[`{name}`]({link})",
454 name = c.name(),
455 link = self.get_link(model.identifier(), c.identifier())
456 ))
457 .collect::<Vec<_>>()
458 .join(", ")
459 )
460 } else {
461 String::default()
462 };
463
464 string.push_str(&format!(
465 "⤇ `{name}:` [`{id}`]({link}){contexts} \n",
466 name = id.name(),
467 link = self.get_link(model.identifier(), treatment.identifier()),
468 ));
469 }
470
471 format!("\n\n---\n\n#### Sources\n\n{}", string)
472 } else {
473 String::default()
474 };
475
476 let base = if let Some(base_model) = model.base_model() {
477 format!(
478 "Based on [`{id}`]({link})\n\n",
479 id = base_model.identifier(),
480 link = self.get_link(model.identifier(), base_model.identifier())
481 )
482 } else {
483 String::new()
484 };
485
486 format!(
487 "# Model {name}\n\n`{id}`\n\n{base}{parameters}{sources}\n\n---\n\n{doc}",
488 name = model.identifier().name(),
489 id = model.identifier().to_string(),
490 base = base,
491 parameters = parameters,
492 doc = model.documentation(),
493 )
494 }
495
496 fn data_content(&self, data: &Arc<dyn Data>) -> String {
497 let traits = if !data.implements().is_empty() {
498 let mut string = String::new();
499
500 for name in data.implements().iter().map(|t| t.to_string()).sorted() {
501 string.push_str(&format!("∈ `{name}` \n",));
502 }
503
504 format!("#### Traits\n\n{}", string)
505 } else {
506 String::from("_This data type do not implement any trait_")
507 };
508
509 format!(
510 "# Data {name}\n\n`{id}`\n\n---\n\n{traits}\n\n---\n\n{doc}",
511 name = data.identifier().name(),
512 id = data.identifier().to_string(),
513 doc = data.documentation(),
514 )
515 }
516
517 fn treatment_content(&self, treatment: &Arc<dyn Treatment>) -> String {
518 let generics = if !treatment.generics().is_empty() {
519 let mut string = String::new();
520
521 for generic in treatment.generics().iter() {
522 if generic.traits.is_empty() {
523 string.push_str(&format!("◻ `{}` _(any)_ \n", generic.name));
524 } else {
525 string.push_str(&format!(
526 "◻ `{}:` {} \n",
527 generic.name,
528 generic
529 .traits
530 .iter()
531 .map(|tr| format!("`{tr}`"))
532 .sorted()
533 .collect::<Vec<_>>()
534 .join(" + ")
535 ));
536 }
537 }
538
539 format!("#### Generics\n\n{}", string)
540 } else {
541 String::default()
542 };
543
544 let models = if !treatment.models().is_empty() {
545 let mut string = String::new();
546
547 for name in treatment.models().keys().sorted() {
548 string.push_str(&format!("⬡ `{name}:` [`{type}`]({link}) \n",
549 type = treatment.models().get(name).unwrap().identifier(),
550 link = self.get_link(treatment.identifier(), treatment.models().get(name).unwrap().identifier()),
551 ));
552 }
553
554 format!("#### Configuration\n\n{}", string)
555 } else {
556 String::default()
557 };
558
559 let mut provided_contexts = HashMap::new();
560 for (model_name, sources) in treatment.source_from() {
561 for (model_source, model_contexts) in
562 treatment.models().get(model_name).unwrap().sources()
563 {
564 if sources.contains(model_source) {
565 model_contexts.iter().for_each(|c| {
566 provided_contexts.insert(c.name(), (Arc::clone(c), model_name));
567 });
568 }
569 }
570 }
571 let provided = if !provided_contexts.is_empty() {
572 let mut string = String::new();
573
574 for context_name in provided_contexts.keys().sorted() {
575 let (context, model_name) = provided_contexts.get(context_name).unwrap();
576 let model = treatment.models().get(*model_name).unwrap();
577 string.push_str(&format!("⥱ `{context_name}:` [`{id}`]({link}) from `{model_name}:` [`{id_model}`]({link_model}) \n",
578 id = context.identifier(),
579 link = self.get_link(treatment.identifier(), context.identifier()),
580 id_model = model.identifier(),
581 link_model = self.get_link(treatment.identifier(), model.identifier()),
582 ));
583 }
584
585 format!("#### Provide contexts\n\n{}", string)
586 } else {
587 String::default()
588 };
589
590 let parameters = if !treatment.parameters().is_empty() {
591 let mut string = String::new();
592
593 for param_name in treatment.parameters().keys().sorted() {
594 string.push_str(&format!(
595 "↳ {} \n",
596 self.parameter(
597 treatment.parameters().get(param_name).unwrap(),
598 treatment.identifier()
599 )
600 ));
601 }
602
603 format!("#### Parameters\n\n{}", string)
604 } else {
605 String::default()
606 };
607
608 let requirements = if !treatment.contexts().is_empty() {
609 let mut string = String::new();
610
611 for name in treatment.contexts().keys().sorted() {
612 string.push_str(&format!("⥱ `{name}:` [`{type}`]({link}) \n",
613 type = treatment.contexts().get(name).unwrap().identifier(),
614 link = self.get_link(treatment.identifier(), treatment.contexts().get(name).unwrap().identifier()),
615 ));
616 }
617
618 format!("#### Required contexts\n\n{}", string)
619 } else {
620 String::default()
621 };
622
623 let inputs = if !treatment.inputs().is_empty() {
624 let mut string = String::new();
625
626 for input_name in treatment.inputs().keys().sorted() {
627 let input = treatment.inputs().get(input_name).unwrap();
628 string.push_str(&format!(
629 "⇥ `{}:` `{}`{type_link} \n",
630 input_name,
631 self.input(input),
632 type_link = if let Some(data) = input.described_type().final_type().data() {
633 format!(
634 " _([`{id}`]({link}))_",
635 id = data.identifier(),
636 link = self.get_link(treatment.identifier(), data.identifier())
637 )
638 } else {
639 String::new()
640 }
641 ));
642 }
643
644 format!("#### Inputs\n\n{}", string)
645 } else {
646 String::default()
647 };
648
649 let outputs = if !treatment.outputs().is_empty() {
650 let mut string = String::new();
651
652 for output_name in treatment.outputs().keys().sorted() {
653 let output = treatment.outputs().get(output_name).unwrap();
654 string.push_str(&format!(
655 "↦ `{}:` `{}`{type_link} \n",
656 output_name,
657 self.output(output),
658 type_link = if let Some(data) = output.described_type().final_type().data() {
659 format!(
660 " _([`{id}`]({link}))_",
661 id = data.identifier(),
662 link = self.get_link(treatment.identifier(), data.identifier())
663 )
664 } else {
665 String::new()
666 }
667 ));
668 }
669
670 format!("#### Outputs\n\n{}", string)
671 } else {
672 String::default()
673 };
674
675 format!("# Treatment {name}\n\n`{id}`\n\n---\n\n{generics}{models}{provided}{parameters}{requirements}{inputs}{outputs}\n\n---\n\n{doc}",
676 name = treatment.identifier().name(),
677 id = treatment.identifier().to_string(),
678 doc = treatment.documentation(),
679 )
680 }
681
682 fn get_link(&self, current_id: &Identifier, to_id: &Identifier) -> String {
683 let mut path = String::new();
684 (0..current_id.path().len()).for_each(|_| path.push_str("../"));
685 path.push_str(&to_id.path().join("/"));
686 path.push_str(&format!("/{}.md", to_id.name()));
687 path
688 }
689
690 fn id_filepath(id: &Identifier) -> String {
691 format!("{}/{}.md", id.path().join("/"), id.name())
692 }
693
694 fn parameter(&self, parameter: &Parameter, current_id: &Identifier) -> String {
695 format!("`{var} {name}:` `{type}{val}`{type_link}",
696 var = parameter.variability(),
697 name = parameter.name(),
698 type = parameter.described_type(),
699 val = parameter.default().as_ref().map(|v| format!(" = {v}")).unwrap_or_default(),
700 type_link = if let Some(data) = parameter.described_type().final_type().data() {
701 format!(" _([`{id}`]({link}))_", id = data.identifier(), link = self.get_link(current_id, data.identifier()))
702 } else {
703 String::new()
704 }
705 )
706 }
707
708 fn input(&self, input: &Input) -> String {
709 let flow = match input.flow() {
710 Flow::Block => "Block",
711 Flow::Stream => "Stream",
712 };
713
714 format!("{}<{}>", flow, input.described_type(),)
715 }
716
717 fn output(&self, output: &Output) -> String {
718 let flow = match output.flow() {
719 Flow::Block => "Block",
720 Flow::Stream => "Stream",
721 };
722
723 format!("{}<{}>", flow, output.described_type())
724 }
725
726 fn get_title() -> String {
727 std::env::var("MELODIUM_DOC_TITLE").unwrap_or("Documentation".to_string())
728 }
729
730 fn get_author() -> String {
731 std::env::var("MELODIUM_DOC_AUTHOR").unwrap_or("The Author".to_string())
732 }
733
734 fn default_mdbook_config() -> String {
735 let title = Self::get_title();
736 let author = Self::get_author();
737
738 format!(
739 r#"[book]
740authors = ["{}"]
741language = "en"
742multilingual = false
743src = "src"
744title = "{}"
745
746[output.html]
747no-section-label = true
748
749[output.html.fold]
750enable = true
751level = 0
752
753[output.html.print]
754enable = false
755"#,
756 author, title
757 )
758 }
759}