1use std::fmt::Display;
19use std::str::FromStr;
20
21use uuid::Uuid;
22
23use crate::{Error, ErrorKind, Result};
24
25#[derive(Clone, Debug, PartialEq)]
27pub struct MetadataLocation {
28 table_location: String,
29 version: i32,
30 id: Uuid,
31}
32
33impl MetadataLocation {
34 pub fn new_with_table_location(table_location: impl ToString) -> Self {
37 Self {
38 table_location: table_location.to_string(),
39 version: 0,
40 id: Uuid::new_v4(),
41 }
42 }
43
44 pub fn with_next_version(&self) -> Self {
46 Self {
47 table_location: self.table_location.clone(),
48 version: self.version + 1,
49 id: Uuid::new_v4(),
50 }
51 }
52
53 fn parse_metadata_path_prefix(path: &str) -> Result<String> {
54 let prefix = path.strip_suffix("/metadata").ok_or(Error::new(
55 ErrorKind::Unexpected,
56 format!("Metadata location not under \"/metadata\" subdirectory: {path}"),
57 ))?;
58
59 Ok(prefix.to_string())
60 }
61
62 fn parse_file_name(file_name: &str) -> Result<(i32, Uuid)> {
64 let (version, id) = file_name
65 .strip_suffix(".metadata.json")
66 .ok_or(Error::new(
67 ErrorKind::Unexpected,
68 format!("Invalid metadata file ending: {file_name}"),
69 ))?
70 .split_once('-')
71 .ok_or(Error::new(
72 ErrorKind::Unexpected,
73 format!("Invalid metadata file name format: {file_name}"),
74 ))?;
75
76 Ok((version.parse::<i32>()?, Uuid::parse_str(id)?))
77 }
78}
79
80impl Display for MetadataLocation {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(
83 f,
84 "{}/metadata/{:0>5}-{}.metadata.json",
85 self.table_location, self.version, self.id
86 )
87 }
88}
89
90impl FromStr for MetadataLocation {
91 type Err = Error;
92
93 fn from_str(s: &str) -> Result<Self> {
94 let (path, file_name) = s.rsplit_once('/').ok_or(Error::new(
95 ErrorKind::Unexpected,
96 format!("Invalid metadata location: {s}"),
97 ))?;
98
99 let prefix = Self::parse_metadata_path_prefix(path)?;
100 let (version, id) = Self::parse_file_name(file_name)?;
101
102 Ok(MetadataLocation {
103 table_location: prefix,
104 version,
105 id,
106 })
107 }
108}
109
110#[cfg(test)]
111mod test {
112 use std::str::FromStr;
113
114 use uuid::Uuid;
115
116 use crate::MetadataLocation;
117
118 #[test]
119 fn test_metadata_location_from_string() {
120 let test_cases = vec![
121 (
123 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
124 Ok(MetadataLocation {
125 table_location: "".to_string(),
126 version: 1234567,
127 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
128 }),
129 ),
130 (
132 "/abc/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
133 Ok(MetadataLocation {
134 table_location: "/abc".to_string(),
135 version: 1234567,
136 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
137 }),
138 ),
139 (
141 "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
142 Ok(MetadataLocation {
143 table_location: "/abc/def".to_string(),
144 version: 1234567,
145 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
146 }),
147 ),
148 (
150 "https://127.0.0.1/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
151 Ok(MetadataLocation {
152 table_location: "https://127.0.0.1".to_string(),
153 version: 1234567,
154 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
155 }),
156 ),
157 (
159 "/abc/metadata/1234567-81056704-ce5b-41c4-bb83-eb6408081af6.metadata.json",
160 Ok(MetadataLocation {
161 table_location: "/abc".to_string(),
162 version: 1234567,
163 id: Uuid::from_str("81056704-ce5b-41c4-bb83-eb6408081af6").unwrap(),
164 }),
165 ),
166 (
168 "/abc/metadata/00000-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
169 Ok(MetadataLocation {
170 table_location: "/abc".to_string(),
171 version: 0,
172 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
173 }),
174 ),
175 (
177 "/metadata/-123-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
178 Err("".to_string()),
179 ),
180 (
182 "/metadata/1234567-no-valid-id.metadata.json",
183 Err("".to_string()),
184 ),
185 (
187 "/metadata/noversion-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
188 Err("".to_string()),
189 ),
190 (
192 "/wrongsubdir/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
193 Err("".to_string()),
194 ),
195 (
197 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata",
198 Err("".to_string()),
199 ),
200 (
201 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.wrong.file",
202 Err("".to_string()),
203 ),
204 ];
205
206 for (input, expected) in test_cases {
207 match MetadataLocation::from_str(input) {
208 Ok(metadata_location) => {
209 assert!(expected.is_ok());
210 assert_eq!(metadata_location, expected.unwrap());
211 }
212 Err(_) => assert!(expected.is_err()),
213 }
214 }
215 }
216
217 #[test]
218 fn test_metadata_location_with_next_version() {
219 let test_cases = vec![
220 MetadataLocation::new_with_table_location("/abc"),
221 MetadataLocation::from_str(
222 "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
223 )
224 .unwrap(),
225 ];
226
227 for input in test_cases {
228 let next = MetadataLocation::from_str(&input.to_string())
229 .unwrap()
230 .with_next_version();
231 assert_eq!(next.table_location, input.table_location);
232 assert_eq!(next.version, input.version + 1);
233 assert_ne!(next.id, input.id);
234 }
235 }
236}