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