diff --git a/crates/fmp-py/python/fmp/_native/company/__init__.pyi b/crates/fmp-py/python/fmp/_native/company/__init__.pyi index 16ed792d..9315aafa 100644 --- a/crates/fmp-py/python/fmp/_native/company/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/company/__init__.pyi @@ -42,7 +42,7 @@ class CompanyExecutive: @property def currency_pay(self) -> builtins.str: ... @property - def gender(self) -> builtins.str: ... + def gender(self) -> typing.Optional[builtins.str]: ... @property def active(self) -> builtins.bool: ... @property @@ -59,7 +59,7 @@ class CompanyExecutive: name: builtins.str, pay: typing.Optional[builtins.str], currency_pay: builtins.str, - gender: builtins.str, + gender: typing.Optional[builtins.str], year_born: typing.Optional[builtins.str], title_since: typing.Optional[builtins.str], active: builtins.bool, @@ -85,10 +85,14 @@ class CompanyMarketCapitalization: @property def date(self) -> datetime.date: ... @property - def market_cap(self) -> builtins.float: ... + def market_cap(self) -> typing.Optional[builtins.float]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( - cls, *, symbol: builtins.str, date: datetime.date, market_cap: builtins.float + cls, + *, + symbol: builtins.str, + date: datetime.date, + market_cap: typing.Optional[builtins.float], ) -> CompanyMarketCapitalization: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -244,7 +248,7 @@ class CompanyNote: @property def title(self) -> builtins.str: ... @property - def exchange(self) -> builtins.str: ... + def exchange(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -252,7 +256,7 @@ class CompanyNote: cik: builtins.str, symbol: builtins.str, title: builtins.str, - exchange: builtins.str, + exchange: typing.Optional[builtins.str], ) -> CompanyNote: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -330,11 +334,11 @@ class CompanyProfile: @property def currency(self) -> builtins.str: ... @property - def cik(self) -> builtins.str: ... + def cik(self) -> typing.Optional[builtins.str]: ... @property def isin(self) -> builtins.str: ... @property - def cusip(self) -> builtins.str: ... + def cusip(self) -> typing.Optional[builtins.str]: ... @property def exchange_full_name(self) -> builtins.str: ... @property @@ -352,9 +356,9 @@ class CompanyProfile: @property def country(self) -> builtins.str: ... @property - def full_time_employees(self) -> builtins.str: ... + def full_time_employees(self) -> typing.Optional[builtins.str]: ... @property - def phone(self) -> builtins.str: ... + def phone(self) -> typing.Optional[builtins.str]: ... @property def address(self) -> builtins.str: ... @property @@ -366,7 +370,7 @@ class CompanyProfile: @property def image(self) -> builtins.str: ... @property - def ipo_date(self) -> datetime.date: ... + def ipo_date(self) -> typing.Optional[datetime.date]: ... @property def default_image(self) -> builtins.bool: ... @property @@ -393,9 +397,9 @@ class CompanyProfile: average_volume: builtins.float, company_name: builtins.str, currency: builtins.str, - cik: builtins.str, + cik: typing.Optional[builtins.str], isin: builtins.str, - cusip: builtins.str, + cusip: typing.Optional[builtins.str], exchange_full_name: builtins.str, exchange: builtins.str, industry: builtins.str, @@ -404,14 +408,14 @@ class CompanyProfile: ceo: builtins.str, sector: builtins.str, country: builtins.str, - full_time_employees: builtins.str, - phone: builtins.str, + full_time_employees: typing.Optional[builtins.str], + phone: typing.Optional[builtins.str], address: builtins.str, city: builtins.str, state: builtins.str, zip: builtins.str, image: builtins.str, - ipo_date: datetime.date, + ipo_date: typing.Optional[datetime.date], default_image: builtins.bool, is_etf: builtins.bool, is_actively_trading: builtins.bool, @@ -450,7 +454,7 @@ class CompanyShareFloat: @property def outstanding_shares(self) -> builtins.float: ... @property - def source(self) -> builtins.str: ... + def source(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -460,7 +464,7 @@ class CompanyShareFloat: free_float: builtins.float, float_shares: builtins.float, outstanding_shares: builtins.float, - source: builtins.str, + source: typing.Optional[builtins.str], ) -> CompanyShareFloat: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... @@ -609,7 +613,7 @@ class ExecutiveCompensation: @property def stock_award(self) -> builtins.float: ... @property - def option_award(self) -> builtins.float: ... + def option_award(self) -> typing.Optional[builtins.float]: ... @property def incentive_plan_compensation(self) -> builtins.float: ... @property @@ -632,7 +636,7 @@ class ExecutiveCompensation: salary: builtins.float, bonus: builtins.float, stock_award: builtins.float, - option_award: builtins.float, + option_award: typing.Optional[builtins.float], incentive_plan_compensation: builtins.float, all_other_compensation: builtins.float, total: builtins.float, @@ -703,9 +707,9 @@ class MergerAcquisition: @property def targeted_company_name(self) -> builtins.str: ... @property - def targeted_cik(self) -> builtins.str: ... + def targeted_cik(self) -> typing.Optional[builtins.str]: ... @property - def targeted_symbol(self) -> builtins.str: ... + def targeted_symbol(self) -> typing.Optional[builtins.str]: ... @property def transaction_date(self) -> datetime.date: ... @property @@ -720,8 +724,8 @@ class MergerAcquisition: company_name: builtins.str, cik: builtins.str, targeted_company_name: builtins.str, - targeted_cik: builtins.str, - targeted_symbol: builtins.str, + targeted_cik: typing.Optional[builtins.str], + targeted_symbol: typing.Optional[builtins.str], transaction_date: datetime.date, accepted_date: datetime.datetime, link: builtins.str, @@ -751,9 +755,9 @@ class ShareFloat: @property def date(self) -> datetime.datetime: ... @property - def free_float(self) -> builtins.float: ... + def free_float(self) -> typing.Optional[builtins.float]: ... @property - def float_shares(self) -> builtins.float: ... + def float_shares(self) -> typing.Optional[builtins.float]: ... @property def outstanding_shares(self) -> builtins.float: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -762,8 +766,8 @@ class ShareFloat: *, symbol: builtins.str, date: datetime.datetime, - free_float: builtins.float, - float_shares: builtins.float, + free_float: typing.Optional[builtins.float], + float_shares: typing.Optional[builtins.float], outstanding_shares: builtins.float, ) -> ShareFloat: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... diff --git a/crates/fmp-py/src/models/company.rs b/crates/fmp-py/src/models/company.rs index e7d019cc..00397929 100644 --- a/crates/fmp-py/src/models/company.rs +++ b/crates/fmp-py/src/models/company.rs @@ -34,11 +34,11 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub currency: String, #[pyo3(get)] - pub cik: String, + pub cik: Option, #[pyo3(get)] pub isin: String, #[pyo3(get)] - pub cusip: String, + pub cusip: Option, #[pyo3(get)] pub exchange_full_name: String, #[pyo3(get)] @@ -56,9 +56,9 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub country: String, #[pyo3(get)] - pub full_time_employees: String, + pub full_time_employees: Option, #[pyo3(get)] - pub phone: String, + pub phone: Option, #[pyo3(get)] pub address: String, #[pyo3(get)] @@ -70,7 +70,7 @@ pub(crate) struct CompanyProfile { #[pyo3(get)] pub image: String, #[pyo3(get)] - pub ipo_date: ::chrono::NaiveDate, + pub ipo_date: Option<::chrono::NaiveDate>, #[pyo3(get)] pub default_image: bool, #[pyo3(get)] @@ -103,9 +103,9 @@ impl CompanyProfile { average_volume: f64, company_name: String, currency: String, - cik: String, + cik: Option, isin: String, - cusip: String, + cusip: Option, exchange_full_name: String, exchange: String, industry: String, @@ -114,14 +114,14 @@ impl CompanyProfile { ceo: String, sector: String, country: String, - full_time_employees: String, - phone: String, + full_time_employees: Option, + phone: Option, address: String, city: String, state: String, zip: String, image: String, - ipo_date: ::chrono::NaiveDate, + ipo_date: Option<::chrono::NaiveDate>, default_image: bool, is_etf: bool, is_actively_trading: bool, @@ -528,9 +528,9 @@ impl From for CompanyProfile { average_volume: value.average_volume, company_name: value.company_name, currency: value.currency.into_inner(), - cik: value.cik.into_inner(), + cik: value.cik.map(|value| value.into_inner()), isin: value.isin.into_inner(), - cusip: value.cusip.into_inner(), + cusip: value.cusip.map(|value| value.into_inner()), exchange_full_name: value.exchange_full_name, exchange: value.exchange.into_inner(), industry: value.industry.into_inner(), @@ -539,14 +539,14 @@ impl From for CompanyProfile { ceo: value.ceo, sector: value.sector.into_inner(), country: value.country.into_inner(), - full_time_employees: value.full_time_employees.into_inner(), + full_time_employees: value.full_time_employees.map(|value| value.into_inner()), phone: value.phone, address: value.address, city: value.city, state: value.state, zip: value.zip, image: value.image, - ipo_date: value.ipo_date.into_inner(), + ipo_date: value.ipo_date.map(|value| value.into_inner()), default_image: value.default_image, is_etf: value.is_etf, is_actively_trading: value.is_actively_trading, @@ -573,7 +573,7 @@ pub(crate) struct CompanyNote { #[pyo3(get)] pub title: String, #[pyo3(get)] - pub exchange: String, + pub exchange: Option, } #[gen_stub_pymethods] @@ -583,7 +583,7 @@ impl CompanyNote { #[allow(clippy::too_many_arguments)] #[allow(clippy::fn_params_excessive_bools)] #[pyo3(signature = (*, cik, symbol, title, exchange))] - fn new(cik: String, symbol: String, title: String, exchange: String) -> Self { + fn new(cik: String, symbol: String, title: String, exchange: Option) -> Self { Self { cik, symbol, @@ -657,7 +657,7 @@ impl From for CompanyNote { cik: value.cik.into_inner(), symbol: value.symbol.into_inner(), title: value.title, - exchange: value.exchange.into_inner(), + exchange: value.exchange.map(|value| value.into_inner()), } } } @@ -1141,7 +1141,7 @@ pub(crate) struct CompanyMarketCapitalization { #[pyo3(get)] pub date: ::chrono::NaiveDate, #[pyo3(get)] - pub market_cap: f64, + pub market_cap: Option, } #[gen_stub_pymethods] @@ -1151,7 +1151,7 @@ impl CompanyMarketCapitalization { #[allow(clippy::too_many_arguments)] #[allow(clippy::fn_params_excessive_bools)] #[pyo3(signature = (*, symbol, date, market_cap))] - fn new(symbol: String, date: ::chrono::NaiveDate, market_cap: f64) -> Self { + fn new(symbol: String, date: ::chrono::NaiveDate, market_cap: Option) -> Self { Self { symbol, date, @@ -1243,7 +1243,7 @@ pub(crate) struct CompanyShareFloat { #[pyo3(get)] pub outstanding_shares: f64, #[pyo3(get)] - pub source: String, + pub source: Option, } #[gen_stub_pymethods] @@ -1259,7 +1259,7 @@ impl CompanyShareFloat { free_float: f64, float_shares: f64, outstanding_shares: f64, - source: String, + source: Option, ) -> Self { Self { symbol, @@ -1392,9 +1392,9 @@ pub(crate) struct ShareFloat { #[pyo3(get)] pub date: ::chrono::NaiveDateTime, #[pyo3(get)] - pub free_float: f64, + pub free_float: Option, #[pyo3(get)] - pub float_shares: f64, + pub float_shares: Option, #[pyo3(get)] pub outstanding_shares: f64, } @@ -1409,8 +1409,8 @@ impl ShareFloat { fn new( symbol: String, date: ::chrono::NaiveDateTime, - free_float: f64, - float_shares: f64, + free_float: Option, + float_shares: Option, outstanding_shares: f64, ) -> Self { Self { @@ -1539,9 +1539,9 @@ pub(crate) struct MergerAcquisition { #[pyo3(get)] pub targeted_company_name: String, #[pyo3(get)] - pub targeted_cik: String, + pub targeted_cik: Option, #[pyo3(get)] - pub targeted_symbol: String, + pub targeted_symbol: Option, #[pyo3(get)] pub transaction_date: ::chrono::NaiveDate, #[pyo3(get)] @@ -1562,8 +1562,8 @@ impl MergerAcquisition { company_name: String, cik: String, targeted_company_name: String, - targeted_cik: String, - targeted_symbol: String, + targeted_cik: Option, + targeted_symbol: Option, transaction_date: ::chrono::NaiveDate, accepted_date: ::chrono::NaiveDateTime, link: String, @@ -1726,8 +1726,8 @@ impl From for MergerAcquisition { company_name: value.company_name, cik: value.cik.into_inner(), targeted_company_name: value.targeted_company_name, - targeted_cik: value.targeted_cik.into_inner(), - targeted_symbol: value.targeted_symbol.into_inner(), + targeted_cik: value.targeted_cik.map(|value| value.into_inner()), + targeted_symbol: value.targeted_symbol.map(|value| value.into_inner()), transaction_date: value.transaction_date.into_inner(), accepted_date: value.accepted_date.into_inner(), link: value.link, @@ -1753,7 +1753,7 @@ pub(crate) struct CompanyExecutive { #[pyo3(get)] pub currency_pay: String, #[pyo3(get)] - pub gender: String, + pub gender: Option, year_born: Option<::serde_json::Value>, title_since: Option<::serde_json::Value>, #[pyo3(get)] @@ -1772,7 +1772,7 @@ impl CompanyExecutive { name: String, pay: Option, currency_pay: String, - gender: String, + gender: Option, year_born: Option, title_since: Option, active: bool, @@ -2017,7 +2017,7 @@ pub(crate) struct ExecutiveCompensation { #[pyo3(get)] pub stock_award: f64, #[pyo3(get)] - pub option_award: f64, + pub option_award: Option, #[pyo3(get)] pub incentive_plan_compensation: f64, #[pyo3(get)] @@ -2046,7 +2046,7 @@ impl ExecutiveCompensation { salary: f64, bonus: f64, stock_award: f64, - option_award: f64, + option_award: Option, incentive_plan_compensation: f64, all_other_compensation: f64, total: f64, diff --git a/crates/fmp-py/tests/test_company_a.py b/crates/fmp-py/tests/test_company_a.py index 1b110502..9eab9ce1 100644 --- a/crates/fmp-py/tests/test_company_a.py +++ b/crates/fmp-py/tests/test_company_a.py @@ -67,7 +67,11 @@ def test_profile_decodes_multiple_rows_with_large_integers(client: Any, fixture_ assert rows[0].volume == float(18_446_744_073_709_551_615) assert isinstance(rows[0].market_cap, float) assert rows[0].market_cap == float(9_007_199_254_740_993) - assert rows[0].ipo_date == datetime.date(2026, 8, 27) + assert rows[0].ipo_date is None + assert rows[0].cik is None + assert rows[0].cusip is None + assert rows[0].full_time_employees is None + assert rows[0].phone is None assert rows[0].is_fund is True assert rows[1].ipo_date == datetime.date(1980, 12, 12) diff --git a/crates/fmp-py/tests/test_company_b.py b/crates/fmp-py/tests/test_company_b.py index 24d804aa..d34db8c3 100644 --- a/crates/fmp-py/tests/test_company_b.py +++ b/crates/fmp-py/tests/test_company_b.py @@ -101,6 +101,8 @@ def test_latest_mergers_acquisitions_with_page_only_decodes_multiple_rows( assert len(rows) == 2 assert [row.symbol for row in rows] == ["AGH", "PEGY"] assert rows[1].transaction_date == datetime.date(2021, 11, 12) + assert rows[1].targeted_cik is None + assert rows[1].targeted_symbol is None def test_search_mergers_acquisitions_preserves_the_name_verbatim(client: Any, fixture_server: FixtureServer) -> None: @@ -151,6 +153,7 @@ def test_key_executives_surfaces_dynamic_json_values(client: Any, fixture_server assert probe.pay == {"amount": "00123.450", "components": [1, True, None]} assert probe.title_since == 1_704_067_200 assert probe.year_born == "01980" + assert probe.gender is None def test_key_executives_keeps_big_integers_exact(client: Any, fixture_server: FixtureServer) -> None: @@ -213,6 +216,7 @@ def test_executive_compensation_decodes_large_amounts(client: Any, fixture_serve assert rows[1].company_name == "Large Amount Probe" assert rows[1].salary == 5_000_000_000 assert rows[1].all_other_compensation == 10_000_000_000 + assert rows[1].option_award is None assert rows[1].accepted_date == datetime.datetime(2026, 1, 1, 0, 0, 0) diff --git a/crates/fmp-py/tests/typecheck_contract.py b/crates/fmp-py/tests/typecheck_contract.py index 132f1b58..7f7db1d3 100644 --- a/crates/fmp-py/tests/typecheck_contract.py +++ b/crates/fmp-py/tests/typecheck_contract.py @@ -145,7 +145,7 @@ def check_bulk_contract(client: FmpClient) -> None: """Type-check the constructor-only bulk arguments and the cross-domain profile rows.""" bulk: BulkNamespace = client.bulk profiles: list[CompanyProfile] = bulk.company_profiles("0") - ipo_date: datetime.date = profiles[0].ipo_date + ipo_date: datetime.date | None = profiles[0].ipo_date statements: list[BulkIncomeStatement] = client.bulk.income_statements(2026, "Q1") accepted: datetime.datetime = statements[0].accepted_date revenue: str = statements[0].revenue @@ -161,11 +161,11 @@ def check_company_contract(client: FmpClient) -> None: """Type-check the flat company namespace, its date keywords, and the batch list input.""" company: CompanyNamespace = client.company profiles: list[CompanyProfile] = company.profile("AAPL") - ipo_date: datetime.date = profiles[0].ipo_date + ipo_date: datetime.date | None = profiles[0].ipo_date history: list[CompanyMarketCapitalization] = client.company.historical_market_capitalization( "AAPL", limit=5001, from_=datetime.date(2026, 4, 16), to="2026-07-16" ) - market_cap: float = history[0].market_cap + market_cap: float | None = history[0].market_cap batch: list[CompanyMarketCapitalization] = client.company.batch_market_capitalization(["AAPL", "MSFT"]) benchmarks: list[ExecutiveCompensationBenchmark] = client.company.executive_compensation_benchmark(year="2024") average: float = benchmarks[0].average_compensation diff --git a/crates/libfmp/src/responses/company.rs b/crates/libfmp/src/responses/company.rs index d8f3c190..fdf4a19d 100644 --- a/crates/libfmp/src/responses/company.rs +++ b/crates/libfmp/src/responses/company.rs @@ -4,10 +4,10 @@ //! The Python binding exposes these thirteen models under `fmp.company` with //! the same names. -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; use crate::{ - codecs::{DynamicJson, NumericString}, + codecs::{DynamicJson, NumericString, empty_or_null_date}, types::{ ApiDateTime, Change, Cik, Count, CountryCode, CurrencyCode, Cusip, Date, ExchangeCode, Industry, Isin, MarketCapitalization, MarketValue, PerShareAmount, Percentage, Price, @@ -15,6 +15,14 @@ use crate::{ }, }; +fn required_option<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: Deserialize<'de>, +{ + Option::::deserialize(deserializer) +} + /// A detailed worldwide company profile returned by symbol or US CIK lookup. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[non_exhaustive] @@ -37,9 +45,11 @@ pub struct CompanyProfile { pub average_volume: Volume, pub company_name: String, pub currency: CurrencyCode, - pub cik: Cik, + #[serde(deserialize_with = "required_option")] + pub cik: Option, pub isin: Isin, - pub cusip: Cusip, + #[serde(deserialize_with = "required_option")] + pub cusip: Option, pub exchange_full_name: String, pub exchange: ExchangeCode, pub industry: Industry, @@ -48,14 +58,17 @@ pub struct CompanyProfile { pub ceo: String, pub sector: Sector, pub country: CountryCode, - pub full_time_employees: NumericString, - pub phone: String, + #[serde(deserialize_with = "required_option")] + pub full_time_employees: Option, + #[serde(deserialize_with = "required_option")] + pub phone: Option, pub address: String, pub city: String, pub state: String, pub zip: String, pub image: String, - pub ipo_date: Date, + #[serde(with = "empty_or_null_date")] + pub ipo_date: Option, pub default_image: bool, pub is_etf: bool, pub is_actively_trading: bool, @@ -71,7 +84,8 @@ pub struct CompanyNote { pub cik: Cik, pub symbol: Ticker, pub title: String, - pub exchange: ExchangeCode, + #[serde(deserialize_with = "required_option")] + pub exchange: Option, } /// One worldwide stock peer selected by the provider. @@ -123,8 +137,9 @@ pub struct EmployeeCount { pub struct CompanyMarketCapitalization { pub symbol: Ticker, pub date: Date, - #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] - pub market_cap: MarketCapitalization, + #[serde(deserialize_with = "required_option")] + #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] + pub market_cap: Option, } /// One worldwide company share-float observation with its filing source. @@ -139,7 +154,8 @@ pub struct CompanyShareFloat { pub float_shares: Quantity, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub outstanding_shares: Quantity, - pub source: String, + #[serde(deserialize_with = "required_option")] + pub source: Option, } /// One all-company share-float observation, whose payload has no source field. @@ -149,9 +165,11 @@ pub struct CompanyShareFloat { pub struct ShareFloat { pub symbol: Ticker, pub date: ApiDateTime, - pub free_float: Percentage, - #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] - pub float_shares: Quantity, + #[serde(deserialize_with = "required_option")] + pub free_float: Option, + #[serde(deserialize_with = "required_option")] + #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] + pub float_shares: Option, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub outstanding_shares: Quantity, } @@ -165,8 +183,10 @@ pub struct MergerAcquisition { pub company_name: String, pub cik: Cik, pub targeted_company_name: String, - pub targeted_cik: Cik, - pub targeted_symbol: Ticker, + #[serde(deserialize_with = "required_option")] + pub targeted_cik: Option, + #[serde(deserialize_with = "required_option")] + pub targeted_symbol: Option, pub transaction_date: Date, pub accepted_date: ApiDateTime, pub link: String, @@ -181,7 +201,8 @@ pub struct CompanyExecutive { pub name: String, pub pay: Option, pub currency_pay: CurrencyCode, - pub gender: String, + #[serde(deserialize_with = "required_option")] + pub gender: Option, pub year_born: Option, pub title_since: Option, pub active: bool, @@ -205,8 +226,9 @@ pub struct ExecutiveCompensation { pub bonus: MarketValue, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub stock_award: MarketValue, - #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] - pub option_award: MarketValue, + #[serde(deserialize_with = "required_option")] + #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] + pub option_award: Option, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub incentive_plan_compensation: MarketValue, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] diff --git a/crates/libfmp/tests/bulk_snapshot_responses.rs b/crates/libfmp/tests/bulk_snapshot_responses.rs index 1c0038c3..6ed0be56 100644 --- a/crates/libfmp/tests/bulk_snapshot_responses.rs +++ b/crates/libfmp/tests/bulk_snapshot_responses.rs @@ -33,7 +33,10 @@ fn exact_outer_md_fixtures_round_trip_with_exact_field_counts() { #[test] fn every_documented_field_is_required_non_null_and_unknown_fields_are_tolerated() { - assert_required::(PROFILE); + assert_required_nullable::( + PROFILE, + &["cik", "cusip", "fullTimeEmployees", "phone", "ipoDate"], + ); assert_required::(RATING); assert_required::(DCF); assert_required::(SCORES); @@ -112,6 +115,13 @@ where } fn assert_required(fixture: &[u8]) +where + T: DeserializeOwned, +{ + assert_required_nullable::(fixture, &[]); +} + +fn assert_required_nullable(fixture: &[u8], nullable: &[&str]) where T: DeserializeOwned, { @@ -126,9 +136,10 @@ where let mut null = row.clone(); null.insert(field.clone(), Value::Null); - assert!( - serde_json::from_value::(Value::Object(null)).is_err(), - "null {field} unexpectedly decoded" + assert_eq!( + serde_json::from_value::(Value::Object(null)).is_ok(), + nullable.contains(&field.as_str()), + "null {field} decoded contrary to its nullability" ); } diff --git a/crates/libfmp/tests/company_endpoints.rs b/crates/libfmp/tests/company_endpoints.rs index f7906bbf..f9636b03 100644 --- a/crates/libfmp/tests/company_endpoints.rs +++ b/crates/libfmp/tests/company_endpoints.rs @@ -82,6 +82,8 @@ async fn custom_proxy_client_executes_all_four_paths_with_exact_required_queries .await .unwrap()[0] .cik + .as_ref() + .unwrap() .as_str(), "0000320193" ); @@ -91,6 +93,8 @@ async fn custom_proxy_client_executes_all_four_paths_with_exact_required_queries .await .unwrap()[0] .full_time_employees + .as_ref() + .unwrap() .as_str(), "166000" ); @@ -151,7 +155,7 @@ async fn profile_client_preserves_empty_multiple_and_unknown_field_arrays() { assert_eq!(multiple[0].market_cap, 9_007_199_254_740_992.0); assert_eq!(multiple[0].volume, u64::MAX as f64); assert_eq!(unknown.len(), 1); - assert_eq!(unknown[0].cik.as_str(), "0000320193"); + assert_eq!(unknown[0].cik.as_ref().unwrap().as_str(), "0000320193"); } #[tokio::test] diff --git a/crates/libfmp/tests/company_governance_responses.rs b/crates/libfmp/tests/company_governance_responses.rs index 298daefc..bbeae7b6 100644 --- a/crates/libfmp/tests/company_governance_responses.rs +++ b/crates/libfmp/tests/company_governance_responses.rs @@ -20,7 +20,7 @@ fn documented_company_executive_decodes_exact_fields_and_nulls() { assert_eq!(row.name, "Kristin Huguet Quayle"); assert_eq!(row.pay, None); assert_eq!(row.currency_pay.as_str(), "USD"); - assert_eq!(row.gender, "female"); + assert_eq!(row.gender.as_deref(), Some("female")); assert_eq!(row.year_born, None); assert_eq!(row.title_since, None); assert!(row.active); @@ -56,6 +56,8 @@ fn company_executive_dynamic_fields_preserve_synthetic_non_null_json() { ); assert_eq!(row.year_born, Some(json!("01980"))); assert_eq!(row.title_since, Some(json!(1704067200))); + assert_eq!(row.gender, None); + assert!(serde_json::to_value(row).unwrap()["gender"].is_null()); assert!(!row.active); } @@ -77,7 +79,7 @@ fn documented_executive_compensation_decodes_dates_integer_year_and_amounts() { assert_eq!(row.salary, 819_231.0); assert_eq!(row.bonus, 0.0); assert_eq!(row.stock_award, 13_003_031.0); - assert_eq!(row.option_award, 0.0); + assert_eq!(row.option_award, Some(0.0)); assert_eq!(row.incentive_plan_compensation, 1_638_462.0); assert_eq!(row.all_other_compensation, 22_204.0); assert_eq!(row.total, 15_482_928.0); @@ -94,6 +96,8 @@ fn executive_compensation_arrays_decode_large_amounts_and_preserve_exact_casing( assert_eq!(rows[1].salary, 5_000_000_000.0); assert_eq!(rows[1].all_other_compensation, 10_000_000_000.0); assert_eq!(rows[1].total, u64::MAX as f64); + assert_eq!(rows[1].option_award, None); + assert!(serde_json::to_value(&rows[1]).unwrap()["optionAward"].is_null()); let encoded = serde_json::to_value(&rows[0]).unwrap(); assert_eq!(encoded["companyName"], "Apple Inc."); diff --git a/crates/libfmp/tests/company_market_data_responses.rs b/crates/libfmp/tests/company_market_data_responses.rs index 260a407e..91a19858 100644 --- a/crates/libfmp/tests/company_market_data_responses.rs +++ b/crates/libfmp/tests/company_market_data_responses.rs @@ -9,7 +9,7 @@ fn documented_market_capitalization_decodes_every_exact_field_and_wire_type() { let row = &rows[0]; assert_eq!(row.symbol.as_str(), "AAPL"); assert_eq!(row.date.to_string(), "2026-07-30"); - assert_eq!(row.market_cap, 4_874_072_686_740.0); + assert_eq!(row.market_cap, Some(4_874_072_686_740.0)); let wire = serde_json::to_value(row).unwrap(); assert_eq!(wire["date"], "2026-07-30"); @@ -24,7 +24,7 @@ fn documented_market_capitalization_decodes_every_exact_field_and_wire_type() { assert_eq!(historical.len(), 1); assert_eq!(historical[0].symbol.as_str(), "AAPL"); assert_eq!(historical[0].date.to_string(), "2026-07-30"); - assert_eq!(historical[0].market_cap, 4_879_177_245_542.0); + assert_eq!(historical[0].market_cap, Some(4_879_177_245_542.0)); } #[test] @@ -40,8 +40,8 @@ fn documented_company_share_float_decodes_source_and_exact_wire_types() { assert_eq!(row.float_shares, 14_662_387_495.0); assert_eq!(row.outstanding_shares, 14_687_356_000.0); assert_eq!( - row.source, - "https://www.sec.gov/Archives/edgar/data/320193/000032019326000013/aapl-20260328.htm" + row.source.as_deref(), + Some("https://www.sec.gov/Archives/edgar/data/320193/000032019326000013/aapl-20260328.htm") ); let wire = serde_json::to_value(row).unwrap(); @@ -63,8 +63,8 @@ fn documented_all_share_float_is_a_separate_source_free_shape() { let row = &rows[0]; assert_eq!(row.symbol.as_str(), "000001.SZ"); assert_eq!(row.date.to_string(), "2026-07-29 14:23:30"); - assert_eq!(row.free_float, 41.40900000201062); - assert_eq!(row.float_shares, 8_035_796_667.0); + assert_eq!(row.free_float, Some(41.40900000201062)); + assert_eq!(row.float_shares, Some(8_035_796_667.0)); assert_eq!(row.outstanding_shares, 19_405_918_198.0); let wire = serde_json::to_value(row).unwrap(); @@ -98,22 +98,26 @@ fn market_data_arrays_preserve_empty_multiple_unknown_fields_and_large_values() let market_caps: Vec = serde_json::from_str( r#"[ {"symbol":"AAPL","date":"2026-07-30","marketCap":18446744073709551615,"future":true}, - {"symbol":"MSFT","date":"2026-07-30","marketCap":9007199254740993} + {"symbol":"MSFT","date":"2026-07-30","marketCap":9007199254740993}, + {"symbol":"EURUSD","date":"2026-07-30","marketCap":null} ]"#, ) .unwrap(); - assert_eq!(market_caps.len(), 2); - assert_eq!(market_caps[0].market_cap, u64::MAX as f64); - assert_eq!(market_caps[1].market_cap, 9_007_199_254_740_992.0); + assert_eq!(market_caps.len(), 3); + assert_eq!(market_caps[0].market_cap, Some(u64::MAX as f64)); + assert_eq!(market_caps[1].market_cap, Some(9_007_199_254_740_992.0)); + assert_eq!(market_caps[2].market_cap, None); + assert!(serde_json::to_value(&market_caps[2]).unwrap()["marketCap"].is_null()); let company_floats: Vec = serde_json::from_str( r#"[ {"symbol":"AAPL","date":"2026-07-30 15:48:00","freeFloat":99.83,"floatShares":18446744073709551615,"outstandingShares":9007199254740993,"source":"https://www.sec.gov/example","future":{"nested":true}}, - {"symbol":"MSFT","date":"2026-07-30 15:48:00","freeFloat":98.1,"floatShares":1,"outstandingShares":2,"source":"https://www.sec.gov/example"} + {"symbol":"MSFT","date":"2026-07-30 15:48:00","freeFloat":98.1,"floatShares":1,"outstandingShares":2,"source":null} ]"#, ) .unwrap(); assert_eq!(company_floats.len(), 2); + assert_eq!(company_floats[1].source, None); assert_eq!(company_floats[0].float_shares, u64::MAX as f64); assert_eq!( company_floats[0].outstanding_shares, @@ -123,11 +127,16 @@ fn market_data_arrays_preserve_empty_multiple_unknown_fields_and_large_values() let all_floats: Vec = serde_json::from_str( r#"[ {"symbol":"000001.SZ","date":"2026-07-29 14:23:30","freeFloat":41.409,"floatShares":18446744073709551615,"outstandingShares":9007199254740993,"future":null}, - {"symbol":"000002.SZ","date":"2026-07-29 14:23:30","freeFloat":42.0,"floatShares":3,"outstandingShares":4} + {"symbol":"000002.SZ","date":"2026-07-29 14:23:30","freeFloat":null,"floatShares":null,"outstandingShares":4} ]"#, ) .unwrap(); assert_eq!(all_floats.len(), 2); - assert_eq!(all_floats[0].float_shares, u64::MAX as f64); + assert_eq!(all_floats[0].float_shares, Some(u64::MAX as f64)); + assert_eq!(all_floats[1].free_float, None); + assert_eq!(all_floats[1].float_shares, None); + let null_wire = serde_json::to_value(&all_floats[1]).unwrap(); + assert!(null_wire["freeFloat"].is_null()); + assert!(null_wire["floatShares"].is_null()); assert_eq!(all_floats[0].outstanding_shares, 9_007_199_254_740_992.0); } diff --git a/crates/libfmp/tests/company_mergers_acquisitions_endpoints.rs b/crates/libfmp/tests/company_mergers_acquisitions_endpoints.rs index dfed1a11..1799b7e5 100644 --- a/crates/libfmp/tests/company_mergers_acquisitions_endpoints.rs +++ b/crates/libfmp/tests/company_mergers_acquisitions_endpoints.rs @@ -79,7 +79,10 @@ async fn proxy_client_preserves_omission_order_zero_above_bound_and_name_encodin .await .unwrap(); assert_eq!(latest[0].cik.as_str(), "0002009312"); - assert_eq!(latest[0].targeted_cik.as_str(), "0002009312"); + assert_eq!( + latest[0].targeted_cik.as_ref().unwrap().as_str(), + "0002009312" + ); assert_eq!( latest[0].targeted_company_name, "Aureus Greenway Holdings, Inc." @@ -144,7 +147,7 @@ async fn merger_acquisition_arrays_preserve_multiple_empty_and_unknown_field_pay .unwrap(); assert_eq!(multiple.len(), 2); assert_eq!(multiple[1].symbol.as_str(), "PEGY"); - assert_eq!(multiple[1].targeted_symbol.as_str(), "JCS"); + assert_eq!(multiple[1].targeted_symbol, None); assert_eq!(multiple[1].transaction_date.to_string(), "2021-11-12"); assert_eq!(multiple[1].accepted_date.to_string(), "2021-11-12 09:54:22"); diff --git a/crates/libfmp/tests/company_mergers_acquisitions_responses.rs b/crates/libfmp/tests/company_mergers_acquisitions_responses.rs index 4da80e9f..62b89409 100644 --- a/crates/libfmp/tests/company_mergers_acquisitions_responses.rs +++ b/crates/libfmp/tests/company_mergers_acquisitions_responses.rs @@ -15,8 +15,8 @@ fn documented_latest_merger_acquisition_decodes_every_exact_field_and_wire_type( assert_eq!(row.company_name, "Aureus Greenway Holdings Inc"); assert_eq!(row.cik.as_str(), "0002009312"); assert_eq!(row.targeted_company_name, "Aureus Greenway Holdings, Inc."); - assert_eq!(row.targeted_cik.as_str(), "0002009312"); - assert_eq!(row.targeted_symbol.as_str(), "PUSA"); + assert_eq!(row.targeted_cik.as_ref().unwrap().as_str(), "0002009312"); + assert_eq!(row.targeted_symbol.as_ref().unwrap().as_str(), "PUSA"); assert_eq!(row.transaction_date.to_string(), "2026-07-29"); assert_eq!(row.accepted_date.to_string(), "2026-07-29 16:00:46"); assert_eq!( @@ -34,8 +34,8 @@ fn documented_search_merger_acquisition_preserves_leading_zero_ciks_names_and_ur assert_eq!(row.company_name, "Pineapple Energy Inc."); assert_eq!(row.cik.as_str(), "0000022701"); assert_eq!(row.targeted_company_name, "Communications Systems, Inc."); - assert_eq!(row.targeted_cik.as_str(), "0000022701"); - assert_eq!(row.targeted_symbol.as_str(), "JCS"); + assert_eq!(row.targeted_cik.as_ref().unwrap().as_str(), "0000022701"); + assert_eq!(row.targeted_symbol.as_ref().unwrap().as_str(), "JCS"); assert_eq!(row.transaction_date.to_string(), "2021-11-12"); assert_eq!(row.accepted_date.to_string(), "2021-11-12 09:54:22"); assert_eq!( @@ -52,8 +52,15 @@ fn merger_acquisition_arrays_preserve_empty_multiple_and_unknown_field_shapes() assert!(empty.is_empty()); assert_eq!(multiple.len(), 2); - assert_eq!(multiple[0].targeted_symbol.as_str(), "PUSA"); - assert_eq!(multiple[1].targeted_symbol.as_str(), "JCS"); + assert_eq!( + multiple[0].targeted_symbol.as_ref().unwrap().as_str(), + "PUSA" + ); + assert_eq!(multiple[1].targeted_cik, None); + assert_eq!(multiple[1].targeted_symbol, None); + let wire = serde_json::to_value(&multiple[1]).unwrap(); + assert!(wire["targetedCik"].is_null()); + assert!(wire["targetedSymbol"].is_null()); assert_eq!(unknown.len(), 1); assert_eq!(unknown[0].cik.as_str(), "0002009312"); } diff --git a/crates/libfmp/tests/company_responses.rs b/crates/libfmp/tests/company_responses.rs index bab9d3e1..3fc23030 100644 --- a/crates/libfmp/tests/company_responses.rs +++ b/crates/libfmp/tests/company_responses.rs @@ -19,9 +19,9 @@ fn documented_company_profile_decodes_every_exact_field_and_wire_type() { assert_eq!(row.average_volume, 55_309_000.0); assert_eq!(row.company_name, "Apple Inc."); assert_eq!(row.currency.as_str(), "USD"); - assert_eq!(row.cik.as_str(), "0000320193"); + assert_eq!(row.cik.as_ref().unwrap().as_str(), "0000320193"); assert_eq!(row.isin.as_str(), "US0378331005"); - assert_eq!(row.cusip.as_str(), "037833100"); + assert_eq!(row.cusip.as_ref().unwrap().as_str(), "037833100"); assert_eq!(row.exchange_full_name, "NASDAQ Global Select"); assert_eq!(row.exchange.as_str(), "NASDAQ"); assert_eq!(row.industry.as_str(), "Consumer Electronics"); @@ -33,8 +33,8 @@ fn documented_company_profile_decodes_every_exact_field_and_wire_type() { assert_eq!(row.ceo, "Timothy D. Cook"); assert_eq!(row.sector.as_str(), "Technology"); assert_eq!(row.country.as_str(), "US"); - assert_eq!(row.full_time_employees.as_str(), "166000"); - assert_eq!(row.phone, "(408) 996-1010"); + assert_eq!(row.full_time_employees.as_ref().unwrap().as_str(), "166000"); + assert_eq!(row.phone.as_deref(), Some("(408) 996-1010")); assert_eq!(row.address, "One Apple Park Way"); assert_eq!(row.city, "Cupertino"); assert_eq!(row.state, "CA"); @@ -43,7 +43,7 @@ fn documented_company_profile_decodes_every_exact_field_and_wire_type() { row.image, "https://images.financialmodelingprep.com/symbol/AAPL.png" ); - assert_eq!(row.ipo_date.to_string(), "1980-12-12"); + assert_eq!(row.ipo_date.unwrap().to_string(), "1980-12-12"); assert!(!row.default_image); assert!(!row.is_etf); assert!(row.is_actively_trading); @@ -77,7 +77,7 @@ fn documented_company_note_and_stock_peer_preserve_exact_names() { assert_eq!(notes[0].cik.as_str(), "0000320193"); assert_eq!(notes[0].symbol.as_str(), "AAPL"); assert_eq!(notes[0].title, "0.000% Notes due 2025"); - assert_eq!(notes[0].exchange.as_str(), "NASDAQ"); + assert_eq!(notes[0].exchange.as_ref().unwrap().as_str(), "NASDAQ"); assert_eq!(peers.len(), 1); assert_eq!(peers[0].symbol.as_str(), "GOOGL"); @@ -119,12 +119,34 @@ fn company_arrays_preserve_empty_multiple_unknown_and_large_values() { assert!(profiles[0].market_cap >= 2_f64.powi(53)); assert_eq!(profiles[0].volume, u64::MAX as f64); assert_eq!(profiles[0].average_volume, 4_294_967_296.0); - assert_eq!(profiles[0].full_time_employees.as_str(), "42"); + assert_eq!(profiles[0].cik, None); + assert_eq!(profiles[0].cusip, None); + assert_eq!(profiles[0].full_time_employees, None); + assert_eq!(profiles[0].phone, None); + assert_eq!(profiles[0].ipo_date, None); let large_wire = serde_json::to_value(&profiles[0]).unwrap(); - assert_eq!(large_wire["fullTimeEmployees"], "42"); - assert!(large_wire["fullTimeEmployees"].is_string()); + assert!(large_wire["cik"].is_null()); + assert!(large_wire["fullTimeEmployees"].is_null()); + assert!(large_wire["ipoDate"].is_null()); assert_eq!(profiles[0].sector.as_str(), "Future Sector"); assert_eq!(profiles[0].industry.as_str(), "Future Industry"); assert_eq!(unknown.len(), 1); assert_eq!(unknown[0].symbol.as_str(), "AAPL"); } + +#[test] +fn company_note_decodes_a_null_exchange_as_none() { + let notes: Vec = serde_json::from_str( + r#"[{"cik":"0001577552","symbol":"BABA","title":"2.125% Notes due 2031","exchange":null}]"#, + ) + .unwrap(); + + assert_eq!(notes[0].exchange, None); + assert!(serde_json::to_value(¬es[0]).unwrap()["exchange"].is_null()); + assert!( + serde_json::from_str::>( + r#"[{"cik":"0001577552","symbol":"BABA","title":"t"}]"# + ) + .is_err() + ); +} diff --git a/crates/libfmp/tests/fixtures/company_executive_compensation_large.json b/crates/libfmp/tests/fixtures/company_executive_compensation_large.json index 4e8cd2d9..94c3b945 100644 --- a/crates/libfmp/tests/fixtures/company_executive_compensation_large.json +++ b/crates/libfmp/tests/fixtures/company_executive_compensation_large.json @@ -26,7 +26,7 @@ "incentivePlanCompensation": 9000000000, "link": "https://example.test/filing", "nameAndPosition": "Synthetic Executive", - "optionAward": 8000000000, + "optionAward": null, "salary": 5000000000, "stockAward": 7000000000, "symbol": "BIG", diff --git a/crates/libfmp/tests/fixtures/company_key_executives_dynamic.json b/crates/libfmp/tests/fixtures/company_key_executives_dynamic.json index f2ebdd82..1574ad86 100644 --- a/crates/libfmp/tests/fixtures/company_key_executives_dynamic.json +++ b/crates/libfmp/tests/fixtures/company_key_executives_dynamic.json @@ -12,7 +12,7 @@ { "active": false, "currencyPay": "XTS", - "gender": "unspecified", + "gender": null, "name": "Representation Probe", "pay": { "amount": "00123.450", diff --git a/crates/libfmp/tests/fixtures/company_mergers_acquisitions_multiple.json b/crates/libfmp/tests/fixtures/company_mergers_acquisitions_multiple.json index d9a9aaef..e46f4797 100644 --- a/crates/libfmp/tests/fixtures/company_mergers_acquisitions_multiple.json +++ b/crates/libfmp/tests/fixtures/company_mergers_acquisitions_multiple.json @@ -16,9 +16,9 @@ "companyName": "Pineapple Energy Inc.", "link": "https://www.sec.gov/Archives/edgar/data/22701/000089710121000932/a211292_s-4.htm", "symbol": "PEGY", - "targetedCik": "0000022701", + "targetedCik": null, "targetedCompanyName": "Communications Systems, Inc.", - "targetedSymbol": "JCS", + "targetedSymbol": null, "transactionDate": "2021-11-12" } ] diff --git a/crates/libfmp/tests/fixtures/company_profile_multiple.json b/crates/libfmp/tests/fixtures/company_profile_multiple.json index e63a3f29..a71bd35b 100644 --- a/crates/libfmp/tests/fixtures/company_profile_multiple.json +++ b/crates/libfmp/tests/fixtures/company_profile_multiple.json @@ -6,20 +6,20 @@ "ceo": "A. Person", "change": 0.25, "changePercentage": 20, - "cik": "0000000001", + "cik": null, "city": "Testville", "companyName": "Large Values Inc.", "country": "ZZ", "currency": "XTS", - "cusip": "000000001", + "cusip": null, "defaultImage": true, "description": "Large integer fixture.", "exchange": "FUTURE", "exchangeFullName": "Future Exchange", - "fullTimeEmployees": "42", + "fullTimeEmployees": null, "image": "https://example.com/image.png", "industry": "Future Industry", - "ipoDate": "2026-08-27", + "ipoDate": "", "isActivelyTrading": false, "isAdr": true, "isEtf": true, @@ -27,7 +27,7 @@ "isin": "TEST00000001", "lastDividend": 0, "marketCap": 9007199254740993, - "phone": "0", + "phone": null, "price": 1.5, "range": "0-2", "sector": "Future Sector", diff --git a/sdk/go/bulk_parity_test.go b/sdk/go/bulk_parity_test.go index cf97dc66..60bb6b16 100644 --- a/sdk/go/bulk_parity_test.go +++ b/sdk/go/bulk_parity_test.go @@ -42,7 +42,7 @@ func TestBulkFixturesDecodeAndReencodeToTheSameMemberSet(t *testing.T) { func TestDocumentedBulkSnapshotsDecodeExactValues(t *testing.T) { t.Parallel() profiles := assertFixtureParity[CompanyProfile](t, "bulk_company_profiles.json") - if len(profiles) != 1 || profiles[0].Symbol != "AAPL" || profiles[0].CIK != "0000320193" { + if len(profiles) != 1 || profiles[0].Symbol != "AAPL" || profiles[0].CIK == nil || *profiles[0].CIK != "0000320193" { t.Fatalf("bulk_company_profiles = %+v", profiles) } ratings := assertFixtureParity[BulkStockRating](t, "bulk_stock_ratings.json") diff --git a/sdk/go/company_models.go b/sdk/go/company_models.go index 2e28789e..d76ec3de 100644 --- a/sdk/go/company_models.go +++ b/sdk/go/company_models.go @@ -25,9 +25,9 @@ type CompanyProfile struct { AverageVolume float64 `json:"averageVolume"` CompanyName string `json:"companyName"` Currency string `json:"currency"` - CIK string `json:"cik"` + CIK *string `json:"cik"` ISIN string `json:"isin"` - CUSIP string `json:"cusip"` + CUSIP *string `json:"cusip"` ExchangeFullName string `json:"exchangeFullName"` Exchange string `json:"exchange"` Industry string `json:"industry"` @@ -36,14 +36,14 @@ type CompanyProfile struct { Ceo string `json:"ceo"` Sector string `json:"sector"` Country string `json:"country"` - FullTimeEmployees string `json:"fullTimeEmployees"` - Phone string `json:"phone"` + FullTimeEmployees *string `json:"fullTimeEmployees"` + Phone *string `json:"phone"` Address string `json:"address"` City string `json:"city"` State string `json:"state"` Zip string `json:"zip"` Image string `json:"image"` - IPODate Date `json:"ipoDate"` + IPODate *Date `json:"ipoDate"` DefaultImage bool `json:"defaultImage"` IsETF bool `json:"isEtf"` IsActivelyTrading bool `json:"isActivelyTrading"` @@ -55,42 +55,42 @@ type CompanyProfile struct { // every required member so a missing or null member is observable after // decoding. type companyProfileShadow struct { - Symbol *string `json:"symbol"` - Price *float64 `json:"price"` - MarketCap *float64 `json:"marketCap"` - Beta *float64 `json:"beta"` - LastDividend *float64 `json:"lastDividend"` - Range *string `json:"range"` - Change *float64 `json:"change"` - ChangePercentage *float64 `json:"changePercentage"` - Volume *float64 `json:"volume"` - AverageVolume *float64 `json:"averageVolume"` - CompanyName *string `json:"companyName"` - Currency *string `json:"currency"` - CIK *string `json:"cik"` - ISIN *string `json:"isin"` - CUSIP *string `json:"cusip"` - ExchangeFullName *string `json:"exchangeFullName"` - Exchange *string `json:"exchange"` - Industry *string `json:"industry"` - Website *string `json:"website"` - Description *string `json:"description"` - Ceo *string `json:"ceo"` - Sector *string `json:"sector"` - Country *string `json:"country"` - FullTimeEmployees *string `json:"fullTimeEmployees"` - Phone *string `json:"phone"` - Address *string `json:"address"` - City *string `json:"city"` - State *string `json:"state"` - Zip *string `json:"zip"` - Image *string `json:"image"` - IPODate *Date `json:"ipoDate"` - DefaultImage *bool `json:"defaultImage"` - IsETF *bool `json:"isEtf"` - IsActivelyTrading *bool `json:"isActivelyTrading"` - IsAdr *bool `json:"isAdr"` - IsFund *bool `json:"isFund"` + Symbol *string `json:"symbol"` + Price *float64 `json:"price"` + MarketCap *float64 `json:"marketCap"` + Beta *float64 `json:"beta"` + LastDividend *float64 `json:"lastDividend"` + Range *string `json:"range"` + Change *float64 `json:"change"` + ChangePercentage *float64 `json:"changePercentage"` + Volume *float64 `json:"volume"` + AverageVolume *float64 `json:"averageVolume"` + CompanyName *string `json:"companyName"` + Currency *string `json:"currency"` + CIK jsontext.Value `json:"cik"` + ISIN *string `json:"isin"` + CUSIP jsontext.Value `json:"cusip"` + ExchangeFullName *string `json:"exchangeFullName"` + Exchange *string `json:"exchange"` + Industry *string `json:"industry"` + Website *string `json:"website"` + Description *string `json:"description"` + Ceo *string `json:"ceo"` + Sector *string `json:"sector"` + Country *string `json:"country"` + FullTimeEmployees jsontext.Value `json:"fullTimeEmployees"` + Phone jsontext.Value `json:"phone"` + Address *string `json:"address"` + City *string `json:"city"` + State *string `json:"state"` + Zip *string `json:"zip"` + Image *string `json:"image"` + IPODate jsontext.Value `json:"ipoDate"` + DefaultImage *bool `json:"defaultImage"` + IsETF *bool `json:"isEtf"` + IsActivelyTrading *bool `json:"isActivelyTrading"` + IsAdr *bool `json:"isAdr"` + IsFund *bool `json:"isFund"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -126,11 +126,11 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "companyName") case shadow.Currency == nil: return missingMemberError("CompanyProfile", "currency") - case shadow.CIK == nil: + case len(shadow.CIK) == 0: return missingMemberError("CompanyProfile", "cik") case shadow.ISIN == nil: return missingMemberError("CompanyProfile", "isin") - case shadow.CUSIP == nil: + case len(shadow.CUSIP) == 0: return missingMemberError("CompanyProfile", "cusip") case shadow.ExchangeFullName == nil: return missingMemberError("CompanyProfile", "exchangeFullName") @@ -148,9 +148,9 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "sector") case shadow.Country == nil: return missingMemberError("CompanyProfile", "country") - case shadow.FullTimeEmployees == nil: + case len(shadow.FullTimeEmployees) == 0: return missingMemberError("CompanyProfile", "fullTimeEmployees") - case shadow.Phone == nil: + case len(shadow.Phone) == 0: return missingMemberError("CompanyProfile", "phone") case shadow.Address == nil: return missingMemberError("CompanyProfile", "address") @@ -162,7 +162,7 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyProfile", "zip") case shadow.Image == nil: return missingMemberError("CompanyProfile", "image") - case shadow.IPODate == nil: + case len(shadow.IPODate) == 0: return missingMemberError("CompanyProfile", "ipoDate") case shadow.DefaultImage == nil: return missingMemberError("CompanyProfile", "defaultImage") @@ -175,6 +175,42 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.IsFund == nil: return missingMemberError("CompanyProfile", "isFund") } + var cik *string + if shadow.CIK.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.CIK, &value); err != nil { + return err + } + cik = &value + } + var cusip *string + if shadow.CUSIP.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.CUSIP, &value); err != nil { + return err + } + cusip = &value + } + var fullTimeEmployees *string + if shadow.FullTimeEmployees.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.FullTimeEmployees, &value); err != nil { + return err + } + fullTimeEmployees = &value + } + var phone *string + if shadow.Phone.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Phone, &value); err != nil { + return err + } + phone = &value + } + ipoDate, err := decodeEmptyDate("CompanyProfile", "ipoDate", shadow.IPODate, true) + if err != nil { + return err + } *m = CompanyProfile{ Symbol: *shadow.Symbol, Price: *shadow.Price, @@ -188,9 +224,9 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { AverageVolume: *shadow.AverageVolume, CompanyName: *shadow.CompanyName, Currency: *shadow.Currency, - CIK: *shadow.CIK, + CIK: cik, ISIN: *shadow.ISIN, - CUSIP: *shadow.CUSIP, + CUSIP: cusip, ExchangeFullName: *shadow.ExchangeFullName, Exchange: *shadow.Exchange, Industry: *shadow.Industry, @@ -199,14 +235,14 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Ceo: *shadow.Ceo, Sector: *shadow.Sector, Country: *shadow.Country, - FullTimeEmployees: *shadow.FullTimeEmployees, - Phone: *shadow.Phone, + FullTimeEmployees: fullTimeEmployees, + Phone: phone, Address: *shadow.Address, City: *shadow.City, State: *shadow.State, Zip: *shadow.Zip, Image: *shadow.Image, - IPODate: *shadow.IPODate, + IPODate: ipoDate, DefaultImage: *shadow.DefaultImage, IsETF: *shadow.IsETF, IsActivelyTrading: *shadow.IsActivelyTrading, @@ -218,19 +254,19 @@ func (m *CompanyProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // CompanyNote is one US company-issued note. type CompanyNote struct { - CIK string `json:"cik"` - Symbol string `json:"symbol"` - Title string `json:"title"` - Exchange string `json:"exchange"` + CIK string `json:"cik"` + Symbol string `json:"symbol"` + Title string `json:"title"` + Exchange *string `json:"exchange"` } // companyNoteShadow mirrors CompanyNote with a pointer or raw value for every // required member so a missing or null member is observable after decoding. type companyNoteShadow struct { - CIK *string `json:"cik"` - Symbol *string `json:"symbol"` - Title *string `json:"title"` - Exchange *string `json:"exchange"` + CIK *string `json:"cik"` + Symbol *string `json:"symbol"` + Title *string `json:"title"` + Exchange jsontext.Value `json:"exchange"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -248,14 +284,22 @@ func (m *CompanyNote) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyNote", "symbol") case shadow.Title == nil: return missingMemberError("CompanyNote", "title") - case shadow.Exchange == nil: + case len(shadow.Exchange) == 0: return missingMemberError("CompanyNote", "exchange") } + var exchange *string + if shadow.Exchange.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Exchange, &value); err != nil { + return err + } + exchange = &value + } *m = CompanyNote{ CIK: *shadow.CIK, Symbol: *shadow.Symbol, Title: *shadow.Title, - Exchange: *shadow.Exchange, + Exchange: exchange, } return nil } @@ -431,18 +475,18 @@ func (m *EmployeeCount) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // CompanyMarketCapitalization is one current or historical worldwide // market-capitalization observation. type CompanyMarketCapitalization struct { - Symbol string `json:"symbol"` - Date Date `json:"date"` - MarketCap float64 `json:"marketCap"` + Symbol string `json:"symbol"` + Date Date `json:"date"` + MarketCap *float64 `json:"marketCap"` } // companyMarketCapitalizationShadow mirrors CompanyMarketCapitalization with a // pointer or raw value for every required member so a missing or null member // is observable after decoding. type companyMarketCapitalizationShadow struct { - Symbol *string `json:"symbol"` - Date *Date `json:"date"` - MarketCap *float64 `json:"marketCap"` + Symbol *string `json:"symbol"` + Date *Date `json:"date"` + MarketCap jsontext.Value `json:"marketCap"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -458,13 +502,21 @@ func (m *CompanyMarketCapitalization) UnmarshalJSONFrom(dec *jsontext.Decoder) e return missingMemberError("CompanyMarketCapitalization", "symbol") case shadow.Date == nil: return missingMemberError("CompanyMarketCapitalization", "date") - case shadow.MarketCap == nil: + case len(shadow.MarketCap) == 0: return missingMemberError("CompanyMarketCapitalization", "marketCap") } + var marketCap *float64 + if shadow.MarketCap.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.MarketCap, &value); err != nil { + return err + } + marketCap = &value + } *m = CompanyMarketCapitalization{ Symbol: *shadow.Symbol, Date: *shadow.Date, - MarketCap: *shadow.MarketCap, + MarketCap: marketCap, } return nil } @@ -477,19 +529,19 @@ type CompanyShareFloat struct { FreeFloat float64 `json:"freeFloat"` FloatShares float64 `json:"floatShares"` OutstandingShares float64 `json:"outstandingShares"` - Source string `json:"source"` + Source *string `json:"source"` } // companyShareFloatShadow mirrors CompanyShareFloat with a pointer or raw // value for every required member so a missing or null member is observable // after decoding. type companyShareFloatShadow struct { - Symbol *string `json:"symbol"` - Date *DateTime `json:"date"` - FreeFloat *float64 `json:"freeFloat"` - FloatShares *float64 `json:"floatShares"` - OutstandingShares *float64 `json:"outstandingShares"` - Source *string `json:"source"` + Symbol *string `json:"symbol"` + Date *DateTime `json:"date"` + FreeFloat *float64 `json:"freeFloat"` + FloatShares *float64 `json:"floatShares"` + OutstandingShares *float64 `json:"outstandingShares"` + Source jsontext.Value `json:"source"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -511,16 +563,24 @@ func (m *CompanyShareFloat) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyShareFloat", "floatShares") case shadow.OutstandingShares == nil: return missingMemberError("CompanyShareFloat", "outstandingShares") - case shadow.Source == nil: + case len(shadow.Source) == 0: return missingMemberError("CompanyShareFloat", "source") } + var source *string + if shadow.Source.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Source, &value); err != nil { + return err + } + source = &value + } *m = CompanyShareFloat{ Symbol: *shadow.Symbol, Date: *shadow.Date, FreeFloat: *shadow.FreeFloat, FloatShares: *shadow.FloatShares, OutstandingShares: *shadow.OutstandingShares, - Source: *shadow.Source, + Source: source, } return nil } @@ -530,19 +590,19 @@ func (m *CompanyShareFloat) UnmarshalJSONFrom(dec *jsontext.Decoder) error { type ShareFloat struct { Symbol string `json:"symbol"` Date DateTime `json:"date"` - FreeFloat float64 `json:"freeFloat"` - FloatShares float64 `json:"floatShares"` + FreeFloat *float64 `json:"freeFloat"` + FloatShares *float64 `json:"floatShares"` OutstandingShares float64 `json:"outstandingShares"` } // shareFloatShadow mirrors ShareFloat with a pointer or raw value for every // required member so a missing or null member is observable after decoding. type shareFloatShadow struct { - Symbol *string `json:"symbol"` - Date *DateTime `json:"date"` - FreeFloat *float64 `json:"freeFloat"` - FloatShares *float64 `json:"floatShares"` - OutstandingShares *float64 `json:"outstandingShares"` + Symbol *string `json:"symbol"` + Date *DateTime `json:"date"` + FreeFloat jsontext.Value `json:"freeFloat"` + FloatShares jsontext.Value `json:"floatShares"` + OutstandingShares *float64 `json:"outstandingShares"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -558,18 +618,34 @@ func (m *ShareFloat) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("ShareFloat", "symbol") case shadow.Date == nil: return missingMemberError("ShareFloat", "date") - case shadow.FreeFloat == nil: + case len(shadow.FreeFloat) == 0: return missingMemberError("ShareFloat", "freeFloat") - case shadow.FloatShares == nil: + case len(shadow.FloatShares) == 0: return missingMemberError("ShareFloat", "floatShares") case shadow.OutstandingShares == nil: return missingMemberError("ShareFloat", "outstandingShares") } + var freeFloat *float64 + if shadow.FreeFloat.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.FreeFloat, &value); err != nil { + return err + } + freeFloat = &value + } + var floatShares *float64 + if shadow.FloatShares.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.FloatShares, &value); err != nil { + return err + } + floatShares = &value + } *m = ShareFloat{ Symbol: *shadow.Symbol, Date: *shadow.Date, - FreeFloat: *shadow.FreeFloat, - FloatShares: *shadow.FloatShares, + FreeFloat: freeFloat, + FloatShares: floatShares, OutstandingShares: *shadow.OutstandingShares, } return nil @@ -582,8 +658,8 @@ type MergerAcquisition struct { CompanyName string `json:"companyName"` CIK string `json:"cik"` TargetedCompanyName string `json:"targetedCompanyName"` - TargetedCIK string `json:"targetedCik"` - TargetedSymbol string `json:"targetedSymbol"` + TargetedCIK *string `json:"targetedCik"` + TargetedSymbol *string `json:"targetedSymbol"` TransactionDate Date `json:"transactionDate"` AcceptedDate DateTime `json:"acceptedDate"` Link string `json:"link"` @@ -593,15 +669,15 @@ type MergerAcquisition struct { // value for every required member so a missing or null member is observable // after decoding. type mergerAcquisitionShadow struct { - Symbol *string `json:"symbol"` - CompanyName *string `json:"companyName"` - CIK *string `json:"cik"` - TargetedCompanyName *string `json:"targetedCompanyName"` - TargetedCIK *string `json:"targetedCik"` - TargetedSymbol *string `json:"targetedSymbol"` - TransactionDate *Date `json:"transactionDate"` - AcceptedDate *DateTime `json:"acceptedDate"` - Link *string `json:"link"` + Symbol *string `json:"symbol"` + CompanyName *string `json:"companyName"` + CIK *string `json:"cik"` + TargetedCompanyName *string `json:"targetedCompanyName"` + TargetedCIK jsontext.Value `json:"targetedCik"` + TargetedSymbol jsontext.Value `json:"targetedSymbol"` + TransactionDate *Date `json:"transactionDate"` + AcceptedDate *DateTime `json:"acceptedDate"` + Link *string `json:"link"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -621,9 +697,9 @@ func (m *MergerAcquisition) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("MergerAcquisition", "cik") case shadow.TargetedCompanyName == nil: return missingMemberError("MergerAcquisition", "targetedCompanyName") - case shadow.TargetedCIK == nil: + case len(shadow.TargetedCIK) == 0: return missingMemberError("MergerAcquisition", "targetedCik") - case shadow.TargetedSymbol == nil: + case len(shadow.TargetedSymbol) == 0: return missingMemberError("MergerAcquisition", "targetedSymbol") case shadow.TransactionDate == nil: return missingMemberError("MergerAcquisition", "transactionDate") @@ -632,13 +708,29 @@ func (m *MergerAcquisition) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.Link == nil: return missingMemberError("MergerAcquisition", "link") } + var targetedCIK *string + if shadow.TargetedCIK.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.TargetedCIK, &value); err != nil { + return err + } + targetedCIK = &value + } + var targetedSymbol *string + if shadow.TargetedSymbol.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.TargetedSymbol, &value); err != nil { + return err + } + targetedSymbol = &value + } *m = MergerAcquisition{ Symbol: *shadow.Symbol, CompanyName: *shadow.CompanyName, CIK: *shadow.CIK, TargetedCompanyName: *shadow.TargetedCompanyName, - TargetedCIK: *shadow.TargetedCIK, - TargetedSymbol: *shadow.TargetedSymbol, + TargetedCIK: targetedCIK, + TargetedSymbol: targetedSymbol, TransactionDate: *shadow.TransactionDate, AcceptedDate: *shadow.AcceptedDate, Link: *shadow.Link, @@ -653,7 +745,7 @@ type CompanyExecutive struct { Name string `json:"name"` Pay *jsontext.Value `json:"pay"` CurrencyPay string `json:"currencyPay"` - Gender string `json:"gender"` + Gender *string `json:"gender"` YearBorn *jsontext.Value `json:"yearBorn"` TitleSince *jsontext.Value `json:"titleSince"` Active bool `json:"active"` @@ -667,7 +759,7 @@ type companyExecutiveShadow struct { Name *string `json:"name"` Pay *jsontext.Value `json:"pay"` CurrencyPay *string `json:"currencyPay"` - Gender *string `json:"gender"` + Gender jsontext.Value `json:"gender"` YearBorn *jsontext.Value `json:"yearBorn"` TitleSince *jsontext.Value `json:"titleSince"` Active *bool `json:"active"` @@ -688,17 +780,25 @@ func (m *CompanyExecutive) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CompanyExecutive", "name") case shadow.CurrencyPay == nil: return missingMemberError("CompanyExecutive", "currencyPay") - case shadow.Gender == nil: + case len(shadow.Gender) == 0: return missingMemberError("CompanyExecutive", "gender") case shadow.Active == nil: return missingMemberError("CompanyExecutive", "active") } + var gender *string + if shadow.Gender.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Gender, &value); err != nil { + return err + } + gender = &value + } *m = CompanyExecutive{ Title: *shadow.Title, Name: *shadow.Name, Pay: shadow.Pay, CurrencyPay: *shadow.CurrencyPay, - Gender: *shadow.Gender, + Gender: gender, YearBorn: shadow.YearBorn, TitleSince: shadow.TitleSince, Active: *shadow.Active, @@ -719,7 +819,7 @@ type ExecutiveCompensation struct { Salary float64 `json:"salary"` Bonus float64 `json:"bonus"` StockAward float64 `json:"stockAward"` - OptionAward float64 `json:"optionAward"` + OptionAward *float64 `json:"optionAward"` IncentivePlanCompensation float64 `json:"incentivePlanCompensation"` AllOtherCompensation float64 `json:"allOtherCompensation"` Total float64 `json:"total"` @@ -730,21 +830,21 @@ type ExecutiveCompensation struct { // raw value for every required member so a missing or null member is // observable after decoding. type executiveCompensationShadow struct { - CIK *string `json:"cik"` - Symbol *string `json:"symbol"` - CompanyName *string `json:"companyName"` - FilingDate *Date `json:"filingDate"` - AcceptedDate *DateTime `json:"acceptedDate"` - NameAndPosition *string `json:"nameAndPosition"` - Year *int64 `json:"year"` - Salary *float64 `json:"salary"` - Bonus *float64 `json:"bonus"` - StockAward *float64 `json:"stockAward"` - OptionAward *float64 `json:"optionAward"` - IncentivePlanCompensation *float64 `json:"incentivePlanCompensation"` - AllOtherCompensation *float64 `json:"allOtherCompensation"` - Total *float64 `json:"total"` - Link *string `json:"link"` + CIK *string `json:"cik"` + Symbol *string `json:"symbol"` + CompanyName *string `json:"companyName"` + FilingDate *Date `json:"filingDate"` + AcceptedDate *DateTime `json:"acceptedDate"` + NameAndPosition *string `json:"nameAndPosition"` + Year *int64 `json:"year"` + Salary *float64 `json:"salary"` + Bonus *float64 `json:"bonus"` + StockAward *float64 `json:"stockAward"` + OptionAward jsontext.Value `json:"optionAward"` + IncentivePlanCompensation *float64 `json:"incentivePlanCompensation"` + AllOtherCompensation *float64 `json:"allOtherCompensation"` + Total *float64 `json:"total"` + Link *string `json:"link"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -776,7 +876,7 @@ func (m *ExecutiveCompensation) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("ExecutiveCompensation", "bonus") case shadow.StockAward == nil: return missingMemberError("ExecutiveCompensation", "stockAward") - case shadow.OptionAward == nil: + case len(shadow.OptionAward) == 0: return missingMemberError("ExecutiveCompensation", "optionAward") case shadow.IncentivePlanCompensation == nil: return missingMemberError("ExecutiveCompensation", "incentivePlanCompensation") @@ -787,6 +887,14 @@ func (m *ExecutiveCompensation) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.Link == nil: return missingMemberError("ExecutiveCompensation", "link") } + var optionAward *float64 + if shadow.OptionAward.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.OptionAward, &value); err != nil { + return err + } + optionAward = &value + } *m = ExecutiveCompensation{ CIK: *shadow.CIK, Symbol: *shadow.Symbol, @@ -798,7 +906,7 @@ func (m *ExecutiveCompensation) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Salary: *shadow.Salary, Bonus: *shadow.Bonus, StockAward: *shadow.StockAward, - OptionAward: *shadow.OptionAward, + OptionAward: optionAward, IncentivePlanCompensation: *shadow.IncentivePlanCompensation, AllOtherCompensation: *shadow.AllOtherCompensation, Total: *shadow.Total, diff --git a/sdk/go/company_parity_test.go b/sdk/go/company_parity_test.go index f556570d..8c50bbf0 100644 --- a/sdk/go/company_parity_test.go +++ b/sdk/go/company_parity_test.go @@ -4,6 +4,7 @@ import ( "encoding/json/jsontext" "encoding/json/v2" "errors" + "reflect" "strings" "testing" ) @@ -52,15 +53,15 @@ func TestDocumentedCompanyProfileNoteAndPeerDecodeExactValues(t *testing.T) { want := CompanyProfile{ Symbol: "AAPL", Price: 331.85501, MarketCap: 4_874_072_686_740, Beta: 1.097, LastDividend: 1.05, Range: "201.5-344.57", Change: -6.33498, ChangePercentage: -1.8732, Volume: 28_718_014, - AverageVolume: 55_309_000, CompanyName: "Apple Inc.", Currency: "USD", CIK: "0000320193", - ISIN: "US0378331005", CUSIP: "037833100", ExchangeFullName: "NASDAQ Global Select", Exchange: "NASDAQ", + AverageVolume: 55_309_000, CompanyName: "Apple Inc.", Currency: "USD", CIK: new("0000320193"), + ISIN: "US0378331005", CUSIP: new("037833100"), ExchangeFullName: "NASDAQ Global Select", Exchange: "NASDAQ", Industry: "Consumer Electronics", Website: "https://www.apple.com", Description: profile.Description, - Ceo: "Timothy D. Cook", Sector: "Technology", Country: "US", FullTimeEmployees: "166000", - Phone: "(408) 996-1010", Address: "One Apple Park Way", City: "Cupertino", State: "CA", Zip: "95014", - Image: "https://images.financialmodelingprep.com/symbol/AAPL.png", IPODate: mustParseDate(t, "1980-12-12"), + Ceo: "Timothy D. Cook", Sector: "Technology", Country: "US", FullTimeEmployees: new("166000"), + Phone: new("(408) 996-1010"), Address: "One Apple Park Way", City: "Cupertino", State: "CA", Zip: "95014", + Image: "https://images.financialmodelingprep.com/symbol/AAPL.png", IPODate: new(mustParseDate(t, "1980-12-12")), DefaultImage: false, IsETF: false, IsActivelyTrading: true, IsAdr: false, IsFund: false, } - if profile != want { + if !reflect.DeepEqual(profile, want) { t.Fatalf("profile = %+v, want %+v", profile, want) } if !strings.HasPrefix(profile.Description, "Apple Inc. is a global technology corporation") { @@ -74,7 +75,8 @@ func TestDocumentedCompanyProfileNoteAndPeerDecodeExactValues(t *testing.T) { multiple := assertFixtureParity[CompanyProfile](t, "company_profile_multiple.json") if len(multiple) != 2 || multiple[0].MarketCap != 9_007_199_254_740_993 || multiple[0].Volume != 18_446_744_073_709_551_615 || - multiple[0].AverageVolume != 4_294_967_296 || multiple[0].FullTimeEmployees != "42" || + multiple[0].AverageVolume != 4_294_967_296 || multiple[0].FullTimeEmployees != nil || + multiple[0].CIK != nil || multiple[0].CUSIP != nil || multiple[0].Phone != nil || multiple[0].IPODate != nil || multiple[0].Sector != "Future Sector" || multiple[0].Industry != "Future Industry" { t.Fatalf("company_profile_multiple = %+v", multiple) } @@ -87,8 +89,8 @@ func TestDocumentedCompanyProfileNoteAndPeerDecodeExactValues(t *testing.T) { } notes := assertFixtureParity[CompanyNote](t, "company_note.json") - if want := (CompanyNote{CIK: "0000320193", Symbol: "AAPL", Title: "0.000% Notes due 2025", Exchange: "NASDAQ"}); len(notes) != 1 || - notes[0] != want { + if want := (CompanyNote{CIK: "0000320193", Symbol: "AAPL", Title: "0.000% Notes due 2025", Exchange: new("NASDAQ")}); len(notes) != 1 || + !reflect.DeepEqual(notes[0], want) { t.Fatalf("company_note = %+v", notes) } peers := assertFixtureParity[StockPeer](t, "stock_peer.json") @@ -120,24 +122,24 @@ func TestDocumentedWorkforceAndMarketDataFixturesDecodeExactValues(t *testing.T) } caps := assertFixtureParity[CompanyMarketCapitalization](t, "company_market_capitalization.json") - if want := (CompanyMarketCapitalization{Symbol: "AAPL", Date: mustParseDate(t, "2026-07-30"), MarketCap: 4_874_072_686_740}); len(caps) != 1 || - caps[0] != want { + if want := (CompanyMarketCapitalization{Symbol: "AAPL", Date: mustParseDate(t, "2026-07-30"), MarketCap: new(4_874_072_686_740.0)}); len(caps) != 1 || + !reflect.DeepEqual(caps[0], want) { t.Fatalf("company_market_capitalization = %+v", caps) } historical := assertFixtureParity[CompanyMarketCapitalization](t, "company_historical_market_capitalization.json") - if len(historical) != 1 || historical[0].Symbol != "AAPL" || historical[0].MarketCap != 4_879_177_245_542 { + if len(historical) != 1 || historical[0].Symbol != "AAPL" || historical[0].MarketCap == nil || *historical[0].MarketCap != 4_879_177_245_542 { t.Fatalf("company_historical_market_capitalization = %+v", historical) } floats := assertFixtureParity[CompanyShareFloat](t, "company_shares_float.json") if want := (CompanyShareFloat{Symbol: "AAPL", Date: mustParseDateTime(t, "2026-07-30 15:48:00"), FreeFloat: 99.83000000136171, FloatShares: 14_662_387_495, OutstandingShares: 14_687_356_000, - Source: "https://www.sec.gov/Archives/edgar/data/320193/000032019326000013/aapl-20260328.htm"}); len(floats) != 1 || - floats[0] != want { + Source: new("https://www.sec.gov/Archives/edgar/data/320193/000032019326000013/aapl-20260328.htm")}); len(floats) != 1 || + !reflect.DeepEqual(floats[0], want) { t.Fatalf("company_shares_float = %+v", floats) } all := assertFixtureParity[ShareFloat](t, "company_shares_float_all.json") - if want := (ShareFloat{Symbol: "000001.SZ", Date: mustParseDateTime(t, "2026-07-29 14:23:30"), FreeFloat: 41.40900000201062, - FloatShares: 8_035_796_667, OutstandingShares: 19_405_918_198}); len(all) != 1 || all[0] != want { + if want := (ShareFloat{Symbol: "000001.SZ", Date: mustParseDateTime(t, "2026-07-29 14:23:30"), FreeFloat: new(41.40900000201062), + FloatShares: new(8_035_796_667.0), OutstandingShares: 19_405_918_198}); len(all) != 1 || !reflect.DeepEqual(all[0], want) { t.Fatalf("company_all_shares_float = %+v", all) } } @@ -148,33 +150,34 @@ func TestDocumentedMergerAndGovernanceFixturesDecodeExactValues(t *testing.T) { t.Parallel() latest := assertFixtureParity[MergerAcquisition](t, "company_mergers_acquisitions_latest.json") if want := (MergerAcquisition{Symbol: "AGH", CompanyName: "Aureus Greenway Holdings Inc", CIK: "0002009312", - TargetedCompanyName: "Aureus Greenway Holdings, Inc.", TargetedCIK: "0002009312", TargetedSymbol: "PUSA", + TargetedCompanyName: "Aureus Greenway Holdings, Inc.", TargetedCIK: new("0002009312"), TargetedSymbol: new("PUSA"), TransactionDate: mustParseDate(t, "2026-07-29"), AcceptedDate: mustParseDateTime(t, "2026-07-29 16:00:46"), Link: "https://www.sec.gov/Archives/edgar/data/2009312/000149315226035181/forms-4.htm"}); len(latest) != 1 || - latest[0] != want { + !reflect.DeepEqual(latest[0], want) { t.Fatalf("company_latest_mergers_acquisitions = %+v", latest) } search := assertFixtureParity[MergerAcquisition](t, "company_mergers_acquisitions_search.json") - if len(search) != 1 || search[0].Symbol != "PEGY" || search[0].CIK != "0000022701" || search[0].TargetedSymbol != "JCS" || + if len(search) != 1 || search[0].Symbol != "PEGY" || search[0].CIK != "0000022701" || search[0].TargetedSymbol == nil || *search[0].TargetedSymbol != "JCS" || search[0].TransactionDate != mustParseDate(t, "2021-11-12") { t.Fatalf("company_search_mergers_acquisitions = %+v", search) } multiple := assertFixtureParity[MergerAcquisition](t, "company_mergers_acquisitions_multiple.json") - if len(multiple) != 2 || multiple[0].TargetedSymbol != "PUSA" || multiple[1].TargetedSymbol != "JCS" { + if len(multiple) != 2 || multiple[0].TargetedSymbol == nil || *multiple[0].TargetedSymbol != "PUSA" || + multiple[1].TargetedCIK != nil || multiple[1].TargetedSymbol != nil { t.Fatalf("company_mergers_acquisitions_multiple = %+v", multiple) } executives := assertFixtureParity[CompanyExecutive](t, "company_key_executives.json") if len(executives) != 1 || executives[0].Title != "Vice President of Worldwide Communications" || executives[0].Name != "Kristin Huguet Quayle" || executives[0].CurrencyPay != "USD" || - executives[0].Gender != "female" || !executives[0].Active { + executives[0].Gender == nil || *executives[0].Gender != "female" || !executives[0].Active { t.Fatalf("company_key_executives = %+v", executives) } if executives[0].Pay != nil || executives[0].YearBorn != nil || executives[0].TitleSince != nil { t.Fatalf("null dynamic members decoded as non-nil: %+v", executives[0]) } dynamic := assertFixtureParity[CompanyExecutive](t, "company_key_executives_dynamic.json") - if len(dynamic) != 2 || dynamic[1].Active { + if len(dynamic) != 2 || dynamic[1].Active || dynamic[1].Gender != nil { t.Fatalf("company_key_executives_dynamic = %+v", dynamic) } assertCanonicalJSON(t, dynamic[1].Pay, `{"amount":"00123.450","components":[1,true,null]}`) @@ -185,15 +188,15 @@ func TestDocumentedMergerAndGovernanceFixturesDecodeExactValues(t *testing.T) { if want := (ExecutiveCompensation{CIK: "0000320193", Symbol: "AAPL", CompanyName: "Apple Inc.", FilingDate: mustParseDate(t, "2026-01-08"), AcceptedDate: mustParseDateTime(t, "2026-01-08 16:31:36"), NameAndPosition: "Luca Maestri Former Senior Vice President, Chief Financial Officer", Year: 2025, Salary: 819_231, - Bonus: 0, StockAward: 13_003_031, OptionAward: 0, IncentivePlanCompensation: 1_638_462, + Bonus: 0, StockAward: 13_003_031, OptionAward: new(0.0), IncentivePlanCompensation: 1_638_462, AllOtherCompensation: 22_204, Total: 15_482_928, Link: "https://www.sec.gov/Archives/edgar/data/320193/000130817926000008/0001308179-26-000008-index.htm"}); len(compensation) != 1 || - compensation[0] != want { + !reflect.DeepEqual(compensation[0], want) { t.Fatalf("company_executive_compensation = %+v", compensation) } large := assertFixtureParity[ExecutiveCompensation](t, "company_executive_compensation_large.json") if len(large) != 2 || large[1].Salary != 5_000_000_000 || large[1].AllOtherCompensation != 10_000_000_000 || - large[1].Total != 18_446_744_073_709_551_615 { + large[1].Total != 18_446_744_073_709_551_615 || large[1].OptionAward != nil { t.Fatalf("company_executive_compensation_large = %+v", large) } benchmark := assertFixtureParity[ExecutiveCompensationBenchmark](t, "company_executive_compensation_benchmark.json") @@ -219,7 +222,7 @@ func TestCompanyRequiredMembersAreEnforcedLikeSerde(t *testing.T) { member string }{ {"missing member", `[{"symbol":"AAPL","marketCap":1}]`, "date"}, - {"null member", `[{"symbol":"AAPL","date":"2026-07-30","marketCap":null}]`, "marketCap"}, + {"null member", `[{"symbol":"AAPL","date":null,"marketCap":1}]`, "date"}, {"empty object", `[{}]`, "symbol"}, {"null element", `[null]`, "symbol"}, } @@ -236,6 +239,34 @@ func TestCompanyRequiredMembersAreEnforcedLikeSerde(t *testing.T) { assertCompanyDecodeError(t, json.Unmarshal([]byte(wire), &executives), "CompanyExecutive", "active") } +// Members FMP sends as null decode to nil like serde's None, while a missing +// key still fails as a missing required member. +func TestCompanyNullableMembersDecodeNullAsNil(t *testing.T) { + t.Parallel() + var notes []CompanyNote + if err := json.Unmarshal([]byte(`[{"cik":"0001577552","symbol":"BABA","title":"t","exchange":null}]`), ¬es); err != nil || + notes[0].Exchange != nil { + t.Fatalf("company note = %+v, %v", notes, err) + } + var caps []CompanyMarketCapitalization + if err := json.Unmarshal([]byte(`[{"symbol":"EURUSD","date":"2026-07-30","marketCap":null}]`), &caps); err != nil || + caps[0].MarketCap != nil { + t.Fatalf("market cap = %+v, %v", caps, err) + } + var floats []CompanyShareFloat + wire := `[{"symbol":"SPY","date":"2026-07-30 15:48:00","freeFloat":1,"floatShares":1,"outstandingShares":1,"source":null}]` + if err := json.Unmarshal([]byte(wire), &floats); err != nil || floats[0].Source != nil { + t.Fatalf("company share float = %+v, %v", floats, err) + } + var all []ShareFloat + wire = `[{"symbol":"0050.TW","date":"2026-07-29 14:23:30","freeFloat":null,"floatShares":null,"outstandingShares":1}]` + if err := json.Unmarshal([]byte(wire), &all); err != nil || all[0].FreeFloat != nil || all[0].FloatShares != nil { + t.Fatalf("share float = %+v, %v", all, err) + } + err := json.Unmarshal([]byte(`[{"cik":"0001577552","symbol":"BABA","title":"t"}]`), ¬es) + assertCompanyDecodeError(t, err, "CompanyNote", "exchange") +} + func assertCompanyDecodeError(t *testing.T, err error, model, member string) { t.Helper() var typed *Error diff --git a/sdk/go/company_test.go b/sdk/go/company_test.go index 5b5a525d..4b0bfa88 100644 --- a/sdk/go/company_test.go +++ b/sdk/go/company_test.go @@ -49,7 +49,7 @@ func TestCompanyMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { ctx := context.Background() profile, err := client.Company.Profile(ctx, NewProfileQuery("AAPL")) - if err != nil || len(profile) != 1 || profile[0].CIK != "0000320193" { + if err != nil || len(profile) != 1 || profile[0].CIK == nil || *profile[0].CIK != "0000320193" { t.Fatalf("Profile = %+v, %v", profile, err) } byCIK, err := client.Company.ProfileByCIK(ctx, NewProfileByCIKQuery("0000320193")) @@ -57,11 +57,11 @@ func TestCompanyMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { t.Fatalf("ProfileByCIK = %+v, %v", byCIK, err) } batch, err := client.Company.BatchMarketCapitalization(ctx, NewBatchMarketCapitalizationQuery([]string{"AAPL", "MSFT"})) - if err != nil || len(batch) != 1 || batch[0].MarketCap != 4_874_072_686_740 { + if err != nil || len(batch) != 1 || batch[0].MarketCap == nil || *batch[0].MarketCap != 4_874_072_686_740 { t.Fatalf("BatchMarketCapitalization = %+v, %v", batch, err) } search, err := client.Company.SearchMergersAcquisitions(ctx, NewSearchMergersAcquisitionsQuery("Pineapple Energy")) - if err != nil || len(search) != 1 || search[0].TargetedSymbol != "JCS" { + if err != nil || len(search) != 1 || search[0].TargetedSymbol == nil || *search[0].TargetedSymbol != "JCS" { t.Fatalf("SearchMergersAcquisitions = %+v, %v", search, err) } @@ -82,7 +82,7 @@ func TestCompanyMethodsUseExactPathsAndWireParameterOrder(t *testing.T) { to := mustParseDate(t, "2026-07-16") history, err := client.Company.HistoricalMarketCapitalization(ctx, NewHistoricalMarketCapitalizationQuery("AAPL").WithFrom(from)) - if err != nil || len(history) != 1 || history[0].MarketCap != 4_879_177_245_542 { + if err != nil || len(history) != 1 || history[0].MarketCap == nil || *history[0].MarketCap != 4_879_177_245_542 { t.Fatalf("HistoricalMarketCapitalization from = %+v, %v", history, err) } reversed, err := client.Company.HistoricalMarketCapitalization(ctx,