Skip to main content

canic_host/
response_parse.rs

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