wdl_doc/
callable.rs

1//! HTML generation for WDL callables (workflows and tasks).
2
3pub mod task;
4pub mod workflow;
5
6use std::collections::BTreeMap;
7use std::collections::BTreeSet;
8
9use maud::Markup;
10use maud::html;
11use wdl_ast::AstToken;
12use wdl_ast::v1::InputSection;
13use wdl_ast::v1::MetadataSection;
14use wdl_ast::v1::MetadataValue;
15use wdl_ast::v1::OutputSection;
16use wdl_ast::v1::ParameterMetadataSection;
17
18use crate::meta::render_value;
19use crate::parameter::InputOutput;
20use crate::parameter::Parameter;
21
22/// A map of metadata key-value pairs, sorted by key.
23pub type MetaMap = BTreeMap<String, MetadataValue>;
24
25/// A group of inputs.
26#[derive(Debug, Eq, PartialEq)]
27pub struct Group(pub String);
28
29impl PartialOrd for Group {
30    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
31        Some(self.cmp(other))
32    }
33}
34
35impl Ord for Group {
36    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
37        if self.0 == "Common" {
38            return std::cmp::Ordering::Less;
39        }
40        if other.0 == "Common" {
41            return std::cmp::Ordering::Greater;
42        }
43        if self.0 == "Resources" {
44            return std::cmp::Ordering::Greater;
45        }
46        if other.0 == "Resources" {
47            return std::cmp::Ordering::Less;
48        }
49        self.0.cmp(&other.0)
50    }
51}
52
53/// A callable (workflow or task) in a WDL document.
54pub trait Callable {
55    /// Get the name of the callable.
56    fn name(&self) -> &str;
57
58    /// Get the [`MetaMap`] of the callable.
59    fn meta(&self) -> &MetaMap;
60
61    /// Get the inputs of the callable.
62    fn inputs(&self) -> &[Parameter];
63
64    /// Get the outputs of the callable.
65    fn outputs(&self) -> &[Parameter];
66
67    /// Get the description of the callable.
68    fn description(&self) -> Markup {
69        self.meta()
70            .get("description")
71            .map(render_value)
72            .unwrap_or_else(|| html! {})
73    }
74
75    /// Get the required input parameters of the callable.
76    fn required_inputs(&self) -> impl Iterator<Item = &Parameter> {
77        self.inputs().iter().filter(|param| {
78            param
79                .required()
80                .expect("inputs should return Some(required)")
81        })
82    }
83
84    /// Get the sorted set of unique `group` values of the inputs.
85    ///
86    /// The `Common` group, if present, will always be first in the set,
87    /// followed by any other groups in alphabetical order, and lastly
88    /// the `Resources` group.
89    fn input_groups(&self) -> BTreeSet<Group> {
90        self.inputs()
91            .iter()
92            .filter_map(|param| param.group())
93            .map(|arg0: Group| Group(arg0.0.clone()))
94            .collect()
95    }
96
97    /// Get the inputs of the callable that are part of `group`.
98    fn inputs_in_group<'a>(&'a self, group: &'a Group) -> impl Iterator<Item = &'a Parameter> {
99        self.inputs().iter().filter(move |param| {
100            if let Some(param_group) = param.group() {
101                if param_group == *group {
102                    return true;
103                }
104            }
105            false
106        })
107    }
108
109    /// Get the inputs of the callable that are neither required nor part of a
110    /// group.
111    fn other_inputs(&self) -> impl Iterator<Item = &Parameter> {
112        self.inputs().iter().filter(|param| {
113            !param
114                .required()
115                .expect("inputs should return Some(required)")
116                && param.group().is_none()
117        })
118    }
119
120    /// Render the required inputs of the callable.
121    fn render_required_inputs(&self) -> Markup {
122        let mut iter = self.required_inputs().peekable();
123        if iter.peek().is_some() {
124            return html! {
125                h3 { "Required Inputs" }
126                table class="border" {
127                    thead class="border" { tr {
128                        th { "Name" }
129                        th { "Type" }
130                        th { "Description" }
131                        th { "Additional Meta" }
132                    }}
133                    tbody class="border" {
134                        @for param in iter {
135                            (param.render())
136                        }
137                    }
138                }
139            };
140        };
141        html! {}
142    }
143
144    /// Render the inputs with a group of the callable.
145    fn render_group_inputs(&self) -> Markup {
146        let group_tables = self.input_groups().into_iter().map(|group| {
147            html! {
148                h3 { (group.0) }
149                table class="border" {
150                    thead class="border" { tr {
151                        th { "Name" }
152                        th { "Type" }
153                        th { "Default" }
154                        th { "Description" }
155                        th { "Additional Meta" }
156                    }}
157                    tbody class="border" {
158                        @for param in self.inputs_in_group(&group) {
159                            (param.render())
160                        }
161                    }
162                }
163            }
164        });
165        html! {
166            @for group_table in group_tables {
167                (group_table)
168            }
169        }
170    }
171
172    /// Render the inputs that are neither required nor part of a group.
173    fn render_other_inputs(&self) -> Markup {
174        let mut iter = self.other_inputs().peekable();
175        if iter.peek().is_some() {
176            return html! {
177                h3 { "Other Inputs" }
178                table class="border" {
179                    thead class="border" { tr {
180                        th { "Name" }
181                        th { "Type" }
182                        th { "Default" }
183                        th { "Description" }
184                        th { "Additional Meta" }
185                    }}
186                    tbody class="border" {
187                        @for param in iter {
188                            (param.render())
189                        }
190                    }
191                }
192            };
193        };
194        html! {}
195    }
196
197    /// Render the inputs of the callable.
198    fn render_inputs(&self) -> Markup {
199        html! {
200            h2 { "Inputs" }
201            (self.render_required_inputs())
202            (self.render_group_inputs())
203            (self.render_other_inputs())
204        }
205    }
206
207    /// Render the outputs of the callable.
208    fn render_outputs(&self) -> Markup {
209        html! {
210            h2 { "Outputs" }
211            table  {
212                thead class="border" { tr {
213                    th { "Name" }
214                    th { "Type" }
215                    th { "Expression" }
216                    th { "Description" }
217                    th { "Additional Meta" }
218                }}
219                tbody class="border" {
220                    @for param in self.outputs() {
221                        (param.render())
222                    }
223                }
224            }
225        }
226    }
227}
228
229/// Parse a [`MetadataSection`] into a [`MetaMap`].
230fn parse_meta(meta: &MetadataSection) -> MetaMap {
231    meta.items()
232        .map(|m| {
233            let name = m.name().text().to_owned();
234            let item = m.value();
235            (name, item)
236        })
237        .collect()
238}
239
240/// Parse a [`ParameterMetadataSection`] into a [`MetaMap`].
241fn parse_parameter_meta(parameter_meta: &ParameterMetadataSection) -> MetaMap {
242    parameter_meta
243        .items()
244        .map(|m| {
245            let name = m.name().text().to_owned();
246            let item = m.value();
247            (name, item)
248        })
249        .collect()
250}
251
252/// Parse the [`InputSection`] into a vector of [`Parameter`]s.
253fn parse_inputs(input_section: &InputSection, parameter_meta: &MetaMap) -> Vec<Parameter> {
254    input_section
255        .declarations()
256        .map(|decl| {
257            let name = decl.name().text().to_owned();
258            let meta = parameter_meta.get(&name);
259            Parameter::new(decl.clone(), meta.cloned(), InputOutput::Input)
260        })
261        .collect()
262}
263
264/// Parse the [`OutputSection`] into a vector of [`Parameter`]s.
265fn parse_outputs(
266    output_section: &OutputSection,
267    meta: &MetaMap,
268    parameter_meta: &MetaMap,
269) -> Vec<Parameter> {
270    let output_meta: MetaMap = meta
271        .get("outputs")
272        .and_then(|v| match v {
273            MetadataValue::Object(o) => Some(o),
274            _ => None,
275        })
276        .map(|o| {
277            o.items()
278                .map(|i| (i.name().text().to_owned(), i.value().clone()))
279                .collect()
280        })
281        .unwrap_or_default();
282
283    output_section
284        .declarations()
285        .map(|decl| {
286            let name = decl.name().text().to_owned();
287            let meta = parameter_meta.get(&name).or_else(|| output_meta.get(&name));
288            Parameter::new(
289                wdl_ast::v1::Decl::Bound(decl.clone()),
290                meta.cloned(),
291                InputOutput::Output,
292            )
293        })
294        .collect()
295}
296
297#[cfg(test)]
298mod tests {
299    use wdl_ast::Document;
300
301    use super::*;
302
303    #[test]
304    fn test_group_cmp() {
305        let common = Group("Common".to_string());
306        let resources = Group("Resources".to_string());
307        let a = Group("A".to_string());
308        let b = Group("B".to_string());
309        let c = Group("C".to_string());
310
311        let mut groups = vec![c, a, resources, common, b];
312        groups.sort();
313        assert_eq!(
314            groups,
315            vec![
316                Group("Common".to_string()),
317                Group("A".to_string()),
318                Group("B".to_string()),
319                Group("C".to_string()),
320                Group("Resources".to_string()),
321            ]
322        );
323    }
324
325    #[test]
326    fn test_parse_meta() {
327        let wdl = r#"
328        version 1.1
329
330        workflow wf {
331            meta {
332                name: "Workflow"
333                description: "A workflow"
334            }
335        }
336        "#;
337
338        let (doc, _) = Document::parse(wdl);
339        let doc_item = doc.ast().into_v1().unwrap().items().next().unwrap();
340        let meta_map = parse_meta(
341            &doc_item
342                .as_workflow_definition()
343                .unwrap()
344                .metadata()
345                .unwrap(),
346        );
347        assert_eq!(meta_map.len(), 2);
348        assert_eq!(
349            meta_map
350                .get("name")
351                .unwrap()
352                .clone()
353                .unwrap_string()
354                .text()
355                .unwrap()
356                .text(),
357            "Workflow"
358        );
359        assert_eq!(
360            meta_map
361                .get("description")
362                .unwrap()
363                .clone()
364                .unwrap_string()
365                .text()
366                .unwrap()
367                .text(),
368            "A workflow"
369        );
370    }
371
372    #[test]
373    fn test_parse_parameter_meta() {
374        let wdl = r#"
375        version 1.1
376
377        workflow wf {
378            input {
379                Int a
380            }
381            parameter_meta {
382                a: {
383                    description: "An integer"
384                }
385            }
386        }
387        "#;
388
389        let (doc, _) = Document::parse(wdl);
390        let doc_item = doc.ast().into_v1().unwrap().items().next().unwrap();
391        let meta_map = parse_parameter_meta(
392            &doc_item
393                .as_workflow_definition()
394                .unwrap()
395                .parameter_metadata()
396                .unwrap(),
397        );
398        assert_eq!(meta_map.len(), 1);
399        assert_eq!(
400            meta_map
401                .get("a")
402                .unwrap()
403                .clone()
404                .unwrap_object()
405                .items()
406                .next()
407                .unwrap()
408                .value()
409                .clone()
410                .unwrap_string()
411                .text()
412                .unwrap()
413                .text(),
414            "An integer"
415        );
416    }
417
418    #[test]
419    fn test_parse_inputs() {
420        let wdl = r#"
421        version 1.1
422
423        workflow wf {
424            input {
425                Int a
426                Int b
427                Int c
428            }
429            parameter_meta {
430                a: "An integer"
431                c: {
432                    description: "Another integer"
433                }
434            }
435        }
436        "#;
437
438        let (doc, _) = Document::parse(wdl);
439        let doc_item = doc.ast().into_v1().unwrap().items().next().unwrap();
440        let meta_map = parse_parameter_meta(
441            &doc_item
442                .as_workflow_definition()
443                .unwrap()
444                .parameter_metadata()
445                .unwrap(),
446        );
447        let inputs = parse_inputs(
448            &doc_item.as_workflow_definition().unwrap().input().unwrap(),
449            &meta_map,
450        );
451        assert_eq!(inputs.len(), 3);
452        assert_eq!(inputs[0].name(), "a");
453        assert_eq!(inputs[0].description().into_string(), "An integer");
454        assert_eq!(inputs[1].name(), "b");
455        assert_eq!(inputs[1].description().into_string(), "");
456        assert_eq!(inputs[2].name(), "c");
457        assert_eq!(inputs[2].description().into_string(), "Another integer");
458    }
459
460    #[test]
461    fn test_parse_outputs() {
462        let wdl = r#"
463        version 1.1
464
465        workflow wf {
466            output {
467                Int a = 1
468                Int b = 2
469                Int c = 3
470            }
471            meta {
472                outputs: {
473                    a: "An integer"
474                    c: {
475                        description: "Another integer"
476                    }
477                }
478            }
479            parameter_meta {
480                b: "A different place!"
481            }
482        }
483        "#;
484
485        let (doc, _) = Document::parse(wdl);
486        let doc_item = doc.ast().into_v1().unwrap().items().next().unwrap();
487        let meta_map = parse_meta(
488            &doc_item
489                .as_workflow_definition()
490                .unwrap()
491                .metadata()
492                .unwrap(),
493        );
494        let parameter_meta = parse_parameter_meta(
495            &doc_item
496                .as_workflow_definition()
497                .unwrap()
498                .parameter_metadata()
499                .unwrap(),
500        );
501        let outputs = parse_outputs(
502            &doc_item.as_workflow_definition().unwrap().output().unwrap(),
503            &meta_map,
504            &parameter_meta,
505        );
506        assert_eq!(outputs.len(), 3);
507        assert_eq!(outputs[0].name(), "a");
508        assert_eq!(outputs[0].description().into_string(), "An integer");
509        assert_eq!(outputs[1].name(), "b");
510        assert_eq!(outputs[1].description().into_string(), "A different place!");
511        assert_eq!(outputs[2].name(), "c");
512        assert_eq!(outputs[2].description().into_string(), "Another integer");
513    }
514}