langenv 0.0.2

langenv aims to be a cross-platform package manager for Linux, macOS, and Windows.
Documentation
#[cfg(test)]
mod tests {
	#[test]
	fn exploration() {
		assert_eq!(2 + 2, 4);
	}
}

pub mod langenv {
	use directories::ProjectDirs;
	use flate2::read::GzDecoder;
	// use fs_extra::dir::copy;
	use semver::Version;
	use std::collections::HashMap;
	use std::fs;
	use std::fs::File;
	use std::io;
	use std::path::{Path, PathBuf};
	use std::str::FromStr;
	use tar::Archive;
	use url::Url;

	// Based on: https://doc.rust-lang.org/std/env/consts/constant.OS.html
	#[derive(serde::Deserialize, Debug, PartialEq, strum::EnumString, strum::Display)]
	pub enum Platform {
		#[strum(serialize = "linux")]
		#[serde(rename = "linux")]
		Linux,
		#[strum(serialize = "windows")]
		#[serde(rename = "windows")]
		Windows,
		#[strum(serialize = "macos")]
		#[serde(rename = "macos")]
		MacOs,
	}

	// Based on: https://doc.rust-lang.org/std/env/consts/constant.ARCH.html
	#[derive(serde::Deserialize, Debug, PartialEq, strum::EnumString, strum::Display)]
	pub enum CpuArchitecture {
		#[strum(serialize = "x86_64")]
		#[serde(rename = "x86_64")]
		Amd64,
		#[strum(serialize = "aarch64")]
		#[serde(rename = "aarch64")]
		Arm64,
	}

	#[derive(serde::Deserialize, Debug, PartialEq, strum::EnumString, strum::Display)]
	pub enum PackageFormat {
		#[strum(serialize = "zip")]
		#[serde(rename = "zip")]
		Zip,
		#[strum(serialize = "tar.gz")]
		#[serde(rename = "tar.gz")]
		TarGz,
		#[strum(serialize = "bin")]
		#[serde(rename = "bin")]
		Binary,
	}

	#[derive(Debug)]
	pub struct Tool {
		pub name: String,
		pub downloads_url: String,
		pub project_url: String,
		pub repo_url: String,
		pub releases: Vec<Release>,
	}

	#[derive(Debug)]
	pub struct Repo {
		pub tools: Vec<Tool>,
	}

	impl Repo {
		pub fn parse_dir(dir_path: &Path) -> Result<Repo, std::io::Error> {
			let file_path: PathBuf = [PathBuf::from(dir_path), PathBuf::from("repo.json")]
				.iter()
				.collect();
			let contents: String =
				fs::read_to_string(&file_path).expect("Something went wrong reading the file");
			let repo_file: RepoFile = serde_json::from_str(&contents)?;

			let mut tools: Vec<Tool> = vec![];

			for tool in repo_file.tools {
				let tool_dir: PathBuf = [
					PathBuf::from(dir_path),
					PathBuf::from(&tool.name),
					PathBuf::from("releases"),
				]
				.iter()
				.collect();
				let paths = fs::read_dir(&tool_dir)?;

				let mut releases: Vec<Release> = vec![];

				for path in paths {
					let release_file_path: PathBuf = path?.path();
					releases.push(Release::parse_file(&release_file_path)?);
				}

				tools.push(Tool {
					name: tool.name,
					downloads_url: tool.downloads_url,
					project_url: tool.project_url,
					repo_url: tool.repo_url,
					releases,
				})
			}

			Ok(Repo { tools })
		}
	}

	#[derive(serde::Deserialize, Debug)]
	pub struct Release {
		pub version: Version,
		pub metadata: HashMap<String, String>,
		pub packages: Vec<Package>,
	}

	#[derive(serde::Deserialize, Debug)]
	pub struct Package {
		pub metadata: HashMap<String, String>,
		pub download_url: Url,
		pub platform: Platform,
		pub cpu_architecture: CpuArchitecture,
		pub package_format: PackageFormat,
		pub checksums: HashMap<CheckSumType, String>,
	}

	#[derive(serde::Deserialize, Eq, Debug, PartialEq, strum::EnumString, strum::Display, Hash)]
	pub enum CheckSumType {
		#[strum(serialize = "sha256")]
		#[serde(rename = "sha256")]
		Sha256,
	}

	impl Package {
		pub fn file_name(&self, tool_name: &str) -> String {
			match self.package_format {
				PackageFormat::Binary => tool_name.to_string(),
				_ => format!("{}.{}", tool_name, self.package_format),
			}
		}
	}

	impl Release {
		pub fn parse_file(file_path: &Path) -> Result<Release, std::io::Error> {
			let contents: String =
				fs::read_to_string(&file_path).expect("Something went wrong reading the file");
			let release: Release = serde_json::from_str(&contents)?;

			Ok(release)
		}
	}

	#[derive(serde::Deserialize, Debug)]
	pub struct RepoFile {
		pub tools: Vec<ToolItem>,
	}

	#[derive(serde::Deserialize, Debug)]
	pub struct ToolItem {
		pub name: String,
		pub downloads_url: String,
		pub project_url: String,
		pub repo_url: String,
	}

	#[derive(Debug)]
	pub struct Langenv {
		pub cpu_architecture: CpuArchitecture,
		pub platform: Platform,
		pub repo: Repo,
		pub project_dirs: ProjectDirs,
	}

	impl Langenv {
		pub fn new(repo_dir: &Path) -> Result<Langenv, std::io::Error> {
			let langenv = Langenv {
				cpu_architecture: CpuArchitecture::from_str(std::env::consts::ARCH).unwrap(),
				platform: Platform::from_str(std::env::consts::OS).unwrap(),
				repo: Repo::parse_dir(repo_dir)?,
				project_dirs: ProjectDirs::from("com", "Langenv", "langenv").unwrap(),
			};

			for path in &[
				langenv.project_dirs.cache_dir(),
				langenv.project_dirs.config_dir(),
				langenv.project_dirs.data_local_dir(),
			] {
				println!("Making sure directory {:?} exists", &path);
				fs::create_dir_all(&path)?;
			}
			println!();

			Ok(langenv)
		}

		pub fn install_package(
			&self,
			tool: &Tool,
			release: &Release,
			package: &Package,
		) -> Result<(), std::io::Error> {
			let archive_file_path: PathBuf = [
				self.project_dirs.cache_dir(),
				&PathBuf::from(&tool.name),
				&PathBuf::from(&release.version.to_string()),
				&PathBuf::from("downloads"),
				&PathBuf::from(&package.file_name(&tool.name)),
			]
			.iter()
			.collect();
			// Download archive to cache
			{
				println!(
					"Downloading {:?} version {} from {} to {:?}",
					&tool.name, &release.version, &package.download_url, &archive_file_path
				);

				let mut resp = reqwest::blocking::get(&package.download_url.to_string())
					.expect("request failed");

				fs::create_dir_all(archive_file_path.parent().unwrap())?;

				let mut out = File::create(&archive_file_path).expect("failed to create file");
				io::copy(&mut resp, &mut out).expect("failed to copy content");
			}

			// Verify checksums (if they exist)
			{}

			// TODO: Extract archive to cache
			{
				let extract_dir_path: PathBuf = [
					self.project_dirs.cache_dir(),
					&PathBuf::from(&tool.name),
					&PathBuf::from(&release.version.to_string()),
					&PathBuf::from("extract"),
				]
				.iter()
				.collect();

				fs::create_dir_all(&extract_dir_path)?;

				match package.package_format {
					PackageFormat::TarGz => {
						let tar_gz = File::open(&archive_file_path)?;
						let tar = GzDecoder::new(tar_gz);
						let mut archive = Archive::new(tar);
						archive.unpack(&extract_dir_path)?;
					}
					PackageFormat::Zip => {
						// TODO: Zipfiles
					}
					PackageFormat::Binary => {
						let extract_file_path: PathBuf = [
							&extract_dir_path,
							&PathBuf::from(&package.file_name(&tool.name)),
						]
						.iter()
						.collect();
						fs::copy(&archive_file_path, &extract_file_path)?;
					}
				}
			}

			// TODO: Move extracted cache to data directory
			{}

			Ok(())
		}
	}
}