canic_host/response_parse/
mod.rs1pub 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}