use super::CompanyOperations;
use super::Edgar;
use super::error::{EdgarError, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize)]
pub struct CompanyTicker {
#[serde(rename = "cik_str")]
pub cik: u64,
pub ticker: String,
pub title: String,
}
#[derive(Debug, Deserialize)]
pub struct MutualFundTicker {
pub cik: u64,
pub series_id: String,
pub class_id: String,
pub symbol: String,
}
#[derive(Debug, Deserialize)]
pub struct CompanyTickerExchange {
pub cik: u64,
pub ticker: String,
pub name: String,
pub exchange: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompanyFacts {
pub cik: u64,
#[serde(rename = "entityName")]
pub entity_name: String,
#[serde(rename = "facts")]
pub taxonomies: TaxonomyGroups,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaxonomyGroups {
#[serde(rename = "us-gaap")]
pub us_gaap: HashMap<String, Fact>,
pub dei: HashMap<String, Fact>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Fact {
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub units: HashMap<String, Vec<DataPoint>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPoint {
#[serde(skip_serializing_if = "Option::is_none")]
pub start: Option<String>,
pub end: String,
pub val: serde_json::Value, pub accn: String,
#[serde(default)]
pub fy: Option<i32>,
#[serde(default)]
pub fp: Option<String>,
pub form: String,
pub filed: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub frame: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompanyConcept {
pub cik: u64,
pub taxonomy: String,
pub tag: String,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub units: HashMap<String, Vec<DataPoint>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub ccp: String,
pub tag: String,
pub taxonomy: String,
pub uom: String,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub description: Option<String>,
pub pts: u64,
#[serde(rename = "data")]
pub data_points: Vec<FrameDataPoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrameDataPoint {
#[serde(rename = "entityName")]
pub entity_name: String,
pub cik: u64,
pub val: u64,
pub accn: String,
pub loc: String,
pub end: String,
}
#[derive(Debug)]
enum CompanyUrlType {
CompanyTickers,
CompanyTickersExchange,
MutualFundTickers,
CompanyFacts,
CompanyConcept,
Frames,
}
impl Edgar {
fn build_company_url(&self, url_type: CompanyUrlType, params: &[&str]) -> Result<String> {
match url_type {
CompanyUrlType::CompanyTickers => {
Ok(format!("{}/company_tickers.json", self.edgar_files_url))
}
CompanyUrlType::CompanyTickersExchange => Ok(format!(
"{}/company_tickers_exchange.json",
self.edgar_files_url
)),
CompanyUrlType::MutualFundTickers => {
Ok(format!("{}/company_tickers_mf.json", self.edgar_files_url))
}
CompanyUrlType::CompanyFacts => {
let padded_cik = format!("{:0>10}", params[0]);
Ok(format!(
"{}/api/xbrl/companyfacts/CIK{}.json",
self.edgar_data_url, padded_cik
))
}
CompanyUrlType::CompanyConcept => {
let (cik, taxonomy, tag) = (params[0], params[1], params[2]);
let padded_cik = format!("{:0>10}", cik);
Ok(format!(
"{}/api/xbrl/companyconcept/CIK{}/{}/{}.json",
self.edgar_data_url, padded_cik, taxonomy, tag
))
}
CompanyUrlType::Frames => {
let (taxonomy, tag, unit, period) = (params[0], params[1], params[2], params[3]);
Ok(format!(
"{}/api/xbrl/frames/{}/{}/{}/{}.json",
self.edgar_data_url, taxonomy, tag, unit, period
))
}
}
}
}
trait JsonParser {
fn parse_json_array<T, F>(
&self,
content: &str,
required_fields: &[&str],
mapper: F,
) -> Result<Vec<T>>
where
F: Fn(&FieldExtractor, &[serde_json::Value]) -> Option<T>;
}
impl JsonParser for Edgar {
fn parse_json_array<T, F>(
&self,
content: &str,
required_fields: &[&str],
mapper: F,
) -> Result<Vec<T>>
where
F: Fn(&FieldExtractor, &[serde_json::Value]) -> Option<T>,
{
let json: serde_json::Value = serde_json::from_str(content)?;
let fields = json["fields"]
.as_array()
.ok_or_else(|| EdgarError::InvalidResponse("Missing 'fields' array".to_string()))?;
let data = json["data"]
.as_array()
.ok_or_else(|| EdgarError::InvalidResponse("Missing 'data' array".to_string()))?;
let extractor = FieldExtractor::new(fields.to_vec(), required_fields)?;
Ok(data
.iter()
.filter_map(|row| row.as_array().and_then(|r| mapper(&extractor, r)))
.collect())
}
}
struct FieldExtractor {
indices: HashMap<String, usize>,
}
impl FieldExtractor {
fn new(fields: Vec<serde_json::Value>, required: &[&str]) -> Result<Self> {
let mut indices = HashMap::new();
for field_name in required {
let idx = fields
.iter()
.position(|field| field.as_str() == Some(field_name))
.ok_or_else(|| {
EdgarError::InvalidResponse(format!("Missing '{}' field", field_name))
})?;
indices.insert(field_name.to_string(), idx);
}
Ok(Self { indices })
}
fn get_index(&self, field: &str) -> Result<usize> {
self.indices
.get(field)
.copied()
.ok_or_else(|| EdgarError::InvalidResponse(format!("Field '{}' not found", field)))
}
fn extract_value<T, F>(&self, row: &[serde_json::Value], field: &str, converter: F) -> Option<T>
where
F: Fn(&serde_json::Value) -> Option<T>,
{
let idx = self.get_index(field).ok()?;
row.get(idx).and_then(converter)
}
}
#[async_trait]
impl CompanyOperations for Edgar {
async fn company_tickers(&self) -> Result<Vec<CompanyTicker>> {
let url = self.build_company_url(CompanyUrlType::CompanyTickers, &[])?;
let response = self.get(&url).await?;
let map: HashMap<String, CompanyTicker> = serde_json::from_str(&response)?;
Ok(map.into_values().collect())
}
async fn company_cik(&self, ticker: &str) -> Result<u64> {
let tickers = self.company_tickers().await?;
let company = tickers
.iter()
.find(|t| t.ticker == ticker.to_uppercase())
.ok_or(EdgarError::TickerNotFound)?;
Ok(company.cik.clone())
}
async fn mutual_fund_cik(&self, ticker: &str) -> Result<u64> {
let tickers = self.mutual_fund_tickers().await?;
let fund = tickers
.iter()
.find(|t| t.symbol == ticker.to_uppercase())
.ok_or(EdgarError::TickerNotFound)?;
Ok(fund.cik.clone())
}
async fn company_tickers_with_exchange(&self) -> Result<Vec<CompanyTickerExchange>> {
let url = self.build_company_url(CompanyUrlType::CompanyTickersExchange, &[])?;
let response = self.get(&url).await?;
self.parse_json_array(
&response,
&["cik", "name", "ticker", "exchange"],
|extractor, row| {
Some(CompanyTickerExchange {
cik: extractor.extract_value(row, "cik", |v| v.as_str()?.parse().ok())?,
name: extractor.extract_value(row, "name", |v| v.as_str().map(String::from))?,
ticker: extractor
.extract_value(row, "ticker", |v| v.as_str().map(String::from))?,
exchange: extractor
.extract_value(row, "exchange", |v| v.as_str().map(String::from))?,
})
},
)
}
async fn mutual_fund_tickers(&self) -> Result<Vec<MutualFundTicker>> {
let url = self.build_company_url(CompanyUrlType::MutualFundTickers, &[])?;
let response = self.get(&url).await?;
self.parse_json_array(
&response,
&["cik", "seriesId", "classId", "symbol"],
|extractor, row| {
Some(MutualFundTicker {
cik: extractor.extract_value(row, "cik", |v| v.as_u64())?,
series_id: extractor
.extract_value(row, "seriesId", |v| v.as_str().map(String::from))?,
class_id: extractor
.extract_value(row, "classId", |v| v.as_str().map(String::from))?,
symbol: extractor
.extract_value(row, "symbol", |v| v.as_str().map(String::from))?,
})
},
)
}
async fn company_facts(&self, cik: u64) -> Result<CompanyFacts> {
let url = self.build_company_url(CompanyUrlType::CompanyFacts, &[&cik.to_string()])?;
let response = self.get(&url).await?;
Ok(serde_json::from_str(&response)?)
}
async fn company_concept(&self, cik: u64, taxonomy: &str, tag: &str) -> Result<CompanyConcept> {
let url = self.build_company_url(
CompanyUrlType::CompanyConcept,
&[&cik.to_string(), taxonomy, tag],
)?;
let response = self.get(&url).await?;
Ok(serde_json::from_str(&response)?)
}
async fn frames(&self, taxonomy: &str, tag: &str, unit: &str, period: &str) -> Result<Frame> {
let url = self.build_company_url(CompanyUrlType::Frames, &[taxonomy, tag, unit, period])?;
let response = self.get(&url).await?;
Ok(serde_json::from_str(&response)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_invalid_json() {
let edgar = Edgar::new("test_agent").unwrap();
let result =
edgar.parse_json_array::<CompanyTicker, _>("invalid json", &["cik"], |_, _| None);
assert!(result.is_err());
}
#[test]
fn test_parse_fact_with_null_fields() {
let json = r#"{
"label": null,
"description": null,
"units": {
"USD": [
{
"end": "2021-12-31",
"val": 1000000,
"accn": "0001234567-21-000001",
"fy": 2021,
"fp": "FY",
"form": "10-K",
"filed": "2022-01-31"
}
]
}
}"#;
let fact: Fact = serde_json::from_str(json).unwrap();
assert!(fact.label.is_none());
assert!(fact.description.is_none());
assert!(!fact.units.is_empty());
}
}