khive-runtime 0.14.0

Composable Service API: entity/note CRUD, graph traversal, hybrid search, curation.
Documentation
use std::collections::BTreeMap;

use khive_storage::StorageError;
use khive_types::{Header, Namespace, PropertyValue, Timestamp};

fn invalid(field: &str) -> StorageError {
    StorageError::Internal(format!("canonical record cannot represent {field}"))
}

fn timestamp(value: i64, field: &str) -> Result<Timestamp, StorageError> {
    u64::try_from(value)
        .map(Timestamp::from_micros)
        .map_err(|_| invalid(field))
}

pub(super) fn properties(
    value: &Option<serde_json::Value>,
) -> Result<BTreeMap<String, PropertyValue>, StorageError> {
    match value {
        None => Ok(BTreeMap::new()),
        Some(serde_json::Value::Object(_)) => {
            serde_json::from_value(value.as_ref().expect("matched present properties").clone())
                .map_err(|_| invalid("properties"))
        }
        Some(_) => Err(invalid("properties")),
    }
}

pub(super) fn note(value: &khive_storage::Note) -> Result<khive_types::Note, StorageError> {
    let mut properties = properties(&value.properties)?;
    let tags = match properties.remove("tags") {
        None => Vec::new(),
        Some(PropertyValue::Array(tags)) => tags
            .into_iter()
            .map(|tag| match tag {
                PropertyValue::String(tag) => Ok(tag),
                _ => Err(invalid("note tags")),
            })
            .collect::<Result<Vec<_>, _>>()?,
        Some(_) => return Err(invalid("note tags")),
    };
    let note = khive_types::Note {
        header: Header {
            id: super::id128(value.id),
            namespace: Namespace::parse(&value.namespace).map_err(|_| invalid("namespace"))?,
            created_at: timestamp(value.created_at, "created_at")?,
            updated_at: timestamp(value.updated_at, "updated_at")?,
        },
        kind: value.kind.clone(),
        status: value.status.parse().map_err(|_| invalid("status"))?,
        content: value.content.clone(),
        properties,
        tags,
        salience: value.salience,
        decay_factor: value.decay_factor,
        expires_at: value
            .expires_at
            .map(|v| timestamp(v, "expires_at"))
            .transpose()?,
        deleted_at: value
            .deleted_at
            .map(|v| timestamp(v, "deleted_at"))
            .transpose()?,
    };
    if !note.is_valid() {
        return Err(invalid("note numeric fields"));
    }
    Ok(note)
}

pub(super) fn entity(value: &khive_storage::Entity) -> Result<khive_types::Entity, StorageError> {
    Ok(khive_types::Entity {
        header: Header {
            id: super::id128(value.id),
            namespace: Namespace::parse(&value.namespace).map_err(|_| invalid("namespace"))?,
            created_at: timestamp(value.created_at, "created_at")?,
            updated_at: timestamp(value.updated_at, "updated_at")?,
        },
        kind: value.kind.parse().map_err(|_| invalid("kind"))?,
        entity_type: value.entity_type.clone(),
        name: value.name.clone(),
        description: value.description.clone(),
        properties: properties(&value.properties)?,
        tags: value.tags.clone(),
        deleted_at: value
            .deleted_at
            .map(|v| timestamp(v, "deleted_at"))
            .transpose()?,
    })
}

pub(super) fn edge(value: &khive_storage::Edge) -> Result<khive_types::Link, StorageError> {
    let edge = khive_types::Link {
        id: super::id128(uuid::Uuid::from(value.id)),
        namespace: value.namespace.clone(),
        source: super::id128(value.source_id),
        target: super::id128(value.target_id),
        relation: value.relation,
        properties: properties(&value.metadata)?,
        weight: value.weight,
        created_at: timestamp(value.created_at.timestamp_micros(), "created_at")?,
        updated_at: timestamp(value.updated_at.timestamp_micros(), "updated_at")?,
        deleted_at: value
            .deleted_at
            .map(|v| timestamp(v.timestamp_micros(), "deleted_at"))
            .transpose()?,
    };
    if !edge.is_valid() {
        return Err(invalid("weight"));
    }
    Ok(edge)
}