betfair_xml_parser/
lib.rs1#![warn(missing_docs, unreachable_pub, unused_crate_dependencies)]
2#![deny(unused_must_use, rust_2018_idioms)]
3#![allow(clippy::self_named_module_files)]
4#![doc(test(
5 no_crate_inject,
6 attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
7))]
8
9use common::Description;
17use log as _;
18use serde::{Deserialize, Serialize};
19
20pub mod common;
21pub mod data_type;
22pub mod exception_type;
23pub mod operation;
24pub mod simple_type;
25
26#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
28pub struct Interface {
29 pub name: String,
31 pub owner: String,
33 pub version: String,
35 pub date: String,
37 pub namespace: String,
39 #[serde(rename = "$value")]
41 pub items: Vec<InterfaceItems>,
42}
43
44#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "camelCase")]
47pub enum InterfaceItems {
48 Description(Description),
50 SimpleType(simple_type::SimpleType),
52 DataType(data_type::DataType),
54 ExceptionType(exception_type::ExceptionType),
56 Operation(operation::Operation),
58}
59
60pub fn parse_interface(xml: &str) -> Result<Interface, serde_xml_rs::Error> {
91 serde_xml_rs::from_str(xml)
92}
93
94impl From<&str> for Interface {
95 fn from(val: &str) -> Self {
96 parse_interface(val).unwrap_or_else(|_| Into::into("Failed to parse XML file"))
97 }
98}
99
100#[cfg(test)]
101#[expect(clippy::indexing_slicing)]
102mod tests {
103 use rstest::rstest;
104
105 use super::*;
106
107 #[rstest]
108 fn interface_test() {
109 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
110<interface name="HeartbeatAPING" owner="BDP" version="1.0.0" date="now()" namespace="com.betfair.heartbeat.api"
111 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
112 <description>Heartbeat</description>
113
114 <operation name="heartbeat" since="1.0.0">
115 <description>
116 This heartbeat operation is provided to help customers have their positions managed automatically in the
117 event of their API clients losing connectivity with the Betfair API.
118
119 If a heartbeat request is not received within a prescribed time period, then Betfair will attempt to cancel
120 all 'LIMIT' type bets for the given customer on the given exchange.
121
122 There is no guarantee that this service will result in all bets being cancelled as there are a number of
123 circumstances where bets are unable to be cancelled. Manual intervention is strongly advised in the event of a loss of connectivity
124 to ensure that positions are correctly managed.
125
126 If this service becomes unavailable for any reason, then your heartbeat will be unregistered automatically to avoid bets being
127 inadvertently cancelled upon resumption of service.
128 you should manage your position manually until the service is resumed.
129
130 Heartbeat data may also be lost in the unlikely event of nodes failing within the cluster, which
131 may result in your position not being managed until a subsequent heartbeat request is received.
132 </description>
133
134 <parameters>
135 <request>
136 <parameter mandatory="true" name="preferredTimeoutSeconds" type="i32">
137 <description>
138 Maximum period in seconds that may elapse (without a subsequent heartbeat request),
139 before a cancellation request is automatically submitted on your behalf.
140
141 Passing 0 will result in your heartbeat being unregistered (or ignored if you have no current
142 heartbeat registered).
143
144 You will still get an actionPerformed value returned when passing 0, so this may be used to
145 determine if any action was performed since your last heartbeat, without actually registering a new heartbeat.
146
147 Passing a negative value will result in an error being returned, INVALID_INPUT_DATA.
148
149 Any errors while registering your heartbeat will result in a error being returned, UNEXPECTED_ERROR.
150
151 Passing a value that is less than the minimum timeout will result in your heartbeat adopting the
152 minimum timeout.
153
154 Passing a value that is greater than the maximum timeout will result in your heartbeat adopting
155 the maximum timeout.
156
157 The minimum and maximum timeouts are subject to change, so your client should utilise the
158 returned actualTimeoutSeconds to set an appropriate frequency for your subsequent heartbeat requests.
159 </description>
160 </parameter>
161 </request>
162 <simpleResponse type="HeartbeatReport">
163 <description>Response from heartbeat operation</description>
164 </simpleResponse>
165 <exceptions>
166 <exception type="APINGException">
167 <description>Thrown if the operation fails</description>
168 </exception>
169 </exceptions>
170 </parameters>
171 </operation>
172
173
174 <dataType name="HeartbeatReport">
175 <description>Response from heartbeat operation</description>
176 <parameter mandatory="true" name="actionPerformed" type="ActionPerformed">
177 <description>The action performed since your last heartbeat request.</description>
178 </parameter>
179 <parameter mandatory="true" name="actualTimeoutSeconds" type="i32">
180 <description>The actual timeout applied to your heartbeat request, see timeout request parameter description
181 for details.
182 </description>
183 </parameter>
184 </dataType>
185
186
187 <exceptionType name="APINGException" prefix="HBT">
188 <description>This exception is thrown when an operation fails</description>
189 <parameter name="errorCode" type="string">
190 <description>the unique code for this error</description>
191 <validValues>
192 <value id="1" name="INVALID_INPUT_DATA">
193 <description>Invalid input data</description>
194 </value>
195 <value id="2" name="INVALID_SESSION_INFORMATION">
196 <description>The session token passed is invalid</description>
197 </value>
198 <value id="3" name="NO_APP_KEY">
199 <description>An application key is required for this operation</description>
200 </value>
201 <value id="4" name="NO_SESSION">
202 <description>A session token is required for this operation</description>
203 </value>
204 <value id="5" name="INVALID_APP_KEY">
205 <description>The application key passed is invalid</description>
206 </value>
207 <value id="6" name="UNEXPECTED_ERROR">
208 <description>An unexpected internal error occurred that prevented successful request processing.</description>
209 </value>
210 </validValues>
211 </parameter>
212 <parameter name="errorDetails" type="string">
213 <description>Specific error details</description>
214 </parameter>
215 <parameter name="requestUUID" type="string">
216 <description/>
217 </parameter>
218 </exceptionType>
219
220 <simpleType name="ActionPerformed" type="string">
221 <validValues>
222 <value name="NONE">
223 <description>No action was performed since last heartbeat, or this is the first heartbeat</description>
224 </value>
225 <value name="CANCELLATION_REQUEST_SUBMITTED">
226 <description>A request to cancel all unmatched bets was submitted since last heartbeat</description>
227 </value>
228 <value name="ALL_BETS_CANCELLED">
229 <description>All unmatched bets were cancelled since last heartbeat</description>
230 </value>
231 <value name="SOME_BETS_NOT_CANCELLED">
232 <description>Not all unmatched bets were cancelled since last heartbeat</description>
233 </value>
234 <value name="CANCELLATION_REQUEST_ERROR">
235 <description>There was an error requesting cancellation, no bets have been cancelled</description>
236 </value>
237 <value name="CANCELLATION_STATUS_UNKNOWN">
238 <description>There was no response from requesting cancellation, cancellation status unknown</description>
239 </value>
240 </validValues>
241 </simpleType>
242
243</interface>
244 "#;
245
246 let interface: Interface = xml.into();
247 assert_eq!(interface.name, "HeartbeatAPING");
248 assert_eq!(interface.owner, "BDP");
249 assert_eq!(interface.version, "1.0.0");
250 assert_eq!(interface.date, "now()");
251 assert_eq!(interface.namespace, "com.betfair.heartbeat.api");
252 assert_eq!(interface.items.len(), 5);
253 assert!(matches!(interface.items[0], InterfaceItems::Description(_)));
254 assert!(matches!(interface.items[1], InterfaceItems::Operation(_)));
255 assert!(matches!(interface.items[2], InterfaceItems::DataType(_)));
256 assert!(matches!(
257 interface.items[3],
258 InterfaceItems::ExceptionType(_)
259 ));
260 assert!(matches!(interface.items[4], InterfaceItems::SimpleType(_)));
261 }
262}