systemprompt_models/profile/
validation.rs

1//! Profile validation logic.
2//!
3//! This module contains all validation logic for Profile configurations,
4//! including path validation, security settings, CORS, and rate limits.
5
6use std::path::Path;
7
8use super::Profile;
9use anyhow::Result;
10
11impl Profile {
12    /// Validates the entire profile configuration.
13    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}