Skip to main content

uptrakit_web_api_types/
validation.rs

1use std::fmt;
2
3/// Error returned when request field validation fails.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct ValidationError {
6    pub field: &'static str,
7    pub message: String,
8}
9
10impl fmt::Display for ValidationError {
11    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12        write!(f, "{}: {}", self.field, self.message)
13    }
14}
15
16impl std::error::Error for ValidationError {}
17
18/// Trait for validating request types before processing.
19pub trait Validate {
20    fn validate(&self) -> Result<(), ValidationError>;
21}
22
23pub(crate) mod sealed {
24    pub trait Sealed {}
25}
26// pub(crate): the Sealed impl lives in the sibling surfaces.rs module (a
27// private `mod sealed` would E0603 there); still unimplementable outside
28// the crate, so the seal holds.
29
30/// Routing envelope for both invoke paths: projected out of an
31/// `Unvalidated<T>` body by [`RoutingEnvelope`], and built directly from the
32/// GET query string by web-api's `split_get_envelope`
33/// (`crates/ui/web-api/src/routes/surfaces.rs`) — one type, so the two paths
34/// cannot drift apart.
35/// Declared beside [`RoutingEnvelope`] so the carve-out's breadth is fixed
36/// here: widening what pre-validation code can see means adding a field to
37/// THIS struct — a reviewed change at the trait's own home, never a per-impl
38/// decision.
39#[derive(Debug, Clone)]
40pub struct InvokeRoutingEnvelope {
41    pub target_provider_id: Option<String>,
42    pub timeout_seconds: Option<u16>,
43}
44
45/// Routing metadata a dispatcher may read from a body before validation.
46/// Sealed: implementable only inside web-api-types, so declaring a type's
47/// envelope is a reviewed change in the crate that owns the request types —
48/// a doc-comment convention alone would be an ungated escape hatch from the
49/// `Unvalidated<T>` type-state guarantee. Envelope fields are pure routing
50/// inputs (they select a target; they are never business payload). No
51/// associated type: an unconstrained `type Envelope` would let a future impl
52/// return `Self` and hand the whole pre-validation body out — the concrete
53/// return type bounds the projection at the trait, not per impl.
54pub trait RoutingEnvelope: sealed::Sealed {
55    fn routing_envelope(&self) -> InvokeRoutingEnvelope;
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn validation_error_display() {
64        let err = ValidationError {
65            field: "email",
66            message: "must contain @".to_string(),
67        };
68        assert_eq!(err.to_string(), "email: must contain @");
69    }
70}