ml_cellar/
ml_bin.rs

1use std::env;
2use std::path::PathBuf;
3
4/// ML-bin is a specific release of a model stored under a rack (e.g., `vit-l/1.1/`).
5/// ML-bin has artifacts like checkpoints, configs, logs.
6/// It corresponds to a “vintage” in the wine analogy.
7/// ML-bin is expressed as `{cellar_directory}/{rack_directory}/{project}/{version}/`.
8pub struct MLBin {
9    /// The absolute path to the root of the ML-cellar storage.
10    pub cellar_directory: PathBuf,
11    /// The name (relative directory path) of the rack.
12    pub rack_name: PathBuf,
13    /// The name (relative directory path) of the project.
14    pub project_name: PathBuf,
15    /// The version of the project.
16    pub version: String,
17}
18
19impl MLBin {
20    /// Creates a new MLBin instance.
21    ///
22    /// # Arguments
23    ///
24    /// - `cellar_directory` - The absolute path to the root of the ML-cellar storage
25    /// - `rack_name` - The name (relative directory path) of the rack
26    /// - `project_name` - The name (relative directory path) of the project
27    /// - `version` - The version string of the model
28    pub fn new(
29        cellar_directory: PathBuf,
30        rack_name: impl Into<PathBuf>,
31        project_name: impl Into<PathBuf>,
32        version: String,
33    ) -> Self {
34        Self {
35            cellar_directory,
36            rack_name: rack_name.into(),
37            project_name: project_name.into(),
38            version,
39        }
40    }
41
42    /// Creates a new MLBin instance from an ML-bin path.
43    ///
44    /// This method parses the ML-bin path and extracts the rack name, project name,
45    /// and version by analyzing the directory structure relative to the cellar and rack directories.
46    ///
47    /// # Arguments
48    ///
49    /// - `ml_bin_path` - The path to the ML-bin (can be relative or absolute)
50    /// - `cellar_directory` - The absolute path to the root directory of the ML-cellar
51    /// - `rack_directory` - The absolute path to the directory of the rack
52    ///
53    pub fn from_ml_bin_path(
54        ml_bin_path: PathBuf,
55        cellar_directory: PathBuf,
56        rack_directory: PathBuf,
57    ) -> Self {
58        // Convert to absolute path
59        // ml_bin_absolute_path: {cellar_directory(absolute)}/{rack_name}/{project_name}/{version}/
60        let ml_bin_absolute_path = if ml_bin_path.is_absolute() {
61            ml_bin_path
62        } else {
63            env::current_dir().unwrap().join(&ml_bin_path)
64        };
65
66        // rack_name: {rack_name}
67        let rack_name = rack_directory
68            .strip_prefix(&cellar_directory)
69            .unwrap_or(&rack_directory)
70            .to_path_buf();
71
72        // ml_bin_path_from_rack: {project_name}/{version}
73        let ml_bin_path_from_rack = ml_bin_absolute_path
74            .strip_prefix(&rack_directory)
75            .unwrap_or(&ml_bin_absolute_path)
76            .to_path_buf();
77
78        let project_name = match ml_bin_path_from_rack.parent() {
79            Some(p) => p.to_path_buf(),
80            None => PathBuf::new(),
81        };
82
83        let version = match ml_bin_path_from_rack.file_name().and_then(|n| n.to_str()) {
84            Some(s) => s.to_string(),
85            None => "".to_string(),
86        };
87
88        Self {
89            cellar_directory,
90            rack_name,
91            project_name,
92            version,
93        }
94    }
95
96    /// Returns the full absolute path to the ML-bin directory.
97    /// Constructs the path by joining: `{cellar_directory}/{rack_name}/{project_name}/{version}`
98    pub fn get_full_path(&self) -> PathBuf {
99        self.cellar_directory
100            .join(&self.rack_name)
101            .join(&self.project_name)
102            .join(&self.version)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::path::PathBuf;
110
111    #[test]
112    fn test_new() {
113        let ml_bin = MLBin::new(
114            PathBuf::from("/path/to/cellar"),
115            "vit-l",
116            "base",
117            "1.1".to_string(),
118        );
119
120        assert_eq!(ml_bin.cellar_directory, PathBuf::from("/path/to/cellar"));
121        assert_eq!(ml_bin.rack_name, PathBuf::from("vit-l"));
122        assert_eq!(ml_bin.project_name, PathBuf::from("base"));
123        assert_eq!(ml_bin.version, "1.1");
124    }
125
126    #[test]
127    fn test_new_with_pathbuf() {
128        let ml_bin = MLBin::new(
129            PathBuf::from("/path/to/cellar"),
130            PathBuf::from("llm-model"),
131            PathBuf::from("projectA"),
132            "2.0.3".to_string(),
133        );
134
135        assert_eq!(ml_bin.cellar_directory, PathBuf::from("/path/to/cellar"));
136        assert_eq!(ml_bin.rack_name, PathBuf::from("llm-model"));
137        assert_eq!(ml_bin.project_name, PathBuf::from("projectA"));
138        assert_eq!(ml_bin.version, "2.0.3");
139    }
140
141    #[test]
142    fn test_get_full_path() {
143        let ml_bin = MLBin::new(
144            PathBuf::from("/path/to/cellar"),
145            "vit-l",
146            "base",
147            "1.1".to_string(),
148        );
149
150        let full_path = ml_bin.get_full_path();
151        assert_eq!(full_path, PathBuf::from("/path/to/cellar/vit-l/base/1.1"));
152    }
153
154    #[test]
155    fn test_get_full_path_with_nested_project() {
156        let ml_bin = MLBin::new(
157            PathBuf::from("/cellar"),
158            "model",
159            "project/subproject",
160            "0.1.0".to_string(),
161        );
162
163        let full_path = ml_bin.get_full_path();
164        assert_eq!(
165            full_path,
166            PathBuf::from("/cellar/model/project/subproject/0.1.0")
167        );
168    }
169
170    #[test]
171    fn test_from_ml_bin_path_absolute() {
172        let cellar_dir = PathBuf::from("/path/to/cellar");
173        let rack_dir = PathBuf::from("/path/to/cellar/vit-l");
174        let ml_bin_path = PathBuf::from("/path/to/cellar/vit-l/base/1.1");
175
176        let ml_bin = MLBin::from_ml_bin_path(ml_bin_path, cellar_dir.clone(), rack_dir);
177
178        assert_eq!(ml_bin.cellar_directory, cellar_dir);
179        assert_eq!(ml_bin.rack_name, PathBuf::from("vit-l"));
180        assert_eq!(ml_bin.project_name, PathBuf::from("base"));
181        assert_eq!(ml_bin.version, "1.1");
182    }
183
184    #[test]
185    fn test_from_ml_bin_path_with_nested_project() {
186        let cellar_dir = PathBuf::from("/cellar");
187        let rack_dir = PathBuf::from("/cellar/model");
188        let ml_bin_path = PathBuf::from("/cellar/model/project/subproject/2.0.0");
189
190        let ml_bin = MLBin::from_ml_bin_path(ml_bin_path, cellar_dir.clone(), rack_dir);
191
192        assert_eq!(ml_bin.cellar_directory, cellar_dir);
193        assert_eq!(ml_bin.rack_name, PathBuf::from("model"));
194        assert_eq!(ml_bin.project_name, PathBuf::from("project/subproject"));
195        assert_eq!(ml_bin.version, "2.0.0");
196    }
197
198    /// Test that creating an MLBin and getting its path results in the same path
199    #[test]
200    fn test_from_ml_bin_path_roundtrip() {
201        let cellar_dir = PathBuf::from("/test/cellar");
202        let rack_dir = PathBuf::from("/test/cellar/my-rack");
203        let original_path = PathBuf::from("/test/cellar/my-rack/my-project/1.2.3");
204
205        let ml_bin =
206            MLBin::from_ml_bin_path(original_path.clone(), cellar_dir.clone(), rack_dir.clone());
207        let reconstructed_path = ml_bin.get_full_path();
208
209        assert_eq!(original_path, reconstructed_path);
210    }
211
212    /// Test edge case where ml_bin_path_from_rack has no parent
213    #[test]
214    fn test_from_ml_bin_path_version_only() {
215        let cellar_dir = PathBuf::from("/cellar");
216        let rack_dir = PathBuf::from("/cellar/rack");
217        let ml_bin_path = PathBuf::from("/cellar/rack/1.0");
218
219        let ml_bin = MLBin::from_ml_bin_path(ml_bin_path, cellar_dir.clone(), rack_dir);
220
221        assert_eq!(ml_bin.cellar_directory, cellar_dir);
222        assert_eq!(ml_bin.rack_name, PathBuf::from("rack"));
223        assert_eq!(ml_bin.project_name, PathBuf::from(""));
224        assert_eq!(ml_bin.version, "1.0");
225    }
226}