Skip to main content

wpl/parser/
utils.rs

1use smol_str::SmolStr;
2use winnow::ascii::{multispace0, take_escaped};
3use winnow::combinator::{alt, delimited, fail, opt, peek, preceded, separated_pair};
4use winnow::error::{ContextError, ErrMode};
5use winnow::stream::Stream;
6use winnow::token::{any, literal, none_of, one_of, take, take_until, take_while};
7use wp_model_core::model::Value;
8use wp_primitives::Parser;
9use wp_primitives::WResult;
10
11use wp_primitives::symbol::ctx_desc;
12
13//#[allow(clippy::nonminimal_bool)]
14pub fn take_ref_path<'a>(input: &mut &'a str) -> WResult<&'a str> {
15    let s = *input;
16    let mut pos = 0usize;
17    let mut paren_depth = 0usize;
18
19    while pos < s.len() {
20        let rest = &s[pos..];
21        let ch = rest.chars().next().expect("pos bounds checked");
22        let ch_len = ch.len_utf8();
23        match ch {
24            ')' if paren_depth == 0 => break,
25            '(' => {
26                paren_depth += 1;
27                pos += ch_len;
28            }
29            ')' => {
30                paren_depth = paren_depth.saturating_sub(1);
31                pos += ch_len;
32            }
33            '<' => {
34                if let Some(seg_len) = parse_wrapped_key_segment(rest, '<', '>') {
35                    pos += seg_len;
36                } else {
37                    break;
38                }
39            }
40            '{' => {
41                if let Some(seg_len) = parse_wrapped_key_segment(rest, '{', '}') {
42                    pos += seg_len;
43                } else {
44                    break;
45                }
46            }
47            '>' | '}' => break,
48            _ => {
49                let allowed =
50                    ch.is_alphanumeric() || matches!(ch, '_' | '/' | '-' | '.' | '[' | ']' | '*');
51                if !allowed {
52                    break;
53                }
54                pos += ch_len;
55            }
56        }
57    }
58
59    if pos == 0 {
60        return fail.context(ctx_desc("<ref_path>")).parse_next(input);
61    }
62
63    let (head, tail) = s.split_at(pos);
64    *input = tail;
65    Ok(head)
66}
67
68fn parse_wrapped_key_segment(s: &str, open: char, close: char) -> Option<usize> {
69    let mut iter = s.char_indices();
70    let (_, first) = iter.next()?;
71    if first != open {
72        return None;
73    }
74    let mut has_inner = false;
75    for (idx, ch) in iter {
76        if ch == close {
77            return has_inner.then_some(idx + ch.len_utf8());
78        }
79        if !is_wrapped_key_char(ch) {
80            return None;
81        }
82        has_inner = true;
83    }
84    None
85}
86
87#[inline]
88fn is_wrapped_key_char(ch: char) -> bool {
89    ch.is_alphanumeric() || matches!(ch, '_' | '/' | '-' | '.')
90}
91
92/// Parse field reference path: supports either bare identifiers or single-quoted strings
93/// Examples: `@field_name`, `@'@special-field'`
94/// Single quotes are raw strings - only \' is escaped
95pub fn take_ref_path_or_quoted(input: &mut &str) -> WResult<String> {
96    alt((
97        single_quot_raw_str,
98        take_ref_path.map(|s: &str| s.to_string()),
99    ))
100    .parse_next(input)
101}
102pub fn take_exact_path<'a>(input: &mut &'a str) -> WResult<&'a str> {
103    take_while(1.., |c: char| {
104        c.is_alphanumeric() || c == '_' || c == '/' || c == '-' || c == '.'
105    })
106    .parse_next(input)
107}
108
109pub fn take_key<'a>(input: &mut &'a str) -> WResult<&'a str> {
110    take_while(1.., |c: char| {
111        c.is_alphanumeric() || c == '_' || c == '/' || c == '-' || c == '.'
112    })
113    .parse_next(input)
114}
115
116pub fn take_kv_key<'a>(input: &mut &'a str) -> WResult<&'a str> {
117    take_while(1.., |c: char| {
118        c.is_alphanumeric()
119            || matches!(
120                c,
121                '_' | '/' | '-' | '.' | '(' | ')' | '<' | '>' | '[' | ']' | '{' | '}'
122            )
123    })
124    .parse_next(input)
125}
126
127pub fn take_var_name<'a>(input: &mut &'a str) -> WResult<&'a str> {
128    take_while(1.., |c: char| {
129        c.is_alphanumeric() || c == '_' || c == '.' || c == '-'
130    })
131    .parse_next(input)
132}
133
134pub fn take_fun_name<'a>(input: &mut &'a str) -> WResult<&'a str> {
135    //trace("var_name", move |input: &mut &'a str| {
136    take_while(1.., |c: char| c.is_alphanumeric() || c == '_' || c == '.').parse_next(input)
137    //})
138    //.parse_next(input)
139}
140
141pub fn take_meta_name<'a>(input: &mut &'a str) -> WResult<&'a str> {
142    //trace("keyword", move |input: &mut &'a str| {
143    take_while(1.., |c: char| c.is_alphanumeric() || c == '_' || c == '/').parse_next(input)
144    //})
145    //.parse_next(input)
146}
147
148pub fn take_sql_tval(input: &mut &str) -> WResult<Value> {
149    let chars = opt(alt((
150        delimited('"', take_until(0.., "\""), '"'),
151        delimited('\'', take_until(0.., "'"), '\''),
152    )))
153    .parse_next(input)?;
154    if let Some(chars) = chars {
155        return Ok(Value::Chars(chars.into()));
156    }
157    if let Some(value) = opt(take_while(0.., ('0'..='9', '.', '-', '+'))).parse_next(input)? {
158        if let Ok(digit) = value.parse::<i64>() {
159            return Ok(Value::Digit(digit));
160        } else {
161            return Ok(Value::Float(value.parse::<f64>().unwrap_or(0.0)));
162        }
163    }
164
165    //fail get value;
166    "fail-value".parse_next(input)?;
167    Ok(Value::Chars("fail-value".into()))
168}
169
170#[inline]
171pub fn quot_str<'a>(input: &mut &'a str) -> WResult<&'a str> {
172    alt((
173        duble_quot_str_impl.context(ctx_desc(
174            "<quoted_string>::= '\"' , <character_sequence> , '\"' ",
175        )),
176        single_quot_str_impl.context(ctx_desc(
177            "<quoted_string>::= '\"' , <character_sequence> , '\"' ",
178        )),
179    ))
180    .parse_next(input)
181}
182
183#[inline]
184pub fn chars_value<'a>(input: &mut &'a str) -> WResult<&'a str> {
185    let s = *input;
186    if s.starts_with('"') || s.starts_with('\'') {
187        let cp = input.checkpoint();
188        if let Ok(value) = quot_str(input) {
189            return Ok(value);
190        }
191        input.reset(&cp);
192        return alt((window_path, take_to_end)).parse_next(input);
193    }
194    if s.starts_with("r#\"") || s.starts_with("r\"") {
195        let cp = input.checkpoint();
196        if let Ok(value) = quot_r_str(input) {
197            return Ok(value);
198        }
199        input.reset(&cp);
200    }
201    alt((window_path, take_to_end)).parse_next(input)
202}
203
204#[inline]
205pub fn interval_data<'a>(input: &mut &'a str) -> WResult<&'a str> {
206    interval_impl
207        .context(ctx_desc("extract bracketed segments: (), [], {}, <>"))
208        .parse_next(input)
209}
210
211// 不要匹配 ‘\’ 和 ‘“’
212// 引号字符串:允许任意非引号/反斜杠字符,转义支持 \" \\ \n \t \r \xHH
213#[inline]
214pub fn duble_quot_str_impl<'a>(input: &mut &'a str) -> WResult<&'a str> {
215    literal('"')
216        .context(ctx_desc("<beg>\""))
217        .parse_next(input)?;
218    let content = take_escaped(none_of(['\\', '"']), '\\', any).parse_next(input)?;
219    literal('"')
220        .context(ctx_desc("<end>\""))
221        .parse_next(input)?;
222    Ok(content)
223}
224#[inline]
225pub fn single_quot_str_impl<'a>(input: &mut &'a str) -> WResult<&'a str> {
226    literal('\'')
227        .context(ctx_desc("<beg>'"))
228        .parse_next(input)?;
229    let content = take_escaped(none_of(['\\', '\'']), '\\', any).parse_next(input)?;
230    literal('\'')
231        .context(ctx_desc("<end>'"))
232        .parse_next(input)?;
233    Ok(content)
234}
235
236/// Parse single-quoted raw string: only \' is escaped, others are literal
237/// Used for field references where single quotes represent raw strings
238#[inline]
239pub fn single_quot_raw_str(input: &mut &str) -> WResult<String> {
240    literal('\'')
241        .context(ctx_desc("<beg>'"))
242        .parse_next(input)?;
243
244    let mut result = String::new();
245    let mut chars = input.chars();
246
247    loop {
248        match chars.next() {
249            None => {
250                return fail
251                    .context(ctx_desc("unclosed single quote"))
252                    .parse_next(input);
253            }
254            Some('\\') => {
255                // Only handle \' escape, others are literal
256                match chars.as_str().chars().next() {
257                    Some('\'') => {
258                        result.push('\'');
259                        chars.next(); // consume '
260                    }
261                    _ => {
262                        // Other cases, \ is literal
263                        result.push('\\');
264                    }
265                }
266            }
267            Some('\'') => {
268                // Closing quote
269                let consumed = input.len() - chars.as_str().len();
270                *input = &input[consumed..];
271                return Ok(result);
272            }
273            Some(ch) => result.push(ch),
274        }
275    }
276}
277
278#[inline]
279pub fn interval_impl<'a>(input: &mut &'a str) -> WResult<&'a str> {
280    let s = *input;
281    let mut chars = s.char_indices();
282    let Some((_, first)) = chars.next() else {
283        return fail
284            .context(ctx_desc("interval requires leading bracket"))
285            .parse_next(input);
286    };
287
288    let Some(first_close) = closing_for_bracket(first) else {
289        return fail
290            .context(ctx_desc("interval must start with [ ( { <"))
291            .parse_next(input);
292    };
293    let mut stack: Vec<char> = vec![first_close];
294    let mut iter = chars.peekable();
295
296    while let Some((idx, ch)) = iter.next() {
297        if ch == '\\' {
298            // skip escaped character outside of quoted sections
299            let _ = iter.next();
300            continue;
301        }
302        match ch {
303            '[' | '(' | '{' | '<' => {
304                if let Some(close) = closing_for_bracket(ch) {
305                    stack.push(close);
306                }
307            }
308            ']' | ')' | '}' | '>' => {
309                let expected = stack.pop().unwrap();
310                if ch != expected {
311                    return fail
312                        .context(ctx_desc("interval bracket mismatch"))
313                        .parse_next(input);
314                }
315                if stack.is_empty() {
316                    let end = idx + ch.len_utf8();
317                    let (matched, rest) = s.split_at(end);
318                    *input = rest;
319                    return Ok(matched);
320                }
321            }
322            '"' | '\'' => {
323                let quote = ch;
324                let mut escaped = false;
325                for (_, qc) in iter.by_ref() {
326                    if escaped {
327                        escaped = false;
328                        continue;
329                    }
330                    if qc == '\\' {
331                        escaped = true;
332                        continue;
333                    }
334                    if qc == quote {
335                        break;
336                    }
337                }
338            }
339            _ => {}
340        }
341    }
342
343    fail.context(ctx_desc("interval missing closing bracket"))
344        .parse_next(input)
345}
346
347fn closing_for_bracket(ch: char) -> Option<char> {
348    match ch {
349        '[' => Some(']'),
350        '(' => Some(')'),
351        '{' => Some('}'),
352        '<' => Some('>'),
353        _ => None,
354    }
355}
356
357pub fn window_path<'a>(input: &mut &'a str) -> WResult<&'a str> {
358    literal('"').parse_next(input)?;
359    let content = take_until(0.., "\"").parse_next(input)?;
360    literal('"').parse_next(input)?;
361    Ok(content)
362}
363
364/// 原始字符串(首选):r#"..."#,内容不做转义处理;
365/// 兼容旧写法:r"..."(仅为向后兼容,未来可能移除)。
366pub fn quot_r_str<'a>(input: &mut &'a str) -> WResult<&'a str> {
367    let s = *input;
368    // 优先解析 r#"..."#
369    if let Some(rest) = s.strip_prefix("r#\"") {
370        if let Some(pos) = rest.find("\"#") {
371            let content = &rest[..pos];
372            let new_rest = &rest[pos + 2..];
373            *input = new_rest;
374            return Ok(content);
375        } else {
376            return fail
377                .context(ctx_desc("raw string not closed: r#\"...\"#"))
378                .parse_next(input);
379        }
380    }
381    // 回退兼容 r"..."
382    if let Some(rest) = s.strip_prefix("r\"") {
383        if let Some(pos) = rest.find('"') {
384            let content = &rest[..pos];
385            let new_rest = &rest[pos + 1..];
386            *input = new_rest;
387            return Ok(content);
388        } else {
389            return fail
390                .context(ctx_desc("raw string not closed: r\"...\""))
391                .parse_next(input);
392        }
393    }
394    // 不匹配
395    fail.parse_next(input)
396}
397
398pub fn quot_raw<'a>(input: &mut &'a str) -> WResult<&'a str> {
399    let cp = input.checkpoint();
400    literal('"').parse_next(input)?;
401    let content = take_escaped(none_of(['\\', '"']), '\\', any).parse_next(input)?;
402    literal('"').parse_next(input)?;
403    let len = content.len() + 2;
404    input.reset(&cp);
405    let raw = take(len).parse_next(input)?;
406    Ok(raw)
407}
408
409pub fn take_parentheses<'a>(input: &mut &'a str) -> WResult<&'a str> {
410    literal('(').parse_next(input)?;
411    let content = take_escaped(none_of(['\\', ')']), '\\', one_of([')'])).parse_next(input)?;
412    literal(')').parse_next(input)?;
413    Ok(content)
414}
415
416// #[tag(tag : "hello", raw_copy : "raw" ), copy_raw(name:"hello")]
417pub fn decode_escapes(s: &str) -> String {
418    let mut out: Vec<u8> = Vec::with_capacity(s.len());
419    let mut it = s.chars().peekable();
420    while let Some(c) = it.next() {
421        if c == '\\' {
422            match it.peek().copied() {
423                Some('"') => {
424                    out.push(b'"');
425                    it.next();
426                }
427                Some('\'') => {
428                    out.push(b'\'');
429                    it.next();
430                }
431                Some('\\') => {
432                    out.push(b'\\');
433                    it.next();
434                }
435                Some('n') => {
436                    out.push(b'\n');
437                    it.next();
438                }
439                Some('t') => {
440                    out.push(b'\t');
441                    it.next();
442                }
443                Some('r') => {
444                    out.push(b'\r');
445                    it.next();
446                }
447                Some('x') => {
448                    it.next();
449                    let h1 = it.next();
450                    let h2 = it.next();
451                    if let (Some(h1), Some(h2)) = (h1, h2) {
452                        let hex = [h1, h2];
453                        let val = hex
454                            .iter()
455                            .try_fold(0u8, |v, ch| ch.to_digit(16).map(|d| (v << 4) | (d as u8)));
456                        if let Some(b) = val {
457                            out.push(b);
458                        } else {
459                            out.extend_from_slice(b"\\x");
460                            out.extend_from_slice(h1.to_string().as_bytes());
461                            out.extend_from_slice(h2.to_string().as_bytes());
462                        }
463                    } else {
464                        out.extend_from_slice(b"\\x");
465                        if let Some(h1) = h1 {
466                            out.extend_from_slice(h1.to_string().as_bytes());
467                        }
468                        if let Some(h2) = h2 {
469                            out.extend_from_slice(h2.to_string().as_bytes());
470                        }
471                    }
472                }
473                Some(ch) => {
474                    out.push(b'\\');
475                    out.extend_from_slice(ch.to_string().as_bytes());
476                    it.next();
477                }
478                None => {}
479            }
480        } else {
481            let mut buf = [0u8; 4];
482            let s = c.encode_utf8(&mut buf);
483            out.extend_from_slice(s.as_bytes());
484        }
485    }
486    String::from_utf8_lossy(&out).to_string()
487}
488
489pub fn take_tag_kv(input: &mut &str) -> WResult<(SmolStr, SmolStr)> {
490    // 值支持普通引号字符串与原始字符串;普通字符串会做一次反转义,原始字符串保持原样
491    separated_pair(
492        preceded(multispace0, take_key),
493        (multispace0, ':', multispace0),
494        alt((
495            quot_r_str.map(|s: &str| SmolStr::from(s)),
496            quot_str.map(|s: &str| SmolStr::from(decode_escapes(s))),
497        )),
498    )
499    .map(|(k, v)| (SmolStr::from(k), v))
500    .parse_next(input)
501}
502
503#[inline]
504pub fn take_to_end<'a>(input: &mut &'a str) -> WResult<&'a str> {
505    //trace("take_to_end", move |input: &mut &'a str| {
506    take_while(0.., |_| true).parse_next(input)
507    //})
508    //.parse_next(input)
509}
510
511pub fn peek_str(what: &str, input: &mut &str) -> WResult<()> {
512    // In winnow 0.7, `peek` over a string may produce `Result<_, ContextError>`.
513    // Convert it into `ModalResult<()>` by wrapping the error in `ErrMode`.
514    match peek(what).parse_next(input) {
515        Ok(_) => Ok(()),
516        Err(e) => Err(ErrMode::Backtrack(e)),
517    }
518}
519
520pub fn peek_next<'a, O, ParseNext>(parser: ParseNext, input: &mut &'a str) -> WResult<()>
521where
522    ParseNext: Parser<&'a str, O, ContextError>,
523{
524    match peek(parser).parse_next(input) {
525        Ok(_) => Ok(()),
526        Err(e) => Err(ErrMode::Backtrack(e)),
527    }
528}
529pub fn is_sep_next(input: &mut &str) -> bool {
530    let _ = multispace0::<&str, ErrMode<ContextError>>.parse_next(input);
531    if peek_str(",", input).is_ok() {
532        let _: Result<&str, ErrMode<ContextError>> = literal(",").parse_next(input);
533        return true;
534    }
535    false
536}
537pub fn is_next_unit(prefix: &str, input: &mut &str) -> bool {
538    let _ = multispace0::<&str, ErrMode<ContextError>>.parse_next(input);
539    if peek_str(prefix, input).is_ok() {
540        return true;
541    }
542    false
543}
544
545pub fn is_next<'a, O, ParseNext>(parser: ParseNext, input: &mut &'a str) -> bool
546where
547    ParseNext: Parser<&'a str, O, ContextError>,
548{
549    let _ = multispace0::<&str, ErrMode<ContextError>>.parse_next(input);
550    if peek_next(parser, input).is_ok() {
551        return true;
552    }
553    false
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::parser::error::error_detail;
560    use crate::parser::utils::{
561        chars_value, quot_str, take_key, take_kv_key, take_parentheses, take_to_end,
562    };
563    use crate::parser::wpl_pkg::wpl_package;
564    use orion_error::dev::testing::TestAssert;
565    use winnow::LocatingSlice;
566    use wp_primitives::WResult as ModalResult;
567
568    #[test]
569    fn test_take_val() -> ModalResult<()> {
570        assert_eq!(
571            Value::Chars("key".into()),
572            take_sql_tval.parse_next(&mut "'key'")?
573        );
574        assert_eq!(Value::Digit(100), take_sql_tval.parse_next(&mut "100")?);
575        assert_eq!(
576            Value::Float(100.01),
577            take_sql_tval.parse_next(&mut "100.01")?
578        );
579        assert_eq!(
580            Value::Float(-100.01),
581            take_sql_tval.parse_next(&mut "-100.01")?
582        );
583        Ok(())
584    }
585
586    #[test]
587    fn test_key_ident() {
588        assert_eq!(Ok(("", "key")), take_key.parse_peek("key"));
589        assert_eq!(Ok(("!", "key")), take_key.parse_peek("key!"));
590        assert_eq!(
591            Ok(("!", "http/request")),
592            take_key.parse_peek("http/request!")
593        );
594        assert_eq!(
595            Ok(("!", "123http/request")),
596            take_key.parse_peek("123http/request!")
597        );
598    }
599    #[test]
600    fn test_kv_key_ident() {
601        // basic key chars (same as take_key)
602        assert_eq!(Ok(("", "key")), take_kv_key.parse_peek("key"));
603        assert_eq!(Ok(("!", "key")), take_kv_key.parse_peek("key!"));
604        assert_eq!(
605            Ok(("!", "http/request")),
606            take_kv_key.parse_peek("http/request!")
607        );
608        // parentheses
609        assert_eq!(Ok(("=v", "fn(arg)")), take_kv_key.parse_peek("fn(arg)=v"));
610        // angle brackets
611        assert_eq!(
612            Ok(("=1", "list<int>")),
613            take_kv_key.parse_peek("list<int>=1")
614        );
615        // square brackets
616        assert_eq!(Ok((":x", "arr[0]")), take_kv_key.parse_peek("arr[0]:x"));
617        // curly braces
618        assert_eq!(Ok(("=ok", "set{a}")), take_kv_key.parse_peek("set{a}=ok"));
619        // mixed brackets
620        assert_eq!(
621            Ok(("=v", "a(b)[c]<d>{e}")),
622            take_kv_key.parse_peek("a(b)[c]<d>{e}=v")
623        );
624        // stops at '=' and ':'
625        assert_eq!(Ok(("=val", "key(x)")), take_kv_key.parse_peek("key(x)=val"));
626        assert_eq!(Ok((":val", "key(x)")), take_kv_key.parse_peek("key(x):val"));
627    }
628    #[test]
629    fn test_quot_str() {
630        assert_eq!(quot_str.parse_peek("\"123\""), Ok(("", "123")));
631        assert_eq!(quot_str.parse_peek(r#""\a123""#), Ok(("", r#"\a123"#)));
632        assert_eq!(quot_str.parse_peek("'123'"), Ok(("", "123")));
633        assert_eq!(quot_str.parse_peek("\"1-?#ab\""), Ok(("", "1-?#ab")));
634        assert_eq!(quot_str.parse_peek(r#""12\"3""#), Ok(("", r#"12\"3"#)));
635        assert_eq!(quot_str.parse_peek(r#"'12\"3'"#), Ok(("", r#"12\"3"#)));
636        // 支持 Unicode
637        assert_eq!(quot_str.parse_peek("\"中文🙂\""), Ok(("", "中文🙂")));
638        //assert_eq!(quot_str.parse_peek(r#""sddD:\招标项目\6-MSS\mss日志映射表""#),
639        assert_eq!(
640            window_path.parse_peek(r#""sddD:\招标项目\6-MSS\mss日志映射表""#),
641            Ok(("", r#"sddD:\招标项目\6-MSS\mss日志映射表"#))
642        );
643    }
644    #[test]
645    fn test_quot_r_str() {
646        use crate::parser::utils::quot_r_str;
647        // r#"..."# 支持内部包含引号
648        assert_eq!(
649            quot_r_str.parse_peek("r#\"a\\b \"c\"\"#"),
650            Ok(("", "a\\b \"c\""))
651        );
652        assert_eq!(quot_r_str.parse_peek("r#\"end\"#"), Ok(("", "end")));
653        // 兼容旧写法 r"..."
654        assert_eq!(quot_r_str.parse_peek("r\"raw\""), Ok(("", "raw")));
655    }
656    #[test]
657    fn test_chars_value_dispatch() {
658        assert_eq!(
659            chars_value.parse_peek(r#""GET / HTTP/1.1""#),
660            Ok(("", "GET / HTTP/1.1"))
661        );
662        assert_eq!(
663            chars_value.parse_peek("r#\"raw value\"#"),
664            Ok(("", "raw value"))
665        );
666        assert_eq!(
667            chars_value.parse_peek(r#""unterminated fallback"#),
668            Ok(("", r#""unterminated fallback"#))
669        );
670        assert_eq!(
671            chars_value.parse_peek("plain chars"),
672            Ok(("", "plain chars"))
673        );
674    }
675    #[test]
676    fn test_take_pat() {
677        assert_eq!(take_parentheses.parse_peek("(123)"), Ok(("", "123")));
678        assert_eq!(
679            take_parentheses.parse_peek(r#"(12\)3)"#),
680            Ok(("", r#"12\)3"#))
681        );
682    }
683
684    #[test]
685    fn test_take_to_end() {
686        let input = "";
687        let x = take_to_end.parse(input).assert();
688        assert_eq!(x, "");
689
690        let input = "hello 你好 😂 😁 π \u{3001} \n \t en";
691        let x = take_to_end.parse(input).assert();
692        assert_eq!(x, input);
693    }
694
695    #[test]
696    fn test_prefix() {
697        let data = "{ (digit, time,sn,chars,time,kv,ip,kv,chars,kv,kv,chars,kv,kv,chars,chars,ip,chars,http/request,http/agent)}";
698        if let Err(err) = crate::parser::parse_code::segment.parse(data) {
699            println!("{}", error_detail(err));
700        }
701        assert_eq!(
702            crate::parser::parse_code::segment
703                .parse(data)
704                .assert()
705                .to_string(),
706            r#"  (
707    digit,
708    time,
709    sn,
710    chars,
711    time,
712    kv,
713    ip,
714    kv,
715    chars,
716    kv,
717    kv,
718    chars,
719    kv,
720    kv,
721    chars,
722    chars,
723    ip,
724    chars,
725    http/request,
726    http/agent
727  )"#
728        );
729    }
730    #[test]
731    fn test_meta() {
732        let input = r#"    package test {
733                rule test { (
734                time,
735                time_timestamp
736                ) }
737        }
738    "#;
739
740        assert_eq!(
741            wpl_package
742                .parse(&LocatingSlice::new(input))
743                .assert()
744                .to_string(),
745            r#"package test {
746  rule test {
747    (
748      time,
749      time_timestamp
750    )
751  }
752}
753"#
754        );
755    }
756
757    #[test]
758    fn test_tag_kv_hex_escape() {
759        use super::take_tag_kv;
760        let mut s = "key:\"\\xE4\\xB8\\xAD\\xE6\\x96\\x87\"";
761        let (k, v) = take_tag_kv.parse_next(&mut s).assert();
762        assert_eq!(k, "key");
763        assert_eq!(v, "中文");
764    }
765
766    #[test]
767    fn test_interval_simple() {
768        let mut input = "{payload}";
769        let parsed = interval_data(&mut input).assert();
770        assert_eq!(parsed, "{payload}");
771        assert_eq!(input, "");
772    }
773
774    #[test]
775    fn test_interval_nested_with_quotes() {
776        let mut input = "<({\"[(foo)]\"}, ['x'])>tail";
777        let parsed = interval_data(&mut input).assert();
778        assert_eq!(parsed, "<({\"[(foo)]\"}, ['x'])>");
779        assert_eq!(input, "tail");
780    }
781
782    #[test]
783    fn test_interval_missing_closer() {
784        let mut input = "[1,2";
785        assert!(interval_data(&mut input).is_err());
786    }
787
788    #[test]
789    fn test_take_ref_path_or_quoted() {
790        // Test bare identifier
791        assert_eq!(
792            take_ref_path_or_quoted.parse_peek("field_name"),
793            Ok(("", "field_name".to_string()))
794        );
795
796        // Test single-quoted with @ prefix
797        assert_eq!(
798            take_ref_path_or_quoted.parse_peek("'@abc'"),
799            Ok(("", "@abc".to_string()))
800        );
801
802        // Test single-quoted with spaces
803        assert_eq!(
804            take_ref_path_or_quoted.parse_peek("'field with spaces'"),
805            Ok(("", "field with spaces".to_string()))
806        );
807
808        // Test single-quoted with special characters
809        assert_eq!(
810            take_ref_path_or_quoted.parse_peek("'@special-field#123'"),
811            Ok(("", "@special-field#123".to_string()))
812        );
813
814        // Test escaped quote inside single-quoted string
815        let input = "'field\\'s name'";
816        assert_eq!(
817            take_ref_path_or_quoted.parse_peek(input),
818            Ok(("", "field's name".to_string()))
819        );
820
821        // Test path-like identifier
822        assert_eq!(
823            take_ref_path_or_quoted.parse_peek("process/path[0]"),
824            Ok(("", "process/path[0]".to_string()))
825        );
826        assert_eq!(
827            take_ref_path_or_quoted.parse_peek("list<int>"),
828            Ok(("", "list<int>".to_string()))
829        );
830        assert_eq!(
831            take_ref_path_or_quoted.parse_peek("set{a}"),
832            Ok(("", "set{a}".to_string()))
833        );
834        assert_eq!(
835            take_ref_path_or_quoted.parse_peek("curr<[,]>"),
836            Ok(("<[,]>", "curr".to_string()))
837        );
838        assert_eq!(
839            take_ref_path_or_quoted.parse_peek("curr{\\s(\\S=)}"),
840            Ok(("{\\s(\\S=)}", "curr".to_string()))
841        );
842
843        assert_eq!(
844            take_ref_path_or_quoted.parse_peek("protocal(80)"),
845            Ok(("", "protocal(80)".to_string()))
846        );
847        assert_eq!(
848            take_ref_path_or_quoted.parse_peek("protocal(80))"),
849            Ok((")", "protocal(80)".to_string()))
850        );
851
852        // Test single quotes are raw strings - \n, \t are literal
853        assert_eq!(
854            take_ref_path_or_quoted.parse_peek(r"'raw\nstring'"),
855            Ok(("", r"raw\nstring".to_string()))
856        );
857
858        assert_eq!(
859            take_ref_path_or_quoted.parse_peek(r"'path\to\file'"),
860            Ok(("", r"path\to\file".to_string()))
861        );
862
863        // Only \' is escaped in single quotes
864        assert_eq!(
865            take_ref_path_or_quoted.parse_peek(r"'it\'s here'"),
866            Ok(("", "it's here".to_string()))
867        );
868    }
869}