nautilus_betfair/common/types.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Common type aliases for Betfair identifiers and values.
17
18use rust_decimal::Decimal;
19use serde::{Deserialize, Deserializer};
20use serde_json;
21
22/// Betfair market identifier (e.g., "1.201070830").
23pub type MarketId = String;
24
25/// Betfair selection (runner) identifier.
26pub type SelectionId = u64;
27
28/// Deserializes a `SelectionId` from either a JSON number or string.
29///
30/// The streaming API sometimes sends selection IDs as strings (e.g. `"19248890"`)
31/// rather than bare integers.
32///
33/// # Errors
34///
35/// Returns an error if the value is a string that cannot be parsed as `u64`.
36pub fn deserialize_selection_id<'de, D>(deserializer: D) -> Result<SelectionId, D::Error>
37where
38 D: Deserializer<'de>,
39{
40 #[derive(Deserialize)]
41 #[serde(untagged)]
42 enum StringOrU64 {
43 U64(u64),
44 Str(String),
45 }
46
47 match StringOrU64::deserialize(deserializer)? {
48 StringOrU64::U64(v) => Ok(v),
49 StringOrU64::Str(s) => s.parse().map_err(serde::de::Error::custom),
50 }
51}
52
53/// Betfair bet identifier.
54pub type BetId = String;
55
56/// Betfair event identifier.
57pub type EventId = String;
58
59/// Betfair event type identifier.
60pub type EventTypeId = String;
61
62/// Betfair exchange identifier.
63pub type ExchangeId = String;
64
65/// Competition identifier.
66pub type CompetitionId = String;
67
68/// Customer order reference (max 32 characters).
69pub type CustomerOrderRef = String;
70
71/// Customer strategy reference (max 15 characters).
72pub type CustomerStrategyRef = String;
73
74/// Handicap value for Asian handicap markets.
75pub type Handicap = Decimal;
76
77/// Deserializes an `Option<String>` from either a JSON string or number.
78///
79/// Betfair API docs define many ID fields as strings, but the actual responses
80/// sometimes send them as bare integers (e.g. `"id": 7` instead of `"id": "7"`).
81///
82/// # Errors
83///
84/// Returns an error if the underlying JSON value cannot be deserialized.
85pub fn deserialize_optional_string_lenient<'de, D>(
86 deserializer: D,
87) -> Result<Option<String>, D::Error>
88where
89 D: Deserializer<'de>,
90{
91 let value = Option::<serde_json::Value>::deserialize(deserializer)?;
92 Ok(value.map(|v| match v {
93 serde_json::Value::String(s) => s,
94 serde_json::Value::Number(n) => n.to_string(),
95 other => other.to_string(),
96 }))
97}
98
99/// Deserializes an `Option<u32>` leniently from a JSON number, numeric string, or
100/// empty string.
101///
102/// Betfair navigation responses sometimes send `"numberOfWinners": ""` for
103/// handicap/total-points markets, which would fail a strict `Option<u32>` parse.
104///
105/// # Errors
106///
107/// Returns an error if the value is a non-empty string that cannot be parsed as `u32`.
108pub fn deserialize_optional_u32_lenient<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
109where
110 D: Deserializer<'de>,
111{
112 #[derive(Deserialize)]
113 #[serde(untagged)]
114 enum Value {
115 Num(u32),
116 Str(String),
117 }
118
119 match Option::<Value>::deserialize(deserializer)? {
120 None => Ok(None),
121 Some(Value::Num(n)) => Ok(Some(n)),
122 Some(Value::Str(s)) if s.is_empty() => Ok(None),
123 Some(Value::Str(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
124 }
125}