ortho_config_macros 0.9.0

Procedural macros for ortho_config.
Documentation
//! Documentation-related attribute parsing.

use syn::meta::ParseNestedMeta;
use syn::spanned::Spanned;
use syn::{LitBool, Token};

use super::doc_types::{
    DocExampleAttr, DocFieldAttrs, DocLinkAttr, DocNoteAttr, DocStructAttrs, HeadingOverrides,
    PrecedenceAttrs, WindowsAttrs,
};
use super::literals::{lit_char, lit_str};
use super::{FieldAttrs, discard_unknown};

pub(crate) fn apply_struct_doc_attr(
    meta: &ParseNestedMeta,
    out: &mut DocStructAttrs,
) -> syn::Result<bool> {
    let Some(ident) = meta.path.get_ident() else {
        return Ok(false);
    };
    let key = ident.to_string();
    match key.as_str() {
        "about_id" => {
            out.about_id = Some(lit_str(meta, "about_id")?.value());
            Ok(true)
        }
        "synopsis_id" => {
            out.synopsis_id = Some(lit_str(meta, "synopsis_id")?.value());
            Ok(true)
        }
        "bin_name" => {
            out.bin_name = Some(lit_str(meta, "bin_name")?.value());
            Ok(true)
        }
        "headings" => {
            let mut headings = out.headings.clone();
            parse_headings_meta(meta, &mut headings)?;
            out.headings = headings;
            Ok(true)
        }
        "precedence" => {
            let mut precedence = out.precedence.take().unwrap_or_default();
            parse_precedence_meta(meta, &mut precedence)?;
            out.precedence = Some(precedence);
            Ok(true)
        }
        "example" => {
            out.examples.push(parse_example_meta(meta)?);
            Ok(true)
        }
        "link" => {
            out.links.push(parse_link_meta(meta)?);
            Ok(true)
        }
        "note" => {
            out.notes.push(parse_note_meta(meta)?);
            Ok(true)
        }
        "windows" => {
            let mut windows = out.windows.take().unwrap_or_default();
            parse_windows_meta(meta, &mut windows)?;
            out.windows = Some(windows);
            Ok(true)
        }
        _ => Ok(false),
    }
}
pub(crate) fn apply_field_doc_attr(
    meta: &ParseNestedMeta,
    out: &mut FieldAttrs,
) -> syn::Result<bool> {
    let Some(ident) = meta.path.get_ident() else {
        return Ok(false);
    };
    let key = ident.to_string();
    match key.as_str() {
        "help_id" => {
            out.doc.help_id = Some(lit_str(meta, "help_id")?.value());
            Ok(true)
        }
        "long_help_id" => {
            out.doc.long_help_id = Some(lit_str(meta, "long_help_id")?.value());
            Ok(true)
        }
        "value" => {
            parse_value_meta(meta, &mut out.doc)?;
            Ok(true)
        }
        "deprecated" => {
            parse_deprecated_meta(meta, &mut out.doc)?;
            Ok(true)
        }
        "required" => {
            out.doc.required = Some(parse_bool(meta)?);
            Ok(true)
        }
        "env" => {
            parse_env_meta(meta, &mut out.doc)?;
            Ok(true)
        }
        "file" => {
            parse_file_meta(meta, &mut out.doc)?;
            Ok(true)
        }
        "cli" => {
            parse_cli_meta(meta, out)?;
            Ok(true)
        }
        "example" => {
            out.doc.examples.push(parse_example_meta(meta)?);
            Ok(true)
        }
        "link" => {
            out.doc.links.push(parse_link_meta(meta)?);
            Ok(true)
        }
        "note" => {
            out.doc.notes.push(parse_note_meta(meta)?);
            Ok(true)
        }
        _ => Ok(false),
    }
}
fn parse_headings_meta(meta: &ParseNestedMeta, headings: &mut HeadingOverrides) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| set_heading_field(&nested, headings))
}

/// Sets the appropriate `HeadingOverrides` field based on the key.
fn set_heading_field(nested: &ParseNestedMeta, headings: &mut HeadingOverrides) -> syn::Result<()> {
    let Some(ident) = nested.path.get_ident() else {
        return discard_unknown(nested);
    };
    let key = ident.to_string();
    let field_ref = match key.as_str() {
        "name" => &mut headings.name,
        "synopsis" => &mut headings.synopsis,
        "description" => &mut headings.description,
        "options" => &mut headings.options,
        "environment" => &mut headings.environment,
        "files" => &mut headings.files,
        "precedence" => &mut headings.precedence,
        "exit_status" => &mut headings.exit_status,
        "examples" => &mut headings.examples,
        "see_also" => &mut headings.see_also,
        _ => return discard_unknown(nested),
    };
    *field_ref = Some(lit_str(nested, &key)?.value());
    Ok(())
}
fn parse_precedence_meta(
    meta: &ParseNestedMeta,
    precedence: &mut PrecedenceAttrs,
) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("order") {
            precedence.order = parse_string_array(&nested, "order")?;
            return Ok(());
        }
        if nested.path.is_ident("rationale_id") {
            precedence.rationale_id = Some(lit_str(&nested, "rationale_id")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_windows_meta(meta: &ParseNestedMeta, windows: &mut WindowsAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("module_name") {
            windows.module_name = Some(lit_str(&nested, "module_name")?.value());
            return Ok(());
        }
        if nested.path.is_ident("export_aliases") {
            windows.export_aliases = parse_string_array(&nested, "export_aliases")?;
            return Ok(());
        }
        if nested.path.is_ident("include_common_parameters") {
            windows.include_common_parameters = Some(parse_bool(&nested)?);
            return Ok(());
        }
        if nested.path.is_ident("split_subcommands") {
            windows.split_subcommands = Some(parse_bool(&nested)?);
            return Ok(());
        }
        if nested.path.is_ident("help_info_uri") {
            windows.help_info_uri = Some(lit_str(&nested, "help_info_uri")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_example_meta(meta: &ParseNestedMeta) -> syn::Result<DocExampleAttr> {
    let mut title_id = None;
    let mut code = None;
    let mut body_id = None;
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("title_id") {
            title_id = Some(lit_str(&nested, "title_id")?.value());
            return Ok(());
        }
        if nested.path.is_ident("code") {
            code = Some(lit_str(&nested, "code")?.value());
            return Ok(());
        }
        if nested.path.is_ident("body_id") {
            body_id = Some(lit_str(&nested, "body_id")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })?;
    let code_value =
        code.ok_or_else(|| syn::Error::new(meta.path.span(), "example requires code"))?;
    Ok(DocExampleAttr {
        title_id,
        code: code_value,
        body_id,
    })
}
fn parse_link_meta(meta: &ParseNestedMeta) -> syn::Result<DocLinkAttr> {
    let mut text_id = None;
    let mut uri = None;
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("text_id") {
            text_id = Some(lit_str(&nested, "text_id")?.value());
            return Ok(());
        }
        if nested.path.is_ident("uri") {
            uri = Some(lit_str(&nested, "uri")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })?;
    let uri_value = uri.ok_or_else(|| syn::Error::new(meta.path.span(), "link requires uri"))?;
    Ok(DocLinkAttr {
        text_id,
        uri: uri_value,
    })
}
fn parse_note_meta(meta: &ParseNestedMeta) -> syn::Result<DocNoteAttr> {
    let mut text_id = None;
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("text_id") {
            text_id = Some(lit_str(&nested, "text_id")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })?;
    let text_id_value =
        text_id.ok_or_else(|| syn::Error::new(meta.path.span(), "note requires text_id"))?;
    Ok(DocNoteAttr {
        text_id: text_id_value,
    })
}
fn parse_value_meta(meta: &ParseNestedMeta, out: &mut DocFieldAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("type") {
            out.value_type = Some(lit_str(&nested, "type")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_deprecated_meta(meta: &ParseNestedMeta, out: &mut DocFieldAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("note_id") {
            out.deprecated_note_id = Some(lit_str(&nested, "note_id")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_env_meta(meta: &ParseNestedMeta, out: &mut DocFieldAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("name") {
            out.env_name = Some(lit_str(&nested, "name")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_file_meta(meta: &ParseNestedMeta, out: &mut DocFieldAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("key_path") {
            out.file_key_path = Some(lit_str(&nested, "key_path")?.value());
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_cli_meta(meta: &ParseNestedMeta, out: &mut FieldAttrs) -> syn::Result<()> {
    meta.parse_nested_meta(|nested| {
        if nested.path.is_ident("long") {
            out.cli_long = Some(lit_str(&nested, "long")?.value());
            return Ok(());
        }
        if nested.path.is_ident("short") {
            out.cli_short = Some(lit_char(&nested, "short")?);
            return Ok(());
        }
        if nested.path.is_ident("value_name") {
            out.doc.cli_value_name = Some(lit_str(&nested, "value_name")?.value());
            return Ok(());
        }
        if nested.path.is_ident("hide_in_help") {
            out.doc.cli_hide_in_help = parse_bool(&nested)?;
            return Ok(());
        }
        discard_unknown(&nested)
    })
}
fn parse_string_array(meta: &ParseNestedMeta, key: &str) -> syn::Result<Vec<String>> {
    let expr_value = meta.value()?.parse::<syn::Expr>()?;
    let expr_array = match expr_value {
        syn::Expr::Array(array) => array,
        other => {
            return Err(syn::Error::new(
                other.span(),
                format!("{key} must be an array of string literals"),
            ));
        }
    };
    let mut values = Vec::new();
    for element in expr_array.elems {
        match element {
            syn::Expr::Lit(lit_expr) => {
                if let syn::Lit::Str(s) = lit_expr.lit {
                    values.push(s.value());
                } else {
                    return Err(syn::Error::new(
                        lit_expr.span(),
                        format!("{key} must contain string literals"),
                    ));
                }
            }
            other => {
                return Err(syn::Error::new(
                    other.span(),
                    format!("{key} must contain string literals"),
                ));
            }
        }
    }
    Ok(values)
}
fn parse_bool(meta: &ParseNestedMeta) -> syn::Result<bool> {
    if meta.input.peek(Token![=]) {
        return Ok(meta.value()?.parse::<LitBool>()?.value);
    }
    Ok(true)
}