From 2728f1113b4679698d31e89a218113d9d5b8d69b Mon Sep 17 00:00:00 2001 From: benbenbang Date: Tue, 29 Sep 2026 09:02:43 +0800 Subject: [PATCH 1/3] fix(fmp-py-gen): teach gen_go the count codec for #367 The generated Go decoders now understand a bare `Count` (u64) field carrying the `crate::codecs::count::deserialize` path. gen_go maps it to a public `uint64` with a raw `jsontext.Value` shadow, treats a missing or null value as a missing member, and emits a `decodeCount` call; an `Option` or `Vec` shape is rejected with a clear error because it has no Go shape yet. **Decode helper:** * Added `decodeCount` to `sdk/go/decode.go`, which accepts a non-negative JSON integer or a finite integral float such as `3.0` below `2^64`, as the provider intermittently sends, and reports a fractional, negative, out-of-range, or non-numeric value as a Decode error that never echoes the value. **Tests and docs:** * Added Rust tests covering the mapping, rendering, and rejection paths and Go tests exercising the accepted and rejected inputs. * Documented the count codec rule in ADR 0030. --- crates/fmp-py-gen/src/bin/gen_go/models.rs | 10 +++++ crates/fmp-py-gen/src/bin/gen_go/tests.rs | 48 ++++++++++++++++++++++ crates/fmp-py-gen/src/bin/gen_go/types.rs | 18 ++++++++ docs/adr/0030-go-sdk-architecture.md | 2 +- sdk/go/decode.go | 27 ++++++++++++ sdk/go/decode_test.go | 47 +++++++++++++++++++++ 6 files changed, 151 insertions(+), 1 deletion(-) diff --git a/crates/fmp-py-gen/src/bin/gen_go/models.rs b/crates/fmp-py-gen/src/bin/gen_go/models.rs index 5455d65d..a75d5379 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/models.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/models.rs @@ -220,6 +220,7 @@ fn render_model(model: &ModelPlan, out: &mut String) { } Codec::RequiredOption | Codec::RequiredNumber + | Codec::Count | Codec::EmptyDate | Codec::EmptyOrNullDate => local_name(rust_name), }; @@ -310,6 +311,7 @@ fn render_required_switch(model: &ModelPlan, out: &mut String) { _ if field.raw_key => format!("{} == nil", wire_local(rust_name)), Codec::RequiredOption | Codec::RequiredNumber + | Codec::Count | Codec::EmptyDate | Codec::EmptyOrNullDate | Codec::DynamicJson => format!("len(shadow.{}) == 0", field.name), @@ -361,6 +363,14 @@ fn render_codec_block(model: &str, field: &GoField, local: &str, out: &mut Strin field.wire, field.name ); } + Codec::Count => { + let _ = writeln!( + out, + "\t{local}, err := decodeCount({model:?}, {:?}, shadow.{})\n\ + \tif err != nil {{\n\t\treturn err\n\t}}", + field.wire, field.name + ); + } Codec::DynamicObject => render_kind_guard(model, field, '{', "object", out), Codec::Number => render_kind_guard(model, field, '0', "number", out), Codec::RequiredNumber => { diff --git a/crates/fmp-py-gen/src/bin/gen_go/tests.rs b/crates/fmp-py-gen/src/bin/gen_go/tests.rs index 4ad67b93..44c4265a 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/tests.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/tests.rs @@ -600,6 +600,54 @@ fn codec_fields_map_to_the_shadow_shapes_of_the_adr() { ); } +#[test] +fn count_codec_decodes_through_the_raw_shadow_and_keeps_the_public_uint64() { + let aliases: BTreeMap = [("Count".to_string(), "u64".to_string())].into(); + let structs = Vec::new(); + let table = TypeTable::new(&structs, &aliases); + let codec = "crate::codecs::count::deserialize"; + let def = row(vec![ + field("strong_buy", "Count", deserialize_with(codec)), + field("hold", "Count", FieldAttrs::default()), + ]); + let mapped = table.go_field(&def, &def.fields[0]).expect("maps"); + assert_eq!( + ( + mapped.public_ty.as_str(), + mapped.shadow_ty.as_str(), + mapped.codec, + mapped.required_key(), + ), + ("uint64", "jsontext.Value", Codec::Count, true) + ); + let models = plan_models("test", &[&def], &table).expect("plans"); + let rendered = render_models("test", &models); + for expected in [ + "StrongBuy uint64 `json:\"strongBuy\"`", + "StrongBuy jsontext.Value `json:\"strongBuy\"`", + "case len(shadow.StrongBuy) == 0:", + "strongBuy, err := decodeCount(\"Row\", \"strongBuy\", shadow.StrongBuy)", + "StrongBuy: strongBuy,", + "Hold *uint64 `json:\"hold\"`", + "Hold: *shadow.Hold,", + ] { + assert!( + rendered.contains(expected), + "missing {expected:?} in\n{rendered}" + ); + } + + for (name, ty) in [ + ("maybe", "Option"), + ("price", "f64"), + ("many", "Vec"), + ] { + let def = row(vec![field(name, ty, deserialize_with(codec))]); + let error = table.go_field(&def, &def.fields[0]).expect_err(name); + assert!(error.contains("bare Count"), "{name}: {error}"); + } +} + #[test] fn skip_serializing_if_none_marks_only_the_public_tag_omitzero() { let aliases = BTreeMap::new(); diff --git a/crates/fmp-py-gen/src/bin/gen_go/types.rs b/crates/fmp-py-gen/src/bin/gen_go/types.rs index 0a321fcf..ba880a9c 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/types.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/types.rs @@ -32,6 +32,10 @@ pub(crate) enum Codec { /// `deserialize_with = "required_option"` on `Option`: the key /// must be present, null is allowed, a present value must be a number. RequiredNumber, + /// `deserialize_with = "crate::codecs::count::deserialize"` on a bare + /// `Count`: the key is required, null is rejected, and an integral float + /// such as `3.0` decodes as the integer. The shadow keeps the raw value. + Count, /// A `DynamicObject` that holds every member the named fields do not /// claim, as serde `flatten` on a map does: `#[serde(flatten)]`, or the /// single `DynamicObject` field of a struct with a hand-written @@ -40,6 +44,9 @@ pub(crate) enum Codec { Embedded, } +/// The `deserialize_with` path of the Rust count codec. +const COUNT_CODEC: &str = "crate::codecs::count::deserialize"; + /// One Go struct member derived from a Rust field. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct GoField { @@ -72,6 +79,7 @@ impl GoField { Codec::Embedded => false, Codec::RequiredOption | Codec::RequiredNumber + | Codec::Count | Codec::EmptyDate | Codec::EmptyOrNullDate | Codec::DynamicJson @@ -171,6 +179,16 @@ impl<'a> TypeTable<'a> { (Some("required_option"), BaseKind::Number, [Wrap::Option]) => { (Codec::RequiredNumber, "jsontext.Value".to_string()) } + (Some(COUNT_CODEC), BaseKind::Scalar, []) if base.go == "uint64" => { + (Codec::Count, "jsontext.Value".to_string()) + } + (Some(COUNT_CODEC), _, _) => { + return Err(fail( + "the count codec needs a bare Count (u64) field; Option has no Go \ + shape yet" + .to_string(), + )); + } (Some("required_option"), _, _) => { return Err(fail( "required_option needs an Option field" diff --git a/docs/adr/0030-go-sdk-architecture.md b/docs/adr/0030-go-sdk-architecture.md index 937ca703..978c92ae 100644 --- a/docs/adr/0030-go-sdk-architecture.md +++ b/docs/adr/0030-go-sdk-architecture.md @@ -125,7 +125,7 @@ that no wire information is invented or lost: | `MarketValue`, `Quantity`, `Ratio`, `PerShareAmount` (`f64`) | `float64` | money amounts, share quantities, ratios such as beta and per-share amounts such as the last dividend and DCF value (#343), documented as integers on many fields but decoded from any JSON number, fractional or negative (#340); Rust re-encodes an integral value as a JSON integer through `codecs::integral_f64` | | `Volume` (`f64`) | `float64` | the provider intermittently sends a fractional volume (#337); Rust re-encodes an integral value as a JSON integer through `codecs::integral_f64`, json/v2 already formats an integral `float64` without a fraction; the screener volume filters stay `uint64` | | `MarketCapitalization`, `TokenSupply`, `SplitTerm` (`f64`) | `float64` | documented as integers but decoded from any JSON number (#339); Rust re-encodes an integral value as a JSON integer through `codecs::integral_f64`; the screener market-cap filters stay `uint64` | -| `Count` (`u64`) | `uint64` | true counts only (transactions, analysts, holders, positions, employees, scores) | +| `Count` (`u64`) | `uint64` | true counts only (transactions, analysts, holders, positions, employees, scores); every response field decodes through `codecs::count`, which also accepts an integral float such as `3.0` (#367); the Go shadow keeps the raw `jsontext.Value` and `decodeCount` applies the same rule, re-encoding stays an integer | | `StatementAmount` (`f64`) | `float64` | statement lines, estimates, DCF outputs and filing amounts, documented as integers but decoded from any JSON number, fractional or negative (#341); values above `2^53` round; Rust re-encodes an integral value as a JSON integer through `codecs::integral_f64` | | `Option` | `*T` | `nil` for absent and for JSON `null` | | `NumericString`, `PercentString` | `string` | exact wire text, never scaled | diff --git a/sdk/go/decode.go b/sdk/go/decode.go index 5a010a36..585d016b 100644 --- a/sdk/go/decode.go +++ b/sdk/go/decode.go @@ -3,6 +3,7 @@ package fmp import ( "encoding/json/jsontext" "encoding/json/v2" + "math" "strconv" ) @@ -39,6 +40,32 @@ func decodeEmptyDate(model, member string, raw jsontext.Value, allowNull bool) ( } } +// decodeCount decodes the count codec of the Rust crate: a Count member +// accepts a non-negative JSON integer or a finite integral JSON number below +// 2^64, since the provider has sent 3.0 for 3. A JSON null is reported as a +// missing member, as a pointer shadow member would be; a fractional, +// negative, or out-of-range number and any other kind is a Decode error that +// never echoes the value. raw is never empty: the generated caller has +// already reported a missing member. +func decodeCount(model, member string, raw jsontext.Value) (uint64, error) { + switch raw.Kind() { + case 'n': + return 0, missingMemberError(model, member) + case '0': + default: + return 0, invalidMemberError(model, member, "non-negative integral number") + } + text := string(raw) + if value, err := strconv.ParseUint(text, 10, 64); err == nil { + return value, nil + } + value, err := strconv.ParseFloat(text, 64) + if err != nil || value < 0 || value >= 0x1p64 || value != math.Trunc(value) { + return 0, invalidMemberError(model, member, "non-negative integral number") + } + return uint64(value), nil +} + // rawMember returns the value of a member that the shadow struct's embedded // fallback collected because encoding/json/v2 cannot spell its wire name in a // struct tag (the name contains a comma, a backslash, or a quote). A missing diff --git a/sdk/go/decode_test.go b/sdk/go/decode_test.go index 8de44326..6a6b3d8e 100644 --- a/sdk/go/decode_test.go +++ b/sdk/go/decode_test.go @@ -3,6 +3,7 @@ package fmp import ( "encoding/json/jsontext" "errors" + "math" "strings" "testing" ) @@ -48,6 +49,52 @@ func TestDecodeEmptyDateMirrorsTheRustSentinelCodecs(t *testing.T) { } } +func TestDecodeCountAcceptsIntegralNumbersOnly(t *testing.T) { + t.Parallel() + accepted := []struct { + raw string + want uint64 + }{ + {`3`, 3}, + {`3.0`, 3}, + {`0`, 0}, + {`-0.0`, 0}, + {`1e2`, 100}, + {`18446744073709551615`, math.MaxUint64}, + {`18446744073709549568.0`, 18446744073709549568}, + } + for _, tc := range accepted { + got, err := decodeCount("Row", "buy", jsontext.Value(tc.raw)) + if err != nil || got != tc.want { + t.Fatalf("decodeCount(%s) = %d, %v, want %d", tc.raw, got, err, tc.want) + } + } + rejected := []struct { + raw string + wantErr string + }{ + {`2.9`, `member "buy" of Row must be a JSON non-negative integral number`}, + {`-1`, "non-negative integral number"}, + {`-1.0`, "non-negative integral number"}, + {`18446744073709551616`, "non-negative integral number"}, + {`1e20`, "non-negative integral number"}, + {`1e400`, "non-negative integral number"}, + {`"3"`, "non-negative integral number"}, + {`true`, "non-negative integral number"}, + {`null`, `required member "buy" of Row is missing or null`}, + } + for _, tc := range rejected { + _, err := decodeCount("Row", "buy", jsontext.Value(tc.raw)) + var typed *Error + if !errors.As(err, &typed) || typed.Category != CategoryDecode { + t.Fatalf("decodeCount(%s) error = %v, want a Decode *Error", tc.raw, err) + } + if !strings.Contains(err.Error(), tc.wantErr) || (tc.raw != `null` && strings.Contains(err.Error(), tc.raw)) { + t.Fatalf("decodeCount(%s) error = %q, want %q without the value", tc.raw, err, tc.wantErr) + } + } +} + func TestRawMemberTreatsMissingAndNullAlike(t *testing.T) { t.Parallel() members := map[string]jsontext.Value{ From 6cb4a2e20eb3cbb39a5e47cc4e4a24530867a476 Mon Sep 17 00:00:00 2001 From: benbenbang Date: Tue, 29 Sep 2026 09:07:17 +0800 Subject: [PATCH 2/3] fix(sdk/go/artifact): accept integral floats in Count response fields Providers occasionally send a count as an integral JSON float such as `3.0` where an integer is documented, which previously failed to decode. Count response fields now accept a non-negative finite integral number from either the integer or float path, while still rejecting fractional, negative, non-finite, or out-of-range numbers and non-numbers. **Rust count codec:** * Added `crate::codecs::count` with `deserialize` and `deserialize_option` helpers that decode a `Count` from an integer or an integral float below `2^64`, with an error message that never echoes the offending value. * Applied the codec via `deserialize_with` to every `Count` response field across analyst, company, fundraising, funds, insider-trading, statement summary, and TipRanks models; serialization stays serde's default `u64`. * Documented the accepted range on the `Count` type alias and covered both the `from_str` and `from_value` decode paths with tests. **Regenerated Go models:** * Regenerated the Go SDK models so count shadow fields decode through `decodeCount`, matching the Rust codec they are generated from and keeping the go-sdk-tests hook satisfied in the same commit. Refs: #367 --- crates/libfmp/src/codecs.rs | 128 +++++- crates/libfmp/src/responses/analyst.rs | 30 ++ crates/libfmp/src/responses/company.rs | 1 + crates/libfmp/src/responses/fundraising.rs | 2 + crates/libfmp/src/responses/funds.rs | 1 + .../libfmp/src/responses/insider_trading.rs | 4 + .../src/responses/statements/summaries.rs | 1 + crates/libfmp/src/responses/tipranks.rs | 34 ++ crates/libfmp/src/types.rs | 4 + sdk/go/analyst_models.go | 370 ++++++++++++------ sdk/go/company_models.go | 26 +- sdk/go/fundraising_models.go | 20 +- sdk/go/funds_models.go | 10 +- sdk/go/insider_trading_models.go | 58 ++- sdk/go/statements_models.go | 30 +- sdk/go/tipranks_models.go | 340 +++++++++++----- 16 files changed, 777 insertions(+), 282 deletions(-) diff --git a/crates/libfmp/src/codecs.rs b/crates/libfmp/src/codecs.rs index 7208a5bd..606904c7 100644 --- a/crates/libfmp/src/codecs.rs +++ b/crates/libfmp/src/codecs.rs @@ -748,11 +748,137 @@ pub(crate) mod integral_f64 { } } +/// Deserializers for response fields typed with [`Count`](crate::types::Count). +/// +/// The provider documents counts as JSON integers but has sent an integral +/// float such as `3.0`, so a count decodes from a non-negative JSON integer +/// or from a finite integral JSON float below `2^64`. A fractional, negative, +/// non-finite or out-of-range number, and any non-number, is a decode error +/// whose message never includes the value. Apply [`deserialize`] with +/// `deserialize_with` on a bare `Count` field and [`deserialize_option`] on an +/// `Option` (with `default` when the key may be missing); +/// serialization is serde's default `u64` path. +pub(crate) mod count { + use super::*; + + const EXPECTED: &str = "a count must be a non-negative integral JSON number below 2^64"; + + fn from_number(number: &Number) -> Result { + if let Some(value) = number.as_u64() { + return Ok(value); + } + match number.as_f64() { + Some(value) + if value.is_finite() + && value.fract() == 0.0 + && value >= 0.0 + && value < u64::MAX as f64 => + { + Ok(value as u64) + } + _ => Err(E::custom(EXPECTED)), + } + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + from_number(&Number::deserialize(deserializer)?) + } + + #[cfg_attr(not(test), allow(dead_code))] + pub(crate) fn deserialize_option<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Option::::deserialize(deserializer)? + .map(|number| from_number(&number)) + .transpose() + } +} + #[cfg(test)] mod tests { use serde::{Deserialize, Serialize}; - use crate::types::Volume; + use crate::types::{Count, Volume}; + + #[test] + fn count_accepts_integers_and_integral_floats_on_both_decode_paths() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Row { + #[serde(deserialize_with = "super::count::deserialize")] + buy: Count, + } + let from_text = |text: &str| { + serde_json::from_str::(&format!(r#"{{"buy":{text}}}"#)).map(|row| row.buy) + }; + let from_value = |text: &str| { + let value: serde_json::Value = + serde_json::from_str(&format!(r#"{{"buy":{text}}}"#)).unwrap(); + serde_json::from_value::(value).map(|row| row.buy) + }; + + for (text, expected) in [ + ("3", 3), + ("3.0", 3), + ("0", 0), + ("0.0", 0), + ("-0.0", 0), + ("1e2", 100), + ("18446744073709551615", u64::MAX), + ("18446744073709549568.0", 18_446_744_073_709_549_568), + ] { + assert_eq!(from_text(text).unwrap(), expected, "from_str {text}"); + assert_eq!(from_value(text).unwrap(), expected, "from_value {text}"); + } + for text in [ + "2.9", + "-1", + "-1.0", + "18446744073709551616", + "18446744073709551616.0", + "1e20", + "null", + r#""3""#, + "true", + ] { + let error = from_text(text).unwrap_err(); + assert!(from_value(text).is_err(), "from_value {text}"); + if text.len() > 2 && text.starts_with(['1', '2', '-']) { + assert!( + !error.to_string().contains(text.trim_start_matches('-')), + "the error must not echo the value: {error}" + ); + } + } + assert_eq!( + serde_json::to_string(&Row { buy: 3 }).unwrap(), + r#"{"buy":3}"# + ); + } + + #[test] + fn count_option_accepts_null_and_integral_floats() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Row { + #[serde(default, deserialize_with = "super::count::deserialize_option")] + buy: Option, + } + let decode = |text: &str| serde_json::from_str::(text).map(|row| row.buy); + + assert_eq!(decode(r#"{"buy":3.0}"#).unwrap(), Some(3)); + assert_eq!(decode(r#"{"buy":7}"#).unwrap(), Some(7)); + assert_eq!(decode(r#"{"buy":null}"#).unwrap(), None); + assert_eq!(decode("{}").unwrap(), None); + assert!(decode(r#"{"buy":2.9}"#).is_err()); + assert!(decode(r#"{"buy":-1}"#).is_err()); + assert_eq!( + serde_json::to_string(&Row { buy: Some(3) }).unwrap(), + r#"{"buy":3}"# + ); + } #[test] fn integral_f64_accepts_any_json_number_and_re_encodes_integral_values_as_integers() { diff --git a/crates/libfmp/src/responses/analyst.rs b/crates/libfmp/src/responses/analyst.rs index 79ac9790..51359375 100644 --- a/crates/libfmp/src/responses/analyst.rs +++ b/crates/libfmp/src/responses/analyst.rs @@ -44,7 +44,9 @@ pub struct FinancialEstimate { pub eps_avg: f64, pub eps_high: f64, pub eps_low: f64, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub num_analysts_revenue: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub num_analysts_eps: Count, } @@ -55,12 +57,19 @@ pub struct FinancialEstimate { pub struct RatingSnapshot { pub symbol: Ticker, pub rating: String, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub overall_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub discounted_cash_flow_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub return_on_equity_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub return_on_assets_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub debt_to_equity_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub price_to_earnings_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub price_to_book_score: Count, } @@ -72,12 +81,19 @@ pub struct HistoricalRating { pub symbol: Ticker, pub date: Date, pub rating: String, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub overall_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub discounted_cash_flow_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub return_on_equity_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub return_on_assets_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub debt_to_equity_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub price_to_earnings_score: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub price_to_book_score: Count, } @@ -87,12 +103,16 @@ pub struct HistoricalRating { #[serde(rename_all = "camelCase")] pub struct PriceTargetSummary { pub symbol: Ticker, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub last_month_count: Count, pub last_month_avg_price_target: Price, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub last_quarter_count: Count, pub last_quarter_avg_price_target: Price, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub last_year_count: Count, pub last_year_avg_price_target: Price, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub all_time_count: Count, pub all_time_avg_price_target: Price, /// The exact provider string, whose contents happen to be JSON-array text. @@ -131,10 +151,15 @@ pub struct StockGrade { pub struct HistoricalStockGrade { pub symbol: Ticker, pub date: Date, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_ratings_strong_buy: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_ratings_buy: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_ratings_hold: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_ratings_sell: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_ratings_strong_sell: Count, } @@ -144,10 +169,15 @@ pub struct HistoricalStockGrade { #[serde(rename_all = "camelCase")] pub struct StockGradesSummary { pub symbol: Ticker, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub strong_buy: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub buy: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub hold: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub sell: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub strong_sell: Count, pub consensus: String, } diff --git a/crates/libfmp/src/responses/company.rs b/crates/libfmp/src/responses/company.rs index b44f7e6b..d8f3c190 100644 --- a/crates/libfmp/src/responses/company.rs +++ b/crates/libfmp/src/responses/company.rs @@ -111,6 +111,7 @@ pub struct EmployeeCount { pub company_name: String, pub form_type: String, pub filing_date: Date, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub employee_count: Count, pub source: String, } diff --git a/crates/libfmp/src/responses/fundraising.rs b/crates/libfmp/src/responses/fundraising.rs index f7303ee6..76401dc9 100644 --- a/crates/libfmp/src/responses/fundraising.rs +++ b/crates/libfmp/src/responses/fundraising.rs @@ -86,6 +86,7 @@ pub struct CrowdfundingOffering { #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub maximum_offering_amount: StatementAmount, pub offering_deadline_date: UsDate, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub current_number_of_employees: Count, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub total_asset_most_recent_fiscal_year: StatementAmount, @@ -182,6 +183,7 @@ pub struct RegulationDOffering { #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub total_amount_remaining: MarketValue, pub has_non_accredited_investors: bool, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_number_already_invested: Count, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub sales_commissions: MarketValue, diff --git a/crates/libfmp/src/responses/funds.rs b/crates/libfmp/src/responses/funds.rs index df3a65cf..ffb2e8d9 100644 --- a/crates/libfmp/src/responses/funds.rs +++ b/crates/libfmp/src/responses/funds.rs @@ -59,6 +59,7 @@ pub struct EtfFundInfo { pub inception_date: Date, pub nav: f64, pub nav_currency: CurrencyCode, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub holdings_count: Count, pub is_actively_trading: bool, pub updated_at: IsoTimestamp, diff --git a/crates/libfmp/src/responses/insider_trading.rs b/crates/libfmp/src/responses/insider_trading.rs index 063a206e..9b6a386a 100644 --- a/crates/libfmp/src/responses/insider_trading.rs +++ b/crates/libfmp/src/responses/insider_trading.rs @@ -65,7 +65,9 @@ pub struct InsiderTradeStatistics { pub cik: Cik, pub year: CalendarYear, pub quarter: CalendarQuarter, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub acquired_transactions: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub disposed_transactions: Count, pub acquired_disposed_ratio: f64, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] @@ -74,7 +76,9 @@ pub struct InsiderTradeStatistics { pub total_disposed: Quantity, pub average_acquired: f64, pub average_disposed: f64, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_purchases: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_sales: Count, } diff --git a/crates/libfmp/src/responses/statements/summaries.rs b/crates/libfmp/src/responses/statements/summaries.rs index 412be387..ff7c5d36 100644 --- a/crates/libfmp/src/responses/statements/summaries.rs +++ b/crates/libfmp/src/responses/statements/summaries.rs @@ -31,6 +31,7 @@ pub struct FinancialScore { pub symbol: Ticker, pub reported_currency: CurrencyCode, pub altman_z_score: f64, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub piotroski_score: Count, #[serde(serialize_with = "crate::codecs::integral_f64::serialize")] pub working_capital: StatementAmount, diff --git a/crates/libfmp/src/responses/tipranks.rs b/crates/libfmp/src/responses/tipranks.rs index 5b06b681..d648bee1 100644 --- a/crates/libfmp/src/responses/tipranks.rs +++ b/crates/libfmp/src/responses/tipranks.rs @@ -72,8 +72,11 @@ pub struct TipRanksPointInTimeRating { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TipRanksRecommendationCounts { + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub buy: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub hold: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub sell: Count, } @@ -82,11 +85,17 @@ pub struct TipRanksRecommendationCounts { #[non_exhaustive] #[serde(rename_all = "camelCase")] pub struct TipRanksAnalystActionCounts { + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub initiated: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub maintained: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub upgraded: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub downgraded: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub reiterated: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub resumed: Count, } @@ -98,15 +107,22 @@ pub struct TipRanksSymbolSummary { pub symbol: Ticker, pub from: Date, pub to: Date, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_recommendations: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_symbols: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_analysts: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub valid_price_targets: Count, pub recommendations: TipRanksRecommendationCounts, #[serde(rename = "analystAction")] pub analyst_action: TipRanksAnalystActionCounts, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub compared_price_targets: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub beats: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub misses: Count, pub average_return: Number, pub top_return: Number, @@ -122,15 +138,22 @@ pub struct TipRanksAnalystSummary { pub expert_uid: TipRanksExpertUid, pub from: Date, pub to: Date, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_recommendations: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_symbols: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_analysts: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub valid_price_targets: Count, pub recommendations: TipRanksRecommendationCounts, #[serde(rename = "analystAction")] pub analyst_action: TipRanksAnalystActionCounts, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub compared_price_targets: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub beats: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub misses: Count, pub average_return: Number, pub top_return: Number, @@ -145,15 +168,22 @@ pub struct TipRanksFirmSummary { pub firm_name: String, pub from: Date, pub to: Date, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_recommendations: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_symbols: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub distinct_analysts: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub valid_price_targets: Count, pub recommendations: TipRanksRecommendationCounts, #[serde(rename = "analystAction")] pub analyst_action: TipRanksAnalystActionCounts, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub compared_price_targets: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub beats: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub misses: Count, pub average_return: Number, pub top_return: Number, @@ -171,8 +201,12 @@ pub struct TipRanksAnalystProfile { pub firm_name: String, pub success_rate: Number, pub excess_return: Number, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub total_recommendations: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub good_recommendations: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub analyst_rank: Count, + #[serde(deserialize_with = "crate::codecs::count::deserialize")] pub num_of_stars: Count, } diff --git a/crates/libfmp/src/types.rs b/crates/libfmp/src/types.rs index 467d31cf..589800e9 100644 --- a/crates/libfmp/src/types.rs +++ b/crates/libfmp/src/types.rs @@ -720,6 +720,10 @@ pub type TokenSupply = f64; /// integer through `crate::codecs::integral_f64`. pub type SplitTerm = f64; /// A non-negative count represented by the provider as a JSON integer. +/// +/// The provider has also sent integral floats such as `3.0`, so response +/// fields decode through `crate::codecs::count`, which accepts a finite +/// integral number and rejects a fractional, negative or non-finite one. pub type Count = u64; /// A signed currency amount reported in a financial statement. /// diff --git a/sdk/go/analyst_models.go b/sdk/go/analyst_models.go index 545b75dd..98735d2d 100644 --- a/sdk/go/analyst_models.go +++ b/sdk/go/analyst_models.go @@ -40,28 +40,28 @@ type FinancialEstimate struct { // value for every required member so a missing or null member is observable // after decoding. type financialEstimateShadow struct { - Symbol *string `json:"symbol"` - Date *Date `json:"date"` - RevenueLow *float64 `json:"revenueLow"` - RevenueHigh *float64 `json:"revenueHigh"` - RevenueAvg *float64 `json:"revenueAvg"` - EbitdaLow *float64 `json:"ebitdaLow"` - EbitdaHigh *float64 `json:"ebitdaHigh"` - EbitdaAvg *float64 `json:"ebitdaAvg"` - EbitLow *float64 `json:"ebitLow"` - EbitHigh *float64 `json:"ebitHigh"` - EbitAvg *float64 `json:"ebitAvg"` - NetIncomeLow *float64 `json:"netIncomeLow"` - NetIncomeHigh *float64 `json:"netIncomeHigh"` - NetIncomeAvg *float64 `json:"netIncomeAvg"` - SgaExpenseLow *float64 `json:"sgaExpenseLow"` - SgaExpenseHigh *float64 `json:"sgaExpenseHigh"` - SgaExpenseAvg *float64 `json:"sgaExpenseAvg"` - EPSAvg *float64 `json:"epsAvg"` - EPSHigh *float64 `json:"epsHigh"` - EPSLow *float64 `json:"epsLow"` - NumAnalystsRevenue *uint64 `json:"numAnalystsRevenue"` - NumAnalystsEPS *uint64 `json:"numAnalystsEps"` + Symbol *string `json:"symbol"` + Date *Date `json:"date"` + RevenueLow *float64 `json:"revenueLow"` + RevenueHigh *float64 `json:"revenueHigh"` + RevenueAvg *float64 `json:"revenueAvg"` + EbitdaLow *float64 `json:"ebitdaLow"` + EbitdaHigh *float64 `json:"ebitdaHigh"` + EbitdaAvg *float64 `json:"ebitdaAvg"` + EbitLow *float64 `json:"ebitLow"` + EbitHigh *float64 `json:"ebitHigh"` + EbitAvg *float64 `json:"ebitAvg"` + NetIncomeLow *float64 `json:"netIncomeLow"` + NetIncomeHigh *float64 `json:"netIncomeHigh"` + NetIncomeAvg *float64 `json:"netIncomeAvg"` + SgaExpenseLow *float64 `json:"sgaExpenseLow"` + SgaExpenseHigh *float64 `json:"sgaExpenseHigh"` + SgaExpenseAvg *float64 `json:"sgaExpenseAvg"` + EPSAvg *float64 `json:"epsAvg"` + EPSHigh *float64 `json:"epsHigh"` + EPSLow *float64 `json:"epsLow"` + NumAnalystsRevenue jsontext.Value `json:"numAnalystsRevenue"` + NumAnalystsEPS jsontext.Value `json:"numAnalystsEps"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -113,11 +113,19 @@ func (m *FinancialEstimate) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("FinancialEstimate", "epsHigh") case shadow.EPSLow == nil: return missingMemberError("FinancialEstimate", "epsLow") - case shadow.NumAnalystsRevenue == nil: + case len(shadow.NumAnalystsRevenue) == 0: return missingMemberError("FinancialEstimate", "numAnalystsRevenue") - case shadow.NumAnalystsEPS == nil: + case len(shadow.NumAnalystsEPS) == 0: return missingMemberError("FinancialEstimate", "numAnalystsEps") } + numAnalystsRevenue, err := decodeCount("FinancialEstimate", "numAnalystsRevenue", shadow.NumAnalystsRevenue) + if err != nil { + return err + } + numAnalystsEPS, err := decodeCount("FinancialEstimate", "numAnalystsEps", shadow.NumAnalystsEPS) + if err != nil { + return err + } *m = FinancialEstimate{ Symbol: *shadow.Symbol, Date: *shadow.Date, @@ -139,8 +147,8 @@ func (m *FinancialEstimate) UnmarshalJSONFrom(dec *jsontext.Decoder) error { EPSAvg: *shadow.EPSAvg, EPSHigh: *shadow.EPSHigh, EPSLow: *shadow.EPSLow, - NumAnalystsRevenue: *shadow.NumAnalystsRevenue, - NumAnalystsEPS: *shadow.NumAnalystsEPS, + NumAnalystsRevenue: numAnalystsRevenue, + NumAnalystsEPS: numAnalystsEPS, } return nil } @@ -162,15 +170,15 @@ type RatingSnapshot struct { // every required member so a missing or null member is observable after // decoding. type ratingSnapshotShadow struct { - Symbol *string `json:"symbol"` - Rating *string `json:"rating"` - OverallScore *uint64 `json:"overallScore"` - DiscountedCashFlowScore *uint64 `json:"discountedCashFlowScore"` - ReturnOnEquityScore *uint64 `json:"returnOnEquityScore"` - ReturnOnAssetsScore *uint64 `json:"returnOnAssetsScore"` - DebtToEquityScore *uint64 `json:"debtToEquityScore"` - PriceToEarningsScore *uint64 `json:"priceToEarningsScore"` - PriceToBookScore *uint64 `json:"priceToBookScore"` + Symbol *string `json:"symbol"` + Rating *string `json:"rating"` + OverallScore jsontext.Value `json:"overallScore"` + DiscountedCashFlowScore jsontext.Value `json:"discountedCashFlowScore"` + ReturnOnEquityScore jsontext.Value `json:"returnOnEquityScore"` + ReturnOnAssetsScore jsontext.Value `json:"returnOnAssetsScore"` + DebtToEquityScore jsontext.Value `json:"debtToEquityScore"` + PriceToEarningsScore jsontext.Value `json:"priceToEarningsScore"` + PriceToBookScore jsontext.Value `json:"priceToBookScore"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -186,31 +194,59 @@ func (m *RatingSnapshot) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("RatingSnapshot", "symbol") case shadow.Rating == nil: return missingMemberError("RatingSnapshot", "rating") - case shadow.OverallScore == nil: + case len(shadow.OverallScore) == 0: return missingMemberError("RatingSnapshot", "overallScore") - case shadow.DiscountedCashFlowScore == nil: + case len(shadow.DiscountedCashFlowScore) == 0: return missingMemberError("RatingSnapshot", "discountedCashFlowScore") - case shadow.ReturnOnEquityScore == nil: + case len(shadow.ReturnOnEquityScore) == 0: return missingMemberError("RatingSnapshot", "returnOnEquityScore") - case shadow.ReturnOnAssetsScore == nil: + case len(shadow.ReturnOnAssetsScore) == 0: return missingMemberError("RatingSnapshot", "returnOnAssetsScore") - case shadow.DebtToEquityScore == nil: + case len(shadow.DebtToEquityScore) == 0: return missingMemberError("RatingSnapshot", "debtToEquityScore") - case shadow.PriceToEarningsScore == nil: + case len(shadow.PriceToEarningsScore) == 0: return missingMemberError("RatingSnapshot", "priceToEarningsScore") - case shadow.PriceToBookScore == nil: + case len(shadow.PriceToBookScore) == 0: return missingMemberError("RatingSnapshot", "priceToBookScore") } + overallScore, err := decodeCount("RatingSnapshot", "overallScore", shadow.OverallScore) + if err != nil { + return err + } + discountedCashFlowScore, err := decodeCount("RatingSnapshot", "discountedCashFlowScore", shadow.DiscountedCashFlowScore) + if err != nil { + return err + } + returnOnEquityScore, err := decodeCount("RatingSnapshot", "returnOnEquityScore", shadow.ReturnOnEquityScore) + if err != nil { + return err + } + returnOnAssetsScore, err := decodeCount("RatingSnapshot", "returnOnAssetsScore", shadow.ReturnOnAssetsScore) + if err != nil { + return err + } + debtToEquityScore, err := decodeCount("RatingSnapshot", "debtToEquityScore", shadow.DebtToEquityScore) + if err != nil { + return err + } + priceToEarningsScore, err := decodeCount("RatingSnapshot", "priceToEarningsScore", shadow.PriceToEarningsScore) + if err != nil { + return err + } + priceToBookScore, err := decodeCount("RatingSnapshot", "priceToBookScore", shadow.PriceToBookScore) + if err != nil { + return err + } *m = RatingSnapshot{ Symbol: *shadow.Symbol, Rating: *shadow.Rating, - OverallScore: *shadow.OverallScore, - DiscountedCashFlowScore: *shadow.DiscountedCashFlowScore, - ReturnOnEquityScore: *shadow.ReturnOnEquityScore, - ReturnOnAssetsScore: *shadow.ReturnOnAssetsScore, - DebtToEquityScore: *shadow.DebtToEquityScore, - PriceToEarningsScore: *shadow.PriceToEarningsScore, - PriceToBookScore: *shadow.PriceToBookScore, + OverallScore: overallScore, + DiscountedCashFlowScore: discountedCashFlowScore, + ReturnOnEquityScore: returnOnEquityScore, + ReturnOnAssetsScore: returnOnAssetsScore, + DebtToEquityScore: debtToEquityScore, + PriceToEarningsScore: priceToEarningsScore, + PriceToBookScore: priceToBookScore, } return nil } @@ -233,16 +269,16 @@ type HistoricalRating struct { // for every required member so a missing or null member is observable after // decoding. type historicalRatingShadow struct { - Symbol *string `json:"symbol"` - Date *Date `json:"date"` - Rating *string `json:"rating"` - OverallScore *uint64 `json:"overallScore"` - DiscountedCashFlowScore *uint64 `json:"discountedCashFlowScore"` - ReturnOnEquityScore *uint64 `json:"returnOnEquityScore"` - ReturnOnAssetsScore *uint64 `json:"returnOnAssetsScore"` - DebtToEquityScore *uint64 `json:"debtToEquityScore"` - PriceToEarningsScore *uint64 `json:"priceToEarningsScore"` - PriceToBookScore *uint64 `json:"priceToBookScore"` + Symbol *string `json:"symbol"` + Date *Date `json:"date"` + Rating *string `json:"rating"` + OverallScore jsontext.Value `json:"overallScore"` + DiscountedCashFlowScore jsontext.Value `json:"discountedCashFlowScore"` + ReturnOnEquityScore jsontext.Value `json:"returnOnEquityScore"` + ReturnOnAssetsScore jsontext.Value `json:"returnOnAssetsScore"` + DebtToEquityScore jsontext.Value `json:"debtToEquityScore"` + PriceToEarningsScore jsontext.Value `json:"priceToEarningsScore"` + PriceToBookScore jsontext.Value `json:"priceToBookScore"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -260,32 +296,60 @@ func (m *HistoricalRating) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("HistoricalRating", "date") case shadow.Rating == nil: return missingMemberError("HistoricalRating", "rating") - case shadow.OverallScore == nil: + case len(shadow.OverallScore) == 0: return missingMemberError("HistoricalRating", "overallScore") - case shadow.DiscountedCashFlowScore == nil: + case len(shadow.DiscountedCashFlowScore) == 0: return missingMemberError("HistoricalRating", "discountedCashFlowScore") - case shadow.ReturnOnEquityScore == nil: + case len(shadow.ReturnOnEquityScore) == 0: return missingMemberError("HistoricalRating", "returnOnEquityScore") - case shadow.ReturnOnAssetsScore == nil: + case len(shadow.ReturnOnAssetsScore) == 0: return missingMemberError("HistoricalRating", "returnOnAssetsScore") - case shadow.DebtToEquityScore == nil: + case len(shadow.DebtToEquityScore) == 0: return missingMemberError("HistoricalRating", "debtToEquityScore") - case shadow.PriceToEarningsScore == nil: + case len(shadow.PriceToEarningsScore) == 0: return missingMemberError("HistoricalRating", "priceToEarningsScore") - case shadow.PriceToBookScore == nil: + case len(shadow.PriceToBookScore) == 0: return missingMemberError("HistoricalRating", "priceToBookScore") } + overallScore, err := decodeCount("HistoricalRating", "overallScore", shadow.OverallScore) + if err != nil { + return err + } + discountedCashFlowScore, err := decodeCount("HistoricalRating", "discountedCashFlowScore", shadow.DiscountedCashFlowScore) + if err != nil { + return err + } + returnOnEquityScore, err := decodeCount("HistoricalRating", "returnOnEquityScore", shadow.ReturnOnEquityScore) + if err != nil { + return err + } + returnOnAssetsScore, err := decodeCount("HistoricalRating", "returnOnAssetsScore", shadow.ReturnOnAssetsScore) + if err != nil { + return err + } + debtToEquityScore, err := decodeCount("HistoricalRating", "debtToEquityScore", shadow.DebtToEquityScore) + if err != nil { + return err + } + priceToEarningsScore, err := decodeCount("HistoricalRating", "priceToEarningsScore", shadow.PriceToEarningsScore) + if err != nil { + return err + } + priceToBookScore, err := decodeCount("HistoricalRating", "priceToBookScore", shadow.PriceToBookScore) + if err != nil { + return err + } *m = HistoricalRating{ Symbol: *shadow.Symbol, Date: *shadow.Date, Rating: *shadow.Rating, - OverallScore: *shadow.OverallScore, - DiscountedCashFlowScore: *shadow.DiscountedCashFlowScore, - ReturnOnEquityScore: *shadow.ReturnOnEquityScore, - ReturnOnAssetsScore: *shadow.ReturnOnAssetsScore, - DebtToEquityScore: *shadow.DebtToEquityScore, - PriceToEarningsScore: *shadow.PriceToEarningsScore, - PriceToBookScore: *shadow.PriceToBookScore, + OverallScore: overallScore, + DiscountedCashFlowScore: discountedCashFlowScore, + ReturnOnEquityScore: returnOnEquityScore, + ReturnOnAssetsScore: returnOnAssetsScore, + DebtToEquityScore: debtToEquityScore, + PriceToEarningsScore: priceToEarningsScore, + PriceToBookScore: priceToBookScore, } return nil } @@ -309,16 +373,16 @@ type PriceTargetSummary struct { // value for every required member so a missing or null member is observable // after decoding. type priceTargetSummaryShadow struct { - Symbol *string `json:"symbol"` - LastMonthCount *uint64 `json:"lastMonthCount"` - LastMonthAvgPriceTarget *float64 `json:"lastMonthAvgPriceTarget"` - LastQuarterCount *uint64 `json:"lastQuarterCount"` - LastQuarterAvgPriceTarget *float64 `json:"lastQuarterAvgPriceTarget"` - LastYearCount *uint64 `json:"lastYearCount"` - LastYearAvgPriceTarget *float64 `json:"lastYearAvgPriceTarget"` - AllTimeCount *uint64 `json:"allTimeCount"` - AllTimeAvgPriceTarget *float64 `json:"allTimeAvgPriceTarget"` - Publishers *string `json:"publishers"` + Symbol *string `json:"symbol"` + LastMonthCount jsontext.Value `json:"lastMonthCount"` + LastMonthAvgPriceTarget *float64 `json:"lastMonthAvgPriceTarget"` + LastQuarterCount jsontext.Value `json:"lastQuarterCount"` + LastQuarterAvgPriceTarget *float64 `json:"lastQuarterAvgPriceTarget"` + LastYearCount jsontext.Value `json:"lastYearCount"` + LastYearAvgPriceTarget *float64 `json:"lastYearAvgPriceTarget"` + AllTimeCount jsontext.Value `json:"allTimeCount"` + AllTimeAvgPriceTarget *float64 `json:"allTimeAvgPriceTarget"` + Publishers *string `json:"publishers"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -332,34 +396,50 @@ func (m *PriceTargetSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { switch { case shadow.Symbol == nil: return missingMemberError("PriceTargetSummary", "symbol") - case shadow.LastMonthCount == nil: + case len(shadow.LastMonthCount) == 0: return missingMemberError("PriceTargetSummary", "lastMonthCount") case shadow.LastMonthAvgPriceTarget == nil: return missingMemberError("PriceTargetSummary", "lastMonthAvgPriceTarget") - case shadow.LastQuarterCount == nil: + case len(shadow.LastQuarterCount) == 0: return missingMemberError("PriceTargetSummary", "lastQuarterCount") case shadow.LastQuarterAvgPriceTarget == nil: return missingMemberError("PriceTargetSummary", "lastQuarterAvgPriceTarget") - case shadow.LastYearCount == nil: + case len(shadow.LastYearCount) == 0: return missingMemberError("PriceTargetSummary", "lastYearCount") case shadow.LastYearAvgPriceTarget == nil: return missingMemberError("PriceTargetSummary", "lastYearAvgPriceTarget") - case shadow.AllTimeCount == nil: + case len(shadow.AllTimeCount) == 0: return missingMemberError("PriceTargetSummary", "allTimeCount") case shadow.AllTimeAvgPriceTarget == nil: return missingMemberError("PriceTargetSummary", "allTimeAvgPriceTarget") case shadow.Publishers == nil: return missingMemberError("PriceTargetSummary", "publishers") } + lastMonthCount, err := decodeCount("PriceTargetSummary", "lastMonthCount", shadow.LastMonthCount) + if err != nil { + return err + } + lastQuarterCount, err := decodeCount("PriceTargetSummary", "lastQuarterCount", shadow.LastQuarterCount) + if err != nil { + return err + } + lastYearCount, err := decodeCount("PriceTargetSummary", "lastYearCount", shadow.LastYearCount) + if err != nil { + return err + } + allTimeCount, err := decodeCount("PriceTargetSummary", "allTimeCount", shadow.AllTimeCount) + if err != nil { + return err + } *m = PriceTargetSummary{ Symbol: *shadow.Symbol, - LastMonthCount: *shadow.LastMonthCount, + LastMonthCount: lastMonthCount, LastMonthAvgPriceTarget: *shadow.LastMonthAvgPriceTarget, - LastQuarterCount: *shadow.LastQuarterCount, + LastQuarterCount: lastQuarterCount, LastQuarterAvgPriceTarget: *shadow.LastQuarterAvgPriceTarget, - LastYearCount: *shadow.LastYearCount, + LastYearCount: lastYearCount, LastYearAvgPriceTarget: *shadow.LastYearAvgPriceTarget, - AllTimeCount: *shadow.AllTimeCount, + AllTimeCount: allTimeCount, AllTimeAvgPriceTarget: *shadow.AllTimeAvgPriceTarget, Publishers: *shadow.Publishers, } @@ -487,13 +567,13 @@ type HistoricalStockGrade struct { // raw value for every required member so a missing or null member is // observable after decoding. type historicalStockGradeShadow struct { - Symbol *string `json:"symbol"` - Date *Date `json:"date"` - AnalystRatingsStrongBuy *uint64 `json:"analystRatingsStrongBuy"` - AnalystRatingsBuy *uint64 `json:"analystRatingsBuy"` - AnalystRatingsHold *uint64 `json:"analystRatingsHold"` - AnalystRatingsSell *uint64 `json:"analystRatingsSell"` - AnalystRatingsStrongSell *uint64 `json:"analystRatingsStrongSell"` + Symbol *string `json:"symbol"` + Date *Date `json:"date"` + AnalystRatingsStrongBuy jsontext.Value `json:"analystRatingsStrongBuy"` + AnalystRatingsBuy jsontext.Value `json:"analystRatingsBuy"` + AnalystRatingsHold jsontext.Value `json:"analystRatingsHold"` + AnalystRatingsSell jsontext.Value `json:"analystRatingsSell"` + AnalystRatingsStrongSell jsontext.Value `json:"analystRatingsStrongSell"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -509,25 +589,45 @@ func (m *HistoricalStockGrade) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("HistoricalStockGrade", "symbol") case shadow.Date == nil: return missingMemberError("HistoricalStockGrade", "date") - case shadow.AnalystRatingsStrongBuy == nil: + case len(shadow.AnalystRatingsStrongBuy) == 0: return missingMemberError("HistoricalStockGrade", "analystRatingsStrongBuy") - case shadow.AnalystRatingsBuy == nil: + case len(shadow.AnalystRatingsBuy) == 0: return missingMemberError("HistoricalStockGrade", "analystRatingsBuy") - case shadow.AnalystRatingsHold == nil: + case len(shadow.AnalystRatingsHold) == 0: return missingMemberError("HistoricalStockGrade", "analystRatingsHold") - case shadow.AnalystRatingsSell == nil: + case len(shadow.AnalystRatingsSell) == 0: return missingMemberError("HistoricalStockGrade", "analystRatingsSell") - case shadow.AnalystRatingsStrongSell == nil: + case len(shadow.AnalystRatingsStrongSell) == 0: return missingMemberError("HistoricalStockGrade", "analystRatingsStrongSell") } + analystRatingsStrongBuy, err := decodeCount("HistoricalStockGrade", "analystRatingsStrongBuy", shadow.AnalystRatingsStrongBuy) + if err != nil { + return err + } + analystRatingsBuy, err := decodeCount("HistoricalStockGrade", "analystRatingsBuy", shadow.AnalystRatingsBuy) + if err != nil { + return err + } + analystRatingsHold, err := decodeCount("HistoricalStockGrade", "analystRatingsHold", shadow.AnalystRatingsHold) + if err != nil { + return err + } + analystRatingsSell, err := decodeCount("HistoricalStockGrade", "analystRatingsSell", shadow.AnalystRatingsSell) + if err != nil { + return err + } + analystRatingsStrongSell, err := decodeCount("HistoricalStockGrade", "analystRatingsStrongSell", shadow.AnalystRatingsStrongSell) + if err != nil { + return err + } *m = HistoricalStockGrade{ Symbol: *shadow.Symbol, Date: *shadow.Date, - AnalystRatingsStrongBuy: *shadow.AnalystRatingsStrongBuy, - AnalystRatingsBuy: *shadow.AnalystRatingsBuy, - AnalystRatingsHold: *shadow.AnalystRatingsHold, - AnalystRatingsSell: *shadow.AnalystRatingsSell, - AnalystRatingsStrongSell: *shadow.AnalystRatingsStrongSell, + AnalystRatingsStrongBuy: analystRatingsStrongBuy, + AnalystRatingsBuy: analystRatingsBuy, + AnalystRatingsHold: analystRatingsHold, + AnalystRatingsSell: analystRatingsSell, + AnalystRatingsStrongSell: analystRatingsStrongSell, } return nil } @@ -548,13 +648,13 @@ type StockGradesSummary struct { // value for every required member so a missing or null member is observable // after decoding. type stockGradesSummaryShadow struct { - Symbol *string `json:"symbol"` - StrongBuy *uint64 `json:"strongBuy"` - Buy *uint64 `json:"buy"` - Hold *uint64 `json:"hold"` - Sell *uint64 `json:"sell"` - StrongSell *uint64 `json:"strongSell"` - Consensus *string `json:"consensus"` + Symbol *string `json:"symbol"` + StrongBuy jsontext.Value `json:"strongBuy"` + Buy jsontext.Value `json:"buy"` + Hold jsontext.Value `json:"hold"` + Sell jsontext.Value `json:"sell"` + StrongSell jsontext.Value `json:"strongSell"` + Consensus *string `json:"consensus"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -568,26 +668,46 @@ func (m *StockGradesSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { switch { case shadow.Symbol == nil: return missingMemberError("StockGradesSummary", "symbol") - case shadow.StrongBuy == nil: + case len(shadow.StrongBuy) == 0: return missingMemberError("StockGradesSummary", "strongBuy") - case shadow.Buy == nil: + case len(shadow.Buy) == 0: return missingMemberError("StockGradesSummary", "buy") - case shadow.Hold == nil: + case len(shadow.Hold) == 0: return missingMemberError("StockGradesSummary", "hold") - case shadow.Sell == nil: + case len(shadow.Sell) == 0: return missingMemberError("StockGradesSummary", "sell") - case shadow.StrongSell == nil: + case len(shadow.StrongSell) == 0: return missingMemberError("StockGradesSummary", "strongSell") case shadow.Consensus == nil: return missingMemberError("StockGradesSummary", "consensus") } + strongBuy, err := decodeCount("StockGradesSummary", "strongBuy", shadow.StrongBuy) + if err != nil { + return err + } + buy, err := decodeCount("StockGradesSummary", "buy", shadow.Buy) + if err != nil { + return err + } + hold, err := decodeCount("StockGradesSummary", "hold", shadow.Hold) + if err != nil { + return err + } + sell, err := decodeCount("StockGradesSummary", "sell", shadow.Sell) + if err != nil { + return err + } + strongSell, err := decodeCount("StockGradesSummary", "strongSell", shadow.StrongSell) + if err != nil { + return err + } *m = StockGradesSummary{ Symbol: *shadow.Symbol, - StrongBuy: *shadow.StrongBuy, - Buy: *shadow.Buy, - Hold: *shadow.Hold, - Sell: *shadow.Sell, - StrongSell: *shadow.StrongSell, + StrongBuy: strongBuy, + Buy: buy, + Hold: hold, + Sell: sell, + StrongSell: strongSell, Consensus: *shadow.Consensus, } return nil diff --git a/sdk/go/company_models.go b/sdk/go/company_models.go index ac192793..2e28789e 100644 --- a/sdk/go/company_models.go +++ b/sdk/go/company_models.go @@ -371,15 +371,15 @@ type EmployeeCount struct { // every required member so a missing or null member is observable after // decoding. type employeeCountShadow struct { - Symbol *string `json:"symbol"` - CIK *string `json:"cik"` - AcceptanceTime *DateTime `json:"acceptanceTime"` - PeriodOfReport *Date `json:"periodOfReport"` - CompanyName *string `json:"companyName"` - FormType *string `json:"formType"` - FilingDate *Date `json:"filingDate"` - EmployeeCount *uint64 `json:"employeeCount"` - Source *string `json:"source"` + Symbol *string `json:"symbol"` + CIK *string `json:"cik"` + AcceptanceTime *DateTime `json:"acceptanceTime"` + PeriodOfReport *Date `json:"periodOfReport"` + CompanyName *string `json:"companyName"` + FormType *string `json:"formType"` + FilingDate *Date `json:"filingDate"` + EmployeeCount jsontext.Value `json:"employeeCount"` + Source *string `json:"source"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -405,11 +405,15 @@ func (m *EmployeeCount) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("EmployeeCount", "formType") case shadow.FilingDate == nil: return missingMemberError("EmployeeCount", "filingDate") - case shadow.EmployeeCount == nil: + case len(shadow.EmployeeCount) == 0: return missingMemberError("EmployeeCount", "employeeCount") case shadow.Source == nil: return missingMemberError("EmployeeCount", "source") } + employeeCount, err := decodeCount("EmployeeCount", "employeeCount", shadow.EmployeeCount) + if err != nil { + return err + } *m = EmployeeCount{ Symbol: *shadow.Symbol, CIK: *shadow.CIK, @@ -418,7 +422,7 @@ func (m *EmployeeCount) UnmarshalJSONFrom(dec *jsontext.Decoder) error { CompanyName: *shadow.CompanyName, FormType: *shadow.FormType, FilingDate: *shadow.FilingDate, - EmployeeCount: *shadow.EmployeeCount, + EmployeeCount: employeeCount, Source: *shadow.Source, } return nil diff --git a/sdk/go/fundraising_models.go b/sdk/go/fundraising_models.go index 6d3f82dc..6cdfd79b 100644 --- a/sdk/go/fundraising_models.go +++ b/sdk/go/fundraising_models.go @@ -185,7 +185,7 @@ type crowdfundingOfferingShadow struct { OverSubscriptionAllocationType *string `json:"overSubscriptionAllocationType"` MaximumOfferingAmount *float64 `json:"maximumOfferingAmount"` OfferingDeadlineDate *USDate `json:"offeringDeadlineDate"` - CurrentNumberOfEmployees *uint64 `json:"currentNumberOfEmployees"` + CurrentNumberOfEmployees jsontext.Value `json:"currentNumberOfEmployees"` TotalAssetMostRecentFiscalYear *float64 `json:"totalAssetMostRecentFiscalYear"` TotalAssetPriorFiscalYear *float64 `json:"totalAssetPriorFiscalYear"` CashAndCashEquivalentMostRecentFiscalYear *float64 `json:"cashAndCashEquiValentMostRecentFiscalYear"` @@ -273,7 +273,7 @@ func (m *CrowdfundingOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("CrowdfundingOffering", "maximumOfferingAmount") case shadow.OfferingDeadlineDate == nil: return missingMemberError("CrowdfundingOffering", "offeringDeadlineDate") - case shadow.CurrentNumberOfEmployees == nil: + case len(shadow.CurrentNumberOfEmployees) == 0: return missingMemberError("CrowdfundingOffering", "currentNumberOfEmployees") case shadow.TotalAssetMostRecentFiscalYear == nil: return missingMemberError("CrowdfundingOffering", "totalAssetMostRecentFiscalYear") @@ -323,6 +323,10 @@ func (m *CrowdfundingOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { if shadow.OfferingPrice.Kind() != '0' { return invalidMemberError("CrowdfundingOffering", "offeringPrice", "number") } + currentNumberOfEmployees, err := decodeCount("CrowdfundingOffering", "currentNumberOfEmployees", shadow.CurrentNumberOfEmployees) + if err != nil { + return err + } *m = CrowdfundingOffering{ CIK: *shadow.CIK, CompanyName: *shadow.CompanyName, @@ -353,7 +357,7 @@ func (m *CrowdfundingOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { OverSubscriptionAllocationType: *shadow.OverSubscriptionAllocationType, MaximumOfferingAmount: *shadow.MaximumOfferingAmount, OfferingDeadlineDate: *shadow.OfferingDeadlineDate, - CurrentNumberOfEmployees: *shadow.CurrentNumberOfEmployees, + CurrentNumberOfEmployees: currentNumberOfEmployees, TotalAssetMostRecentFiscalYear: *shadow.TotalAssetMostRecentFiscalYear, TotalAssetPriorFiscalYear: *shadow.TotalAssetPriorFiscalYear, CashAndCashEquivalentMostRecentFiscalYear: *shadow.CashAndCashEquivalentMostRecentFiscalYear, @@ -466,7 +470,7 @@ type regulationDOfferingShadow struct { TotalAmountSold *float64 `json:"totalAmountSold"` TotalAmountRemaining *float64 `json:"totalAmountRemaining"` HasNonAccreditedInvestors *bool `json:"hasNonAccreditedInvestors"` - TotalNumberAlreadyInvested *uint64 `json:"totalNumberAlreadyInvested"` + TotalNumberAlreadyInvested jsontext.Value `json:"totalNumberAlreadyInvested"` SalesCommissions *float64 `json:"salesCommissions"` FindersFees *float64 `json:"findersFees"` GrossProceedsUsed *float64 `json:"grossProceedsUsed"` @@ -559,7 +563,7 @@ func (m *RegulationDOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("RegulationDOffering", "totalAmountRemaining") case shadow.HasNonAccreditedInvestors == nil: return missingMemberError("RegulationDOffering", "hasNonAccreditedInvestors") - case shadow.TotalNumberAlreadyInvested == nil: + case len(shadow.TotalNumberAlreadyInvested) == 0: return missingMemberError("RegulationDOffering", "totalNumberAlreadyInvested") case shadow.SalesCommissions == nil: return missingMemberError("RegulationDOffering", "salesCommissions") @@ -580,6 +584,10 @@ func (m *RegulationDOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { if err != nil { return err } + totalNumberAlreadyInvested, err := decodeCount("RegulationDOffering", "totalNumberAlreadyInvested", shadow.TotalNumberAlreadyInvested) + if err != nil { + return err + } *m = RegulationDOffering{ CIK: *shadow.CIK, CompanyName: *shadow.CompanyName, @@ -620,7 +628,7 @@ func (m *RegulationDOffering) UnmarshalJSONFrom(dec *jsontext.Decoder) error { TotalAmountSold: *shadow.TotalAmountSold, TotalAmountRemaining: *shadow.TotalAmountRemaining, HasNonAccreditedInvestors: *shadow.HasNonAccreditedInvestors, - TotalNumberAlreadyInvested: *shadow.TotalNumberAlreadyInvested, + TotalNumberAlreadyInvested: totalNumberAlreadyInvested, SalesCommissions: *shadow.SalesCommissions, FindersFees: *shadow.FindersFees, GrossProceedsUsed: *shadow.GrossProceedsUsed, diff --git a/sdk/go/funds_models.go b/sdk/go/funds_models.go index 138ec1a8..61f3f656 100644 --- a/sdk/go/funds_models.go +++ b/sdk/go/funds_models.go @@ -158,7 +158,7 @@ type etfFundInfoShadow struct { InceptionDate *Date `json:"inceptionDate"` Nav *float64 `json:"nav"` NavCurrency *string `json:"navCurrency"` - HoldingsCount *uint64 `json:"holdingsCount"` + HoldingsCount jsontext.Value `json:"holdingsCount"` IsActivelyTrading *bool `json:"isActivelyTrading"` UpdatedAt *string `json:"updatedAt"` SectorsList *[]ETFSectorExposure `json:"sectorsList"` @@ -203,7 +203,7 @@ func (m *ETFFundInfo) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("ETFFundInfo", "nav") case shadow.NavCurrency == nil: return missingMemberError("ETFFundInfo", "navCurrency") - case shadow.HoldingsCount == nil: + case len(shadow.HoldingsCount) == 0: return missingMemberError("ETFFundInfo", "holdingsCount") case shadow.IsActivelyTrading == nil: return missingMemberError("ETFFundInfo", "isActivelyTrading") @@ -212,6 +212,10 @@ func (m *ETFFundInfo) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.SectorsList == nil: return missingMemberError("ETFFundInfo", "sectorsList") } + holdingsCount, err := decodeCount("ETFFundInfo", "holdingsCount", shadow.HoldingsCount) + if err != nil { + return err + } *m = ETFFundInfo{ Symbol: *shadow.Symbol, Name: *shadow.Name, @@ -228,7 +232,7 @@ func (m *ETFFundInfo) UnmarshalJSONFrom(dec *jsontext.Decoder) error { InceptionDate: *shadow.InceptionDate, Nav: *shadow.Nav, NavCurrency: *shadow.NavCurrency, - HoldingsCount: *shadow.HoldingsCount, + HoldingsCount: holdingsCount, IsActivelyTrading: *shadow.IsActivelyTrading, UpdatedAt: *shadow.UpdatedAt, SectorsList: *shadow.SectorsList, diff --git a/sdk/go/insider_trading_models.go b/sdk/go/insider_trading_models.go index bcc782d8..c47d46c6 100644 --- a/sdk/go/insider_trading_models.go +++ b/sdk/go/insider_trading_models.go @@ -202,19 +202,19 @@ type InsiderTradeStatistics struct { // or raw value for every required member so a missing or null member is // observable after decoding. type insiderTradeStatisticsShadow struct { - Symbol *string `json:"symbol"` - CIK *string `json:"cik"` - Year *uint32 `json:"year"` - Quarter *uint8 `json:"quarter"` - AcquiredTransactions *uint64 `json:"acquiredTransactions"` - DisposedTransactions *uint64 `json:"disposedTransactions"` - AcquiredDisposedRatio *float64 `json:"acquiredDisposedRatio"` - TotalAcquired *float64 `json:"totalAcquired"` - TotalDisposed *float64 `json:"totalDisposed"` - AverageAcquired *float64 `json:"averageAcquired"` - AverageDisposed *float64 `json:"averageDisposed"` - TotalPurchases *uint64 `json:"totalPurchases"` - TotalSales *uint64 `json:"totalSales"` + Symbol *string `json:"symbol"` + CIK *string `json:"cik"` + Year *uint32 `json:"year"` + Quarter *uint8 `json:"quarter"` + AcquiredTransactions jsontext.Value `json:"acquiredTransactions"` + DisposedTransactions jsontext.Value `json:"disposedTransactions"` + AcquiredDisposedRatio *float64 `json:"acquiredDisposedRatio"` + TotalAcquired *float64 `json:"totalAcquired"` + TotalDisposed *float64 `json:"totalDisposed"` + AverageAcquired *float64 `json:"averageAcquired"` + AverageDisposed *float64 `json:"averageDisposed"` + TotalPurchases jsontext.Value `json:"totalPurchases"` + TotalSales jsontext.Value `json:"totalSales"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -234,9 +234,9 @@ func (m *InsiderTradeStatistics) UnmarshalJSONFrom(dec *jsontext.Decoder) error return missingMemberError("InsiderTradeStatistics", "year") case shadow.Quarter == nil: return missingMemberError("InsiderTradeStatistics", "quarter") - case shadow.AcquiredTransactions == nil: + case len(shadow.AcquiredTransactions) == 0: return missingMemberError("InsiderTradeStatistics", "acquiredTransactions") - case shadow.DisposedTransactions == nil: + case len(shadow.DisposedTransactions) == 0: return missingMemberError("InsiderTradeStatistics", "disposedTransactions") case shadow.AcquiredDisposedRatio == nil: return missingMemberError("InsiderTradeStatistics", "acquiredDisposedRatio") @@ -248,25 +248,41 @@ func (m *InsiderTradeStatistics) UnmarshalJSONFrom(dec *jsontext.Decoder) error return missingMemberError("InsiderTradeStatistics", "averageAcquired") case shadow.AverageDisposed == nil: return missingMemberError("InsiderTradeStatistics", "averageDisposed") - case shadow.TotalPurchases == nil: + case len(shadow.TotalPurchases) == 0: return missingMemberError("InsiderTradeStatistics", "totalPurchases") - case shadow.TotalSales == nil: + case len(shadow.TotalSales) == 0: return missingMemberError("InsiderTradeStatistics", "totalSales") } + acquiredTransactions, err := decodeCount("InsiderTradeStatistics", "acquiredTransactions", shadow.AcquiredTransactions) + if err != nil { + return err + } + disposedTransactions, err := decodeCount("InsiderTradeStatistics", "disposedTransactions", shadow.DisposedTransactions) + if err != nil { + return err + } + totalPurchases, err := decodeCount("InsiderTradeStatistics", "totalPurchases", shadow.TotalPurchases) + if err != nil { + return err + } + totalSales, err := decodeCount("InsiderTradeStatistics", "totalSales", shadow.TotalSales) + if err != nil { + return err + } *m = InsiderTradeStatistics{ Symbol: *shadow.Symbol, CIK: *shadow.CIK, Year: *shadow.Year, Quarter: *shadow.Quarter, - AcquiredTransactions: *shadow.AcquiredTransactions, - DisposedTransactions: *shadow.DisposedTransactions, + AcquiredTransactions: acquiredTransactions, + DisposedTransactions: disposedTransactions, AcquiredDisposedRatio: *shadow.AcquiredDisposedRatio, TotalAcquired: *shadow.TotalAcquired, TotalDisposed: *shadow.TotalDisposed, AverageAcquired: *shadow.AverageAcquired, AverageDisposed: *shadow.AverageDisposed, - TotalPurchases: *shadow.TotalPurchases, - TotalSales: *shadow.TotalSales, + TotalPurchases: totalPurchases, + TotalSales: totalSales, } return nil } diff --git a/sdk/go/statements_models.go b/sdk/go/statements_models.go index 4a7bd8c5..8a73da3c 100644 --- a/sdk/go/statements_models.go +++ b/sdk/go/statements_models.go @@ -3605,17 +3605,17 @@ type FinancialScore struct { // every required member so a missing or null member is observable after // decoding. type financialScoreShadow struct { - Symbol *string `json:"symbol"` - ReportedCurrency *string `json:"reportedCurrency"` - AltmanZScore *float64 `json:"altmanZScore"` - PiotroskiScore *uint64 `json:"piotroskiScore"` - WorkingCapital *float64 `json:"workingCapital"` - TotalAssets *float64 `json:"totalAssets"` - RetainedEarnings *float64 `json:"retainedEarnings"` - Ebit *float64 `json:"ebit"` - MarketCap *float64 `json:"marketCap"` - TotalLiabilities *float64 `json:"totalLiabilities"` - Revenue *float64 `json:"revenue"` + Symbol *string `json:"symbol"` + ReportedCurrency *string `json:"reportedCurrency"` + AltmanZScore *float64 `json:"altmanZScore"` + PiotroskiScore jsontext.Value `json:"piotroskiScore"` + WorkingCapital *float64 `json:"workingCapital"` + TotalAssets *float64 `json:"totalAssets"` + RetainedEarnings *float64 `json:"retainedEarnings"` + Ebit *float64 `json:"ebit"` + MarketCap *float64 `json:"marketCap"` + TotalLiabilities *float64 `json:"totalLiabilities"` + Revenue *float64 `json:"revenue"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -3633,7 +3633,7 @@ func (m *FinancialScore) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("FinancialScore", "reportedCurrency") case shadow.AltmanZScore == nil: return missingMemberError("FinancialScore", "altmanZScore") - case shadow.PiotroskiScore == nil: + case len(shadow.PiotroskiScore) == 0: return missingMemberError("FinancialScore", "piotroskiScore") case shadow.WorkingCapital == nil: return missingMemberError("FinancialScore", "workingCapital") @@ -3650,11 +3650,15 @@ func (m *FinancialScore) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.Revenue == nil: return missingMemberError("FinancialScore", "revenue") } + piotroskiScore, err := decodeCount("FinancialScore", "piotroskiScore", shadow.PiotroskiScore) + if err != nil { + return err + } *m = FinancialScore{ Symbol: *shadow.Symbol, ReportedCurrency: *shadow.ReportedCurrency, AltmanZScore: *shadow.AltmanZScore, - PiotroskiScore: *shadow.PiotroskiScore, + PiotroskiScore: piotroskiScore, WorkingCapital: *shadow.WorkingCapital, TotalAssets: *shadow.TotalAssets, RetainedEarnings: *shadow.RetainedEarnings, diff --git a/sdk/go/tipranks_models.go b/sdk/go/tipranks_models.go index bdfd189a..e1d7d615 100644 --- a/sdk/go/tipranks_models.go +++ b/sdk/go/tipranks_models.go @@ -255,9 +255,9 @@ type TipRanksRecommendationCounts struct { // a pointer or raw value for every required member so a missing or null member // is observable after decoding. type tipRanksRecommendationCountsShadow struct { - Buy *uint64 `json:"buy"` - Hold *uint64 `json:"hold"` - Sell *uint64 `json:"sell"` + Buy jsontext.Value `json:"buy"` + Hold jsontext.Value `json:"hold"` + Sell jsontext.Value `json:"sell"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -269,17 +269,29 @@ func (m *TipRanksRecommendationCounts) UnmarshalJSONFrom(dec *jsontext.Decoder) return err } switch { - case shadow.Buy == nil: + case len(shadow.Buy) == 0: return missingMemberError("TipRanksRecommendationCounts", "buy") - case shadow.Hold == nil: + case len(shadow.Hold) == 0: return missingMemberError("TipRanksRecommendationCounts", "hold") - case shadow.Sell == nil: + case len(shadow.Sell) == 0: return missingMemberError("TipRanksRecommendationCounts", "sell") } + buy, err := decodeCount("TipRanksRecommendationCounts", "buy", shadow.Buy) + if err != nil { + return err + } + hold, err := decodeCount("TipRanksRecommendationCounts", "hold", shadow.Hold) + if err != nil { + return err + } + sell, err := decodeCount("TipRanksRecommendationCounts", "sell", shadow.Sell) + if err != nil { + return err + } *m = TipRanksRecommendationCounts{ - Buy: *shadow.Buy, - Hold: *shadow.Hold, - Sell: *shadow.Sell, + Buy: buy, + Hold: hold, + Sell: sell, } return nil } @@ -299,12 +311,12 @@ type TipRanksAnalystActionCounts struct { // pointer or raw value for every required member so a missing or null member // is observable after decoding. type tipRanksAnalystActionCountsShadow struct { - Initiated *uint64 `json:"initiated"` - Maintained *uint64 `json:"maintained"` - Upgraded *uint64 `json:"upgraded"` - Downgraded *uint64 `json:"downgraded"` - Reiterated *uint64 `json:"reiterated"` - Resumed *uint64 `json:"resumed"` + Initiated jsontext.Value `json:"initiated"` + Maintained jsontext.Value `json:"maintained"` + Upgraded jsontext.Value `json:"upgraded"` + Downgraded jsontext.Value `json:"downgraded"` + Reiterated jsontext.Value `json:"reiterated"` + Resumed jsontext.Value `json:"resumed"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -316,26 +328,50 @@ func (m *TipRanksAnalystActionCounts) UnmarshalJSONFrom(dec *jsontext.Decoder) e return err } switch { - case shadow.Initiated == nil: + case len(shadow.Initiated) == 0: return missingMemberError("TipRanksAnalystActionCounts", "initiated") - case shadow.Maintained == nil: + case len(shadow.Maintained) == 0: return missingMemberError("TipRanksAnalystActionCounts", "maintained") - case shadow.Upgraded == nil: + case len(shadow.Upgraded) == 0: return missingMemberError("TipRanksAnalystActionCounts", "upgraded") - case shadow.Downgraded == nil: + case len(shadow.Downgraded) == 0: return missingMemberError("TipRanksAnalystActionCounts", "downgraded") - case shadow.Reiterated == nil: + case len(shadow.Reiterated) == 0: return missingMemberError("TipRanksAnalystActionCounts", "reiterated") - case shadow.Resumed == nil: + case len(shadow.Resumed) == 0: return missingMemberError("TipRanksAnalystActionCounts", "resumed") } + initiated, err := decodeCount("TipRanksAnalystActionCounts", "initiated", shadow.Initiated) + if err != nil { + return err + } + maintained, err := decodeCount("TipRanksAnalystActionCounts", "maintained", shadow.Maintained) + if err != nil { + return err + } + upgraded, err := decodeCount("TipRanksAnalystActionCounts", "upgraded", shadow.Upgraded) + if err != nil { + return err + } + downgraded, err := decodeCount("TipRanksAnalystActionCounts", "downgraded", shadow.Downgraded) + if err != nil { + return err + } + reiterated, err := decodeCount("TipRanksAnalystActionCounts", "reiterated", shadow.Reiterated) + if err != nil { + return err + } + resumed, err := decodeCount("TipRanksAnalystActionCounts", "resumed", shadow.Resumed) + if err != nil { + return err + } *m = TipRanksAnalystActionCounts{ - Initiated: *shadow.Initiated, - Maintained: *shadow.Maintained, - Upgraded: *shadow.Upgraded, - Downgraded: *shadow.Downgraded, - Reiterated: *shadow.Reiterated, - Resumed: *shadow.Resumed, + Initiated: initiated, + Maintained: maintained, + Upgraded: upgraded, + Downgraded: downgraded, + Reiterated: reiterated, + Resumed: resumed, } return nil } @@ -367,15 +403,15 @@ type tipRanksSymbolSummaryShadow struct { Symbol *string `json:"symbol"` From *Date `json:"from"` To *Date `json:"to"` - TotalRecommendations *uint64 `json:"totalRecommendations"` - DistinctSymbols *uint64 `json:"distinctSymbols"` - DistinctAnalysts *uint64 `json:"distinctAnalysts"` - ValidPriceTargets *uint64 `json:"validPriceTargets"` + TotalRecommendations jsontext.Value `json:"totalRecommendations"` + DistinctSymbols jsontext.Value `json:"distinctSymbols"` + DistinctAnalysts jsontext.Value `json:"distinctAnalysts"` + ValidPriceTargets jsontext.Value `json:"validPriceTargets"` Recommendations *TipRanksRecommendationCounts `json:"recommendations"` AnalystAction *TipRanksAnalystActionCounts `json:"analystAction"` - ComparedPriceTargets *uint64 `json:"comparedPriceTargets"` - Beats *uint64 `json:"beats"` - Misses *uint64 `json:"misses"` + ComparedPriceTargets jsontext.Value `json:"comparedPriceTargets"` + Beats jsontext.Value `json:"beats"` + Misses jsontext.Value `json:"misses"` AverageReturn *jsontext.Value `json:"averageReturn"` TopReturn *jsontext.Value `json:"topReturn"` WorstReturn *jsontext.Value `json:"worstReturn"` @@ -396,23 +432,23 @@ func (m *TipRanksSymbolSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("TipRanksSymbolSummary", "from") case shadow.To == nil: return missingMemberError("TipRanksSymbolSummary", "to") - case shadow.TotalRecommendations == nil: + case len(shadow.TotalRecommendations) == 0: return missingMemberError("TipRanksSymbolSummary", "totalRecommendations") - case shadow.DistinctSymbols == nil: + case len(shadow.DistinctSymbols) == 0: return missingMemberError("TipRanksSymbolSummary", "distinctSymbols") - case shadow.DistinctAnalysts == nil: + case len(shadow.DistinctAnalysts) == 0: return missingMemberError("TipRanksSymbolSummary", "distinctAnalysts") - case shadow.ValidPriceTargets == nil: + case len(shadow.ValidPriceTargets) == 0: return missingMemberError("TipRanksSymbolSummary", "validPriceTargets") case shadow.Recommendations == nil: return missingMemberError("TipRanksSymbolSummary", "recommendations") case shadow.AnalystAction == nil: return missingMemberError("TipRanksSymbolSummary", "analystAction") - case shadow.ComparedPriceTargets == nil: + case len(shadow.ComparedPriceTargets) == 0: return missingMemberError("TipRanksSymbolSummary", "comparedPriceTargets") - case shadow.Beats == nil: + case len(shadow.Beats) == 0: return missingMemberError("TipRanksSymbolSummary", "beats") - case shadow.Misses == nil: + case len(shadow.Misses) == 0: return missingMemberError("TipRanksSymbolSummary", "misses") case shadow.AverageReturn == nil: return missingMemberError("TipRanksSymbolSummary", "averageReturn") @@ -421,6 +457,34 @@ func (m *TipRanksSymbolSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.WorstReturn == nil: return missingMemberError("TipRanksSymbolSummary", "worstReturn") } + totalRecommendations, err := decodeCount("TipRanksSymbolSummary", "totalRecommendations", shadow.TotalRecommendations) + if err != nil { + return err + } + distinctSymbols, err := decodeCount("TipRanksSymbolSummary", "distinctSymbols", shadow.DistinctSymbols) + if err != nil { + return err + } + distinctAnalysts, err := decodeCount("TipRanksSymbolSummary", "distinctAnalysts", shadow.DistinctAnalysts) + if err != nil { + return err + } + validPriceTargets, err := decodeCount("TipRanksSymbolSummary", "validPriceTargets", shadow.ValidPriceTargets) + if err != nil { + return err + } + comparedPriceTargets, err := decodeCount("TipRanksSymbolSummary", "comparedPriceTargets", shadow.ComparedPriceTargets) + if err != nil { + return err + } + beats, err := decodeCount("TipRanksSymbolSummary", "beats", shadow.Beats) + if err != nil { + return err + } + misses, err := decodeCount("TipRanksSymbolSummary", "misses", shadow.Misses) + if err != nil { + return err + } if shadow.AverageReturn.Kind() != '0' { return invalidMemberError("TipRanksSymbolSummary", "averageReturn", "number") } @@ -434,15 +498,15 @@ func (m *TipRanksSymbolSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { Symbol: *shadow.Symbol, From: *shadow.From, To: *shadow.To, - TotalRecommendations: *shadow.TotalRecommendations, - DistinctSymbols: *shadow.DistinctSymbols, - DistinctAnalysts: *shadow.DistinctAnalysts, - ValidPriceTargets: *shadow.ValidPriceTargets, + TotalRecommendations: totalRecommendations, + DistinctSymbols: distinctSymbols, + DistinctAnalysts: distinctAnalysts, + ValidPriceTargets: validPriceTargets, Recommendations: *shadow.Recommendations, AnalystAction: *shadow.AnalystAction, - ComparedPriceTargets: *shadow.ComparedPriceTargets, - Beats: *shadow.Beats, - Misses: *shadow.Misses, + ComparedPriceTargets: comparedPriceTargets, + Beats: beats, + Misses: misses, AverageReturn: *shadow.AverageReturn, TopReturn: *shadow.TopReturn, WorstReturn: *shadow.WorstReturn, @@ -477,15 +541,15 @@ type tipRanksAnalystSummaryShadow struct { ExpertUID *string `json:"expertUID"` From *Date `json:"from"` To *Date `json:"to"` - TotalRecommendations *uint64 `json:"totalRecommendations"` - DistinctSymbols *uint64 `json:"distinctSymbols"` - DistinctAnalysts *uint64 `json:"distinctAnalysts"` - ValidPriceTargets *uint64 `json:"validPriceTargets"` + TotalRecommendations jsontext.Value `json:"totalRecommendations"` + DistinctSymbols jsontext.Value `json:"distinctSymbols"` + DistinctAnalysts jsontext.Value `json:"distinctAnalysts"` + ValidPriceTargets jsontext.Value `json:"validPriceTargets"` Recommendations *TipRanksRecommendationCounts `json:"recommendations"` AnalystAction *TipRanksAnalystActionCounts `json:"analystAction"` - ComparedPriceTargets *uint64 `json:"comparedPriceTargets"` - Beats *uint64 `json:"beats"` - Misses *uint64 `json:"misses"` + ComparedPriceTargets jsontext.Value `json:"comparedPriceTargets"` + Beats jsontext.Value `json:"beats"` + Misses jsontext.Value `json:"misses"` AverageReturn *jsontext.Value `json:"averageReturn"` TopReturn *jsontext.Value `json:"topReturn"` WorstReturn *jsontext.Value `json:"worstReturn"` @@ -506,23 +570,23 @@ func (m *TipRanksAnalystSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error return missingMemberError("TipRanksAnalystSummary", "from") case shadow.To == nil: return missingMemberError("TipRanksAnalystSummary", "to") - case shadow.TotalRecommendations == nil: + case len(shadow.TotalRecommendations) == 0: return missingMemberError("TipRanksAnalystSummary", "totalRecommendations") - case shadow.DistinctSymbols == nil: + case len(shadow.DistinctSymbols) == 0: return missingMemberError("TipRanksAnalystSummary", "distinctSymbols") - case shadow.DistinctAnalysts == nil: + case len(shadow.DistinctAnalysts) == 0: return missingMemberError("TipRanksAnalystSummary", "distinctAnalysts") - case shadow.ValidPriceTargets == nil: + case len(shadow.ValidPriceTargets) == 0: return missingMemberError("TipRanksAnalystSummary", "validPriceTargets") case shadow.Recommendations == nil: return missingMemberError("TipRanksAnalystSummary", "recommendations") case shadow.AnalystAction == nil: return missingMemberError("TipRanksAnalystSummary", "analystAction") - case shadow.ComparedPriceTargets == nil: + case len(shadow.ComparedPriceTargets) == 0: return missingMemberError("TipRanksAnalystSummary", "comparedPriceTargets") - case shadow.Beats == nil: + case len(shadow.Beats) == 0: return missingMemberError("TipRanksAnalystSummary", "beats") - case shadow.Misses == nil: + case len(shadow.Misses) == 0: return missingMemberError("TipRanksAnalystSummary", "misses") case shadow.AverageReturn == nil: return missingMemberError("TipRanksAnalystSummary", "averageReturn") @@ -531,6 +595,34 @@ func (m *TipRanksAnalystSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error case shadow.WorstReturn == nil: return missingMemberError("TipRanksAnalystSummary", "worstReturn") } + totalRecommendations, err := decodeCount("TipRanksAnalystSummary", "totalRecommendations", shadow.TotalRecommendations) + if err != nil { + return err + } + distinctSymbols, err := decodeCount("TipRanksAnalystSummary", "distinctSymbols", shadow.DistinctSymbols) + if err != nil { + return err + } + distinctAnalysts, err := decodeCount("TipRanksAnalystSummary", "distinctAnalysts", shadow.DistinctAnalysts) + if err != nil { + return err + } + validPriceTargets, err := decodeCount("TipRanksAnalystSummary", "validPriceTargets", shadow.ValidPriceTargets) + if err != nil { + return err + } + comparedPriceTargets, err := decodeCount("TipRanksAnalystSummary", "comparedPriceTargets", shadow.ComparedPriceTargets) + if err != nil { + return err + } + beats, err := decodeCount("TipRanksAnalystSummary", "beats", shadow.Beats) + if err != nil { + return err + } + misses, err := decodeCount("TipRanksAnalystSummary", "misses", shadow.Misses) + if err != nil { + return err + } if shadow.AverageReturn.Kind() != '0' { return invalidMemberError("TipRanksAnalystSummary", "averageReturn", "number") } @@ -544,15 +636,15 @@ func (m *TipRanksAnalystSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error ExpertUID: *shadow.ExpertUID, From: *shadow.From, To: *shadow.To, - TotalRecommendations: *shadow.TotalRecommendations, - DistinctSymbols: *shadow.DistinctSymbols, - DistinctAnalysts: *shadow.DistinctAnalysts, - ValidPriceTargets: *shadow.ValidPriceTargets, + TotalRecommendations: totalRecommendations, + DistinctSymbols: distinctSymbols, + DistinctAnalysts: distinctAnalysts, + ValidPriceTargets: validPriceTargets, Recommendations: *shadow.Recommendations, AnalystAction: *shadow.AnalystAction, - ComparedPriceTargets: *shadow.ComparedPriceTargets, - Beats: *shadow.Beats, - Misses: *shadow.Misses, + ComparedPriceTargets: comparedPriceTargets, + Beats: beats, + Misses: misses, AverageReturn: *shadow.AverageReturn, TopReturn: *shadow.TopReturn, WorstReturn: *shadow.WorstReturn, @@ -587,15 +679,15 @@ type tipRanksFirmSummaryShadow struct { FirmName *string `json:"firmName"` From *Date `json:"from"` To *Date `json:"to"` - TotalRecommendations *uint64 `json:"totalRecommendations"` - DistinctSymbols *uint64 `json:"distinctSymbols"` - DistinctAnalysts *uint64 `json:"distinctAnalysts"` - ValidPriceTargets *uint64 `json:"validPriceTargets"` + TotalRecommendations jsontext.Value `json:"totalRecommendations"` + DistinctSymbols jsontext.Value `json:"distinctSymbols"` + DistinctAnalysts jsontext.Value `json:"distinctAnalysts"` + ValidPriceTargets jsontext.Value `json:"validPriceTargets"` Recommendations *TipRanksRecommendationCounts `json:"recommendations"` AnalystAction *TipRanksAnalystActionCounts `json:"analystAction"` - ComparedPriceTargets *uint64 `json:"comparedPriceTargets"` - Beats *uint64 `json:"beats"` - Misses *uint64 `json:"misses"` + ComparedPriceTargets jsontext.Value `json:"comparedPriceTargets"` + Beats jsontext.Value `json:"beats"` + Misses jsontext.Value `json:"misses"` AverageReturn *jsontext.Value `json:"averageReturn"` TopReturn *jsontext.Value `json:"topReturn"` WorstReturn *jsontext.Value `json:"worstReturn"` @@ -616,23 +708,23 @@ func (m *TipRanksFirmSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return missingMemberError("TipRanksFirmSummary", "from") case shadow.To == nil: return missingMemberError("TipRanksFirmSummary", "to") - case shadow.TotalRecommendations == nil: + case len(shadow.TotalRecommendations) == 0: return missingMemberError("TipRanksFirmSummary", "totalRecommendations") - case shadow.DistinctSymbols == nil: + case len(shadow.DistinctSymbols) == 0: return missingMemberError("TipRanksFirmSummary", "distinctSymbols") - case shadow.DistinctAnalysts == nil: + case len(shadow.DistinctAnalysts) == 0: return missingMemberError("TipRanksFirmSummary", "distinctAnalysts") - case shadow.ValidPriceTargets == nil: + case len(shadow.ValidPriceTargets) == 0: return missingMemberError("TipRanksFirmSummary", "validPriceTargets") case shadow.Recommendations == nil: return missingMemberError("TipRanksFirmSummary", "recommendations") case shadow.AnalystAction == nil: return missingMemberError("TipRanksFirmSummary", "analystAction") - case shadow.ComparedPriceTargets == nil: + case len(shadow.ComparedPriceTargets) == 0: return missingMemberError("TipRanksFirmSummary", "comparedPriceTargets") - case shadow.Beats == nil: + case len(shadow.Beats) == 0: return missingMemberError("TipRanksFirmSummary", "beats") - case shadow.Misses == nil: + case len(shadow.Misses) == 0: return missingMemberError("TipRanksFirmSummary", "misses") case shadow.AverageReturn == nil: return missingMemberError("TipRanksFirmSummary", "averageReturn") @@ -641,6 +733,34 @@ func (m *TipRanksFirmSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { case shadow.WorstReturn == nil: return missingMemberError("TipRanksFirmSummary", "worstReturn") } + totalRecommendations, err := decodeCount("TipRanksFirmSummary", "totalRecommendations", shadow.TotalRecommendations) + if err != nil { + return err + } + distinctSymbols, err := decodeCount("TipRanksFirmSummary", "distinctSymbols", shadow.DistinctSymbols) + if err != nil { + return err + } + distinctAnalysts, err := decodeCount("TipRanksFirmSummary", "distinctAnalysts", shadow.DistinctAnalysts) + if err != nil { + return err + } + validPriceTargets, err := decodeCount("TipRanksFirmSummary", "validPriceTargets", shadow.ValidPriceTargets) + if err != nil { + return err + } + comparedPriceTargets, err := decodeCount("TipRanksFirmSummary", "comparedPriceTargets", shadow.ComparedPriceTargets) + if err != nil { + return err + } + beats, err := decodeCount("TipRanksFirmSummary", "beats", shadow.Beats) + if err != nil { + return err + } + misses, err := decodeCount("TipRanksFirmSummary", "misses", shadow.Misses) + if err != nil { + return err + } if shadow.AverageReturn.Kind() != '0' { return invalidMemberError("TipRanksFirmSummary", "averageReturn", "number") } @@ -654,15 +774,15 @@ func (m *TipRanksFirmSummary) UnmarshalJSONFrom(dec *jsontext.Decoder) error { FirmName: *shadow.FirmName, From: *shadow.From, To: *shadow.To, - TotalRecommendations: *shadow.TotalRecommendations, - DistinctSymbols: *shadow.DistinctSymbols, - DistinctAnalysts: *shadow.DistinctAnalysts, - ValidPriceTargets: *shadow.ValidPriceTargets, + TotalRecommendations: totalRecommendations, + DistinctSymbols: distinctSymbols, + DistinctAnalysts: distinctAnalysts, + ValidPriceTargets: validPriceTargets, Recommendations: *shadow.Recommendations, AnalystAction: *shadow.AnalystAction, - ComparedPriceTargets: *shadow.ComparedPriceTargets, - Beats: *shadow.Beats, - Misses: *shadow.Misses, + ComparedPriceTargets: comparedPriceTargets, + Beats: beats, + Misses: misses, AverageReturn: *shadow.AverageReturn, TopReturn: *shadow.TopReturn, WorstReturn: *shadow.WorstReturn, @@ -693,10 +813,10 @@ type tipRanksAnalystProfileShadow struct { FirmName *string `json:"firmName"` SuccessRate *jsontext.Value `json:"successRate"` ExcessReturn *jsontext.Value `json:"excessReturn"` - TotalRecommendations *uint64 `json:"totalRecommendations"` - GoodRecommendations *uint64 `json:"goodRecommendations"` - AnalystRank *uint64 `json:"analystRank"` - NumOfStars *uint64 `json:"numOfStars"` + TotalRecommendations jsontext.Value `json:"totalRecommendations"` + GoodRecommendations jsontext.Value `json:"goodRecommendations"` + AnalystRank jsontext.Value `json:"analystRank"` + NumOfStars jsontext.Value `json:"numOfStars"` } // UnmarshalJSONFrom decodes one JSON object and rejects it with a Decode @@ -718,13 +838,13 @@ func (m *TipRanksAnalystProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error return missingMemberError("TipRanksAnalystProfile", "successRate") case shadow.ExcessReturn == nil: return missingMemberError("TipRanksAnalystProfile", "excessReturn") - case shadow.TotalRecommendations == nil: + case len(shadow.TotalRecommendations) == 0: return missingMemberError("TipRanksAnalystProfile", "totalRecommendations") - case shadow.GoodRecommendations == nil: + case len(shadow.GoodRecommendations) == 0: return missingMemberError("TipRanksAnalystProfile", "goodRecommendations") - case shadow.AnalystRank == nil: + case len(shadow.AnalystRank) == 0: return missingMemberError("TipRanksAnalystProfile", "analystRank") - case shadow.NumOfStars == nil: + case len(shadow.NumOfStars) == 0: return missingMemberError("TipRanksAnalystProfile", "numOfStars") } if shadow.SuccessRate.Kind() != '0' { @@ -733,16 +853,32 @@ func (m *TipRanksAnalystProfile) UnmarshalJSONFrom(dec *jsontext.Decoder) error if shadow.ExcessReturn.Kind() != '0' { return invalidMemberError("TipRanksAnalystProfile", "excessReturn", "number") } + totalRecommendations, err := decodeCount("TipRanksAnalystProfile", "totalRecommendations", shadow.TotalRecommendations) + if err != nil { + return err + } + goodRecommendations, err := decodeCount("TipRanksAnalystProfile", "goodRecommendations", shadow.GoodRecommendations) + if err != nil { + return err + } + analystRank, err := decodeCount("TipRanksAnalystProfile", "analystRank", shadow.AnalystRank) + if err != nil { + return err + } + numOfStars, err := decodeCount("TipRanksAnalystProfile", "numOfStars", shadow.NumOfStars) + if err != nil { + return err + } *m = TipRanksAnalystProfile{ ExpertUID: *shadow.ExpertUID, AnalystName: *shadow.AnalystName, FirmName: *shadow.FirmName, SuccessRate: *shadow.SuccessRate, ExcessReturn: *shadow.ExcessReturn, - TotalRecommendations: *shadow.TotalRecommendations, - GoodRecommendations: *shadow.GoodRecommendations, - AnalystRank: *shadow.AnalystRank, - NumOfStars: *shadow.NumOfStars, + TotalRecommendations: totalRecommendations, + GoodRecommendations: goodRecommendations, + AnalystRank: analystRank, + NumOfStars: numOfStars, } return nil } From 3d83788afaa7fff87b46365768194e470d03f63c Mon Sep 17 00:00:00 2001 From: benbenbang Date: Tue, 29 Sep 2026 09:18:13 +0800 Subject: [PATCH 3/3] fix(libfmp): guard Count decoding against fractional floats This commit adds coverage that verifies Count members accept integral floats such as 3.0 while rejecting fractional values such as 2.9. **Count source-scan test:** * Added `crates/libfmp/tests/count_fields.rs` to scan the Count source and enforce the decoding contract. **Decode tests across SDKs:** * Added a Rust test in `analyst_stock_grades_endpoints.rs` asserting `3.0` decodes to `3` and `2.9` raises a decode error for `grades-consensus`. * Added a Go test in `analyst_test.go` covering the same integral-float and fractional-count behaviour. * Added Python tests in `test_analyst.py` confirming `3.0` decodes to an `int` and `2.9` maps to `FmpDecodeError`. close #367 --- crates/fmp-py/tests/test_analyst.py | 19 +++ .../tests/analyst_stock_grades_endpoints.rs | 28 ++++ crates/libfmp/tests/count_fields.rs | 123 ++++++++++++++++++ sdk/go/analyst_test.go | 18 +++ 4 files changed, 188 insertions(+) create mode 100644 crates/libfmp/tests/count_fields.rs diff --git a/crates/fmp-py/tests/test_analyst.py b/crates/fmp-py/tests/test_analyst.py index c3e7cc71..5464f53f 100644 --- a/crates/fmp-py/tests/test_analyst.py +++ b/crates/fmp-py/tests/test_analyst.py @@ -220,6 +220,25 @@ def test_stock_grades_summary_takes_one_symbol(client: Any, fixture_server: Fixt assert row.consensus == "Buy" +def test_stock_grades_summary_accepts_an_integral_float_count(client: Any, fixture_server: FixtureServer) -> None: + """A count sent as an integral float such as ``3.0`` decodes to the ``int`` ``3``.""" + fixture_server.route("/grades-consensus", [{**load_fixture("stock_grades_summary.json")[0], "buy": 3.0}]) + rows = client.analyst.stock_grades_summary("AAPL") + + assert rows[0].buy == 3 + assert type(rows[0].buy) is int + + +def test_stock_grades_summary_rejects_a_fractional_count( + client: Any, fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """A fractional count such as ``2.9`` maps to ``FmpDecodeError`` with the endpoint id.""" + fixture_server.route("/grades-consensus", [{**load_fixture("stock_grades_summary.json")[0], "buy": 2.9}]) + with pytest.raises(errors.FmpDecodeError) as raised: + client.analyst.stock_grades_summary("AAPL") + assert raised.value.endpoint == "grades-consensus" + + def test_empty_array_decodes_to_an_empty_list(client: Any, fixture_server: FixtureServer) -> None: """A bare ``[]`` body is a valid, empty result.""" fixture_server.route("/grades", []) diff --git a/crates/libfmp/tests/analyst_stock_grades_endpoints.rs b/crates/libfmp/tests/analyst_stock_grades_endpoints.rs index 3f25f784..9226adf1 100644 --- a/crates/libfmp/tests/analyst_stock_grades_endpoints.rs +++ b/crates/libfmp/tests/analyst_stock_grades_endpoints.rs @@ -262,6 +262,34 @@ async fn malformed_non_array_responses_keep_each_endpoint_identity() { assert_decode_error(&summary_error, "grades-consensus"); } +#[tokio::test] +async fn count_members_accept_integral_floats_and_reject_fractions() { + let executor = Arc::new(FixtureExecutor::new([ + json_fixture( + br#"[{"symbol":"AAPL","strongBuy":1,"buy":3.0,"hold":32,"sell":8,"strongSell":0,"consensus":"Buy"}]"#, + ), + json_fixture( + br#"[{"symbol":"AAPL","strongBuy":1,"buy":2.9,"hold":32,"sell":8,"strongSell":0,"consensus":"Buy"}]"#, + ), + ])); + let client = Client::builder() + .authentication(Authentication::fmp_header("secret")) + .executor(executor) + .build() + .unwrap(); + let query = || StockGradesSummaryQuery::new(Ticker::new("AAPL").unwrap()); + + let rows = client.stock_grades_summary(query()).await.unwrap(); + assert_eq!(rows[0].buy, 3); + assert_eq!( + serde_json::to_value(&rows).unwrap()[0]["buy"], + serde_json::json!(3) + ); + + let error = client.stock_grades_summary(query()).await.unwrap_err(); + assert_decode_error(&error, "grades-consensus"); +} + fn assert_decode_error(error: &libfmp::Error, endpoint: &'static str) { assert_eq!(error.category(), ErrorCategory::Decode); assert_eq!(error.endpoint(), Some(endpoint)); diff --git a/crates/libfmp/tests/count_fields.rs b/crates/libfmp/tests/count_fields.rs new file mode 100644 index 00000000..8438d1bd --- /dev/null +++ b/crates/libfmp/tests/count_fields.rs @@ -0,0 +1,123 @@ +//! Source scan: every response field typed with `Count` must decode through +//! `crate::codecs::count`. +//! +//! `Count` is a plain `u64`, so the compiler cannot tell a field that forgot +//! the attribute from one that has it; without the attribute an integral +//! float such as `3.0` fails the whole payload. The scan reads the response +//! sources as text so fields declared inside `macro_rules!` bodies are +//! covered too. + +use std::{ + fs, + path::{Path, PathBuf}, +}; + +const DESERIALIZE: &str = r#"deserialize_with = "crate::codecs::count::deserialize""#; +const DESERIALIZE_OPTION: &str = r#"deserialize_with = "crate::codecs::count::deserialize_option""#; + +fn rust_sources(dir: &Path, out: &mut Vec) { + for entry in fs::read_dir(dir).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + rust_sources(&path, out); + } else if path.extension().is_some_and(|ext| ext == "rs") { + out.push(path); + } + } +} + +/// Returns the field type when `line` declares a struct field. +fn field_type(line: &str) -> Option<&str> { + let rest = line.trim().strip_prefix("pub ")?; + let (name, ty) = rest.split_once(':')?; + if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { + return None; + } + Some(ty.trim().trim_end_matches(',').trim()) +} + +fn mentions_count(ty: &str) -> bool { + ty.split(|c: char| !c.is_ascii_alphanumeric() && c != '_') + .any(|token| token == "Count") +} + +/// Collects the `#[...]` attribute lines directly above `index`, skipping doc comments. +fn attributes_above(lines: &[&str], index: usize) -> Vec { + lines[..index] + .iter() + .rev() + .map(|line| line.trim()) + .take_while(|line| line.starts_with("#[") || line.starts_with("///")) + .filter(|line| line.starts_with("#[")) + .map(str::to_string) + .collect() +} + +fn violations() -> (usize, Vec) { + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src/responses"); + let mut files = Vec::new(); + rust_sources(&root, &mut files); + files.sort(); + let mut checked = 0; + let mut problems = Vec::new(); + for file in files { + let source = fs::read_to_string(&file).unwrap(); + let lines: Vec<&str> = source.lines().collect(); + for (index, line) in lines.iter().enumerate() { + let Some(ty) = field_type(line) else { continue }; + if !mentions_count(ty) { + continue; + } + checked += 1; + let expected = match ty { + "Count" => DESERIALIZE, + "Option" => DESERIALIZE_OPTION, + _ => { + problems.push(format!( + "{}:{}: `{ty}` wraps `Count` in a shape the count codec does not cover", + file.display(), + index + 1 + )); + continue; + } + }; + if !attributes_above(&lines, index) + .iter() + .any(|attr| attr.contains(expected)) + { + problems.push(format!( + "{}:{}: `{ty}` field lacks `#[serde({expected})]`", + file.display(), + index + 1 + )); + } + } + } + (checked, problems) +} + +#[test] +fn every_count_field_uses_the_count_codec() { + let (checked, problems) = violations(); + assert!(checked > 0, "the scan found no Count fields"); + assert!(problems.is_empty(), "{}", problems.join("\n")); +} + +#[test] +fn field_type_recognises_declarations_and_count_tokens() { + assert_eq!(field_type(" pub buy: Count,"), Some("Count")); + assert_eq!( + field_type(" pub holders: Option,"), + Some("Option") + ); + assert_eq!(field_type("pub struct EmployeeCount {"), None); + assert!(mentions_count("Option")); + assert!(!mentions_count("EmployeeCount")); + assert!(!mentions_count("CountryCode")); + let lines = [ + " /// Buy ratings.", + r#" #[serde(deserialize_with = "crate::codecs::count::deserialize")]"#, + " pub buy: Count,", + ]; + assert_eq!(attributes_above(&lines, 2).len(), 1); +} diff --git a/sdk/go/analyst_test.go b/sdk/go/analyst_test.go index f1938bc7..f77fb8a2 100644 --- a/sdk/go/analyst_test.go +++ b/sdk/go/analyst_test.go @@ -145,3 +145,21 @@ func TestAnalystMethodsReportMissingMembersAsDecodeErrors(t *testing.T) { t.Fatalf("decode failure was retried: %d requests", rec.count()) } } + +func TestAnalystCountMembersAcceptIntegralFloatsAndRejectFractions(t *testing.T) { + t.Parallel() + const row = `[{"symbol":"AAPL","strongBuy":1,"buy":%s,"hold":32,"sell":8,"strongSell":0,"consensus":"Buy"}]` + ctx := context.Background() + + server, _ := newServer(t, jsonHandler(fmt.Sprintf(row, "3.0"))) + client := newClient(t, server, WithAuthentication(FMPHeader("route-secret"))) + rows, err := client.Analyst.StockGradesSummary(ctx, NewStockGradesSummaryQuery("AAPL")) + if err != nil || len(rows) != 1 || rows[0].Buy != 3 { + t.Fatalf("StockGradesSummary = %+v, %v, want Buy 3", rows, err) + } + + server, _ = newServer(t, jsonHandler(fmt.Sprintf(row, "2.9"))) + client = newClient(t, server, WithAuthentication(FMPHeader("route-secret"))) + _, err = client.Analyst.StockGradesSummary(ctx, NewStockGradesSummaryQuery("AAPL")) + _ = assertQuoteError(t, err, CategoryDecode, http.StatusOK, "grades-consensus") +}