1use std::fmt;
2use std::str::FromStr;
3
4use thiserror::Error;
5
6use crate::SkillId;
7
8pub const MAX_SKILL_DESCRIPTION_BYTES: usize = 4096;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct SkillMetadata {
14 id: SkillId,
15 description: String,
16}
17
18impl SkillMetadata {
19 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 #[must_use]
36 pub const fn id(&self) -> &SkillId {
37 &self.id
38 }
39 #[must_use]
41 pub fn description(&self) -> &str {
42 &self.description
43 }
44 #[must_use]
46 pub fn invocation(&self) -> SkillInvocation {
47 SkillInvocation {
48 skill_id: self.id.clone(),
49 }
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SkillInvocation {
56 skill_id: SkillId,
57}
58
59impl SkillInvocation {
60 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
89pub enum SkillError {
90 #[error("skill description is invalid")]
92 InvalidDescription,
93 #[error("skill invocation must use exact '@skill <skill-id>' syntax")]
95 InvalidInvocation,
96}