aster_forge_webdav/
event.rs

1//! Observable `WebDAV` operation events.
2
3use std::time::Duration;
4use std::{panic::AssertUnwindSafe, panic::catch_unwind};
5
6use crate::{DavBackendErrorKind, DavPath, DavRequestHead};
7
8/// Protocol operations exposed to event observers.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DavOperation {
11    Options,
12    Propfind,
13    Proppatch,
14    Get,
15    Head,
16    Post,
17    Put,
18    Patch,
19    Mkcol,
20    Delete,
21    Copy,
22    Move,
23    Lock,
24    Unlock,
25    Acl,
26    Report,
27    VersionControl,
28    Checkout,
29    Checkin,
30    Uncheckout,
31    Mkworkspace,
32    Update,
33    Label,
34    Merge,
35    BaselineControl,
36    Mkactivity,
37    Search,
38    Orderpatch,
39    Mkredirectref,
40    Updateredirectref,
41    Bind,
42    Unbind,
43    Rebind,
44}
45
46/// Protocol result exposed to observers without credentials, bodies, or lock tokens.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum DavEventOutcome {
49    Succeeded {
50        /// HTTP/WebDAV response status without leaking a transport crate version.
51        status: u16,
52    },
53    Failed {
54        /// HTTP/WebDAV response status without leaking a transport crate version.
55        status: u16,
56        backend_error: Option<DavBackendErrorKind>,
57    },
58}
59
60/// Product-neutral class for a protocol-side failure observation.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum DavProtocolFailureClass {
63    Request,
64    Precondition,
65    Capability,
66    Backend,
67    Response,
68    Transport,
69}
70
71/// Outcome of a response stream after the operation has been planned.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum DavStreamOutcome {
74    Completed,
75    Cancelled { response_started: bool },
76    Failed { response_started: bool },
77}
78
79/// Low-frequency scalar observations attached to a completed operation.
80///
81/// `None` means that a product did not collect that fact; zero is a collected zero. The type
82/// intentionally contains no request body, credential, lock token, object key, or label string.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84pub struct DavOperationObservations {
85    pub bytes_received: Option<u64>,
86    pub bytes_sent: Option<u64>,
87    pub requested_ranges: Option<u64>,
88    pub served_ranges: Option<u64>,
89    pub resources: Option<u64>,
90    pub backend_open_count: Option<u64>,
91    pub backend_call_count: Option<u64>,
92    pub protocol_failure: Option<DavProtocolFailureClass>,
93    pub stream: Option<DavStreamOutcome>,
94}
95
96/// Failure reported by a non-authoritative observation sink.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
98#[error("WebDAV observation sink failed")]
99pub struct DavObservationError;
100
101impl DavEventOutcome {
102    /// Classifies a completed response. Informational, success, and redirection statuses are
103    /// successful protocol outcomes; client and server errors are failures.
104    #[must_use]
105    pub const fn from_status(status: u16, backend_error: Option<DavBackendErrorKind>) -> Self {
106        if status < 400 {
107            Self::Succeeded { status }
108        } else {
109            Self::Failed {
110                status,
111                backend_error,
112            }
113        }
114    }
115
116    /// Returns the completed HTTP/WebDAV status.
117    #[must_use]
118    pub const fn status(self) -> u16 {
119        match self {
120            Self::Succeeded { status } | Self::Failed { status, .. } => status,
121        }
122    }
123}
124
125/// One completed `WebDAV` operation.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct DavEvent {
128    pub request_id: Option<String>,
129    pub operation: DavOperation,
130    pub source: DavPath,
131    pub destination: Option<DavPath>,
132    pub outcome: DavEventOutcome,
133    pub elapsed: Duration,
134    pub observations: DavOperationObservations,
135}
136
137impl DavEvent {
138    /// Builds the transport-neutral event for one completed request.
139    ///
140    /// Only protocol routing data is copied from the request head. Conditional headers,
141    /// credentials, request bodies, and lock tokens are deliberately excluded.
142    #[must_use]
143    pub fn completed(
144        request_head: &DavRequestHead,
145        status: u16,
146        elapsed: Duration,
147        backend_error: Option<DavBackendErrorKind>,
148    ) -> Self {
149        Self::completed_with_observations(
150            request_head,
151            status,
152            elapsed,
153            backend_error,
154            DavOperationObservations::default(),
155        )
156    }
157
158    /// Builds a completed event with optional low-frequency observations.
159    #[must_use]
160    pub fn completed_with_observations(
161        request_head: &DavRequestHead,
162        status: u16,
163        elapsed: Duration,
164        backend_error: Option<DavBackendErrorKind>,
165        observations: DavOperationObservations,
166    ) -> Self {
167        Self {
168            request_id: None,
169            operation: request_head.method.operation(),
170            source: request_head.target.clone(),
171            destination: request_head
172                .destination
173                .as_ref()
174                .map(|destination| destination.path.clone()),
175            outcome: DavEventOutcome::from_status(status, backend_error),
176            elapsed,
177            observations,
178        }
179    }
180}
181
182/// Non-authoritative observer for audit adapters, metrics, tracing, and notifications.
183///
184/// Required mutations, quota updates, lock persistence, and cache correctness must complete in
185/// the synchronous backend operation before this observer is called. `publish` must return
186/// promptly without blocking on I/O; products that need asynchronous work must use a bounded,
187/// non-blocking enqueue into a product-owned worker.
188pub trait DavEventSink: Send + Sync {
189    ///
190    /// # Errors
191    ///
192    /// Returns an observation error when the configured event sink rejects the event.
193    fn publish(&self, event: &DavEvent) -> Result<(), DavObservationError>;
194}
195
196/// Publishes a non-authoritative event without allowing observer failure to affect the operation.
197pub fn publish_non_authoritative(sink: Option<&dyn DavEventSink>, event: &DavEvent) {
198    if let Some(sink) = sink {
199        let _ = catch_unwind(AssertUnwindSafe(|| sink.publish(event)));
200    }
201}
202
203/// Event sink used when a product does not need protocol-level observation.
204#[derive(Debug, Clone, Copy, Default)]
205pub struct NoopDavEventSink;
206
207impl DavEventSink for NoopDavEventSink {
208    fn publish(&self, _event: &DavEvent) -> Result<(), DavObservationError> {
209        Ok(())
210    }
211}