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