Skip to main content

tea_context/
skill.rs

1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6use crate::SkillId;
7
8/// Maximum UTF-8 bytes in one skill description.
9pub const MAX_SKILL_DESCRIPTION_BYTES: usize = 4096;
10
11/// Bounded declarative skill metadata; it does not execute the skill.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SkillMetadata {
14    id: SkillId,
15    description: String,
16}
17
18impl SkillMetadata {
19    /// Creates one skill metadata entry.
20    ///
21    /// # Errors
22    ///
23    /// Returns an error for empty, oversized, or null-containing description.
24    pub fn new(id: SkillId, description: impl Into<String>) -> Result<Self, SkillError> {
25        let description = description.into();
26        if description.is_empty()
27            || description.len() > MAX_SKILL_DESCRIPTION_BYTES
28            || description.contains('\0')
29        {
30            return Err(SkillError::InvalidDescription);
31        }
32        Ok(Self { id, description })
33    }
34    /// Returns skill identity.
35    #[must_use]
36    pub const fn id(&self) -> &SkillId {
37        &self.id
38    }
39    /// Returns model-visible skill description.
40    #[must_use]
41    pub fn description(&self) -> &str {
42        &self.description
43    }
44    /// Returns the sole explicit invocation form.
45    #[must_use]
46    pub fn invocation(&self) -> SkillInvocation {
47        SkillInvocation {
48            skill_id: self.id.clone(),
49        }
50    }
51}
52
53/// Parsed explicit `@skill <skill-id>` invocation.
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SkillInvocation {
56    skill_id: SkillId,
57}
58
59impl SkillInvocation {
60    /// Returns invoked skill.
61    #[must_use]
62    pub const fn skill_id(&self) -> &SkillId {
63        &self.skill_id
64    }
65}
66
67impl FromStr for SkillInvocation {
68    type Err = SkillError;
69
70    fn from_str(value: &str) -> Result<Self, Self::Err> {
71        let skill_id = value
72            .strip_prefix("@skill ")
73            .filter(|remaining| !remaining.contains(' '))
74            .ok_or(SkillError::InvalidInvocation)?
75            .parse()
76            .map_err(|_| SkillError::InvalidInvocation)?;
77        Ok(Self { skill_id })
78    }
79}
80
81impl fmt::Display for SkillInvocation {
82    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
83        write!(formatter, "@skill {}", self.skill_id)
84    }
85}
86
87/// Invalid skill metadata or invocation.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
89pub enum SkillError {
90    /// Description violates bounds.
91    #[error("skill description is invalid")]
92    InvalidDescription,
93    /// Invocation is not exact explicit skill syntax.
94    #[error("skill invocation must use exact '@skill <skill-id>' syntax")]
95    InvalidInvocation,
96}