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