From 847d702b4075c5f7221e7e6afad40e4f7be52bbe Mon Sep 17 00:00:00 2001 From: benbenbang Date: Wed, 30 Sep 2026 20:56:50 +0800 Subject: [PATCH] feat(calendar): make omittable calendar members optional Members that FMP can omit or send as null are now optional across the calendar models so consumers observe None instead of receiving a wrong or missing value. **Optional member changes:** * `DividendEvent.record_date` and `payment_date` are now `Option` / pointer / `Optional`, decoded with `empty_or_null_date` so both `""` and `null` become None. * `EarningsEvent.eps_estimated` and `revenue_estimated` are now optional and decode null as None. * `IpoCalendarEvent.exchange` is now optional. * `IpoProspectus.discounts_and_commissions_per_share` and `discounts_and_commissions_total` are now optional. * `StockSplitEvent.split_type` is now optional. **Cross-language parity:** * Updated the Rust `libfmp` responses, the PyO3 models and stubs, and the Go SDK shadow decoders to expose these members as nullable. **Tests:** * Added Rust, Python, and Go cases asserting the newly optional members decode as None, and updated existing assertions to the optional shapes. BREAKING CHANGE: DividendEvent, EarningsEvent, IpoCalendarEvent, IpoProspectus and StockSplitEvent members FMP can omit are now optional (Option / pointer / Optional); handle None. --- .../python/fmp/_native/calendar/__init__.pyi | 34 ++-- crates/fmp-py/src/models/calendar.rs | 38 ++-- crates/fmp-py/tests/test_calendar_a.py | 36 ++++ crates/fmp-py/tests/test_calendar_b.py | 16 ++ crates/fmp-py/tests/typecheck_contract.py | 3 +- crates/libfmp/src/responses/calendar.rs | 28 ++- .../tests/calendar_earnings_endpoints.rs | 4 +- .../libfmp/tests/calendar_ipos_endpoints.rs | 4 +- crates/libfmp/tests/calendar_responses.rs | 68 +++++-- crates/libfmp/tests/integral_f64_responses.rs | 2 +- sdk/go/calendar_models.go | 172 ++++++++++++------ sdk/go/calendar_parity_test.go | 76 +++++--- sdk/go/integral_f64_parity_test.go | 2 +- 13 files changed, 325 insertions(+), 158 deletions(-) diff --git a/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi b/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi index cf59aaed..37301da4 100644 --- a/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/calendar/__init__.pyi @@ -131,9 +131,9 @@ class DividendEvent: @property def date(self) -> datetime.date: ... @property - def record_date(self) -> datetime.date: ... + def record_date(self) -> typing.Optional[datetime.date]: ... @property - def payment_date(self) -> datetime.date: ... + def payment_date(self) -> typing.Optional[datetime.date]: ... @property def declaration_date(self) -> typing.Optional[datetime.date]: ... @property @@ -150,8 +150,8 @@ class DividendEvent: *, symbol: builtins.str, date: datetime.date, - record_date: datetime.date, - payment_date: datetime.date, + record_date: typing.Optional[datetime.date], + payment_date: typing.Optional[datetime.date], declaration_date: typing.Optional[datetime.date], adj_dividend: builtins.float, dividend: builtins.float, @@ -187,11 +187,11 @@ class EarningsEvent: @property def eps_actual(self) -> typing.Optional[builtins.float]: ... @property - def eps_estimated(self) -> builtins.float: ... + def eps_estimated(self) -> typing.Optional[builtins.float]: ... @property def revenue_actual(self) -> typing.Optional[builtins.float]: ... @property - def revenue_estimated(self) -> builtins.float: ... + def revenue_estimated(self) -> typing.Optional[builtins.float]: ... @property def last_updated(self) -> datetime.date: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... @@ -201,9 +201,9 @@ class EarningsEvent: symbol: builtins.str, date: datetime.date, eps_actual: typing.Optional[builtins.float], - eps_estimated: builtins.float, + eps_estimated: typing.Optional[builtins.float], revenue_actual: typing.Optional[builtins.float], - revenue_estimated: builtins.float, + revenue_estimated: typing.Optional[builtins.float], last_updated: datetime.date, ) -> EarningsEvent: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... @@ -239,7 +239,7 @@ class IpoCalendarEvent: @property def company(self) -> builtins.str: ... @property - def exchange(self) -> builtins.str: ... + def exchange(self) -> typing.Optional[builtins.str]: ... @property def actions(self) -> builtins.str: ... @property @@ -256,7 +256,7 @@ class IpoCalendarEvent: date: datetime.date, daa: builtins.str, company: builtins.str, - exchange: builtins.str, + exchange: typing.Optional[builtins.str], actions: builtins.str, shares: typing.Optional[builtins.str], price_range: typing.Optional[builtins.str], @@ -353,9 +353,11 @@ class IpoProspectus: @property def price_public_total(self) -> builtins.float: ... @property - def discounts_and_commissions_per_share(self) -> builtins.float: ... + def discounts_and_commissions_per_share( + self, + ) -> typing.Optional[builtins.float]: ... @property - def discounts_and_commissions_total(self) -> builtins.float: ... + def discounts_and_commissions_total(self) -> typing.Optional[builtins.float]: ... @property def proceeds_before_expenses_per_share(self) -> builtins.float: ... @property @@ -375,8 +377,8 @@ class IpoProspectus: cik: builtins.str, price_public_per_share: builtins.float, price_public_total: builtins.float, - discounts_and_commissions_per_share: builtins.float, - discounts_and_commissions_total: builtins.float, + discounts_and_commissions_per_share: typing.Optional[builtins.float], + discounts_and_commissions_total: typing.Optional[builtins.float], proceeds_before_expenses_per_share: builtins.float, proceeds_before_expenses_total: builtins.float, form: builtins.str, @@ -411,7 +413,7 @@ class StockSplitEvent: @property def denominator(self) -> builtins.float: ... @property - def split_type(self) -> builtins.str: ... + def split_type(self) -> typing.Optional[builtins.str]: ... def __eq__(self, other: builtins.object, /) -> builtins.bool: ... def __new__( cls, @@ -420,7 +422,7 @@ class StockSplitEvent: date: datetime.date, numerator: builtins.float, denominator: builtins.float, - split_type: builtins.str, + split_type: typing.Optional[builtins.str], ) -> StockSplitEvent: ... def __getnewargs_ex__(self) -> tuple[tuple, dict]: ... def __repr__(self) -> builtins.str: ... diff --git a/crates/fmp-py/src/models/calendar.rs b/crates/fmp-py/src/models/calendar.rs index c7a92103..9ae1d3d3 100644 --- a/crates/fmp-py/src/models/calendar.rs +++ b/crates/fmp-py/src/models/calendar.rs @@ -14,9 +14,9 @@ pub(crate) struct DividendEvent { #[pyo3(get)] pub date: ::chrono::NaiveDate, #[pyo3(get)] - pub record_date: ::chrono::NaiveDate, + pub record_date: Option<::chrono::NaiveDate>, #[pyo3(get)] - pub payment_date: ::chrono::NaiveDate, + pub payment_date: Option<::chrono::NaiveDate>, #[pyo3(get)] pub declaration_date: Option<::chrono::NaiveDate>, #[pyo3(get)] @@ -39,8 +39,8 @@ impl DividendEvent { fn new( symbol: String, date: ::chrono::NaiveDate, - record_date: ::chrono::NaiveDate, - payment_date: ::chrono::NaiveDate, + record_date: Option<::chrono::NaiveDate>, + payment_date: Option<::chrono::NaiveDate>, declaration_date: Option<::chrono::NaiveDate>, adj_dividend: f64, dividend: f64, @@ -191,8 +191,8 @@ impl From for DividendEvent { Self { symbol: value.symbol.into_inner(), date: value.date.into_inner(), - record_date: value.record_date.into_inner(), - payment_date: value.payment_date.into_inner(), + record_date: value.record_date.map(|value| value.into_inner()), + payment_date: value.payment_date.map(|value| value.into_inner()), declaration_date: value.declaration_date.map(|value| value.into_inner()), adj_dividend: value.adj_dividend, dividend: value.dividend, @@ -219,11 +219,11 @@ pub(crate) struct EarningsEvent { #[pyo3(get)] pub eps_actual: Option, #[pyo3(get)] - pub eps_estimated: f64, + pub eps_estimated: Option, #[pyo3(get)] pub revenue_actual: Option, #[pyo3(get)] - pub revenue_estimated: f64, + pub revenue_estimated: Option, #[pyo3(get)] pub last_updated: ::chrono::NaiveDate, } @@ -239,9 +239,9 @@ impl EarningsEvent { symbol: String, date: ::chrono::NaiveDate, eps_actual: Option, - eps_estimated: f64, + eps_estimated: Option, revenue_actual: Option, - revenue_estimated: f64, + revenue_estimated: Option, last_updated: ::chrono::NaiveDate, ) -> Self { Self { @@ -400,7 +400,7 @@ pub(crate) struct IpoCalendarEvent { #[pyo3(get)] pub company: String, #[pyo3(get)] - pub exchange: String, + pub exchange: Option, #[pyo3(get)] pub actions: String, shares: Option<::serde_json::Value>, @@ -420,7 +420,7 @@ impl IpoCalendarEvent { date: ::chrono::NaiveDate, daa: String, company: String, - exchange: String, + exchange: Option, actions: String, shares: Option, price_range: Option, @@ -629,7 +629,7 @@ impl From for IpoCalendarEvent { date: value.date.into_inner(), daa: value.daa.as_str().to_owned(), company: value.company, - exchange: value.exchange.into_inner(), + exchange: value.exchange.map(|value| value.into_inner()), actions: value.actions, shares: value.shares, price_range: value.price_range, @@ -836,9 +836,9 @@ pub(crate) struct IpoProspectus { #[pyo3(get)] pub price_public_total: f64, #[pyo3(get)] - pub discounts_and_commissions_per_share: f64, + pub discounts_and_commissions_per_share: Option, #[pyo3(get)] - pub discounts_and_commissions_total: f64, + pub discounts_and_commissions_total: Option, #[pyo3(get)] pub proceeds_before_expenses_per_share: f64, #[pyo3(get)] @@ -864,8 +864,8 @@ impl IpoProspectus { cik: String, price_public_per_share: f64, price_public_total: f64, - discounts_and_commissions_per_share: f64, - discounts_and_commissions_total: f64, + discounts_and_commissions_per_share: Option, + discounts_and_commissions_total: Option, proceeds_before_expenses_per_share: f64, proceeds_before_expenses_total: f64, form: String, @@ -1133,7 +1133,7 @@ pub(crate) struct StockSplitEvent { #[pyo3(get)] pub denominator: f64, #[pyo3(get)] - pub split_type: String, + pub split_type: Option, } #[gen_stub_pymethods] @@ -1148,7 +1148,7 @@ impl StockSplitEvent { date: ::chrono::NaiveDate, numerator: f64, denominator: f64, - split_type: String, + split_type: Option, ) -> Self { Self { symbol, diff --git a/crates/fmp-py/tests/test_calendar_a.py b/crates/fmp-py/tests/test_calendar_a.py index 75d001b8..d03eeca9 100644 --- a/crates/fmp-py/tests/test_calendar_a.py +++ b/crates/fmp-py/tests/test_calendar_a.py @@ -214,3 +214,39 @@ def test_earnings_calendar_decodes_fractional_revenue(client: Any, fixture_serve assert isinstance(rows[0].revenue_estimated, float) assert rows[0].revenue_estimated == 1_086_300_000.5 assert rows[0].revenue_actual == 1_101_500_000.0 + + +@pytest.mark.parametrize("absent", ["", None]) +def test_dividends_calendar_absent_record_and_payment_dates_are_none( + client: Any, fixture_server: FixtureServer, absent: str | None +) -> None: + """Issue #368: ``""`` and ``null`` record / payment dates decode as ``None``.""" + body = load_fixture("dividends_calendar.json") + body[0]["recordDate"] = absent + body[0]["paymentDate"] = absent + fixture_server.route("/dividends-calendar", body) + row = client.calendar.dividends_calendar()[0] + + assert row.record_date is None + assert row.payment_date is None + + +def test_earnings_calendar_null_estimates_are_none(client: Any, fixture_server: FixtureServer) -> None: + """Issue #368: ``null`` EPS and revenue estimates decode as ``None``.""" + body = load_fixture("earnings_calendar.json") + body[0]["epsEstimated"] = None + body[0]["revenueEstimated"] = None + fixture_server.route("/earnings-calendar", body) + row = client.calendar.earnings_calendar()[0] + + assert row.eps_estimated is None + assert row.revenue_estimated is None + + +def test_stock_splits_calendar_null_split_type_is_none(client: Any, fixture_server: FixtureServer) -> None: + """Issue #368: a ``null`` split type decodes as ``None``.""" + body = load_fixture("stock_splits_calendar.json") + body[0]["splitType"] = None + fixture_server.route("/splits-calendar", body) + + assert client.calendar.stock_splits_calendar()[0].split_type is None diff --git a/crates/fmp-py/tests/test_calendar_b.py b/crates/fmp-py/tests/test_calendar_b.py index 70bb21e4..6daaccdc 100644 --- a/crates/fmp-py/tests/test_calendar_b.py +++ b/crates/fmp-py/tests/test_calendar_b.py @@ -95,6 +95,22 @@ def test_ipos_prospectus_with_from_only(client: Any, fixture_server: FixtureServ assert row.proceeds_before_expenses_total == pytest.approx(82_251.0) +def test_ipos_null_exchange_and_discounts_are_none(client: Any, fixture_server: FixtureServer) -> None: + """Issue #368: a ``null`` exchange and ``null`` discount values decode as ``None``.""" + calendar = load_fixture("ipos_calendar.json") + calendar[0]["exchange"] = None + fixture_server.route("/ipos-calendar", calendar) + prospectus = load_fixture("ipos_prospectus.json") + prospectus[0]["discountsAndCommissionsPerShare"] = None + prospectus[0]["discountsAndCommissionsTotal"] = None + fixture_server.route("/ipos-prospectus", prospectus) + + assert client.calendar.ipos_calendar()[0].exchange is None + row = client.calendar.ipos_prospectus()[0] + assert row.discounts_and_commissions_per_share is None + assert row.discounts_and_commissions_total is None + + def test_ipos_prospectus_with_both_dates(client: Any, fixture_server: FixtureServer) -> None: """``ipos_prospectus`` encodes both ISO strings in ``from``, ``to`` order.""" fixture_server.route("/ipos-prospectus", load_fixture("ipos_prospectus.json")) diff --git a/crates/fmp-py/tests/typecheck_contract.py b/crates/fmp-py/tests/typecheck_contract.py index 9642a0bc..013171f9 100644 --- a/crates/fmp-py/tests/typecheck_contract.py +++ b/crates/fmp-py/tests/typecheck_contract.py @@ -136,9 +136,10 @@ def check_calendar_contract(client: FmpClient) -> None: from_=datetime.date(2026, 4, 27), to="2026-07-26", page=0, include_report_times=True ) eps_actual: float | None = earnings[0].eps_actual + eps_estimated: float | None = earnings[0].eps_estimated ipos: list[IpoCalendarEvent] = calendar.ipos_calendar(from_="2026-03-06", to=datetime.date(2026, 6, 6)) ipo_date: datetime.date = ipos[0].date - _ = (yield_, declared, eps_actual, ipo_date) + _ = (yield_, declared, eps_actual, eps_estimated, ipo_date) def check_bulk_contract(client: FmpClient) -> None: diff --git a/crates/libfmp/src/responses/calendar.rs b/crates/libfmp/src/responses/calendar.rs index faf0c4a9..133d6048 100644 --- a/crates/libfmp/src/responses/calendar.rs +++ b/crates/libfmp/src/responses/calendar.rs @@ -22,8 +22,10 @@ where pub struct DividendEvent { pub symbol: Ticker, pub date: Date, - pub record_date: Date, - pub payment_date: Date, + #[serde(with = "empty_or_null_date")] + pub record_date: Option, + #[serde(with = "empty_or_null_date")] + pub payment_date: Option, #[serde(with = "empty_or_null_date")] pub declaration_date: Option, pub adj_dividend: Price, @@ -41,12 +43,14 @@ pub struct EarningsEvent { pub date: Date, #[serde(deserialize_with = "required_option")] pub eps_actual: Option, - pub eps_estimated: f64, + #[serde(deserialize_with = "required_option")] + pub eps_estimated: Option, #[serde(deserialize_with = "required_option")] #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] pub revenue_actual: Option, - #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] - pub revenue_estimated: MarketValue, + #[serde(deserialize_with = "required_option")] + #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] + pub revenue_estimated: Option, pub last_updated: Date, } @@ -59,7 +63,8 @@ pub struct IpoCalendarEvent { pub date: Date, pub daa: IsoTimestamp, pub company: String, - pub exchange: ExchangeCode, + #[serde(deserialize_with = "required_option")] + pub exchange: Option, pub actions: String, #[serde(deserialize_with = "required_option")] pub shares: Option, @@ -96,9 +101,11 @@ pub struct IpoProspectus { pub price_public_per_share: Price, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub price_public_total: MarketValue, - pub discounts_and_commissions_per_share: Price, - #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] - pub discounts_and_commissions_total: MarketValue, + #[serde(deserialize_with = "required_option")] + pub discounts_and_commissions_per_share: Option, + #[serde(deserialize_with = "required_option")] + #[serde(serialize_with = "crate::codecs::integral_f64::serialize_option")] + pub discounts_and_commissions_total: Option, pub proceeds_before_expenses_per_share: Price, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub proceeds_before_expenses_total: MarketValue, @@ -117,5 +124,6 @@ pub struct StockSplitEvent { pub numerator: SplitTerm, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub denominator: SplitTerm, - pub split_type: String, + #[serde(deserialize_with = "required_option")] + pub split_type: Option, } diff --git a/crates/libfmp/tests/calendar_earnings_endpoints.rs b/crates/libfmp/tests/calendar_earnings_endpoints.rs index fd59936c..b1f76061 100644 --- a/crates/libfmp/tests/calendar_earnings_endpoints.rs +++ b/crates/libfmp/tests/calendar_earnings_endpoints.rs @@ -125,12 +125,12 @@ async fn custom_proxy_preserves_query_states_order_auth_headers_and_bare_arrays( assert_eq!(omitted.len(), 1); assert_eq!(omitted[0].eps_actual, None); assert_eq!(omitted[0].revenue_actual, None); - assert_eq!(omitted[0].revenue_estimated, 109_038_900_000.0); + assert_eq!(omitted[0].revenue_estimated, Some(109_038_900_000.0)); assert_eq!(explicit_false, omitted); assert_eq!(calendar_omitted.len(), 1); assert_eq!(calendar_omitted[0].eps_actual, Some(0.549)); assert_eq!(calendar_omitted[0].revenue_actual, Some(1_101_500_000.0)); - assert_eq!(calendar_omitted[0].revenue_estimated, 1_086_300_000.0); + assert_eq!(calendar_omitted[0].revenue_estimated, Some(1_086_300_000.0)); assert_eq!(calendar_true, calendar_omitted); let requests = executor.requests(); diff --git a/crates/libfmp/tests/calendar_ipos_endpoints.rs b/crates/libfmp/tests/calendar_ipos_endpoints.rs index 6942840a..020acbd1 100644 --- a/crates/libfmp/tests/calendar_ipos_endpoints.rs +++ b/crates/libfmp/tests/calendar_ipos_endpoints.rs @@ -135,8 +135,8 @@ async fn custom_proxy_preserves_exact_queries_auth_headers_and_source_rows() { let offering_values: [f64; 6] = [ prospectus[0].price_public_per_share, prospectus[0].price_public_total, - prospectus[0].discounts_and_commissions_per_share, - prospectus[0].discounts_and_commissions_total, + prospectus[0].discounts_and_commissions_per_share.unwrap(), + prospectus[0].discounts_and_commissions_total.unwrap(), prospectus[0].proceeds_before_expenses_per_share, prospectus[0].proceeds_before_expenses_total, ]; diff --git a/crates/libfmp/tests/calendar_responses.rs b/crates/libfmp/tests/calendar_responses.rs index c2b9e379..3b0553b9 100644 --- a/crates/libfmp/tests/calendar_responses.rs +++ b/crates/libfmp/tests/calendar_responses.rs @@ -35,8 +35,8 @@ fn both_exact_dividend_fixtures_share_the_nine_field_row() { [DividendEvent { symbol: Ticker::new("AAPL").unwrap(), date: Date::from_str("2026-05-11").unwrap(), - record_date: Date::from_str("2026-05-11").unwrap(), - payment_date: Date::from_str("2026-05-14").unwrap(), + record_date: Some(Date::from_str("2026-05-11").unwrap()), + payment_date: Some(Date::from_str("2026-05-14").unwrap()), declaration_date: Some(Date::from_str("2026-04-30").unwrap()), adj_dividend: 0.27, dividend: 0.27, @@ -56,10 +56,24 @@ fn both_exact_dividend_fixtures_share_the_nine_field_row() { serde_json::from_value::>(null).unwrap()[0].declaration_date, None ); + + for member in ["recordDate", "paymentDate"] { + for absent in [serde_json::json!(""), serde_json::Value::Null] { + let mut row: serde_json::Value = serde_json::from_slice(DIVIDENDS_CALENDAR).unwrap(); + row[0][member] = absent; + let decoded = serde_json::from_value::>(row).unwrap(); + let value = if member == "recordDate" { + decoded[0].record_date + } else { + decoded[0].payment_date + }; + assert_eq!(value, None, "{member}"); + } + } } #[test] -fn both_exact_earnings_fixtures_preserve_only_nullable_actual_values() { +fn both_exact_earnings_fixtures_preserve_nullable_actual_and_estimated_values() { assert_field_count(EARNINGS, 7); assert_field_count(EARNINGS_CALENDAR, 7); @@ -70,9 +84,9 @@ fn both_exact_earnings_fixtures_preserve_only_nullable_actual_values() { symbol: Ticker::new("AAPL").unwrap(), date: Date::from_str("2026-07-30").unwrap(), eps_actual: None, - eps_estimated: 1.88, + eps_estimated: Some(1.88), revenue_actual: None, - revenue_estimated: 109_038_900_000.0, + revenue_estimated: Some(109_038_900_000.0), last_updated: Date::from_str("2026-07-30").unwrap(), }] ); @@ -80,9 +94,9 @@ fn both_exact_earnings_fixtures_preserve_only_nullable_actual_values() { let calendar: Vec = serde_json::from_slice(EARNINGS_CALENDAR).unwrap(); assert_eq!(calendar[0].symbol.as_str(), "GRG.L"); assert_eq!(calendar[0].eps_actual, Some(0.549)); - assert_eq!(calendar[0].eps_estimated, 0.501); + assert_eq!(calendar[0].eps_estimated, Some(0.501)); assert_eq!(calendar[0].revenue_actual, Some(1_101_500_000.0)); - assert_eq!(calendar[0].revenue_estimated, 1_086_300_000.0); + assert_eq!(calendar[0].revenue_estimated, Some(1_086_300_000.0)); } #[test] @@ -96,7 +110,7 @@ fn exact_ipo_calendar_fixture_preserves_literal_daa_and_source_only_nulls() { date: Date::from_str("2026-07-29").unwrap(), daa: IsoTimestamp::from_str("2026-07-29T04:00:00.000Z").unwrap(), company: "IMC Rare Earths Ltd".to_owned(), - exchange: ExchangeCode::new("NYSE").unwrap(), + exchange: Some(ExchangeCode::new("NYSE").unwrap()), actions: "Priced".to_owned(), shares: None, price_range: None, @@ -154,8 +168,8 @@ fn exact_ipo_prospectus_fixture_decodes_all_thirteen_fields() { cik: Cik::new("0002083125").unwrap(), price_public_per_share: 1.0, price_public_total: 434.0, - discounts_and_commissions_per_share: 0.0, - discounts_and_commissions_total: 82_251.0, + discounts_and_commissions_per_share: Some(0.0), + discounts_and_commissions_total: Some(82_251.0), proceeds_before_expenses_per_share: 1.0, proceeds_before_expenses_total: 82_251.0, form: "S-1".to_owned(), @@ -177,7 +191,7 @@ fn both_exact_stock_split_fixtures_share_the_five_field_integer_row() { date: Date::from_str("2020-08-31").unwrap(), numerator: 4.0, denominator: 1.0, - split_type: "stock-split".to_owned(), + split_type: Some("stock-split".to_owned()), }] ); @@ -189,15 +203,31 @@ fn both_exact_stock_split_fixtures_share_the_five_field_integer_row() { #[test] fn documented_fields_are_required_nullable_fields_stay_nullable_and_unknowns_are_accepted() { - assert_contract::(DIVIDENDS, &["declarationDate"]); - assert_contract::(DIVIDENDS_CALENDAR, &["declarationDate"]); - assert_contract::(EARNINGS, &["epsActual", "revenueActual"]); - assert_contract::(EARNINGS_CALENDAR, &["epsActual", "revenueActual"]); - assert_contract::(IPOS_CALENDAR, &["shares", "priceRange", "marketCap"]); + let dividend = ["recordDate", "paymentDate", "declarationDate"]; + assert_contract::(DIVIDENDS, ÷nd); + assert_contract::(DIVIDENDS_CALENDAR, ÷nd); + let earnings = [ + "epsActual", + "epsEstimated", + "revenueActual", + "revenueEstimated", + ]; + assert_contract::(EARNINGS, &earnings); + assert_contract::(EARNINGS_CALENDAR, &earnings); + assert_contract::( + IPOS_CALENDAR, + &["exchange", "shares", "priceRange", "marketCap"], + ); assert_contract::(IPOS_DISCLOSURE, &[]); - assert_contract::(IPOS_PROSPECTUS, &[]); - assert_contract::(STOCK_SPLITS, &[]); - assert_contract::(STOCK_SPLITS_CALENDAR, &[]); + assert_contract::( + IPOS_PROSPECTUS, + &[ + "discountsAndCommissionsPerShare", + "discountsAndCommissionsTotal", + ], + ); + assert_contract::(STOCK_SPLITS, &["splitType"]); + assert_contract::(STOCK_SPLITS_CALENDAR, &["splitType"]); } fn assert_contract(fixture: &[u8], nullable: &[&str]) { diff --git a/crates/libfmp/tests/integral_f64_responses.rs b/crates/libfmp/tests/integral_f64_responses.rs index 641a7f19..0693c480 100644 --- a/crates/libfmp/tests/integral_f64_responses.rs +++ b/crates/libfmp/tests/integral_f64_responses.rs @@ -117,7 +117,7 @@ fn regulation_d_amounts_decode_fractional_and_integral_float_forms() { fn earnings_revenue_decodes_fractional_and_integral_float_forms() { let rows: Vec = serde_json::from_slice(EARNINGS).unwrap(); - assert_eq!(rows[0].revenue_estimated, 1_086_300_000.5); + assert_eq!(rows[0].revenue_estimated, Some(1_086_300_000.5)); assert_eq!(rows[0].revenue_actual, Some(1_101_500_000.0)); let wire = serde_json::to_value(&rows).unwrap(); assert_eq!(wire[0]["revenueEstimated"], json!(1_086_300_000.5)); diff --git a/sdk/go/calendar_models.go b/sdk/go/calendar_models.go index 04180b15..8e300b47 100644 --- a/sdk/go/calendar_models.go +++ b/sdk/go/calendar_models.go @@ -14,8 +14,8 @@ import ( type DividendEvent struct { Symbol string `json:"symbol"` Date Date `json:"date"` - RecordDate Date `json:"recordDate"` - PaymentDate Date `json:"paymentDate"` + RecordDate *Date `json:"recordDate"` + PaymentDate *Date `json:"paymentDate"` DeclarationDate *Date `json:"declarationDate"` AdjDividend float64 `json:"adjDividend"` Dividend float64 `json:"dividend"` @@ -29,8 +29,8 @@ type DividendEvent struct { type dividendEventShadow struct { Symbol *string `json:"symbol"` Date *Date `json:"date"` - RecordDate *Date `json:"recordDate"` - PaymentDate *Date `json:"paymentDate"` + RecordDate jsontext.Value `json:"recordDate"` + PaymentDate jsontext.Value `json:"paymentDate"` DeclarationDate jsontext.Value `json:"declarationDate"` AdjDividend *float64 `json:"adjDividend"` Dividend *float64 `json:"dividend"` @@ -51,9 +51,9 @@ func (m *DividendEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("DividendEvent", "symbol") case shadow.Date == nil: return missingMemberError("DividendEvent", "date") - case shadow.RecordDate == nil: + case len(shadow.RecordDate) == 0: return missingMemberError("DividendEvent", "recordDate") - case shadow.PaymentDate == nil: + case len(shadow.PaymentDate) == 0: return missingMemberError("DividendEvent", "paymentDate") case len(shadow.DeclarationDate) == 0: return missingMemberError("DividendEvent", "declarationDate") @@ -66,6 +66,14 @@ func (m *DividendEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.Frequency == nil: return missingMemberError("DividendEvent", "frequency") } + recordDate, err := decodeEmptyDate("DividendEvent", "recordDate", shadow.RecordDate, true) + if err != nil { + return err + } + paymentDate, err := decodeEmptyDate("DividendEvent", "paymentDate", shadow.PaymentDate, true) + if err != nil { + return err + } declarationDate, err := decodeEmptyDate("DividendEvent", "declarationDate", shadow.DeclarationDate, true) if err != nil { return err @@ -73,8 +81,8 @@ func (m *DividendEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { *m = DividendEvent{ Symbol: *shadow.Symbol, Date: *shadow.Date, - RecordDate: *shadow.RecordDate, - PaymentDate: *shadow.PaymentDate, + RecordDate: recordDate, + PaymentDate: paymentDate, DeclarationDate: declarationDate, AdjDividend: *shadow.AdjDividend, Dividend: *shadow.Dividend, @@ -89,9 +97,9 @@ type EarningsEvent struct { Symbol string `json:"symbol"` Date Date `json:"date"` EPSActual *float64 `json:"epsActual"` - EPSEstimated float64 `json:"epsEstimated"` + EPSEstimated *float64 `json:"epsEstimated"` RevenueActual *float64 `json:"revenueActual"` - RevenueEstimated float64 `json:"revenueEstimated"` + RevenueEstimated *float64 `json:"revenueEstimated"` LastUpdated Date `json:"lastUpdated"` } @@ -102,9 +110,9 @@ type earningsEventShadow struct { Symbol *string `json:"symbol"` Date *Date `json:"date"` EPSActual jsontext.Value `json:"epsActual"` - EPSEstimated *float64 `json:"epsEstimated"` + EPSEstimated jsontext.Value `json:"epsEstimated"` RevenueActual jsontext.Value `json:"revenueActual"` - RevenueEstimated *float64 `json:"revenueEstimated"` + RevenueEstimated jsontext.Value `json:"revenueEstimated"` LastUpdated *Date `json:"lastUpdated"` } @@ -123,11 +131,11 @@ func (m *EarningsEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("EarningsEvent", "date") case len(shadow.EPSActual) == 0: return missingMemberError("EarningsEvent", "epsActual") - case shadow.EPSEstimated == nil: + case len(shadow.EPSEstimated) == 0: return missingMemberError("EarningsEvent", "epsEstimated") case len(shadow.RevenueActual) == 0: return missingMemberError("EarningsEvent", "revenueActual") - case shadow.RevenueEstimated == nil: + case len(shadow.RevenueEstimated) == 0: return missingMemberError("EarningsEvent", "revenueEstimated") case shadow.LastUpdated == nil: return missingMemberError("EarningsEvent", "lastUpdated") @@ -140,6 +148,14 @@ func (m *EarningsEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { } epsActual = &value } + var epsEstimated *float64 + if shadow.EPSEstimated.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.EPSEstimated, &value); err != nil { + return err + } + epsEstimated = &value + } var revenueActual *float64 if shadow.RevenueActual.Kind() != 'n' { var value float64 @@ -148,13 +164,21 @@ func (m *EarningsEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { } revenueActual = &value } + var revenueEstimated *float64 + if shadow.RevenueEstimated.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.RevenueEstimated, &value); err != nil { + return err + } + revenueEstimated = &value + } *m = EarningsEvent{ Symbol: *shadow.Symbol, Date: *shadow.Date, EPSActual: epsActual, - EPSEstimated: *shadow.EPSEstimated, + EPSEstimated: epsEstimated, RevenueActual: revenueActual, - RevenueEstimated: *shadow.RevenueEstimated, + RevenueEstimated: revenueEstimated, LastUpdated: *shadow.LastUpdated, } return nil @@ -166,7 +190,7 @@ type IPOCalendarEvent struct { Date Date `json:"date"` Daa string `json:"daa"` Company string `json:"company"` - Exchange string `json:"exchange"` + Exchange *string `json:"exchange"` Actions string `json:"actions"` Shares *jsontext.Value `json:"shares"` PriceRange *jsontext.Value `json:"priceRange"` @@ -181,7 +205,7 @@ type ipoCalendarEventShadow struct { Date *Date `json:"date"` Daa *string `json:"daa"` Company *string `json:"company"` - Exchange *string `json:"exchange"` + Exchange jsontext.Value `json:"exchange"` Actions *string `json:"actions"` Shares jsontext.Value `json:"shares"` PriceRange jsontext.Value `json:"priceRange"` @@ -205,7 +229,7 @@ func (m *IPOCalendarEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("IPOCalendarEvent", "daa") case shadow.Company == nil: return missingMemberError("IPOCalendarEvent", "company") - case shadow.Exchange == nil: + case len(shadow.Exchange) == 0: return missingMemberError("IPOCalendarEvent", "exchange") case shadow.Actions == nil: return missingMemberError("IPOCalendarEvent", "actions") @@ -216,6 +240,14 @@ func (m *IPOCalendarEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case len(shadow.MarketCap) == 0: return missingMemberError("IPOCalendarEvent", "marketCap") } + var exchange *string + if shadow.Exchange.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.Exchange, &value); err != nil { + return err + } + exchange = &value + } var shares *jsontext.Value if shadow.Shares.Kind() != 'n' { var value jsontext.Value @@ -245,7 +277,7 @@ func (m *IPOCalendarEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Date: *shadow.Date, Daa: *shadow.Daa, Company: *shadow.Company, - Exchange: *shadow.Exchange, + Exchange: exchange, Actions: *shadow.Actions, Shares: shares, PriceRange: priceRange, @@ -317,38 +349,38 @@ func (m *IPODisclosure) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // IPOProspectus is one US IPO prospectus filing and its documented offering // values. type IPOProspectus struct { - Symbol string `json:"symbol"` - AcceptedDate Date `json:"acceptedDate"` - FilingDate Date `json:"filingDate"` - IPODate Date `json:"ipoDate"` - CIK string `json:"cik"` - PricePublicPerShare float64 `json:"pricePublicPerShare"` - PricePublicTotal float64 `json:"pricePublicTotal"` - DiscountsAndCommissionsPerShare float64 `json:"discountsAndCommissionsPerShare"` - DiscountsAndCommissionsTotal float64 `json:"discountsAndCommissionsTotal"` - ProceedsBeforeExpensesPerShare float64 `json:"proceedsBeforeExpensesPerShare"` - ProceedsBeforeExpensesTotal float64 `json:"proceedsBeforeExpensesTotal"` - Form string `json:"form"` - URL string `json:"url"` + Symbol string `json:"symbol"` + AcceptedDate Date `json:"acceptedDate"` + FilingDate Date `json:"filingDate"` + IPODate Date `json:"ipoDate"` + CIK string `json:"cik"` + PricePublicPerShare float64 `json:"pricePublicPerShare"` + PricePublicTotal float64 `json:"pricePublicTotal"` + DiscountsAndCommissionsPerShare *float64 `json:"discountsAndCommissionsPerShare"` + DiscountsAndCommissionsTotal *float64 `json:"discountsAndCommissionsTotal"` + ProceedsBeforeExpensesPerShare float64 `json:"proceedsBeforeExpensesPerShare"` + ProceedsBeforeExpensesTotal float64 `json:"proceedsBeforeExpensesTotal"` + Form string `json:"form"` + URL string `json:"url"` } // ipoProspectusShadow mirrors IPOProspectus with a pointer or raw value for // every required member so a missing or null member is observable after // decoding. type ipoProspectusShadow struct { - Symbol *string `json:"symbol"` - AcceptedDate *Date `json:"acceptedDate"` - FilingDate *Date `json:"filingDate"` - IPODate *Date `json:"ipoDate"` - CIK *string `json:"cik"` - PricePublicPerShare *float64 `json:"pricePublicPerShare"` - PricePublicTotal *float64 `json:"pricePublicTotal"` - DiscountsAndCommissionsPerShare *float64 `json:"discountsAndCommissionsPerShare"` - DiscountsAndCommissionsTotal *float64 `json:"discountsAndCommissionsTotal"` - ProceedsBeforeExpensesPerShare *float64 `json:"proceedsBeforeExpensesPerShare"` - ProceedsBeforeExpensesTotal *float64 `json:"proceedsBeforeExpensesTotal"` - Form *string `json:"form"` - URL *string `json:"url"` + Symbol *string `json:"symbol"` + AcceptedDate *Date `json:"acceptedDate"` + FilingDate *Date `json:"filingDate"` + IPODate *Date `json:"ipoDate"` + CIK *string `json:"cik"` + PricePublicPerShare *float64 `json:"pricePublicPerShare"` + PricePublicTotal *float64 `json:"pricePublicTotal"` + DiscountsAndCommissionsPerShare jsontext.Value `json:"discountsAndCommissionsPerShare"` + DiscountsAndCommissionsTotal jsontext.Value `json:"discountsAndCommissionsTotal"` + ProceedsBeforeExpensesPerShare *float64 `json:"proceedsBeforeExpensesPerShare"` + ProceedsBeforeExpensesTotal *float64 `json:"proceedsBeforeExpensesTotal"` + Form *string `json:"form"` + URL *string `json:"url"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -374,9 +406,9 @@ func (m *IPOProspectus) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("IPOProspectus", "pricePublicPerShare") case shadow.PricePublicTotal == nil: return missingMemberError("IPOProspectus", "pricePublicTotal") - case shadow.DiscountsAndCommissionsPerShare == nil: + case len(shadow.DiscountsAndCommissionsPerShare) == 0: return missingMemberError("IPOProspectus", "discountsAndCommissionsPerShare") - case shadow.DiscountsAndCommissionsTotal == nil: + case len(shadow.DiscountsAndCommissionsTotal) == 0: return missingMemberError("IPOProspectus", "discountsAndCommissionsTotal") case shadow.ProceedsBeforeExpensesPerShare == nil: return missingMemberError("IPOProspectus", "proceedsBeforeExpensesPerShare") @@ -387,6 +419,22 @@ func (m *IPOProspectus) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.URL == nil: return missingMemberError("IPOProspectus", "url") } + var discountsAndCommissionsPerShare *float64 + if shadow.DiscountsAndCommissionsPerShare.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.DiscountsAndCommissionsPerShare, &value); err != nil { + return err + } + discountsAndCommissionsPerShare = &value + } + var discountsAndCommissionsTotal *float64 + if shadow.DiscountsAndCommissionsTotal.Kind() != 'n' { + var value float64 + if err := json.Unmarshal(shadow.DiscountsAndCommissionsTotal, &value); err != nil { + return err + } + discountsAndCommissionsTotal = &value + } *m = IPOProspectus{ Symbol: *shadow.Symbol, AcceptedDate: *shadow.AcceptedDate, @@ -395,8 +443,8 @@ func (m *IPOProspectus) UnmarshalJSONFrom(dec *jsontext.Decoder) error { CIK: *shadow.CIK, PricePublicPerShare: *shadow.PricePublicPerShare, PricePublicTotal: *shadow.PricePublicTotal, - DiscountsAndCommissionsPerShare: *shadow.DiscountsAndCommissionsPerShare, - DiscountsAndCommissionsTotal: *shadow.DiscountsAndCommissionsTotal, + DiscountsAndCommissionsPerShare: discountsAndCommissionsPerShare, + DiscountsAndCommissionsTotal: discountsAndCommissionsTotal, ProceedsBeforeExpensesPerShare: *shadow.ProceedsBeforeExpensesPerShare, ProceedsBeforeExpensesTotal: *shadow.ProceedsBeforeExpensesTotal, Form: *shadow.Form, @@ -411,18 +459,18 @@ type StockSplitEvent struct { Date Date `json:"date"` Numerator float64 `json:"numerator"` Denominator float64 `json:"denominator"` - SplitType string `json:"splitType"` + SplitType *string `json:"splitType"` } // stockSplitEventShadow mirrors StockSplitEvent with a pointer or raw value // for every required member so a missing or null member is observable after // decoding. type stockSplitEventShadow struct { - Symbol *string `json:"symbol"` - Date *Date `json:"date"` - Numerator *float64 `json:"numerator"` - Denominator *float64 `json:"denominator"` - SplitType *string `json:"splitType"` + Symbol *string `json:"symbol"` + Date *Date `json:"date"` + Numerator *float64 `json:"numerator"` + Denominator *float64 `json:"denominator"` + SplitType jsontext.Value `json:"splitType"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -442,15 +490,23 @@ func (m *StockSplitEvent) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("StockSplitEvent", "numerator") case shadow.Denominator == nil: return missingMemberError("StockSplitEvent", "denominator") - case shadow.SplitType == nil: + case len(shadow.SplitType) == 0: return missingMemberError("StockSplitEvent", "splitType") } + var splitType *string + if shadow.SplitType.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.SplitType, &value); err != nil { + return err + } + splitType = &value + } *m = StockSplitEvent{ Symbol: *shadow.Symbol, Date: *shadow.Date, Numerator: *shadow.Numerator, Denominator: *shadow.Denominator, - SplitType: *shadow.SplitType, + SplitType: splitType, } return nil } diff --git a/sdk/go/calendar_parity_test.go b/sdk/go/calendar_parity_test.go index 50120659..de51e85a 100644 --- a/sdk/go/calendar_parity_test.go +++ b/sdk/go/calendar_parity_test.go @@ -4,6 +4,7 @@ import ( "encoding/json/jsontext" "encoding/json/v2" "errors" + "reflect" "strings" "testing" ) @@ -39,21 +40,21 @@ func patchedFixture(t *testing.T, name string, edit func(row map[string]jsontext return encoded } -// Exact values copied from calendar_responses.rs. declarationDate carries the -// empty_or_null_date codec: "" and null are nil, a non-string is rejected, and -// the key itself is still required. -func TestDocumentedDividendFixturesDecodeExactValuesAndEmptyDeclarationDates(t *testing.T) { +// Exact values copied from calendar_responses.rs. recordDate, paymentDate, and +// declarationDate carry the empty_or_null_date codec: "" and null are nil, a +// non-string is rejected, and the key itself is still required. +func TestDocumentedDividendFixturesDecodeExactValuesAndEmptyDates(t *testing.T) { t.Parallel() company := assertFixtureParity[DividendEvent](t, "dividends.json") if len(company) != 1 || company[0].DeclarationDate == nil { t.Fatalf("dividends = %+v", company) } want := DividendEvent{ - Symbol: "AAPL", Date: mustParseDate(t, "2026-05-11"), RecordDate: mustParseDate(t, "2026-05-11"), - PaymentDate: mustParseDate(t, "2026-05-14"), DeclarationDate: company[0].DeclarationDate, + Symbol: "AAPL", Date: mustParseDate(t, "2026-05-11"), RecordDate: new(mustParseDate(t, "2026-05-11")), + PaymentDate: new(mustParseDate(t, "2026-05-14")), DeclarationDate: company[0].DeclarationDate, AdjDividend: 0.27, Dividend: 0.27, Yield: 0.3587535875358754, Frequency: "Quarterly", } - if company[0] != want || *company[0].DeclarationDate != mustParseDate(t, "2026-04-30") { + if !reflect.DeepEqual(company[0], want) || *company[0].DeclarationDate != mustParseDate(t, "2026-04-30") { t.Fatalf("dividends = %+v, want %+v", company[0], want) } @@ -88,24 +89,38 @@ func TestDocumentedDividendFixturesDecodeExactValuesAndEmptyDeclarationDates(t * if !errors.As(err, &typed) || !strings.Contains(typed.Message, `"declarationDate"`) { t.Fatalf("missing declarationDate: error = %v", err) } + for _, member := range []string{"recordDate", "paymentDate"} { + for _, absent := range []string{`""`, "null"} { + wire := patchedFixture(t, "dividends_calendar.json", func(row map[string]jsontext.Value) { + row[member] = jsontext.Value(absent) + }) + err := json.Unmarshal(wire, &rows) + if err != nil { + t.Fatalf("%s %s: %v", member, absent, err) + } + if got := map[string]*Date{"recordDate": rows[0].RecordDate, "paymentDate": rows[0].PaymentDate}; got[member] != nil { + t.Fatalf("%s %s decoded as %v, want nil", member, absent, *got[member]) + } + } + } } -// epsActual and revenueActual carry required_option: the key must be present -// and null is a value, so the two fixtures decode to nil and to pointers. -func TestDocumentedEarningsFixturesKeepOnlyActualValuesNullable(t *testing.T) { +// The actual and estimated values carry required_option: the key must be +// present and null is a value, so the two fixtures decode to nil and to pointers. +func TestDocumentedEarningsFixturesKeepActualAndEstimatedValuesNullable(t *testing.T) { t.Parallel() company := assertFixtureParity[EarningsEvent](t, "earnings.json") want := EarningsEvent{ - Symbol: "AAPL", Date: mustParseDate(t, "2026-07-30"), EPSEstimated: 1.88, - RevenueEstimated: 109_038_900_000, LastUpdated: mustParseDate(t, "2026-07-30"), + Symbol: "AAPL", Date: mustParseDate(t, "2026-07-30"), EPSEstimated: new(1.88), + RevenueEstimated: new(109_038_900_000.0), LastUpdated: mustParseDate(t, "2026-07-30"), } - if len(company) != 1 || company[0] != want { + if len(company) != 1 || !reflect.DeepEqual(company[0], want) { t.Fatalf("earnings = %+v, want %+v", company, want) } calendar := assertFixtureParity[EarningsEvent](t, "earnings_calendar.json") - if len(calendar) != 1 || calendar[0].Symbol != "GRG.L" || calendar[0].EPSEstimated != 0.501 || - calendar[0].RevenueEstimated != 1_086_300_000 { + if len(calendar) != 1 || calendar[0].Symbol != "GRG.L" || !reflect.DeepEqual(calendar[0].EPSEstimated, new(0.501)) || + !reflect.DeepEqual(calendar[0].RevenueEstimated, new(1_086_300_000.0)) { t.Fatalf("earnings_calendar = %+v", calendar) } if calendar[0].EPSActual == nil || *calendar[0].EPSActual != 0.549 || @@ -121,9 +136,9 @@ func TestDocumentedIpoCalendarFixturePreservesLiteralDaaAndRawDynamicMembers(t * rows := assertFixtureParity[IPOCalendarEvent](t, "ipos_calendar.json") want := IPOCalendarEvent{ Symbol: "IMC", Date: mustParseDate(t, "2026-07-29"), Daa: "2026-07-29T04:00:00.000Z", - Company: "IMC Rare Earths Ltd", Exchange: "NYSE", Actions: "Priced", + Company: "IMC Rare Earths Ltd", Exchange: new("NYSE"), Actions: "Priced", } - if len(rows) != 1 || rows[0] != want { + if len(rows) != 1 || !reflect.DeepEqual(rows[0], want) { t.Fatalf("ipos_calendar = %+v, want %+v", rows, want) } @@ -160,18 +175,18 @@ func TestDocumentedIpoFilingAndStockSplitFixturesDecodeExactValues(t *testing.T) wantProspectus := IPOProspectus{ Symbol: "FTW-WT", AcceptedDate: mustParseDate(t, "2026-07-29"), FilingDate: mustParseDate(t, "2026-07-30"), IPODate: mustParseDate(t, "2026-07-28"), CIK: "0002083125", PricePublicPerShare: 1, PricePublicTotal: 434, - DiscountsAndCommissionsPerShare: 0, DiscountsAndCommissionsTotal: 82_251, ProceedsBeforeExpensesPerShare: 1, + DiscountsAndCommissionsPerShare: new(0.0), DiscountsAndCommissionsTotal: new(82_251.0), ProceedsBeforeExpensesPerShare: 1, ProceedsBeforeExpensesTotal: 82_251, Form: "S-1", URL: "https://www.sec.gov/Archives/edgar/data/2083125/000121390026082963/ea0298363-s1_presidio.htm", } - if len(prospectus) != 1 || prospectus[0] != wantProspectus { + if len(prospectus) != 1 || !reflect.DeepEqual(prospectus[0], wantProspectus) { t.Fatalf("ipos_prospectus = %+v", prospectus) } company := assertFixtureParity[StockSplitEvent](t, "stock_splits.json") wantSplit := StockSplitEvent{Symbol: "AAPL", Date: mustParseDate(t, "2020-08-31"), Numerator: 4, Denominator: 1, - SplitType: "stock-split"} - if len(company) != 1 || company[0] != wantSplit { + SplitType: new("stock-split")} + if len(company) != 1 || !reflect.DeepEqual(company[0], wantSplit) { t.Fatalf("stock_splits = %+v", company) } calendar := assertFixtureParity[StockSplitEvent](t, "stock_splits_calendar.json") @@ -211,15 +226,18 @@ func assertMemberContract[T any](t *testing.T, name string, nullable ...string) func TestCalendarRequiredMembersAreEnforcedLikeSerde(t *testing.T) { t.Parallel() - assertMemberContract[DividendEvent](t, "dividends.json", "declarationDate") - assertMemberContract[DividendEvent](t, "dividends_calendar.json", "declarationDate") - assertMemberContract[EarningsEvent](t, "earnings.json", "epsActual", "revenueActual") - assertMemberContract[EarningsEvent](t, "earnings_calendar.json", "epsActual", "revenueActual") - assertMemberContract[IPOCalendarEvent](t, "ipos_calendar.json", "shares", "priceRange", "marketCap") + dividend := []string{"recordDate", "paymentDate", "declarationDate"} + assertMemberContract[DividendEvent](t, "dividends.json", dividend...) + assertMemberContract[DividendEvent](t, "dividends_calendar.json", dividend...) + earnings := []string{"epsActual", "epsEstimated", "revenueActual", "revenueEstimated"} + assertMemberContract[EarningsEvent](t, "earnings.json", earnings...) + assertMemberContract[EarningsEvent](t, "earnings_calendar.json", earnings...) + assertMemberContract[IPOCalendarEvent](t, "ipos_calendar.json", "exchange", "shares", "priceRange", "marketCap") assertMemberContract[IPODisclosure](t, "ipos_disclosure.json") - assertMemberContract[IPOProspectus](t, "ipos_prospectus.json") - assertMemberContract[StockSplitEvent](t, "stock_splits.json") - assertMemberContract[StockSplitEvent](t, "stock_splits_calendar.json") + assertMemberContract[IPOProspectus](t, "ipos_prospectus.json", + "discountsAndCommissionsPerShare", "discountsAndCommissionsTotal") + assertMemberContract[StockSplitEvent](t, "stock_splits.json", "splitType") + assertMemberContract[StockSplitEvent](t, "stock_splits_calendar.json", "splitType") for _, wire := range []string{"[]", "[ ]"} { var rows []DividendEvent diff --git a/sdk/go/integral_f64_parity_test.go b/sdk/go/integral_f64_parity_test.go index 3d94f196..9164901d 100644 --- a/sdk/go/integral_f64_parity_test.go +++ b/sdk/go/integral_f64_parity_test.go @@ -75,7 +75,7 @@ func TestQuantityAndAmountFieldsDecodeFractionalForms(t *testing.T) { } earnings := assertFixtureParity[EarningsEvent](t, "earnings_calendar_fractional_synthetic.json") - if len(earnings) != 1 || earnings[0].RevenueEstimated != 1_086_300_000.5 || + if len(earnings) != 1 || earnings[0].RevenueEstimated == nil || *earnings[0].RevenueEstimated != 1_086_300_000.5 || earnings[0].RevenueActual == nil || *earnings[0].RevenueActual != 1_101_500_000 { t.Fatalf("earnings_calendar_fractional_synthetic = %+v", earnings) }