tatara_engine/nix_eval/
sui_client.rs1use anyhow::{Context, Result};
10use serde::{Deserialize, Serialize};
11use tracing::{debug, info};
12
13pub struct SuiClient {
15 client: reqwest::Client,
16 base_url: String,
17}
18
19#[derive(Debug, Serialize)]
20struct BuildRequest {
21 flake_ref: String,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 system: Option<String>,
24 #[serde(skip_serializing_if = "Vec::is_empty")]
25 extra_args: Vec<String>,
26}
27
28#[derive(Debug, Deserialize)]
29struct BuildResponse {
30 store_path: String,
31 #[serde(default)]
32 build_time_ms: u64,
33}
34
35#[derive(Debug, Serialize)]
36struct EvalRequest {
37 expression: String,
38}
39
40#[derive(Debug, Deserialize)]
41struct EvalResponse {
42 result: serde_json::Value,
43}
44
45#[derive(Debug, Serialize)]
46struct CachePushRequest {
47 store_path: String,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 cache_name: Option<String>,
50}
51
52impl SuiClient {
53 pub fn new(daemon_addr: &str) -> Self {
55 let base_url = if daemon_addr.starts_with("http") {
56 daemon_addr.to_string()
57 } else {
58 format!("http://{daemon_addr}")
59 };
60
61 let client = reqwest::Client::builder()
62 .timeout(std::time::Duration::from_secs(300))
63 .build()
64 .unwrap_or_default();
65
66 Self { client, base_url }
67 }
68
69 pub async fn build(
71 &self,
72 flake_ref: &str,
73 system: Option<&str>,
74 extra_args: Vec<String>,
75 ) -> Result<String> {
76 let url = format!("{}/api/v1/build", self.base_url);
77 info!(flake_ref, "building via sui-daemon");
78
79 let resp = self
80 .client
81 .post(&url)
82 .json(&BuildRequest {
83 flake_ref: flake_ref.to_string(),
84 system: system.map(String::from),
85 extra_args,
86 })
87 .send()
88 .await
89 .context("sui-daemon build request failed")?;
90
91 if !resp.status().is_success() {
92 let status = resp.status();
93 let body = resp.text().await.unwrap_or_default();
94 anyhow::bail!("sui-daemon build failed ({status}): {body}");
95 }
96
97 let result: BuildResponse = resp.json().await?;
98 debug!(store_path = %result.store_path, build_time_ms = result.build_time_ms, "build complete");
99 Ok(result.store_path)
100 }
101
102 pub async fn eval_json(&self, expr: &str) -> Result<serde_json::Value> {
104 let url = format!("{}/api/v1/eval", self.base_url);
105 debug!(expr_len = expr.len(), "evaluating via sui-daemon");
106
107 let resp = self
108 .client
109 .post(&url)
110 .json(&EvalRequest {
111 expression: expr.to_string(),
112 })
113 .send()
114 .await
115 .context("sui-daemon eval request failed")?;
116
117 if !resp.status().is_success() {
118 let status = resp.status();
119 let body = resp.text().await.unwrap_or_default();
120 anyhow::bail!("sui-daemon eval failed ({status}): {body}");
121 }
122
123 let result: EvalResponse = resp.json().await?;
124 Ok(result.result)
125 }
126
127 pub async fn push_to_cache(&self, store_path: &str, cache_name: Option<&str>) -> Result<()> {
129 let url = format!("{}/api/v1/cache/push", self.base_url);
130 info!(store_path, "pushing to sui-cache");
131
132 let resp = self
133 .client
134 .post(&url)
135 .json(&CachePushRequest {
136 store_path: store_path.to_string(),
137 cache_name: cache_name.map(String::from),
138 })
139 .send()
140 .await
141 .context("sui-daemon cache push failed")?;
142
143 if !resp.status().is_success() {
144 let status = resp.status();
145 let body = resp.text().await.unwrap_or_default();
146 anyhow::bail!("sui-daemon cache push failed ({status}): {body}");
147 }
148
149 Ok(())
150 }
151
152 pub async fn health_check(&self) -> bool {
154 let url = format!("{}/health", self.base_url);
155 self.client
156 .get(&url)
157 .send()
158 .await
159 .map(|r| r.status().is_success())
160 .unwrap_or(false)
161 }
162}