1use serde_json::Value;
6
7use futu_core::{trade_market, trade_parsing};
8
9use super::symbol_normalize::{
10 expand_single_symbol_shorthand_to_security_and_owner, expand_symbols_array_to_security_list,
11};
12
13pub(crate) type EndpointRequestNormalizer = fn(&mut Value) -> Result<(), String>;
14
15#[derive(serde::Serialize)]
16pub struct ApiResponse<T: serde::Serialize> {
17 pub ret_type: i32,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub ret_msg: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub data: Option<T>,
22}
23
24#[cfg(test)]
59pub(crate) fn apply_known_field_aliases(value: &mut Value) {
60 apply_known_field_aliases_with_scope(value, true, true);
61}
62
63pub(crate) fn apply_known_field_aliases_for_proto_id(value: &mut Value, proto_id: Option<u32>) {
64 apply_known_field_aliases_with_scope(
65 value,
66 proto_id.is_some_and(supports_max_ack_kl_num_aliases),
67 proto_id.is_some_and(supports_begin_time_aliases),
68 );
69}
70
71fn apply_known_field_aliases_with_scope(
72 value: &mut Value,
73 apply_max_ack_kl_num_aliases: bool,
74 apply_time_aliases: bool,
75) {
76 const MAX_ACK_KL_NUM_ALIASES: &[(&str, &str)] = &[
88 ("req_count", "max_ack_kl_num"),
89 ("max_count", "max_ack_kl_num"),
90 ];
91 const COMMON_ALIASES: &[(&str, &str)] = &[
92 ("symbols", "security_list"),
94 ("stocks", "security_list"),
95 ("sub_types", "sub_type_list"),
96 ("is_sub", "is_sub_or_un_sub"),
97 ];
98 const TIME_ALIASES: &[(&str, &str)] = &[
103 ("begin", "begin_time"),
108 ("end", "end_time"),
109 ("start", "begin_time"),
114 ];
115
116 match value {
117 Value::Object(map) => {
118 if apply_max_ack_kl_num_aliases {
119 apply_aliases_to_map(map, MAX_ACK_KL_NUM_ALIASES);
120 }
121 apply_aliases_to_map(map, COMMON_ALIASES);
122 if apply_time_aliases {
123 apply_aliases_to_map(map, TIME_ALIASES);
124 }
125 for v in map.values_mut() {
126 apply_known_field_aliases_with_scope(
127 v,
128 apply_max_ack_kl_num_aliases,
129 apply_time_aliases,
130 );
131 }
132 }
133 Value::Array(arr) => {
134 for item in arr {
135 apply_known_field_aliases_with_scope(
136 item,
137 apply_max_ack_kl_num_aliases,
138 apply_time_aliases,
139 );
140 }
141 }
142 _ => {}
143 }
144}
145
146fn normalize_cancel_order_env_alias_in_map(
155 map: &mut serde_json::Map<String, Value>,
156) -> Result<(), String> {
157 if map.contains_key("trd_env") {
158 map.remove("env");
159 return Ok(());
160 }
161 let Some(value) = map.remove("env") else {
162 return Ok(());
163 };
164 let trd_env = parse_rest_trd_env(&value)?;
165 map.insert("trd_env".to_string(), Value::Number(trd_env.into()));
166 Ok(())
167}
168
169pub(crate) fn normalize_cancel_order_env_alias(value: &mut Value) -> Result<(), String> {
170 let Some(root) = value.as_object_mut() else {
171 return Ok(());
172 };
173 let owner = match root.get_mut("c2s") {
174 Some(Value::Object(c2s)) => c2s,
175 _ => root,
176 };
177
178 let header_has_canonical = owner
179 .get("header")
180 .and_then(Value::as_object)
181 .is_some_and(|header| header.contains_key("trd_env"));
182 if header_has_canonical {
183 owner.remove("trd_env");
184 owner.remove("env");
185 if let Some(header) = owner.get_mut("header").and_then(Value::as_object_mut) {
186 header.remove("env");
187 }
188 return Ok(());
189 }
190
191 normalize_cancel_order_env_alias_in_map(owner)?;
192 let owner_env = owner.remove("trd_env");
193 if let Some(header) = owner.get_mut("header").and_then(Value::as_object_mut) {
194 if let Some(owner_env) = owner_env {
195 header.remove("env");
196 header.insert("trd_env".to_string(), owner_env);
197 } else {
198 normalize_cancel_order_env_alias_in_map(header)?;
199 }
200 } else if let Some(owner_env) = owner_env {
201 owner.insert("trd_env".to_string(), owner_env);
202 }
203 Ok(())
204}
205
206pub(crate) fn normalize_cancel_order_request_aliases(value: &mut Value) -> Result<(), String> {
207 normalize_cancel_order_env_alias(value)?;
208 let Some(map) = value.as_object_mut() else {
209 return Ok(());
210 };
211 if let Some(c2s) = map.get_mut("c2s").and_then(Value::as_object_mut) {
212 normalize_cancel_order_c2s_aliases(c2s)?;
213 } else {
214 normalize_cancel_order_c2s_aliases(map)?;
215 }
216 Ok(())
217}
218
219pub(crate) fn normalize_history_order_request_aliases(value: &mut Value) -> Result<(), String> {
227 let Some(map) = value.as_object_mut() else {
228 return Ok(());
229 };
230 if let Some(c2s) = map.get_mut("c2s").and_then(Value::as_object_mut) {
231 normalize_history_order_c2s_aliases(c2s)?;
232 } else {
233 normalize_history_order_c2s_aliases(map)?;
234 }
235 Ok(())
236}
237
238pub(crate) fn normalize_margin_ratio_request_aliases(value: &mut Value) -> Result<(), String> {
242 let Some(root) = value.as_object_mut() else {
243 return Ok(());
244 };
245 let owner = match root.get_mut("c2s") {
246 Some(Value::Object(c2s)) => c2s,
247 Some(_) => return Err("invalid margin-ratio c2s; expected object".to_string()),
248 None => root,
249 };
250 let mut header = match owner.remove("header") {
251 Some(Value::Object(header)) => header,
252 Some(_) => return Err("invalid margin-ratio header; expected object".to_string()),
253 None => serde_json::Map::new(),
254 };
255
256 if let Some(value) =
257 take_unique_margin_ratio_header_field(owner, &mut header, "acc_id", &["acc_id"])?
258 {
259 header.insert("acc_id".to_string(), value);
260 }
261 if let Some(value) = take_unique_margin_ratio_header_field(
262 owner,
263 &mut header,
264 "env/trd_env",
265 &["trd_env", "env"],
266 )? {
267 let trd_env = parse_rest_trd_env(&value)?;
268 header.insert("trd_env".to_string(), Value::Number(trd_env.into()));
269 }
270 if let Some(value) = take_unique_margin_ratio_header_field(
271 owner,
272 &mut header,
273 "market/trd_market",
274 &["trd_market", "market"],
275 )? {
276 let trd_market = parse_rest_write_trd_market(&value)?;
277 header.insert("trd_market".to_string(), Value::Number(trd_market.into()));
278 }
279
280 if !header.is_empty() {
281 owner.insert("header".to_string(), Value::Object(header));
282 }
283 Ok(())
284}
285
286fn take_unique_margin_ratio_header_field(
287 owner: &mut serde_json::Map<String, Value>,
288 header: &mut serde_json::Map<String, Value>,
289 logical_name: &str,
290 names: &[&str],
291) -> Result<Option<Value>, String> {
292 let mut values = Vec::new();
293 for name in names {
294 if let Some(value) = header.remove(*name) {
295 values.push(value);
296 }
297 if let Some(value) = owner.remove(*name) {
298 values.push(value);
299 }
300 }
301 if values.len() > 1 {
302 return Err(format!(
303 "conflicting margin-ratio {logical_name} fields; supply exactly one alias or canonical field"
304 ));
305 }
306 Ok(values.pop())
307}
308
309pub(crate) fn endpoint_local_request_normalizer_for_spec(
310 spec: &futu_surface_spec::EndpointSpec,
311) -> Option<EndpointRequestNormalizer> {
312 super::endpoint_policy::endpoint_adapter_policy_for_spec(spec)
313 .and_then(|policy| policy.request_normalizer)
314}
315
316pub(crate) fn normalize_endpoint_local_request_aliases_for_spec(
317 spec: &futu_surface_spec::EndpointSpec,
318 value: &mut Value,
319) -> Result<(), String> {
320 if let Some(normalizer) = endpoint_local_request_normalizer_for_spec(spec) {
321 normalizer(value)
322 } else {
323 Ok(())
324 }
325}
326
327pub(crate) fn normalize_endpoint_local_request_aliases_for_rest_path(
328 rest_path: &str,
329 value: &mut Value,
330) -> Result<(), String> {
331 if let Some(policy) = super::endpoint_policy::endpoint_adapter_policy_for_rest_path(rest_path)
332 && let Some(normalizer) = policy.request_normalizer
333 {
334 return normalizer(value);
335 }
336 let Some(spec) = futu_surface_spec::lookup_endpoint_by_rest_path(rest_path) else {
337 return Ok(());
338 };
339 normalize_endpoint_local_request_aliases_for_spec(spec, value)
340}
341
342fn normalize_cancel_order_c2s_aliases(
343 c2s: &mut serde_json::Map<String, Value>,
344) -> Result<(), String> {
345 normalize_cancel_order_header_aliases(c2s)?;
346 normalize_cancel_order_id_aliases(c2s)?;
347 let move_trd_market = c2s
348 .get("header")
349 .and_then(Value::as_object)
350 .is_some_and(|header| !header.contains_key("trd_market"))
351 .then(|| c2s.remove("trd_market"))
352 .flatten();
353 if let Some(header) = c2s.get_mut("header").and_then(Value::as_object_mut) {
354 normalize_cancel_order_header_aliases(header)?;
355 if let Some(value) = move_trd_market {
356 header.insert("trd_market".to_string(), value);
357 }
358 }
359 normalize_cancel_order_op(c2s)
360}
361
362fn normalize_history_order_c2s_aliases(
363 c2s: &mut serde_json::Map<String, Value>,
364) -> Result<(), String> {
365 let header_market = normalize_history_order_header_aliases(c2s)?;
366 normalize_history_order_filter_aliases(c2s, header_market)
367}
368
369fn normalize_history_order_header_aliases(
370 c2s: &mut serde_json::Map<String, Value>,
371) -> Result<Option<i32>, String> {
372 let mut header = c2s
373 .remove("header")
374 .map(|v| match v {
375 Value::Object(map) => map,
376 other => {
377 let mut map = serde_json::Map::new();
378 map.insert("invalid_header".to_string(), other);
379 map
380 }
381 })
382 .unwrap_or_default();
383
384 if !header.contains_key("acc_id")
385 && let Some(value) = c2s.remove("acc_id")
386 {
387 header.insert("acc_id".to_string(), value);
388 }
389 if !header.contains_key("trd_env")
390 && let Some(value) = remove_first(c2s, &["trd_env", "env"])
391 {
392 let trd_env = parse_rest_trd_env(&value)?;
393 header.insert("trd_env".to_string(), Value::Number(trd_env.into()));
394 }
395
396 let mut header_market = header
397 .get("trd_market")
398 .map(parse_rest_read_trd_market)
399 .transpose()?;
400 if !header.contains_key("trd_market")
401 && let Some(value) = remove_first(c2s, &["trd_market", "market", "order_market"])
402 {
403 let trd_market = parse_rest_read_trd_market(&value)?;
404 header.insert("trd_market".to_string(), Value::Number(trd_market.into()));
405 header_market = Some(trd_market);
406 } else if header_market.is_some() {
407 c2s.remove("trd_market");
408 c2s.remove("market");
409 }
410
411 if !header.is_empty() {
412 c2s.insert("header".to_string(), Value::Object(header));
413 }
414 Ok(header_market)
415}
416
417fn normalize_history_order_filter_aliases(
418 c2s: &mut serde_json::Map<String, Value>,
419 header_market: Option<i32>,
420) -> Result<(), String> {
421 let mut filter = c2s
422 .remove("filter_conditions")
423 .map(|v| match v {
424 Value::Object(map) => map,
425 other => {
426 let mut map = serde_json::Map::new();
427 map.insert("invalid_filter_conditions".to_string(), other);
428 map
429 }
430 })
431 .unwrap_or_default();
432
433 move_alias_if_missing(
434 c2s,
435 &mut filter,
436 "begin_time",
437 &["begin_time", "begin", "start"],
438 );
439 move_alias_if_missing(c2s, &mut filter, "end_time", &["end_time", "end"]);
440 move_alias_if_missing(c2s, &mut filter, "code_list", &["code_list", "codes"]);
441 move_alias_if_missing(c2s, &mut filter, "id_list", &["id_list"]);
442 move_alias_if_missing(c2s, &mut filter, "order_id_ex_list", &["order_id_ex_list"]);
443
444 if !filter.contains_key("filter_market") {
445 if let Some(value) = remove_first(c2s, &["filter_market", "order_market"]) {
446 let filter_market = parse_rest_read_trd_market(&value)?;
447 filter.insert(
448 "filter_market".to_string(),
449 Value::Number(filter_market.into()),
450 );
451 } else if let Some(market) = header_market {
452 filter.insert("filter_market".to_string(), Value::Number(market.into()));
453 }
454 }
455
456 if !filter.is_empty() {
457 c2s.insert("filter_conditions".to_string(), Value::Object(filter));
458 }
459 Ok(())
460}
461
462fn move_alias_if_missing(
463 from: &mut serde_json::Map<String, Value>,
464 to: &mut serde_json::Map<String, Value>,
465 canonical: &str,
466 aliases: &[&str],
467) {
468 if to.contains_key(canonical) {
469 for alias in aliases {
470 if *alias != canonical {
471 from.remove(*alias);
472 }
473 }
474 return;
475 }
476 if let Some(value) = remove_first(from, aliases) {
477 to.insert(canonical.to_string(), value);
478 }
479}
480
481fn remove_first(map: &mut serde_json::Map<String, Value>, aliases: &[&str]) -> Option<Value> {
482 for alias in aliases {
483 if let Some(value) = map.remove(*alias) {
484 return Some(value);
485 }
486 }
487 None
488}
489
490fn normalize_cancel_order_id_aliases(
491 c2s: &mut serde_json::Map<String, Value>,
492) -> Result<(), String> {
493 let Some(value) = c2s.get_mut("order_id") else {
494 return Ok(());
495 };
496 let Some(raw) = value.as_str() else {
497 return Ok(());
498 };
499 let raw = raw.trim();
500 if raw.is_empty() {
501 return Ok(());
502 }
503 let parsed = raw
504 .parse::<u64>()
505 .map_err(|_| "invalid order_id; expected numeric order_id or order_id_ex".to_string())?;
506 *value = Value::Number(parsed.into());
507 Ok(())
508}
509
510fn normalize_cancel_order_header_aliases(
511 map: &mut serde_json::Map<String, Value>,
512) -> Result<(), String> {
513 if !map.contains_key("trd_market")
514 && let Some(value) = map.remove("market")
515 {
516 let trd_market = parse_rest_write_trd_market(&value)?;
517 map.insert("trd_market".to_string(), Value::Number(trd_market.into()));
518 } else {
519 map.remove("market");
520 }
521 Ok(())
522}
523
524fn normalize_cancel_order_op(c2s: &mut serde_json::Map<String, Value>) -> Result<(), String> {
525 let canonical = c2s.remove("modify_order_op");
526 let alias = c2s.remove("op");
527 for value in canonical.iter().chain(alias.iter()) {
528 let op = parse_rest_modify_order_op(value)?;
529 if op != trade_parsing::MODIFY_ORDER_OP_CANCEL {
530 return Err(format!(
531 "/api/cancel-order only accepts cancel operation (2/CANCEL), got {op}"
532 ));
533 }
534 }
535 c2s.insert(
536 "modify_order_op".to_string(),
537 Value::Number(trade_parsing::MODIFY_ORDER_OP_CANCEL.into()),
538 );
539 Ok(())
540}
541
542fn parse_rest_trd_env(value: &Value) -> Result<i32, String> {
543 if let Some(n) = value.as_i64() {
544 return match n {
545 0 | 1 => Ok(n as i32),
546 _ => Err(format!(
547 "invalid env {n}; expected real|simulate|sim or 1|0"
548 )),
549 };
550 }
551 let Some(raw) = value.as_str() else {
552 return Err("invalid env; expected real|simulate|sim or 1|0".to_string());
553 };
554 if let Ok(n) = raw.trim().parse::<i64>() {
555 return match n {
556 0 | 1 => Ok(n as i32),
557 _ => Err(format!(
558 "invalid env {n}; expected real|simulate|sim or 1|0"
559 )),
560 };
561 }
562 trade_parsing::parse_trd_env_id(raw).ok_or_else(|| {
563 format!(
564 "invalid env {raw:?}; expected {}",
565 trade_parsing::TRD_ENV_PARSE_CHOICES
566 )
567 })
568}
569
570fn parse_rest_write_trd_market(value: &Value) -> Result<i32, String> {
571 if let Some(n) = value.as_i64() {
572 let n = i32::try_from(n).map_err(|_| {
573 format!(
574 "invalid market {n}; expected {}",
575 trade_market::TRD_MARKET_NON_FUND_PARSE_CHOICES
576 )
577 })?;
578 return trade_market::is_trd_market_id(n)
579 .then_some(n)
580 .filter(|market| trade_market::canonical_fund_trd_market_label(*market).is_none())
581 .ok_or_else(|| {
582 format!(
583 "invalid market {n}; expected {}",
584 trade_market::TRD_MARKET_NON_FUND_PARSE_CHOICES
585 )
586 });
587 }
588 let Some(raw) = value.as_str() else {
589 return Err(format!(
590 "invalid market; expected {}",
591 trade_market::TRD_MARKET_NON_FUND_PARSE_CHOICES
592 ));
593 };
594 trade_market::parse_non_fund_trd_market_id(raw).ok_or_else(|| {
595 format!(
596 "invalid market {raw:?}; expected {}",
597 trade_market::TRD_MARKET_NON_FUND_PARSE_CHOICES
598 )
599 })
600}
601
602fn parse_rest_read_trd_market(value: &Value) -> Result<i32, String> {
603 if let Some(n) = value.as_i64() {
604 let n = i32::try_from(n).map_err(|_| {
605 format!(
606 "invalid market {n}; expected {}",
607 trade_market::TRD_MARKET_PARSE_CHOICES
608 )
609 })?;
610 return trade_market::is_trd_market_id(n)
611 .then_some(n)
612 .ok_or_else(|| {
613 format!(
614 "invalid market {n}; expected {}",
615 trade_market::TRD_MARKET_PARSE_CHOICES
616 )
617 });
618 }
619 let Some(raw) = value.as_str() else {
620 return Err(format!(
621 "invalid market; expected {}",
622 trade_market::TRD_MARKET_PARSE_CHOICES
623 ));
624 };
625 trade_market::parse_trd_market_id(raw).ok_or_else(|| {
626 format!(
627 "invalid market {raw:?}; expected {}",
628 trade_market::TRD_MARKET_PARSE_CHOICES
629 )
630 })
631}
632
633fn parse_rest_modify_order_op(value: &Value) -> Result<i32, String> {
634 if let Some(n) = value.as_i64() {
635 return i32::try_from(n).map_err(|_| {
636 format!(
637 "invalid op {n}; expected {}",
638 trade_parsing::MODIFY_OP_PARSE_CHOICES
639 )
640 });
641 }
642 let Some(raw) = value.as_str() else {
643 return Err(format!(
644 "invalid op; expected {}",
645 trade_parsing::MODIFY_OP_PARSE_CHOICES
646 ));
647 };
648 if let Ok(n) = raw.trim().parse::<i32>() {
649 return Ok(n);
650 }
651 trade_parsing::parse_modify_op_id(raw).ok_or_else(|| {
652 format!(
653 "invalid op {raw:?}; expected {}",
654 trade_parsing::MODIFY_OP_PARSE_CHOICES
655 )
656 })
657}
658
659fn apply_aliases_to_map(map: &mut serde_json::Map<String, Value>, aliases: &[(&str, &str)]) {
660 for (alias, canonical) in aliases {
661 if map.contains_key(*canonical) {
662 map.remove(*alias);
664 } else if let Some(v) = map.remove(*alias) {
665 map.insert((*canonical).to_string(), v);
666 }
667 }
668}
669
670fn supports_begin_time_aliases(proto_id: u32) -> bool {
671 matches!(
672 proto_id,
673 futu_core::proto_id::QOT_GET_HISTORY_KL
674 | futu_core::proto_id::QOT_REQUEST_HISTORY_KL
675 | futu_core::proto_id::QOT_GET_OPTION_CHAIN
676 | futu_core::proto_id::QOT_GET_CAPITAL_FLOW
677 | futu_core::proto_id::QOT_GET_SUSPEND
678 | futu_core::proto_id::QOT_GET_HOLDING_CHANGE_LIST
679 | futu_core::proto_id::QOT_GET_TRADE_DATE
680 | futu_core::proto_id::QOT_REQUEST_TRADE_DATE
681 )
682}
683
684fn supports_max_ack_kl_num_aliases(proto_id: u32) -> bool {
685 matches!(
686 proto_id,
687 futu_core::proto_id::QOT_GET_HISTORY_KL
688 | futu_core::proto_id::QOT_REQUEST_HISTORY_KL
689 | futu_core::proto_id::QOT_REQUEST_HISTORY_EVENT_CONTRACT_KL
690 )
691}
692
693pub(crate) fn maybe_wrap_flat_body_as_c2s(value: &mut Value) {
713 let Value::Object(map) = value else {
714 return;
715 };
716 if map.contains_key("c2s") {
718 return;
719 }
720 for response_key in ["s2c", "ret_type", "ret_msg", "err_code"] {
722 if map.contains_key(response_key) {
723 return;
724 }
725 }
726 if map.is_empty() {
728 return;
729 }
730 let inner = std::mem::take(map);
732 map.insert("c2s".to_string(), Value::Object(inner));
733}
734
735pub(crate) fn maybe_expand_flat_trd_header(value: &mut Value) {
758 let Value::Object(top) = value else {
759 return;
760 };
761 let Some(Value::Object(c2s)) = top.get_mut("c2s") else {
762 return;
763 };
764 if matches!(c2s.get("header"), Some(Value::Object(_))) {
766 return;
767 }
768 const HEADER_FIELDS: &[&str] = &["trd_env", "acc_id", "trd_market", "jp_acc_type"];
770 let mut header_map = serde_json::Map::new();
772 for field in HEADER_FIELDS {
773 if let Some(v) = c2s.remove(*field) {
774 header_map.insert((*field).to_string(), v);
775 }
776 }
777 if header_map.is_empty() {
779 return;
780 }
781 c2s.insert("header".to_string(), Value::Object(header_map));
782}
783
784pub(crate) fn expand_symbol_shorthand(value: &mut Value) -> Result<(), String> {
800 let Value::Object(top) = value else {
801 return Ok(());
802 };
803 let Some(Value::Object(inner)) = top.get_mut("c2s") else {
805 return Ok(());
806 };
807
808 expand_symbols_array_to_security_list(inner)?;
821
822 expand_single_symbol_shorthand_to_security_and_owner(inner)?;
837 Ok(())
838}