miyabi_modes/
validator.rs1use crate::error::{ModeError, ModeResult};
2use crate::mode::MiyabiMode;
3
4pub struct ModeValidator;
6
7impl ModeValidator {
8 pub fn validate(mode: &MiyabiMode) -> ModeResult<()> {
10 Self::validate_slug(&mode.slug)?;
11 Self::validate_name(&mode.name)?;
12 Self::validate_character(&mode.character)?;
13 Self::validate_role_definition(&mode.role_definition)?;
14 Self::validate_groups(&mode.groups)?;
15 Self::validate_source(&mode.source)?;
16
17 if let Some(ref regex) = mode.file_regex {
18 Self::validate_regex(regex)?;
19 }
20
21 Ok(())
22 }
23
24 fn validate_slug(slug: &str) -> ModeResult<()> {
25 if slug.is_empty() {
26 return Err(ModeError::MissingField("slug".into()));
27 }
28
29 if !slug.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
31 return Err(ModeError::ValidationFailed(
32 "Slug must contain only lowercase letters, numbers, and hyphens".into(),
33 ));
34 }
35
36 if slug.len() > 50 {
37 return Err(ModeError::ValidationFailed("Slug must be 50 characters or less".into()));
38 }
39
40 Ok(())
41 }
42
43 fn validate_name(name: &str) -> ModeResult<()> {
44 if name.is_empty() {
45 return Err(ModeError::MissingField("name".into()));
46 }
47
48 if name.len() > 100 {
49 return Err(ModeError::ValidationFailed("Name must be 100 characters or less".into()));
50 }
51
52 Ok(())
53 }
54
55 fn validate_character(character: &str) -> ModeResult<()> {
56 if character.is_empty() {
57 return Err(ModeError::MissingField("character".into()));
58 }
59
60 if character.len() > 50 {
61 return Err(ModeError::ValidationFailed(
62 "Character name must be 50 characters or less".into(),
63 ));
64 }
65
66 Ok(())
67 }
68
69 fn validate_role_definition(role_def: &str) -> ModeResult<()> {
70 if role_def.is_empty() {
71 return Err(ModeError::MissingField("roleDefinition".into()));
72 }
73
74 if role_def.len() < 10 {
75 return Err(ModeError::ValidationFailed(
76 "Role definition too short (minimum 10 characters)".into(),
77 ));
78 }
79
80 Ok(())
81 }
82
83 fn validate_groups(groups: &[crate::mode::ToolGroup]) -> ModeResult<()> {
84 if groups.is_empty() {
85 return Err(ModeError::ValidationFailed("At least one tool group is required".into()));
86 }
87
88 Ok(())
89 }
90
91 fn validate_source(source: &str) -> ModeResult<()> {
92 if source != "miyabi-core" && source != "user" {
93 return Err(ModeError::ValidationFailed(
94 "Source must be 'miyabi-core' or 'user'".into(),
95 ));
96 }
97
98 Ok(())
99 }
100
101 fn validate_regex(pattern: &str) -> ModeResult<()> {
102 regex::Regex::new(pattern)?;
103 Ok(())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::mode::ToolGroup;
111
112 fn create_valid_mode() -> MiyabiMode {
113 MiyabiMode {
114 slug: "test-mode".into(),
115 name: "Test Mode".into(),
116 character: "てすとん".into(),
117 role_definition: "This is a test mode for validation".into(),
118 when_to_use: "Use for testing".into(),
119 groups: vec![ToolGroup::Read],
120 custom_instructions: "Test instructions".into(),
121 source: "user".into(),
122 file_regex: None,
123 description: None,
124 system_prompt_args: None,
125 tools: vec![],
126 }
127 }
128
129 #[test]
130 fn test_valid_mode() {
131 let mode = create_valid_mode();
132 assert!(ModeValidator::validate(&mode).is_ok());
133 }
134
135 #[test]
136 fn test_invalid_slug_uppercase() {
137 let mut mode = create_valid_mode();
138 mode.slug = "TestMode".into();
139 assert!(ModeValidator::validate(&mode).is_err());
140 }
141
142 #[test]
143 fn test_invalid_slug_special_chars() {
144 let mut mode = create_valid_mode();
145 mode.slug = "test_mode".into();
146 assert!(ModeValidator::validate(&mode).is_err());
147 }
148
149 #[test]
150 fn test_empty_slug() {
151 let mut mode = create_valid_mode();
152 mode.slug = "".into();
153 assert!(ModeValidator::validate(&mode).is_err());
154 }
155
156 #[test]
157 fn test_short_role_definition() {
158 let mut mode = create_valid_mode();
159 mode.role_definition = "Short".into();
160 assert!(ModeValidator::validate(&mode).is_err());
161 }
162
163 #[test]
164 fn test_empty_groups() {
165 let mut mode = create_valid_mode();
166 mode.groups = vec![];
167 assert!(ModeValidator::validate(&mode).is_err());
168 }
169
170 #[test]
171 fn test_invalid_source() {
172 let mut mode = create_valid_mode();
173 mode.source = "invalid-source".into();
174 assert!(ModeValidator::validate(&mode).is_err());
175 }
176
177 #[test]
178 fn test_invalid_regex() {
179 let mut mode = create_valid_mode();
180 mode.file_regex = Some("[invalid".into());
181 assert!(ModeValidator::validate(&mode).is_err());
182 }
183
184 #[test]
185 fn test_valid_regex() {
186 let mut mode = create_valid_mode();
187 mode.file_regex = Some(r".*\.rs$".into());
188 assert!(ModeValidator::validate(&mode).is_ok());
189 }
190}