kcode-k1-chat-thread-ktools 0.1.0

Synchronous Ktool dispatch for one K1 chat thread
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../Documentation.md")]

use std::sync::Arc;

use kcode_k1_access_kmap::K1AccessKmap;
use kcode_k1_chat_thread_actions::ChatThreadActions;
pub use kcode_k1_chat_thread_actions::{AccessContext, AccessPolicy, ProfileId};
use kcode_k1_ktool_social::SocialKtools;

const DIFFERENT_AUTHORIZATION: &str =
    "different Kmap access context, profile ID, or policy is already active";
const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";

#[derive(Clone, Eq, PartialEq)]
struct Authorization {
    context: AccessContext,
    profile_id: ProfileId,
    policy: AccessPolicy,
}

pub struct ChatThreadKtools {
    actions: ChatThreadActions,
    social: Option<SocialKtools>,
    authorization: Option<Authorization>,
}

impl ChatThreadKtools {
    pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
        Self {
            actions: ChatThreadActions::new(kmap),
            social: None,
            authorization: None,
        }
    }

    pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
        Self {
            actions: ChatThreadActions::new(kmap),
            social: Some(social),
            authorization: None,
        }
    }

    pub fn bind_authorization(
        &mut self,
        context: AccessContext,
        profile_id: ProfileId,
        policy: AccessPolicy,
    ) -> Result<(), String> {
        let candidate = Authorization {
            context,
            profile_id,
            policy,
        };
        if self
            .authorization
            .as_ref()
            .is_some_and(|active| active != &candidate)
        {
            return Err(DIFFERENT_AUTHORIZATION.to_owned());
        }
        self.actions.bind_authorization(
            candidate.context.clone(),
            candidate.profile_id,
            candidate.policy.clone(),
        )?;
        self.authorization = Some(candidate);
        Ok(())
    }

    pub fn clear_authorization(&mut self) {
        self.actions.clear_authorization();
        self.authorization = None;
    }

    pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
        if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
            let social = self
                .social
                .as_ref()
                .ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
            let context = self
                .authorization
                .as_ref()
                .map(|authorization| &authorization.context)
                .ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
            return social.launch(context, name, arguments);
        }
        self.actions.launch(name, arguments)
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn social_names_are_the_only_intercepted_names() {
        for name in ["ListContacts", "ListGroups", "GetGroup"] {
            assert!(matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
        }
        for name in ["CurrentTime", "KmapOpenNode", "WebSearch", "SetLaunchNode"] {
            assert!(!matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
        }
    }

    #[test]
    fn complete_package_stays_below_the_managed_limit() {
        let files = [
            include_str!("../Cargo.toml"),
            include_str!("../Documentation.md"),
            include_str!("lib.rs"),
        ];
        let count = files
            .iter()
            .flat_map(|file| file.lines())
            .filter(|line| !line.trim().is_empty())
            .count();
        assert!(count < 500, "complete package has {count} nonblank lines");
    }
}