Skip to main content

wowlab_engine_adapter_data/remote/cache/
error.rs

1use std::{fmt, sync::Arc};
2
3use wowlab_fs::{
4    error,
5    path::{Path, PathBuf},
6};
7use wowlab_supabase::SupabaseError;
8
9/// Operation that failed while loading or maintaining game-data cache state.
10#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum CacheOperation {
13    CreateDirectory,
14    ReadEntry,
15    WriteEntry,
16    CommitEntry,
17    DeleteEntry,
18    ClearCategory,
19    ReadPatchVersion,
20    WritePatchVersion,
21    FetchRemote,
22    LoadShared,
23}
24
25impl fmt::Display for CacheOperation {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(match self {
28            Self::CreateDirectory => "create cache directory",
29            Self::ReadEntry => "read cache entry",
30            Self::WriteEntry => "write cache entry",
31            Self::CommitEntry => "commit cache entry",
32            Self::DeleteEntry => "delete cache entry",
33            Self::ClearCategory => "clear cache category",
34            Self::ReadPatchVersion => "read patch version",
35            Self::WritePatchVersion => "write patch version",
36            Self::FetchRemote => "fetch remote data",
37            Self::LoadShared => "load shared cache value",
38        })
39    }
40}
41
42wowlab_engine_macros::define_error! {
43/// Failure while loading or maintaining the patch-versioned game-data cache.
44#[non_exhaustive]
45pub struct CacheError {
46    #[source]
47    kind: CacheErrorKind,
48}
49
50#[derive(Debug, thiserror::Error)]
51enum CacheErrorKind {
52    #[error(
53        "cache {operation} failed{entry}: {source}",
54        entry = CacheEntryLocation::new(entry_key.as_deref())
55    )]
56    Filesystem {
57        operation: CacheOperation,
58        entry_key: Option<String>,
59        #[source]
60        source: error::Error,
61    },
62    #[error(
63        "cache {operation} failed{location}: {source}",
64        location = CacheFailureLocation::new(path, entry_key.as_deref())
65    )]
66    Json {
67        operation: CacheOperation,
68        path: PathBuf,
69        entry_key: Option<String>,
70        #[source]
71        source: serde_json::Error,
72    },
73    #[error("cache {operation} failed for {entry_key}: {source}")]
74    Remote {
75        operation: CacheOperation,
76        entry_key: String,
77        #[source]
78        source: SupabaseError,
79    },
80    #[error("Not found: {resource} with {key}={value}")]
81    MissingRow {
82        resource: String,
83        key: String,
84        value: String,
85    },
86    #[error("cache {operation} failed for {entry_key}: {source}")]
87    Shared {
88        operation: CacheOperation,
89        entry_key: String,
90        #[source]
91        source: Arc<CacheError>,
92    },
93}
94}
95
96struct CacheFailureLocation<'a> {
97    path: &'a Path,
98    entry_key: Option<&'a str>,
99}
100
101impl<'a> CacheFailureLocation<'a> {
102    const fn new(path: &'a Path, entry_key: Option<&'a str>) -> Self {
103        Self { path, entry_key }
104    }
105}
106
107impl fmt::Display for CacheFailureLocation<'_> {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        if let Some(entry_key) = self.entry_key {
110            write!(f, " for {entry_key} at {}", self.path.display())
111        } else {
112            write!(f, " at {}", self.path.display())
113        }
114    }
115}
116
117struct CacheEntryLocation<'a>(Option<&'a str>);
118
119impl<'a> CacheEntryLocation<'a> {
120    const fn new(entry_key: Option<&'a str>) -> Self {
121        Self(entry_key)
122    }
123}
124
125impl fmt::Display for CacheEntryLocation<'_> {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match self.0 {
128            Some(entry_key) => write!(f, " for {entry_key}"),
129            None => Ok(()),
130        }
131    }
132}
133
134impl CacheError {
135    pub(super) fn filesystem(
136        operation: CacheOperation,
137        entry_key: Option<String>,
138        source: error::Error,
139    ) -> Self {
140        Self {
141            kind: CacheErrorKind::Filesystem {
142                operation,
143                entry_key,
144                source,
145            },
146        }
147    }
148
149    pub(super) fn json(
150        operation: CacheOperation,
151        path: impl Into<PathBuf>,
152        entry_key: Option<String>,
153        source: serde_json::Error,
154    ) -> Self {
155        Self {
156            kind: CacheErrorKind::Json {
157                operation,
158                path: path.into(),
159                entry_key,
160                source,
161            },
162        }
163    }
164
165    pub(super) fn remote(entry_key: impl Into<String>, source: SupabaseError) -> Self {
166        Self {
167            kind: CacheErrorKind::Remote {
168                operation: CacheOperation::FetchRemote,
169                entry_key: entry_key.into(),
170                source,
171            },
172        }
173    }
174
175    pub(super) fn missing(
176        resource: impl Into<String>,
177        key: impl Into<String>,
178        value: impl Into<String>,
179    ) -> Self {
180        Self {
181            kind: CacheErrorKind::MissingRow {
182                resource: resource.into(),
183                key: key.into(),
184                value: value.into(),
185            },
186        }
187    }
188
189    pub(super) fn shared(entry_key: impl Into<String>, source: Arc<Self>) -> Self {
190        Self {
191            kind: CacheErrorKind::Shared {
192                operation: CacheOperation::LoadShared,
193                entry_key: entry_key.into(),
194                source,
195            },
196        }
197    }
198
199    /// Returns the failed cache operation.
200    #[must_use]
201    pub fn operation(&self) -> CacheOperation {
202        match &self.kind {
203            CacheErrorKind::Filesystem { operation, .. }
204            | CacheErrorKind::Json { operation, .. }
205            | CacheErrorKind::Remote { operation, .. }
206            | CacheErrorKind::Shared { operation, .. } => *operation,
207            CacheErrorKind::MissingRow { .. } => CacheOperation::FetchRemote,
208        }
209    }
210
211    /// Returns the affected filesystem path for disk-cache failures.
212    #[must_use]
213    pub fn path(&self) -> Option<&Path> {
214        match &self.kind {
215            CacheErrorKind::Filesystem { source, .. } => Some(source.path()),
216            CacheErrorKind::Json { path, .. } => Some(path),
217            _ => None,
218        }
219    }
220
221    /// Returns the stable cache entry identity when one entry was involved.
222    #[must_use]
223    pub fn entry_key(&self) -> Option<&str> {
224        match &self.kind {
225            CacheErrorKind::Filesystem { entry_key, .. }
226            | CacheErrorKind::Json { entry_key, .. } => entry_key.as_deref(),
227            CacheErrorKind::Remote { entry_key, .. } | CacheErrorKind::Shared { entry_key, .. } => {
228                Some(entry_key)
229            }
230            CacheErrorKind::MissingRow { .. } => None,
231        }
232    }
233
234    /// Returns the missing remote-row identity, when applicable.
235    #[must_use]
236    pub fn missing_row(&self) -> Option<(&str, &str, &str)> {
237        match &self.kind {
238            CacheErrorKind::MissingRow {
239                resource,
240                key,
241                value,
242            } => Some((resource, key, value)),
243            _ => None,
244        }
245    }
246}
247
248impl fmt::Debug for CacheError {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        f.debug_struct("CacheError")
251            .field("operation", &self.operation())
252            .field("entry_key", &self.entry_key())
253            .field("path", &self.path().map(|_| "<redacted>"))
254            .field("source", &"<redacted>")
255            .finish_non_exhaustive()
256    }
257}