use std::fmt;
use url::{ParseError, Url};
#[macro_use]
extern crate derive_builder;
#[derive(Clone, Debug)]
pub enum SortBy {
Relevance,
Date,
}
impl fmt::Display for SortBy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use SortBy::*;
write!(
f,
"{}",
match self {
Relevance => "relevance",
Date => "date",
},
)
}
}
#[derive(Clone, Debug)]
pub enum ShowFrom {
All,
JobSite,
Employer,
}
impl fmt::Display for ShowFrom {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ShowFrom::*;
write!(
f,
"{}",
match self {
All => "",
JobSite => "jobsite",
Employer => "employer",
},
)
}
}
#[derive(Clone, Debug)]
pub enum JobType {
AllJobTypes,
FullTime,
Contract,
PartTime,
Temporary,
Internship,
Commission,
}
impl fmt::Display for JobType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use JobType::*;
write!(
f,
"{}",
match self {
AllJobTypes => "",
FullTime => "fulltime",
Contract => "contract",
PartTime => "parttime",
Temporary => "temporary",
Internship => "internship",
Commission => "commission",
},
)
}
}
#[derive(Clone, Debug)]
pub enum ExperienceLevel {
AllLevels,
EntryLevel,
MidLevel,
SeniorLevel,
}
impl fmt::Display for ExperienceLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ExperienceLevel::*;
write!(
f,
"{}",
match self {
AllLevels => "",
EntryLevel => "entry_level",
MidLevel => "mid_level",
SeniorLevel => "senior_level",
},
)
}
}
#[derive(Clone, Debug)]
pub enum ExcludeStaffingAgencies {
True,
False,
}
impl fmt::Display for ExcludeStaffingAgencies {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ExcludeStaffingAgencies::*;
write!(
f,
"{}",
match self {
True => "directhire",
False => "",
},
)
}
}
#[derive(Clone, Debug)]
pub enum City {
CityState(String, String),
ZipCode(String),
}
impl fmt::Display for City {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use City::*;
write!(
f,
"{}",
match self {
CityState(city, state) => format!("{}, {}", city, state).to_string(),
ZipCode(zip) => zip.to_string(),
},
)
}
}
#[allow(dead_code)]
#[derive(Builder, Debug, Clone)]
#[builder(setter(into))]
pub struct IndeedQuery {
#[builder(default = "City::ZipCode("")")]
pub city: City,
#[builder(default = "0")]
pub radius: u32,
#[builder(default = "14")]
pub max_age: u32,
#[builder(default = "0")]
pub min_salary: u32,
#[builder(default = "ExperienceLevel::AllLevels")]
pub experience_level: ExperienceLevel,
#[builder(default = "JobType::AllJobTypes")]
pub job_type: JobType,
#[builder(default = "Vec::new()")]
pub contains_all_words: Vec<String>,
#[builder(default = "String::new()")]
pub has_exact_phrase: String,
#[builder(default = "Vec::new()")]
pub has_any_words: Vec<String>,
#[builder(default = "Vec::new()")]
pub excludes_all_words: Vec<String>,
#[builder(default = "Vec::new()")]
pub has_words_in_title: Vec<String>,
#[builder(default = "String::new()")]
pub company: String,
#[builder(default = "ExcludeStaffingAgencies::False")]
pub exclude_staffing_agencies: ExcludeStaffingAgencies,
#[builder(default = "ShowFrom::All")]
pub show_from: ShowFrom,
#[builder(default = "SortBy::Relevance")]
pub sort_by: SortBy,
#[builder(default = "50")]
pub limit: u32,
#[builder(default = "0")]
pub start: u32,
}
impl IndeedQuery {
pub fn build_url(&self) -> Result<String, ParseError> {
let url = Url::parse_with_params(
"https://www.indeed.com/jobs",
&[
("as_and", self.contains_all_words.join(" ")),
("as_phr", self.has_exact_phrase.to_string()),
("as_any", self.has_any_words.join(" ")),
("as_not", self.excludes_all_words.join(" ")),
("as_ttl", self.has_words_in_title.join(" ")),
("as_cmp", self.company.to_string()),
("jt", self.job_type.to_string()),
("st", self.show_from.to_string()),
("explvl", self.experience_level.to_string()),
("sr", self.exclude_staffing_agencies.to_string()),
("salary", self.min_salary.to_string()),
("radius", self.radius.to_string()),
("l", self.city.to_string()),
("fromage", self.max_age.to_string()),
("limit", self.limit.to_string()),
("sort", self.sort_by.to_string()),
("psf", "advsrch".to_string()),
("from", "advancedsearch".to_string()),
("start", self.start.to_string()),
],
)?;
Ok(url.as_ref().to_string())
}
pub fn increment_page(&mut self) {
self.start += self.limit;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_url_test() {
let query = IndeedQueryBuilder::default()
.has_words_in_title(vec!["Developer".to_string()])
.excludes_all_words(vec![
"C#".to_string(),
".NET".to_string(),
"Azure".to_string(),
"Unpaid".to_string(),
"Senior".to_string(),
])
.exclude_staffing_agencies(ExcludeStaffingAgencies::True)
.has_any_words(vec![
"Embedded".to_string(),
"Full-Stack".to_string(),
"Linux".to_string(),
"Rust".to_string(),
"Unix".to_string(),
"Web".to_string(),
])
.min_salary(85000 as u32)
.city(City::CityState(
"San Francisco".to_string(),
"CA".to_string(),
))
.radius(40 as u32)
.job_type(JobType::FullTime)
.experience_level(ExperienceLevel::EntryLevel)
.sort_by(SortBy::Date)
.max_age(14 as u32)
.build()
.unwrap();
assert_eq!(query.build_url().unwrap(), "https://www.indeed.com/jobs?as_and=&as_phr=&as_any=Embedded+Full-Stack+Linux+Rust+Unix+Web&as_not=C%23+.NET+Azure+Unpaid+Senior&as_ttl=Developer&as_cmp=&jt=fulltime&st=&explvl=entry_level&sr=directhire&salary=85000&radius=40&l=San+Francisco%2C+CA&fromage=14&limit=50&sort=date&psf=advsrch&from=advancedsearch&start=0");
}
}