agent_skills/
frontmatter.rs1use serde::{Deserialize, Serialize};
4
5use crate::allowed_tools::AllowedTools;
6use crate::compatibility::Compatibility;
7use crate::description::SkillDescription;
8use crate::metadata::Metadata;
9use crate::name::SkillName;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29pub struct Frontmatter {
30 name: SkillName,
31 description: SkillDescription,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 license: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 compatibility: Option<Compatibility>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 metadata: Option<Metadata>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 allowed_tools: Option<AllowedTools>,
40}
41
42impl Frontmatter {
43 #[must_use]
47 pub const fn new(name: SkillName, description: SkillDescription) -> Self {
48 Self {
49 name,
50 description,
51 license: None,
52 compatibility: None,
53 metadata: None,
54 allowed_tools: None,
55 }
56 }
57
58 #[must_use]
76 pub const fn builder(name: SkillName, description: SkillDescription) -> FrontmatterBuilder {
77 FrontmatterBuilder::new(name, description)
78 }
79
80 #[must_use]
82 pub const fn name(&self) -> &SkillName {
83 &self.name
84 }
85
86 #[must_use]
88 pub const fn description(&self) -> &SkillDescription {
89 &self.description
90 }
91
92 #[must_use]
94 pub fn license(&self) -> Option<&str> {
95 self.license.as_deref()
96 }
97
98 #[must_use]
100 pub const fn compatibility(&self) -> Option<&Compatibility> {
101 self.compatibility.as_ref()
102 }
103
104 #[must_use]
106 pub const fn metadata(&self) -> Option<&Metadata> {
107 self.metadata.as_ref()
108 }
109
110 #[must_use]
112 pub const fn allowed_tools(&self) -> Option<&AllowedTools> {
113 self.allowed_tools.as_ref()
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct FrontmatterBuilder {
120 name: SkillName,
121 description: SkillDescription,
122 license: Option<String>,
123 compatibility: Option<Compatibility>,
124 metadata: Option<Metadata>,
125 allowed_tools: Option<AllowedTools>,
126}
127
128impl FrontmatterBuilder {
129 #[must_use]
131 pub const fn new(name: SkillName, description: SkillDescription) -> Self {
132 Self {
133 name,
134 description,
135 license: None,
136 compatibility: None,
137 metadata: None,
138 allowed_tools: None,
139 }
140 }
141
142 #[must_use]
144 pub fn license(mut self, license: impl Into<String>) -> Self {
145 self.license = Some(license.into());
146 self
147 }
148
149 #[must_use]
151 pub fn compatibility(mut self, compat: Compatibility) -> Self {
152 self.compatibility = Some(compat);
153 self
154 }
155
156 #[must_use]
158 pub fn metadata(mut self, metadata: Metadata) -> Self {
159 self.metadata = Some(metadata);
160 self
161 }
162
163 #[must_use]
165 pub fn allowed_tools(mut self, tools: AllowedTools) -> Self {
166 self.allowed_tools = Some(tools);
167 self
168 }
169
170 #[must_use]
172 pub fn build(self) -> Frontmatter {
173 Frontmatter {
174 name: self.name,
175 description: self.description,
176 license: self.license,
177 compatibility: self.compatibility,
178 metadata: self.metadata,
179 allowed_tools: self.allowed_tools,
180 }
181 }
182}
183
184#[cfg(test)]
185#[allow(clippy::unwrap_used, clippy::expect_used)]
186mod tests {
187 use super::*;
188
189 fn test_name() -> SkillName {
190 SkillName::new("test-skill").unwrap()
191 }
192
193 fn test_description() -> SkillDescription {
194 SkillDescription::new("A test skill.").unwrap()
195 }
196
197 #[test]
198 fn new_creates_frontmatter_with_required_fields() {
199 let fm = Frontmatter::new(test_name(), test_description());
200 assert_eq!(fm.name().as_str(), "test-skill");
201 assert_eq!(fm.description().as_str(), "A test skill.");
202 assert!(fm.license().is_none());
203 assert!(fm.compatibility().is_none());
204 assert!(fm.metadata().is_none());
205 assert!(fm.allowed_tools().is_none());
206 }
207
208 #[test]
209 fn builder_sets_license() {
210 let fm = Frontmatter::builder(test_name(), test_description())
211 .license("MIT")
212 .build();
213 assert_eq!(fm.license(), Some("MIT"));
214 }
215
216 #[test]
217 fn builder_sets_compatibility() {
218 let compat = Compatibility::new("Requires docker").unwrap();
219 let fm = Frontmatter::builder(test_name(), test_description())
220 .compatibility(compat)
221 .build();
222 assert!(fm.compatibility().is_some());
223 assert_eq!(fm.compatibility().unwrap().as_str(), "Requires docker");
224 }
225
226 #[test]
227 fn builder_sets_metadata() {
228 let metadata = Metadata::from_pairs([("author", "test")]);
229 let fm = Frontmatter::builder(test_name(), test_description())
230 .metadata(metadata)
231 .build();
232 assert!(fm.metadata().is_some());
233 assert_eq!(fm.metadata().unwrap().get("author"), Some("test"));
234 }
235
236 #[test]
237 fn builder_sets_allowed_tools() {
238 let tools = AllowedTools::new("Read Write");
239 let fm = Frontmatter::builder(test_name(), test_description())
240 .allowed_tools(tools)
241 .build();
242 assert!(fm.allowed_tools().is_some());
243 assert_eq!(fm.allowed_tools().unwrap().len(), 2);
244 }
245
246 #[test]
247 fn builder_chains_all_options() {
248 let compat = Compatibility::new("Requires git").unwrap();
249 let metadata = Metadata::from_pairs([("version", "1.0")]);
250 let tools = AllowedTools::new("Bash");
251
252 let fm = Frontmatter::builder(test_name(), test_description())
253 .license("Apache-2.0")
254 .compatibility(compat)
255 .metadata(metadata)
256 .allowed_tools(tools)
257 .build();
258
259 assert_eq!(fm.license(), Some("Apache-2.0"));
260 assert!(fm.compatibility().is_some());
261 assert!(fm.metadata().is_some());
262 assert!(fm.allowed_tools().is_some());
263 }
264}