dir_structure/
versioned.rs

1//! A versioned value.
2//!
3//! See [`Versioned`] for more details.
4
5use std::ops::Deref;
6use std::ops::DerefMut;
7use std::path::Path;
8use std::pin::Pin;
9#[cfg(feature = "async")]
10use std::task::Context;
11#[cfg(feature = "async")]
12use std::task::Poll;
13
14#[cfg(feature = "async")]
15use pin_project::pin_project;
16
17use crate::error::VfsResult;
18use crate::prelude::*;
19#[cfg(feature = "async")]
20use crate::traits::async_vfs::VfsAsync;
21#[cfg(feature = "async")]
22use crate::traits::async_vfs::WriteSupportingVfsAsync;
23#[cfg(feature = "resolve-path")]
24use crate::traits::resolve::DynamicHasField;
25#[cfg(feature = "resolve-path")]
26use crate::traits::resolve::HAS_FIELD_MAX_LEN;
27#[cfg(feature = "resolve-path")]
28use crate::traits::resolve::HasField;
29use crate::traits::vfs;
30#[cfg(feature = "resolve-path")]
31use crate::traits::vfs::OwnedPathType;
32use crate::traits::vfs::PathType;
33#[cfg(feature = "async")]
34use crate::traits::vfs::VfsCore;
35
36/// A versioned value. This is a wrapper around a value that will keep track of
37/// how many times it has been changed. This is useful to not write the value
38/// to disk if it hasn't changed.
39///
40/// You can get a reference to the value via its [`Deref`] implementation, and
41/// you can get a mutable reference to the value via its [`DerefMut`] implementation.
42///
43/// The version is incremented every time [`DerefMut::deref_mut`] is called.
44///
45/// Alternatively, for [`Eq`] types, you can use the [`Versioned::edit_eq_check`]
46/// method to edit the value, and it will increment the version if the value has changed.
47///
48/// # Example
49///
50/// ```
51/// use std::path::Path;
52/// use dir_structure::versioned::VersionedString;
53///
54/// let mut v = VersionedString::<Path>::new("value".to_owned(), "path".to_owned());
55/// assert!(v.is_clean());
56/// assert!(!v.is_dirty());
57///
58/// *v = "new value".to_owned();
59/// assert!(v.is_dirty());
60/// ```
61#[derive(Debug, Hash, PartialEq, Eq)]
62#[cfg_attr(feature = "assert_eq", derive(assert_eq::AssertEq))]
63pub struct Versioned<T, P: PathType + ?Sized = Path> {
64    value: T,
65    version: usize,
66    path: P::OwnedPath,
67}
68
69impl<T, P: PathType + ?Sized> Clone for Versioned<T, P>
70where
71    T: Clone,
72    P::OwnedPath: Clone,
73{
74    fn clone(&self) -> Self {
75        Self {
76            value: self.value.clone(),
77            version: self.version,
78            path: self.path.clone(),
79        }
80    }
81}
82
83impl<T, P: PathType + ?Sized> Versioned<T, P> {
84    const DEFAULT_VERSION: usize = 0;
85
86    /// Creates a new [`Versioned`] with the specified value.
87    ///
88    /// The version is set to the default value.
89    pub fn new(value: T, path: impl Into<P::OwnedPath>) -> Self {
90        Self {
91            value,
92            version: Self::DEFAULT_VERSION,
93            path: path.into(),
94        }
95    }
96
97    /// Creates a new [`Versioned`] with the specified value, and in a dirty state.
98    ///
99    /// # Example
100    ///
101    /// ```
102    /// use std::path::Path;
103    /// use dir_structure::versioned::VersionedString;
104    ///
105    /// let v = VersionedString::<Path>::new_dirty("value".to_owned(), "path".to_owned());
106    /// assert!(v.is_dirty());
107    /// ```
108    pub fn new_dirty(value: T, path: impl Into<P::OwnedPath>) -> Self {
109        Self {
110            value,
111            version: Self::DEFAULT_VERSION + 1,
112            path: path.into(),
113        }
114    }
115
116    /// Checks if the value has been changed.
117    ///
118    /// # Example
119    ///
120    /// ```
121    /// use std::path::Path;
122    /// use dir_structure::versioned::VersionedString;
123    ///
124    /// let mut v = VersionedString::<Path>::new("value".to_owned(), "path".to_owned());
125    /// assert!(!v.is_dirty());
126    /// *v = "new value".to_owned();
127    /// assert!(v.is_dirty());
128    /// ```
129    pub fn is_dirty(&self) -> bool {
130        !self.is_clean()
131    }
132
133    /// Checks if the value has not been changed.
134    ///
135    /// # Example
136    ///
137    /// ```
138    /// use std::path::Path;
139    /// use dir_structure::versioned::VersionedString;
140    ///
141    /// let mut v = VersionedString::<Path>::new("value".to_owned(), "path".to_owned());
142    /// assert!(v.is_clean());
143    /// *v = "new value".to_owned();
144    /// assert!(!v.is_clean());
145    /// ```
146    pub fn is_clean(&self) -> bool {
147        self.version == Self::DEFAULT_VERSION
148    }
149
150    /// Edits the value using the provided closure, and increments the version
151    /// if the value has changed.
152    ///
153    /// # Example
154    ///
155    /// ```
156    /// use std::path::Path;
157    /// use dir_structure::versioned::VersionedString;
158    ///
159    /// let mut v = VersionedString::<Path>::new("value".to_owned(), "path".to_owned());
160    ///
161    /// v.edit_eq_check(|s| *s = "value".to_owned());
162    /// assert!(v.is_clean());
163    /// v.edit_eq_check(|s| *s = "new value".to_owned());
164    /// assert!(v.is_dirty());
165    /// ```
166    pub fn edit_eq_check(&mut self, f: impl FnOnce(&mut T))
167    where
168        T: Eq + Clone,
169    {
170        let copy = self.value.clone();
171
172        f(&mut self.value);
173
174        if copy != self.value {
175            self.version += 1;
176        }
177    }
178
179    /// Resets the version to the default value, making the value clean.
180    /// This is useful if you want to mark the value as not changed,
181    /// without actually changing it.
182    ///
183    /// # Safety
184    ///
185    /// This function is unsafe because it allows you to reset the version to 0,
186    /// which means that the value will be considered clean, and any unsaved changes
187    /// will be lost. Trying to save a clean value (e.g. after calling this function) will *not* write it to disk!
188    ///
189    /// Use with caution!
190    ///
191    /// # Examples
192    ///
193    /// ```
194    /// use std::path::Path;
195    /// use dir_structure::{traits::sync::DirStructureItem, versioned::VersionedString};
196    /// std::fs::write("path", "value").unwrap();
197    ///
198    /// let mut v = VersionedString::<Path>::new("value".to_owned(), "path".to_owned());
199    /// assert!(v.is_clean());
200    /// v.edit_eq_check(|s| *s = "new value".to_owned());
201    /// assert!(v.is_dirty());
202    /// unsafe { v.reset(); }
203    /// assert!(v.is_clean());
204    ///
205    /// // if you try to write it now, it won't write anything,
206    /// v.write("path").unwrap();
207    ///
208    /// assert_eq!(std::fs::read_to_string("path").unwrap(), "value");
209    /// # std::fs::remove_file("path").unwrap();
210    /// ```
211    #[expect(unsafe_code, reason = "This function is unsafe by design")]
212    pub unsafe fn reset(&mut self) {
213        // This is unsafe because it allows us to reset the version to 0,
214        // which means that the value will be considered clean.
215        // Use with caution!
216        self.version = Self::DEFAULT_VERSION;
217    }
218}
219
220impl<'a, Vfs: vfs::Vfs<'a>, T> ReadFrom<'a, Vfs> for Versioned<T, Vfs::Path>
221where
222    T: ReadFrom<'a, Vfs>,
223{
224    fn read_from(path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<Self, Vfs>
225    where
226        Self: Sized,
227    {
228        T::read_from(path, vfs).map(|it| Self::new(it, path.owned()))
229    }
230}
231
232#[cfg(feature = "async")]
233#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
234#[pin_project]
235#[doc(hidden)]
236pub struct VersionedReadFuture<'a, Vfs: VfsAsync, T: ReadFromAsync<'a, Vfs> + Send + 'static> {
237    #[pin]
238    inner: T::Future,
239    path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
240}
241
242#[cfg(feature = "async")]
243#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
244impl<'a, Vfs: VfsAsync + 'static, T> Future for VersionedReadFuture<'a, Vfs, T>
245where
246    T: ReadFromAsync<'a, Vfs> + Send + 'static,
247{
248    type Output = VfsResult<Versioned<T, Vfs::Path>, Vfs>;
249
250    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
251        let projection = self.project();
252        <T::Future as Future>::poll(projection.inner, cx)
253            .map_ok(|value| Versioned::new(value, projection.path.clone()))
254    }
255}
256
257#[cfg(feature = "async")]
258#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
259impl<'a, Vfs: VfsAsync + 'static, T: ReadFromAsync<'a, Vfs> + Send + 'static> ReadFromAsync<'a, Vfs>
260    for Versioned<T, Vfs::Path>
261{
262    type Future = VersionedReadFuture<'a, Vfs, T>;
263
264    fn read_from_async(
265        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
266        vfs: Pin<&'a Vfs>,
267    ) -> Self::Future {
268        VersionedReadFuture {
269            inner: T::read_from_async(path.clone(), vfs),
270            path,
271        }
272    }
273}
274
275impl<'a, Vfs: vfs::WriteSupportingVfs<'a>, T: WriteTo<'a, Vfs>> WriteTo<'a, Vfs>
276    for Versioned<T, Vfs::Path>
277where
278    Vfs::Path: PartialEq,
279{
280    fn write_to(&self, path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<(), Vfs> {
281        if self.path.as_ref() == path && self.is_clean() {
282            return Ok(());
283        }
284
285        self.value.write_to(path, vfs)
286    }
287}
288
289#[cfg(feature = "async")]
290#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
291#[pin_project(project_replace = VersionedWriteFutureProj)]
292#[doc(hidden)]
293pub enum VersionedWriteFuture<'a, T, Vfs: WriteSupportingVfsAsync + 'a>
294where
295    T: WriteToAsync<'a, Vfs> + Send + Sync + 'static,
296    <T as WriteToAsync<'a, Vfs>>::Future: Future<Output = VfsResult<(), Vfs>> + Unpin + 'a,
297{
298    Poisson,
299    NotTouched,
300    Writing {
301        inner: <T as WriteToAsync<'a, Vfs>>::Future,
302    },
303}
304
305#[cfg(feature = "async")]
306#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
307impl<'a, T, Vfs: WriteSupportingVfsAsync + 'a> Future for VersionedWriteFuture<'a, T, Vfs>
308where
309    T: WriteToAsync<'a, Vfs> + Send + Sync + 'static,
310    <T as WriteToAsync<'a, Vfs>>::Future: Future<Output = VfsResult<(), Vfs>> + Unpin + 'a,
311{
312    type Output = VfsResult<(), Vfs>;
313
314    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
315        let this = self.as_mut().project_replace(Self::Poisson);
316        match this {
317            VersionedWriteFutureProj::NotTouched => Poll::Ready(Ok(())),
318            VersionedWriteFutureProj::Writing { mut inner } => {
319                match Pin::new(&mut inner).poll(cx) {
320                    Poll::Ready(res) => Poll::Ready(res),
321                    Poll::Pending => {
322                        self.project_replace(Self::Writing { inner });
323                        Poll::Pending
324                    }
325                }
326            }
327            VersionedWriteFutureProj::Poisson => {
328                panic!("VersionedWriteFuture is in an invalid state. This is a bug in the code.");
329            }
330        }
331    }
332}
333
334#[cfg(feature = "async")]
335#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
336impl<'a, T, Vfs: WriteSupportingVfsAsync + 'static> WriteToAsync<'a, Vfs>
337    for Versioned<T, Vfs::Path>
338where
339    T: WriteToAsync<'a, Vfs> + Send + Sync + 'static,
340    <T as WriteToAsync<'a, Vfs>>::Future: Future<Output = VfsResult<(), Vfs>> + Unpin,
341    Vfs::Path: PartialEq,
342{
343    type Future = VersionedWriteFuture<'a, T, Vfs>;
344
345    fn write_to_async(
346        self,
347        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
348        vfs: Pin<&'a Vfs>,
349    ) -> Self::Future {
350        if self.path.as_ref() == path.as_ref() && self.is_clean() {
351            return VersionedWriteFuture::NotTouched;
352        }
353
354        VersionedWriteFuture::Writing {
355            inner: self.value.write_to_async(path, vfs),
356        }
357    }
358}
359
360#[cfg(feature = "async")]
361#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
362#[pin_project(project_replace = VersionedWriteRefFutureProj)]
363#[doc(hidden)]
364pub enum VersionedWriteRefFuture<'a, 'f, T, Vfs: WriteSupportingVfsAsync + 'a>
365where
366    T: WriteToAsyncRef<'a, Vfs> + Send + Sync + 'static,
367    <T as WriteToAsyncRef<'a, Vfs>>::Future<'f>: Future<Output = VfsResult<(), Vfs>> + Unpin + 'f,
368    'a: 'f,
369{
370    Poisson,
371    NotTouched,
372    Writing {
373        inner: <T as WriteToAsyncRef<'a, Vfs>>::Future<'f>,
374    },
375}
376
377#[cfg(feature = "async")]
378#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
379impl<'a, 'f, T, Vfs: WriteSupportingVfsAsync + 'a> Future
380    for VersionedWriteRefFuture<'a, 'f, T, Vfs>
381where
382    T: WriteToAsyncRef<'a, Vfs> + Send + Sync + 'static,
383    <T as WriteToAsyncRef<'a, Vfs>>::Future<'f>: Future<Output = VfsResult<(), Vfs>> + Unpin + 'f,
384    'a: 'f,
385{
386    type Output = VfsResult<(), Vfs>;
387
388    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
389        let this = self.as_mut().project_replace(Self::Poisson);
390        match this {
391            VersionedWriteRefFutureProj::NotTouched => Poll::Ready(Ok(())),
392            VersionedWriteRefFutureProj::Writing { mut inner } => {
393                match Pin::new(&mut inner).poll(cx) {
394                    Poll::Ready(res) => Poll::Ready(res),
395                    Poll::Pending => {
396                        self.project_replace(Self::Writing { inner });
397                        Poll::Pending
398                    }
399                }
400            }
401            VersionedWriteRefFutureProj::Poisson => {
402                panic!(
403                    "VersionedWriteRefFuture is in an invalid state. This is a bug in the code."
404                );
405            }
406        }
407    }
408}
409
410#[cfg(feature = "async")]
411#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
412impl<'r, T, Vfs: WriteSupportingVfsAsync + 'static> WriteToAsyncRef<'r, Vfs>
413    for Versioned<T, Vfs::Path>
414where
415    T: WriteToAsyncRef<'r, Vfs> + Send + Sync + 'static,
416    for<'f> <T as WriteToAsyncRef<'r, Vfs>>::Future<'f>:
417        Future<Output = VfsResult<(), Vfs>> + Unpin + 'f,
418    Vfs::Path: PartialEq,
419{
420    type Future<'a>
421        = VersionedWriteRefFuture<'r, 'a, T, Vfs>
422    where
423        Self: 'a,
424        'r: 'a,
425        Vfs: 'a;
426
427    fn write_to_async_ref<'a>(
428        &'a self,
429        path: <<Vfs as VfsCore>::Path as PathType>::OwnedPath,
430        vfs: Pin<&'a Vfs>,
431    ) -> <Self as WriteToAsyncRef<'r, Vfs>>::Future<'a>
432    where
433        'r: 'a,
434    {
435        if self.path.as_ref() == path.as_ref() && self.is_clean() {
436            return VersionedWriteRefFuture::NotTouched;
437        }
438
439        VersionedWriteRefFuture::Writing {
440            inner: self.value.write_to_async_ref(path, vfs),
441        }
442    }
443}
444
445#[cfg(feature = "resolve-path")]
446#[cfg_attr(docsrs, doc(cfg(feature = "resolve-path")))]
447impl<const NAME: [char; HAS_FIELD_MAX_LEN], T, P: PathType + ?Sized> HasField<NAME>
448    for Versioned<T, P>
449where
450    T: HasField<NAME>,
451{
452    type Inner = <T as HasField<NAME>>::Inner;
453
454    fn resolve_path<Pt: OwnedPathType>(p: Pt) -> Pt {
455        T::resolve_path(p)
456    }
457}
458
459#[cfg(feature = "resolve-path")]
460#[cfg_attr(docsrs, doc(cfg(feature = "resolve-path")))]
461impl<T, P: PathType + ?Sized> DynamicHasField for Versioned<T, P>
462where
463    T: DynamicHasField,
464{
465    type Inner = <T as DynamicHasField>::Inner;
466
467    fn resolve_path<Pt: OwnedPathType>(p: Pt, name: &str) -> Pt {
468        T::resolve_path(p, name)
469    }
470}
471
472impl<T, P: PathType + ?Sized> Deref for Versioned<T, P> {
473    type Target = T;
474
475    fn deref(&self) -> &Self::Target {
476        &self.value
477    }
478}
479
480impl<T, P: PathType + ?Sized> DerefMut for Versioned<T, P> {
481    fn deref_mut(&mut self) -> &mut Self::Target {
482        // We will assume that the value has changed, if `deref_mut` was called.
483        // So we increment the version.
484        self.version += 1;
485
486        &mut self.value
487    }
488}
489
490/// A [`Versioned`] [`String`].
491#[expect(type_alias_bounds)]
492pub type VersionedString<P: PathType + ?Sized = Path> = Versioned<String, P>;
493/// A [`Versioned`] `Vec<u8>`.
494#[expect(type_alias_bounds)]
495pub type VersionedBytes<P: PathType + ?Sized = Path> = Versioned<Vec<u8>, P>;
496
497#[cfg(test)]
498mod tests {
499    use std::path::Path;
500    use std::pin::Pin;
501    use std::sync::atomic::AtomicUsize;
502    use std::sync::atomic::Ordering;
503
504    use super::*;
505
506    struct WriteCounter<T> {
507        count: AtomicUsize,
508        inner: T,
509    }
510
511    impl<T> WriteCounter<T> {
512        fn write_count(&self) -> usize {
513            self.count.load(Ordering::SeqCst)
514        }
515    }
516
517    impl<T> Deref for WriteCounter<T> {
518        type Target = T;
519
520        fn deref(&self) -> &Self::Target {
521            &self.inner
522        }
523    }
524
525    impl<T> DerefMut for WriteCounter<T> {
526        fn deref_mut(&mut self) -> &mut Self::Target {
527            &mut self.inner
528        }
529    }
530
531    impl<'a, Vfs: vfs::Vfs<'a>, T: ReadFrom<'a, Vfs>> ReadFrom<'a, Vfs> for WriteCounter<T> {
532        fn read_from(path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<Self, Vfs> {
533            Ok(Self {
534                count: AtomicUsize::new(0),
535                inner: T::read_from(path, vfs)?,
536            })
537        }
538    }
539
540    impl<'a, Vfs: vfs::WriteSupportingVfs<'a>, T: WriteTo<'a, Vfs>> WriteTo<'a, Vfs>
541        for WriteCounter<T>
542    {
543        fn write_to(&self, path: &Vfs::Path, vfs: Pin<&'a Vfs>) -> VfsResult<(), Vfs> {
544            self.inner.write_to(path, vfs)?;
545            self.count.fetch_add(1, Ordering::SeqCst);
546            Ok(())
547        }
548    }
549
550    #[test]
551    fn versioned_works() {
552        let s = VersionedString::<Path>::new("value".to_owned(), "path");
553        assert!(s.is_clean());
554        assert!(!s.is_dirty());
555
556        let mut s = s;
557        *s = "new value".to_owned();
558        assert!(s.is_dirty());
559        assert!(!s.is_clean());
560
561        s.edit_eq_check(|v| *v = "new value".to_owned());
562        assert!(s.is_dirty());
563        assert!(!s.is_clean());
564        s.edit_eq_check(|v| *v = "value".to_owned());
565        assert!(s.is_dirty());
566        assert!(!s.is_clean());
567
568        #[expect(unsafe_code, reason = "This function is unsafe by design")]
569        unsafe {
570            s.reset();
571        }
572
573        assert!(s.is_clean());
574        assert!(!s.is_dirty());
575
576        s.edit_eq_check(|v| *v = "value".to_owned());
577        assert!(s.is_clean());
578        assert!(!s.is_dirty());
579
580        s.edit_eq_check(|v| *v = "new value".to_owned());
581        assert!(s.is_dirty());
582        assert!(!s.is_clean());
583    }
584
585    #[test]
586    fn type_checks() {
587        crate::test_utils::assert_is_read_from::<crate::vfs::fs_vfs::FsVfs, VersionedString<Path>>(
588        );
589        crate::test_utils::assert_is_write_to::<crate::vfs::fs_vfs::FsVfs, VersionedString<Path>>();
590    }
591}