Skip to content

Commit

Permalink
Added Multipart form, UrlEncoded form and settings in Postman import
Browse files Browse the repository at this point in the history
  • Loading branch information
Julien-cpsn committed Apr 15, 2024
1 parent ec012f9 commit aac381e
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 18 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ serde = { version = "1.0.197", features = ["derive", "rc"] }
serde_json = "1.0.114"
toml = "0.8.11"
envfile = "0.2.1"
parse_postman_collection = "0.2.1"
parse_postman_collection = "0.2.2"
clap = { version = "4.5.0", features = ["derive", "color"] }
tokio = { version = "1.36.0", features = ["rt", "rt-multi-thread", "macros"] }
strum = "0.26.2"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,11 @@ cargo run -- -h
- **To add**
- Create a repo wiki
- Document whole code
- File body content-type
- **To improve**
- Pretty print output
- Sign binary
- Add Multipart form, URL encoded form and request settings from Postman import
- **To fix**
- Query parameters bug
Expand All @@ -163,6 +163,7 @@ cargo run -- -h
- Editing cookies
- Insomnia import
- Auto-completion on env file variables
- Manage multipart Content-type header (auto-generated for now)
### Ideas (will think about it later)
Expand Down
87 changes: 87 additions & 0 deletions import_tests/Test Collection.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,93 @@
}
},
"response": []
},
{
"name": "Test Multipart Form",
"protocolProfileBehavior": {
"followRedirects": false,
"disableCookies": true
},
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "formdata",
"formdata": [
{
"key": "custname",
"value": "ddddd",
"type": "text"
},
{
"key": "custtel",
"value": "",
"type": "text"
},
{
"key": "custemail",
"value": "",
"type": "text"
},
{
"key": "delivery",
"value": "",
"type": "text"
},
{
"key": "comments",
"value": "",
"type": "text"
},
{
"key": "some_file",
"type": "file",
"src": "/C:/Users/u248244/Documents/Rust/ATAC/LICENSE"
}
]
},
"url": {
"raw": "https://httpbin.org/post",
"protocol": "https",
"host": [
"httpbin",
"org"
],
"path": [
"post"
]
}
},
"response": []
},
{
"name": "Test URL encoded form",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "some_key",
"value": "some_value",
"type": "text"
}
]
},
"url": {
"raw": "https://httpbin.org/post",
"protocol": "https",
"host": [
"httpbin",
"org"
],
"path": [
"post"
]
}
},
"response": []
}
]
}
2 changes: 1 addition & 1 deletion src/app/app_logic/request/body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ impl App<'_> {
ContentType::NoBody => {
selected_request.find_and_delete_header(CONTENT_TYPE.as_str())
},
// Impossible to set the header for multipart yet, because of boundary and content-length that are computed on reqwest's side
// TODO: Impossible to set the header for multipart yet, because of boundary and content-length that are computed on reqwest's side
ContentType::Multipart(_) => {},
// Create or replace Content-Type header with new body content type
ContentType::Form(_) | ContentType::Raw(_) | ContentType::Json(_) | ContentType::Xml(_) | ContentType::Html(_) => {
Expand Down
116 changes: 104 additions & 12 deletions src/app/files/postman.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::{Arc, RwLock};
use parse_postman_collection::v2_1_0::{AuthType, Body, HeaderUnion, Items, Language, Mode, RequestClass, RequestUnion, Url};

use parse_postman_collection::v2_1_0::{AuthType, Body, FormParameterSrcUnion, HeaderUnion, Items, Language, Mode, RequestClass, RequestUnion, Url};

use crate::app::app::App;
use crate::app::startup::args::ARGS;
use crate::request::auth::Auth;
use crate::request::body::ContentType;
use crate::request::collection::Collection;
use crate::request::method::Method;
use crate::request::request::{DEFAULT_HEADERS, KeyValue, Request};

use crate::request::settings::RequestSettings;

impl App<'_> {
pub fn import_postman_collection(&mut self, path_buf: &PathBuf, max_depth: u16) {
Expand Down Expand Up @@ -148,14 +150,24 @@ fn is_folder(folder: &Items) -> bool {
}

fn parse_request(item: Items) -> Request {
let item_name = item.name.unwrap();
let item_name = item.name.clone().unwrap();

println!("\t\tFound request \"{}\"", item_name);

let mut request = Request::default();

request.name = item_name;

/* SETTINGS */

// TODO: update parse_postman_collection to handle "protocolProfileBehavior"
match retrieve_settings(&item) {
None => {}
Some(request_settings) => request.settings = request_settings
}

/* REQUEST */

let item_request = item.request.unwrap();

match &item_request {
Expand Down Expand Up @@ -189,18 +201,28 @@ fn parse_request(item: Items) -> Request {
Some(auth) => request.auth = auth
}

/* BODY */
/* HEADERS */

match retrieve_body(&request_class) {
match retrieve_headers(&request_class) {
None => {}
Some(content_type) => request.body = content_type
Some(headers) => request.headers = headers
}

/* HEADERS */
/* BODY */

match retrieve_headers(&request_class) {
match retrieve_body(&request_class) {
None => {}
Some(headers) => request.headers = headers
Some(body) => {
match &body {
ContentType::Multipart(_) => {} // TODO: Not handled yet
body_type => {
let content_type = body_type.to_content_type().clone();
request.modify_or_create_header("content-type", &content_type);
}
}

request.body = body;
}
}
}
RequestUnion::String(_) => {}
Expand Down Expand Up @@ -235,11 +257,12 @@ fn retrieve_body(request_class: &RequestClass) -> Option<ContentType> {
match body {
Body::String(body_as_raw) => Some(ContentType::Raw(body_as_raw)),
Body::BodyClass(body) => {
let body_as_raw = body.raw?;
let body_mode = body.mode?;

return match body_mode {
Mode::Raw => {
let body_as_raw = body.raw?;

if let Some(options) = body.options {
let language = options.raw?.language?;

Expand All @@ -257,8 +280,61 @@ fn retrieve_body(request_class: &RequestClass) -> Option<ContentType> {
}
},
Mode::File => None,
Mode::Formdata => None,
Mode::Urlencoded => None
Mode::Formdata => {
let form_data = body.formdata?;

let mut multipart: Vec<KeyValue> = vec![];

for param in form_data {
let param_type = param.form_parameter_type?;

let key_value = match param_type.as_str() {
"text" => KeyValue {
enabled: true,
data: (param.key, param.value.unwrap_or(String::new())),
},
"file" => {
let file = match param.src? {
FormParameterSrcUnion::File(file) => file,
// If there are many files, tries to get the first one
FormParameterSrcUnion::Files(files) => files.get(0)?.to_string()
};

KeyValue {
enabled: true,
data: (param.key, format!("!!{file}")),
}
},
param_type => {
println!("\t\t\tUnknown Multipart form type \"{param_type}\"");
return None;
}
};

multipart.push(key_value);
}

Some(ContentType::Multipart(multipart))
},
Mode::Urlencoded => {
let form_data = body.urlencoded?;

let mut url_encoded: Vec<KeyValue> = vec![];

for param in form_data {
let value = param.value.unwrap_or(String::new());
let is_disabled = param.disabled.unwrap_or(false);

let key_value = KeyValue {
enabled: !is_disabled,
data: (param.key, value),
};

url_encoded.push(key_value);
}

Some(ContentType::Form(url_encoded))
}
}
}
}
Expand Down Expand Up @@ -326,4 +402,20 @@ fn retrieve_headers(request_class: &RequestClass) -> Option<Vec<KeyValue>> {
}
HeaderUnion::String(_) => None
}
}

fn retrieve_settings(item: &Items) -> Option<RequestSettings> {
let protocol_profile_behavior = item.protocol_profile_behavior.clone()?;

let mut settings = RequestSettings::default();

if let Some(follow_redirects) = protocol_profile_behavior.follow_redirects {
settings.allow_redirects = follow_redirects;
}

if let Some(disable_cookies) = protocol_profile_behavior.disable_cookies {
settings.store_received_cookies = !disable_cookies;
}

Some(settings)
}
2 changes: 1 addition & 1 deletion src/request/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ impl Request {
let mut was_header_found = false;

for header in &mut self.headers {
if &header.data.0 == input_header {
if header.data.0.to_lowercase() == input_header.to_lowercase() {
header.data.1 = value.to_string();
was_header_found = true;
}
Expand Down

0 comments on commit aac381e

Please sign in to comment.