use super::Edgar;
use super::error::{EdgarError, Result};
use super::options::FilingOptions;
use super::traits::FilingOperations;
use async_trait::async_trait;
use chrono::{DateTime, FixedOffset};
use serde::Deserialize;
use serde_json;
#[derive(Debug, Clone, Deserialize)]
pub struct Submission {
pub cik: String,
#[serde(rename = "entityType")]
pub entity_type: String,
pub sic: String,
#[serde(rename = "sicDescription")]
pub sic_description: String,
#[serde(rename = "ownerOrg")]
pub owner_org: Option<String>,
#[serde(rename = "insiderTransactionForOwnerExists")]
pub insider_transaction_for_owner_exists: i32,
#[serde(rename = "insiderTransactionForIssuerExists")]
pub insider_transaction_for_issuer_exists: i32,
pub name: String,
pub tickers: Vec<String>,
pub exchanges: Vec<Option<String>>,
pub ein: Option<String>,
pub lei: Option<String>,
pub description: Option<String>,
pub website: Option<String>,
#[serde(rename = "investorWebsite")]
pub investor_website: Option<String>,
#[serde(rename = "investmentCompany")]
pub investment_company: Option<String>,
pub category: Option<String>,
#[serde(rename = "fiscalYearEnd")]
pub fiscal_year_end: Option<String>,
#[serde(rename = "stateOfIncorporation")]
pub state_of_incorporation: String,
#[serde(rename = "stateOfIncorporationDescription")]
pub state_of_incorporation_description: String,
pub addresses: Addresses,
pub phone: String,
pub flags: String,
#[serde(rename = "formerNames")]
pub former_names: Vec<FormerName>,
pub filings: FilingsData,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Addresses {
pub mailing: Address,
pub business: Address,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Address {
pub street1: String,
pub street2: Option<String>,
pub city: String,
#[serde(rename = "stateOrCountry")]
pub state_or_country: Option<String>,
#[serde(rename = "zipCode")]
pub zip_code: Option<String>,
#[serde(rename = "stateOrCountryDescription")]
pub state_or_country_description: Option<String>,
#[serde(rename = "isForeignLocation")]
pub is_foreign_location: Option<i32>,
#[serde(rename = "foreignStateTerritory")]
pub foreign_state_territory: Option<String>,
pub country: Option<String>,
#[serde(rename = "countryCode")]
pub country_code: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FormerName {
pub name: String,
pub from: String,
pub to: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FilingsData {
pub recent: RecentFilings,
pub files: Vec<FilingFile>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct FilingFile {
pub name: String,
#[serde(rename = "filingCount")]
pub filing_count: u64,
#[serde(rename = "filingFrom")]
pub filing_from: String,
#[serde(rename = "filingTo")]
pub filing_to: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RecentFilings {
#[serde(rename = "accessionNumber")]
pub accession_number: Vec<String>,
#[serde(rename = "filingDate")]
pub filing_date: Vec<String>,
#[serde(rename = "reportDate")]
pub report_date: Option<Vec<String>>,
#[serde(rename = "acceptanceDateTime")]
pub acceptance_date_time: Vec<String>,
pub act: Option<Vec<String>>,
pub form: Vec<String>,
#[serde(rename = "fileNumber")]
pub file_number: Option<Vec<String>>,
#[serde(rename = "filmNumber")]
pub film_number: Option<Vec<String>>,
pub items: Option<Vec<String>>,
pub size: Vec<i32>,
#[serde(rename = "isXBRL")]
pub is_xbrl: Option<Vec<i32>>,
#[serde(rename = "isInlineXBRL")]
pub is_inline_xbrl: Option<Vec<i32>>,
#[serde(rename = "primaryDocument")]
pub primary_document: Option<Vec<String>>,
#[serde(rename = "primaryDocDescription")]
pub primary_doc_description: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct DetailedFiling {
pub accession_number: String,
pub filing_date: String,
pub report_date: Option<String>,
pub acceptance_date_time: DateTime<FixedOffset>,
pub act: Option<String>,
pub form: String,
pub file_number: Option<String>,
pub film_number: Option<String>,
pub items: Option<String>,
pub size: i32,
pub is_xbrl: bool,
pub is_inline_xbrl: bool,
pub primary_document: Option<String>,
pub primary_doc_description: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DirectoryResponse {
pub directory: Directory,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Directory {
pub item: Vec<DirectoryItem>,
pub name: String,
#[serde(rename = "parent-dir")]
pub parent_dir: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DirectoryItem {
#[serde(rename = "last-modified")]
pub last_modified: String,
pub name: String,
#[serde(rename = "type")]
pub type_: String,
pub size: String,
}
impl RecentFilings {
fn get_vec_item_at<T: Clone>(&self, vec_opt: &Option<Vec<T>>, idx: usize) -> Option<T> {
vec_opt.as_ref().and_then(|v| v.get(idx).cloned())
}
fn get_bool_at(&self, vec_opt: &Option<Vec<i32>>, idx: usize) -> bool {
vec_opt.as_ref().map(|x| x[idx] == 1).unwrap_or(false)
}
}
impl TryFrom<(&RecentFilings, usize)> for DetailedFiling {
type Error = chrono::ParseError;
fn try_from(
(recent, idx): (&RecentFilings, usize),
) -> std::result::Result<Self, chrono::ParseError> {
let acceptance_date_time = DateTime::parse_from_rfc3339(&recent.acceptance_date_time[idx])?;
Ok(DetailedFiling {
accession_number: recent.accession_number[idx].clone(),
filing_date: recent.filing_date[idx].clone(),
report_date: recent.get_vec_item_at(&recent.report_date, idx),
acceptance_date_time,
act: recent.get_vec_item_at(&recent.act, idx),
form: recent.form[idx].clone(),
file_number: recent.get_vec_item_at(&recent.file_number, idx),
film_number: recent.get_vec_item_at(&recent.film_number, idx),
items: recent.get_vec_item_at(&recent.items, idx),
size: recent.size[idx],
is_xbrl: recent.get_bool_at(&recent.is_xbrl, idx),
is_inline_xbrl: recent.get_bool_at(&recent.is_inline_xbrl, idx),
primary_document: recent.get_vec_item_at(&recent.primary_document, idx),
primary_doc_description: recent.get_vec_item_at(&recent.primary_doc_description, idx),
})
}
}
#[derive(Debug)]
enum UrlType {
Submission,
FilingDirectory,
EntityDirectory,
FilingContent,
TextFiling,
OriginalFiling,
SgmlHeader,
}
impl Edgar {
fn build_url(&self, url_type: UrlType, params: &[&str]) -> Result<String> {
match url_type {
UrlType::Submission => {
let cik = format!("{:0>10}", params[0]);
Ok(format!(
"{}/submissions/CIK{}.json",
self.edgar_data_url, cik
))
}
UrlType::FilingDirectory => {
let (cik, acc_no) = (params[0], params[1]);
let formatted_acc = acc_no.replace("-", "");
Ok(format!(
"{}/data/{}/{}/index.json",
self.edgar_archives_url, cik, formatted_acc
))
}
UrlType::EntityDirectory => {
let cik = format!("{:0>10}", params[0]);
Ok(format!(
"{}/data/{}/index.json",
self.edgar_archives_url, cik
))
}
UrlType::FilingContent => {
let (cik, acc_no, filename) = (params[0], params[1], params[2]);
let formatted_acc = acc_no.replace("-", "");
Ok(format!(
"{}/data/{}/{}/{}",
self.edgar_archives_url, cik, formatted_acc, filename
))
}
UrlType::TextFiling => {
let (cik, acc_no) = (params[0], params[1]);
let formatted_acc = acc_no.replace("-", "");
Ok(format!(
"{}/data/{}/{}/{}.txt",
self.edgar_archives_url, cik, formatted_acc, acc_no
))
}
UrlType::OriginalFiling => {
let (cik, acc_no) = (params[0], params[1]);
let formatted_acc = acc_no.replace("-", "");
Ok(format!(
"{}/data/{}/{}/{}-index.html",
self.edgar_archives_url, cik, formatted_acc, acc_no
))
}
UrlType::SgmlHeader => {
let (cik, acc_no) = (params[0], params[1]);
let formatted_acc = acc_no.replace("-", "");
Ok(format!(
"{}/data/{}/{}/{}.hdr.sgml",
self.edgar_archives_url, cik, formatted_acc, acc_no
))
}
}
}
fn get_filing_url(&self, cik: &str, accession_number: &str, filename: &str) -> Result<String> {
self.build_url(UrlType::FilingContent, &[cik, accession_number, filename])
}
fn get_text_filing_url(&self, cik: &str, accession_number: &str) -> Result<String> {
self.build_url(UrlType::TextFiling, &[cik, accession_number])
}
fn get_original_filing_url(&self, cik: &str, accession_number: &str) -> Result<String> {
self.build_url(UrlType::OriginalFiling, &[cik, accession_number])
}
fn get_sgml_header_url(&self, cik: &str, accession_number: &str) -> Result<String> {
self.build_url(UrlType::SgmlHeader, &[cik, accession_number])
}
}
#[async_trait]
impl FilingOperations for Edgar {
async fn submissions(&self, cik: &str) -> Result<Submission> {
let url = self.build_url(UrlType::Submission, &[cik])?;
let response = self.get(&url).await?;
Ok(serde_json::from_str::<Submission>(&response)?)
}
async fn get_recent_filings(&self, cik: &str) -> Result<Vec<DetailedFiling>> {
let submission = self.submissions(cik).await?;
let mut detailed_filings = Vec::new();
for idx in 0..submission.filings.recent.accession_number.len() {
if let Ok(filing) = DetailedFiling::try_from((&submission.filings.recent, idx)) {
detailed_filings.push(filing);
}
}
Ok(detailed_filings)
}
async fn filings(&self, cik: &str, opts: Option<FilingOptions>) -> Result<Vec<DetailedFiling>> {
let mut all_filings = self.get_recent_filings(cik).await?;
if let Some(opts) = opts {
if let Some(ref form_types) = opts.form_types {
let mut expanded_types = form_types.clone();
if opts.include_amendments {
for form_type in form_types {
if !form_type.ends_with("/A") {
expanded_types.push(format!("{}/A", form_type));
}
}
}
all_filings
.retain(|filing| expanded_types.iter().any(|ft| ft == &filing.form.trim()));
}
if let Some(offset) = opts.offset {
all_filings = all_filings.into_iter().skip(offset).collect();
}
if let Some(limit) = opts.limit {
all_filings.truncate(limit);
}
}
Ok(all_filings)
}
async fn filing_directory(
&self,
cik: &str,
accession_number: &str,
) -> Result<DirectoryResponse> {
let url = self.build_url(UrlType::FilingDirectory, &[cik, accession_number])?;
let response = self.get(&url).await?;
Ok(serde_json::from_str::<DirectoryResponse>(&response)?)
}
async fn entity_directory(&self, cik: &str) -> Result<DirectoryResponse> {
let url = self.build_url(UrlType::EntityDirectory, &[cik])?;
let response = self.get(&url).await?;
Ok(serde_json::from_str::<DirectoryResponse>(&response)?)
}
fn get_filing_url_from_id(&self, cik: &str, filing_id: &str) -> Result<String> {
let parts: Vec<&str> = filing_id.split(":").collect();
if parts.len() != 2 {
return Err(EdgarError::InvalidResponse(
"Invalid filing ID format. Expected 'accession_number:filename'".to_string(),
));
}
Ok(self.get_filing_url(cik, parts[0], parts[1])?)
}
async fn get_filing_content_by_id(&self, cik: &str, filing_id: &str) -> Result<String> {
let url = self.get_filing_url_from_id(cik, filing_id)?;
self.get(&url).await
}
async fn get_latest_filing_content(&self, cik: &str, form_types: &[&str]) -> Result<String> {
if form_types.is_empty() {
return Err(EdgarError::InvalidResponse(
"form_types must not be empty".to_string(),
));
}
let opts = FilingOptions::new()
.with_form_types(form_types.iter().map(|s| (*s).to_string()).collect());
let filings = self.filings(cik, Some(opts)).await?;
let filing = filings
.iter()
.find(|f| f.primary_document.is_some())
.ok_or(EdgarError::NotFound)?;
let primary_doc = filing
.primary_document
.as_ref()
.ok_or_else(|| EdgarError::InvalidResponse("No primary document found".to_string()))?;
let url = self.get_filing_url(cik, &filing.accession_number, primary_doc)?;
self.get(&url).await
}
async fn get_text_filing_links(
&self,
cik: &str,
opts: Option<FilingOptions>,
) -> Result<Vec<(DetailedFiling, String, String)>> {
let filings = self.filings(cik, opts).await?;
let mut links = Vec::new();
for filing in filings {
let text_url = self.get_text_filing_url(cik, &filing.accession_number)?;
let sec_gov_url = self.get_original_filing_url(cik, &filing.accession_number)?;
links.push((filing, text_url, sec_gov_url));
}
Ok(links)
}
async fn get_sgml_header_links(
&self,
cik: &str,
opts: Option<FilingOptions>,
) -> Result<Vec<(DetailedFiling, String, String)>> {
let filings = self.filings(cik, opts).await?;
let mut links = Vec::new();
for filing in filings {
let sgml_url = self.get_sgml_header_url(cik, &filing.accession_number)?;
let sec_gov_url = self.get_original_filing_url(cik, &filing.accession_number)?;
links.push((filing, sgml_url, sec_gov_url));
}
Ok(links)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_datetime_parsing() {
let sample_dates = vec![
"2015-06-01T07:06:52.000Z",
"2015-05-29T18:54:18.000Z",
"2015-05-29T18:53:07.000Z",
];
for date in sample_dates {
let parsed = DateTime::parse_from_rfc3339(&date);
assert!(parsed.is_ok());
}
}
#[test]
fn test_text_filing_url_format() {
let edgar = Edgar::new("test_agent example@example.com").unwrap();
let cik = "1889983";
let accession_number = "0001213900-23-009668";
let url = edgar.get_text_filing_url(cik, accession_number).unwrap();
let formatted_acc = accession_number.replace("-", "");
let expected_url = format!(
"{}/data/{}/{}/{}.txt",
edgar.edgar_archives_url, cik, formatted_acc, accession_number
);
assert_eq!(url, expected_url);
}
#[test]
fn test_sgml_header_url_format() {
let edgar = Edgar::new("test_agent example@example.com").unwrap();
let cik = "1889983";
let accession_number = "0001213900-23-009668";
let url = edgar.get_sgml_header_url(cik, accession_number).unwrap();
let formatted_acc = accession_number.replace("-", "");
let expected_url = format!(
"{}/data/{}/{}/{}.hdr.sgml",
edgar.edgar_archives_url, cik, formatted_acc, accession_number
);
assert_eq!(url, expected_url);
}
}