From 3c4a7b3d1e55131650f8cfa9e93ab15d8470f97e Mon Sep 17 00:00:00 2001 From: meshde Date: Mon, 17 Mar 2025 02:59:41 +0400 Subject: [PATCH 1/8] feat: add command to import swagger file --- Cargo.lock | 385 ++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 + src/cli/import/mod.rs | 341 +++++++++++++++++++++++++++++++++++++ src/cli/mod.rs | 3 + src/config.rs | 29 ++-- src/http.rs | 4 +- 6 files changed, 740 insertions(+), 27 deletions(-) create mode 100644 src/cli/import/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 1416138..84b5483 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,7 +232,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -559,7 +559,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -680,6 +680,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "gimli" version = "0.28.1" @@ -698,7 +710,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -719,6 +731,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -762,11 +780,16 @@ dependencies = [ "getopts", "handlebars", "hyper", + "indexmap 1.9.3", "inquire", + "oapi", + "openapiv3", "regex", "reqwest", "serde", "serde_json", + "serde_yaml 0.9.34+deprecated", + "sppparse", "strum", "tempfile", "tokio", @@ -901,6 +924,17 @@ dependencies = [ "tiff", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.6" @@ -908,7 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", ] [[package]] @@ -986,6 +1020,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1097,6 +1137,35 @@ dependencies = [ "libc", ] +[[package]] +name = "oapi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48b622efb326be8045075dcc476b19de84ddb8d50555b2cbd4af8be6b2e5487c" +dependencies = [ + "getset", + "oapi_derive", + "semver", + "serde", + "serde_json", + "sppparse", + "thiserror", +] + +[[package]] +name = "oapi_derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f92b74feb1092d6c09cfda09ceaa6337c73c136d812c121b06bc2e686b1dca" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "objc" version = "0.2.7" @@ -1141,6 +1210,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openapiv3" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b83630305ecc3355e998ddd2f926f98aae8e105eb42652174a58757851ba47" +dependencies = [ + "indexmap 1.9.3", + "serde", + "serde_json", +] + [[package]] name = "openssl" version = "0.10.64" @@ -1164,7 +1244,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1214,6 +1294,30 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "path-absolutize" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-clean" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" + +[[package]] +name = "path-dedot" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" +dependencies = [ + "once_cell", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1251,7 +1355,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1282,7 +1386,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1316,6 +1420,70 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -1334,6 +1502,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1510,6 +1708,25 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", + "serde", +] + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.200" @@ -1527,7 +1744,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1541,6 +1758,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1553,6 +1780,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap 1.9.3", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.2.6", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.10.8" @@ -1631,6 +1883,39 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "sppparse" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac0a3329c1890f641ad6d145fcbfe590268401ec95f96d84940671fbc424f82a" +dependencies = [ + "getset", + "path-absolutize", + "path-clean", + "rand", + "semver", + "serde", + "serde_json", + "serde_path_to_error", + "serde_yaml 0.8.26", + "sppparse_derive", + "thiserror", + "url", +] + +[[package]] +name = "sppparse_derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7889b3dd4968cb7d6a77ef6a3e38141b9a15f9736fb9ef1401d3de09014de4a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1656,7 +1941,18 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.58", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1676,6 +1972,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -1727,7 +2035,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1793,7 +2101,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -1820,6 +2128,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "tower" version = "0.4.13" @@ -1919,6 +2236,18 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "url" version = "2.5.0" @@ -1984,7 +2313,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -2018,7 +2347,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2254,8 +2583,38 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] diff --git a/Cargo.toml b/Cargo.toml index 7ec9e6f..bd40e8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,11 +21,16 @@ flatten-json-object = "0.6.1" getopts = "0.2.21" handlebars = "5.1.2" hyper = "1.3.1" +indexmap = { version = "1.9", features = ["serde"] } inquire = "0.7.5" +oapi = "0.1.2" +openapiv3 = "1.0.1" regex = "1.10.4" reqwest = {version="0.12.3", features=["json"]} serde = {version="1.0.200", features=["derive"]} serde_json = "1.0" +serde_yaml = "0.9" +sppparse = "0.1.4" strum = {version="0.26.2", features=["derive"]} tempfile = "3.12.0" tokio = {version = "1.37.0", features = ["full"]} diff --git a/src/cli/import/mod.rs b/src/cli/import/mod.rs new file mode 100644 index 0000000..8980f3e --- /dev/null +++ b/src/cli/import/mod.rs @@ -0,0 +1,341 @@ +use crate::config::{Command, CommandType, Config}; +use crate::http::HttpMethod; +use clap::{Args, ValueHint}; +use convert_case::{Case, Casing}; +use openapiv3::{ + OpenAPI, Operation, Parameter, PathItem, ReferenceOr, RequestBody, Schema, SchemaKind, Type, +}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::error::Error; +use std::fs; + +#[derive(Args, Debug)] +pub struct ImportArguments { + #[arg(value_hint = ValueHint::FilePath)] + file: String, +} + +pub fn init(args: ImportArguments) -> Result<(), Box> { + // Read the OpenAPI spec from a file + let spec_content = fs::read_to_string(args.file)?; + let spec: OpenAPI = serde_yaml::from_str(&spec_content)?; + + // Generate configuration + let config = generate_config(&spec)?; + + println!("{:?}", config); + + config.save().expect("could not create config file"); + Ok(()) +} + +fn generate_config(spec: &OpenAPI) -> Result> { + let mut config = Config { + envs: HashMap::new(), + commands: HashMap::new(), + }; + // Extract server URL + let api_url = if let Some(server) = spec.servers.first() { + server.url.clone() + } else { + "".to_string() + }; + + // Create environment configuration + config.envs.insert( + "prod".to_string(), + HashMap::from([("API_URL".to_string(), api_url.to_string())]), + ); + + // Group operations by tag + let mut tag_operations: HashMap> = HashMap::new(); + + // Process paths + for (path, path_item) in spec.paths.iter() { + let path_item = match path_item { + ReferenceOr::Reference { .. } => continue, // Skip references for simplicity + ReferenceOr::Item(item) => item, + }; + + // Process operations (GET, POST, PUT, DELETE, etc.) + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.get, + "get", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.post, + "post", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.put, + "put", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.delete, + "delete", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.patch, + "patch", + ); + } + + // Convert grouped operations to commands + for (tag, operations) in tag_operations { + let mut tag_commands = HashMap::new(); + + for (path, _path_item, operation) in operations { + // Derive command name from operationId or path + let command_name = if let Some(op_id) = &operation.operation_id { + // Convert camelCase or PascalCase to kebab-case + op_id.to_case(Case::Kebab) + } else { + // Use path as fallback, cleaned up + let clean_path = path.replace('/', "-").trim_matches('-').to_string(); + clean_path + }; + + tag_commands.insert( + command_name, + Box::new(CommandType::Command(create_command_for_operation( + path, + &operation, + &spec.components, + ))), + ); + } + + if !tag_commands.is_empty() { + // Use the tag name in kebab-case + let tag_key = tag.to_case(Case::Kebab); + config + .commands + .insert(tag_key, Box::new(CommandType::NestedCommand(tag_commands))); + } + } + + Ok(config) +} + +fn process_operation<'a>( + tag_operations: &mut HashMap>, + path: &'a String, + path_item: &'a PathItem, + operation_opt: &'a Option, + _method: &str, +) { + if let Some(operation) = operation_opt { + // Get tag or use "default" if none specified + let section = if let Some(tag) = operation.tags.first() { + tag.clone() + } else { + let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + match segments.get(0) { + Some(&segment) => segment.to_string(), + None => "default".to_string(), + } + }; + + tag_operations + .entry(section) + .or_insert_with(Vec::new) + .push((path, path_item, operation.clone())); + } +} + +fn create_command_for_operation( + path: &str, + operation: &Operation, + components: &Option, +) -> Command { + let method: HttpMethod = match operation.operation_id.as_ref().map(|s| s.to_lowercase()) { + Some(id) if id.starts_with("get") => HttpMethod::GET, + Some(id) if id.starts_with("create") || id.starts_with("add") => HttpMethod::POST, + Some(id) if id.starts_with("update") => HttpMethod::PUT, + Some(id) if id.starts_with("delete") => HttpMethod::DELETE, + Some(id) if id.starts_with("patch") => HttpMethod::PATCH, + _ => { + // Determine method from operation presence in PathItem + // This is simplistic and based on the calling context + if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("get")) + { + HttpMethod::GET + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("create")) + { + HttpMethod::POST + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("update")) + { + HttpMethod::PUT + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("delete")) + { + HttpMethod::DELETE + } else { + // Default to GET if unsure + HttpMethod::GET + } + } + }; + + // Process path parameters + let url = process_path_and_query(path, &operation.parameters); + + // Process request body if present + let body = if let (Some(req_body), Some(components)) = (&operation.request_body, &components) { + extract_request_body(req_body, components) + } else { + None + }; + + Command { + method, + url: format!("{{{{API_URL}}}}{}", url), + body, + postscript: None, + headers: HashMap::new(), + } +} + +fn process_path_and_query(path: &str, parameters: &[ReferenceOr]) -> String { + let mut result = path.to_string(); + let mut query_params = Vec::new(); + + // Process path parameters - convert {param} to :param + for param in parameters { + if let ReferenceOr::Item(param_item) = param { + if let Parameter::Path { parameter_data, .. } = param_item { + let param_name = ¶meter_data.name; + result = + result.replace(&format!("{{{}}}", param_name), &format!(":{}", param_name)); + } else if let Parameter::Query { parameter_data, .. } = param_item { + let param_name = ¶meter_data.name; + query_params.push(format!("{}=:{}", param_name, param_name)); + } + } + } + + // Add query parameters if any + if !query_params.is_empty() { + result = format!("{}?{}", result, query_params.join("&")); + } + + result +} + +fn extract_request_body( + request_body: &ReferenceOr, + components: &openapiv3::Components, +) -> Option { + match request_body { + ReferenceOr::Item(body) => { + // Try to get JSON schema + if let Some(json_content) = body.content.get("application/json") { + return extract_schema(&json_content.schema, components); + } + None + } + ReferenceOr::Reference { reference } => { + // Handle reference to component + let ref_parts: Vec<&str> = reference.split('/').collect(); + if ref_parts.len() == 4 + && ref_parts[1] == "components" + && ref_parts[2] == "requestBodies" + { + let ref_name = ref_parts[3]; + if let Some(ref_body) = components.request_bodies.get(ref_name) { + return extract_request_body(ref_body, components); + } + } + None + } + } +} + +fn extract_schema( + schema_opt: &Option>, + components: &openapiv3::Components, +) -> Option { + if let Some(schema_ref) = schema_opt { + match schema_ref { + ReferenceOr::Item(schema) => process_schema(schema), + ReferenceOr::Reference { reference } => { + // Handle reference to component schema + let ref_parts: Vec<&str> = reference.split('/').collect(); + if ref_parts.len() == 4 && ref_parts[1] == "components" && ref_parts[2] == "schemas" + { + let ref_name = ref_parts[3]; + if let Some(ref_schema) = components.schemas.get(ref_name) { + extract_schema(&Some(ref_schema.clone()), components) + } else { + // Return an empty object as placeholder if schema not found + Some(json!({})) + } + } else { + None + } + } + } + } else { + None + } +} + +fn process_schema(schema: &Schema) -> Option { + match &schema.schema_kind { + SchemaKind::Type(Type::Object(obj)) => { + let mut properties = json!({}); + + for (prop_name, prop_schema) in &obj.properties { + let default_value = match &prop_schema { + ReferenceOr::Item(schema) => match &schema.schema_kind { + SchemaKind::Type(Type::String(_)) => json!(""), + SchemaKind::Type(Type::Number(_)) => json!(0), + SchemaKind::Type(Type::Integer(_)) => json!(0), + SchemaKind::Type(Type::Boolean {}) => json!(false), + SchemaKind::Type(Type::Array(_)) => json!([]), + SchemaKind::Type(Type::Object(_)) => json!({}), + _ => json!(null), + }, + ReferenceOr::Reference { .. } => json!({}), + }; + + if let Some(obj) = properties.as_object_mut() { + obj.insert(prop_name.clone(), default_value); + } + } + + Some(properties) + } + _ => { + // For non-object schemas, return null or a simple default + Some(json!({})) + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 35af329..38e1991 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,5 +1,6 @@ mod env; mod ephenv; +mod import; mod last; mod run; @@ -18,6 +19,7 @@ enum StaticCommand { Ephenv(ephenv::EphenvCommand), #[command(subcommand)] Last(last::LastCommand), + Import(import::ImportArguments), } fn formulate_command( @@ -119,6 +121,7 @@ pub async fn init() -> ExitCode { StaticCommand::Env(args) => env::init(args), StaticCommand::Ephenv(args) => ephenv::init(args), StaticCommand::Last(args) => last::init(args), + StaticCommand::Import(args) => import::init(args), } } }; diff --git a/src/config.rs b/src/config.rs index ea69dea..bf2eb96 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,13 +13,13 @@ use tempfile::NamedTempFile; const CONFIG_DIR: &str = ".hit"; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct PostScriptConfig { pub command: String, pub file: String, } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Debug)] pub struct Command { pub method: http::HttpMethod, pub url: String, @@ -90,23 +90,26 @@ impl Command { } } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] pub struct Config { pub envs: HashMap>, pub commands: HashMap>, } -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Debug)] #[serde(untagged)] pub enum CommandType { Command(Command), NestedCommand(HashMap>), } +fn get_file_path() -> PathBuf { + PathBuf::from(CONFIG_DIR).join("config.json") +} + impl Config { pub fn new() -> Config { - let file_path = PathBuf::from(CONFIG_DIR).join("config.json"); - + let file_path = get_file_path(); // Create the directory if it doesn't exist if let Some(parent_dir) = file_path.parent() { fs::create_dir_all(parent_dir).expect("Failed to create directory"); @@ -114,18 +117,12 @@ impl Config { // Create the file if it doesn't exist if !file_path.exists() { - let mut file = fs::File::create(&file_path).expect("Failed to create file"); let init_config = Config { commands: HashMap::new(), envs: HashMap::new(), }; - file.write_all( - serde_json::to_string_pretty(&init_config) - .unwrap() - .as_bytes(), - ) - .expect("could not save initial config") + init_config.save().expect("could not save initial config") } let file = fs::File::open(file_path).expect("config file missing"); @@ -134,4 +131,10 @@ impl Config { let config: Config = serde_json::from_reader(reader).expect("Error while reading JSON"); return config; } + pub fn save(&self) -> Result<(), Error> { + let file_path = get_file_path(); + let mut file = fs::File::create(&file_path).expect("Failed to create file"); + + file.write_all(serde_json::to_string_pretty(&self).unwrap().as_bytes()) + } } diff --git a/src/http.rs b/src/http.rs index a90d775..337ef08 100644 --- a/src/http.rs +++ b/src/http.rs @@ -3,12 +3,13 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use strum::Display; -#[derive(Display, Deserialize, Serialize, Clone)] +#[derive(Display, Deserialize, Serialize, Clone, Debug)] pub enum HttpMethod { GET, POST, PUT, DELETE, + PATCH, } #[derive(Deserialize, Serialize, Clone)] @@ -31,6 +32,7 @@ pub async fn handle_request( HttpMethod::POST => reqwest::Method::POST, HttpMethod::PUT => reqwest::Method::PUT, HttpMethod::DELETE => reqwest::Method::DELETE, + HttpMethod::PATCH => reqwest::Method::PATCH, }; let request = reqwest::Request::new(method, reqwest::Url::parse(&url).expect("Invalid url")); From 49d43cfc66ca05e4e3620896cbaebc8878beb5f0 Mon Sep 17 00:00:00 2001 From: meshde Date: Mon, 17 Mar 2025 15:09:24 +0400 Subject: [PATCH 2/8] fix: clean operationId command name --- src/cli/import/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/import/mod.rs b/src/cli/import/mod.rs index 8980f3e..8dd6de9 100644 --- a/src/cli/import/mod.rs +++ b/src/cli/import/mod.rs @@ -104,7 +104,10 @@ fn generate_config(spec: &OpenAPI) -> Result> { // Derive command name from operationId or path let command_name = if let Some(op_id) = &operation.operation_id { // Convert camelCase or PascalCase to kebab-case - op_id.to_case(Case::Kebab) + let name = op_id.replace('/', "-").to_case(Case::Kebab); + name.strip_prefix(&(tag.clone() + "-")) + .unwrap_or(&name) + .to_string() } else { // Use path as fallback, cleaned up let clean_path = path.replace('/', "-").trim_matches('-').to_string(); From e0eee6697762b60241fd9a13ebb67646f2f2fe03 Mon Sep 17 00:00:00 2001 From: meshde Date: Thu, 24 Apr 2025 11:44:02 +0530 Subject: [PATCH 3/8] chore: remove unused items --- Cargo.lock | 334 ++---------------------------------------- Cargo.toml | 3 - src/cli/import/mod.rs | 2 - 3 files changed, 12 insertions(+), 327 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 84b5483..5c5094b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,7 +232,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -559,7 +559,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -680,18 +680,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "getset" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3586f256131df87204eb733da72e3d3eb4f343c639f4b7be279ac7c48baeafe" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.58", -] - [[package]] name = "gimli" version = "0.28.1" @@ -780,16 +768,13 @@ dependencies = [ "getopts", "handlebars", "hyper", - "indexmap 1.9.3", "inquire", - "oapi", "openapiv3", "regex", "reqwest", "serde", "serde_json", - "serde_yaml 0.9.34+deprecated", - "sppparse", + "serde_yaml", "strum", "tempfile", "tokio", @@ -1020,12 +1005,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -1137,35 +1116,6 @@ dependencies = [ "libc", ] -[[package]] -name = "oapi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48b622efb326be8045075dcc476b19de84ddb8d50555b2cbd4af8be6b2e5487c" -dependencies = [ - "getset", - "oapi_derive", - "semver", - "serde", - "serde_json", - "sppparse", - "thiserror", -] - -[[package]] -name = "oapi_derive" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f92b74feb1092d6c09cfda09ceaa6337c73c136d812c121b06bc2e686b1dca" -dependencies = [ - "proc-macro-crate", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] - [[package]] name = "objc" version = "0.2.7" @@ -1244,7 +1194,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -1294,30 +1244,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "path-absolutize" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" -dependencies = [ - "path-dedot", -] - -[[package]] -name = "path-clean" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" - -[[package]] -name = "path-dedot" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" -dependencies = [ - "once_cell", -] - [[package]] name = "percent-encoding" version = "2.3.1" @@ -1355,7 +1281,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -1386,7 +1312,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -1420,70 +1346,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "ppv-lite86" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.58", -] - [[package]] name = "proc-macro2" version = "1.0.79" @@ -1502,36 +1364,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -1708,25 +1540,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", - "serde", -] - -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - [[package]] name = "serde" version = "1.0.200" @@ -1744,7 +1557,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -1758,16 +1571,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1780,18 +1583,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap 1.9.3", - "ryu", - "serde", - "yaml-rust", -] - [[package]] name = "serde_yaml" version = "0.9.34+deprecated" @@ -1883,39 +1674,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "sppparse" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0a3329c1890f641ad6d145fcbfe590268401ec95f96d84940671fbc424f82a" -dependencies = [ - "getset", - "path-absolutize", - "path-clean", - "rand", - "semver", - "serde", - "serde_json", - "serde_path_to_error", - "serde_yaml 0.8.26", - "sppparse_derive", - "thiserror", - "url", -] - -[[package]] -name = "sppparse_derive" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7889b3dd4968cb7d6a77ef6a3e38141b9a15f9736fb9ef1401d3de09014de4a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", - "synstructure", -] - [[package]] name = "strsim" version = "0.11.1" @@ -1941,18 +1699,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.58", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "syn", ] [[package]] @@ -1972,18 +1719,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - [[package]] name = "system-configuration" version = "0.5.1" @@ -2035,7 +1770,7 @@ checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -2101,7 +1836,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", ] [[package]] @@ -2128,15 +1863,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "tower" version = "0.4.13" @@ -2236,12 +1962,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -2313,7 +2033,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.58", + "syn", "wasm-bindgen-shared", ] @@ -2347,7 +2067,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2583,38 +2303,8 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "byteorder", - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.58", -] diff --git a/Cargo.toml b/Cargo.toml index bd40e8b..71b08dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,16 +21,13 @@ flatten-json-object = "0.6.1" getopts = "0.2.21" handlebars = "5.1.2" hyper = "1.3.1" -indexmap = { version = "1.9", features = ["serde"] } inquire = "0.7.5" -oapi = "0.1.2" openapiv3 = "1.0.1" regex = "1.10.4" reqwest = {version="0.12.3", features=["json"]} serde = {version="1.0.200", features=["derive"]} serde_json = "1.0" serde_yaml = "0.9" -sppparse = "0.1.4" strum = {version="0.26.2", features=["derive"]} tempfile = "3.12.0" tokio = {version = "1.37.0", features = ["full"]} diff --git a/src/cli/import/mod.rs b/src/cli/import/mod.rs index 8dd6de9..5b66c05 100644 --- a/src/cli/import/mod.rs +++ b/src/cli/import/mod.rs @@ -24,8 +24,6 @@ pub fn init(args: ImportArguments) -> Result<(), Box> { // Generate configuration let config = generate_config(&spec)?; - println!("{:?}", config); - config.save().expect("could not create config file"); Ok(()) } From dfdbe86d485adbaa22a119689b5f95a840e03015 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 May 2025 17:16:05 +0400 Subject: [PATCH 4/8] chore: fix merge issues --- src/cli/import/mod.rs | 5 +++-- src/core/command.rs | 6 ------ src/core/config.rs | 20 ++++++++++++-------- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/cli/import/mod.rs b/src/cli/import/mod.rs index 5b66c05..706b972 100644 --- a/src/cli/import/mod.rs +++ b/src/cli/import/mod.rs @@ -1,5 +1,6 @@ -use crate::config::{Command, CommandType, Config}; -use crate::http::HttpMethod; +use crate::core::command::Command; +use crate::core::config::{CommandType, Config}; +use crate::utils::http::HttpMethod; use clap::{Args, ValueHint}; use convert_case::{Case, Casing}; use openapiv3::{ diff --git a/src/core/command.rs b/src/core/command.rs index 681d759..a8c34a8 100644 --- a/src/core/command.rs +++ b/src/core/command.rs @@ -142,10 +142,4 @@ mod tests { ] ) } - pub fn save(&self) -> Result<(), Error> { - let file_path = get_file_path(); - let mut file = fs::File::create(&file_path).expect("Failed to create file"); - - file.write_all(serde_json::to_string_pretty(&self).unwrap().as_bytes()) - } } diff --git a/src/core/config.rs b/src/core/config.rs index a60b7eb..c3753b8 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -19,9 +19,13 @@ pub enum CommandType { NestedCommand(HashMap>), } +fn get_config_file_path() -> PathBuf { + PathBuf::from(CONFIG_DIR).join("config.json") +} + impl Config { pub fn new() -> Config { - let file_path = PathBuf::from(CONFIG_DIR).join("config.json"); + let file_path = get_config_file_path(); // Create the directory if it doesn't exist if let Some(parent_dir) = file_path.parent() { @@ -30,18 +34,12 @@ impl Config { // Create the file if it doesn't exist if !file_path.exists() { - let mut file = fs::File::create(&file_path).expect("Failed to create file"); let init_config = Config { commands: HashMap::new(), envs: HashMap::new(), }; - file.write_all( - serde_json::to_string_pretty(&init_config) - .unwrap() - .as_bytes(), - ) - .expect("could not save initial config") + init_config.save().expect("could not save initial config") } let file = fs::File::open(file_path).expect("config file missing"); @@ -50,4 +48,10 @@ impl Config { let config: Config = serde_json::from_reader(reader).expect("Error while reading JSON"); return config; } + pub fn save(&self) -> Result<(), std::io::Error> { + let file_path = get_config_file_path(); + let mut file = fs::File::create(&file_path).expect("Failed to create file"); + + file.write_all(serde_json::to_string_pretty(&self).unwrap().as_bytes()) + } } From 00bc01bae60e75244530ec3c034cf51d54cbbb31 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 May 2025 18:34:20 +0400 Subject: [PATCH 5/8] chore: add tests for swagger import --- Cargo.lock | 37 + Cargo.toml | 1 + tests/fixtures/mod.rs | 12 +- tests/fixtures/swagger.yml | 806 ++++++++++++++++++ tests/import_tests.rs | 25 + .../import_tests__import_swagger.snap | 191 +++++ 6 files changed, 1068 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/swagger.yml create mode 100644 tests/import_tests.rs create mode 100644 tests/snapshots/import_tests__import_swagger.snap diff --git a/Cargo.lock b/Cargo.lock index a2af355..015b9aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -322,6 +322,18 @@ version = "0.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688226b9769bbf11a9d82a94fb4adda15793a6023d33a8ca7695dbafec6f4123" +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -502,6 +514,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -819,6 +837,7 @@ dependencies = [ "handlebars", "hyper", "inquire", + "insta", "openapiv3", "predicates", "regex", @@ -1144,6 +1163,18 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "insta" +version = "1.43.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371" +dependencies = [ + "console", + "once_cell", + "serde", + "similar", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2035,6 +2066,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.9" diff --git a/Cargo.toml b/Cargo.toml index 9af99b7..353b9b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,5 +34,6 @@ tokio = {version = "1.37.0", features = ["full"]} [dev-dependencies] assert_cmd = "2.0.17" +insta = {version="1.43.1", features=["json"]} predicates = "3.1.3" rstest = "0.25.0" diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs index 2207672..f9bf17c 100644 --- a/tests/fixtures/mod.rs +++ b/tests/fixtures/mod.rs @@ -10,8 +10,7 @@ pub struct SetupFixture { } impl SetupFixture { - pub fn new() -> Self { - let temp_dir = TempDir::new_in(".").unwrap(); + pub fn new(temp_dir: TempDir) -> Self { let config_path = temp_dir.path().join(".hit").join("config.json"); fs::create_dir_all(config_path.parent().unwrap()).unwrap(); @@ -38,8 +37,13 @@ impl SetupFixture { } #[fixture] -pub fn hit_setup() -> SetupFixture { - SetupFixture::new() +pub fn temp_dir() -> TempDir { + TempDir::new_in(".").unwrap() +} + +#[fixture] +pub fn hit_setup(temp_dir: TempDir) -> SetupFixture { + SetupFixture::new(temp_dir) } pub fn get_hit_command_for_dir(dir: &std::path::Path) -> Command { diff --git a/tests/fixtures/swagger.yml b/tests/fixtures/swagger.yml new file mode 100644 index 0000000..f4b920a --- /dev/null +++ b/tests/fixtures/swagger.yml @@ -0,0 +1,806 @@ +openapi: 3.0.3 +info: + title: Swagger Petstore - OpenAPI 3.0 + description: |- + This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about + Swagger at [https://swagger.io](https://swagger.io). In the third iteration of the pet store, we've switched to the design first approach! + You can now help us improve the API whether it's by making changes to the definition itself or to the code. + That way, with time, we can improve the API in general, and expose some of the new features in OAS3. + + _If you're looking for the Swagger 2.0/OAS 2.0 version of Petstore, then click [here](https://editor.swagger.io/?url=https://petstore.swagger.io/v2/swagger.yaml). Alternatively, you can load via the `Edit > Load Petstore OAS 2.0` menu option!_ + + Some useful links: + - [The Pet Store repository](https://github.com/swagger-api/swagger-petstore) + - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) + termsOfService: http://swagger.io/terms/ + contact: + email: apiteam@swagger.io + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.11 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: https://petstore3.swagger.io/api/v3 +tags: + - name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io + - name: store + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io + - name: user + description: Operations about user +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + '422': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Add a new pet to the store + description: Add a new pet to the store + operationId: addPet + requestBody: + description: Create a new pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid input + '422': + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByStatus: + get: + tags: + - pet + summary: Finds Pets by status + description: Multiple status values can be provided with comma separated strings + operationId: findPetsByStatus + parameters: + - name: status + in: query + description: Status values that need to be considered for filter + required: false + explode: true + schema: + type: string + default: available + enum: + - available + - pending + - sold + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/findByTags: + get: + tags: + - pet + summary: Finds Pets by tags + description: Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: false + explode: true + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}: + get: + tags: + - pet + summary: Find pet by ID + description: Returns a single pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + security: + - api_key: [] + - petstore_auth: + - write:pets + - read:pets + post: + tags: + - pet + summary: Updates a pet in the store with form data + description: '' + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + - name: name + in: query + description: Name of pet that needs to be updated + schema: + type: string + - name: status + in: query + description: Status of pet that needs to be updated + schema: + type: string + responses: + '400': + description: Invalid input + security: + - petstore_auth: + - write:pets + - read:pets + delete: + tags: + - pet + summary: Deletes a pet + description: delete a pet + operationId: deletePet + parameters: + - name: api_key + in: header + description: '' + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + security: + - petstore_auth: + - write:pets + - read:pets + /pet/{petId}/uploadImage: + post: + tags: + - pet + summary: uploads an image + description: '' + operationId: uploadFile + parameters: + - name: petId + in: path + description: ID of pet to update + required: true + schema: + type: integer + format: int64 + - name: additionalMetadata + in: query + description: Additional Metadata + required: false + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/ApiResponse' + security: + - petstore_auth: + - write:pets + - read:pets + /store/inventory: + get: + tags: + - store + summary: Returns pet inventories by status + description: Returns a map of status codes to quantities + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + security: + - api_key: [] + /store/order: + post: + tags: + - store + summary: Place an order for a pet + description: Place a new order in the store + operationId: placeOrder + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Order' + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid input + '422': + description: Validation exception + /store/order/{orderId}: + get: + tags: + - store + summary: Find purchase order by ID + description: For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + operationId: getOrderById + parameters: + - name: orderId + in: path + description: ID of order that needs to be fetched + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + application/xml: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid ID supplied + '404': + description: Order not found + delete: + tags: + - store + summary: Delete purchase order by ID + description: For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + operationId: deleteOrder + parameters: + - name: orderId + in: path + description: ID of the order that needs to be deleted + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid ID supplied + '404': + description: Order not found + /user: + post: + tags: + - user + summary: Create user + description: This can only be done by the logged in user. + operationId: createUser + requestBody: + description: Created user object + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + /user/createWithList: + post: + tags: + - user + summary: Creates list of users with given input array + description: Creates list of users with given input array + operationId: createUsersWithListInput + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + default: + description: successful operation + /user/login: + get: + tags: + - user + summary: Logs user into the system + description: '' + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: false + schema: + type: string + - name: password + in: query + description: The password for login in clear text + required: false + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + X-Expires-After: + description: date in UTC when token expires + schema: + type: string + format: date-time + content: + application/xml: + schema: + type: string + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + summary: Logs out current logged in user session + description: '' + operationId: logoutUser + parameters: [] + responses: + default: + description: successful operation + /user/{username}: + get: + tags: + - user + summary: Get user by user name + description: '' + operationId: getUserByName + parameters: + - name: username + in: path + description: 'The name that needs to be fetched. Use user1 for testing. ' + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + '400': + description: Invalid username supplied + '404': + description: User not found + put: + tags: + - user + summary: Update user + description: This can only be done by the logged in user. + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + requestBody: + description: Update an existent user in the store + content: + application/json: + schema: + $ref: '#/components/schemas/User' + application/xml: + schema: + $ref: '#/components/schemas/User' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/User' + responses: + default: + description: successful operation + delete: + tags: + - user + summary: Delete user + description: This can only be done by the logged in user. + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid username supplied + '404': + description: User not found +components: + schemas: + Order: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + petId: + type: integer + format: int64 + example: 198772 + quantity: + type: integer + format: int32 + example: 7 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + example: approved + enum: + - placed + - approved + - delivered + complete: + type: boolean + xml: + name: order + Customer: + type: object + properties: + id: + type: integer + format: int64 + example: 100000 + username: + type: string + example: fehguy + address: + type: array + xml: + name: addresses + wrapped: true + items: + $ref: '#/components/schemas/Address' + xml: + name: customer + Address: + type: object + properties: + street: + type: string + example: 437 Lytton + city: + type: string + example: Palo Alto + state: + type: string + example: CA + zip: + type: string + example: '94301' + xml: + name: address + Category: + type: object + properties: + id: + type: integer + format: int64 + example: 1 + name: + type: string + example: Dogs + xml: + name: category + User: + type: object + properties: + id: + type: integer + format: int64 + example: 10 + username: + type: string + example: theUser + firstName: + type: string + example: John + lastName: + type: string + example: James + email: + type: string + example: john@email.com + password: + type: string + example: '12345' + phone: + type: string + example: '12345' + userStatus: + type: integer + description: User Status + format: int32 + example: 1 + xml: + name: user + Tag: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + xml: + name: tag + Pet: + required: + - name + - photoUrls + type: object + properties: + id: + type: integer + format: int64 + example: 10 + name: + type: string + example: doggie + category: + $ref: '#/components/schemas/Category' + photoUrls: + type: array + xml: + wrapped: true + items: + type: string + xml: + name: photoUrl + tags: + type: array + xml: + wrapped: true + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + enum: + - available + - pending + - sold + xml: + name: pet + ApiResponse: + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + xml: + name: '##default' + requestBodies: + Pet: + description: Pet object that needs to be added to the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + UserArray: + description: List of user object + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: https://petstore3.swagger.io/oauth/authorize + scopes: + write:pets: modify pets in your account + read:pets: read your pets + api_key: + type: apiKey + name: api_key + in: header diff --git a/tests/import_tests.rs b/tests/import_tests.rs new file mode 100644 index 0000000..87d9971 --- /dev/null +++ b/tests/import_tests.rs @@ -0,0 +1,25 @@ +mod fixtures; +use assert_cmd::prelude::*; +use fixtures::{get_hit_command_for_dir, temp_dir}; +use rstest::*; +use std::fs; +use std::path::PathBuf; +use tempfile::TempDir; + +#[rstest] +fn test_import_swagger(temp_dir: TempDir) { + fs::copy( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/swagger.yml"), + temp_dir.path().join("swagger.yml"), + ) + .unwrap(); + + let mut cmd = get_hit_command_for_dir(&temp_dir.path()); + cmd.args(["import", "./swagger.yml"]); + cmd.assert().success(); + + let config_path = temp_dir.path().join(".hit").join("config.json"); + let reader = fs::File::open(config_path).unwrap(); + let hit_config: serde_json::Value = serde_json::from_reader(reader).unwrap(); + insta::assert_json_snapshot!(hit_config); +} diff --git a/tests/snapshots/import_tests__import_swagger.snap b/tests/snapshots/import_tests__import_swagger.snap new file mode 100644 index 0000000..3a2185c --- /dev/null +++ b/tests/snapshots/import_tests__import_swagger.snap @@ -0,0 +1,191 @@ +--- +source: tests/import_tests.rs +expression: hit_config +--- +{ + "commands": { + "pet": { + "add-pet": { + "body": { + "category": {}, + "id": 0, + "name": "", + "photoUrls": [], + "status": "", + "tags": [] + }, + "headers": {}, + "method": "POST", + "postscript": null, + "url": "{{API_URL}}/pet" + }, + "delete-pet": { + "body": null, + "headers": {}, + "method": "DELETE", + "postscript": null, + "url": "{{API_URL}}/pet/:petId" + }, + "find-pets-by-status": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/pet/findByStatus?status=:status" + }, + "find-pets-by-tags": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/pet/findByTags?tags=:tags" + }, + "get-pet-by-id": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/pet/:petId" + }, + "update-pet": { + "body": { + "category": {}, + "id": 0, + "name": "", + "photoUrls": [], + "status": "", + "tags": [] + }, + "headers": {}, + "method": "PUT", + "postscript": null, + "url": "{{API_URL}}/pet" + }, + "update-pet-with-form": { + "body": null, + "headers": {}, + "method": "PUT", + "postscript": null, + "url": "{{API_URL}}/pet/:petId?name=:name&status=:status" + }, + "upload-file": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/pet/:petId/uploadImage?additionalMetadata=:additionalMetadata" + } + }, + "store": { + "delete-order": { + "body": null, + "headers": {}, + "method": "DELETE", + "postscript": null, + "url": "{{API_URL}}/store/order/:orderId" + }, + "get-inventory": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/store/inventory" + }, + "get-order-by-id": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/store/order/:orderId" + }, + "place-order": { + "body": { + "complete": false, + "id": 0, + "petId": 0, + "quantity": 0, + "shipDate": "", + "status": "" + }, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/store/order" + } + }, + "user": { + "create-user": { + "body": { + "email": "", + "firstName": "", + "id": 0, + "lastName": "", + "password": "", + "phone": "", + "userStatus": 0, + "username": "" + }, + "headers": {}, + "method": "POST", + "postscript": null, + "url": "{{API_URL}}/user" + }, + "create-users-with-list-input": { + "body": {}, + "headers": {}, + "method": "POST", + "postscript": null, + "url": "{{API_URL}}/user/createWithList" + }, + "delete-user": { + "body": null, + "headers": {}, + "method": "DELETE", + "postscript": null, + "url": "{{API_URL}}/user/:username" + }, + "get-user-by-name": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/user/:username" + }, + "login-user": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/user/login?username=:username&password=:password" + }, + "logout-user": { + "body": null, + "headers": {}, + "method": "GET", + "postscript": null, + "url": "{{API_URL}}/user/logout" + }, + "update-user": { + "body": { + "email": "", + "firstName": "", + "id": 0, + "lastName": "", + "password": "", + "phone": "", + "userStatus": 0, + "username": "" + }, + "headers": {}, + "method": "PUT", + "postscript": null, + "url": "{{API_URL}}/user/:username" + } + } + }, + "envs": { + "prod": { + "API_URL": "https://petstore3.swagger.io/api/v3" + } + } +} From ccc9b8b32d07b8ca803b3e21824cefdea6c12a45 Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 May 2025 18:35:00 +0400 Subject: [PATCH 6/8] chore: bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 015b9aa..e654fec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -818,7 +818,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hit-cli" -version = "0.4.3" +version = "0.5.0" dependencies = [ "arboard", "array_tool", diff --git a/Cargo.toml b/Cargo.toml index 353b9b1..966ee01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hit-cli" -version = "0.4.3" +version = "0.5.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 67800be57af17da33d41df1714800a0add26fe1a Mon Sep 17 00:00:00 2001 From: meshde Date: Sun, 4 May 2025 18:41:57 +0400 Subject: [PATCH 7/8] chore: extract openapi functions into core directory --- src/cli/import/mod.rs | 325 +----------------------------------------- src/core/mod.rs | 1 + src/core/openapi.rs | 323 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 326 insertions(+), 323 deletions(-) create mode 100644 src/core/openapi.rs diff --git a/src/cli/import/mod.rs b/src/cli/import/mod.rs index 706b972..36dd48c 100644 --- a/src/cli/import/mod.rs +++ b/src/cli/import/mod.rs @@ -1,14 +1,6 @@ -use crate::core::command::Command; -use crate::core::config::{CommandType, Config}; -use crate::utils::http::HttpMethod; +use crate::core::openapi::generate_config; use clap::{Args, ValueHint}; -use convert_case::{Case, Casing}; -use openapiv3::{ - OpenAPI, Operation, Parameter, PathItem, ReferenceOr, RequestBody, Schema, SchemaKind, Type, -}; -use serde_json::{json, Value}; -use std::collections::HashMap; -use std::error::Error; +use openapiv3::OpenAPI; use std::fs; #[derive(Args, Debug)] @@ -28,316 +20,3 @@ pub fn init(args: ImportArguments) -> Result<(), Box> { config.save().expect("could not create config file"); Ok(()) } - -fn generate_config(spec: &OpenAPI) -> Result> { - let mut config = Config { - envs: HashMap::new(), - commands: HashMap::new(), - }; - // Extract server URL - let api_url = if let Some(server) = spec.servers.first() { - server.url.clone() - } else { - "".to_string() - }; - - // Create environment configuration - config.envs.insert( - "prod".to_string(), - HashMap::from([("API_URL".to_string(), api_url.to_string())]), - ); - - // Group operations by tag - let mut tag_operations: HashMap> = HashMap::new(); - - // Process paths - for (path, path_item) in spec.paths.iter() { - let path_item = match path_item { - ReferenceOr::Reference { .. } => continue, // Skip references for simplicity - ReferenceOr::Item(item) => item, - }; - - // Process operations (GET, POST, PUT, DELETE, etc.) - process_operation( - &mut tag_operations, - &path, - &path_item, - &path_item.get, - "get", - ); - process_operation( - &mut tag_operations, - &path, - &path_item, - &path_item.post, - "post", - ); - process_operation( - &mut tag_operations, - &path, - &path_item, - &path_item.put, - "put", - ); - process_operation( - &mut tag_operations, - &path, - &path_item, - &path_item.delete, - "delete", - ); - process_operation( - &mut tag_operations, - &path, - &path_item, - &path_item.patch, - "patch", - ); - } - - // Convert grouped operations to commands - for (tag, operations) in tag_operations { - let mut tag_commands = HashMap::new(); - - for (path, _path_item, operation) in operations { - // Derive command name from operationId or path - let command_name = if let Some(op_id) = &operation.operation_id { - // Convert camelCase or PascalCase to kebab-case - let name = op_id.replace('/', "-").to_case(Case::Kebab); - name.strip_prefix(&(tag.clone() + "-")) - .unwrap_or(&name) - .to_string() - } else { - // Use path as fallback, cleaned up - let clean_path = path.replace('/', "-").trim_matches('-').to_string(); - clean_path - }; - - tag_commands.insert( - command_name, - Box::new(CommandType::Command(create_command_for_operation( - path, - &operation, - &spec.components, - ))), - ); - } - - if !tag_commands.is_empty() { - // Use the tag name in kebab-case - let tag_key = tag.to_case(Case::Kebab); - config - .commands - .insert(tag_key, Box::new(CommandType::NestedCommand(tag_commands))); - } - } - - Ok(config) -} - -fn process_operation<'a>( - tag_operations: &mut HashMap>, - path: &'a String, - path_item: &'a PathItem, - operation_opt: &'a Option, - _method: &str, -) { - if let Some(operation) = operation_opt { - // Get tag or use "default" if none specified - let section = if let Some(tag) = operation.tags.first() { - tag.clone() - } else { - let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); - match segments.get(0) { - Some(&segment) => segment.to_string(), - None => "default".to_string(), - } - }; - - tag_operations - .entry(section) - .or_insert_with(Vec::new) - .push((path, path_item, operation.clone())); - } -} - -fn create_command_for_operation( - path: &str, - operation: &Operation, - components: &Option, -) -> Command { - let method: HttpMethod = match operation.operation_id.as_ref().map(|s| s.to_lowercase()) { - Some(id) if id.starts_with("get") => HttpMethod::GET, - Some(id) if id.starts_with("create") || id.starts_with("add") => HttpMethod::POST, - Some(id) if id.starts_with("update") => HttpMethod::PUT, - Some(id) if id.starts_with("delete") => HttpMethod::DELETE, - Some(id) if id.starts_with("patch") => HttpMethod::PATCH, - _ => { - // Determine method from operation presence in PathItem - // This is simplistic and based on the calling context - if operation - .operation_id - .as_ref() - .map_or(false, |id| id.starts_with("get")) - { - HttpMethod::GET - } else if operation - .operation_id - .as_ref() - .map_or(false, |id| id.starts_with("create")) - { - HttpMethod::POST - } else if operation - .operation_id - .as_ref() - .map_or(false, |id| id.starts_with("update")) - { - HttpMethod::PUT - } else if operation - .operation_id - .as_ref() - .map_or(false, |id| id.starts_with("delete")) - { - HttpMethod::DELETE - } else { - // Default to GET if unsure - HttpMethod::GET - } - } - }; - - // Process path parameters - let url = process_path_and_query(path, &operation.parameters); - - // Process request body if present - let body = if let (Some(req_body), Some(components)) = (&operation.request_body, &components) { - extract_request_body(req_body, components) - } else { - None - }; - - Command { - method, - url: format!("{{{{API_URL}}}}{}", url), - body, - postscript: None, - headers: HashMap::new(), - } -} - -fn process_path_and_query(path: &str, parameters: &[ReferenceOr]) -> String { - let mut result = path.to_string(); - let mut query_params = Vec::new(); - - // Process path parameters - convert {param} to :param - for param in parameters { - if let ReferenceOr::Item(param_item) = param { - if let Parameter::Path { parameter_data, .. } = param_item { - let param_name = ¶meter_data.name; - result = - result.replace(&format!("{{{}}}", param_name), &format!(":{}", param_name)); - } else if let Parameter::Query { parameter_data, .. } = param_item { - let param_name = ¶meter_data.name; - query_params.push(format!("{}=:{}", param_name, param_name)); - } - } - } - - // Add query parameters if any - if !query_params.is_empty() { - result = format!("{}?{}", result, query_params.join("&")); - } - - result -} - -fn extract_request_body( - request_body: &ReferenceOr, - components: &openapiv3::Components, -) -> Option { - match request_body { - ReferenceOr::Item(body) => { - // Try to get JSON schema - if let Some(json_content) = body.content.get("application/json") { - return extract_schema(&json_content.schema, components); - } - None - } - ReferenceOr::Reference { reference } => { - // Handle reference to component - let ref_parts: Vec<&str> = reference.split('/').collect(); - if ref_parts.len() == 4 - && ref_parts[1] == "components" - && ref_parts[2] == "requestBodies" - { - let ref_name = ref_parts[3]; - if let Some(ref_body) = components.request_bodies.get(ref_name) { - return extract_request_body(ref_body, components); - } - } - None - } - } -} - -fn extract_schema( - schema_opt: &Option>, - components: &openapiv3::Components, -) -> Option { - if let Some(schema_ref) = schema_opt { - match schema_ref { - ReferenceOr::Item(schema) => process_schema(schema), - ReferenceOr::Reference { reference } => { - // Handle reference to component schema - let ref_parts: Vec<&str> = reference.split('/').collect(); - if ref_parts.len() == 4 && ref_parts[1] == "components" && ref_parts[2] == "schemas" - { - let ref_name = ref_parts[3]; - if let Some(ref_schema) = components.schemas.get(ref_name) { - extract_schema(&Some(ref_schema.clone()), components) - } else { - // Return an empty object as placeholder if schema not found - Some(json!({})) - } - } else { - None - } - } - } - } else { - None - } -} - -fn process_schema(schema: &Schema) -> Option { - match &schema.schema_kind { - SchemaKind::Type(Type::Object(obj)) => { - let mut properties = json!({}); - - for (prop_name, prop_schema) in &obj.properties { - let default_value = match &prop_schema { - ReferenceOr::Item(schema) => match &schema.schema_kind { - SchemaKind::Type(Type::String(_)) => json!(""), - SchemaKind::Type(Type::Number(_)) => json!(0), - SchemaKind::Type(Type::Integer(_)) => json!(0), - SchemaKind::Type(Type::Boolean {}) => json!(false), - SchemaKind::Type(Type::Array(_)) => json!([]), - SchemaKind::Type(Type::Object(_)) => json!({}), - _ => json!(null), - }, - ReferenceOr::Reference { .. } => json!({}), - }; - - if let Some(obj) = properties.as_object_mut() { - obj.insert(prop_name.clone(), default_value); - } - } - - Some(properties) - } - _ => { - // For non-object schemas, return null or a simple default - Some(json!({})) - } - } -} diff --git a/src/core/mod.rs b/src/core/mod.rs index 1b46861..8deaefe 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -3,3 +3,4 @@ pub mod command; pub mod config; pub mod env; pub mod ephenv; +pub mod openapi; diff --git a/src/core/openapi.rs b/src/core/openapi.rs new file mode 100644 index 0000000..6111121 --- /dev/null +++ b/src/core/openapi.rs @@ -0,0 +1,323 @@ +use crate::core::command::Command; +use crate::core::config::{CommandType, Config}; +use crate::utils::http::HttpMethod; +use convert_case::{Case, Casing}; +use openapiv3::{ + OpenAPI, Operation, Parameter, PathItem, ReferenceOr, RequestBody, Schema, SchemaKind, Type, +}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::error::Error; + +pub fn generate_config(spec: &OpenAPI) -> Result> { + let mut config = Config { + envs: HashMap::new(), + commands: HashMap::new(), + }; + // Extract server URL + let api_url = if let Some(server) = spec.servers.first() { + server.url.clone() + } else { + "".to_string() + }; + + // Create environment configuration + config.envs.insert( + "prod".to_string(), + HashMap::from([("API_URL".to_string(), api_url.to_string())]), + ); + + // Group operations by tag + let mut tag_operations: HashMap> = HashMap::new(); + + // Process paths + for (path, path_item) in spec.paths.iter() { + let path_item = match path_item { + ReferenceOr::Reference { .. } => continue, // Skip references for simplicity + ReferenceOr::Item(item) => item, + }; + + // Process operations (GET, POST, PUT, DELETE, etc.) + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.get, + "get", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.post, + "post", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.put, + "put", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.delete, + "delete", + ); + process_operation( + &mut tag_operations, + &path, + &path_item, + &path_item.patch, + "patch", + ); + } + + // Convert grouped operations to commands + for (tag, operations) in tag_operations { + let mut tag_commands = HashMap::new(); + + for (path, _path_item, operation) in operations { + // Derive command name from operationId or path + let command_name = if let Some(op_id) = &operation.operation_id { + // Convert camelCase or PascalCase to kebab-case + let name = op_id.replace('/', "-").to_case(Case::Kebab); + name.strip_prefix(&(tag.clone() + "-")) + .unwrap_or(&name) + .to_string() + } else { + // Use path as fallback, cleaned up + let clean_path = path.replace('/', "-").trim_matches('-').to_string(); + clean_path + }; + + tag_commands.insert( + command_name, + Box::new(CommandType::Command(create_command_for_operation( + path, + &operation, + &spec.components, + ))), + ); + } + + if !tag_commands.is_empty() { + // Use the tag name in kebab-case + let tag_key = tag.to_case(Case::Kebab); + config + .commands + .insert(tag_key, Box::new(CommandType::NestedCommand(tag_commands))); + } + } + + Ok(config) +} + +fn process_operation<'a>( + tag_operations: &mut HashMap>, + path: &'a String, + path_item: &'a PathItem, + operation_opt: &'a Option, + _method: &str, +) { + if let Some(operation) = operation_opt { + // Get tag or use "default" if none specified + let section = if let Some(tag) = operation.tags.first() { + tag.clone() + } else { + let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); + match segments.get(0) { + Some(&segment) => segment.to_string(), + None => "default".to_string(), + } + }; + + tag_operations + .entry(section) + .or_insert_with(Vec::new) + .push((path, path_item, operation.clone())); + } +} + +fn create_command_for_operation( + path: &str, + operation: &Operation, + components: &Option, +) -> Command { + let method: HttpMethod = match operation.operation_id.as_ref().map(|s| s.to_lowercase()) { + Some(id) if id.starts_with("get") => HttpMethod::GET, + Some(id) if id.starts_with("create") || id.starts_with("add") => HttpMethod::POST, + Some(id) if id.starts_with("update") => HttpMethod::PUT, + Some(id) if id.starts_with("delete") => HttpMethod::DELETE, + Some(id) if id.starts_with("patch") => HttpMethod::PATCH, + _ => { + // Determine method from operation presence in PathItem + // This is simplistic and based on the calling context + if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("get")) + { + HttpMethod::GET + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("create")) + { + HttpMethod::POST + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("update")) + { + HttpMethod::PUT + } else if operation + .operation_id + .as_ref() + .map_or(false, |id| id.starts_with("delete")) + { + HttpMethod::DELETE + } else { + // Default to GET if unsure + HttpMethod::GET + } + } + }; + + // Process path parameters + let url = process_path_and_query(path, &operation.parameters); + + // Process request body if present + let body = if let (Some(req_body), Some(components)) = (&operation.request_body, &components) { + extract_request_body(req_body, components) + } else { + None + }; + + Command { + method, + url: format!("{{{{API_URL}}}}{}", url), + body, + postscript: None, + headers: HashMap::new(), + } +} + +fn process_path_and_query(path: &str, parameters: &[ReferenceOr]) -> String { + let mut result = path.to_string(); + let mut query_params = Vec::new(); + + // Process path parameters - convert {param} to :param + for param in parameters { + if let ReferenceOr::Item(param_item) = param { + if let Parameter::Path { parameter_data, .. } = param_item { + let param_name = ¶meter_data.name; + result = + result.replace(&format!("{{{}}}", param_name), &format!(":{}", param_name)); + } else if let Parameter::Query { parameter_data, .. } = param_item { + let param_name = ¶meter_data.name; + query_params.push(format!("{}=:{}", param_name, param_name)); + } + } + } + + // Add query parameters if any + if !query_params.is_empty() { + result = format!("{}?{}", result, query_params.join("&")); + } + + result +} + +fn extract_request_body( + request_body: &ReferenceOr, + components: &openapiv3::Components, +) -> Option { + match request_body { + ReferenceOr::Item(body) => { + // Try to get JSON schema + if let Some(json_content) = body.content.get("application/json") { + return extract_schema(&json_content.schema, components); + } + None + } + ReferenceOr::Reference { reference } => { + // Handle reference to component + let ref_parts: Vec<&str> = reference.split('/').collect(); + if ref_parts.len() == 4 + && ref_parts[1] == "components" + && ref_parts[2] == "requestBodies" + { + let ref_name = ref_parts[3]; + if let Some(ref_body) = components.request_bodies.get(ref_name) { + return extract_request_body(ref_body, components); + } + } + None + } + } +} + +fn extract_schema( + schema_opt: &Option>, + components: &openapiv3::Components, +) -> Option { + if let Some(schema_ref) = schema_opt { + match schema_ref { + ReferenceOr::Item(schema) => process_schema(schema), + ReferenceOr::Reference { reference } => { + // Handle reference to component schema + let ref_parts: Vec<&str> = reference.split('/').collect(); + if ref_parts.len() == 4 && ref_parts[1] == "components" && ref_parts[2] == "schemas" + { + let ref_name = ref_parts[3]; + if let Some(ref_schema) = components.schemas.get(ref_name) { + extract_schema(&Some(ref_schema.clone()), components) + } else { + // Return an empty object as placeholder if schema not found + Some(json!({})) + } + } else { + None + } + } + } + } else { + None + } +} + +fn process_schema(schema: &Schema) -> Option { + match &schema.schema_kind { + SchemaKind::Type(Type::Object(obj)) => { + let mut properties = json!({}); + + for (prop_name, prop_schema) in &obj.properties { + let default_value = match &prop_schema { + ReferenceOr::Item(schema) => match &schema.schema_kind { + SchemaKind::Type(Type::String(_)) => json!(""), + SchemaKind::Type(Type::Number(_)) => json!(0), + SchemaKind::Type(Type::Integer(_)) => json!(0), + SchemaKind::Type(Type::Boolean {}) => json!(false), + SchemaKind::Type(Type::Array(_)) => json!([]), + SchemaKind::Type(Type::Object(_)) => json!({}), + _ => json!(null), + }, + ReferenceOr::Reference { .. } => json!({}), + }; + + if let Some(obj) = properties.as_object_mut() { + obj.insert(prop_name.clone(), default_value); + } + } + + Some(properties) + } + _ => { + // For non-object schemas, return null or a simple default + Some(json!({})) + } + } +} From 02565287ad7c7984b494d1a7239d814ebbf4692a Mon Sep 17 00:00:00 2001 From: meshde Date: Wed, 7 May 2025 08:07:15 +0400 Subject: [PATCH 8/8] chore(.github): remove verbosity from test workflow --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 000bb2c..c653f90 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,4 +19,4 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose + run: cargo test