Skip to main content

kcode_k1_chat_thread_ktools/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../Documentation.md")]
3
4use std::sync::Arc;
5
6use kcode_k1_access_kmap::K1AccessKmap;
7use kcode_k1_chat_thread_actions::ChatThreadActions;
8pub use kcode_k1_chat_thread_actions::{AccessContext, AccessPolicy, ProfileId};
9use kcode_k1_ktool_social::SocialKtools;
10
11const DIFFERENT_AUTHORIZATION: &str =
12    "different Kmap access context, profile ID, or policy is already active";
13const NO_SOCIAL_AUTHORIZATION: &str = "social authorization is not active";
14const NO_SOCIAL_KTOOLS: &str = "social Ktools are unavailable";
15
16#[derive(Clone, Eq, PartialEq)]
17struct Authorization {
18    context: AccessContext,
19    profile_id: ProfileId,
20    policy: AccessPolicy,
21}
22
23pub struct ChatThreadKtools {
24    actions: ChatThreadActions,
25    social: Option<SocialKtools>,
26    authorization: Option<Authorization>,
27}
28
29impl ChatThreadKtools {
30    pub fn new(kmap: Arc<K1AccessKmap>) -> Self {
31        Self {
32            actions: ChatThreadActions::new(kmap),
33            social: None,
34            authorization: None,
35        }
36    }
37
38    pub fn new_with_social(kmap: Arc<K1AccessKmap>, social: SocialKtools) -> Self {
39        Self {
40            actions: ChatThreadActions::new(kmap),
41            social: Some(social),
42            authorization: None,
43        }
44    }
45
46    pub fn bind_authorization(
47        &mut self,
48        context: AccessContext,
49        profile_id: ProfileId,
50        policy: AccessPolicy,
51    ) -> Result<(), String> {
52        let candidate = Authorization {
53            context,
54            profile_id,
55            policy,
56        };
57        if self
58            .authorization
59            .as_ref()
60            .is_some_and(|active| active != &candidate)
61        {
62            return Err(DIFFERENT_AUTHORIZATION.to_owned());
63        }
64        self.actions.bind_authorization(
65            candidate.context.clone(),
66            candidate.profile_id,
67            candidate.policy.clone(),
68        )?;
69        self.authorization = Some(candidate);
70        Ok(())
71    }
72
73    pub fn clear_authorization(&mut self) {
74        self.actions.clear_authorization();
75        self.authorization = None;
76    }
77
78    pub fn launch(&mut self, name: &str, arguments: &str) -> Result<String, String> {
79        if matches!(name, "ListContacts" | "ListGroups" | "GetGroup") {
80            let social = self
81                .social
82                .as_ref()
83                .ok_or_else(|| NO_SOCIAL_KTOOLS.to_owned())?;
84            let context = self
85                .authorization
86                .as_ref()
87                .map(|authorization| &authorization.context)
88                .ok_or_else(|| NO_SOCIAL_AUTHORIZATION.to_owned())?;
89            return social.launch(context, name, arguments);
90        }
91        self.actions.launch(name, arguments)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    #[test]
98    fn social_names_are_the_only_intercepted_names() {
99        for name in ["ListContacts", "ListGroups", "GetGroup"] {
100            assert!(matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
101        }
102        for name in ["CurrentTime", "KmapOpenNode", "WebSearch", "SetLaunchNode"] {
103            assert!(!matches!(name, "ListContacts" | "ListGroups" | "GetGroup"));
104        }
105    }
106
107    #[test]
108    fn complete_package_stays_below_the_managed_limit() {
109        let files = [
110            include_str!("../Cargo.toml"),
111            include_str!("../Documentation.md"),
112            include_str!("lib.rs"),
113        ];
114        let count = files
115            .iter()
116            .flat_map(|file| file.lines())
117            .filter(|line| !line.trim().is_empty())
118            .count();
119        assert!(count < 500, "complete package has {count} nonblank lines");
120    }
121}