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}