chiral_client/api/
projects.rs

1use tonic::transport::Channel;
2use tonic::{Request, metadata::MetadataValue};
3use std::str::FromStr;
4use crate::api::client::chiral::chiral_client::ChiralClient;
5use crate::api::client::chiral::RequestUserCommunicate;
6
7
8pub async fn list_of_projects(client: &mut ChiralClient<Channel>, email: &str, token_auth: &str)->  Result<serde_json::Value, Box<dyn std::error::Error>>{
9    let end_point = "ListOfProjects";
10    let serialized = format!("{{\"{end_point}\": null}}");
11
12
13    let req_msg = RequestUserCommunicate{
14        serialized_request : serialized.clone(),
15    }; 
16    let mut request = Request::new(req_msg);
17
18    request.metadata_mut().insert("user_id", MetadataValue::from_str(email)?);
19    request.metadata_mut().insert("auth_token", MetadataValue::from_str(token_auth)?);
20
21    let response = client.user_communicate(request).await?.into_inner();
22    if !response.serialized_reply.trim().is_empty() {
23        let parsed: serde_json::Value = serde_json::from_str(&response.serialized_reply)?;
24        if let Some(result) = parsed.get(end_point) {
25            return Ok(result.clone());
26        } else {
27            return Err("Missing endpoint data in server response".into());
28        }
29    }
30
31    if !response.error.trim().is_empty() {
32        return Err(format!("Server error: {}", response.error).into());
33    }
34
35    Err("Unexpected empty response from server".into())
36}
37
38
39pub async fn list_of_example_projects(client: &mut ChiralClient<Channel>, email: &str, token_auth: &str)->  Result<serde_json::Value, Box<dyn std::error::Error>>{
40    let end_point = "ListOfExampleProjects";
41    let serialized = format!("{{\"{end_point}\": null}}");
42
43    let req_msg = RequestUserCommunicate{
44        serialized_request : serialized.clone(),
45    }; 
46    let mut request = Request::new(req_msg);
47
48    request.metadata_mut().insert("user_id", MetadataValue::from_str(email)?);
49    request.metadata_mut().insert("auth_token", MetadataValue::from_str(token_auth)?);
50
51    let response = client.user_communicate(request).await?.into_inner();
52    if !response.serialized_reply.trim().is_empty() {
53        let parsed: serde_json::Value = serde_json::from_str(&response.serialized_reply)?;
54        if let Some(result) = parsed.get(end_point) {
55            return Ok(result.clone());
56        } else {
57            return Err("Missing endpoint data in server response".into());
58        }
59    }
60
61    if !response.error.trim().is_empty() {
62        return Err(format!("Server error: {}", response.error).into());
63    }
64
65    Err("Unexpected empty response from server".into())
66}
67
68
69pub async fn list_of_project_files(client: &mut ChiralClient<Channel>, email: &str, token_auth: &str, project_name: &str)->  Result<serde_json::Value, Box<dyn std::error::Error>>{
70    let end_point = "ListOfProjectFiles";
71    let serialized = format!("{{\"{end_point}\": \"{project_name}\"}}");
72
73
74    let req_msg = RequestUserCommunicate{
75        serialized_request : serialized.clone(),
76    }; 
77    let mut request = Request::new(req_msg);
78
79    request.metadata_mut().insert("user_id", MetadataValue::from_str(email)?);
80    request.metadata_mut().insert("auth_token", MetadataValue::from_str(token_auth)?);
81
82    let response = client.user_communicate(request).await?.into_inner();
83    if !response.serialized_reply.trim().is_empty() {
84        let parsed: serde_json::Value = serde_json::from_str(&response.serialized_reply)?;
85        if let Some(result) = parsed.get(end_point) {
86            return Ok(result.clone());
87        } else {
88            return Err("Missing endpoint data in server response".into());
89        }
90    }
91
92    if !response.error.trim().is_empty() {
93        return Err(format!("Server error: {}", response.error).into());
94    }
95
96    Err("Unexpected empty response from server".into())
97}
98
99
100pub async fn import_example_project(client: &mut ChiralClient<Channel>, email: &str, token_auth: &str, project_name: &str)->  Result<serde_json::Value, Box<dyn std::error::Error>>{
101    let end_point = "ImportExampleProject";
102    let serialized = format!("{{\"{end_point}\": \"{project_name}\"}}");
103
104
105    let req_msg = RequestUserCommunicate{
106        serialized_request : serialized.clone(),
107    }; 
108    let mut request = Request::new(req_msg);
109
110    request.metadata_mut().insert("user_id", MetadataValue::from_str(email)?);
111    request.metadata_mut().insert("auth_token", MetadataValue::from_str(token_auth)?);
112
113    let response = client.user_communicate(request).await?.into_inner();
114    if !response.serialized_reply.trim().is_empty() {
115        let parsed: serde_json::Value = serde_json::from_str(&response.serialized_reply)?;
116        if let Some(result) = parsed.get(end_point) {
117            return Ok(result.clone());
118        } else {
119            return Err("Missing endpoint data in server response".into());
120        }
121    }
122
123    if !response.error.trim().is_empty() {
124        return Err(format!("Server error: {}", response.error).into());
125    }
126
127    Err("Unexpected empty response from server".into())
128}
129
130
131pub async fn get_project_files(client: &mut ChiralClient<Channel>, email: &str, token_auth: &str, project_name: &str, file_name: &str) -> Result<serde_json::Value, Box<dyn std::error::Error>> {    let _end_point = "GetJobs";
132    let end_point = "GetProjectFile";
133    let serialized = format!("{{\"{end_point}\": [\"{project_name}\", \"{file_name}\"]}}");
134
135
136    let req_msg = RequestUserCommunicate {
137        serialized_request: serialized.clone(),
138    };
139
140    println!("Sending payload: {serialized}" ); 
141
142    let mut request = Request::new(req_msg);
143    request.metadata_mut().insert("user_id", MetadataValue::from_str(email)?);
144    request.metadata_mut().insert("auth_token", MetadataValue::from_str(token_auth)?);
145
146    let response = client.user_communicate(request).await?.into_inner();
147    println!("Reply JSON: {}", response.serialized_reply);
148
149
150    if !response.serialized_reply.trim().is_empty() {
151        let parsed: serde_json::Value = serde_json::from_str(&response.serialized_reply)?;
152        if let Some(result) = parsed.get(end_point) {
153            return Ok(result.clone());
154        } else {
155            return Err("Missing endpoint data in server response".into());
156        }
157    }
158
159    if !response.error.trim().is_empty() {
160        return Err(format!("Server error: {}", response.error).into());
161    }
162
163    Err("Unexpected empty response from server".into())
164
165}
166
167#[cfg(test)]
168mod tests{
169    use super::*;
170    use crate::api::create_client;
171    use dotenvy;
172    use rand::seq::SliceRandom;
173    use rand::thread_rng;
174
175    #[tokio::test]
176    async fn test_list_of_projects() {
177        dotenvy::from_filename(".env.staging").ok();
178        let url = std::env::var("CHIRAL_STAGING_API_URL").expect("Missing env").trim() .to_string();
179        let email = std::env::var("TEST_EMAIL").expect("Missing env").trim() .to_string();
180        let token_auth = std::env::var("TEST_TOKEN_AUTH").expect("Missing env").trim() .to_string();
181        let mut client = create_client(&url).await.expect("Failed to create API client.");
182
183        let projects = list_of_projects(&mut client, &email, &token_auth).await.expect("List of projects failed");
184
185        assert!(projects.is_array(),"Expected JSON array but got: {}",projects);
186
187        let projects_array = projects.as_array().expect("Projects response is not a valid JSON array");
188
189        for (i, project) in projects_array.iter().enumerate() {
190            match project {
191                serde_json::Value::Object(obj) => {
192                    assert!(obj.get("id").is_some(),"Project at index {} missing 'id': {}",i,project);
193                    assert!(obj.get("name").is_some(),"Project at index {} missing 'name': {}",i,project);
194                }
195                serde_json::Value::String(s) => {
196                    assert!(!s.trim().is_empty(),"Project at index {} is an empty string",i);
197                }
198                _ => {
199                    panic!("Project at index {} is neither object nor string: {}",i, project);
200                }
201            }
202        }
203    }
204
205    #[tokio::test]
206    async fn test_list_of_example_projects(){
207        dotenvy::from_filename(".env.staging").ok();
208        let url = std::env::var("CHIRAL_STAGING_API_URL").expect("Missing env").trim() .to_string();
209        let email = std::env::var("TEST_EMAIL").expect("Missing env").trim() .to_string();
210        let token_auth = std::env::var("TEST_TOKEN_AUTH").expect("Missing env").trim() .to_string();
211
212        let mut client = create_client(&url).await.expect("Failed to create API client.");
213
214        let projects = list_of_example_projects(&mut client, &email, &token_auth).await.expect("List of projects failed");
215
216        assert!(projects.is_array(),"Expected JSON array but got: {}",projects);
217        let projects_array = projects.as_array().expect("Projects response is not a valid JSON array");
218        for (i, project) in projects_array.iter().enumerate() {
219            match project {
220                serde_json::Value::Object(obj) => {
221                    assert!(obj.get("id").is_some(),"Project at index {} missing 'id': {}",i,project);
222                    assert!(obj.get("name").is_some(),"Project at index {} missing 'name': {}",i,project);
223                }
224                serde_json::Value::String(s) => {
225                    assert!(!s.trim().is_empty(),"Project at index {} is an empty string",i);
226                }
227                _ => {
228                    panic!("Project at index {} is neither object nor string: {}",i, project);
229                }
230            }
231        }
232
233    }
234
235    #[tokio::test]
236    async fn test_list_of_project_files(){
237        dotenvy::from_filename(".env.staging").ok();
238        let url = std::env::var("CHIRAL_STAGING_API_URL").expect("Missing env").trim() .to_string();
239        let email = std::env::var("TEST_EMAIL").expect("Missing env").trim() .to_string();
240        let token_auth = std::env::var("TEST_TOKEN_AUTH").expect("Missing env").trim() .to_string();
241
242        let mut client = create_client(&url).await.expect("Failed to create API client.");
243        let example_projects = list_of_example_projects(&mut client, &email, &token_auth).await.expect("Failed to get example projects");
244        let project_name_opt = example_projects.as_array().and_then(|arr| {
245        arr.iter().find_map(|p| match p {
246                serde_json::Value::String(name) => Some(name.clone()),
247                serde_json::Value::Object(obj) => obj.get("name").and_then(|v| v.as_str()).map(String::from),
248                _ => None,
249            })
250        });
251
252        let project_name = project_name_opt.expect("No valid project name found in example projects");
253
254        println!("Using example project: {}", project_name);
255
256        let files = list_of_project_files(&mut client, &email, &token_auth, &project_name).await.expect("Failed to get project files");
257
258        assert!(files.is_array(),"Expected project files to be a JSON array, got: {}",files);
259
260        let file_array = files.as_array().unwrap();
261        println!("Found {} file(s) in project '{}'", file_array.len(), project_name);
262        
263        for (i, file) in file_array.iter().enumerate() {
264            assert!(file.is_string(),"Expected file entry at index {} to be a string, got: {}",i,file);
265        }
266    }
267
268    #[tokio::test]
269    async fn test_import_example_project() {
270        dotenvy::from_filename(".env.staging").ok();
271        let url = std::env::var("CHIRAL_STAGING_API_URL").expect("Missing env").trim() .to_string();
272        let email = std::env::var("TEST_EMAIL").expect("Missing env").trim() .to_string();
273        let token_auth = std::env::var("TEST_TOKEN_AUTH").expect("Missing env").trim() .to_string();
274
275        let mut client = create_client(&url).await.expect("Failed to create API client");
276
277        let existing_projects = list_of_projects(&mut client, &email, &token_auth).await.expect("Failed to fetch list of projects");
278
279        println!("Existing projects: {}", existing_projects);
280
281        let example_projects = list_of_example_projects(&mut client, &email, &token_auth).await.expect("Failed to fetch list of example projects");
282
283        let example_project_name = example_projects
284            .as_array()
285            .and_then(|arr| {
286                arr.iter().find_map(|p| match p {
287                    serde_json::Value::String(name) => Some(name.clone()),
288                    serde_json::Value::Object(obj) => obj.get("name").and_then(|v| v.as_str()).map(String::from),
289                    _ => None,
290                })
291            }).expect("No valid example project name found");
292
293        println!("Trying to import: {}", example_project_name);
294
295        let already_exists = existing_projects
296            .as_array()
297            .map(|arr| {
298                arr.iter().any(|p| match p {
299                    serde_json::Value::String(name) => name == &example_project_name,
300                    serde_json::Value::Object(obj) => obj
301                        .get("name")
302                        .and_then(|v| v.as_str())
303                        .map(|s| s == example_project_name)
304                        .unwrap_or(false),
305                    _ => false,
306                })
307            })
308            .unwrap_or(false);
309
310        if already_exists {
311            println!("Project '{}' already exists — skipping import.", example_project_name);
312        } else {
313            let result = import_example_project(&mut client, &email, &token_auth, &example_project_name).await.expect("Failed to import example project");
314
315            println!("Import result: {}", result);
316
317            assert!(result.is_object() || result.is_string(),"Expected object or string in response, got: {}",result);
318        }
319    }
320    
321    #[tokio::test]
322    async fn test_get_project_files() {
323        dotenvy::from_filename(".env.staging").ok();
324        let url = std::env::var("CHIRAL_STAGING_API_URL").expect("Missing env").trim() .to_string();
325        let email = std::env::var("TEST_EMAIL").expect("Missing env").trim() .to_string();
326        let token_auth = std::env::var("TEST_TOKEN_AUTH").expect("Missing env").trim() .to_string();
327
328        let mut client = create_client(&url).await.expect("Failed to create API client");
329
330        let projects = list_of_projects(&mut client, &email, &token_auth)
331            .await
332            .expect("Failed to fetch list of projects");
333
334        let project_name = projects
335            .as_array()
336            .and_then(|arr| {
337                let mut rng = thread_rng();
338                let valid_names: Vec<String> = arr
339                    .iter()
340                    .filter_map(|p| match p {
341                        serde_json::Value::Object(obj) => obj.get("name").and_then(|v| v.as_str()).map(String::from),
342                        serde_json::Value::String(name) => Some(name.clone()),
343                        _ => None,
344                    })
345                    .filter(|name| !name.starts_with("gromacs_bench"))
346                    .collect();
347
348                valid_names.choose(&mut rng).cloned()
349            })
350            .expect("No valid project name found (after filtering broken gromacs_bench projects)");
351
352
353        println!("Using project: {project_name}");
354
355        let files = list_of_project_files(&mut client, &email, &token_auth, &project_name)
356            .await
357            .expect("Failed to list project files");
358
359        let file_array = files.as_array().expect("File list is not an array");
360
361        println!("Found {} file(s) in project.", file_array.len());
362        let mut at_least_one_success = false;
363
364        for file in file_array {
365            let file_name = match file {
366                serde_json::Value::Object(obj) => obj.get("name").and_then(|v| v.as_str()).map(String::from),
367                serde_json::Value::String(name) => Some(name.clone()),
368                _ => None,
369            };
370
371            let Some(file_name) = file_name else {
372                println!("Skipping invalid file entry: {file:?}");
373                continue;
374            };
375
376            println!("Trying file: {}", file_name);
377
378            match get_project_files(&mut client, &email, &token_auth, &project_name, &file_name).await {
379                Ok(file_data) => {
380                    println!("Successfully fetched file: {}\nData: {}", file_name, file_data);
381                    assert!(
382                        file_data.is_string() || file_data.is_object(),
383                        "Expected JSON string or object for file '{}', got: {}",
384                        file_name,
385                        file_data
386                    );
387                    at_least_one_success = true;
388                }
389                Err(e) => {
390                    println!("Error fetching '{}': {}", file_name, e);
391                }
392            }
393        }
394
395        assert!(
396            at_least_one_success,
397            "No project file could be successfully fetched and validated."
398        );
399
400    }
401
402}