use crate::constants::{CDG_BASE_URL, GOVINFO_BASE_URL};
use crate::error::Error;
use crate::legislation::{Chamber, CommitteeDocumentType, Congress, MeasureType};
use crate::parser::CitationParser;
use crate::utils::{DisplayOption, Result};
use std::fmt::Display;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq)]
pub enum Citation {
Measure {
congress: Option<Congress>,
chamber: Chamber,
measure_type: MeasureType,
number: usize,
version: Option<String>,
},
Law {
congress: Option<Congress>,
number: usize,
},
Statute {
volume: usize,
page: usize,
},
CommitteeDocument {
congress: Option<Congress>,
chamber: Chamber,
document_type: CommitteeDocumentType,
number: usize,
},
}
impl Display for Citation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self {
Self::Measure {
chamber,
congress,
measure_type,
number,
version,
} => {
let mut measure = measure_type.to_string();
if (chamber, measure_type) == (&Chamber::House, &MeasureType::Bill) {
measure.push('r');
}
write!(
f,
"{}{chamber}{measure}{number}{}",
DisplayOption(*congress),
DisplayOption(version.to_owned())
)
}
Self::Law { congress, number } => {
write!(f, "{}publ{number}", DisplayOption(*congress))
}
Self::Statute { volume, page } => write!(f, "{volume}stat{page}"),
Self::CommitteeDocument {
congress,
chamber,
document_type,
number,
} => {
write!(
f,
"{}{chamber}{document_type}{number}",
DisplayOption(*congress)
)
}
}
}
}
impl Citation {
pub fn parse(input: &str) -> Result<Self> {
CitationParser::parse(input)
}
#[must_use]
pub fn to_url(&self) -> Option<String> {
match self {
Self::Measure {
congress,
chamber,
measure_type,
number,
version,
} => {
if let Some(congress) = congress {
let mut measure_type = measure_type.to_string();
if *chamber == Chamber::House {
measure_type.push('r');
}
let mut url =
format!("{CDG_BASE_URL}/bill/{congress}/{chamber}{measure_type}/{number}");
if let Some(ver) = version {
url.push_str("/text/");
url.push_str(ver);
}
Some(url)
} else {
None
}
}
Self::CommitteeDocument {
congress,
chamber,
document_type,
number,
} => {
if let Some(congress) = congress {
let collection = match document_type {
CommitteeDocumentType::Report => "congressional-report",
CommitteeDocumentType::Print => "committee-print",
};
let url = format!(
"{CDG_BASE_URL}/{collection}/{congress}/{chamber}{document_type}/{number}"
);
Some(url)
} else {
None
}
}
Self::Statute { volume, page } => Some(format!(
"{GOVINFO_BASE_URL}/app/details/STATUTE-{volume}/STATUTE-{volume}-Pg{page}"
)),
Self::Law { congress, number } => {
if let Some(Congress(congress)) = congress {
Some(format!(
"{CDG_BASE_URL}/{congress}/plaws/publ{number}/PLAW-{congress}publ{number}.pdf"
))
} else {
None
}
}
}
}
pub fn version(&self) -> Option<String> {
match self {
Self::Measure { version, .. } => version.as_ref().map(String::from),
_ => None,
}
}
#[must_use]
pub fn without_version(&self) -> Self {
match self {
Self::Measure {
congress,
chamber,
measure_type,
number,
..
} => Self::Measure {
congress: *congress,
chamber: chamber.clone(),
measure_type: measure_type.clone(),
number: *number,
version: None,
},
_ => self.clone(),
}
}
}
impl FromStr for Citation {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Self::parse(s)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_no_ver_house_bill() {
let input = "118hr8070";
let expected = Citation::Measure {
congress: Some(Congress(118)),
chamber: Chamber::House,
measure_type: MeasureType::Bill,
number: 8070,
version: None,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_parse_house_report() {
let input = "118hrpt529";
let expected = Citation::CommitteeDocument {
congress: Some(Congress(118)),
chamber: Chamber::House,
document_type: CommitteeDocumentType::Report,
number: 529,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_parse_house_print() {
let input = "119hprt58246";
let expected = Citation::CommitteeDocument {
congress: Some(Congress(119)),
chamber: Chamber::House,
document_type: CommitteeDocumentType::Print,
number: 58246,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_parse_senate_print() {
let input = "119sprt57246";
let expected = Citation::CommitteeDocument {
congress: Some(Congress(119)),
chamber: Chamber::Senate,
document_type: CommitteeDocumentType::Print,
number: 57246,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_parse_senate_report() {
let input = "118srpt17";
let expected = Citation::CommitteeDocument {
congress: Some(Congress(118)),
chamber: Chamber::Senate,
document_type: CommitteeDocumentType::Report,
number: 17,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_house_bill_to_url() {
let input = "118hr529";
let expected = Some("https://www.congress.gov/bill/118/hr/529".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_house_bill_with_ver_to_url() {
let input = "118hr529ih";
let expected = Some("https://www.congress.gov/bill/118/hr/529/text/ih".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_house_report_to_url() {
let input = "118hrpt529";
let expected =
Some("https://www.congress.gov/congressional-report/118/hrpt/529".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_house_print_to_url() {
let input = "119hprt58246";
let expected = Some("https://www.congress.gov/committee-print/119/hprt/58246".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_senate_print_to_url() {
let input = "119sprt57246";
let expected = Some("https://www.congress.gov/committee-print/119/sprt/57246".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_get_version() {
let input = "118hr529ih";
let expected = Some("ih");
let citation = input.parse::<Citation>().unwrap();
let result = citation.version();
assert_eq!(expected, result.as_deref());
}
#[test]
fn test_no_congress_to_url_returns_none() {
let input = "hr529";
let expected = None;
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_uppercase_input() {
let input = "118HR529IH";
let expected = Some("https://www.congress.gov/bill/118/hr/529/text/ih".to_string());
let citation = input.parse::<Citation>().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_parse_house_bill_with_dots() {
let input = "118h.r.529";
let expected = Citation::Measure {
congress: Some(Congress(118)),
chamber: Chamber::House,
measure_type: MeasureType::Bill,
number: 529,
version: None,
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_parse_house_bill_with_version_no_congress() {
let input = "hr529ih";
let expected = Citation::Measure {
congress: None,
chamber: Chamber::House,
measure_type: MeasureType::Bill,
number: 529,
version: Some("ih".to_string()),
};
let result = input.parse();
assert_eq!(expected, result.unwrap());
}
#[test]
fn test_print_citation_without_version() {
let input = "118hr529ih";
let citation: Citation = input.parse().unwrap();
let expected = "118hr529";
let result = citation.without_version().to_string();
assert_eq!(expected, result);
}
#[test]
fn test_print_no_congress_citation_without_version() {
let input = "H.R.529.IH";
let citation: Citation = input.parse().unwrap();
let expected = "hr529".to_string();
let result = citation.without_version().to_string();
assert_eq!(expected, result);
}
#[test]
fn test_public_law_to_string() {
let input = "Public Law No: 119-68";
let citation: Citation = input.parse().unwrap();
let expected = "119publ68";
let result = citation.to_string();
assert_eq!(expected, result);
}
#[test]
fn test_statute_to_string() {
let input = "86Stat1326";
let citation: Citation = input.parse().unwrap();
let expected = "86stat1326";
let result = citation.to_string();
assert_eq!(expected, result);
}
#[test]
fn test_house_report_to_string() {
let input = "119.H.Rpt.23";
let citation: Citation = input.parse().unwrap();
let expected = "119hrpt23";
let result = citation.to_string();
assert_eq!(expected, result);
}
#[test]
fn test_normalize_senate_print() {
let input = "119.S.Prt.23";
let citation: Citation = input.parse().unwrap();
let expected = "119sprt23";
let result = citation.to_string();
assert_eq!(expected, result);
}
#[test]
fn test_statute_to_url() {
let input = "135stat2454";
let citation: Citation = input.parse().unwrap();
let expected =
Some("https://www.govinfo.gov/app/details/STATUTE-135/STATUTE-135-Pg2454".to_string());
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_law_to_url() {
let input = "119pl68";
let citation: Citation = input.parse().unwrap();
let expected =
Some("https://www.congress.gov/119/plaws/publ68/PLAW-119publ68.pdf".to_string());
let result = citation.to_url();
assert_eq!(expected, result);
let input = "119publ68";
let citation: Citation = input.parse().unwrap();
let result = citation.to_url();
assert_eq!(expected, result);
}
#[test]
fn test_citations_with_spaces() {
let input = "135 Stat. 2454";
let citation: Citation = input.parse().unwrap();
let expected = "135stat2454";
let result = citation.to_string();
assert_eq!(expected, result);
}
}