1use serde_json::{json, Value};
2
3use rns_net::{
4 Destination, QueryRequest, QueryResponse, RnsNode,
5 DestHash, IdentityHash, ProofStrategy,
6};
7use rns_crypto::identity::Identity;
8
9use crate::auth::check_auth;
10use crate::config::CtlConfig;
11use crate::encode::{from_base64, hex_to_array, to_base64, to_hex};
12use crate::http::{parse_query, HttpRequest, HttpResponse};
13use crate::state::{DestinationEntry, SharedState};
14
15pub type NodeHandle = std::sync::Arc<std::sync::Mutex<Option<RnsNode>>>;
17
18fn with_node<F>(node: &NodeHandle, f: F) -> HttpResponse
20where
21 F: FnOnce(&RnsNode) -> HttpResponse,
22{
23 let guard = node.lock().unwrap();
24 match guard.as_ref() {
25 Some(n) => f(n),
26 None => HttpResponse::internal_error("Node is shutting down"),
27 }
28}
29
30pub fn handle_request(
32 req: &HttpRequest,
33 node: &NodeHandle,
34 state: &SharedState,
35 config: &CtlConfig,
36) -> HttpResponse {
37 if req.method == "GET" && req.path == "/health" {
39 return HttpResponse::ok(json!({"status": "healthy"}));
40 }
41
42 if let Err(resp) = check_auth(req, config) {
44 return resp;
45 }
46
47 match (req.method.as_str(), req.path.as_str()) {
48 ("GET", "/api/info") => handle_info(node, state),
50 ("GET", "/api/interfaces") => handle_interfaces(node),
51 ("GET", "/api/destinations") => handle_destinations(node, state),
52 ("GET", "/api/paths") => handle_paths(req, node),
53 ("GET", "/api/links") => handle_links(node),
54 ("GET", "/api/resources") => handle_resources(node),
55 ("GET", "/api/announces") => handle_event_list(req, state, "announces"),
56 ("GET", "/api/packets") => handle_event_list(req, state, "packets"),
57 ("GET", "/api/proofs") => handle_event_list(req, state, "proofs"),
58 ("GET", "/api/link_events") => handle_event_list(req, state, "link_events"),
59 ("GET", "/api/resource_events") => handle_event_list(req, state, "resource_events"),
60
61 ("GET", path) if path.starts_with("/api/identity/") => {
63 let hash_str = &path["/api/identity/".len()..];
64 handle_recall_identity(hash_str, node)
65 }
66
67 ("POST", "/api/destination") => handle_post_destination(req, node, state),
69 ("POST", "/api/announce") => handle_post_announce(req, node, state),
70 ("POST", "/api/send") => handle_post_send(req, node, state),
71 ("POST", "/api/link") => handle_post_link(req, node),
72 ("POST", "/api/link/send") => handle_post_link_send(req, node),
73 ("POST", "/api/link/close") => handle_post_link_close(req, node),
74 ("POST", "/api/channel") => handle_post_channel(req, node),
75 ("POST", "/api/resource") => handle_post_resource(req, node),
76 ("POST", "/api/path/request") => handle_post_path_request(req, node),
77
78 _ => HttpResponse::not_found(),
79 }
80}
81
82fn handle_info(node: &NodeHandle, state: &SharedState) -> HttpResponse {
85 with_node(node, |n| {
86 let transport_id = match n.query(QueryRequest::TransportIdentity) {
87 Ok(QueryResponse::TransportIdentity(id)) => id,
88 _ => None,
89 };
90 let s = state.read().unwrap();
91 HttpResponse::ok(json!({
92 "transport_id": transport_id.map(|h| to_hex(&h)),
93 "identity_hash": s.identity_hash.map(|h| to_hex(&h)),
94 "uptime_seconds": s.uptime_seconds(),
95 }))
96 })
97}
98
99fn handle_interfaces(node: &NodeHandle) -> HttpResponse {
100 with_node(node, |n| {
101 match n.query(QueryRequest::InterfaceStats) {
102 Ok(QueryResponse::InterfaceStats(stats)) => {
103 let ifaces: Vec<Value> = stats
104 .interfaces
105 .iter()
106 .map(|i| {
107 json!({
108 "name": i.name,
109 "status": if i.status { "up" } else { "down" },
110 "mode": i.mode,
111 "interface_type": i.interface_type,
112 "rxb": i.rxb,
113 "txb": i.txb,
114 "rx_packets": i.rx_packets,
115 "tx_packets": i.tx_packets,
116 "bitrate": i.bitrate,
117 "started": i.started,
118 "ia_freq": i.ia_freq,
119 "oa_freq": i.oa_freq,
120 })
121 })
122 .collect();
123 HttpResponse::ok(json!({
124 "interfaces": ifaces,
125 "transport_enabled": stats.transport_enabled,
126 "transport_uptime": stats.transport_uptime,
127 "total_rxb": stats.total_rxb,
128 "total_txb": stats.total_txb,
129 }))
130 }
131 _ => HttpResponse::internal_error("Query failed"),
132 }
133 })
134}
135
136fn handle_destinations(node: &NodeHandle, state: &SharedState) -> HttpResponse {
137 with_node(node, |n| {
138 match n.query(QueryRequest::LocalDestinations) {
139 Ok(QueryResponse::LocalDestinations(dests)) => {
140 let s = state.read().unwrap();
141 let list: Vec<Value> = dests
142 .iter()
143 .map(|d| {
144 let name = s
145 .destinations
146 .get(&d.hash)
147 .map(|e| e.full_name.as_str())
148 .unwrap_or("");
149 json!({
150 "hash": to_hex(&d.hash),
151 "type": d.dest_type,
152 "name": name,
153 })
154 })
155 .collect();
156 HttpResponse::ok(json!({"destinations": list}))
157 }
158 _ => HttpResponse::internal_error("Query failed"),
159 }
160 })
161}
162
163fn handle_paths(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
164 let params = parse_query(&req.query);
165 let filter_hash: Option<[u8; 16]> = params
166 .get("dest_hash")
167 .and_then(|s| hex_to_array(s));
168
169 with_node(node, |n| {
170 match n.query(QueryRequest::PathTable { max_hops: None }) {
171 Ok(QueryResponse::PathTable(paths)) => {
172 let list: Vec<Value> = paths
173 .iter()
174 .filter(|p| filter_hash.map_or(true, |h| p.hash == h))
175 .map(|p| {
176 json!({
177 "hash": to_hex(&p.hash),
178 "via": to_hex(&p.via),
179 "hops": p.hops,
180 "expires": p.expires,
181 "interface": p.interface_name,
182 "timestamp": p.timestamp,
183 })
184 })
185 .collect();
186 HttpResponse::ok(json!({"paths": list}))
187 }
188 _ => HttpResponse::internal_error("Query failed"),
189 }
190 })
191}
192
193fn handle_links(node: &NodeHandle) -> HttpResponse {
194 with_node(node, |n| {
195 match n.query(QueryRequest::Links) {
196 Ok(QueryResponse::Links(links)) => {
197 let list: Vec<Value> = links
198 .iter()
199 .map(|l| {
200 json!({
201 "link_id": to_hex(&l.link_id),
202 "state": l.state,
203 "is_initiator": l.is_initiator,
204 "dest_hash": to_hex(&l.dest_hash),
205 "remote_identity": l.remote_identity.map(|h| to_hex(&h)),
206 "rtt": l.rtt,
207 })
208 })
209 .collect();
210 HttpResponse::ok(json!({"links": list}))
211 }
212 _ => HttpResponse::internal_error("Query failed"),
213 }
214 })
215}
216
217fn handle_resources(node: &NodeHandle) -> HttpResponse {
218 with_node(node, |n| {
219 match n.query(QueryRequest::Resources) {
220 Ok(QueryResponse::Resources(resources)) => {
221 let list: Vec<Value> = resources
222 .iter()
223 .map(|r| {
224 json!({
225 "link_id": to_hex(&r.link_id),
226 "direction": r.direction,
227 "total_parts": r.total_parts,
228 "transferred_parts": r.transferred_parts,
229 "complete": r.complete,
230 })
231 })
232 .collect();
233 HttpResponse::ok(json!({"resources": list}))
234 }
235 _ => HttpResponse::internal_error("Query failed"),
236 }
237 })
238}
239
240fn handle_event_list(req: &HttpRequest, state: &SharedState, kind: &str) -> HttpResponse {
241 let params = parse_query(&req.query);
242 let clear = params.get("clear").map_or(false, |v| v == "true");
243
244 let mut s = state.write().unwrap();
245 let items: Vec<Value> = match kind {
246 "announces" => {
247 let v: Vec<Value> = s
248 .announces
249 .iter()
250 .map(|r| serde_json::to_value(r).unwrap_or_default())
251 .collect();
252 if clear {
253 s.announces.clear();
254 }
255 v
256 }
257 "packets" => {
258 let v: Vec<Value> = s
259 .packets
260 .iter()
261 .map(|r| serde_json::to_value(r).unwrap_or_default())
262 .collect();
263 if clear {
264 s.packets.clear();
265 }
266 v
267 }
268 "proofs" => {
269 let v: Vec<Value> = s
270 .proofs
271 .iter()
272 .map(|r| serde_json::to_value(r).unwrap_or_default())
273 .collect();
274 if clear {
275 s.proofs.clear();
276 }
277 v
278 }
279 "link_events" => {
280 let v: Vec<Value> = s
281 .link_events
282 .iter()
283 .map(|r| serde_json::to_value(r).unwrap_or_default())
284 .collect();
285 if clear {
286 s.link_events.clear();
287 }
288 v
289 }
290 "resource_events" => {
291 let v: Vec<Value> = s
292 .resource_events
293 .iter()
294 .map(|r| serde_json::to_value(r).unwrap_or_default())
295 .collect();
296 if clear {
297 s.resource_events.clear();
298 }
299 v
300 }
301 _ => Vec::new(),
302 };
303
304 let mut obj = serde_json::Map::new();
305 obj.insert(kind.to_string(), Value::Array(items));
306 HttpResponse::ok(Value::Object(obj))
307}
308
309fn handle_recall_identity(hash_str: &str, node: &NodeHandle) -> HttpResponse {
310 let dest_hash: [u8; 16] = match hex_to_array(hash_str) {
311 Some(h) => h,
312 None => return HttpResponse::bad_request("Invalid dest_hash hex (expected 32 hex chars)"),
313 };
314
315 with_node(node, |n| {
316 match n.recall_identity(&DestHash(dest_hash)) {
317 Ok(Some(ai)) => HttpResponse::ok(json!({
318 "dest_hash": to_hex(&ai.dest_hash.0),
319 "identity_hash": to_hex(&ai.identity_hash.0),
320 "public_key": to_hex(&ai.public_key),
321 "app_data": ai.app_data.as_ref().map(|d| to_base64(d)),
322 "hops": ai.hops,
323 "received_at": ai.received_at,
324 })),
325 Ok(None) => HttpResponse::not_found(),
326 Err(_) => HttpResponse::internal_error("Query failed"),
327 }
328 })
329}
330
331fn parse_json_body(req: &HttpRequest) -> Result<Value, HttpResponse> {
334 serde_json::from_slice(&req.body).map_err(|e| HttpResponse::bad_request(&format!("Invalid JSON: {}", e)))
335}
336
337fn handle_post_destination(
338 req: &HttpRequest,
339 node: &NodeHandle,
340 state: &SharedState,
341) -> HttpResponse {
342 let body = match parse_json_body(req) {
343 Ok(v) => v,
344 Err(r) => return r,
345 };
346
347 let dest_type_str = body["type"].as_str().unwrap_or("");
348 let app_name = match body["app_name"].as_str() {
349 Some(s) => s,
350 None => return HttpResponse::bad_request("Missing app_name"),
351 };
352 let aspects: Vec<&str> = body["aspects"]
353 .as_array()
354 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
355 .unwrap_or_default();
356
357 let (identity_hash, identity_prv_key, identity_pub_key) = {
358 let s = state.read().unwrap();
359 let ih = s.identity_hash;
360 let prv = s.identity.as_ref().and_then(|i| i.get_private_key());
361 let pubk = s.identity.as_ref().and_then(|i| i.get_public_key());
362 (ih, prv, pubk)
363 };
364
365 let (dest, signing_key) = match dest_type_str {
366 "single" => {
367 let direction = body["direction"].as_str().unwrap_or("in");
368 match direction {
369 "in" => {
370 let ih = match identity_hash {
371 Some(h) => IdentityHash(h),
372 None => return HttpResponse::internal_error("No identity loaded"),
373 };
374 let dest = Destination::single_in(app_name, &aspects, ih)
375 .set_proof_strategy(parse_proof_strategy(&body));
376 (dest, identity_prv_key)
377 }
378 "out" => {
379 let dh_str = match body["dest_hash"].as_str() {
380 Some(s) => s,
381 None => return HttpResponse::bad_request("OUT single requires dest_hash of remote"),
382 };
383 let dh: [u8; 16] = match hex_to_array(dh_str) {
384 Some(h) => h,
385 None => return HttpResponse::bad_request("Invalid dest_hash"),
386 };
387 return with_node(node, |n| {
388 match n.recall_identity(&DestHash(dh)) {
389 Ok(Some(recalled)) => {
390 let dest = Destination::single_out(app_name, &aspects, &recalled);
391 let full_name = format_dest_name(app_name, &aspects);
393 let mut s = state.write().unwrap();
394 s.destinations.insert(dest.hash.0, DestinationEntry {
395 destination: dest.clone(),
396 full_name: full_name.clone(),
397 });
398 HttpResponse::created(json!({
399 "dest_hash": to_hex(&dest.hash.0),
400 "name": full_name,
401 "type": "single",
402 "direction": "out",
403 }))
404 }
405 Ok(None) => HttpResponse::bad_request("No recalled identity for dest_hash"),
406 Err(_) => HttpResponse::internal_error("Query failed"),
407 }
408 });
409 }
410 _ => return HttpResponse::bad_request("direction must be 'in' or 'out'"),
411 }
412 }
413 "plain" => {
414 let dest = Destination::plain(app_name, &aspects)
415 .set_proof_strategy(parse_proof_strategy(&body));
416 (dest, None)
417 }
418 "group" => {
419 let mut dest = Destination::group(app_name, &aspects)
420 .set_proof_strategy(parse_proof_strategy(&body));
421 if let Some(key_b64) = body["group_key"].as_str() {
422 match from_base64(key_b64) {
423 Some(key) => {
424 if let Err(e) = dest.load_private_key(key) {
425 return HttpResponse::bad_request(&format!("Invalid group key: {}", e));
426 }
427 }
428 None => return HttpResponse::bad_request("Invalid base64 group_key"),
429 }
430 } else {
431 dest.create_keys();
432 }
433 (dest, None)
434 }
435 _ => return HttpResponse::bad_request("type must be 'single', 'plain', or 'group'"),
436 };
437
438 with_node(node, |n| {
439 match n.register_destination_with_proof(&dest, signing_key) {
440 Ok(()) => {
441 if dest_type_str == "single"
444 && body["direction"].as_str().unwrap_or("in") == "in"
445 {
446 if let (Some(prv), Some(pubk)) = (identity_prv_key, identity_pub_key) {
447 let mut sig_prv = [0u8; 32];
448 sig_prv.copy_from_slice(&prv[32..64]);
449 let mut sig_pub = [0u8; 32];
450 sig_pub.copy_from_slice(&pubk[32..64]);
451 let _ = n.register_link_destination(dest.hash.0, sig_prv, sig_pub);
452 }
453 }
454
455 let full_name = format_dest_name(app_name, &aspects);
456 let hash_hex = to_hex(&dest.hash.0);
457 let group_key_b64 = dest.get_private_key().map(to_base64);
458 let mut s = state.write().unwrap();
459 s.destinations.insert(
460 dest.hash.0,
461 DestinationEntry {
462 destination: dest,
463 full_name: full_name.clone(),
464 },
465 );
466 let mut resp = json!({
467 "dest_hash": hash_hex,
468 "name": full_name,
469 "type": dest_type_str,
470 });
471 if let Some(gk) = group_key_b64 {
472 resp["group_key"] = Value::String(gk);
473 }
474 HttpResponse::created(resp)
475 }
476 Err(_) => HttpResponse::internal_error("Failed to register destination"),
477 }
478 })
479}
480
481fn handle_post_announce(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
482 let body = match parse_json_body(req) {
483 Ok(v) => v,
484 Err(r) => return r,
485 };
486
487 let dh_str = match body["dest_hash"].as_str() {
488 Some(s) => s,
489 None => return HttpResponse::bad_request("Missing dest_hash"),
490 };
491 let dh: [u8; 16] = match hex_to_array(dh_str) {
492 Some(h) => h,
493 None => return HttpResponse::bad_request("Invalid dest_hash"),
494 };
495
496 let app_data: Option<Vec<u8>> = body["app_data"]
497 .as_str()
498 .and_then(from_base64);
499
500 let (dest, identity) = {
501 let s = state.read().unwrap();
502 let dest = match s.destinations.get(&dh) {
503 Some(entry) => entry.destination.clone(),
504 None => return HttpResponse::bad_request("Destination not registered via API"),
505 };
506 let identity = match s.identity.as_ref().and_then(|i| i.get_private_key()) {
507 Some(prv) => Identity::from_private_key(&prv),
508 None => return HttpResponse::internal_error("No identity loaded"),
509 };
510 (dest, identity)
511 };
512
513 with_node(node, |n| {
514 match n.announce(&dest, &identity, app_data.as_deref()) {
515 Ok(()) => HttpResponse::ok(json!({"status": "announced", "dest_hash": dh_str})),
516 Err(_) => HttpResponse::internal_error("Announce failed"),
517 }
518 })
519}
520
521fn handle_post_send(req: &HttpRequest, node: &NodeHandle, state: &SharedState) -> HttpResponse {
522 let body = match parse_json_body(req) {
523 Ok(v) => v,
524 Err(r) => return r,
525 };
526
527 let dh_str = match body["dest_hash"].as_str() {
528 Some(s) => s,
529 None => return HttpResponse::bad_request("Missing dest_hash"),
530 };
531 let dh: [u8; 16] = match hex_to_array(dh_str) {
532 Some(h) => h,
533 None => return HttpResponse::bad_request("Invalid dest_hash"),
534 };
535 let data = match body["data"].as_str().and_then(from_base64) {
536 Some(d) => d,
537 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
538 };
539
540 let s = state.read().unwrap();
541 let dest = match s.destinations.get(&dh) {
542 Some(entry) => entry.destination.clone(),
543 None => return HttpResponse::bad_request("Destination not registered via API"),
544 };
545 drop(s);
546
547 with_node(node, |n| {
548 match n.send_packet(&dest, &data) {
549 Ok(ph) => HttpResponse::ok(json!({
550 "status": "sent",
551 "packet_hash": to_hex(&ph.0),
552 })),
553 Err(_) => HttpResponse::internal_error("Send failed"),
554 }
555 })
556}
557
558fn handle_post_link(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
559 let body = match parse_json_body(req) {
560 Ok(v) => v,
561 Err(r) => return r,
562 };
563
564 let dh_str = match body["dest_hash"].as_str() {
565 Some(s) => s,
566 None => return HttpResponse::bad_request("Missing dest_hash"),
567 };
568 let dh: [u8; 16] = match hex_to_array(dh_str) {
569 Some(h) => h,
570 None => return HttpResponse::bad_request("Invalid dest_hash"),
571 };
572
573 with_node(node, |n| {
574 let recalled = match n.recall_identity(&DestHash(dh)) {
576 Ok(Some(ai)) => ai,
577 Ok(None) => return HttpResponse::bad_request("No recalled identity for dest_hash"),
578 Err(_) => return HttpResponse::internal_error("Query failed"),
579 };
580 let mut sig_pub = [0u8; 32];
582 sig_pub.copy_from_slice(&recalled.public_key[32..64]);
583
584 match n.create_link(dh, sig_pub) {
585 Ok(link_id) => HttpResponse::created(json!({
586 "link_id": to_hex(&link_id),
587 })),
588 Err(_) => HttpResponse::internal_error("Create link failed"),
589 }
590 })
591}
592
593fn handle_post_link_send(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
594 let body = match parse_json_body(req) {
595 Ok(v) => v,
596 Err(r) => return r,
597 };
598
599 let link_id: [u8; 16] = match body["link_id"]
600 .as_str()
601 .and_then(|s| hex_to_array(s))
602 {
603 Some(h) => h,
604 None => return HttpResponse::bad_request("Missing or invalid link_id"),
605 };
606 let data = match body["data"].as_str().and_then(from_base64) {
607 Some(d) => d,
608 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
609 };
610 let context = body["context"].as_u64().unwrap_or(0) as u8;
611
612 with_node(node, |n| {
613 match n.send_on_link(link_id, data, context) {
614 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
615 Err(_) => HttpResponse::internal_error("Send on link failed"),
616 }
617 })
618}
619
620fn handle_post_link_close(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
621 let body = match parse_json_body(req) {
622 Ok(v) => v,
623 Err(r) => return r,
624 };
625
626 let link_id: [u8; 16] = match body["link_id"]
627 .as_str()
628 .and_then(|s| hex_to_array(s))
629 {
630 Some(h) => h,
631 None => return HttpResponse::bad_request("Missing or invalid link_id"),
632 };
633
634 with_node(node, |n| {
635 match n.teardown_link(link_id) {
636 Ok(()) => HttpResponse::ok(json!({"status": "closed"})),
637 Err(_) => HttpResponse::internal_error("Teardown link failed"),
638 }
639 })
640}
641
642fn handle_post_channel(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
643 let body = match parse_json_body(req) {
644 Ok(v) => v,
645 Err(r) => return r,
646 };
647
648 let link_id: [u8; 16] = match body["link_id"]
649 .as_str()
650 .and_then(|s| hex_to_array(s))
651 {
652 Some(h) => h,
653 None => return HttpResponse::bad_request("Missing or invalid link_id"),
654 };
655 let msgtype = body["msgtype"].as_u64().unwrap_or(0) as u16;
656 let payload = match body["payload"].as_str().and_then(from_base64) {
657 Some(d) => d,
658 None => return HttpResponse::bad_request("Missing or invalid base64 payload"),
659 };
660
661 with_node(node, |n| {
662 match n.send_channel_message(link_id, msgtype, payload) {
663 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
664 Err(_) => HttpResponse::internal_error("Channel message failed"),
665 }
666 })
667}
668
669fn handle_post_resource(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
670 let body = match parse_json_body(req) {
671 Ok(v) => v,
672 Err(r) => return r,
673 };
674
675 let link_id: [u8; 16] = match body["link_id"]
676 .as_str()
677 .and_then(|s| hex_to_array(s))
678 {
679 Some(h) => h,
680 None => return HttpResponse::bad_request("Missing or invalid link_id"),
681 };
682 let data = match body["data"].as_str().and_then(from_base64) {
683 Some(d) => d,
684 None => return HttpResponse::bad_request("Missing or invalid base64 data"),
685 };
686 let metadata = body["metadata"]
687 .as_str()
688 .and_then(from_base64);
689
690 with_node(node, |n| {
691 match n.send_resource(link_id, data, metadata) {
692 Ok(()) => HttpResponse::ok(json!({"status": "sent"})),
693 Err(_) => HttpResponse::internal_error("Resource send failed"),
694 }
695 })
696}
697
698fn handle_post_path_request(req: &HttpRequest, node: &NodeHandle) -> HttpResponse {
699 let body = match parse_json_body(req) {
700 Ok(v) => v,
701 Err(r) => return r,
702 };
703
704 let dh_str = match body["dest_hash"].as_str() {
705 Some(s) => s,
706 None => return HttpResponse::bad_request("Missing dest_hash"),
707 };
708 let dh: [u8; 16] = match hex_to_array(dh_str) {
709 Some(h) => h,
710 None => return HttpResponse::bad_request("Invalid dest_hash"),
711 };
712
713 with_node(node, |n| {
714 match n.request_path(&DestHash(dh)) {
715 Ok(()) => HttpResponse::ok(json!({"status": "requested"})),
716 Err(_) => HttpResponse::internal_error("Path request failed"),
717 }
718 })
719}
720
721fn format_dest_name(app_name: &str, aspects: &[&str]) -> String {
724 if aspects.is_empty() {
725 app_name.to_string()
726 } else {
727 format!("{}.{}", app_name, aspects.join("."))
728 }
729}
730
731fn parse_proof_strategy(body: &Value) -> ProofStrategy {
732 match body["proof_strategy"].as_str() {
733 Some("all") => ProofStrategy::ProveAll,
734 Some("app") => ProofStrategy::ProveApp,
735 _ => ProofStrategy::ProveNone,
736 }
737}