alef 0.115.4

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::{ErrorDef, ErrorVariant, SwiftMapper, associated_values};
use crate::core::ir::{PrimitiveType, TypeRef};
use std::collections::BTreeSet;

#[derive(serde::Serialize)]
struct Case {
    name: String,
    pattern: String,
    expression: String,
}

fn literal(value: &str) -> String {
    let mut result = String::from("\"");
    for character in value.chars() {
        match character {
            '\\' => result.push_str("\\\\"),
            '"' => result.push_str("\\\""),
            '\n' => result.push_str("\\n"),
            '\r' => result.push_str("\\r"),
            '\t' => result.push_str("\\t"),
            control if control.is_control() => result.push_str(&format!("\\u{{{:x}}}", u32::from(control))),
            other => result.push(other),
        }
    }
    result.push('"');
    result
}

fn display_field(variant: &ErrorVariant, name: &str, mapper: &SwiftMapper) -> Option<String> {
    let values = associated_values(variant, mapper);
    let value = if let Ok(index) = name.parse::<usize>() {
        values
            .iter()
            .find(|value| value.field.is_some_and(|field| field.name == format!("_{index}")))
    } else {
        values
            .iter()
            .find(|value| value.field.is_some_and(|field| field.name == name))
    }?;
    let field = value.field?;
    if field.binding_excluded || field.optional {
        return None;
    }
    matches!(
        field.ty,
        TypeRef::String
            | TypeRef::Primitive(
                PrimitiveType::Bool
                    | PrimitiveType::I8
                    | PrimitiveType::I16
                    | PrimitiveType::I32
                    | PrimitiveType::I64
                    | PrimitiveType::Isize
                    | PrimitiveType::U8
                    | PrimitiveType::U16
                    | PrimitiveType::U32
                    | PrimitiveType::U64
                    | PrimitiveType::Usize
            )
    )
    .then(|| value.label.clone())
}

fn placeholder(characters: &mut impl Iterator<Item = char>) -> Option<String> {
    let mut name = String::new();
    for character in characters.by_ref() {
        match character {
            '}' => return Some(name),
            '{' => return None,
            other => name.push(other),
        }
    }
    None
}

fn template_expression(variant: &ErrorVariant, mapper: &SwiftMapper) -> Option<(String, BTreeSet<String>)> {
    let template = variant.message_template.as_deref()?;
    let mut characters = template.chars().peekable();
    let mut text = String::new();
    let mut pieces = Vec::new();
    let mut bindings = BTreeSet::new();
    while let Some(character) = characters.next() {
        if matches!(character, '{' | '}') && characters.peek() == Some(&character) {
            characters.next();
            text.push(character);
            continue;
        }
        match character {
            '{' => {
                pieces.push(literal(&text));
                text.clear();
                let name = placeholder(&mut characters)?;
                let binding = display_field(variant, &name, mapper)?;
                pieces.push(format!("String(describing: {binding})"));
                bindings.insert(binding);
            }
            '}' => return None,
            other => text.push(other),
        }
    }
    pieces.push(literal(&text));
    Some((pieces.join(" + "), bindings))
}

fn description_case(variant: &ErrorVariant, mapper: &SwiftMapper) -> Case {
    let values = if variant.is_unit || variant.fields.is_empty() {
        Vec::new()
    } else {
        associated_values(variant, mapper)
    };
    let synthetic_message = values.iter().find(|value| value.field.is_none());
    let (expression, bindings) = if let Some(value) = synthetic_message {
        (value.label.clone(), BTreeSet::from([value.label.clone()]))
    } else if let Some(rendered) = template_expression(variant, mapper) {
        rendered
    } else if let Some(value) = values.iter().find(|value| value.label == "message") {
        (
            format!("String(describing: {})", value.label),
            BTreeSet::from([value.label.clone()]),
        )
    } else {
        (literal(&variant.name), BTreeSet::new())
    };
    let pattern = if values.is_empty() {
        String::new()
    } else {
        format!(
            "({})",
            values
                .iter()
                .map(|value| {
                    if bindings.contains(&value.label) {
                        format!("{}: let {}", value.label, value.label)
                    } else {
                        format!("{}: _", value.label)
                    }
                })
                .collect::<Vec<_>>()
                .join(", ")
        )
    };
    Case {
        name: super::super::enums::swift_variant_case(&variant.name),
        pattern,
        expression,
    }
}

pub(super) fn emit(error: &ErrorDef, name: &str, mapper: &SwiftMapper, out: &mut String) {
    let mut cases = error
        .variants
        .iter()
        .map(|variant| description_case(variant, mapper))
        .collect::<Vec<_>>();
    if !cases.iter().any(|case| case.name == "validation") {
        cases.push(Case {
            name: "validation".into(),
            pattern: "(message: let message, source: _)".into(),
            expression: "message".into(),
        });
    }
    out.push_str(&crate::backends::swift::template_env::render(
        "swift_error_description.swift.jinja",
        crate::alef_context! { name => name, cases => cases },
    ));
}

#[cfg(test)]
#[path = "errors_description_tests.rs"]
mod tests;