Skip to main content

swf_core/validation/
mod.rs

1use crate::models::expression::is_strict_expr;
2use regex::Regex;
3use std::sync::LazyLock;
4
5mod authentication;
6mod document;
7mod enum_validators;
8mod one_of_validators;
9mod task;
10#[cfg(test)]
11mod tests;
12
13// Re-export all pub items from sub-modules
14pub use authentication::{
15    validate_auth_policy, validate_basic_auth, validate_bearer_auth, validate_digest_auth,
16    validate_oauth2_auth, validate_oidc_auth,
17};
18pub use document::validate_workflow;
19pub use enum_validators::{
20    validate_asyncapi_protocol, validate_container_cleanup, validate_container_lifetime,
21    validate_extension_task_type, validate_http_method, validate_http_output,
22    validate_oauth2_client_auth_method, validate_oauth2_grant_type,
23    validate_oauth2_request_encoding, validate_pull_policy, validate_script_language,
24};
25pub use one_of_validators::{
26    validate_auth_policy_one_of, validate_backoff_one_of, validate_process_type_one_of,
27    validate_schedule_one_of, validate_schema_one_of,
28};
29pub use task::{
30    validate_set_task, validate_switch_task, validate_task_map, validate_workflow_process,
31};
32
33/// Represents a validation error
34#[derive(Debug, Clone, PartialEq)]
35pub struct ValidationError {
36    /// The field path that failed validation (e.g., "document.name", "do.task1.call")
37    pub field: String,
38    /// The validation rule that failed
39    pub rule: ValidationRule,
40    /// A human-readable error message
41    pub message: String,
42}
43
44/// Enumerates the types of validation rules
45#[derive(Debug, Clone, PartialEq)]
46pub enum ValidationRule {
47    Required,
48    Semver,
49    Hostname,
50    Uri,
51    Iso8601Duration,
52    MutualExclusion,
53    InvalidValue,
54    Custom(String),
55}
56
57/// Represents the result of a validation operation
58#[derive(Debug, Clone, PartialEq)]
59pub struct ValidationResult {
60    /// The list of validation errors found
61    pub errors: Vec<ValidationError>,
62}
63
64impl ValidationResult {
65    /// Creates a new empty validation result
66    pub fn new() -> Self {
67        Self { errors: Vec::new() }
68    }
69
70    /// Adds an error to the validation result
71    pub fn add_error(&mut self, field: &str, rule: ValidationRule, message: &str) {
72        self.errors.push(ValidationError {
73            field: field.to_string(),
74            rule,
75            message: message.to_string(),
76        });
77    }
78
79    /// Returns true if no validation errors were found
80    pub fn is_valid(&self) -> bool {
81        self.errors.is_empty()
82    }
83
84    /// Merges another validation result into this one, prefixing field paths
85    pub fn merge_with_prefix(&mut self, prefix: &str, other: ValidationResult) {
86        for error in other.errors {
87            self.errors.push(ValidationError {
88                field: format!("{}.{}", prefix, error.field),
89                rule: error.rule,
90                message: error.message,
91            });
92        }
93    }
94}
95
96impl Default for ValidationResult {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102static SEMVER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
103    Regex::new(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$").expect("static semver regex is valid")
104});
105
106static HOSTNAME_RFC1123_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
107    Regex::new(r"^(([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z]{2,63}|[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)$").expect("static hostname regex is valid")
108});
109
110// URI/URI-template patterns (matching Go SDK's LiteralUriPattern/LiteralUriTemplatePattern)
111static URI_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
112    Regex::new(r"^[A-Za-z][A-Za-z0-9+\-.]*://[^{}\s]+$").expect("static URI regex is valid")
113});
114
115static URI_TEMPLATE_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
116    Regex::new(r"^[A-Za-z][A-Za-z0-9+\-.]*://.*\{.*}.*$")
117        .expect("static URI template regex is valid")
118});
119
120// RFC 6901 JSON Pointer pattern (matching Go SDK's JSONPointerPattern)
121static JSON_POINTER_PATTERN: LazyLock<Regex> = LazyLock::new(|| {
122    Regex::new(r"^(/([^/~]|~[01])*)*$").expect("static JSON pointer regex is valid")
123});
124
125/// Validates a semantic version string
126pub fn is_valid_semver(version: &str) -> bool {
127    SEMVER_PATTERN.is_match(version)
128}
129
130/// Validates an RFC 1123 hostname
131pub fn is_valid_hostname(hostname: &str) -> bool {
132    HOSTNAME_RFC1123_PATTERN.is_match(hostname)
133}
134
135/// Validates a required string field as an RFC 1123 hostname.
136/// Adds a Required error if empty, or a Hostname error if invalid format.
137pub fn validate_required_hostname(value: &str, field: &str, result: &mut ValidationResult) {
138    if value.is_empty() {
139        result.add_error(
140            field,
141            ValidationRule::Required,
142            &format!("{} is required", field),
143        );
144    } else if !is_valid_hostname(value) {
145        result.add_error(
146            field,
147            ValidationRule::Hostname,
148            &format!("{} must be a valid RFC 1123 hostname", field),
149        );
150    }
151}
152
153/// Validates a required string field as a semantic version.
154/// Adds a Required error if empty, or a Semver error if invalid format.
155pub fn validate_required_semver(value: &str, field: &str, result: &mut ValidationResult) {
156    if value.is_empty() {
157        result.add_error(
158            field,
159            ValidationRule::Required,
160            &format!("{} is required", field),
161        );
162    } else if !is_valid_semver(value) {
163        result.add_error(
164            field,
165            ValidationRule::Semver,
166            &format!("{} must be a valid semantic version", field),
167        );
168    }
169}
170
171/// Checks if a value is a non-empty string.
172/// Per the Go SDK's `string_or_runtime_expr` validator, any non-empty string is valid
173/// (either a plain string or a runtime expression).
174pub fn is_non_empty_string(value: &str) -> bool {
175    !value.is_empty()
176}
177
178/// Checks if a string value is a valid URI or a valid runtime expression
179/// Matches Go SDK's uri_template_or_runtime_expr validator
180/// Uses Go SDK's LiteralUriPattern and LiteralUriTemplatePattern for validation
181pub fn is_uri_or_runtime_expr(value: &str) -> bool {
182    if value.is_empty() {
183        return false;
184    }
185    // Runtime expressions are valid
186    if is_strict_expr(value) {
187        return true;
188    }
189    // Match Go SDK's URI/URI-template patterns
190    URI_PATTERN.is_match(value) || URI_TEMPLATE_PATTERN.is_match(value)
191}
192
193/// Checks if a string value is a valid JSON Pointer or a valid runtime expression
194/// Matches Go SDK's json_pointer_or_runtime_expr validator
195/// RFC 6901: "" references the whole document, "/foo/bar" references a path
196/// Escape sequences: ~0 = ~, ~1 = /
197pub fn is_json_pointer_or_runtime_expr(value: &str) -> bool {
198    if value.is_empty() {
199        return false;
200    }
201    // Runtime expressions are valid
202    if is_strict_expr(value) {
203        return true;
204    }
205    // RFC 6901 JSON Pointer pattern (matches Go SDK's JSONPointerPattern)
206    JSON_POINTER_PATTERN.is_match(value)
207}
208
209/// Checks if a string value is a valid literal URI (no placeholders)
210/// Matches Go SDK's LiteralUriPattern: `^[A-Za-z][A-Za-z0-9+\-.]*://[^{}\s]+$`
211pub fn is_valid_uri(value: &str) -> bool {
212    URI_PATTERN.is_match(value)
213}
214
215/// Checks if a string value is a valid URI template (with placeholders)
216/// Matches Go SDK's LiteralUriTemplatePattern: `^[A-Za-z][A-Za-z0-9+\-.]*://.*\{.*}.*$`
217pub fn is_valid_uri_template(value: &str) -> bool {
218    URI_TEMPLATE_PATTERN.is_match(value)
219}
220
221/// Checks if a string value is a valid JSON Pointer (RFC 6901)
222/// Matches Go SDK's JSONPointerPattern: `^(/([^/~]|~[01])*)*$`
223pub fn is_valid_json_pointer(value: &str) -> bool {
224    JSON_POINTER_PATTERN.is_match(value)
225}