1use anyhow::{Result, anyhow};
2use bitrpc::{RpcError, cyper::CyperTransport};
3use faasta_interface::{FunctionResult, FunctionServiceRpcClient};
4use std::io;
5use std::path::{Path as StdPath, PathBuf};
6use std::process::exit;
7use tracing::debug;
8use url::Url;
9
10fn same_file_path(a: &str, b: &str) -> bool {
13 let path_a = StdPath::new(a).components().collect::<Vec<_>>();
15 let path_b = StdPath::new(b).components().collect::<Vec<_>>();
16 path_a == path_b
17}
18
19#[derive(Clone)]
20pub struct FunctionServiceClient {
21 endpoint: String,
22}
23
24impl FunctionServiceClient {
25 fn new(endpoint: String) -> Self {
26 Self { endpoint }
27 }
28
29 fn new_transport(&self) -> CyperTransport {
30 CyperTransport::new(self.endpoint.clone())
31 }
32
33 pub async fn publish(
34 &self,
35 wasm_file: Vec<u8>,
36 name: String,
37 github_auth_token: String,
38 ) -> Result<FunctionResult<String>, RpcError> {
39 let mut client = FunctionServiceRpcClient::new(self.new_transport());
40 let response = client.publish(wasm_file, name, github_auth_token).await?;
41 Ok(response)
42 }
43
44 pub async fn list_functions(
45 &self,
46 github_auth_token: String,
47 ) -> Result<FunctionResult<Vec<faasta_interface::FunctionInfo>>, RpcError> {
48 let mut client = FunctionServiceRpcClient::new(self.new_transport());
49 let response = client.list_functions(github_auth_token).await?;
50 Ok(response)
51 }
52
53 pub async fn unpublish(
54 &self,
55 name: String,
56 github_auth_token: String,
57 ) -> Result<FunctionResult<()>, RpcError> {
58 let mut client = FunctionServiceRpcClient::new(self.new_transport());
59 let response = client.unpublish(name, github_auth_token).await?;
60 Ok(response)
61 }
62
63 pub async fn get_metrics(
64 &self,
65 github_auth_token: String,
66 ) -> Result<FunctionResult<faasta_interface::Metrics>, RpcError> {
67 let mut client = FunctionServiceRpcClient::new(self.new_transport());
68 let response = client.get_metrics(github_auth_token).await?;
69 Ok(response)
70 }
71}
72
73fn normalize_endpoint(server_addr: &str) -> Result<String> {
74 let trimmed = server_addr.trim();
75 if trimmed.is_empty() {
76 return Err(anyhow!("Server address cannot be empty"));
77 }
78
79 let mut url = if trimmed.contains("://") {
80 Url::parse(trimmed).map_err(|e| anyhow!("Invalid server address '{trimmed}': {e}"))?
81 } else {
82 Url::parse(&format!("https://{trimmed}"))
83 .or_else(|_| Url::parse(&format!("https://{trimmed}/")))
84 .map_err(|e| anyhow!("Invalid server address '{trimmed}': {e}"))?
85 };
86
87 if url.scheme() != "https" {
88 url.set_scheme("https")
89 .map_err(|_| anyhow!("Server address must use HTTPS"))?;
90 }
91
92 if url.path() == "/" {
93 url.set_path("/rpc");
94 }
95
96 Ok(url.to_string())
97}
98
99pub async fn connect_to_function_service(server_addr: &str) -> Result<FunctionServiceClient> {
101 let endpoint = normalize_endpoint(server_addr)?;
102 debug!("Configured RPC endpoint: {}", endpoint);
103 Ok(FunctionServiceClient::new(endpoint))
104}
105
106pub fn get_project_info() -> Result<(PathBuf, String, PathBuf), io::Error> {
108 let spinner = indicatif::ProgressBar::new_spinner();
109 spinner.set_message("Getting project information...");
110 spinner.enable_steady_tick(std::time::Duration::from_millis(100));
111
112 let output = std::process::Command::new("cargo")
114 .args(["metadata", "--format-version=1"])
115 .output()
116 .unwrap_or_else(|e| {
117 spinner.finish_and_clear();
118 eprintln!("Failed to run cargo metadata: {e}");
119 exit(1);
120 });
121
122 if !output.status.success() {
123 spinner.finish_and_clear();
124 eprintln!("Failed to retrieve cargo metadata");
125 exit(1);
126 }
127
128 let metadata: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
130 spinner.finish_and_clear();
131 eprintln!("Failed to parse cargo metadata: {e}");
132 exit(1);
133 });
134
135 let target_directory = metadata
137 .get("target_directory")
138 .and_then(serde_json::Value::as_str)
139 .map(PathBuf::from)
140 .unwrap_or_else(|| {
141 spinner.finish_and_clear();
142 eprintln!("No 'target_directory' found in cargo metadata");
143 exit(1);
144 });
145
146 let packages = metadata
148 .get("packages")
149 .and_then(serde_json::Value::as_array)
150 .unwrap_or_else(|| {
151 spinner.finish_and_clear();
152 eprintln!("No 'packages' found in cargo metadata");
153 exit(1);
154 });
155
156 let current_dir = std::env::current_dir().unwrap_or_else(|e| {
158 spinner.finish_and_clear();
159 eprintln!("Failed to get current directory: {e}");
160 exit(1);
161 });
162
163 let package_name = packages
164 .iter()
165 .filter_map(|pkg| {
166 let manifest_path = pkg.get("manifest_path")?.as_str()?;
167 let pkg_dir = StdPath::new(manifest_path).parent()?;
168 if same_file_path(&pkg_dir.to_string_lossy(), ¤t_dir.to_string_lossy()) {
169 pkg.get("name")?.as_str().map(String::from)
170 } else {
171 None
172 }
173 })
174 .next()
175 .unwrap_or_else(|| {
176 spinner.finish_and_clear();
177 eprintln!("Could not find package for current directory");
178 exit(1);
179 });
180
181 spinner.finish_and_clear();
182 Ok((target_directory, package_name, current_dir))
183}
184
185pub fn build_project(package_root: &PathBuf) -> Result<(), io::Error> {
187 let spinner = indicatif::ProgressBar::new_spinner();
188 spinner.set_message("Building optimized WASI component...");
189 spinner.enable_steady_tick(std::time::Duration::from_millis(100));
190
191 if !package_root.join("src").join("lib.rs").exists() {
193 spinner.finish_and_clear();
194 eprintln!("Error: src/lib.rs is missing. This file is required for Faasta functions.");
195 eprintln!("Hint: Run 'cargo faasta new <n>' to create a new Faasta project.");
196 exit(1);
197 }
198
199 let status = std::process::Command::new("cargo")
201 .args(["build", "--release", "--target", "wasm32-wasip2"])
202 .current_dir(package_root)
203 .status()
204 .unwrap_or_else(|e| {
205 spinner.finish_and_clear();
206 eprintln!("Failed to run cargo build: {e}");
207 exit(1);
208 });
209
210 if !status.success() {
211 spinner.finish_and_clear();
212 eprintln!("Build failed");
213 exit(1);
214 }
215
216 spinner.finish_and_clear();
217 println!("✅ Build successful!");
218 Ok(())
219}
220
221pub async fn handle_run(port: u16) -> io::Result<()> {
223 let (target_directory, package_name, package_root) = get_project_info()?;
225
226 println!("Building project: {package_name}");
228 println!("Project root: {}", package_root.display());
229
230 build_project(&package_root)?;
232
233 let rust_compiled_name = package_name.replace('-', "_");
235 let wasm_filename = format!("{rust_compiled_name}.wasm");
236 let wasm_path = target_directory
237 .join("wasm32-wasip2")
238 .join("release")
239 .join(wasm_filename);
240
241 if !wasm_path.exists() {
243 eprintln!(
244 "Error: Could not find compiled WASM at: {}",
245 wasm_path.display()
246 );
247 eprintln!("Build seems to have failed or produced output in a different location.");
248 exit(1);
249 }
250
251 println!("Starting local server on port {port}...");
252 let status = std::process::Command::new("wasmtime")
253 .args(["serve", &wasm_path.to_string_lossy()])
254 .current_dir(&package_root)
255 .status()
256 .unwrap_or_else(|e| {
257 eprintln!("Failed to run wasmtime serve: {e}");
258 exit(1);
259 });
260
261 if !status.success() {
262 eprintln!("wasmtime serve exited with an error");
263 exit(1);
264 }
265
266 Ok(())
267}