diff --git a/pkgr/Cargo.toml b/pkgr/Cargo.toml index df095d2..92a0e91 100644 --- a/pkgr/Cargo.toml +++ b/pkgr/Cargo.toml @@ -6,4 +6,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -manifest = { path = "../manifest" } \ No newline at end of file +fern = "0.6.2" +getopts = "0.2.21" +log = "0.4.19" +regex = "1.9.1" +manifest = { path = "../manifest" } diff --git a/pkgr/src/commands/mod.rs b/pkgr/src/commands/mod.rs new file mode 100644 index 0000000..61e3fe8 --- /dev/null +++ b/pkgr/src/commands/mod.rs @@ -0,0 +1,8 @@ +use crate::package::identifier::PackageIdentifier; + +pub enum Command { + Grab(PackageIdentifier), // grab a package from a remote source + Remove(PackageIdentifier), // remove a package from the local source + List, // list all packages in the local source + Update, +} \ No newline at end of file diff --git a/pkgr/src/main.rs b/pkgr/src/main.rs index f79c691..8d75350 100644 --- a/pkgr/src/main.rs +++ b/pkgr/src/main.rs @@ -1,2 +1,59 @@ +use std::env; +use std::process::exit; +use std::str::FromStr; + +use getopts::Options; +use log::{info, SetLoggerError}; + +mod commands; +mod package; + fn main() { + setup_logger() + .expect("Unable to setup logger."); + + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optflag("h", "help", "Show help message."); + let matches = opts.parse(&args[1..]).unwrap_or_else(|e| panic!("{}", e.to_string())); + + if matches.opt_present("h") { + print_usage(&program, opts); + exit(1); + } + + let command = if !matches.free.is_empty() { + matches.free[0].clone() + } else { + print_usage(&program, opts); + exit(1); + }; + + info!("Identifier: {}", package::identifier::PackageIdentifier::from_str(&command).unwrap()); } + +fn print_usage(program: &str, opts: Options) { + let mut brief = format!("Usage: {} [options]\n\n", program); + brief += &*format!("Commands: grab \n"); + brief += &*format!(" remove\n"); + print!("{}", opts.usage(&brief.trim_end())); +} + +fn setup_logger() -> Result<(), SetLoggerError> { + fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {}", + match record.level().to_string().chars().nth(0).unwrap_or('I') { + 'E' | 'W' => "!!", + _ => "**", + }, + message + )) + }) + .level(log::LevelFilter::Info) + .chain(std::io::stdout()) + .apply() +} \ No newline at end of file diff --git a/pkgr/src/package/identifier.rs b/pkgr/src/package/identifier.rs new file mode 100644 index 0000000..01f9f6b --- /dev/null +++ b/pkgr/src/package/identifier.rs @@ -0,0 +1,114 @@ +use std::str::FromStr; + +use regex::Regex; + +#[derive(Debug)] +pub enum PackageIdentifierError { + InvalidPackageLocator(String), + InvalidURI(String), +} + +impl std::fmt::Display for PackageIdentifierError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + PackageIdentifierError::InvalidPackageLocator(s) => write!(f, "Invalid package locator: {}", s), + PackageIdentifierError::InvalidURI(s) => write!(f, "Invalid URI: {}", s), + } + } +} + +#[derive(Debug)] +pub enum PackageIdentifier { + PackageLocator(PackageLocator), + URI(String), +} + +impl std::fmt::Display for PackageIdentifier { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + PackageIdentifier::PackageLocator(pl) => write!(f, "{}", pl), + PackageIdentifier::URI(uri) => write!(f, "{}", uri), + } + } +} + +#[derive(Debug)] +pub struct PackageLocator { + pub name: String, + pub version: Option, + pub tags: Option>, +} + +impl std::fmt::Display for PackageLocator { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut s = self.name.clone(); + if let Some(version) = self.version { + s += &*format!("@{}", version); + } + if let Some(tags) = &self.tags { + s += &*format!(":{}", tags.join(",")); + } + write!(f, "{}", s) + } +} + +impl FromStr for PackageIdentifier { + type Err = PackageIdentifierError; + + fn from_str(s: &str) -> Result { + let uri_re = regex::Regex::new(r"^[a-zA-Z0-9]+://").unwrap(); + if uri_re.is_match(s) { + // there needs to be stuff after the protocol + let split = s.split("://").collect::>(); + if split.len() != 2 || split[1].is_empty() { + return Err(PackageIdentifierError::InvalidURI(s.to_string())); + } + Ok(PackageIdentifier::URI(s.to_string())) + } else { + let pl = match PackageLocator::from_str(s) { + Ok(pl) => pl, + Err(e) => return Err(PackageIdentifierError::InvalidPackageLocator(e.to_string())), + }; + Ok(PackageIdentifier::PackageLocator(pl)) + } + } +} + +impl FromStr for PackageLocator { + type Err = PackageIdentifierError; + + fn from_str(s: &str) -> Result { + #[allow(unused_assignments)] // false positive + let mut name = None; + let mut version = None; + let mut tags = None; + + if s.is_empty() { + return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string())); + } + + let name_re = Regex::new("^([A-Za-z][A-Za-z0-9_.]+)").unwrap(); + let version_re = Regex::new("@([0-9]+)").unwrap(); + let tags_re = Regex::new(":([a-zA-Z0-9,._]+)").unwrap(); + + if let Some(caps) = name_re.captures(s) { + name = Some(caps.get(1).unwrap().as_str().to_string()); + } else { + return Err(PackageIdentifierError::InvalidPackageLocator(s.to_string())); + } + + if let Some(caps) = version_re.captures(s) { + version = Some(caps.get(1).unwrap().as_str().parse::().unwrap()); + } + + if let Some(caps) = tags_re.captures(s) { + tags = Some(caps.get(1).unwrap().as_str().split(",").map(|s| s.to_string()).collect()); + } + + Ok(PackageLocator { + name: name.unwrap(), + version, + tags, + }) + } +} \ No newline at end of file diff --git a/pkgr/src/package/mod.rs b/pkgr/src/package/mod.rs new file mode 100644 index 0000000..8f83cb4 --- /dev/null +++ b/pkgr/src/package/mod.rs @@ -0,0 +1 @@ +pub mod identifier; \ No newline at end of file