1use serde_json::Value;
6
7use super::symbol_normalize::{
8 expand_single_symbol_shorthand_to_security_and_owner, expand_symbols_array_to_security_list,
9};
10
11pub(crate) type EndpointRequestNormalizer = fn(&mut Value) -> Result<(), String>;
12
13#[derive(serde::Serialize)]
14pub struct ApiResponse<T: serde::Serialize> {
15 pub ret_type: i32,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub ret_msg: Option<String>,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub data: Option<T>,
20}
21
22#[cfg(test)]
54pub(crate) fn apply_known_field_aliases(value: &mut Value) {
55 apply_known_field_aliases_for_proto_id(value, None);
56}
57
58pub(crate) fn apply_known_field_aliases_for_proto_id(value: &mut Value, proto_id: Option<u32>) {
59 const COMMON_ALIASES: &[(&str, &str)] = &[
71 ("req_count", "max_ack_kl_num"),
72 ("max_count", "max_ack_kl_num"),
73 ("symbols", "security_list"),
75 ("stocks", "security_list"),
76 ("sub_types", "sub_type_list"),
77 ("is_sub", "is_sub_or_un_sub"),
78 ];
79 const TIME_ALIASES: &[(&str, &str)] = &[
84 ("begin", "begin_time"),
89 ("end", "end_time"),
90 ("start", "begin_time"),
95 ];
96
97 let apply_time_aliases = proto_id.is_none_or(supports_begin_time_aliases);
98 match value {
99 Value::Object(map) => {
100 apply_aliases_to_map(map, COMMON_ALIASES);
101 if apply_time_aliases {
102 apply_aliases_to_map(map, TIME_ALIASES);
103 }
104 for v in map.values_mut() {
105 apply_known_field_aliases_for_proto_id(v, proto_id);
106 }
107 }
108 Value::Array(arr) => {
109 for item in arr {
110 apply_known_field_aliases_for_proto_id(item, proto_id);
111 }
112 }
113 _ => {}
114 }
115}
116
117pub(crate) fn normalize_cancel_order_request_aliases(value: &mut Value) -> Result<(), String> {
126 let Some(map) = value.as_object_mut() else {
127 return Ok(());
128 };
129 if let Some(c2s) = map.get_mut("c2s").and_then(Value::as_object_mut) {
130 normalize_cancel_order_c2s_aliases(c2s)?;
131 } else {
132 normalize_cancel_order_c2s_aliases(map)?;
133 }
134 Ok(())
135}
136
137pub(crate) fn normalize_history_order_request_aliases(value: &mut Value) -> Result<(), String> {
145 let Some(map) = value.as_object_mut() else {
146 return Ok(());
147 };
148 if let Some(c2s) = map.get_mut("c2s").and_then(Value::as_object_mut) {
149 normalize_history_order_c2s_aliases(c2s)?;
150 } else {
151 normalize_history_order_c2s_aliases(map)?;
152 }
153 Ok(())
154}
155
156pub(crate) fn endpoint_local_request_normalizer_for_spec(
157 spec: &futu_surface_spec::EndpointSpec,
158) -> Option<EndpointRequestNormalizer> {
159 super::endpoint_policy::endpoint_adapter_policy_for_spec(spec)
160 .and_then(|policy| policy.request_normalizer)
161}
162
163pub(crate) fn normalize_endpoint_local_request_aliases_for_spec(
164 spec: &futu_surface_spec::EndpointSpec,
165 value: &mut Value,
166) -> Result<(), String> {
167 if let Some(normalizer) = endpoint_local_request_normalizer_for_spec(spec) {
168 normalizer(value)
169 } else {
170 Ok(())
171 }
172}
173
174pub(crate) fn normalize_endpoint_local_request_aliases_for_rest_path(
175 rest_path: &str,
176 value: &mut Value,
177) -> Result<(), String> {
178 if let Some(policy) = super::endpoint_policy::endpoint_adapter_policy_for_rest_path(rest_path)
179 && let Some(normalizer) = policy.request_normalizer
180 {
181 return normalizer(value);
182 }
183 let Some(spec) = futu_surface_spec::lookup_endpoint_by_rest_path(rest_path) else {
184 return Ok(());
185 };
186 normalize_endpoint_local_request_aliases_for_spec(spec, value)
187}
188
189fn normalize_cancel_order_c2s_aliases(
190 c2s: &mut serde_json::Map<String, Value>,
191) -> Result<(), String> {
192 normalize_cancel_order_header_aliases(c2s)?;
193 normalize_cancel_order_id_aliases(c2s)?;
194 let move_trd_env = c2s
195 .get("header")
196 .and_then(Value::as_object)
197 .is_some_and(|header| !header.contains_key("trd_env"))
198 .then(|| c2s.remove("trd_env"))
199 .flatten();
200 let move_trd_market = c2s
201 .get("header")
202 .and_then(Value::as_object)
203 .is_some_and(|header| !header.contains_key("trd_market"))
204 .then(|| c2s.remove("trd_market"))
205 .flatten();
206 if let Some(header) = c2s.get_mut("header").and_then(Value::as_object_mut) {
207 normalize_cancel_order_header_aliases(header)?;
208 if let Some(value) = move_trd_env {
209 header.insert("trd_env".to_string(), value);
210 }
211 if let Some(value) = move_trd_market {
212 header.insert("trd_market".to_string(), value);
213 }
214 }
215 normalize_cancel_order_op(c2s)
216}
217
218fn normalize_history_order_c2s_aliases(
219 c2s: &mut serde_json::Map<String, Value>,
220) -> Result<(), String> {
221 let header_market = normalize_history_order_header_aliases(c2s)?;
222 normalize_history_order_filter_aliases(c2s, header_market)
223}
224
225fn normalize_history_order_header_aliases(
226 c2s: &mut serde_json::Map<String, Value>,
227) -> Result<Option<i32>, String> {
228 let mut header = c2s
229 .remove("header")
230 .map(|v| match v {
231 Value::Object(map) => map,
232 other => {
233 let mut map = serde_json::Map::new();
234 map.insert("invalid_header".to_string(), other);
235 map
236 }
237 })
238 .unwrap_or_default();
239
240 if !header.contains_key("acc_id")
241 && let Some(value) = c2s.remove("acc_id")
242 {
243 header.insert("acc_id".to_string(), value);
244 }
245 if !header.contains_key("trd_env")
246 && let Some(value) = remove_first(c2s, &["trd_env", "env"])
247 {
248 let trd_env = parse_rest_trd_env(&value)?;
249 header.insert("trd_env".to_string(), Value::Number(trd_env.into()));
250 }
251
252 let mut header_market = header
253 .get("trd_market")
254 .map(parse_rest_read_trd_market)
255 .transpose()?;
256 if !header.contains_key("trd_market")
257 && let Some(value) = remove_first(c2s, &["trd_market", "market", "order_market"])
258 {
259 let trd_market = parse_rest_read_trd_market(&value)?;
260 header.insert("trd_market".to_string(), Value::Number(trd_market.into()));
261 header_market = Some(trd_market);
262 } else if header_market.is_some() {
263 c2s.remove("trd_market");
264 c2s.remove("market");
265 }
266
267 if !header.is_empty() {
268 c2s.insert("header".to_string(), Value::Object(header));
269 }
270 Ok(header_market)
271}
272
273fn normalize_history_order_filter_aliases(
274 c2s: &mut serde_json::Map<String, Value>,
275 header_market: Option<i32>,
276) -> Result<(), String> {
277 let mut filter = c2s
278 .remove("filter_conditions")
279 .map(|v| match v {
280 Value::Object(map) => map,
281 other => {
282 let mut map = serde_json::Map::new();
283 map.insert("invalid_filter_conditions".to_string(), other);
284 map
285 }
286 })
287 .unwrap_or_default();
288
289 move_alias_if_missing(
290 c2s,
291 &mut filter,
292 "begin_time",
293 &["begin_time", "begin", "start"],
294 );
295 move_alias_if_missing(c2s, &mut filter, "end_time", &["end_time", "end"]);
296 move_alias_if_missing(c2s, &mut filter, "code_list", &["code_list", "codes"]);
297 move_alias_if_missing(c2s, &mut filter, "id_list", &["id_list"]);
298 move_alias_if_missing(c2s, &mut filter, "order_id_ex_list", &["order_id_ex_list"]);
299
300 if !filter.contains_key("filter_market") {
301 if let Some(value) = remove_first(c2s, &["filter_market", "order_market"]) {
302 let filter_market = parse_rest_read_trd_market(&value)?;
303 filter.insert(
304 "filter_market".to_string(),
305 Value::Number(filter_market.into()),
306 );
307 } else if let Some(market) = header_market {
308 filter.insert("filter_market".to_string(), Value::Number(market.into()));
309 }
310 }
311
312 if !filter.is_empty() {
313 c2s.insert("filter_conditions".to_string(), Value::Object(filter));
314 }
315 Ok(())
316}
317
318fn move_alias_if_missing(
319 from: &mut serde_json::Map<String, Value>,
320 to: &mut serde_json::Map<String, Value>,
321 canonical: &str,
322 aliases: &[&str],
323) {
324 if to.contains_key(canonical) {
325 for alias in aliases {
326 if *alias != canonical {
327 from.remove(*alias);
328 }
329 }
330 return;
331 }
332 if let Some(value) = remove_first(from, aliases) {
333 to.insert(canonical.to_string(), value);
334 }
335}
336
337fn remove_first(map: &mut serde_json::Map<String, Value>, aliases: &[&str]) -> Option<Value> {
338 for alias in aliases {
339 if let Some(value) = map.remove(*alias) {
340 return Some(value);
341 }
342 }
343 None
344}
345
346fn normalize_cancel_order_id_aliases(
347 c2s: &mut serde_json::Map<String, Value>,
348) -> Result<(), String> {
349 let Some(value) = c2s.get_mut("order_id") else {
350 return Ok(());
351 };
352 let Some(raw) = value.as_str() else {
353 return Ok(());
354 };
355 let raw = raw.trim();
356 if raw.is_empty() {
357 return Ok(());
358 }
359 let parsed = raw
360 .parse::<u64>()
361 .map_err(|_| "invalid order_id; expected numeric order_id or order_id_ex".to_string())?;
362 *value = Value::Number(parsed.into());
363 Ok(())
364}
365
366fn normalize_cancel_order_header_aliases(
367 map: &mut serde_json::Map<String, Value>,
368) -> Result<(), String> {
369 if !map.contains_key("trd_env")
370 && let Some(value) = map.remove("env")
371 {
372 let trd_env = parse_rest_trd_env(&value)?;
373 map.insert("trd_env".to_string(), Value::Number(trd_env.into()));
374 } else {
375 map.remove("env");
376 }
377 if !map.contains_key("trd_market")
378 && let Some(value) = map.remove("market")
379 {
380 let trd_market = parse_rest_write_trd_market(&value)?;
381 map.insert("trd_market".to_string(), Value::Number(trd_market.into()));
382 } else {
383 map.remove("market");
384 }
385 Ok(())
386}
387
388fn normalize_cancel_order_op(c2s: &mut serde_json::Map<String, Value>) -> Result<(), String> {
389 let canonical = c2s.remove("modify_order_op");
390 let alias = c2s.remove("op");
391 for value in canonical.iter().chain(alias.iter()) {
392 let op = parse_rest_modify_order_op(value)?;
393 if op != futu_trd::types::ModifyOrderOp::Cancel as i32 {
394 return Err(format!(
395 "/api/cancel-order only accepts cancel operation (2/CANCEL), got {op}"
396 ));
397 }
398 }
399 c2s.insert(
400 "modify_order_op".to_string(),
401 Value::Number((futu_trd::types::ModifyOrderOp::Cancel as i32).into()),
402 );
403 Ok(())
404}
405
406fn parse_rest_trd_env(value: &Value) -> Result<i32, String> {
407 if let Some(n) = value.as_i64() {
408 return match n {
409 0 | 1 => Ok(n as i32),
410 _ => Err(format!(
411 "invalid env {n}; expected real|simulate|sim or 1|0"
412 )),
413 };
414 }
415 let Some(raw) = value.as_str() else {
416 return Err("invalid env; expected real|simulate|sim or 1|0".to_string());
417 };
418 if let Ok(n) = raw.trim().parse::<i64>() {
419 return match n {
420 0 | 1 => Ok(n as i32),
421 _ => Err(format!(
422 "invalid env {n}; expected real|simulate|sim or 1|0"
423 )),
424 };
425 }
426 futu_trd::parsing::parse_trd_env(raw)
427 .map(|env| env as i32)
428 .ok_or_else(|| {
429 format!(
430 "invalid env {raw:?}; expected {}",
431 futu_trd::parsing::TRD_ENV_PARSE_CHOICES
432 )
433 })
434}
435
436fn parse_rest_write_trd_market(value: &Value) -> Result<i32, String> {
437 if let Some(n) = value.as_i64() {
438 let n = i32::try_from(n).map_err(|_| {
439 format!(
440 "invalid market {n}; expected {}",
441 futu_trd::market::TRD_MARKET_NON_FUND_PARSE_CHOICES
442 )
443 })?;
444 return futu_trd::market::trd_market_from_i32(n)
445 .filter(|market| futu_trd::market::canonical_fund_trd_market_label(*market).is_none())
446 .map(|market| market as i32)
447 .ok_or_else(|| {
448 format!(
449 "invalid market {n}; expected {}",
450 futu_trd::market::TRD_MARKET_NON_FUND_PARSE_CHOICES
451 )
452 });
453 }
454 let Some(raw) = value.as_str() else {
455 return Err(format!(
456 "invalid market; expected {}",
457 futu_trd::market::TRD_MARKET_NON_FUND_PARSE_CHOICES
458 ));
459 };
460 futu_trd::market::parse_non_fund_trd_market(raw)
461 .map(|market| market as i32)
462 .ok_or_else(|| {
463 format!(
464 "invalid market {raw:?}; expected {}",
465 futu_trd::market::TRD_MARKET_NON_FUND_PARSE_CHOICES
466 )
467 })
468}
469
470fn parse_rest_read_trd_market(value: &Value) -> Result<i32, String> {
471 if let Some(n) = value.as_i64() {
472 let n = i32::try_from(n).map_err(|_| {
473 format!(
474 "invalid market {n}; expected {}",
475 futu_trd::market::TRD_MARKET_PARSE_CHOICES
476 )
477 })?;
478 return futu_trd::market::trd_market_from_i32(n)
479 .map(|market| market as i32)
480 .ok_or_else(|| {
481 format!(
482 "invalid market {n}; expected {}",
483 futu_trd::market::TRD_MARKET_PARSE_CHOICES
484 )
485 });
486 }
487 let Some(raw) = value.as_str() else {
488 return Err(format!(
489 "invalid market; expected {}",
490 futu_trd::market::TRD_MARKET_PARSE_CHOICES
491 ));
492 };
493 futu_trd::market::parse_trd_market(raw)
494 .map(|market| market as i32)
495 .ok_or_else(|| {
496 format!(
497 "invalid market {raw:?}; expected {}",
498 futu_trd::market::TRD_MARKET_PARSE_CHOICES
499 )
500 })
501}
502
503fn parse_rest_modify_order_op(value: &Value) -> Result<i32, String> {
504 if let Some(n) = value.as_i64() {
505 return i32::try_from(n).map_err(|_| {
506 format!(
507 "invalid op {n}; expected {}",
508 futu_trd::parsing::MODIFY_OP_PARSE_CHOICES
509 )
510 });
511 }
512 let Some(raw) = value.as_str() else {
513 return Err(format!(
514 "invalid op; expected {}",
515 futu_trd::parsing::MODIFY_OP_PARSE_CHOICES
516 ));
517 };
518 if let Ok(n) = raw.trim().parse::<i32>() {
519 return Ok(n);
520 }
521 futu_trd::parsing::parse_modify_op(raw)
522 .map(|op| op as i32)
523 .ok_or_else(|| {
524 format!(
525 "invalid op {raw:?}; expected {}",
526 futu_trd::parsing::MODIFY_OP_PARSE_CHOICES
527 )
528 })
529}
530
531fn apply_aliases_to_map(map: &mut serde_json::Map<String, Value>, aliases: &[(&str, &str)]) {
532 for (alias, canonical) in aliases {
533 if map.contains_key(*canonical) {
534 map.remove(*alias);
536 } else if let Some(v) = map.remove(*alias) {
537 map.insert((*canonical).to_string(), v);
538 }
539 }
540}
541
542fn supports_begin_time_aliases(proto_id: u32) -> bool {
543 matches!(
544 proto_id,
545 futu_core::proto_id::QOT_GET_HISTORY_KL
546 | futu_core::proto_id::QOT_REQUEST_HISTORY_KL
547 | futu_core::proto_id::QOT_GET_OPTION_CHAIN
548 | futu_core::proto_id::QOT_GET_CAPITAL_FLOW
549 | futu_core::proto_id::QOT_GET_SUSPEND
550 | futu_core::proto_id::QOT_GET_HOLDING_CHANGE_LIST
551 | futu_core::proto_id::QOT_GET_TRADE_DATE
552 | futu_core::proto_id::QOT_REQUEST_TRADE_DATE
553 )
554}
555
556pub(crate) fn maybe_wrap_flat_body_as_c2s(value: &mut Value) {
576 let Value::Object(map) = value else {
577 return;
578 };
579 if map.contains_key("c2s") {
581 return;
582 }
583 for response_key in ["s2c", "ret_type", "ret_msg", "err_code"] {
585 if map.contains_key(response_key) {
586 return;
587 }
588 }
589 if map.is_empty() {
591 return;
592 }
593 let inner = std::mem::take(map);
595 map.insert("c2s".to_string(), Value::Object(inner));
596}
597
598pub(crate) fn maybe_expand_flat_trd_header(value: &mut Value) {
621 let Value::Object(top) = value else {
622 return;
623 };
624 let Some(Value::Object(c2s)) = top.get_mut("c2s") else {
625 return;
626 };
627 if matches!(c2s.get("header"), Some(Value::Object(_))) {
629 return;
630 }
631 const HEADER_FIELDS: &[&str] = &["trd_env", "acc_id", "trd_market", "jp_acc_type"];
633 let mut header_map = serde_json::Map::new();
635 for field in HEADER_FIELDS {
636 if let Some(v) = c2s.remove(*field) {
637 header_map.insert((*field).to_string(), v);
638 }
639 }
640 if header_map.is_empty() {
642 return;
643 }
644 c2s.insert("header".to_string(), Value::Object(header_map));
645}
646
647pub(crate) fn expand_symbol_shorthand(value: &mut Value) -> Result<(), String> {
663 let Value::Object(top) = value else {
664 return Ok(());
665 };
666 let Some(Value::Object(inner)) = top.get_mut("c2s") else {
668 return Ok(());
669 };
670
671 expand_symbols_array_to_security_list(inner)?;
684
685 expand_single_symbol_shorthand_to_security_and_owner(inner)?;
700 Ok(())
701}