Skip to main content

melodium_doc/
lib.rs

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}