cow_sdk_core/validation.rs
1//! Shared validation-failure and transport-classification enums used across
2//! the `cow-sdk` crate family.
3//!
4//! [`ValidationReason`] describes canonical validation-failure modes on the
5//! public input boundary. Downstream crates that surface structured
6//! `{ field, reason: ValidationReason }` error variants route through this
7//! enum so callers can pattern-match on the reason without parsing free-form
8//! strings.
9//!
10//! [`TransportErrorClass`] classifies REST-transport failure categories
11//! produced by `reqwest` error states. Downstream crates that expose typed
12//! `Transport { class, detail }` error variants use this shared enum so
13//! telemetry and retry policies can partition transport outcomes uniformly.
14
15use thiserror::Error;
16
17/// Canonical validation-failure modes emitted by structured error variants.
18///
19/// Variants carry `&'static str` detail slots where applicable so consumers
20/// get actionable context without paying the cost of a heap allocation.
21/// `Missing` names the field itself as the whole reason; structured call
22/// sites still carry a `field: &'static str` alongside this enum to make the
23/// combination self-describing.
24#[non_exhaustive]
25#[derive(Debug, Clone, PartialEq, Eq, Error)]
26pub enum ValidationReason {
27 /// The field was required but was not supplied.
28 #[error("missing required value")]
29 Missing,
30 /// The field was out of the documented acceptable range.
31 #[error("out of range: {details}")]
32 OutOfRange {
33 /// Narrow explanation of the out-of-range condition.
34 details: &'static str,
35 },
36 /// The field did not match the documented shape or format.
37 #[error("bad shape: {details}")]
38 BadShape {
39 /// Narrow explanation of the shape violation.
40 details: &'static str,
41 },
42 /// The field violated a documented precondition for the operation.
43 #[error("precondition violated: {details}")]
44 Precondition {
45 /// Narrow explanation of the violated precondition.
46 details: &'static str,
47 },
48}
49
50/// REST-transport error classification shared across transport-capable crates.
51///
52/// Built by classifying a [`reqwest::Error`] through the documented partition
53/// (`is_timeout`, `is_connect`, `is_redirect`, `is_decode`, `is_body`,
54/// `is_builder`, `is_request`, `is_status`, fallthrough). The
55/// [`ResponseTooLarge`](Self::ResponseTooLarge) class is the exception: it is
56/// produced by the transport's own response-size guard rather than by
57/// `reqwest` classification. Downstream error surfaces pair this enum with a
58/// redacted detail string to produce the public `Transport { class, detail }`
59/// variant shape.
60#[non_exhaustive]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum TransportErrorClass {
63 /// The request timed out before a response was received.
64 Timeout,
65 /// The request could not establish a connection to the remote host.
66 Connect,
67 /// The request hit a redirect-handling failure.
68 Redirect,
69 /// Response body decoding failed.
70 Decode,
71 /// Request or response body-stream handling failed.
72 Body,
73 /// The request could not be built locally.
74 Builder,
75 /// The request failed at the HTTP request layer without a structured status.
76 Request,
77 /// The server returned a non-success status code without a structured body.
78 Status,
79 /// HTTP `101 Switching Protocols` upgrade (e.g., WebSocket); reserved
80 /// for future streaming response API. Not currently produced by any
81 /// in-tree transport implementation.
82 Upgrade,
83 /// The response body exceeded the configured maximum size, so the
84 /// transport refused to buffer it. Produced by the SDK's response-size
85 /// guard, not by `reqwest` classification.
86 ResponseTooLarge,
87 /// The transport failure does not match any of the named categories.
88 Other,
89}
90
91impl TransportErrorClass {
92 /// Returns the canonical lowercase label for this class, suitable for
93 /// telemetry labels and structured log fields.
94 #[must_use]
95 pub const fn as_str(self) -> &'static str {
96 match self {
97 Self::Timeout => "timeout",
98 Self::Connect => "connect",
99 Self::Redirect => "redirect",
100 Self::Decode => "decode",
101 Self::Body => "body",
102 Self::Builder => "builder",
103 Self::Request => "request",
104 Self::Status => "status",
105 Self::Upgrade => "upgrade",
106 Self::ResponseTooLarge => "response_too_large",
107 Self::Other => "other",
108 }
109 }
110}
111
112impl std::fmt::Display for TransportErrorClass {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str(self.as_str())
115 }
116}