subplot 0.2.0

tools for specifying, documenting, and implementing automated acceptance tests for systems and software
Documentation
use crate::{resource, Result};
use crate::{Bindings, TemplateSpec};

use std::ops::Deref;
use std::path::{Path, PathBuf};

use pandoc_ast::{Inline, Map, MetaValue, Pandoc};

/// Metadata of a document, as needed by Subplot.
#[derive(Debug)]
pub struct Metadata {
    title: String,
    date: Option<String>,
    bindings_filenames: Vec<PathBuf>,
    bindings: Bindings,
    functions_filenames: Vec<PathBuf>,
    template: Option<String>,
    spec: Option<TemplateSpec>,
    bibliographies: Vec<PathBuf>,
    /// Extra class names which should be considered 'correct' for this document
    classes: Vec<String>,
}

impl Metadata {
    /// Construct a Metadata from a Document, if possible.
    pub fn new<P>(basedir: P, doc: &Pandoc) -> Result<Metadata>
    where
        P: AsRef<Path>,
    {
        let title = get_title(&doc.meta);
        let date = get_date(&doc.meta);
        let bindings_filenames = get_bindings_filenames(basedir.as_ref(), &doc.meta);
        let functions_filenames = get_functions_filenames(basedir.as_ref(), &doc.meta);
        let (template, spec) = if let Some((template, spec)) = get_template_spec(&doc.meta)? {
            resource::set_template(&template);
            (Some(template), Some(spec))
        } else {
            (None, None)
        };
        let mut bindings = Bindings::new();

        let bibliographies = get_bibliographies(basedir.as_ref(), &doc.meta);
        let classes = get_classes(&doc.meta);

        get_bindings(&bindings_filenames, &mut bindings)?;
        Ok(Metadata {
            title,
            date,
            bindings_filenames,
            bindings,
            functions_filenames,
            template,
            spec,
            bibliographies,
            classes,
        })
    }

    /// Return title of document.
    pub fn title(&self) -> &str {
        &self.title
    }

    /// Return date of document, if any.
    pub fn date(&self) -> Option<&str> {
        self.date.as_deref()
    }

    /// Return filename where bindings are specified.
    pub fn bindings_filenames(&self) -> Vec<&Path> {
        self.bindings_filenames.iter().map(|f| f.as_ref()).collect()
    }

    /// Return filename where functions are specified.
    pub fn functions_filenames(&self) -> Vec<&Path> {
        self.functions_filenames
            .iter()
            .map(|f| f.as_ref())
            .collect()
    }

    /// Return the name of the code template, if specified.
    pub fn template_name(&self) -> Option<&str> {
        self.template.as_deref()
    }

    /// Return the bindings.
    pub fn bindings(&self) -> &Bindings {
        &self.bindings
    }

    /// Return the bibliographies.
    pub fn bibliographies(&self) -> Vec<&Path> {
        self.bibliographies.iter().map(|x| x.as_path()).collect()
    }

    /// The classes which this document also claims are valid
    pub fn classes(&self) -> impl Iterator<Item = &str> {
        self.classes.iter().map(Deref::deref)
    }
}

type Mapp = Map<String, MetaValue>;

fn get_title(map: &Mapp) -> String {
    if let Some(s) = get_string(map, "title") {
        s
    } else {
        "".to_string()
    }
}

fn get_date(map: &Mapp) -> Option<String> {
    get_string(map, "date")
}

fn get_bindings_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
where
    P: AsRef<Path>,
{
    get_paths(basedir, map, "bindings")
}

fn get_functions_filenames<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
where
    P: AsRef<Path>,
{
    get_paths(basedir, map, "functions")
}

fn get_template_spec(map: &Mapp) -> Result<Option<(String, TemplateSpec)>> {
    match get_string(map, "template") {
        Some(s) => {
            let mut spec_path = PathBuf::from(&s);
            spec_path.push("template");
            spec_path.push("template.yaml");
            let spec = TemplateSpec::from_file(&spec_path)?;
            Ok(Some((s, spec)))
        }
        None => Ok(None),
    }
}

fn get_paths<P>(basedir: P, map: &Mapp, field: &str) -> Vec<PathBuf>
where
    P: AsRef<Path>,
{
    match map.get(field) {
        None => vec![],
        Some(v) => pathbufs(basedir, v),
    }
}

fn get_string(map: &Mapp, field: &str) -> Option<String> {
    let v = match map.get(field) {
        None => return None,
        Some(s) => s,
    };
    let v = match v {
        pandoc_ast::MetaValue::MetaString(s) => s.to_string(),
        pandoc_ast::MetaValue::MetaInlines(vec) => join(&vec),
        _ => panic!("don't know how to handle: {:?}", v),
    };
    Some(v)
}

fn get_bibliographies<P>(basedir: P, map: &Mapp) -> Vec<PathBuf>
where
    P: AsRef<Path>,
{
    let v = match map.get("bibliography") {
        None => return vec![],
        Some(s) => s,
    };
    pathbufs(basedir, v)
}

fn pathbufs<P>(basedir: P, v: &MetaValue) -> Vec<PathBuf>
where
    P: AsRef<Path>,
{
    let mut bufs = vec![];
    push_pathbufs(basedir, v, &mut bufs);
    bufs
}

fn get_classes(map: &Mapp) -> Vec<String> {
    let mut ret = Vec::new();
    if let Some(classes) = map.get("classes") {
        push_strings(classes, &mut ret);
    }
    ret
}

fn push_strings(v: &MetaValue, strings: &mut Vec<String>) {
    match v {
        MetaValue::MetaString(s) => strings.push(s.to_string()),
        MetaValue::MetaInlines(vec) => strings.push(join(&vec)),
        MetaValue::MetaList(values) => {
            for value in values {
                push_strings(value, strings);
            }
        }
        _ => panic!("don't know how to handle: {:?}", v),
    };
}

fn push_pathbufs<P>(basedir: P, v: &MetaValue, bufs: &mut Vec<PathBuf>)
where
    P: AsRef<Path>,
{
    match v {
        MetaValue::MetaString(s) => bufs.push(basedir.as_ref().join(Path::new(s))),
        MetaValue::MetaInlines(vec) => bufs.push(basedir.as_ref().join(Path::new(&join(&vec)))),
        MetaValue::MetaList(values) => {
            for value in values {
                push_pathbufs(basedir.as_ref(), value, bufs);
            }
        }
        _ => panic!("don't know how to handle: {:?}", v),
    };
}

fn join(vec: &[Inline]) -> String {
    let mut buf = String::new();
    join_into_buffer(vec, &mut buf);
    buf
}

fn join_into_buffer(vec: &[Inline], buf: &mut String) {
    for item in vec {
        match item {
            pandoc_ast::Inline::Str(s) => buf.push_str(&s),
            pandoc_ast::Inline::Emph(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::Strong(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::Strikeout(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::Superscript(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::Subscript(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::SmallCaps(v) => join_into_buffer(v, buf),
            pandoc_ast::Inline::Space => buf.push(' '),
            pandoc_ast::Inline::SoftBreak => buf.push(' '),
            pandoc_ast::Inline::LineBreak => buf.push(' '),
            pandoc_ast::Inline::Quoted(qtype, v) => {
                let quote = match qtype {
                    pandoc_ast::QuoteType::SingleQuote => '\'',
                    pandoc_ast::QuoteType::DoubleQuote => '"',
                };
                buf.push(quote);
                join_into_buffer(v, buf);
                buf.push(quote);
            }
            _ => panic!("unknown pandoc_ast::Inline component {:?}", item),
        }
    }
}

#[cfg(test)]
mod test_join {
    use super::join;
    use pandoc_ast::{Inline, QuoteType};

    #[test]
    fn join_all_kinds() {
        let v = vec![
            Inline::Str("a".to_string()),
            Inline::Emph(vec![Inline::Str("b".to_string())]),
            Inline::Strong(vec![Inline::Str("c".to_string())]),
            Inline::Strikeout(vec![Inline::Str("d".to_string())]),
            Inline::Superscript(vec![Inline::Str("e".to_string())]),
            Inline::Subscript(vec![Inline::Str("f".to_string())]),
            Inline::SmallCaps(vec![Inline::Str("g".to_string())]),
            Inline::Space,
            Inline::SoftBreak,
            Inline::Quoted(QuoteType::SingleQuote, vec![Inline::Str("h".to_string())]),
            Inline::LineBreak,
            Inline::Quoted(QuoteType::DoubleQuote, vec![Inline::Str("i".to_string())]),
        ];
        assert_eq!(join(&v), r#"abcdefg  'h' "i""#);
    }
}

fn get_bindings<P>(filenames: &[P], bindings: &mut Bindings) -> Result<()>
where
    P: AsRef<Path>,
{
    for filename in filenames {
        bindings.add_from_file(filename)?;
    }
    Ok(())
}