Skip to main content

canic_host/response_parse/
mod.rs

1pub const RECORD_MARKER: &str = "record {";
2
3#[cfg(test)]
4mod tests;
5
6#[must_use]
7pub fn find_field<'a>(value: &'a serde_json::Value, field: &str) -> Option<&'a serde_json::Value> {
8    match value {
9        serde_json::Value::Object(map) => map
10            .get(field)
11            .or_else(|| map.values().find_map(|value| find_field(value, field))),
12        serde_json::Value::Array(values) => {
13            values.iter().find_map(|value| find_field(value, field))
14        }
15        _ => None,
16    }
17}
18
19#[must_use]
20pub fn find_string_field(value: &serde_json::Value, field: &str) -> Option<String> {
21    match value {
22        serde_json::Value::Object(map) => map
23            .get(field)
24            .and_then(|value| value.as_str().map(ToString::to_string))
25            .or_else(|| {
26                map.values()
27                    .find_map(|value| find_string_field(value, field))
28            }),
29        serde_json::Value::Array(values) => values
30            .iter()
31            .find_map(|value| find_string_field(value, field)),
32        _ => None,
33    }
34}
35
36#[must_use]
37pub fn response_candid(value: &serde_json::Value) -> Option<&str> {
38    find_field(value, "response_candid").and_then(serde_json::Value::as_str)
39}
40
41#[must_use]
42pub fn parse_candid_text_field(output: &str, field: &str) -> Option<String> {
43    let after_eq = field_value_after_equals(output, field)?;
44    let after_quote = after_eq.trim_start().strip_prefix('"')?;
45    parse_candid_quoted_text(after_quote)
46}
47
48#[must_use]
49pub fn parse_candid_text_like_field(output: &str, field: &str) -> Option<String> {
50    let after_eq = field_value_after_equals(output, field)?;
51    let after_quote = after_eq
52        .trim_start()
53        .strip_prefix("opt \"")
54        .or_else(|| after_eq.trim_start().strip_prefix('"'))?;
55    parse_candid_quoted_text(after_quote)
56}
57
58#[must_use]
59pub fn parse_cycle_balance_response(output: &str) -> Option<u128> {
60    serde_json::from_str::<serde_json::Value>(output)
61        .ok()
62        .and_then(|value| {
63            find_field(&value, "Ok")
64                .and_then(parse_json_u128)
65                .or_else(|| response_candid(&value).and_then(parse_cycle_balance_candid))
66        })
67        .or_else(|| parse_cycle_balance_candid(output))
68}
69
70fn parse_cycle_balance_candid(output: &str) -> Option<u128> {
71    output
72        .split_once('=')
73        .map_or(output, |(_, cycles)| cycles)
74        .lines()
75        .find_map(parse_leading_u128_digits)
76}
77
78fn parse_candid_quoted_text(text_after_quote: &str) -> Option<String> {
79    let mut value = String::new();
80    let mut escaped = false;
81    for ch in text_after_quote.chars() {
82        if escaped {
83            value.push(ch);
84            escaped = false;
85            continue;
86        }
87        if ch == '\\' {
88            escaped = true;
89            continue;
90        }
91        if ch == '"' {
92            return Some(value);
93        }
94        value.push(ch);
95    }
96    None
97}
98
99#[must_use]
100pub fn parse_json_u64(value: &serde_json::Value) -> Option<u64> {
101    value
102        .as_u64()
103        .or_else(|| value.as_str().and_then(parse_u64_digits))
104}
105
106#[must_use]
107pub fn parse_json_u128(value: &serde_json::Value) -> Option<u128> {
108    value
109        .as_u64()
110        .map(u128::from)
111        .or_else(|| value.as_str().and_then(parse_u128_digits))
112}
113
114#[must_use]
115pub fn field_value_after_equals<'a>(text: &'a str, field: &str) -> Option<&'a str> {
116    let (_, after_field) = text.split_once(field)?;
117    let (_, after_eq) = after_field.split_once('=')?;
118    Some(after_eq.trim_start())
119}
120
121#[must_use]
122pub fn text_after<'a>(text: &'a str, marker: &str) -> Option<&'a str> {
123    let (_, after_marker) = text.split_once(marker)?;
124    Some(after_marker.trim_start())
125}
126
127#[must_use]
128pub fn parse_u64_digits(text: &str) -> Option<u64> {
129    number_digits(text).parse().ok()
130}
131
132#[must_use]
133pub fn parse_u128_digits(text: &str) -> Option<u128> {
134    number_digits(text).parse().ok()
135}
136
137#[must_use]
138fn parse_leading_u128_digits(text: &str) -> Option<u128> {
139    leading_number_digits(text).parse().ok()
140}
141
142#[must_use]
143pub fn quoted_strings(text: &str) -> Vec<String> {
144    let mut values = Vec::new();
145    let mut remaining = text;
146    while let Some((_, after_open)) = remaining.split_once('"') {
147        let Some((value, after_close)) = after_open.split_once('"') else {
148            break;
149        };
150        values.push(value.to_string());
151        remaining = after_close;
152    }
153    values
154}
155
156#[must_use]
157pub fn candid_record_blocks(text: &str) -> Vec<&str> {
158    let mut blocks = Vec::new();
159    let mut index = 0;
160    while let Some(relative_start) = text[index..].find(RECORD_MARKER) {
161        let start = index + relative_start;
162        let mut depth = 1_u32;
163        let mut cursor = start + RECORD_MARKER.len();
164        let bytes = text.as_bytes();
165        while cursor < text.len() {
166            match bytes[cursor] {
167                b'{' => depth = depth.saturating_add(1),
168                b'}' => {
169                    depth = depth.saturating_sub(1);
170                    if depth == 0 {
171                        let end = cursor + 1;
172                        blocks.push(&text[start..end]);
173                        index = start + RECORD_MARKER.len();
174                        break;
175                    }
176                }
177                _ => {}
178            }
179            cursor += 1;
180        }
181        if depth != 0 {
182            break;
183        }
184    }
185    blocks
186}
187
188fn number_digits(text: &str) -> String {
189    text.chars()
190        .skip_while(|ch| !ch.is_ascii_digit())
191        .take_while(|ch| ch.is_ascii_digit() || *ch == '_' || *ch == ',')
192        .filter(char::is_ascii_digit)
193        .collect()
194}
195
196fn leading_number_digits(text: &str) -> String {
197    text.trim_start_matches(|ch: char| ch == '(' || ch.is_whitespace())
198        .chars()
199        .take_while(|ch| ch.is_ascii_digit() || *ch == '_' || *ch == ',')
200        .filter(char::is_ascii_digit)
201        .collect()
202}