From fd2fc28feaa74eb1c1ca9d8b4f1778cc467ce335 Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 24 Jul 2024 09:14:23 +0200 Subject: [PATCH 01/12] correction code + test unitaire --- src/carbone.rs | 67 ++++++++++++++++++++---- src/errors/mod.rs | 5 ++ src/types.rs | 2 + tests/carbone_test.rs | 109 +++++++++++++++++++++++++++++++++++---- tests/data/template2.odt | Bin 0 -> 61768 bytes tests/data/~$mplate2.odt | Bin 0 -> 162 bytes 6 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 tests/data/template2.odt create mode 100644 tests/data/~$mplate2.odt diff --git a/src/carbone.rs b/src/carbone.rs index 33bfb58..cff0122 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -10,6 +10,7 @@ use reqwest::Client; use reqwest::ClientBuilder; use reqwest::StatusCode; + use crate::carbone_response::APIResponse; use crate::config::Config; use crate::errors::*; @@ -18,6 +19,7 @@ use crate::template::*; use crate::types::{ApiJsonToken, JsonData}; use crate::types::Result; +use std::collections::HashMap; #[derive(Debug, Clone)] pub struct Carbone<'a> { @@ -210,21 +212,39 @@ impl<'a> Carbone<'a> { ) -> Result { let template_id_generated = TemplateId::from_bytes(template_data.to_owned(), payload)?; - - let result = self.download_template(&template_id_generated).await; - - let template_id = if result.is_err() { - self.upload_template(template_name.as_str(), template_data, salt).await? - } else { - template_id_generated + let mut template_id = template_id_generated; + let mut render_id = None; + + match self.render_data(template_id, json_data.clone()).await { + Ok(id) => { + render_id = Some(id); + } + Err(e) => match e { + CarboneError::HttpError { status_code, error_message } => { + println!("{:?}", status_code); + if status_code == reqwest::StatusCode::NOT_FOUND { + println!("rrrr"); + template_id = self.upload_template(template_name.as_str(), template_data, salt).await?; + render_id = Some(self.render_data(template_id, json_data).await?); + } else { + return Err(CarboneError::HttpError { status_code, error_message }); + } + }, + CarboneError::Error(error_message) => { + return Err(CarboneError::Error(error_message)); + }, + _ => { + return Err(e); + } + }, }; - - let render_id = self.render_data(template_id, json_data).await?; - let report_content = self.get_report(&render_id).await?; - + + let report_content = self.get_report(&render_id.unwrap()).await?; + Ok(report_content) } + /// Get a new report. /// /// @@ -266,6 +286,22 @@ impl<'a> Carbone<'a> { let response = self.http_client.get(url).send().await?; + // let mut report_name = None; + + // if let Some(content_disposition) = response.headers().get("content-disposition") { + // if let Ok(disposition) = content_disposition.to_str() { + // let split_content_disposition: Vec<&str> = disposition.split('=').collect(); + + // if split_content_disposition.len() == 2 { + // let mut name = split_content_disposition[1].to_string(); + // if name.starts_with('"') && name.ends_with('"') { + // name = name[1..name.len() - 1].to_string(); + // } + // report_name = Some(name); + // } + // } + // } + if response.status() == StatusCode::OK { Ok(response.bytes().await?) } else { @@ -394,6 +430,15 @@ impl<'a> Carbone<'a> { .send() .await?; + if !response.status().is_success() { + let status_code = response.status(); + let json = response.json::().await?; + return Err(CarboneError::HttpError { + status_code, + error_message: json.error.unwrap_or_else(|| "Unknown error".to_string()), + }); + } + let json = response.json::().await?; if json.success { diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 8638d7c..1c89b85 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -29,6 +29,11 @@ pub enum CarboneError { RequestBodyNotWellFormedJsonError, #[error("Carbone SDK {0:?} ParseError {1:?}")] ParseError(String, String), + #[error("Carbone SDK HttpError: {status_code:?} - {error_message}")] + HttpError { + status_code: reqwest::StatusCode, + error_message: String, + }, } impl From for CarboneError { diff --git a/src/types.rs b/src/types.rs index b2070bb..1de527f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,6 +4,8 @@ use crate::errors::CarboneError; pub type Result = std::result::Result; +// pub type Result<(T,U)> = std::result::Result<(T,U), CarboneError>; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ApiJsonToken(String); diff --git a/tests/carbone_test.rs b/tests/carbone_test.rs index 8e40d88..bf14bc0 100644 --- a/tests/carbone_test.rs +++ b/tests/carbone_test.rs @@ -8,7 +8,7 @@ use carbone_sdk_rust::carbone_response::APIResponse; use carbone_sdk_rust::errors::CarboneError; use carbone_sdk_rust::render::*; use carbone_sdk_rust::types::JsonData; - +use reqwest::StatusCode; mod helper; use helper::Helper; @@ -309,15 +309,106 @@ mod tests { let expected_content = fs::read(file_path)?; - let mock_template_response = server.mock(|when, then| { + let mock_render_response = server.mock(|when, then| { + when.method("POST") + .path(format!("/render/{}", template_id.as_str())); + then.status(200).json_body(json!({ + "success": true, + "data": { + "renderId": render_id.as_str(), + "inputFileExtension": "odt" + } + })); + }); + + let mock_get_report_response = server.mock(|when, then| { when.method("GET") - .path(format!("/template/{}", template_id.as_str())); - then.status(200).body_from_file(template_file.path_as_str()); + .path(format!("/render/{}", render_id.as_str())); + then.status(200).body(&expected_content); }); - let mock_render_response = server.mock(|when, then| { + let result = carbone + .generate_report(template_name, template_data, json_data, None, None) + .await + .unwrap(); + + mock_render_response.assert(); + mock_get_report_response.assert(); + + assert_eq!(result, expected_content); + + Ok(()) + } + + #[tokio::test] + async fn test_generate_report_unupload_template() -> Result<(), CarboneError> + { + // Start a lightweight mock server. + let server = MockServer::start(); + + let helper = Helper::new(); + + let config = helper.create_config_for_mock_server(Some(&server))?; + let api_token = helper.create_api_token()?; + + let carbone = Carbone::new(&config, &api_token)?; + + let report_data = fs::read_to_string("tests/data/report_data.json")?; + + let template_name = "template2.odt".to_string(); + let template_path = format!("tests/data/{}", template_name); + let template_data = fs::read(template_path.to_owned())?; + + let template_file = TemplateFile::new(template_path, Some(template_data.to_owned()))?; + let template_id = template_file.generate_id(None)?; + + let template_id_expected = TemplateId::new( + "0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string(), + )?; + + let data = APIResponseData { + template_id: Some(template_id_expected.clone()), + render_id: None, + template_file_extension: None, + }; + + let body = APIResponse { + success: true, + data: Some(data), + error: None, + code: None, + }; + + let json_data = JsonData::new(report_data)?; + + let render_id_value = "MTAuMjAuMjEuNDAgICAgBY4OM11wQg11ekv6_R0n0wcmVwb3J0.pdf".to_string(); + let render_id = &RenderId::new(&render_id_value)?; + + let file_path = "tests/data/report.pdf"; + + let expected_content = fs::read(file_path)?; + + let mock_render_response_false = server.mock(|when, then| { when.method("POST") .path(format!("/render/{}", template_id.as_str())); + then.status(404) + .json_body(json!({ + "success": false, + "error": "template no found" + })); + }); + + let mock_upload_template = server.mock(|when, then| { + when.method("POST") + .path(format!("/template")); + then.status(200) + .header("content-type", "application/json") + .json_body_obj(&body); + }); + + let mock_render_response = server.mock(|when, then| { + when.method("POST") + .path(format!("/render/{}", template_id_expected.as_str())); then.status(200).json_body(json!({ "success": true, "data": { @@ -338,7 +429,6 @@ mod tests { .await .unwrap(); - mock_template_response.assert(); mock_render_response.assert(); mock_get_report_response.assert(); @@ -481,9 +571,10 @@ mod tests { let json_data = JsonData::new(json_data)?; let result = carbone.render_data(template_id, json_data).await; - let expected_error = CarboneError::Error( - "Invalid or undefined TemplateId or RenderId in the URL".to_string(), - ); + let expected_error = CarboneError::HttpError { + status_code: StatusCode::BAD_REQUEST, + error_message: "Invalid or undefined TemplateId or RenderId in the URL".to_string(), + }; mock_server.assert(); assert!(result.is_err()); diff --git a/tests/data/template2.odt b/tests/data/template2.odt new file mode 100644 index 0000000000000000000000000000000000000000..0ae11433f644d0005bc37bcc5d31a2d7e8e5e927 GIT binary patch literal 61768 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 literal 0 HcmV?d00001 diff --git a/tests/data/~$mplate2.odt b/tests/data/~$mplate2.odt new file mode 100644 index 0000000000000000000000000000000000000000..105dfb757b9c6f685254a013fb560e67b472d353 GIT binary patch literal 162 zcmWd$EKSWT$;?ymNmX#mFG?*g$xJO`AOu7h0vSpfQW^3XN*FSMqymEvkf*@l#*oiY O1Y{M1#Z!T7$^ifs!VftB literal 0 HcmV?d00001 From 1cd8fb849ad3495ba01e0c09ed6c11ad128b6b36 Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:07:38 +0200 Subject: [PATCH 02/12] get status + test unitaire + doc --- README.md | 318 ++++++++++++++++++++++++++++++++++++++++-- src/carbone.rs | 25 +++- tests/carbone_test.rs | 27 ++++ 3 files changed, 351 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ab98d83..d242e99 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ sequenceDiagram # Installation -TODO +carbone-sdk-rust = "x.x" # Render a new report @@ -45,35 +45,327 @@ use carbone_sdk_rust::errors::CarboneError; #[tokio::main] async fn main() -> Result<(), CarboneError> { - let token = match env::var("CARBONE_TOKEN") { - Ok(v) => v, - Err(e) => panic!("{}", e.to_string()) - }; - + let token = "Token"; + let config: Config = Default::default(); let api_token = ApiJsonToken::new(token)?; let json_data_value = String::from(r#" - "data" : { - "firstname" : "John", - "lastname" : "Wick" - }, - "convertTo" : "odt" + { + "data" : { + "firstname" : "John", + "lastname" : "Wick" + }, + "convertTo" : "odt" + } "#); let json_data = JsonData::new(json_data_value)?; - let template_id = TemplateId::new("0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string())?; + let template_id = TemplateId::new("YourTemplateId".to_string())?; let carbone = Carbone::new(&config, &api_token)?; - let _report_content = carbone.generate_report_with_template_id(template_id, json_data).await?; + let report_content = match carbone.generate_report_with_template_id(template_id.clone(), json_data.clone()).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; + + let mut output_file = File::create("report.pdf").expect("Failed to create file"); + + if let Err(e) = output_file.write_all(&report_content) { + eprintln!("Failed to write to file: {:?}", e); + } Ok(()) } ``` +## Rust SDK API + +### Table of content + +- SDK functions: + - [CarboneSDK Constructor](#carbone-sdk-constructor) + - [Generate and Download a Document](#generate-and-download-document) + - [Generate a Document Only](#generate-document-only) + - [Download a Document Only](#download-document-only) + - [Add a Template](#add-template) + - [Delete a Template](#delete-template) + - [Get a Template](#get-template) + - [Set Carbone URL](#set-carbone-url) + - [Get API status](#get-api-status) + - [Set API Version](#set-api-version) +- [Build commands](#build-commands) +- [Test commands](#test-commands) +- [Project history](#-history) +- [Contributing](#-contributing) + +### Carbone SDK Constructor + +**Definition** + +```rust +let config: Config; +``` + +**Example** + +Example of a new SDK instance for **Carbone Cloud**: +Get your API key on your Carbone account: https://account.carbone.io/. +```rust +// For Carbone Cloud, provide your API Access Token as first argument: +let token = "Token"; +let config: Config = Default::default(); +let carbone = Carbone::new(&config, &api_token)?; +``` + +Example of a new SDK instance for **Carbone On-premise** or **Carbone On-AWS**: +```rust +let token = match env::var("CARBONE_TOKEN") { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +// Define the URL of your Carbone On-premise Server or AWS EC2 URL: +let config: Config = Config::new("ON_PREMISE_URL".to_string(), "api_time_out_in_sec_in_u64", ApiVersion::new("Version".to_string()).expect("REASON")).expect("REASON"); +let carbone = Carbone::new(&config, &api_token)?; +``` + +### Download Document + +```rust +pub async fn generate_report( &self, template_name: String, template_data: Vec, json_data: JsonData, payload: Option<&str>, salt: Option<&str>); +``` + +or + +```rust +pub async fn pub async fn generate_report_with_template_id( &self, template_id: TemplateId, json_data: JsonData); +``` + +The render function generates a document using a specified template and data. It takes two parameters: +* json_data: A stringified JSON containing the data to populate the template. +* template_data: The content of the file in `Vec`. +* template_data: A template ID. + +**Function Behavior** + +2. Template ID as Second Argument: + - If a template ID is provided, the function calls [render_data](#generate-document-only) to generate the report. It then calls [get_report](#download-document-only) to retrieve the generated report. + - If the template ID does not exist, an error is returned. + +**Example** + +```rust +let file_name = "name_file.extention"; +let file_path = format!("your/path/{}", file_name); +let filte_content = fs::read(file_path)?; + +let json_data_value = String::from(r#" + { + "data" : { + "firstname" : "John", + "lastname" : "Wick" + }, + "convertTo" : "odt" + } + "#); + +let json_data = JsonData::new(json_data_value)?; + +let content = match generate_report(template_name.to_string(), filte_content, json_data, None, None).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; + +``` + +or + +```rust +let template_id = TemplateId::new("template_id".to_string())?; +let json_data = String::from(r#" + { + "data" : { + "firstname" : "John", + "lastname" : "Wick" + }, + "convertTo" : "odt" + } + "#); + +let json_data = JsonData::new(json_data_value)?; + +let content = match generate_report_with_template_id( template_id, filte_content, json_data).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +``` + + +### upload Document + +```rust +pub async fn upload_template(&self,file_name: &str,file_content: Vec,salt: Option<&str>); +``` + +Add a template as file-content `Vec` and the function return the template ID as `String`. + +**Example** + +```rust + +let template_name = "template.odt".to_string(); +let template_path = format!("src/{}", template_name); +let template_data = fs::read(template_path.to_owned())?; + +let template_id = match carbone.upload_template("report", template_data, None).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +``` + +### delete Document + +```rust +pub async fn delete_template(&self, template_id: TemplateId); +``` + +Delete a template by providing a template ID as `TemplateId`, and it returns whether the request succeeded as a `Boolean`. + +**Example** + +```rust +let template_id = TemplateId::new("template_id".to_string())?; + +let boolean = match delete_template(template_id).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +``` + +### generate Document only + +The generate_report function takes a template ID as `String`, and the JSON data-set as `String`. +It return a `renderId`, you can pass this `renderId` at [get_report](#download-document-only) for download the document. + +```rust +pub async fn render_data( &self, template_id: TemplateId, json_data: JsonData); +``` + +**Example** + +```rust + +let template_id = TemplateId::new("template_id".to_string())?; + +let json_data = String::from(r#" + { + "data" : { + "firstname" : "John", + "lastname" : "Wick" + }, + "convertTo" : "odt" + } + "#); + +let json_data = JsonData::new(json_data_value)?; + +let render_id = match render_data(template_id, json_data).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; + + +``` + +### Download Document Only + +**Definition** +```rust +pub async fn get_report(&self, render_id: &RenderId); +``` + +**Example** + +```rust + +let render_id = RenderId::new("render_id".to_string())?; + +let content = match get_report(render_id).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; + +``` + +## Get Template + +**Definition** + +```rust +pub async fn download_template(&self, template_id: &TemplateId); +``` + +Provide a template ID as `String` and it returns the file as `Bytes`. + +**Example** + +```rust +let template_id = TemplateId::new("template_id".to_string())?; + +let content = match download_template(template_id).await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +``` +### Set API + +**Definition** + +```rust +pub fn new(api_url: String, api_timeout: u64, api_version: ApiVersion) +``` + +Set the API URL for Carbone On-premise or Carbone On-AWS. + +Specify the version of the Carbone CLoud API you want to request as second argument of the constructor. +By default, all requested are made to the Carbone API version `4`. + +**Example** + +```rust +let config: Config = Config::new("ON_PREMISE_URL".to_string(), "api_time_out_in_sec_in_u64", ApiVersion::new("Version".to_string()).expect("REASON")).expect("REASON"); +``` +## Build commands + +At the root of the SDK repository run: +```sh +cargo build +``` + +In another Java project, you can load the local build of the SDK, in the pom.xml: +```xml + +carbone-sdk-rust = {path = "your/local/path"} +``` +Finally, compile your Java project with the SDK: +```sh +cargo run +``` + +## Test commands + +Execute unit tests: +```sh +cargo test +``` +Execute unit tests with coverage: +```sh +cargo tarpaulin +``` + # References [Carbone.io](https://carbone.io) a report generator. diff --git a/src/carbone.rs b/src/carbone.rs index cff0122..30f76c9 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -9,6 +9,7 @@ use reqwest::multipart; use reqwest::Client; use reqwest::ClientBuilder; use reqwest::StatusCode; +use reqwest::Response; use crate::carbone_response::APIResponse; @@ -221,9 +222,7 @@ impl<'a> Carbone<'a> { } Err(e) => match e { CarboneError::HttpError { status_code, error_message } => { - println!("{:?}", status_code); if status_code == reqwest::StatusCode::NOT_FOUND { - println!("rrrr"); template_id = self.upload_template(template_name.as_str(), template_data, salt).await?; render_id = Some(self.render_data(template_id, json_data).await?); } else { @@ -236,7 +235,7 @@ impl<'a> Carbone<'a> { _ => { return Err(e); } - }, + } }; let report_content = self.get_report(&render_id.unwrap()).await?; @@ -509,7 +508,6 @@ impl<'a> Carbone<'a> { None => return Err(CarboneError::Error("Failed to fetch file name".to_string())), }; - println!("file_name = {}", file_name); let ext = file_path .extension() @@ -524,8 +522,6 @@ impl<'a> Carbone<'a> { let url = format!("{}/template", self.config.api_url); - println!("url = {}", url); - println!("form = {:?}", form); let response = self.http_client.post(url).multipart(form).send().await?; @@ -537,4 +533,21 @@ impl<'a> Carbone<'a> { Err(CarboneError::Error(json.error.unwrap())) } } + + + pub async fn get_status(&self) -> Result + { + let url = format!("{}/status", self.config.api_url); + + let response = self.http_client.get(url).send().await?; + println!("{:?}",response.headers()); + + if response.status() == StatusCode::OK { + let body = response.text().await?; + Ok(body) + } else { + let json = response.json::().await?; + Err(CarboneError::Error(json.error.unwrap())) + } + } } diff --git a/tests/carbone_test.rs b/tests/carbone_test.rs index bf14bc0..17c59c1 100644 --- a/tests/carbone_test.rs +++ b/tests/carbone_test.rs @@ -762,4 +762,31 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_get_status() -> Result<(), CarboneError> { + let body : String = "{\"success\":true,\"code\":200,\"message\":\"OK\",\"version\":\"4.22.11\"}".to_string(); + let server = MockServer::start(); + + // Create a mock on the server. + let mock_server = server.mock(|when, then| { + when.method("GET") + .path(format!("/status")); + then.status(200).body(body.clone()); + }); + + let helper = Helper::new(); + let config = helper.create_config_for_mock_server(Some(&server))?; + + let api_token = helper.create_api_token()?; + + let carbone = Carbone::new(&config, &api_token)?; + + let response = carbone.get_status().await.unwrap(); + + mock_server.assert(); + + assert_eq!(body, response); + Ok(()) + } } From 5effdb9addd6085f37c6663ee539e8f2c7565308 Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:12:04 +0200 Subject: [PATCH 03/12] =?UTF-8?q?suppr=C3=A9tion=20des=20fichiers=20DS=5FS?= =?UTF-8?q?tore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.DS_Store | Bin 6148 -> 0 bytes tests/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.DS_Store delete mode 100644 tests/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 82d0ec32b86438c755ce65b5287dc1a2129499b1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK!AiqG5S?wSO({YT3Oz1(E!f&p5HF$DA26Z^m718Mp)p&U)*ebBXZ<0+#P4xt zcO#bCgBOu91G8^-b|%ZbgqfCuxYVJ+_ z=`8K}lN*{{DisI0?FU!Ua6YK*pQ&W(N6B!a6QXd4A-C625~_Jm&5|(Hxt>{YDo$lk zt1lMqR;M9bt?sfR7bnNfhU~Pv%VotmI6OMN7>wfQM76_AKV`hQ@~KUc5;X ztK<RVJL7Zg&cfbMgx($fQip?Z4f4ng zFaxU$6wS0o{r`OP{eLxyXUqUIuvH9*Qs3+Muq4}C*NUTFD^c%JNhmHe_?3c&Zp9c& et+S5Z-O0O({YS3Oz1(E!e6p6fYsx7cim+m70*E!I&*gY7V84v%Zi|;`2DO zyAg}^Dq?3~_nY6{><8H&#u)eJQI9c)F=j(UbgvE7Ofn+JF~TC7hKUTpelxMZ z4*2aBi&(&N7JU8waGJzf-tE5gTHV^(ZrBaGW#9S_vhedDpJ!e$yGH9m$|S7xAiRtx z#n{13t~;%EXXH&<~Q$-8SivhOpX+({XK5nSd+=5HRURQRKnxHA#K7h< zV9o@)wYe0~%83DD;0Fe9e-O|RU4x}YwRJ#;*Jq4dh$x`rTLMuSbPbjoAp*j6DWERp z=83^|IrxRia}Aanbvff|W*EoJTs>a6njQQ?r8Dkoq@EZc238rUYtzB={|tVawU7MO z60(Q^V&I=Kz^#!#@?cTsZ2h)8JZlBCJ7_4FSE2#}`pP8$4BSWBDyZWEb;xrKmKt#s S^s90}x(Fyjs3Qh`fq@S=8A>$( From dd7057adeeefc097e794d0ab8b5019e6a24968be Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 31 Jul 2024 10:44:34 +0200 Subject: [PATCH 04/12] ajout du change log + correction doc + code clean --- .gitignore | 18 ++- CHANGELOG.md | 16 +++ README.md | 63 ++++++++--- src/carbone.rs | 289 +------------------------------------------------ src/config.rs | 58 +--------- 5 files changed, 84 insertions(+), 360 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.gitignore b/.gitignore index 8e55166..1433f52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,21 @@ **/target **/build **/.vscode -Cargo.lock + .DS_Store + +### Rust ### +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..700c9a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## 2.0.0 +- Released on 2024//: The package was originally made by Pascal CHENEVAS from . The Carbone team is now maintaining the SDK. This version brings all missing functions to interact with the Carbone API. +- Added function `getStatus`: It return the current status and the version of the API as `String`. +- Added error `HttpError`: It return the status code and a error message. +- Modified for the `new` constructor: the `api-token` has become optional. +- Modified for the `generate_report`: Optimization of api calls when there is error 404. +- Modified for the `render_data`: When there is an error in the request, the function returns the status code and an error message. +- Added units tests. + +### v1.0.0 +- Released on \ No newline at end of file diff --git a/README.md b/README.md index d242e99..5eba1d8 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ async fn main() -> Result<(), CarboneError> { let config: Config = Default::default(); - let api_token = ApiJsonToken::new(token)?; + let api_token = ApiJsonToken::new(token.to_string())?; let json_data_value = String::from(r#" { @@ -67,7 +67,7 @@ async fn main() -> Result<(), CarboneError> { let carbone = Carbone::new(&config, &api_token)?; - let report_content = match carbone.generate_report_with_template_id(template_id.clone(), json_data.clone()).await { + let report_content = match carbone.generate_report_with_template_id(template_id, json_data).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -99,7 +99,6 @@ async fn main() -> Result<(), CarboneError> { - [Set API Version](#set-api-version) - [Build commands](#build-commands) - [Test commands](#test-commands) -- [Project history](#-history) - [Contributing](#-contributing) ### Carbone SDK Constructor @@ -128,10 +127,22 @@ let token = match env::var("CARBONE_TOKEN") { Err(e) => panic!("{}", e.to_string()) }; // Define the URL of your Carbone On-premise Server or AWS EC2 URL: -let config: Config = Config::new("ON_PREMISE_URL".to_string(), "api_time_out_in_sec_in_u64", ApiVersion::new("Version".to_string()).expect("REASON")).expect("REASON"); +let config: Config = Config::new("ON_PREMISE_URL".to_string(), "api_time_out_in_sec_in_u64", ApiVersion::new("4".to_string()).expect("REASON")).expect("REASON"); let carbone = Carbone::new(&config, &api_token)?; ``` +Constructor to create a new instance of CarboneSDK. +The access token can be pass as an argument or by the environment variable "CARBONE_TOKEN". +Get your API key on your Carbone account: https://account.carbone.io/. +To set a new environment variable, use the command: +```bash +$ export CARBONE_TOKEN=your-secret-token +``` +Check if it is set by running: +```bash +$ printenv | grep "CARBONE_TOKEN" +``` + ### Download Document ```rust @@ -174,7 +185,7 @@ let json_data_value = String::from(r#" let json_data = JsonData::new(json_data_value)?; -let content = match generate_report(template_name.to_string(), filte_content, json_data, None, None).await { +let content = match carbone.generate_report(template_name.to_string(), filte_content, json_data, None, None).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -197,7 +208,7 @@ let json_data = String::from(r#" let json_data = JsonData::new(json_data_value)?; -let content = match generate_report_with_template_id( template_id, filte_content, json_data).await { +let content = match carbone.generate_report_with_template_id( template_id, filte_content, json_data).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -232,14 +243,14 @@ let template_id = match carbone.upload_template("report", template_data, None).a pub async fn delete_template(&self, template_id: TemplateId); ``` -Delete a template by providing a template ID as `TemplateId`, and it returns whether the request succeeded as a `Boolean`. +Delete a template by providing a template ID as `template_id`, and it returns whether the request succeeded as a `Boolean`. **Example** ```rust let template_id = TemplateId::new("template_id".to_string())?; -let boolean = match delete_template(template_id).await { +let boolean = match carbone.delete_template(template_id).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -247,7 +258,7 @@ let boolean = match delete_template(template_id).await { ### generate Document only -The generate_report function takes a template ID as `String`, and the JSON data-set as `String`. +The generate_report function takes a template ID as `String`, and the JSON data-set as `JsonData`. It return a `renderId`, you can pass this `renderId` at [get_report](#download-document-only) for download the document. ```rust @@ -272,7 +283,7 @@ let json_data = String::from(r#" let json_data = JsonData::new(json_data_value)?; -let render_id = match render_data(template_id, json_data).await { +let render_id = match carbone.render_data(template_id, json_data).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -293,7 +304,7 @@ pub async fn get_report(&self, render_id: &RenderId); let render_id = RenderId::new("render_id".to_string())?; -let content = match get_report(render_id).await { +let content = match carbone.get_report(&render_id).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -315,11 +326,33 @@ Provide a template ID as `String` and it returns the file as `Bytes`. ```rust let template_id = TemplateId::new("template_id".to_string())?; -let content = match download_template(template_id).await { +let content = match carbone.download_template(template_id).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; ``` + +### Get API Status + +**Definition** + +```rust + +pub async fn get_status(&self); + +``` + +The function requests the Carbone API to get the current status and version as `String`. + +**Example** + +```rust +let status = match carbone.get_status().await { + Ok(v) => v, + Err(e) => panic!("{}", e.to_string()) + }; +``` + ### Set API **Definition** @@ -345,12 +378,12 @@ At the root of the SDK repository run: cargo build ``` -In another Java project, you can load the local build of the SDK, in the pom.xml: -```xml +In another Rust project, you can load the local build of the SDK, in the Cargo.toml: +```toml carbone-sdk-rust = {path = "your/local/path"} ``` -Finally, compile your Java project with the SDK: +Finally, compile your Rust project with the SDK: ```sh cargo run ``` diff --git a/src/carbone.rs b/src/carbone.rs index 30f76c9..5163b87 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -29,14 +29,14 @@ pub struct Carbone<'a> { } impl<'a> Carbone<'a> { - pub fn new(config: &'a Config, api_token: &'a ApiJsonToken) -> Result { + pub fn new(config: &'a Config, api_token: Option<&'a ApiJsonToken>) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( "carbone-version", HeaderValue::from_str(config.api_version.as_str()).unwrap(), ); - let bearer = format!("Bearer {}", api_token.as_str()); + let bearer = format!("Bearer {}", api_token.expect("REASON").as_str()); let mut auth_value = header::HeaderValue::from_str(bearer.as_str()).unwrap(); auth_value.set_sensitive(true); @@ -55,41 +55,6 @@ impl<'a> Carbone<'a> { } // Delete a template from the Carbone Service. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::ApiJsonToken; - /// use carbone_sdk_rust::template::TemplateId; - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = ApiJsonToken::new(token)?; - /// - /// let template_id = TemplateId::new("0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string())?; - /// - /// let carbone = Carbone::new(&config, &api_token)?; - /// let is_deleted = carbone.delete_template(template_id).await.unwrap(); - /// - /// assert_eq!(is_deleted, true); - /// - /// Ok(()) - /// } - /// ``` pub async fn delete_template(&self, template_id: TemplateId) -> Result { let url = format!("{}/template/{}", self.config.api_url, template_id.as_str()); @@ -105,43 +70,6 @@ impl<'a> Carbone<'a> { } // Download a template from the Carbone Service. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::ApiJsonToken; - /// use carbone_sdk_rust::template::TemplateId; - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = ApiJsonToken::new(token)?; - /// - /// let template_file = String::from("template.odt"); - /// - /// let template_id = TemplateId::new("0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string())?; - /// let carbone = Carbone::new(&config, &api_token)?; - /// - /// let template_content = carbone.download_template(&template_id).await.unwrap(); - /// - /// assert_eq!(template_content.is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn download_template(&self, template_id: &TemplateId) -> Result { let url = format!("{}/template/{}", self.config.api_url, template_id.as_str()); @@ -156,53 +84,6 @@ impl<'a> Carbone<'a> { } /// Generate a report. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::render::*; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::{ApiJsonToken, JsonData}; - /// use carbone_sdk_rust::template::{TemplateFile,TemplateId}; - /// - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = &ApiJsonToken::new(token)?; - /// - /// let carbone = Carbone::new(&config, api_token)?; - /// - /// let json_data_value = String::from(r#" - /// "data" : { - /// "firstname" : "John", - /// "lastname" : "Wick" - /// }, - /// "convertTo" : "odt" - /// "#); - /// - /// let json_data = JsonData::new(json_data_value)?; - /// - /// let template_data: Vec = Vec::new(); // content of the template - /// let report_content = carbone.generate_report("template.odt".to_string(), template_data, json_data, None, None).await.unwrap(); - /// - /// assert_eq!(report_content.is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn generate_report( &self, template_name: String, @@ -245,41 +126,6 @@ impl<'a> Carbone<'a> { /// Get a new report. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::render::RenderId; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::ApiJsonToken; - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = ApiJsonToken::new(token)?; - /// - /// let carbone = Carbone::new(&config, &api_token)?; - /// - /// let render_id = &RenderId::new("MTAuMjAuMjEuMTAgICAg01E98H4R7PMC2H6XSE5Z6J8XYQ.pdf".to_string())?; - /// let report_content = carbone.get_report(render_id).await.unwrap(); - /// - /// assert_eq!(report_content.is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn get_report(&self, render_id: &RenderId) -> Result { let url = format!("{}/render/{}", self.config.api_url, render_id.as_str()); @@ -310,53 +156,6 @@ impl<'a> Carbone<'a> { } /// Generate a report with a template_id given. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::render::*; - /// use carbone_sdk_rust::types::JsonData; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::ApiJsonToken; - /// use carbone_sdk_rust::template::TemplateId; - /// - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = &ApiJsonToken::new(token)?; - /// - /// let template_id = TemplateId::new("0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string())?; - /// let carbone = Carbone::new(&config, &api_token)?; - /// - /// let json_data_value = String::from(r#" - /// "data" : { - /// "firstname" : "John", - /// "lastname" : "Wick" - /// }, - /// "convertTo" : "odt" - /// "#); - /// - /// let json_data = JsonData::new(json_data_value)?; - /// let report_content = carbone.generate_report_with_template_id(template_id, json_data).await.unwrap(); - /// - /// assert_eq!(report_content.is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn generate_report_with_template_id( &self, template_id: TemplateId, @@ -369,51 +168,6 @@ impl<'a> Carbone<'a> { } /// Render data with a given template_id. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::template::TemplateId; - /// use carbone_sdk_rust::errors::CarboneError; - /// use carbone_sdk_rust::types::{ApiJsonToken, JsonData}; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// let api_token = ApiJsonToken::new(token)?; - /// - /// let template_id = TemplateId::new("foiejwoi21e093ru3209jf2093j".to_string())?; - /// - /// let carbone = Carbone::new(&config, &api_token)?; - /// - /// let json_data_value = String::from(r#" - /// "data" : { - /// "firstname" : "John", - /// "lastname" : "Wick" - /// }, - /// "convertTo" : "odt" - /// "#); - /// - /// let json_data = JsonData::new(json_data_value)?; - /// - /// let render_id = carbone.render_data(template_id, json_data).await.unwrap(); - /// - /// assert_eq!(render_id.as_str().is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn render_data( &self, template_id: TemplateId, @@ -448,44 +202,6 @@ impl<'a> Carbone<'a> { } /// Upload a template to the Carbone Service. - /// - /// - /// # Example - /// - /// ```no_run - /// use std::env; - /// use std::fs; - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::carbone::Carbone; - /// use carbone_sdk_rust::types::ApiJsonToken; - /// use carbone_sdk_rust::template::TemplateFile; - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), CarboneError> { - /// - /// let token = match env::var("CARBONE_TOKEN") { - /// Ok(v) => v, - /// Err(e) => panic!("{}", e.to_string()) - /// }; - /// - /// let config: Config = Default::default(); - /// - /// let api_token = ApiJsonToken::new(token)?; - /// - /// let file_name = "template.odt"; - /// let file_path = format!("tests/data/{}", file_name); - /// let filte_content = fs::read(file_path)?; - /// - /// let carbone = Carbone::new(&config, &api_token)?; - /// let template_id = carbone.upload_template(file_name, filte_content, None).await.unwrap(); - /// - /// assert_eq!(template_id.as_str().is_empty(), false); - /// - /// Ok(()) - /// } - /// ``` pub async fn upload_template( &self, file_name: &str, @@ -540,7 +256,6 @@ impl<'a> Carbone<'a> { let url = format!("{}/status", self.config.api_url); let response = self.http_client.get(url).send().await?; - println!("{:?}",response.headers()); if response.status() == StatusCode::OK { let body = response.text().await?; diff --git a/src/config.rs b/src/config.rs index c34dec4..ce3a06a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,27 +23,7 @@ pub struct Config { impl Config { /// Create a new Configuraiton. - /// - /// This function will create new Config. - /// - /// # Example - /// - /// ```no_run - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::errors::CarboneError; - /// use carbone_sdk_rust::types::ApiVersion; - /// - /// fn main() -> Result<(), CarboneError> { - /// let api_version: ApiVersion = ApiVersion::new("4".to_string())?; - /// let config = Config::new( - /// "http://127.0.0.1:57780".to_string(), - /// 4, - /// api_version)?; - /// Ok(()) - /// } - /// ``` - pub fn new(api_url: String, api_timeout: u64, api_version: ApiVersion) -> Result { + pub fn new(api_url: String, api_timeout: u64, api_version: Option) -> Result { let config = Self { api_url, api_timeout, @@ -55,22 +35,6 @@ impl Config { } /// Load a Configuraiton from a file. - /// - /// This function will create new Config struct with, - /// the values from the file. - /// - /// # Example - /// - /// ```no_run - /// - /// use carbone_sdk_rust::config::Config; - /// use carbone_sdk_rust::errors::CarboneError; - /// - /// fn main() -> Result<(), CarboneError> { - /// let config = Config::from_file("tests/config.test.json")?; - /// Ok(()) - /// } - /// ``` pub fn from_file(path: &str) -> Result { let file_content = fs::read_to_string(path).or(Err(CarboneError::FileNotFound(path.to_string())))?; @@ -81,26 +45,6 @@ impl Config { } /// Load a Default Configuraiton. -/// -/// This function will create new Config struct the with, -/// the default values. -/// -/// # Example -/// -/// ```no_run -/// -/// use carbone_sdk_rust::config::Config; -/// use carbone_sdk_rust::errors::CarboneError; -/// -/// fn main() -> Result<(), CarboneError> { -/// -/// let config: Config = Default::default(); -/// -/// assert_eq!(config.api_url, "https://api.carbone.io".to_string()); -/// -/// Ok(()) -/// } -/// ``` impl Default for Config { fn default() -> Self { Self { From 50c37b9dcae8c3f1ccfa4724ecb7eedd9fa26a2f Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:49:00 +0200 Subject: [PATCH 05/12] petit fix --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index ce3a06a..f17b241 100644 --- a/src/config.rs +++ b/src/config.rs @@ -23,7 +23,7 @@ pub struct Config { impl Config { /// Create a new Configuraiton. - pub fn new(api_url: String, api_timeout: u64, api_version: Option) -> Result { + pub fn new(api_url: String, api_timeout: u64, api_version: ApiVersion) -> Result { let config = Self { api_url, api_timeout, From 6e6d75125c7ef3bdbc1473043463b4f8094ce0ce Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:51:32 +0200 Subject: [PATCH 06/12] autre fix --- src/carbone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/carbone.rs b/src/carbone.rs index 5163b87..27730a7 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -29,7 +29,7 @@ pub struct Carbone<'a> { } impl<'a> Carbone<'a> { - pub fn new(config: &'a Config, api_token: Option<&'a ApiJsonToken>) -> Result { + pub fn new(config: &'a Config, api_token: &'a ApiJsonToken) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( "carbone-version", From 81a8f32ee0d32b63156810d12687909e5b45a06e Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:53:14 +0200 Subject: [PATCH 07/12] fix --- src/carbone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/carbone.rs b/src/carbone.rs index 27730a7..7249395 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -36,7 +36,7 @@ impl<'a> Carbone<'a> { HeaderValue::from_str(config.api_version.as_str()).unwrap(), ); - let bearer = format!("Bearer {}", api_token.expect("REASON").as_str()); + let bearer = format!("Bearer {}", api_token.as_str()); let mut auth_value = header::HeaderValue::from_str(bearer.as_str()).unwrap(); auth_value.set_sensitive(true); From 02b7219de1aa1e26500a39f6a413f4298578af96 Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:05:05 +0200 Subject: [PATCH 08/12] correction change log --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 700c9a0..2a69923 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ All notable changes to this project will be documented in this file. This projec - Released on 2024//: The package was originally made by Pascal CHENEVAS from . The Carbone team is now maintaining the SDK. This version brings all missing functions to interact with the Carbone API. - Added function `getStatus`: It return the current status and the version of the API as `String`. - Added error `HttpError`: It return the status code and a error message. -- Modified for the `new` constructor: the `api-token` has become optional. - Modified for the `generate_report`: Optimization of api calls when there is error 404. - Modified for the `render_data`: When there is an error in the request, the function returns the status code and an error message. - Added units tests. From 720b3d2f4f54e46973b9a40680b4424e3b72d20b Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:49:17 +0200 Subject: [PATCH 09/12] api token optional --- CHANGELOG.md | 1 + README.md | 17 ++++++++--------- src/carbone.rs | 26 +++++++++++++------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a69923..3b14147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. This projec - Added error `HttpError`: It return the status code and a error message. - Modified for the `generate_report`: Optimization of api calls when there is error 404. - Modified for the `render_data`: When there is an error in the request, the function returns the status code and an error message. +- Modified for the `new`: The value `api_token` is optional. - Added units tests. ### v1.0.0 diff --git a/README.md b/README.md index 5eba1d8..176f1d1 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ use carbone_sdk_rust::template::TemplateId; use carbone_sdk_rust::errors::CarboneError; +use std::fs::File; +use std::io::Write + #[tokio::main] async fn main() -> Result<(), CarboneError> { @@ -117,18 +120,14 @@ Get your API key on your Carbone account: https://account.carbone.io/. // For Carbone Cloud, provide your API Access Token as first argument: let token = "Token"; let config: Config = Default::default(); -let carbone = Carbone::new(&config, &api_token)?; +let carbone = Carbone::new(&config, Some(&api_token))?; ``` Example of a new SDK instance for **Carbone On-premise** or **Carbone On-AWS**: ```rust -let token = match env::var("CARBONE_TOKEN") { - Ok(v) => v, - Err(e) => panic!("{}", e.to_string()) - }; // Define the URL of your Carbone On-premise Server or AWS EC2 URL: let config: Config = Config::new("ON_PREMISE_URL".to_string(), "api_time_out_in_sec_in_u64", ApiVersion::new("4".to_string()).expect("REASON")).expect("REASON"); -let carbone = Carbone::new(&config, &api_token)?; +let carbone = Carbone::new(&config, None)?; ``` Constructor to create a new instance of CarboneSDK. @@ -185,7 +184,7 @@ let json_data_value = String::from(r#" let json_data = JsonData::new(json_data_value)?; -let content = match carbone.generate_report(template_name.to_string(), filte_content, json_data, None, None).await { +let content = match carbone.generate_report(file_name.to_string(), file_content, json_data, None, None).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -231,7 +230,7 @@ let template_name = "template.odt".to_string(); let template_path = format!("src/{}", template_name); let template_data = fs::read(template_path.to_owned())?; -let template_id = match carbone.upload_template("report", template_data, None).await { +let template_id = match carbone.upload_template(template_name, template_data, None).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; @@ -326,7 +325,7 @@ Provide a template ID as `String` and it returns the file as `Bytes`. ```rust let template_id = TemplateId::new("template_id".to_string())?; -let content = match carbone.download_template(template_id).await { +let content = match carbone.download_template(&template_id).await { Ok(v) => v, Err(e) => panic!("{}", e.to_string()) }; diff --git a/src/carbone.rs b/src/carbone.rs index 7249395..eb89485 100644 --- a/src/carbone.rs +++ b/src/carbone.rs @@ -9,7 +9,6 @@ use reqwest::multipart; use reqwest::Client; use reqwest::ClientBuilder; use reqwest::StatusCode; -use reqwest::Response; use crate::carbone_response::APIResponse; @@ -20,7 +19,6 @@ use crate::template::*; use crate::types::{ApiJsonToken, JsonData}; use crate::types::Result; -use std::collections::HashMap; #[derive(Debug, Clone)] pub struct Carbone<'a> { @@ -29,24 +27,26 @@ pub struct Carbone<'a> { } impl<'a> Carbone<'a> { - pub fn new(config: &'a Config, api_token: &'a ApiJsonToken) -> Result { + pub fn new(config: &'a Config, api_token: Option<&'a ApiJsonToken>) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( "carbone-version", HeaderValue::from_str(config.api_version.as_str()).unwrap(), ); + if api_token != None + { + let bearer = format!("Bearer {}", api_token.expect("REASON").as_str()); - let bearer = format!("Bearer {}", api_token.as_str()); + let mut auth_value = header::HeaderValue::from_str(bearer.as_str()).unwrap(); + auth_value.set_sensitive(true); - let mut auth_value = header::HeaderValue::from_str(bearer.as_str()).unwrap(); - auth_value.set_sensitive(true); - - headers.insert(header::AUTHORIZATION, auth_value); + headers.insert(header::AUTHORIZATION, auth_value); + } - let http_client = ClientBuilder::new() - .default_headers(headers) - .timeout(Duration::from_secs(config.api_timeout)) - .build()?; + let http_client = ClientBuilder::new() + .default_headers(headers) + .timeout(Duration::from_secs(config.api_timeout)) + .build()?; Ok(Self { config, @@ -95,7 +95,7 @@ impl<'a> Carbone<'a> { let template_id_generated = TemplateId::from_bytes(template_data.to_owned(), payload)?; let mut template_id = template_id_generated; - let mut render_id = None; + let render_id; match self.render_data(template_id, json_data.clone()).await { Ok(id) => { From 6f4ceb24a7a031b4443b727390a4dd10cc6c4c8f Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Wed, 7 Aug 2024 10:57:43 +0200 Subject: [PATCH 10/12] correction test unitaire --- tests/carbone_test.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/carbone_test.rs b/tests/carbone_test.rs index 17c59c1..3b2810a 100644 --- a/tests/carbone_test.rs +++ b/tests/carbone_test.rs @@ -50,7 +50,7 @@ mod tests { let api_token = &helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let is_deleted = carbone.delete_template(template_id).await.unwrap(); mock_server.assert(); @@ -72,7 +72,7 @@ mod tests { "0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string(), )?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let result = carbone.delete_template(template_id).await; assert!(result.is_err()); @@ -110,7 +110,7 @@ mod tests { let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let result = carbone.delete_template(template_id).await; let expected_error = CarboneError::Error(error_msg); @@ -146,7 +146,7 @@ mod tests { let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let template_content = carbone.download_template(&template_id).await.unwrap(); @@ -169,7 +169,7 @@ mod tests { "0545253258577a632a99065f0572720225f5165cc43db9515e9cef0e17b40114".to_string(), )?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let result = carbone.download_template(&template_id).await; @@ -208,7 +208,7 @@ mod tests { let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let result = carbone.download_template(&template_id).await; @@ -232,7 +232,7 @@ mod tests { let config = helper.create_config_for_mock_server(Some(&server))?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let report_data = fs::read_to_string("tests/data/report_data.json")?; @@ -289,7 +289,7 @@ mod tests { let config = helper.create_config_for_mock_server(Some(&server))?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let report_data = fs::read_to_string("tests/data/report_data.json")?; @@ -351,7 +351,7 @@ mod tests { let config = helper.create_config_for_mock_server(Some(&server))?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let report_data = fs::read_to_string("tests/data/report_data.json")?; @@ -447,7 +447,7 @@ mod tests { let config = helper.create_config_for_mock_server(Some(&server))?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let render_id_value = "844318fe97904fb0897d4b0a47fbe9bbd1ce5c9624ae694545cbc1877f581d86.pdf"; @@ -477,7 +477,7 @@ mod tests { let config = Config::new("http://bad_url".to_string(), 1, api_version)?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let render_id_value = "844318fe97904fb0897d4b0a47fbe9bbd1ce5c9624ae694545cbc1877f581d86.pdf"; @@ -500,7 +500,7 @@ mod tests { let config = helper.create_config_for_mock_server(Some(&server))?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let render_id_value = "unknown_render_id.pdf"; let render_id = &RenderId::new(render_id_value.to_string())?; @@ -556,7 +556,7 @@ mod tests { let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let json_data = String::from( r#" @@ -593,7 +593,7 @@ mod tests { let config = Config::new("http://bad_url".to_string(), 1, api_version)?; let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let json_data = String::from( r#" @@ -652,7 +652,7 @@ mod tests { let file_path = format!("tests/data/{}", file_name); let filte_content = fs::read(file_path)?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let template_id = carbone .upload_template(file_name, filte_content, None) .await @@ -704,7 +704,7 @@ mod tests { let file_path = format!("tests/data/{}", file_name); let filte_content = fs::read(file_path)?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let template_id = carbone .upload_template(file_name, filte_content, Some("salt1234")) .await @@ -748,7 +748,7 @@ mod tests { let file_path = format!("tests/data/{}", file_name); let filte_content = fs::read(file_path)?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let result = carbone .upload_template(file_name, filte_content, None) .await; @@ -780,7 +780,7 @@ mod tests { let api_token = helper.create_api_token()?; - let carbone = Carbone::new(&config, &api_token)?; + let carbone = Carbone::new(&config, Some(&api_token))?; let response = carbone.get_status().await.unwrap(); From abbf8f88459da3a241a104b72e37978b38cb37b7 Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:27:16 +0200 Subject: [PATCH 11/12] correction Doc --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 176f1d1..0aa66d9 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ async fn main() -> Result<(), CarboneError> { - [Get a Template](#get-template) - [Set Carbone URL](#set-carbone-url) - [Get API status](#get-api-status) - - [Set API Version](#set-api-version) + - [Set API Confing](#set-api-version) - [Build commands](#build-commands) - [Test commands](#test-commands) - [Contributing](#-contributing) @@ -120,6 +120,7 @@ Get your API key on your Carbone account: https://account.carbone.io/. // For Carbone Cloud, provide your API Access Token as first argument: let token = "Token"; let config: Config = Default::default(); +let api_token = ApiJsonToken::new(token.to_string())?; let carbone = Carbone::new(&config, Some(&api_token))?; ``` @@ -142,7 +143,7 @@ Check if it is set by running: $ printenv | grep "CARBONE_TOKEN" ``` -### Download Document +### Generate and Download a Document ```rust pub async fn generate_report( &self, template_name: String, template_data: Vec, json_data: JsonData, payload: Option<&str>, salt: Option<&str>); @@ -170,7 +171,7 @@ The render function generates a document using a specified template and data. It ```rust let file_name = "name_file.extention"; let file_path = format!("your/path/{}", file_name); -let filte_content = fs::read(file_path)?; +let file_content = fs::read(file_path)?; let json_data_value = String::from(r#" { @@ -214,7 +215,7 @@ let content = match carbone.generate_report_with_template_id( template_id, filte ``` -### upload Document +### Upload Template ```rust pub async fn upload_template(&self,file_name: &str,file_content: Vec,salt: Option<&str>); @@ -236,7 +237,7 @@ let template_id = match carbone.upload_template(template_name, template_data, No }; ``` -### delete Document +### Delete Template ```rust pub async fn delete_template(&self, template_id: TemplateId); @@ -255,7 +256,7 @@ let boolean = match carbone.delete_template(template_id).await { }; ``` -### generate Document only +### Generate Document only The generate_report function takes a template ID as `String`, and the JSON data-set as `JsonData`. It return a `renderId`, you can pass this `renderId` at [get_report](#download-document-only) for download the document. @@ -352,7 +353,7 @@ let status = match carbone.get_status().await { }; ``` -### Set API +### Set API Confing **Definition** From 12b2fb824fac79aa0a504ed0459125c729bcf4ca Mon Sep 17 00:00:00 2001 From: p0izon <115532860+MtPoison@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:28:36 +0200 Subject: [PATCH 12/12] correction --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0aa66d9..6658c87 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ async fn main() -> Result<(), CarboneError> { - [Get a Template](#get-template) - [Set Carbone URL](#set-carbone-url) - [Get API status](#get-api-status) - - [Set API Confing](#set-api-version) + - [Set API Config](#set-api-version) - [Build commands](#build-commands) - [Test commands](#test-commands) - [Contributing](#-contributing) @@ -353,7 +353,7 @@ let status = match carbone.get_status().await { }; ``` -### Set API Confing +### Set API Config **Definition**