episodic 0.2.3

Reusable Observational Memory core models and pure transforms.
Documentation
use crate::model::{OmScope, OmThreadRefV2};
use std::borrow::Cow;

use super::OmTransformError;

fn normalize_identifier(raw: Option<&str>) -> Option<String> {
    raw.map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

fn scope_key_identifier(scope_key: &str, prefix: &str) -> Option<String> {
    scope_key
        .trim()
        .strip_prefix(prefix)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

#[must_use]
pub fn resolve_canonical_thread_ref(
    scope: OmScope,
    scope_key: &str,
    source_thread_id: Option<&str>,
    source_session_id: Option<&str>,
    thread_id: Option<&str>,
    session_id: Option<&str>,
    resource_id: Option<&str>,
) -> OmThreadRefV2 {
    let normalized_scope_key = scope_key.trim().to_string();
    let origin_thread_id =
        normalize_identifier(source_thread_id).or_else(|| normalize_identifier(thread_id));
    let origin_session_id =
        normalize_identifier(source_session_id).or_else(|| normalize_identifier(session_id));

    let thread_from_scope = scope_key_identifier(&normalized_scope_key, "thread:");
    let session_from_scope = scope_key_identifier(&normalized_scope_key, "session:");
    let resource_from_scope = scope_key_identifier(&normalized_scope_key, "resource:");
    let resolved_resource_id = normalize_identifier(resource_id).or_else(|| {
        matches!(scope, OmScope::Resource)
            .then(|| resource_from_scope.as_deref())
            .flatten()
            .map(ToString::to_string)
    });

    let preferred_thread = origin_thread_id.as_deref().or(thread_from_scope.as_deref());
    let preferred_session = origin_session_id
        .as_deref()
        .or(session_from_scope.as_deref());
    let preferred_resource = resolved_resource_id.as_deref();

    let (kind, canonical_id): (&str, Cow<'_, str>) = match scope {
        OmScope::Session => {
            if let Some(id) = preferred_session {
                ("session", Cow::Borrowed(id))
            } else if let Some(id) = preferred_thread {
                ("thread", Cow::Borrowed(id))
            } else if let Some(id) = preferred_resource {
                ("resource", Cow::Borrowed(id))
            } else {
                ("session", Cow::Borrowed("default"))
            }
        }
        OmScope::Thread => {
            if let Some(id) = preferred_thread {
                ("thread", Cow::Borrowed(id))
            } else if let Some(id) = preferred_session {
                ("session", Cow::Borrowed(id))
            } else if let Some(id) = preferred_resource {
                ("resource", Cow::Borrowed(id))
            } else {
                ("thread", Cow::Borrowed("default"))
            }
        }
        OmScope::Resource => {
            if let Some(id) = preferred_thread {
                ("thread", Cow::Borrowed(id))
            } else if let Some(id) = preferred_session {
                ("session", Cow::Borrowed(id))
            } else if let Some(id) = preferred_resource {
                ("resource", Cow::Borrowed(id))
            } else {
                ("resource", Cow::Borrowed("default"))
            }
        }
    };

    OmThreadRefV2 {
        canonical_thread_id: format!("{kind}:{canonical_id}"),
        scope,
        scope_key: normalized_scope_key,
        origin_thread_id,
        origin_session_id,
        resource_id: resolved_resource_id,
    }
}

pub fn build_scope_key(
    scope: OmScope,
    session_id: Option<&str>,
    thread_id: Option<&str>,
    resource_id: Option<&str>,
) -> Result<String, OmTransformError> {
    match scope {
        OmScope::Session => session_id
            .filter(|x| !x.trim().is_empty())
            .map(|x| format!("session:{}", x.trim()))
            .ok_or(OmTransformError::MissingScopeIdentifier("session_id")),
        OmScope::Thread => thread_id
            .filter(|x| !x.trim().is_empty())
            .map(|x| format!("thread:{}", x.trim()))
            .ok_or(OmTransformError::MissingScopeIdentifier("thread_id")),
        OmScope::Resource => resource_id
            .filter(|x| !x.trim().is_empty())
            .map(|x| format!("resource:{}", x.trim()))
            .ok_or(OmTransformError::MissingScopeIdentifier("resource_id")),
    }
}