alef 0.115.7

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Text assertions on a `fields_display_as_text` field.
//!
//! Such a field holds a data enum (for example a message `content` with a text variant and a
//! parts variant). The pyo3 binding exposes the text variant as the `.text` property. A
//! comparison through `str()` reads the enum's string form and passes even when `.text`
//! returns `None`, so the comparison reads `.text` instead, and a `.text` that is `None` fails
//! the assertion. ~keep

use crate::e2e::fixture::Assertion;

use super::super::json::value_to_python_string;

/// Render a text assertion through the `.text` accessor of `field_access`.
///
/// `fold_case` is true when the field is an enum field: the standard assertions compare those
/// case-insensitively, and every other field exactly. `starts_with` and `ends_with` are exact
/// for both.
///
/// Returns `false` and writes nothing when the assertion is not a text comparison against string
/// values. The caller then renders it as for any other field: `not_empty`, `is_empty` and the
/// length checks ask whether a value is present, and a parts value is present but has no text.
pub(super) fn try_render(
    out: &mut String,
    assertion: &Assertion,
    field_access: &str,
    field_is_optional: bool,
    fold_case: bool,
) -> bool {
    let kind = assertion.assertion_type.as_str();
    let expected: Vec<&serde_json::Value> = match kind {
        "equals" | "contains" | "starts_with" | "ends_with" => assertion.value.iter().collect(),
        "contains_all" | "contains_any" => assertion.values.iter().flatten().collect(),
        "not_contains" => assertion.expected_values(),
        _ => return false,
    };
    if expected.is_empty() || !expected.iter().all(|v| v.is_string()) {
        return false;
    }
    let literals: Vec<String> = expected.iter().map(|v| value_to_python_string(v)).collect();
    let text = format!("{field_access}.text");
    // `.text` on `None` raises `AttributeError`, so an optional field is checked for presence
    // first. `not_contains` keeps its meaning for an absent value: nothing is contained. A
    // present value whose `.text` is `None` fails every comparison, `not_contains` included. ~keep
    let guard = if field_is_optional && kind == "not_contains" {
        format!("{field_access} is None or ")
    } else {
        String::new()
    };
    if field_is_optional && kind != "not_contains" {
        out.push_str(&format!("    assert {field_access} is not None\n"));
    }
    out.push_str(&format!("    assert {guard}{text} is not None\n"));
    let lower = if fold_case { ".lower()" } else { "" };
    match kind {
        "equals" => out.push_str(&format!("    assert {text}{lower} == {}{lower}\n", literals[0])),
        "contains" | "contains_all" => {
            for literal in &literals {
                out.push_str(&format!("    assert {literal}{lower} in {text}{lower}\n"));
            }
        }
        "contains_any" => {
            let list = literals.join(", ");
            out.push_str(&format!(
                "    assert any(v{lower} in {text}{lower} for v in [{list}])\n"
            ));
        }
        "not_contains" => {
            for literal in &literals {
                out.push_str(&format!("    assert {guard}{literal}{lower} not in {text}{lower}\n"));
            }
        }
        "starts_with" => out.push_str(&format!("    assert {text}.startswith({})\n", literals[0])),
        "ends_with" => out.push_str(&format!("    assert {text}.endswith({})\n", literals[0])),
        _ => return false,
    }
    true
}