subplot 0.6.0

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

use std::collections::HashMap;
use std::fmt::Debug;
use std::ops::Deref;
use std::path::{Path, PathBuf};

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

use log::trace;

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

#[derive(Debug)]
pub struct DocumentImpl {
    spec: TemplateSpec,
    functions: Vec<PathBuf>,
}

impl Metadata {
    /// Construct a Metadata from a Document, if possible.
    pub fn new<P>(
        basedir: P,
        meta: &YamlMetadata,
        template: Option<&str>,
    ) -> Result<Metadata, SubplotError>
    where
        P: AsRef<Path> + Debug,
    {
        let map = meta.to_map();
        let title = get_title(&map);
        let date = get_date(&map);
        let bindings_filenames = get_bindings_filenames(&map);
        let bibliographies = get_bibliographies(basedir.as_ref(), &map);
        let classes = get_classes(&map);
        trace!("Loaded basic metadata");

        let mut impls = HashMap::new();

        if let Some(raw_impls) = map.get("impls") {
            match raw_impls {
                MetaValue::MetaMap(raw_impls) => {
                    for (impl_name, functions_filenames) in raw_impls.iter() {
                        let template_spec = load_template_spec(impl_name)?;
                        let filenames = pathbufs("", functions_filenames);
                        let docimpl = DocumentImpl::new(template_spec, filenames);
                        impls.insert(impl_name.to_string(), docimpl);
                    }
                }
                _ => {
                    trace!("Ignoring unknown raw implementation value");
                }
            }
        }

        let template = template.or_else(|| impls.keys().next().map(String::as_str));

        let mut bindings = Bindings::new();

        get_bindings(&bindings_filenames, &mut bindings, template)?;

        trace!("Loaded all metadata successfully");

        Ok(Metadata {
            basedir: basedir.as_ref().to_path_buf(),
            title,
            date,
            markdown_filename: meta.markdown().into(),
            bindings_filenames,
            bindings,
            impls,
            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 base dir for all relative filenames.
    pub fn basedir(&self) -> &Path {
        &self.basedir
    }

    /// Return filename of the markdown file.
    pub fn markdown_filename(&self) -> &Path {
        &self.markdown_filename
    }

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

    /// Return the document implementation (filenames, spec, etc) for the given template name
    pub fn document_impl(&self, template: &str) -> Option<&DocumentImpl> {
        self.impls.get(template)
    }

    /// Return the templates the document expects to implement
    pub fn templates(&self) -> impl Iterator<Item = &str> {
        self.impls.keys().map(String::as_str)
    }

    /// 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)
    }
}

impl DocumentImpl {
    fn new(spec: TemplateSpec, functions: Vec<PathBuf>) -> Self {
        Self { spec, functions }
    }

    pub fn functions_filenames(&self) -> impl Iterator<Item = &Path> {
        self.functions.iter().map(PathBuf::as_path)
    }

    pub fn spec(&self) -> &TemplateSpec {
        &self.spec
    }
}

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(map: &Mapp) -> Vec<PathBuf> {
    get_paths("", map, "bindings")
}

fn load_template_spec(template: &str) -> Result<TemplateSpec, SubplotError> {
    let mut spec_path = PathBuf::from(template);
    spec_path.push("template");
    spec_path.push("template.yaml");
    TemplateSpec::from_file(&spec_path)
}

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::Code(_, 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,
    template: Option<&str>,
) -> Result<(), SubplotError>
where
    P: AsRef<Path> + Debug,
{
    for filename in filenames {
        bindings.add_from_file(filename, template)?;
    }
    Ok(())
}