alef 0.21.1

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Option decoding for R bindings.

use extendr_api::prelude::*;

/// Helper: extract and convert a value from an R list by name.
fn list_get(list: &List, key: &str) -> Option<Robj> {
    let names = list.names().ok()?;
    names
        .iter()
        .zip(list.iter())
        .find(|(name, _)| name == key)
        .map(|(_, val)| val)
}

/// Decode a heading style enum from its string representation.
fn decode_heading_style(val: Robj) -> std::result::Result<crate::HeadingStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("heading_style: {e}"))?;
    match s.as_str() {
        "Atx" => Ok(crate::HeadingStyle::Atx),
        "Underlined" => Ok(crate::HeadingStyle::Underlined),
        "AtxClosed" => Ok(crate::HeadingStyle::AtxClosed),
        _ => Err(format!("heading_style: unknown variant '{}'", s)),
    }
}

/// Decode a list indent type enum from its string representation.
fn decode_list_indent_type(val: Robj) -> std::result::Result<crate::ListIndentType, String> {
    let s = String::try_from(&val).map_err(|e| format!("list_indent_type: {e}"))?;
    match s.as_str() {
        "Spaces" => Ok(crate::ListIndentType::Spaces),
        "Tabs" => Ok(crate::ListIndentType::Tabs),
        _ => Err(format!("list_indent_type: unknown variant '{}'", s)),
    }
}

/// Decode a highlight style enum from its string representation.
fn decode_highlight_style(val: Robj) -> std::result::Result<crate::HighlightStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("highlight_style: {e}"))?;
    match s.as_str() {
        "DoubleEqual" => Ok(crate::HighlightStyle::DoubleEqual),
        "SingleMark" => Ok(crate::HighlightStyle::SingleMark),
        _ => Err(format!("highlight_style: unknown variant '{}'", s)),
    }
}

/// Decode a whitespace mode enum from its string representation.
fn decode_whitespace_mode(val: Robj) -> std::result::Result<crate::WhitespaceMode, String> {
    let s = String::try_from(&val).map_err(|e| format!("whitespace_mode: {e}"))?;
    match s.as_str() {
        "Normalized" => Ok(crate::WhitespaceMode::Normalized),
        "Strict" => Ok(crate::WhitespaceMode::Strict),
        _ => Err(format!("whitespace_mode: unknown variant '{}'", s)),
    }
}

/// Decode a newline style enum from its string representation.
fn decode_newline_style(val: Robj) -> std::result::Result<crate::NewlineStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("newline_style: {e}"))?;
    match s.as_str() {
        "Spaces" => Ok(crate::NewlineStyle::Spaces),
        "Backslash" => Ok(crate::NewlineStyle::Backslash),
        "TwoSpaces" => Ok(crate::NewlineStyle::TwoSpaces),
        "Html" => Ok(crate::NewlineStyle::Html),
        _ => Err(format!("newline_style: unknown variant '{}'", s)),
    }
}

/// Decode a code block style enum from its string representation.
fn decode_code_block_style(val: Robj) -> std::result::Result<crate::CodeBlockStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("code_block_style: {e}"))?;
    match s.as_str() {
        "Backtick" => Ok(crate::CodeBlockStyle::Backtick),
        "Tilde" => Ok(crate::CodeBlockStyle::Tilde),
        _ => Err(format!("code_block_style: unknown variant '{}'", s)),
    }
}

/// Decode a highlight style enum from its string representation.
fn decode_url_escape_style(val: Robj) -> std::result::Result<crate::UrlEscapeStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("url_escape_style: {e}"))?;
    match s.as_str() {
        "Angle" => Ok(crate::UrlEscapeStyle::Angle),
        "Percent" => Ok(crate::UrlEscapeStyle::Percent),
        _ => Err(format!("url_escape_style: unknown variant '{}'", s)),
    }
}

/// Decode a link style enum from its string representation.
fn decode_link_style(val: Robj) -> std::result::Result<crate::LinkStyle, String> {
    let s = String::try_from(&val).map_err(|e| format!("link_style: {e}"))?;
    match s.as_str() {
        "Inline" => Ok(crate::LinkStyle::Inline),
        "Reference" => Ok(crate::LinkStyle::Reference),
        _ => Err(format!("link_style: unknown variant '{}'", s)),
    }
}

/// Decode an output format enum from its string representation.
fn decode_output_format(val: Robj) -> std::result::Result<crate::OutputFormat, String> {
    let s = String::try_from(&val).map_err(|e| format!("output_format: {e}"))?;
    match s.as_str() {
        "Markdown" => Ok(crate::OutputFormat::Markdown),
        "PlainText" => Ok(crate::OutputFormat::PlainText),
        _ => Err(format!("output_format: unknown variant '{}'", s)),
    }
}

/// Decode preprocessing options from an R list.
fn decode_preprocessing_options(val: Robj) -> std::result::Result<crate::PreprocessingOptions, String> {
    if val.is_null() {
        return Ok(crate::PreprocessingOptions::default());
    }
    let list = List::try_from(&val).map_err(|e| format!("preprocessing: {e}"))?;
    let mut opts = crate::PreprocessingOptions::default();

    if let Some(v) = list_get(&list, "enabled") {
        opts.enabled = bool::try_from(&v).map_err(|e| format!("preprocessing.enabled: {e}"))?;
    }
    if let Some(v) = list_get(&list, "preset") {
        opts.preset = decode_preprocessing_preset(v)?;
    }
    if let Some(v) = list_get(&list, "remove_navigation") {
        opts.remove_navigation = bool::try_from(&v).map_err(|e| format!("preprocessing.remove_navigation: {e}"))?;
    }
    if let Some(v) = list_get(&list, "remove_forms") {
        opts.remove_forms = bool::try_from(&v).map_err(|e| format!("preprocessing.remove_forms: {e}"))?;
    }

    Ok(opts)
}

/// Decode a preprocessing preset enum from its string representation.
fn decode_preprocessing_preset(val: Robj) -> std::result::Result<crate::PreprocessingPreset, String> {
    let s = String::try_from(&val).map_err(|e| format!("preprocessing.preset: {e}"))?;
    match s.as_str() {
        "Minimal" => Ok(crate::PreprocessingPreset::Minimal),
        "Standard" => Ok(crate::PreprocessingPreset::Standard),
        "Aggressive" => Ok(crate::PreprocessingPreset::Aggressive),
        _ => Err(format!("preprocessing.preset: unknown variant '{}'", s)),
    }
}

/// Decode an R ExternalPtr, NULL, or named list into ConversionOptions.
///
/// Accepts:
/// - ExternalPtr<ConversionOptions> (from $default() or builder methods) — unwraps and converts
/// - NULL — returns default ConversionOptions
/// - Named list with field names matching struct fields — decodes field by field
///
/// Fields are optional: omitted fields retain their defaults. Unknown fields are ignored.
pub fn decode_options(options: Robj) -> std::result::Result<crate::ConversionOptions, String> {
    if options.is_null() {
        return Ok(crate::ConversionOptions::default());
    }

    // Accept the wrapper struct returned by `ConversionOptions$default()` / builder methods,
    // which extendr exposes as an `ExternalPtr`. The binding struct is returned directly
    // from the #[extendr] impl methods, so unwrap it as the binding type.
    if let Ok(ext) = ExternalPtr::<crate::ConversionOptions>::try_from(&options) {
        // Clone the binding struct and convert to core type via the generated From impl
        return Ok((*ext).clone().into());
    }

    // Try to decode as a named list
    let list = List::try_from(&options).map_err(|e| format!("options must be NULL, ExternalPtr, or named list: {e}"))?;
    let mut opts = crate::ConversionOptions::default();

    // Decode each optional field from the list
    if let Some(v) = list_get(&list, "heading_style") {
        opts.heading_style = decode_heading_style(v)?;
    }
    if let Some(v) = list_get(&list, "list_indent_type") {
        opts.list_indent_type = decode_list_indent_type(v)?;
    }
    if let Some(v) = list_get(&list, "list_indent_width") {
        opts.list_indent_width = usize::try_from(&v).map_err(|e| format!("list_indent_width: {e}"))?;
    }
    if let Some(v) = list_get(&list, "bullets") {
        opts.bullets = String::try_from(&v).map_err(|e| format!("bullets: {e}"))?;
    }
    if let Some(v) = list_get(&list, "strong_em_symbol") {
        let s = String::try_from(&v).map_err(|e| format!("strong_em_symbol: {e}"))?;
        if s.len() != 1 {
            return Err("strong_em_symbol: must be a single character string".to_string());
        }
        opts.strong_em_symbol = s.chars().next().unwrap();
    }
    if let Some(v) = list_get(&list, "escape_asterisks") {
        opts.escape_asterisks = bool::try_from(&v).map_err(|e| format!("escape_asterisks: {e}"))?;
    }
    if let Some(v) = list_get(&list, "escape_underscores") {
        opts.escape_underscores = bool::try_from(&v).map_err(|e| format!("escape_underscores: {e}"))?;
    }
    if let Some(v) = list_get(&list, "escape_misc") {
        opts.escape_misc = bool::try_from(&v).map_err(|e| format!("escape_misc: {e}"))?;
    }
    if let Some(v) = list_get(&list, "escape_ascii") {
        opts.escape_ascii = bool::try_from(&v).map_err(|e| format!("escape_ascii: {e}"))?;
    }
    if let Some(v) = list_get(&list, "code_language") {
        opts.code_language = String::try_from(&v).map_err(|e| format!("code_language: {e}"))?;
    }
    if let Some(v) = list_get(&list, "autolinks") {
        opts.autolinks = bool::try_from(&v).map_err(|e| format!("autolinks: {e}"))?;
    }
    if let Some(v) = list_get(&list, "default_title") {
        opts.default_title = bool::try_from(&v).map_err(|e| format!("default_title: {e}"))?;
    }
    if let Some(v) = list_get(&list, "br_in_tables") {
        opts.br_in_tables = bool::try_from(&v).map_err(|e| format!("br_in_tables: {e}"))?;
    }
    if let Some(v) = list_get(&list, "compact_tables") {
        opts.compact_tables = bool::try_from(&v).map_err(|e| format!("compact_tables: {e}"))?;
    }
    if let Some(v) = list_get(&list, "highlight_style") {
        opts.highlight_style = decode_highlight_style(v)?;
    }
    if let Some(v) = list_get(&list, "extract_metadata") {
        opts.extract_metadata = bool::try_from(&v).map_err(|e| format!("extract_metadata: {e}"))?;
    }
    if let Some(v) = list_get(&list, "whitespace_mode") {
        opts.whitespace_mode = decode_whitespace_mode(v)?;
    }
    if let Some(v) = list_get(&list, "strip_newlines") {
        opts.strip_newlines = bool::try_from(&v).map_err(|e| format!("strip_newlines: {e}"))?;
    }
    if let Some(v) = list_get(&list, "wrap") {
        opts.wrap = bool::try_from(&v).map_err(|e| format!("wrap: {e}"))?;
    }
    if let Some(v) = list_get(&list, "wrap_width") {
        opts.wrap_width = usize::try_from(&v).map_err(|e| format!("wrap_width: {e}"))?;
    }
    if let Some(v) = list_get(&list, "convert_as_inline") {
        opts.convert_as_inline = bool::try_from(&v).map_err(|e| format!("convert_as_inline: {e}"))?;
    }
    if let Some(v) = list_get(&list, "sub_symbol") {
        opts.sub_symbol = String::try_from(&v).map_err(|e| format!("sub_symbol: {e}"))?;
    }
    if let Some(v) = list_get(&list, "sup_symbol") {
        opts.sup_symbol = String::try_from(&v).map_err(|e| format!("sup_symbol: {e}"))?;
    }
    if let Some(v) = list_get(&list, "newline_style") {
        opts.newline_style = decode_newline_style(v)?;
    }
    if let Some(v) = list_get(&list, "code_block_style") {
        opts.code_block_style = decode_code_block_style(v)?;
    }
    if let Some(v) = list_get(&list, "keep_inline_images_in") {
        let vec: Vec<String> = Strings::try_from(&v)
            .map_err(|e| format!("keep_inline_images_in: {e}"))?
            .iter()
            .map(String::from)
            .collect();
        opts.keep_inline_images_in = vec;
    }
    if let Some(v) = list_get(&list, "preprocessing") {
        opts.preprocessing = decode_preprocessing_options(v)?;
    }
    if let Some(v) = list_get(&list, "encoding") {
        opts.encoding = String::try_from(&v).map_err(|e| format!("encoding: {e}"))?;
    }
    if let Some(v) = list_get(&list, "debug") {
        opts.debug = bool::try_from(&v).map_err(|e| format!("debug: {e}"))?;
    }
    if let Some(v) = list_get(&list, "strip_tags") {
        let vec: Vec<String> = Strings::try_from(&v)
            .map_err(|e| format!("strip_tags: {e}"))?
            .iter()
            .map(String::from)
            .collect();
        opts.strip_tags = vec;
    }
    if let Some(v) = list_get(&list, "preserve_tags") {
        let vec: Vec<String> = Strings::try_from(&v)
            .map_err(|e| format!("preserve_tags: {e}"))?
            .iter()
            .map(String::from)
            .collect();
        opts.preserve_tags = vec;
    }
    if let Some(v) = list_get(&list, "skip_images") {
        opts.skip_images = bool::try_from(&v).map_err(|e| format!("skip_images: {e}"))?;
    }
    if let Some(v) = list_get(&list, "url_escape_style") {
        opts.url_escape_style = decode_url_escape_style(v)?;
    }
    if let Some(v) = list_get(&list, "link_style") {
        opts.link_style = decode_link_style(v)?;
    }
    if let Some(v) = list_get(&list, "output_format") {
        opts.output_format = decode_output_format(v)?;
    }
    if let Some(v) = list_get(&list, "include_document_structure") {
        opts.include_document_structure = bool::try_from(&v).map_err(|e| format!("include_document_structure: {e}"))?;
    }
    if let Some(v) = list_get(&list, "extract_images") {
        opts.extract_images = bool::try_from(&v).map_err(|e| format!("extract_images: {e}"))?;
    }
    if let Some(v) = list_get(&list, "max_image_size") {
        opts.max_image_size = u64::try_from(&v).map_err(|e| format!("max_image_size: {e}"))?;
    }
    if let Some(v) = list_get(&list, "capture_svg") {
        opts.capture_svg = bool::try_from(&v).map_err(|e| format!("capture_svg: {e}"))?;
    }
    if let Some(v) = list_get(&list, "infer_dimensions") {
        opts.infer_dimensions = bool::try_from(&v).map_err(|e| format!("infer_dimensions: {e}"))?;
    }
    if let Some(v) = list_get(&list, "max_depth") {
        // max_depth is Option<usize> — NULL means None, integer means Some(value)
        if !v.is_null() {
            let depth = usize::try_from(&v).map_err(|e| format!("max_depth: {e}"))?;
            opts.max_depth = Some(depth);
        }
    }
    if let Some(v) = list_get(&list, "exclude_selectors") {
        let vec: Vec<String> = Strings::try_from(&v)
            .map_err(|e| format!("exclude_selectors: {e}"))?
            .iter()
            .map(String::from)
            .collect();
        opts.exclude_selectors = vec;
    }
    // Note: visitor field is skipped — R has no visitor concept, so it remains at default None

    Ok(opts)
}