Skip to main content

futu_rest/adapter/
symbol_normalize.rs

1//! Split from adapter.rs: symbol_normalize.
2//!
3//! pub items: normalize_json_keys_snake_case,expand_single_symbol_shorthand_to_security_and_owner,expand_symbols_array_to_security_list,parse_symbol_prefix,try_parse_mixed_array_to_securities,to_snake_case.
4
5use serde_json::Value;
6
7use super::*;
8
9/// 只是现在同时也加 `owner` 方便 option-chain 等 endpoint。
10pub(super) fn expand_single_symbol_shorthand_to_security_and_owner(
11    inner: &mut serde_json::Map<String, Value>,
12) {
13    // 若用户已显式传 security **对象** 或 owner **对象** → 不动
14    // (但如 owner 是 **string**, 属 shorthand input, 继续处理)
15    let has_security_obj = matches!(inner.get("security"), Some(Value::Object(_)));
16    let has_owner_obj = matches!(inner.get("owner"), Some(Value::Object(_)));
17    if has_security_obj || has_owner_obj {
18        return;
19    }
20    // v1.4.104 eli P1-001 (P1) fix: 若 array path (expand_symbols_array_to_security_list)
21    // 已经生成 `security_list` (说明输入用 list-style endpoint shorthand
22    // `symbol`/`code` singular → list), 单字符串 shorthand 路径**也跳过**.
23    // 否则 single path 会从 `c2s.symbol` 再生成 `c2s.security` + `c2s.owner`
24    // orphan objects, strict_fields validator 见 list-style proto (e.g.
25    // qot_get_basic_qot.Request 仅有 security_list) 会把 security/owner
26    // 当 unknown field → 400 reject.
27    //
28    // 注意 single path 仍要 remove `symbol`/`code` 这些 source key (不让它们
29    // 留下被 strict_fields 当 unknown field). 所以条件是 "security_list 已
30    // 存在 → 只 remove source 不再 insert security/owner".
31    let already_has_security_list = matches!(inner.get("security_list"), Some(Value::Array(_)));
32
33    // 找 shorthand source key (按优先级: symbol > code > owner-string > security_string)
34    const CANDIDATES: &[&str] = &["symbol", "code", "owner", "security_string"];
35    let mut source_key_opt: Option<&'static str> = None;
36    for key in CANDIDATES {
37        if let Some(v) = inner.get(*key)
38            && v.is_string()
39        {
40            source_key_opt = Some(*key);
41            break;
42        }
43    }
44    let Some(source_key) = source_key_opt else {
45        return;
46    };
47    let Some(raw) = inner.remove(source_key) else {
48        return;
49    };
50    let Value::String(sym) = raw else {
51        inner.insert(source_key.to_string(), raw);
52        return;
53    };
54    if let Some((market, code)) = parse_symbol_prefix(&sym) {
55        // v1.4.104 eli P1-001 (P1) follow-up: 保留原行为 (生成 security + owner 双
56        // 对象), 即使 array path 已生成 security_list. 因为大量已有测试期望
57        // single path 生成 security/owner. 改修在 strict_fields 把 c2s.security
58        // / c2s.owner 加进 ignore_paths for list-style endpoint (quote / snapshot
59        // / subscribe). 此变量 already_has_security_list 仅作 debug 提示, 不再
60        // 改变行为.
61        let _ = already_has_security_list;
62        let mut sec = serde_json::Map::new();
63        sec.insert("market".to_string(), Value::Number(market.into()));
64        sec.insert("code".to_string(), Value::String(code));
65        // 生成 security + owner 双对象 (各 proto 按自己字段名取)
66        inner.insert("security".to_string(), Value::Object(sec.clone()));
67        inner.insert("owner".to_string(), Value::Object(sec));
68    } else {
69        // prefix 不认识 → 放回 source key, 让 serde 报精确错误
70        inner.insert(source_key.to_string(), Value::String(sym));
71    }
72}
73
74/// v1.4.82 B1: `"HK.00700"` / `"US.AAPL"` 等 prefix-style symbol 字符串 →
75/// `(market: i32, code: String)`。
76///
77/// 返 None 表示无 `.` 分隔 或 prefix 未知。抽成独立 fn 是因为 `security_list`
78/// (v1.4.82 B1) 和 `security` (v1.4.73 BUG-005) 两条 shorthand 路径都需要。
79fn parse_symbol_prefix(sym: &str) -> Option<(i32, String)> {
80    let (prefix, code) = sym.split_once('.')?;
81    let market = match prefix {
82        "HK" => 1,
83        "HK_FUTURE" => 2,
84        "US" => 11,
85        "SH" => 21,
86        "SZ" => 22,
87        "SG" => 31,
88        "JP" => 41,
89        "AU" => 51,
90        "MY" => 61,
91        "CA" => 71,
92        "FX" => 81,
93        "CRYPTO" | "CC" => 91,
94        "US_FUTURE" => 12, // 备用(QotMarket_US_Future=12 非标准,可能不用)
95        _ => return None,
96    };
97    Some((market, code.to_string()))
98}
99
100/// v1.4.82 B1: 把 `code_list` / `symbols` / `stocks` / `symbol_list` 的字符串
101/// 数组展开为 `security_list: [{market, code}, ...]` proto 期望格式。
102///
103/// **两类来源**:
104/// 1. `security_list` 本身是 `[String]`(A1 alias 已把 `stocks`/`symbols`
105///    rename 成 `security_list` 但保留原 string array 值)— 就地 transform
106/// 2. `code_list` / `symbol_list` 等独立 shorthand key 存在 — take + transform
107///
108/// 用户已传 `security_list: [{market, code}, ...]`(正确 proto 格式)时不动。
109/// 处理三种形式的 transform:
110///   - 纯 `[String]` 数组(v1.4.82 B1 原实装)
111///   - 纯 `[{market, code}]` 对象数组(proto 正确格式,不动)
112///   - **混合** `[{market, code}, "US.AAPL", ...]`(v1.4.88 扩展;string 元素
113///     被就地展开成 object,object 元素透传)
114///
115/// 若任一 string 元素 prefix 未知或非 String/Object → 整体回退放回,让 serde
116/// 报精确错误,不 silent transform。
117///
118/// **v1.4.90 P0-C**: 任一路径数组长度超 `MAX_SYMBOLS_PER_REQUEST` 立即返
119/// `Err(msg)`, 上抛由 `proto_request_with_idempotency` 转 400. v1.4.82 B1
120/// 漏的 input validation, 攻击者 / buggy SDK 一次传 150k symbols → quota
121/// 爆 + RSS 40× 放大. 检查覆盖三种来源:
122///   - `security_list` 已含 string 元素(混合或纯 string)
123///   - `security_list` 已是纯 object array(用户绕过 shorthand 直传)
124///   - 独立 shorthand key (`code_list` / `symbols` / `stocks` / `symbol_list`)
125pub(super) fn expand_symbols_array_to_security_list(
126    inner: &mut serde_json::Map<String, Value>,
127) -> Result<(), String> {
128    // 路径 1: security_list 存在(A1 alias rename 后的形态 / 用户直传)
129    if let Some(existing) = inner.get("security_list") {
130        // v1.4.90 P0-C: 不论 string / object array 都先 cap 长度
131        if let Value::Array(items) = existing
132            && items.len() > MAX_SYMBOLS_PER_REQUEST
133        {
134            return Err(format!(
135                "security_list length {} exceeds MAX_SYMBOLS_PER_REQUEST={}; \
136                 split into multiple requests",
137                items.len(),
138                MAX_SYMBOLS_PER_REQUEST
139            ));
140        }
141        // 判断 security_list 当前值是否需要 transform(含 string 元素即需要)
142        if let Value::Array(items) = existing
143            && items.iter().any(|it| it.is_string())
144        {
145            // 从 shorthand string/mixed array → Security objects(v1.4.88
146            // 扩展:支持 string + object 混合,object 元素直接透传)
147            // 取出后重新插入(避免 borrow 冲突)
148            let Some(raw) = inner.remove("security_list") else {
149                return Ok(());
150            };
151            let Value::Array(items) = raw else {
152                inner.insert("security_list".to_string(), raw);
153                return Ok(());
154            };
155            if let Some(expanded) = try_parse_mixed_array_to_securities(&items) {
156                inner.insert("security_list".to_string(), Value::Array(expanded));
157            } else {
158                // parse 失败 → 放回让 serde 报错
159                inner.insert("security_list".to_string(), Value::Array(items));
160            }
161            // 已处理 path 1,不再 fall through 到 path 2 (security_list 已
162            // 就地 transformed,避免再次触发)
163            return Ok(());
164        }
165        // security_list 是纯 object array / 空数组 / 非 array — 用户显式传,不动
166        return Ok(());
167    }
168    // 路径 2: 独立 shorthand key(code_list / symbols / stocks / symbol_list)
169    //
170    // v1.4.104 eli P1-001 (P1) extension: 也接受单字符串 `symbol` / `code`
171    // 作 1-element list shorthand. CLI/MCP 都接 `symbol: "US.AAPL"`, REST 之
172    // 前要求 array (`symbols: ["US.AAPL"]`), 不一致让 agent 困惑. 现在两边
173    // 都接, single-string 路径生成 `security_list: [{market, code}]`.
174    //
175    // 注: `symbol` / `code` 也是 `expand_single_symbol_shorthand_to_security_and_owner`
176    // (path 后续) 的输入. 两个 path 用同 source key 时, 这里 (array path)
177    // 优先 — security_list 是更通用形态, 单 security/owner endpoint 仍能用
178    // security_list[0] 的 fallback (因 proto 字段名不同 serde 会 silent drop).
179    // 但 single-symbol endpoint 通常显式传 `security` 对象, shorthand 走
180    // single path 才需要; 此处只在 list 类 endpoint 配合.
181    //
182    // **去重策略**: 我们不 remove `symbol`/`code` (留给后续 single path 处理),
183    // 只 clone 其值生成 `security_list`. 这样两个 path 各取所需, 互不打架.
184    const CANDIDATES: &[&str] = &["code_list", "symbols", "stocks", "symbol_list"];
185    const SINGULAR_CANDIDATES: &[&str] = &["symbol", "code"];
186    let mut source_key_opt: Option<&'static str> = None;
187    for key in CANDIDATES {
188        if inner.contains_key(*key) {
189            source_key_opt = Some(*key);
190            break;
191        }
192    }
193    if source_key_opt.is_none() {
194        // v1.4.104 eli P1-001 (P1) fix: 尝试 singular shorthand → 1-element list.
195        // **重要**: 不 remove `symbol`/`code` 让 single path 后续也用 (生成
196        // security/owner objects). single path 会 remove. 但 strict_fields
197        // validator 在 expand_symbol_shorthand 后立即跑 deny_unknown_fields,
198        // 此时 single path 已经 remove 了, 不会冲突.
199        for key in SINGULAR_CANDIDATES {
200            if let Some(Value::String(s)) = inner.get(*key)
201                && let Some((market, code)) = parse_symbol_prefix(s)
202            {
203                let mut sec = serde_json::Map::new();
204                sec.insert("market".to_string(), Value::Number(market.into()));
205                sec.insert("code".to_string(), Value::String(code));
206                inner.insert(
207                    "security_list".to_string(),
208                    Value::Array(vec![Value::Object(sec)]),
209                );
210                // 不 return — 让外层 expand_symbol_shorthand 继续走 single path
211                // (会 remove `symbol`/`code`, 防 strict_fields unknown-field reject)
212                return Ok(());
213            }
214        }
215        return Ok(());
216    }
217    let Some(source_key) = source_key_opt else {
218        return Ok(());
219    };
220    let Some(raw) = inner.remove(source_key) else {
221        return Ok(());
222    };
223    let Value::Array(items) = raw else {
224        // 不是 array → 放回让 serde 报错
225        inner.insert(source_key.to_string(), raw);
226        return Ok(());
227    };
228    // v1.4.90 P0-C: shorthand-key 路径同样 cap 长度
229    if items.len() > MAX_SYMBOLS_PER_REQUEST {
230        return Err(format!(
231            "{} length {} exceeds MAX_SYMBOLS_PER_REQUEST={}; \
232             split into multiple requests",
233            source_key,
234            items.len(),
235            MAX_SYMBOLS_PER_REQUEST
236        ));
237    }
238    if let Some(expanded) = try_parse_mixed_array_to_securities(&items) {
239        inner.insert("security_list".to_string(), Value::Array(expanded));
240    } else {
241        // parse 失败 → 回退原 key + 原 array
242        inner.insert(source_key.to_string(), Value::Array(items));
243    }
244    Ok(())
245}
246
247/// v1.4.88: 把 `[String | Security{market, code}]` 混合数组解析为
248/// `[Security{market, code}]`(pure object / pure string / mixed 三形态)。
249///
250/// 每个元素的处理:
251///   - `Value::String(sym)` → parse_symbol_prefix → `{market, code}` object
252///   - `Value::Object` 含 `market` + `code` 字段 → 直接透传(不重 parse)
253///   - 其他(Object 缺字段 / Number / Bool / null / Array) → 整体返 None
254///
255/// 只要一个 string 元素 prefix 未知 → 返 None,调用方回退原数组让 serde 报精确错。
256/// 这保证 "要么全部合法转换,要么原样回退" 的 all-or-nothing 语义,避免半转换
257/// 留下混合状态 confuse 下游 handler。
258fn try_parse_mixed_array_to_securities(items: &[Value]) -> Option<Vec<Value>> {
259    let mut out: Vec<Value> = Vec::with_capacity(items.len());
260    for item in items {
261        match item {
262            Value::String(sym) => {
263                let (market, code) = parse_symbol_prefix(sym)?;
264                let mut sec = serde_json::Map::new();
265                sec.insert("market".to_string(), Value::Number(market.into()));
266                sec.insert("code".to_string(), Value::String(code));
267                out.push(Value::Object(sec));
268            }
269            // 已是 Security object — 校验含 market + code 字段(防用户误传
270            // 其他 schema 也 silently 透传 junk 下去)
271            Value::Object(obj) if obj.contains_key("market") && obj.contains_key("code") => {
272                out.push(Value::Object(obj.clone()));
273            }
274            _ => return None,
275        }
276    }
277    Some(out)
278}
279
280pub fn normalize_json_keys_snake_case(value: &mut Value) {
281    match value {
282        Value::Object(map) => {
283            // 取出所有 entries,重建 map(遇到同 key snake_case 已存在,后到的覆盖)
284            let mut new_map = serde_json::Map::with_capacity(map.len());
285            // 用 std::mem::take 避免 clone
286            let old = std::mem::take(map);
287            for (k, mut v) in old {
288                normalize_json_keys_snake_case(&mut v);
289                let sk = to_snake_case(&k);
290                new_map.insert(sk, v);
291            }
292            *map = new_map;
293        }
294        Value::Array(arr) => {
295            for item in arr {
296                normalize_json_keys_snake_case(item);
297            }
298        }
299        _ => {}
300    }
301}
302
303/// camelCase / PascalCase → snake_case 转换。
304/// 已经是 snake_case / lowercase 的保持不变。
305/// 规则:每个 uppercase letter 前加 `_`(除非在开头),然后全小写。
306///
307/// 例:
308/// - `accID` → `acc_id` (ID 两个大写合并成一个 _id)
309/// - `trdEnv` → `trd_env`
310/// - `filterConditions` → `filter_conditions`
311/// - `acc_id` → `acc_id` (已经 snake_case, 不动)
312/// - `id` → `id`
313/// - `ID` → `id`
314pub(super) fn to_snake_case(s: &str) -> String {
315    let mut out = String::with_capacity(s.len() + 4);
316    let chars: Vec<char> = s.chars().collect();
317    for (i, &c) in chars.iter().enumerate() {
318        if c.is_ascii_uppercase() {
319            // v1.4.47 P1.3 修(eli 验收报告 §14.5):之前 `pwdMD5` → `pwd_m_d5`(错),
320            // `TRD_ENV` → `tr_d_env`(错)。根因:之前"连续大写 abbreviation"判据用
321            // `next is uppercase or end`,不考虑 next 是**数字或下划线**的情况 ——
322            // 这时应该**继续** treat as abbreviation(MD5 整体 / TRD 整体)不 break。
323            //
324            // 新规则:boundary 触发 = 从 non-upper 进 upper (正常 camelCase),
325            // OR 从 upper 进 upper 再接 **lowercase** (acronym 结束,如 XMLParser
326            // 在 P 前 break: `xml_parser`)。数字 / 下划线 / 结尾都算 "abbreviation
327            // 继续",不触发 boundary.
328            //
329            // 覆盖新场景:
330            //  - `pwdMD5` → `pwd_md5` ✓(MD5 作为 abbreviation)
331            //  - `TRD_ENV` → `trd_env` ✓(TRD 下划线前不 break 内部)
332            //  - `accID` → `acc_id` ✓(原行为保持)
333            //  - `XMLParser` → `xml_parser` ✓(acronym 遇 lowercase break)
334            //
335            // 未覆盖:`trdenv` / `TRDENV` 这种无 boundary marker 的全连写,无法拆(需字典)。
336            let prev = if i > 0 { Some(chars[i - 1]) } else { None };
337            let next = chars.get(i + 1).copied();
338            let prev_upper = prev.map(|p| p.is_ascii_uppercase()).unwrap_or(false);
339            // v1.4.47 P1.3: 判"abbreviation 继续"时考虑数字和下划线
340            let next_is_not_lower = next.map(|n| !n.is_ascii_lowercase()).unwrap_or(true);
341            let at_boundary = i > 0 && prev != Some('_') && !(prev_upper && next_is_not_lower);
342            if at_boundary {
343                out.push('_');
344            }
345            out.push(c.to_ascii_lowercase());
346        } else {
347            out.push(c);
348        }
349    }
350    out
351}