systemprompt_models/profile/
validation.rs1use std::path::Path;
7
8use super::Profile;
9use anyhow::Result;
10
11impl Profile {
12 pub fn validate(&self) -> Result<()> {
14 let mut errors: Vec<String> = Vec::new();
15 let is_cloud = self.target.is_cloud();
16
17 self.validate_required_fields(&mut errors);
18 self.validate_paths(&mut errors, is_cloud);
19 self.validate_security_settings(&mut errors);
20 self.validate_cors_origins(&mut errors);
21 self.validate_rate_limits(&mut errors);
22
23 if errors.is_empty() {
24 Ok(())
25 } else {
26 anyhow::bail!(
27 "Profile '{}' validation failed:\n - {}",
28 self.name,
29 errors.join("\n - ")
30 )
31 }
32 }
33
34 pub(super) fn validate_paths(&self, errors: &mut Vec<String>, is_cloud: bool) {
35 if is_cloud {
36 self.validate_cloud_paths(errors);
37 } else {
38 self.validate_local_paths(errors);
39 }
40 }
41
42 pub(super) fn validate_cloud_paths(&self, errors: &mut Vec<String>) {
43 Self::require_non_empty(errors, &self.paths.system, "Paths system");
44 Self::require_non_empty(errors, &self.paths.services, "Paths services");
45 Self::require_non_empty(errors, &self.paths.bin, "Paths bin");
46
47 for (name, path) in [
48 ("system", self.paths.system.as_str()),
49 ("services", self.paths.services.as_str()),
50 ("bin", self.paths.bin.as_str()),
51 ] {
52 if !path.is_empty() && !path.starts_with("/app") {
53 errors.push(format!(
54 "Cloud profile {} path should start with /app, got: {}",
55 name, path
56 ));
57 }
58 }
59 }
60
61 pub(super) fn validate_local_paths(&self, errors: &mut Vec<String>) {
62 Self::validate_local_required_path(errors, "system", &self.paths.system);
63 Self::validate_local_required_path(errors, "services", &self.paths.services);
64 Self::validate_local_required_path(errors, "bin", &self.paths.bin);
65
66 Self::validate_local_optional_path(errors, "storage", self.paths.storage.as_ref());
67 Self::validate_local_optional_path(
68 errors,
69 "geoip_database",
70 self.paths.geoip_database.as_ref(),
71 );
72 Self::validate_local_optional_path(errors, "web_path", self.paths.web_path.as_ref());
73 }
74
75 fn validate_local_required_path(errors: &mut Vec<String>, name: &str, path: &str) {
76 if path.is_empty() {
77 errors.push(format!("Paths {} is required", name));
78 return;
79 }
80
81 if !Path::new(path).exists() {
82 errors.push(format!("{} path does not exist: {}", name, path));
83 }
84 }
85
86 fn validate_local_optional_path(errors: &mut Vec<String>, name: &str, path: Option<&String>) {
87 if let Some(p) = path {
88 if !p.is_empty() && !Path::new(p).exists() {
89 errors.push(format!("paths.{} does not exist: {}", name, p));
90 }
91 }
92 }
93
94 pub(super) fn validate_required_fields(&self, errors: &mut Vec<String>) {
95 Self::require_non_empty(errors, &self.name, "Profile name");
96 Self::require_non_empty(errors, &self.display_name, "Profile display_name");
97 Self::require_non_empty(errors, &self.site.name, "Site name");
98 Self::require_non_empty(errors, &self.server.host, "Server host");
99 Self::require_non_empty(errors, &self.server.api_server_url, "Server api_server_url");
100 Self::require_non_empty(
101 errors,
102 &self.server.api_internal_url,
103 "Server api_internal_url",
104 );
105 Self::require_non_empty(
106 errors,
107 &self.server.api_external_url,
108 "Server api_external_url",
109 );
110
111 if self.server.port == 0 {
112 errors.push("Server port must be greater than 0".to_string());
113 }
114 }
115
116 pub(super) fn require_non_empty(errors: &mut Vec<String>, value: &str, field_name: &str) {
117 if value.is_empty() {
118 errors.push(format!("{field_name} is required"));
119 }
120 }
121
122 pub(super) fn validate_security_settings(&self, errors: &mut Vec<String>) {
123 if self.security.access_token_expiration <= 0 {
124 errors.push("Security access_token_expiration must be positive".to_string());
125 }
126
127 if self.security.refresh_token_expiration <= 0 {
128 errors.push("Security refresh_token_expiration must be positive".to_string());
129 }
130 }
131
132 pub(super) fn validate_cors_origins(&self, errors: &mut Vec<String>) {
133 for origin in &self.server.cors_allowed_origins {
134 if origin.is_empty() {
135 errors.push("CORS origin cannot be empty".to_string());
136 continue;
137 }
138
139 let is_valid = origin.starts_with("http://") || origin.starts_with("https://");
140 if !is_valid {
141 errors.push(format!(
142 "Invalid CORS origin (must start with http:// or https://): {}",
143 origin
144 ));
145 }
146 }
147 }
148
149 pub(super) fn validate_rate_limits(&self, errors: &mut Vec<String>) {
150 if self.rate_limits.disabled {
151 return;
152 }
153
154 if self.rate_limits.burst_multiplier == 0 {
155 errors.push("rate_limits.burst_multiplier must be greater than 0".to_string());
156 }
157
158 Self::validate_rate_limit(
159 errors,
160 "oauth_public",
161 self.rate_limits.oauth_public_per_second,
162 );
163 Self::validate_rate_limit(errors, "oauth_auth", self.rate_limits.oauth_auth_per_second);
164 Self::validate_rate_limit(errors, "contexts", self.rate_limits.contexts_per_second);
165 Self::validate_rate_limit(errors, "tasks", self.rate_limits.tasks_per_second);
166 Self::validate_rate_limit(errors, "artifacts", self.rate_limits.artifacts_per_second);
167 Self::validate_rate_limit(errors, "agents", self.rate_limits.agents_per_second);
168 Self::validate_rate_limit(errors, "mcp", self.rate_limits.mcp_per_second);
169 Self::validate_rate_limit(errors, "stream", self.rate_limits.stream_per_second);
170 Self::validate_rate_limit(errors, "content", self.rate_limits.content_per_second);
171 }
172
173 fn validate_rate_limit(errors: &mut Vec<String>, name: &str, value: u64) {
174 if value == 0 {
175 errors.push(format!(
176 "rate_limits.{}_per_second must be greater than 0",
177 name
178 ));
179 }
180 }
181}