use super::Metadata;
use crate::{validate, Error, Result};
use async_std::{
fs::{File, OpenOptions},
io::{
prelude::{BufReadExt, WriteExt},
BufReader,
},
path::{Path, PathBuf},
stream::StreamExt,
};
use itertools::Itertools;
use semver::Version;
use std::{collections::BTreeMap, fmt, io};
#[derive(Debug)]
pub struct IndexFile {
crate_name: String,
file: File,
entries: BTreeMap<Version, Metadata>,
}
impl IndexFile {
pub async fn open(root: impl AsRef<Path>, crate_name: impl Into<String>) -> Result<Self> {
let crate_name = crate_name.into();
let path = root.as_ref().join(get_path(&crate_name));
create_parents(&path).await?;
let file = open_file(&path).await?;
let mut lines = BufReader::new(&file).lines();
let mut entries = BTreeMap::new();
while let Some(line) = lines.next().await {
let line = line?;
println!("{}", &line);
let metadata: Metadata = serde_json::from_str(&line).expect("JSON encoding error");
entries.insert(metadata.version().clone(), metadata);
}
Ok(Self {
crate_name,
file,
entries,
})
}
pub async fn insert(&mut self, metadata: Metadata) -> Result<()> {
self.validate(&metadata)?;
self.entries.insert(metadata.version().clone(), metadata);
self.save().await?;
Ok(())
}
fn get_mut(&mut self, version: &Version) -> Option<&mut Metadata> {
self.entries.get_mut(version)
}
pub async fn yank(&mut self, version: &Version) -> Result<()> {
self.get_mut(version).ok_or(Error::NotFound)?.yank();
self.save().await?;
Ok(())
}
pub async fn unyank(&mut self, version: &Version) -> Result<()> {
self.get_mut(version).ok_or(Error::NotFound)?.unyank();
self.save().await?;
Ok(())
}
pub fn latest_version(&self) -> Option<(&Version, &Metadata)> {
self.entries.iter().next_back()
}
fn validate(&self, metadata: &Metadata) -> std::result::Result<(), validate::Error> {
self.validate_name(metadata.name())?;
self.validate_version(metadata.version())?;
Ok(())
}
fn validate_name(&self, given: impl AsRef<str>) -> std::result::Result<(), validate::Error> {
validate::name(given.as_ref())?;
if self.crate_name == given.as_ref() {
Ok(())
} else {
Err(validate::Error::name_mismatch(
self.crate_name.clone(),
given.as_ref().to_string(),
))
}
}
fn validate_version(&self, version: &Version) -> std::result::Result<(), validate::Error> {
match self.greatest_minor_version(version.major) {
Some(current) => {
if current.0 < version {
Ok(())
} else {
Err(validate::Error::version(current.0, version.clone()))
}
}
None => Ok(()),
}
}
fn greatest_minor_version(&self, major_version: u64) -> Option<(&Version, &Metadata)> {
let min = Version::new(major_version, 0, 0);
let max = Version::new(major_version + 1, 0, 0);
self.entries.range(min..max).next_back()
}
async fn save(&mut self) -> io::Result<()> {
self.file.write_all(self.to_string().as_bytes()).await
}
}
impl fmt::Display for IndexFile {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
self.entries.values().map(|x| x.to_string()).join("\n")
)
}
}
async fn create_parents(path: &Path) -> io::Result<()> {
async_std::fs::DirBuilder::new()
.recursive(true)
.create(path.parent().unwrap())
.await
}
async fn open_file(path: &Path) -> io::Result<File> {
OpenOptions::new()
.write(true)
.read(true)
.create(true)
.open(path)
.await
}
fn get_path(name: impl AsRef<str>) -> PathBuf {
let name = name.as_ref();
let canonical_name = name.to_ascii_lowercase().replace('_', "-");
let mut path = PathBuf::new();
match name.len() {
1 => {
path.push("1");
path.push(name);
path
}
2 => {
path.push("2");
path.push(name);
path
}
3 => {
path.push("3");
path.push(&canonical_name[0..1]);
path.push(name);
path
}
_ => {
path.push(&canonical_name[0..2]);
path.push(&canonical_name[2..4]);
path.push(name);
path
}
}
}
impl<'a> IntoIterator for &'a IndexFile {
type Item = &'a Metadata;
type IntoIter = std::collections::btree_map::Values<'a, Version, Metadata>;
fn into_iter(self) -> Self::IntoIter {
self.entries.values()
}
}
impl IntoIterator for IndexFile {
type Item = (Version, Metadata);
type IntoIter = std::collections::btree_map::IntoIter<Version, Metadata>;
fn into_iter(self) -> Self::IntoIter {
self.entries.into_iter()
}
}
#[cfg(test)]
mod tests {
use super::IndexFile;
use crate::{Error, Metadata};
use semver::Version;
use test_case::test_case;
#[async_std::test]
async fn open() {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path();
IndexFile::open(root, "other-name").await.unwrap();
}
#[test_case("Some-Name", "2.1.1" ; "when used properly")]
#[test_case("Some_Name", "2.1.1" => panics "invalid" ; "when crate names differ only by hypens and underscores")]
#[test_case("some_name", "2.1.1" => panics "invalid" ; "when crate names differ only by capitalisation")]
#[test_case("other-name", "2.1.1" => panics "invalid" ; "when inserting a different crate")]
#[test_case("Some-Name", "2.1.0" => panics "invalid"; "when version is the same")]
#[test_case("Some-Name", "2.0.0" => panics "invalid"; "when version is lower and major version is the same")]
#[test_case("Some-Name", "1.0.0" ; "when version is lower but major version is different")]
#[test_case("nul", "2.1.1" => panics "invalid"; "when name is reserved word")]
#[test_case("-start-with-hyphen", "2.1.1" => panics "invalid"; "when name starts with non-alphabetical character")]
fn insert(name: &str, version: &str) {
async_std::task::block_on(async move {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path();
let initial_metadata = Metadata::new("Some-Name", Version::new(2, 1, 0), "checksum");
let mut index_file = IndexFile::open(root, initial_metadata.name())
.await
.unwrap();
index_file.insert(initial_metadata).await.unwrap();
let new_metadata = Metadata::new(name, Version::parse(version).unwrap(), "checksum");
index_file.insert(new_metadata).await.expect("invalid");
});
}
#[async_std::test]
async fn latest() {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path();
let mut index_file = IndexFile::open(root, "some-name").await.unwrap();
index_file
.insert(Metadata::new(
"some-name",
Version::new(0, 1, 0),
"checksum",
))
.await
.unwrap();
index_file
.insert(Metadata::new(
"some-name",
Version::new(0, 1, 1),
"checksum",
))
.await
.unwrap();
index_file
.insert(Metadata::new(
"some-name",
Version::new(0, 2, 0),
"checksum",
))
.await
.unwrap();
assert_eq!(
index_file.latest_version().unwrap().0,
&Version::new(0, 2, 0)
);
}
#[test_case("x" => "1/x" ; "one-letter crate name")]
#[test_case("xx" => "2/xx" ; "two-letter crate name")]
#[test_case("xxx" =>"3/x/xxx" ; "three-letter crate name")]
#[test_case("abcd" => "ab/cd/abcd" ; "four-letter crate name")]
#[test_case("abcde" => "ab/cd/abcde" ; "five-letter crate name")]
#[test_case("aBcD" => "ab/cd/aBcD" ; "mixed-case crate name")]
fn get_path(name: &str) -> String {
super::super::get_path(name).to_str().unwrap().to_string()
}
fn metadata(version: &str) -> Metadata {
Metadata::new("Some-Name", Version::parse(version).unwrap(), "checksum")
}
#[test_case("0.1.0"; "when version exists")]
#[test_case("0.2.0" => panics "version doesn't exist"; "when version doesnt exist")]
fn yank(version: &str) {
let version = Version::parse(version).unwrap();
async_std::task::block_on(async {
let temp_dir = tempfile::tempdir().unwrap();
let root = temp_dir.path();
let initial_metadata = metadata("0.1.0");
let mut index_file = IndexFile::open(root.clone(), initial_metadata.name())
.await
.expect("couldn't open index file");
index_file
.insert(initial_metadata)
.await
.expect("couldn't insert initial metadata");
match index_file.yank(&version).await {
Ok(()) => (),
Err(Error::NotFound) => panic!("version doesn't exist"),
_ => panic!("something else went wrong"),
}
})
}
}