crate_index/
index.rs

1//! This module contains the constituent parts of the [`Index`](crate::Index).
2//!
3//! In normal usage, it would not be required to use these underlying types.
4//! They are exposed here so that can be reused in other crates.
5
6use crate::{Metadata, Result, Url};
7use async_std::path::PathBuf;
8
9mod file;
10pub(crate) use file::IndexFile;
11
12mod config;
13pub(crate) use config::Config;
14
15mod tree;
16pub use tree::{Builder as TreeBuilder, Tree};
17
18mod git;
19
20use git::Identity;
21pub use git::Repository;
22
23/// A representation of a crates registry, backed by both a directory and a git
24/// repository on the filesystem.
25///
26/// This struct is essentially a thin wrapper around both an index [`Tree`] and
27/// a git [`Repository`].
28///
29/// It functions exactly the same way as a [`Tree`], except that all changes to
30/// the crates index are also committed to the git repository, which allows this
31/// to be synced to a remote.
32pub struct Index {
33    tree: Tree,
34    repo: Repository,
35}
36
37/// A builder for initialising a new [`Index`]
38pub struct Builder<'a> {
39    tree_builder: TreeBuilder,
40    root: PathBuf,
41    origin: Option<Url>,
42    identity: Option<Identity<'a>>,
43}
44
45impl<'a> Builder<'a> {
46    // Set the Url for the registry API.
47    ///
48    /// The API should implement the REST interface as defined in
49    /// [the Cargo book](https://doc.rust-lang.org/cargo/reference/registries.html)
50    pub fn api(mut self, api: Url) -> Self {
51        self.tree_builder = self.tree_builder.api(api);
52        self
53    }
54
55    /// Add a remote to the repository
56    pub fn origin(mut self, remote: Url) -> Self {
57        self.origin = Some(remote);
58        self
59    }
60
61    /// Add an allowed registry.
62    ///
63    /// Crates in this registry are only allowed to have dependencies which are
64    /// also in this registry, or in one of the allowed registries.
65    ///
66    /// Add multiple registries my calling this method multiple times.
67    pub fn allowed_registry(mut self, registry: Url) -> Self {
68        self.tree_builder = self.tree_builder.allowed_registry(registry);
69        self
70    }
71
72    /// Add crates.io as an allowed registry.
73    ///
74    /// You will almost always want this, so this exists as a handy shortcut.
75    pub fn allow_crates_io(mut self) -> Self {
76        self.tree_builder = self.tree_builder.allow_crates_io();
77        self
78    }
79
80    /// Optionally set the username and email for the git repository
81    pub fn identity(mut self, username: &'a str, email: &'a str) -> Self {
82        self.identity = Some(Identity { username, email });
83        self
84    }
85
86    /// Construct the [`Index`] with the given parameters.
87    ///
88    /// # Errors
89    ///
90    /// This method can fail if the root path doesn't exist, or the filesystem
91    /// cannot be written to.
92    pub async fn build(self) -> Result<Index> {
93        let tree = self.tree_builder.build().await?;
94        let repo = Repository::init(self.root)?;
95
96        if let Some(url) = self.origin {
97            repo.add_origin(&url)?;
98        }
99
100        if let Some(identity) = self.identity {
101            repo.set_username(identity.username)?;
102            repo.set_email(identity.email)?;
103        }
104
105        repo.create_initial_commit()?;
106
107        let index = Index { tree, repo };
108
109        Ok(index)
110    }
111}
112
113impl Index {
114    /// Create a new index.
115    ///
116    /// The root path, and the URL for downloading .crate files is required.
117    /// Additional options can be set using the builder API (see
118    /// [`Builder`] for options).
119    ///
120    /// # Example
121    ///
122    /// ## Basic Config
123    /// ```no_run
124    /// use crate_index::Index;
125    /// # use crate_index::{Error, Url};
126    /// # async {
127    /// let root = "/index";
128    /// let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
129    ///
130    /// let index = Index::initialise(root, download).build().await?;
131    /// # Ok::<(), Error>(())
132    /// # };
133    /// ```
134    /// ## More Options
135    ///
136    /// ```no_run
137    /// use crate_index::{Index, Url};
138    /// # use crate_index::Error;
139    /// # async {
140    /// let root = "/index";
141    /// let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
142    /// let origin = Url::parse("https://github.com/crates/index.git").unwrap();
143    ///
144    ///
145    /// let index = Index::initialise(root, download)
146    ///     .api(Url::parse("https://my-crates-server.com/").unwrap())
147    ///     .allowed_registry(Url::parse("https://my-intranet:8080/index").unwrap())
148    ///     .allow_crates_io()
149    ///     .origin(origin)
150    ///     .build()
151    ///     .await?;
152    /// # Ok::<(), Error>(())
153    /// # };
154    /// ```
155    pub fn initialise<'a>(root: impl Into<PathBuf>, download: impl Into<String>) -> Builder<'a> {
156        let root = root.into();
157        let tree_builder = Tree::initialise(&root, download);
158        let origin = None;
159        let identity = None;
160
161        Builder {
162            tree_builder,
163            root,
164            origin,
165            identity,
166        }
167    }
168
169    /// Open an existing index at the given root path.
170    ///
171    /// # Example
172    /// ```no_run
173    /// use crate_index::Index;
174    /// # use crate_index::Error;
175    /// # async {
176    /// let root = "/index";
177    ///
178    /// let index = Index::open("/index").await?;
179    /// # Ok::<(), Error>(())
180    /// # };
181    /// ```
182    pub async fn open(root: impl Into<PathBuf>) -> Result<Self> {
183        let root = root.into();
184        let tree = Tree::open(&root).await?;
185        let repo = Repository::open(&root)?;
186
187        Ok(Self { tree, repo })
188    }
189
190    /// Insert crate ['Metadata'] into the index.
191    ///
192    /// # Errors
193    ///
194    /// This method can fail if the metadata is deemed to be invalid, or if the
195    /// filesystem cannot be written to.
196    pub async fn insert(&mut self, crate_metadata: Metadata) -> Result<()> {
197        let commit_message = format!(
198            "updating crate `{}#{}`",
199            crate_metadata.name(),
200            crate_metadata.version()
201        );
202        self.tree.insert(crate_metadata).await?;
203        self.repo.add_all()?; //TODO: add just the required path
204        self.repo.commit(commit_message)?;
205        Ok(())
206    }
207
208    /// The location on the filesystem of the root of the index
209    pub fn root(&self) -> &PathBuf {
210        self.tree.root()
211    }
212
213    /// The Url for downloading .crate files
214    pub fn download(&self) -> &String {
215        self.tree.download()
216    }
217
218    /// The Url of the API
219    pub fn api(&self) -> &Option<Url> {
220        self.tree.api()
221    }
222
223    /// The list of registries which crates in this index are allowed to have
224    /// dependencies on
225    pub fn allowed_registries(&self) -> &Vec<Url> {
226        self.tree.allowed_registries()
227    }
228
229    /// Split this [`Index`] into its constituent parts
230    pub fn into_parts(self) -> (Tree, Repository) {
231        (self.tree, self.repo)
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::Index;
238    use crate::{index::Metadata, Url};
239    use async_std::path::PathBuf;
240    use semver::Version;
241    use test_case::test_case;
242
243    #[async_std::test]
244    async fn get_and_set() {
245        let temp_dir = tempfile::tempdir().unwrap();
246        let root: PathBuf = temp_dir.path().into();
247        let origin = Url::parse("https://my-git-server.com/").unwrap();
248
249        let api = Url::parse("https://my-crates-server.com/").unwrap();
250
251        let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
252
253        let index = Index::initialise(root.clone(), download)
254            .origin(origin)
255            .api(api.clone())
256            .allowed_registry(Url::parse("https://my-intranet:8080/index").unwrap())
257            .allow_crates_io()
258            .identity("dummy username", "dummy@email.com")
259            .build()
260            .await
261            .unwrap();
262
263        let expected_allowed_registries = vec![
264            Url::parse("https://my-intranet:8080/index").unwrap(),
265            Url::parse("https://github.com/rust-lang/crates.io-index").unwrap(),
266        ];
267
268        assert_eq!(index.root().as_path(), &root);
269        assert_eq!(index.download(), download);
270        assert_eq!(index.api(), &Some(api));
271        assert_eq!(index.allowed_registries(), &expected_allowed_registries);
272    }
273
274    #[test_case("Some-Name", "0.1.1" ; "when used properly")]
275    #[test_case("Some_Name", "0.1.1" => panics "invalid" ; "when crate names differ only by hypens and underscores")]
276    #[test_case("some_name", "0.1.1" => panics "invalid" ; "when crate names differ only by capitalisation")]
277    #[test_case("other-name", "0.1.1" ; "when inserting a different crate")]
278    #[test_case("Some-Name", "0.1.0" => panics "invalid"; "when version is the same")]
279    #[test_case("Some-Name", "0.0.1" => panics "invalid"; "when version is lower")]
280    #[test_case("nul", "0.0.1" => panics "invalid"; "when name is reserved word")]
281    #[test_case("-start-with-hyphen", "0.0.1" => panics "invalid"; "when name starts with non-alphabetical character")]
282    fn insert(name: &str, version: &str) {
283        async_std::task::block_on(async move {
284            // create temporary directory
285            let temp_dir = tempfile::tempdir().unwrap();
286            let root = temp_dir.path();
287            let download = "https://my-crates-server.com/api/v1/crates/{crate}/{version}/download";
288            let origin = Url::parse("https://my-git-server.com/").unwrap();
289
290            let initial_metadata = metadata("Some-Name", "0.1.0");
291
292            // create index file and seed with initial metadata
293            let mut index = Index::initialise(root, download)
294                .origin(origin)
295                .identity("dummy username", "dummy@email.com")
296                .build()
297                .await
298                .expect("couldn't create index");
299
300            index
301                .insert(initial_metadata)
302                .await
303                .expect("couldn't insert initial metadata");
304
305            // create and insert new metadata
306            let new_metadata = metadata(name, version);
307            index.insert(new_metadata).await.expect("invalid");
308        });
309    }
310
311    fn metadata(name: &str, version: &str) -> Metadata {
312        Metadata::new(name, Version::parse(version).unwrap(), "checksum")
313    }
314}