datasources 0.1.48

Async datasource interfaces, middleware, registry, and test doubles
Documentation
use crate::datasource_error::DatasourceError;
use crate::enrichment::Enrichment;
use crate::entity::Entity;
use crate::repository::ICRUDRepository;
use crate::result::Result;
use crate::row::Row;
use crate::stored_enrichment::StoredEnrichment;
use crate::value::Value;
use async_trait::async_trait;
use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex, OnceLock};

#[async_trait]
pub trait CreateEnrichmentStrategy<T: Entity = Row>: Send + Sync {
    async fn create_children(
        &self,
        writer: Arc<dyn ICRUDRepository>,
        enrichment: &Enrichment,
        parent: &mut T,
    ) -> Result<()>;

    async fn create_linked(
        &self,
        writer: Arc<dyn ICRUDRepository>,
        enrichment: &Enrichment,
        parent: &mut T,
    ) -> Result<()>;
}

static LOOKUPS: OnceLock<Mutex<HashMap<usize, Row>>> = OnceLock::new();

fn lookups() -> &'static Mutex<HashMap<usize, Row>> {
    LOOKUPS.get_or_init(|| Mutex::new(HashMap::new()))
}

fn item_key<T>(item: &T) -> usize {
    std::ptr::from_ref(item) as usize
}

fn missing(value: &Value) -> bool {
    matches!(value, Value::Null | Value::Int(0))
        || matches!(value, Value::Text(text) if text.is_empty())
}

pub(crate) fn try_get<T: Entity>(item: &T, name: &str) -> Option<Value> {
    item.cell(name)
}

pub(crate) fn key(value: Option<Value>) -> Option<Value> {
    match value {
        None => None,
        Some(Value::Null) => Some(Value::Null),
        Some(other) => Some(other),
    }
}

pub(crate) fn map_key<TChild: Entity>(related_key: &str) -> &'static str {
    let probe = TChild::default();
    if probe.has("Key") {
        "Key"
    } else if probe.has("key") {
        "key"
    } else if probe.has("Name") {
        "Name"
    } else if probe.has("name") {
        "name"
    } else {
        match related_key.chars().next() {
            Some(first) if related_key.contains('_') || first.is_lowercase() => "key",
            _ => "Key",
        }
    }
}

pub(crate) fn children<T: Entity, TChild: Entity>(
    enrichment: &Enrichment,
    parent: &T,
) -> Result<Vec<TChild>> {
    if enrichment.r#type == crate::EnrichmentPropertyType::Dictionary {
        dictionary_children(enrichment, parent)
    } else {
        Ok(parent.related(&enrichment.destination_property))
    }
}

fn dictionary_children<T: Entity, TChild: Entity>(
    enrichment: &Enrichment,
    parent: &T,
) -> Result<Vec<TChild>> {
    let key_name = map_key::<TChild>(&enrichment.related_key);
    parent
        .payload(&enrichment.destination_property)
        .into_iter()
        .map(|row| {
            let mut child = TChild::default();
            let key = row
                .get(key_name)
                .or_else(|| row.get("key"))
                .or_else(|| row.get("Key"))
                .cloned()
                .unwrap_or(Value::Null);
            set(&mut child, key_name, key)?;
            if let Some(select) = enrichment.related_property.as_deref() {
                let value = row
                    .get(select)
                    .or_else(|| row.get("value"))
                    .or_else(|| row.get("Value"))
                    .cloned()
                    .unwrap_or(Value::Null);
                set(&mut child, select, value)?;
            } else {
                for (name, cell) in row {
                    if name.eq_ignore_ascii_case(key_name) {
                        continue;
                    }
                    set(&mut child, &name, cell)?;
                }
            }
            Ok(child)
        })
        .collect()
}

pub(crate) fn set<T: Entity>(item: &mut T, name: &str, value: Value) -> Result<()> {
    if T::is_dictionary() || item.has(name) {
        return item.set_cell(name, value);
    }
    lookups()
        .lock()
        .expect("lookups")
        .entry(item_key(item))
        .or_default()
        .insert(name.to_string(), value);
    Ok(())
}

pub(crate) fn merge_lookups<T: Entity>(item: &T, record: &mut Row) {
    let Some(extras) = lookups().lock().expect("lookups").get(&item_key(item)).cloned() else {
        return;
    };
    for (key, value) in extras {
        record.insert(key, value);
    }
}

#[allow(dead_code)]
pub(crate) fn lookup_keys<T: Entity>(item: &T) -> HashSet<String> {
    lookups()
        .lock()
        .expect("lookups")
        .get(&item_key(item))
        .map(|row| row.keys().cloned().collect())
        .unwrap_or_default()
}

pub(crate) async fn resolve_lookups<T: Entity>(
    entries: &[StoredEnrichment<T>],
    items: &mut [T],
) -> Result<()> {
    for item in items.iter_mut() {
        for entry in entries {
            let enrichment = &entry.enrichment;
            let Some(related) = enrichment.related_property.as_deref() else {
                continue;
            };
            if enrichment.association.is_some() {
                resolve_linked(entry, item, related).await?;
                continue;
            }
            if enrichment.r#type != crate::EnrichmentPropertyType::Object {
                continue;
            }
            let Some(name) = try_get(item, &enrichment.destination_property).filter(|value| !missing(value))
            else {
                continue;
            };
            if key(try_get(item, &enrichment.source_property)).is_some_and(|value| !missing(&value))
            {
                continue;
            }
            let found = entry
                .lookup(related, name.clone(), &enrichment.related_key)
                .await?;
            let id = found
                .filter(|value| !missing(value))
                .ok_or_else(|| {
                    DatasourceError::message(format!(
                        "Unknown {} \"{}\"",
                        enrichment.destination_property,
                        name.wire_text()
                    ))
                })?;
            set(item, &enrichment.source_property, id)?;
        }
    }
    Ok(())
}

async fn resolve_linked<T: Entity>(
    entry: &StoredEnrichment<T>,
    item: &mut T,
    related: &str,
) -> Result<()> {
    let enrichment = &entry.enrichment;
    let mut rows = item.payload(&enrichment.destination_property);
    let mut changed = false;
    for row in &mut rows {
        if row
            .get(&enrichment.related_key)
            .is_some_and(|value| !missing(value))
        {
            continue;
        }
        let Some(name) = row.get(related).cloned().filter(|value| !missing(value)) else {
            continue;
        };
        let found = entry
            .lookup(related, name.clone(), &enrichment.related_key)
            .await?;
        let id = found.filter(|value| !missing(value)).ok_or_else(|| {
            DatasourceError::message(format!(
                "Unknown {} \"{}\"",
                enrichment.destination_property,
                name.wire_text()
            ))
        })?;
        row.insert(enrichment.related_key.clone(), id);
        changed = true;
    }
    if changed {
        item.set_payload(&enrichment.destination_property, rows)?;
    }
    Ok(())
}