canic_host/
response_parse.rs1pub 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 output
49 .split_once('=')
50 .map_or(output, |(_, cycles)| cycles)
51 .lines()
52 .find_map(parse_leading_u128_digits)
53}
54
55#[must_use]
56pub fn parse_json_u64(value: &serde_json::Value) -> Option<u64> {
57 value
58 .as_u64()
59 .or_else(|| value.as_str().and_then(parse_u64_digits))
60}
61
62#[must_use]
63pub fn parse_json_u128(value: &serde_json::Value) -> Option<u128> {
64 value
65 .as_u64()
66 .map(u128::from)
67 .or_else(|| value.as_str().and_then(parse_u128_digits))
68}
69
70#[must_use]
71pub fn field_value_after_equals<'a>(text: &'a str, field: &str) -> Option<&'a str> {
72 let (_, after_field) = text.split_once(field)?;
73 let (_, after_eq) = after_field.split_once('=')?;
74 Some(after_eq.trim_start())
75}
76
77#[must_use]
78pub fn text_after<'a>(text: &'a str, marker: &str) -> Option<&'a str> {
79 let (_, after_marker) = text.split_once(marker)?;
80 Some(after_marker.trim_start())
81}
82
83#[must_use]
84pub fn parse_u64_digits(text: &str) -> Option<u64> {
85 number_digits(text).parse().ok()
86}
87
88#[must_use]
89pub fn parse_u128_digits(text: &str) -> Option<u128> {
90 number_digits(text).parse().ok()
91}
92
93#[must_use]
94pub fn parse_leading_u128_digits(text: &str) -> Option<u128> {
95 leading_number_digits(text).parse().ok()
96}
97
98#[must_use]
99pub fn quoted_strings(text: &str) -> Vec<String> {
100 let mut values = Vec::new();
101 let mut remaining = text;
102 while let Some((_, after_open)) = remaining.split_once('"') {
103 let Some((value, after_close)) = after_open.split_once('"') else {
104 break;
105 };
106 values.push(value.to_string());
107 remaining = after_close;
108 }
109 values
110}
111
112#[must_use]
113pub fn candid_record_blocks(text: &str) -> Vec<&str> {
114 let mut blocks = Vec::new();
115 let mut index = 0;
116 while let Some(relative_start) = text[index..].find(RECORD_MARKER) {
117 let start = index + relative_start;
118 let mut depth = 1_u32;
119 let mut cursor = start + RECORD_MARKER.len();
120 let bytes = text.as_bytes();
121 while cursor < text.len() {
122 match bytes[cursor] {
123 b'{' => depth = depth.saturating_add(1),
124 b'}' => {
125 depth = depth.saturating_sub(1);
126 if depth == 0 {
127 let end = cursor + 1;
128 blocks.push(&text[start..end]);
129 index = start + RECORD_MARKER.len();
130 break;
131 }
132 }
133 _ => {}
134 }
135 cursor += 1;
136 }
137 if depth != 0 {
138 break;
139 }
140 }
141 blocks
142}
143
144fn number_digits(text: &str) -> String {
145 text.chars()
146 .skip_while(|ch| !ch.is_ascii_digit())
147 .take_while(|ch| ch.is_ascii_digit() || *ch == '_' || *ch == ',')
148 .filter(char::is_ascii_digit)
149 .collect()
150}
151
152fn leading_number_digits(text: &str) -> String {
153 text.trim_start_matches(|ch: char| ch == '(' || ch.is_whitespace())
154 .chars()
155 .take_while(|ch| ch.is_ascii_digit() || *ch == '_' || *ch == ',')
156 .filter(char::is_ascii_digit)
157 .collect()
158}