use super::Job;
use super::JobList;
use std::fmt::Display;
use std::fmt::Formatter;
use std::num::NonZeroUsize;
use thiserror::Error;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JobId<'a> {
CurrentJob,
PreviousJob,
JobNumber(NonZeroUsize),
NamePrefix(&'a str),
NameSubstring(&'a str),
}
impl Default for JobId<'_> {
fn default() -> Self {
JobId::CurrentJob
}
}
impl Display for JobId<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match *self {
JobId::CurrentJob => "%+".fmt(f),
JobId::PreviousJob => "%-".fmt(f),
JobId::JobNumber(number) => write!(f, "%{number}"),
JobId::NamePrefix(prefix) => write!(f, "%{prefix}"),
JobId::NameSubstring(substring) => write!(f, "%?{substring}"),
}
}
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
#[error("a job ID must start with a '%'")]
pub struct ParseError;
pub fn parse_tail(tail: &str) -> JobId<'_> {
match tail {
"" | "%" | "+" => JobId::CurrentJob,
"-" => JobId::PreviousJob,
_ => match tail.strip_prefix('?') {
Some(substring) => JobId::NameSubstring(substring),
None => match tail.parse::<NonZeroUsize>() {
Ok(number) => JobId::JobNumber(number),
Err(_) => JobId::NamePrefix(tail),
},
},
}
}
pub fn parse(job_id: &str) -> Result<JobId<'_>, ParseError> {
match job_id.strip_prefix('%') {
Some(tail) => Ok(parse_tail(tail)),
None => Err(ParseError),
}
}
impl<'a> TryFrom<&'a str> for JobId<'a> {
type Error = ParseError;
#[inline(always)]
fn try_from(s: &'a str) -> Result<JobId<'a>, ParseError> {
parse(s)
}
}
#[derive(Clone, Copy, Debug, Eq, Error, Hash, PartialEq)]
pub enum FindError {
#[error("job not found")]
NotFound,
#[error("ambiguous job")]
Ambiguous,
}
impl JobId<'_> {
pub fn find(&self, jobs: &JobList) -> Result<usize, FindError> {
fn find_one(
jobs: &JobList,
pred: &mut dyn FnMut(&(usize, &Job)) -> bool,
) -> Result<usize, FindError> {
let mut i = jobs.iter().filter(pred).map(|(index, _)| index);
let index = i.next().ok_or(FindError::NotFound)?;
match i.next() {
Some(_) => Err(FindError::Ambiguous),
None => Ok(index),
}
}
match *self {
JobId::CurrentJob => jobs.current_job().ok_or(FindError::NotFound),
JobId::PreviousJob => jobs.previous_job().ok_or(FindError::NotFound),
JobId::JobNumber(number) => {
let index = number.get() - 1;
match jobs.get(index) {
Some(_) => Ok(index),
None => Err(FindError::NotFound),
}
}
JobId::NamePrefix(prefix) => {
find_one(jobs, &mut |&(_, job)| job.name.starts_with(prefix))
}
JobId::NameSubstring(substring) => {
find_one(jobs, &mut |&(_, job)| job.name.contains(substring))
}
}
}
}
#[cfg(test)]
mod tests {
use super::super::Pid;
use super::*;
#[test]
fn job_id_display() {
assert_eq!(JobId::CurrentJob.to_string(), "%+");
assert_eq!(JobId::PreviousJob.to_string(), "%-");
assert_eq!(
JobId::JobNumber(NonZeroUsize::new(42).unwrap()).to_string(),
"%42"
);
assert_eq!(JobId::NamePrefix("foo").to_string(), "%foo");
assert_eq!(JobId::NameSubstring("bar").to_string(), "%?bar");
}
fn sample_job_list() -> JobList {
let mut list = JobList::default();
let mut job = Job::new(Pid(10));
job.name = "first job".to_string();
list.add(job);
let mut job = Job::new(Pid(11));
job.name = "job 2".to_string();
list.add(job);
let mut job = Job::new(Pid(12));
job.name = "last one".to_string();
list.add(job);
list
}
#[test]
fn find_unique_current_job() {
let list = sample_job_list();
let job_id = JobId::CurrentJob;
let current_job_index = list.current_job().unwrap();
assert_eq!(job_id.find(&list), Ok(current_job_index));
}
#[test]
fn find_unique_previous_job() {
let list = sample_job_list();
let job_id = JobId::PreviousJob;
let previous_job_index = list.previous_job().unwrap();
assert_eq!(job_id.find(&list), Ok(previous_job_index));
}
#[test]
fn find_unique_job_by_job_number() {
let list = sample_job_list();
let job_id = JobId::JobNumber(NonZeroUsize::new(1).unwrap());
assert_eq!(job_id.find(&list), Ok(0));
let job_id = JobId::JobNumber(NonZeroUsize::new(2).unwrap());
assert_eq!(job_id.find(&list), Ok(1));
let job_id = JobId::JobNumber(NonZeroUsize::new(3).unwrap());
assert_eq!(job_id.find(&list), Ok(2));
}
#[test]
fn find_unique_job_by_name_prefix() {
let list = sample_job_list();
let job_id = JobId::NamePrefix("first");
assert_eq!(job_id.find(&list), Ok(0));
let job_id = JobId::NamePrefix("job");
assert_eq!(job_id.find(&list), Ok(1));
}
#[test]
fn find_unique_job_by_name_substring() {
let list = sample_job_list();
let job_id = JobId::NameSubstring("one");
assert_eq!(job_id.find(&list), Ok(2));
}
#[test]
fn find_no_current_job() {
let list = JobList::default();
let job_id = JobId::CurrentJob;
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
}
#[test]
fn find_no_previous_job() {
let list = JobList::default();
let job_id = JobId::PreviousJob;
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
}
#[test]
fn find_no_job_for_job_number() {
let list = JobList::default();
let job_id = JobId::JobNumber(NonZeroUsize::new(1).unwrap());
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
let job_id = JobId::JobNumber(NonZeroUsize::new(2).unwrap());
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
let job_id = JobId::JobNumber(NonZeroUsize::new(3).unwrap());
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
}
#[test]
fn find_no_job_for_prefix() {
let list = JobList::default();
let job_id = JobId::NamePrefix("first");
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
let list = sample_job_list();
let job_id = JobId::NamePrefix("one");
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
}
#[test]
fn find_no_job_for_substring() {
let list = JobList::default();
let job_id = JobId::NameSubstring("foo");
assert_eq!(job_id.find(&list), Err(FindError::NotFound));
}
#[test]
fn find_ambiguous_prefix() {
let mut list = sample_job_list();
let mut job = Job::new(Pid(20));
job.name = "job 3".to_string();
list.add(job);
let job_id = JobId::NamePrefix("job");
assert_eq!(job_id.find(&list), Err(FindError::Ambiguous));
}
#[test]
fn find_ambiguous_substring() {
let list = sample_job_list();
let job_id = JobId::NameSubstring("job");
assert_eq!(job_id.find(&list), Err(FindError::Ambiguous));
}
}