-
Notifications
You must be signed in to change notification settings - Fork 35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Export as code (axios, fetch, Go net/http...) #64
Comments
I think as this could also be implemented with the changes proposed in #86 i think i'd like to take this one as well. |
Oops, sorry. Let me do that |
Hi! I'd like to discuss about some refactors i've been thinking about this feature... well, the thing is that to export for others languages, in the way the export code service is working right now, one would have to manually build the string using rust code to do the loops of headers, etc, with conditionals for proper formatting on code generation... This could be kinda manual and tedious considering we're gonna aim to have more and more formats to export such as the ones in the title of this issue. I'd like to propose the usage of some kind of compile time template engine processor such as askama which i tried recently and seems to work pretty good, in fact, i've got to generate in a sample project a curl request string based on a template and a structure with the data. Here's the code snippet I used:
As you can see, one can create macros, loops, conditionals, formatting, even use rust expressions inside conditions loops and others, also allows calling rust functions (
#![feature(iter_intersperse)]
use askama::Template;
use std::{collections::HashMap, convert::From};
struct HttpHeader {
key: String,
value: String,
}
impl<'a> From<(&'a str, &'a str)> for HttpHeader {
fn from(value: (&'a str, &'a str)) -> HttpHeader {
HttpHeader {
key: value.0.into(),
value: value.1.into(),
}
}
}
#[derive(Clone, Copy)]
enum RawEncoding {
Json,
Raw,
}
#[derive(Clone)]
enum Body {
UrlEncoded(HashMap<String, String>),
Raw {
encoding: RawEncoding,
contents: String,
}
}
fn format_body_urlencoded(urlencoded: &HashMap<String, String>) -> String {
let mut value: Vec<String> = Vec::new();
for (key, val) in urlencoded.iter() {
value.push(format!("{key}={val}"));
}
value.join("&")
}
impl From<Body> for String {
fn from(value: Body) -> Self {
match value {
Body::UrlEncoded(hashmap) => {
format_body_urlencoded(&hashmap)
},
Body::Raw {
encoding: _encoding,
contents,
} => contents,
}
}
}
/// Formats the body for the curl output.
fn format_body_curl(body: &&Body) -> Option<String> {
let body = *body;
let body = body.clone();
Some(body.into())
}
#[derive(Template)]
#[template(path = "curl")]
struct CurlTemplate {
url: String,
method: String,
headers: Option<Vec<HttpHeader>>,
body: Option<Body>,
}
fn main() {
let example_1 = CurlTemplate {
url: "https://jsonplaceholder.typicode.com/users".into(),
method: "GET".into(),
headers: Some(vec![
("Content-Type", "application/json").into(),
("Accept", "application/json").into(),
]),
body: Some(Body::Raw {
encoding: RawEncoding::Json,
contents: "{\"name\": \"Arch\"}".into(),
}),
};
println!("\nExample 1\n");
if let Ok(rendered) = example_1.render() {
println!("{}", rendered.trim());
}
let example_2 = CurlTemplate {
url: "https://jsonplaceholder.typicode.com/todos/1".into(),
method: "GET".into(),
headers: None,
body: Some(Body::UrlEncoded({
let data: Vec<(String, String)> = vec![
("example".into(), "key".into()),
("another".into(), "one".into()),
];
data.into_iter().collect::<HashMap<String, String>>()
}))
};
println!("\nExample 2\n");
if let Ok(rendered) = example_2.render() {
println!("{}", rendered.trim());
}
} Sorry for the quite large example, just wanted to test having both headers and body in a similar structure like the BoundRequest one, we would just have to maybe convert some types but that's kinda trivial. Lmk your comments, do you think this could help us to build the others formats? Or in the opposite, there's some other option could help us more or just build the Strings manually. |
Here's a patch which integrates askama in the current service we're using to generate curl code, this will make cartero export the request to curl using askama templates instead, just in case you wanna see it working in the app itself. diff --git a/Cargo.lock b/Cargo.lock
index 312e0ba..3553fc5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "addr2line"
@@ -32,6 +32,50 @@ version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+[[package]]
+name = "askama"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
+dependencies = [
+ "askama_derive",
+ "askama_escape",
+ "humansize",
+ "num-traits",
+ "percent-encoding 2.3.1",
+]
+
+[[package]]
+name = "askama_derive"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
+dependencies = [
+ "askama_parser",
+ "basic-toml",
+ "mime 0.3.17",
+ "mime_guess",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.53",
+]
+
+[[package]]
+name = "askama_escape"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
+
+[[package]]
+name = "askama_parser"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
+dependencies = [
+ "nom",
+]
+
[[package]]
name = "async-channel"
version = "1.9.0"
@@ -80,6 +124,15 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
+[[package]]
+name = "basic-toml"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -144,6 +197,7 @@ dependencies = [
name = "cartero"
version = "0.2.0"
dependencies = [
+ "askama",
"formdata",
"futures-lite 2.3.0",
"gettext-rs",
@@ -814,6 +868,15 @@ version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
+[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
[[package]]
name = "hyper"
version = "0.10.16"
@@ -829,7 +892,7 @@ dependencies = [
"time",
"traitobject",
"typeable",
- "unicase",
+ "unicase 1.4.2",
"url 1.7.2",
]
@@ -956,6 +1019,12 @@ version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+[[package]]
+name = "libm"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+
[[package]]
name = "libnghttp2-sys"
version = "0.1.9+1.58.0"
@@ -1067,6 +1136,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime 0.3.17",
+ "unicase 2.8.0",
+]
+
[[package]]
name = "mime_multipart"
version = "0.6.1"
@@ -1108,6 +1187,15 @@ dependencies = [
"minimal-lexical",
]
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
[[package]]
name = "num_cpus"
version = "1.16.0"
@@ -1860,6 +1948,12 @@ dependencies = [
"version_check 0.1.5",
]
+[[package]]
+name = "unicase"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df"
+
[[package]]
name = "unicode-bidi"
version = "0.3.15"
diff --git a/Cargo.toml b/Cargo.toml
index 9f983d8..06868c1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ csd = []
[dependencies]
adw = { version = "0.6.0", package = "libadwaita", features = ["v1_5", "gtk_v4_12"] }
+askama = "0.12.1"
formdata = "0.13.0"
futures-lite = "2.3.0"
gettext-rs = { version = "0.7.0", features = ["gettext-system"] }
diff --git a/src/error.rs b/src/error.rs
index 4489e22..fbee291 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -34,4 +34,7 @@ pub enum CarteroError {
#[error("Outdated schema, please update the software")]
OutdatedSchema,
+
+ #[error("Template generation error")]
+ AskamaFailed,
}
diff --git a/src/widgets/export_tab/code.rs b/src/widgets/export_tab/code.rs
index cd0d270..8fda36e 100644
--- a/src/widgets/export_tab/code.rs
+++ b/src/widgets/export_tab/code.rs
@@ -266,7 +266,7 @@ impl BaseExportPaneExt for CodeExportPane {
let service = CodeExportService::new(data.clone());
let imp = self.imp();
- if let Ok(command) = service.generate() {
+ if let Ok(command) = service.into_curl_like() {
imp.set_buffer_content(command.as_bytes());
}
}
diff --git a/src/widgets/export_tab/service.rs b/src/widgets/export_tab/service.rs
index 7e2f6b4..897ae54 100644
--- a/src/widgets/export_tab/service.rs
+++ b/src/widgets/export_tab/service.rs
@@ -15,12 +15,52 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
-use serde_json::{Error, Value};
+use askama::Template;
use crate::client::BoundRequest;
-use crate::entities::{EndpointData, RequestPayload};
+use crate::entities::EndpointData;
use crate::error::CarteroError;
+mod templates {
+ use crate::client::BoundRequest;
+ use askama::Template;
+ use serde_json::Value;
+ use std::convert::From;
+
+ #[macro_export]
+ macro_rules! generate_template_struct {
+ ($struct_name:ident, $template_path:expr) => {
+ #[derive(Template)]
+ #[template(path = $template_path)]
+ pub struct $struct_name {
+ pub url: String,
+ pub method: String,
+ pub headers: std::collections::HashMap<String, String>,
+ pub body: Option<String>,
+ }
+
+ impl From<BoundRequest> for $struct_name {
+ fn from(value: BoundRequest) -> Self {
+ Self {
+ url: value.url,
+ method: value.method.into(),
+ headers: value.headers,
+ body: value.body.map(|v| {
+ let body = String::from_utf8_lossy(&v).to_string();
+
+ serde_json::from_str(body.as_ref()).map_or(body, |v: Value| {
+ serde_json::to_string(&v).unwrap().replace("'", "\\\\'")
+ })
+ }),
+ }
+ }
+ }
+ };
+ }
+
+ generate_template_struct!(CurlTemplate, "curl");
+}
+
pub struct CodeExportService {
endpoint_data: EndpointData,
}
@@ -30,71 +70,10 @@ impl CodeExportService {
Self { endpoint_data }
}
- pub fn generate(&self) -> Result<String, CarteroError> {
+ pub fn into_curl_like(&self) -> Result<String, CarteroError> {
let bound_request = BoundRequest::try_from(self.endpoint_data.clone())?;
- let mut command = "curl".to_string();
-
- command.push_str(&{
- let method_str: String = bound_request.method.into();
- format!(" -X {} '{}'", method_str, bound_request.url)
- });
-
- if !bound_request.headers.is_empty() {
- let size = bound_request.headers.len();
- let mut keys: Vec<&String> = bound_request.headers.keys().collect();
- keys.sort();
-
- command.push_str(" \\\n");
-
- for (i, key) in keys.iter().enumerate() {
- let val = bound_request.headers.get(*key).unwrap();
-
- command.push_str(&{
- let mut initial = format!(" -H '{key}: {val}'");
-
- if i < size - 1 {
- initial.push_str(" \\\n");
- }
-
- initial
- });
- }
- }
-
- if let RequestPayload::Urlencoded(_) = &self.endpoint_data.body {
- if let Some(bd) = bound_request.body {
- let str = String::from_utf8_lossy(&bd).to_string();
- command.push_str(&format!(" \\\n -d '{str}'"));
- }
- }
-
- if let RequestPayload::Raw {
- encoding: _,
- content,
- } = &self.endpoint_data.body
- {
- command.push_str(&'fmt: {
- let body = String::from_utf8_lossy(content).to_string();
- let value: Result<Value, Error> = serde_json::from_str(body.as_ref());
-
- if value.is_err() {
- break 'fmt String::new();
- }
-
- let value = value.unwrap();
- let trimmed_json_str = serde_json::to_string(&value);
-
- if trimmed_json_str.is_err() {
- break 'fmt String::new();
- }
-
- let trimmed_json_str = trimmed_json_str.unwrap();
- let trimmed_json_str = trimmed_json_str.replace("'", "\\\\'");
-
- format!(" \\\n -d '{}'", trimmed_json_str)
- });
- }
+ let template: templates::CurlTemplate = bound_request.into();
- Ok(command)
+ template.render().map_err(|_| CarteroError::AskamaFailed)
}
}
diff --git a/templates/curl b/templates/curl
new file mode 100644
index 0000000..93eb988
--- /dev/null
+++ b/templates/curl
@@ -0,0 +1,11 @@
+{%- macro backslash(headers, index) %}
+{%- if index < headers.len() - 1 %} \{%- endif %}
+{%- endmacro -%}
+
+curl -X {{ method }} '{{ url }}'{% if !headers.is_empty() %} \
+{%- for (key, value) in headers.iter() %}
+ -H '{{ key }}: {{ value }}'{% call backslash(headers, loop.index0) -%}
+{%- endfor %}
+{%- endif %}{% if let Some(body) = body %} \
+ -d '{{ body }}'
+{%- endif %}
\ No newline at end of file |
No description provided.
The text was updated successfully, but these errors were encountered: