devlog/
path.rs

1//! Path to a devlog entry file.
2
3use crate::error::Error;
4use std::cmp::Ordering;
5use std::path::{Path, PathBuf};
6
7/// The maximum possible sequence number of a devlog entry file.
8pub const MAX_SEQ_NUM: usize = 999_999_999;
9
10/// The number of digits in a devlog entry filename.
11pub const NUM_DIGITS: usize = 9;
12
13#[derive(Debug, Eq)]
14pub struct LogPath {
15    path: PathBuf,
16    seq_num: usize,
17}
18
19/// Devlog entry files are numbered sequentially, starting from one.
20/// Each filename is nine digits with the extension ".devlog"; for example, "000000123.devlog".
21/// This ensures that the devlog files appear in sequential order when sorted alphabetically.
22impl LogPath {
23    /// Create a new path with the specified sequence number, which must be at least one
24    /// and at most `MAX_SEQ_NUM`.
25    pub fn new(dir: &Path, seq_num: usize) -> LogPath {
26        assert!(seq_num > 0 && seq_num <= MAX_SEQ_NUM);
27        let mut path = dir.to_path_buf();
28        path.push(format!("{:09}.devlog", seq_num));
29        LogPath { path, seq_num }
30    }
31
32    /// Parse the sequence number from a filesystem path.
33    /// Returns `None` if the filename isn't formatted like "000000123.devlog".
34    pub fn from_path(path: PathBuf) -> Option<LogPath> {
35        let stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
36        let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
37        let seq_num: Option<usize> = stem.parse().ok();
38        match (stem, ext, seq_num) {
39            (s, "devlog", Some(seq_num)) if s.len() == NUM_DIGITS => {
40                Some(LogPath { path, seq_num })
41            }
42            _ => None,
43        }
44    }
45
46    /// Returns the path for the next entry in the sequence.
47    /// In the unlikely event that the maximum sequence number is reached,
48    /// returns `Error::LogFileLimitExceeded`.
49    pub fn next(&self) -> Result<LogPath, Error> {
50        let seq_num = self.seq_num + 1;
51        if seq_num > MAX_SEQ_NUM {
52            Err(Error::LogFileLimitExceeded)
53        } else {
54            let mut path = match self.path.parent() {
55                Some(p) => p.to_path_buf(),
56                None => PathBuf::new(),
57            };
58            path.push(format!("{:09}.devlog", seq_num));
59            Ok(LogPath { path, seq_num })
60        }
61    }
62
63    /// Returns the sequence number (e.g. "00000123.devlog" would have sequence number 123)
64    pub fn seq_num(&self) -> usize {
65        self.seq_num
66    }
67
68    /// Returns the filesystem path.
69    pub fn path(&self) -> &Path {
70        &self.path
71    }
72}
73
74/// Order by sequence number.
75impl PartialOrd for LogPath {
76    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77        Some(self.cmp(other))
78    }
79}
80
81/// Order by sequence number.
82impl Ord for LogPath {
83    fn cmp(&self, other: &Self) -> Ordering {
84        self.seq_num.cmp(&other.seq_num)
85    }
86}
87
88/// Equal if and only if the sequence numbers are equal.
89impl PartialEq for LogPath {
90    fn eq(&self, other: &Self) -> bool {
91        self.seq_num == other.seq_num
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn dir() -> PathBuf {
100        From::from(String::from("/foo/bar"))
101    }
102
103    fn rootdir() -> PathBuf {
104        From::from(String::from("/"))
105    }
106
107    #[test]
108    fn test_new() {
109        let d = dir();
110        let p = LogPath::new(&d, 123);
111        assert_eq!(p.seq_num(), 123);
112        assert_eq!(p.path(), d.join("000000123.devlog"));
113    }
114
115    #[test]
116    fn test_from_path() {
117        let path = dir().join("000000123.devlog");
118        let p = LogPath::from_path(path).unwrap();
119        assert_eq!(p.seq_num(), 123);
120        assert_eq!(p.path(), dir().join("000000123.devlog"));
121    }
122
123    #[test]
124    fn test_from_path_max_seq_num() {
125        let fname = format!("{}.devlog", MAX_SEQ_NUM);
126        let path = dir().join(&fname);
127        let p = LogPath::from_path(path).unwrap();
128        assert_eq!(p.seq_num(), MAX_SEQ_NUM);
129        assert_eq!(p.path(), dir().join(&fname));
130    }
131
132    #[test]
133    fn test_from_path_not_a_number() {
134        let path = dir().join("abc123.devlog");
135        assert!(LogPath::from_path(path).is_none());
136    }
137
138    #[test]
139    fn test_from_path_too_few_digits() {
140        let path = dir().join("12345678.devlog");
141        assert!(LogPath::from_path(path).is_none());
142    }
143
144    #[test]
145    fn test_from_path_too_many_digits() {
146        let path = dir().join("1234567890.devlog");
147        assert!(LogPath::from_path(path).is_none());
148    }
149
150    #[test]
151    fn test_from_path_seq_num_too_large() {
152        let fname = format!("{}.devlog", MAX_SEQ_NUM + 1);
153        let path = dir().join(&fname);
154        assert!(LogPath::from_path(path).is_none());
155    }
156
157    #[test]
158    fn test_from_path_wrong_ext() {
159        let path = dir().join("000000001.csv");
160        assert!(LogPath::from_path(path).is_none());
161    }
162
163    #[test]
164    fn test_next_in_subdir() {
165        let d = dir();
166        let p = LogPath::new(&d, 123).next().unwrap();
167        assert_eq!(p.seq_num(), 124);
168        assert_eq!(p.path(), dir().join("000000124.devlog"));
169    }
170
171    #[test]
172    fn test_next_in_rootdir() {
173        let d = rootdir();
174        let p = LogPath::new(&d, 123).next().unwrap();
175        assert_eq!(p.seq_num(), 124);
176        assert_eq!(p.path(), d.join("000000124.devlog"));
177    }
178
179    #[test]
180    fn test_next_file_limit_exceeded() {
181        let d = dir();
182        let p = LogPath::new(&d, MAX_SEQ_NUM).next();
183        match p {
184            Err(Error::LogFileLimitExceeded) => {}
185            _ => assert!(false),
186        }
187    }
188
189    #[test]
190    fn test_ordering() {
191        let d = dir();
192        let p1 = LogPath::new(&d, 1);
193        let p2 = LogPath::new(&d, 2);
194        let p3 = LogPath::new(&d, 2);
195        assert!(p1 < p2);
196        assert!(p2 > p1);
197        assert!(p2 == p3);
198    }
199}