omena-abstract-value 0.2.0

Abstract class value domain and selector projection contracts
Documentation
use std::collections::BTreeSet;

use crate::{
    AbstractPropertyValueCandidateV0, AbstractPropertyValueNarrowingV0, AbstractPropertyValueV0,
};

pub fn narrow_abstract_property_value_for_pseudo_state(
    property_name: &str,
    requested_pseudo_state: Option<&str>,
    candidates: &[AbstractPropertyValueCandidateV0],
) -> AbstractPropertyValueNarrowingV0 {
    let matched = candidates
        .iter()
        .filter(|candidate| candidate.property_name == property_name)
        .filter(|candidate| {
            candidate.pseudo_state.as_deref().is_none()
                || candidate.pseudo_state.as_deref() == requested_pseudo_state
        })
        .collect::<Vec<_>>();
    let value = abstract_property_value_from_matched_candidates(
        property_name,
        requested_pseudo_state,
        &matched,
    );

    AbstractPropertyValueNarrowingV0 {
        schema_version: "0",
        product: "omena-abstract-value.property-value-narrowing",
        stylesheet_scope: "singleStylesheet",
        property_name: property_name.to_string(),
        requested_pseudo_state: requested_pseudo_state.map(str::to_string),
        candidate_count: candidates.len(),
        matched_candidate_count: matched.len(),
        value,
    }
}

fn abstract_property_value_from_matched_candidates(
    property_name: &str,
    requested_pseudo_state: Option<&str>,
    matched: &[&AbstractPropertyValueCandidateV0],
) -> AbstractPropertyValueV0 {
    let mut values = matched
        .iter()
        .map(|candidate| candidate.value.trim())
        .filter(|value| !value.is_empty())
        .collect::<BTreeSet<_>>();
    if values.is_empty() {
        return AbstractPropertyValueV0::Bottom {
            property_name: property_name.to_string(),
        };
    }

    if values.len() == 1 {
        let value = values.pop_first().unwrap_or_default().to_string();
        if let Some(custom_property_name) = referenced_custom_property_name(value.as_str()) {
            return AbstractPropertyValueV0::CustomPropertyReference {
                property_name: property_name.to_string(),
                custom_property_name,
                pseudo_state: requested_pseudo_state.map(str::to_string),
            };
        }
        return AbstractPropertyValueV0::Exact {
            property_name: property_name.to_string(),
            value,
            pseudo_state: requested_pseudo_state.map(str::to_string),
        };
    }

    let pseudo_states = matched
        .iter()
        .filter_map(|candidate| candidate.pseudo_state.as_deref())
        .map(str::to_string)
        .collect::<BTreeSet<_>>()
        .into_iter()
        .collect();

    AbstractPropertyValueV0::FiniteSet {
        property_name: property_name.to_string(),
        values: values.into_iter().map(str::to_string).collect(),
        pseudo_states,
    }
}

fn referenced_custom_property_name(value: &str) -> Option<String> {
    let value = value.trim();
    let name_start = value.find("var(")? + "var(".len();
    let tail = value.get(name_start..)?.trim_start();
    if !tail.starts_with("--") {
        return None;
    }
    let name_end = tail
        .find(|character: char| character == ')' || character == ',' || character.is_whitespace())
        .unwrap_or(tail.len());
    let name = tail.get(..name_end)?.trim();
    (!name.is_empty()).then(|| name.to_string())
}