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!(
57 "Metadata location not under \"/metadata\" subdirectory: {}",
58 path
59 ),
60 ))?;
61
62 Ok(prefix.to_string())
63 }
64
65 fn parse_file_name(file_name: &str) -> Result<(i32, Uuid)> {
67 let (version, id) = file_name
68 .strip_suffix(".metadata.json")
69 .ok_or(Error::new(
70 ErrorKind::Unexpected,
71 format!("Invalid metadata file ending: {}", file_name),
72 ))?
73 .split_once('-')
74 .ok_or(Error::new(
75 ErrorKind::Unexpected,
76 format!("Invalid metadata file name format: {}", file_name),
77 ))?;
78
79 Ok((version.parse::<i32>()?, Uuid::parse_str(id)?))
80 }
81}
82
83impl Display for MetadataLocation {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 write!(
86 f,
87 "{}/metadata/{:0>5}-{}.metadata.json",
88 self.table_location, self.version, self.id
89 )
90 }
91}
92
93impl FromStr for MetadataLocation {
94 type Err = Error;
95
96 fn from_str(s: &str) -> Result<Self> {
97 let (path, file_name) = s.rsplit_once('/').ok_or(Error::new(
98 ErrorKind::Unexpected,
99 format!("Invalid metadata location: {}", s),
100 ))?;
101
102 let prefix = Self::parse_metadata_path_prefix(path)?;
103 let (version, id) = Self::parse_file_name(file_name)?;
104
105 Ok(MetadataLocation {
106 table_location: prefix,
107 version,
108 id,
109 })
110 }
111}
112
113#[cfg(test)]
114mod test {
115 use std::str::FromStr;
116
117 use uuid::Uuid;
118
119 use crate::MetadataLocation;
120
121 #[test]
122 fn test_metadata_location_from_string() {
123 let test_cases = vec![
124 (
126 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
127 Ok(MetadataLocation {
128 table_location: "".to_string(),
129 version: 1234567,
130 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
131 }),
132 ),
133 (
135 "/abc/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
136 Ok(MetadataLocation {
137 table_location: "/abc".to_string(),
138 version: 1234567,
139 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
140 }),
141 ),
142 (
144 "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
145 Ok(MetadataLocation {
146 table_location: "/abc/def".to_string(),
147 version: 1234567,
148 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
149 }),
150 ),
151 (
153 "https://127.0.0.1/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
154 Ok(MetadataLocation {
155 table_location: "https://127.0.0.1".to_string(),
156 version: 1234567,
157 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
158 }),
159 ),
160 (
162 "/abc/metadata/1234567-81056704-ce5b-41c4-bb83-eb6408081af6.metadata.json",
163 Ok(MetadataLocation {
164 table_location: "/abc".to_string(),
165 version: 1234567,
166 id: Uuid::from_str("81056704-ce5b-41c4-bb83-eb6408081af6").unwrap(),
167 }),
168 ),
169 (
171 "/abc/metadata/00000-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
172 Ok(MetadataLocation {
173 table_location: "/abc".to_string(),
174 version: 0,
175 id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
176 }),
177 ),
178 (
180 "/metadata/-123-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
181 Err("".to_string()),
182 ),
183 (
185 "/metadata/1234567-no-valid-id.metadata.json",
186 Err("".to_string()),
187 ),
188 (
190 "/metadata/noversion-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
191 Err("".to_string()),
192 ),
193 (
195 "/wrongsubdir/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
196 Err("".to_string()),
197 ),
198 (
200 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata",
201 Err("".to_string()),
202 ),
203 (
204 "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.wrong.file",
205 Err("".to_string()),
206 ),
207 ];
208
209 for (input, expected) in test_cases {
210 match MetadataLocation::from_str(input) {
211 Ok(metadata_location) => {
212 assert!(expected.is_ok());
213 assert_eq!(metadata_location, expected.unwrap());
214 }
215 Err(_) => assert!(expected.is_err()),
216 }
217 }
218 }
219
220 #[test]
221 fn test_metadata_location_with_next_version() {
222 let test_cases = vec![
223 MetadataLocation::new_with_table_location("/abc"),
224 MetadataLocation::from_str(
225 "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
226 )
227 .unwrap(),
228 ];
229
230 for input in test_cases {
231 let next = MetadataLocation::from_str(&input.to_string())
232 .unwrap()
233 .with_next_version();
234 assert_eq!(next.table_location, input.table_location);
235 assert_eq!(next.version, input.version + 1);
236 assert_ne!(next.id, input.id);
237 }
238 }
239}