diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f4552f..857b2ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -6,7 +6,7 @@ on: branches: - main schedule: - - cron: '00 00 * * *' + - cron: "00 00 * * *" jobs: test: @@ -43,7 +43,7 @@ jobs: rust: nightly - build: msrv os: ubuntu-latest - rust: 1.64.0 + rust: 1.65.0 - build: stable os: ubuntu-latest rust: stable diff --git a/.github/workflows/website.yaml b/.github/workflows/website.yaml index 8f91ba6..92a117d 100644 --- a/.github/workflows/website.yaml +++ b/.github/workflows/website.yaml @@ -12,7 +12,7 @@ permissions: id-token: write concurrency: - group: 'pages' + group: "pages" cancel-in-progress: true jobs: @@ -26,7 +26,7 @@ jobs: uses: ructions/toolchain@v2.0.0 with: profile: minimal - toolchain: 1.64.0 + toolchain: 1.65.0 override: true - name: Use Node.js @@ -34,11 +34,21 @@ jobs: with: node-version: 20.x - - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Install wasm-bindgen + run: cargo install wasm-bindgen-cli - - name: Build WebAssembly module - run: wasm-pack build --release --out-dir ./web/wasm --target bundler + - name: Install wasm-snip + run: cargo install wasm-snip + + - name: Install binaryen + run: apt-ge update && apt-get -y install binaryen + + - name: Build and optimize WebAssembly module + run: | + cargo build --target wasm32-unknown-unknown --release && \ + wasm-bindgen --out-dir ./js ./target/wasm32-unknown-unknown/release/postman2openapi.wasm && \ + wasm-snip --snip-rust-panicking-code -o ./js/postman2openapi_bg.wasm ./js/postman2openapi_bg.wasm && \ + wasm-opt -Oz -o ./js/postman2openapi_bg.wasm ./js/postman2openapi_bg.wasm - name: Install dependencies run: npm install --prefix ./web diff --git a/.gitignore b/.gitignore index 4aa11f8..4f70d5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ -/nodejs /target +/js/postman2openapi_bg.js +/js/postman2openapi_bg.wasm +/js/postman2openapi_bg.wasm.d.ts +/js/postman2openapi.js +/js/postman2openapi.d.ts /web/dist /web/node_modules -/web/wasm diff --git a/Cargo.lock b/Cargo.lock index e1a5b02..f9bfeef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "aho-corasick" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" -dependencies = [ - "memchr", -] - [[package]] name = "android-tzdata" version = "0.1.1" @@ -57,9 +48,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" @@ -84,9 +75,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", @@ -139,9 +130,9 @@ checksum = "fb4a24b1aaf0fd0ce8b45161144d6f42cd91677fd5940fd431183eb023b3a2b8" [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "equivalent" @@ -149,19 +140,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "gloo-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -170,9 +148,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" @@ -185,16 +163,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -219,12 +197,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.3", ] [[package]] @@ -235,9 +213,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -250,9 +228,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "log" @@ -260,12 +238,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "memchr" -version = "2.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" - [[package]] name = "memory_units" version = "0.4.0" @@ -274,9 +246,9 @@ checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -289,9 +261,9 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "os_str_bytes" -version = "6.5.1" +version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "postman2openapi" @@ -300,14 +272,10 @@ dependencies = [ "anyhow", "console_error_panic_hook", "convert_case", - "gloo-utils", "indexmap 1.9.3", "js-sys", - "lazy_static", - "regex", - "semver", "serde", - "serde_derive", + "serde-wasm-bindgen", "serde_json", "serde_yaml", "thiserror", @@ -329,9 +297,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -345,35 +313,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "regex" -version = "1.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "ryu" version = "1.0.15" @@ -387,25 +326,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" [[package]] -name = "semver" -version = "1.0.18" +name = "serde" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] [[package]] -name = "serde" -version = "1.0.188" +name = "serde-wasm-bindgen" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d" dependencies = [ - "serde_derive", + "js-sys", + "serde", + "wasm-bindgen", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", @@ -414,9 +358,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.106" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -425,11 +369,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.25" +version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -444,9 +388,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "2.0.32" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -455,9 +399,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -470,18 +414,18 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", @@ -490,9 +434,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unsafe-libyaml" @@ -502,9 +446,9 @@ checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -512,9 +456,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", @@ -527,9 +471,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -539,9 +483,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -549,9 +493,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", @@ -562,15 +506,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-bindgen-test" -version = "0.3.37" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +checksum = "513df541345bb9fcc07417775f3d51bbb677daf307d8035c0afafd87dc2e6599" dependencies = [ "console_error_panic_hook", "js-sys", @@ -582,9 +526,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.37" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +checksum = "6150d36a03e90a3cf6c12650be10626a9902d70c5270fd47d7a47e5389a10d56" dependencies = [ "proc-macro2", "quote", @@ -592,9 +536,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -630,9 +574,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -644,10 +588,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] diff --git a/Cargo.toml b/Cargo.toml index 0ec5242..70e08f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,33 +19,31 @@ path = "src/lib.rs" crate-type = ["cdylib", "rlib"] [dependencies] -anyhow = "1.0" convert_case = "0.5.0" indexmap = { version = "1.5.1", features = ["serde-1"] } -lazy_static = "1.4.0" -regex = { version = "1.6", default-features = false, features = ["std"] } -semver = "1.0.12" -serde = "1.0" -serde_derive = "1.0" +serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["raw_value"]} thiserror = "1.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +anyhow = "1.0" serde_yaml = "0.9" [target.'cfg(target_arch = "wasm32")'.dependencies] console_error_panic_hook = { version = "0.1.6", optional = true } -gloo-utils = { version = "0.2", features = ["serde"] } -wasm-bindgen = "0.2" +js-sys = "0.3" +serde-wasm-bindgen = "0.6" +wasm-bindgen = "0.2.82" wee_alloc = { version = "0.4.5", optional = true } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] -js-sys = "0.3" wasm-bindgen-test = "0.3.0" [profile.release] +debug = true opt-level = "z" lto = true [package.metadata.wasm-pack.profile.release] -wasm-opt = ["-Oz", "--enable-mutable-globals"] +wasm-opt = false +# wasm-opt = ["-Oz", "--enable-mutable-globals"] diff --git a/cli/src/main.rs b/cli/src/main.rs index 082ad06..53d0c13 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -25,7 +25,7 @@ fn main() { .long("output-format") .help("The output format") .value_name("format") - .possible_values(&["yaml", "json"]) + .possible_values(["yaml", "json"]) .default_value("yaml"), ) .arg( diff --git a/js/LICENSE b/js/LICENSE new file mode 120000 index 0000000..7a694c9 --- /dev/null +++ b/js/LICENSE @@ -0,0 +1 @@ +LICENSE \ No newline at end of file diff --git a/js/README.md b/js/README.md new file mode 120000 index 0000000..42061c0 --- /dev/null +++ b/js/README.md @@ -0,0 +1 @@ +README.md \ No newline at end of file diff --git a/js/package-lock.json b/js/package-lock.json new file mode 100644 index 0000000..b2b8513 --- /dev/null +++ b/js/package-lock.json @@ -0,0 +1,13 @@ +{ + "name": "postman2openapi", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "postman2openapi", + "version": "1.2.0", + "license": "Apache-2.0" + } + } +} diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..0826ea2 --- /dev/null +++ b/js/package.json @@ -0,0 +1,21 @@ +{ + "name": "postman2openapi", + "collaborators": [ + "Kevin Swiber " + ], + "description": "Convert a Postman collection to an OpenAPI definition. ", + "version": "1.2.0", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/kevinswiber/postman2openapi" + }, + "files": [ + "postman2openapi_bg.wasm", + "postman2openapi.js", + "postman2openapi.d.ts" + ], + "main": "postman2openapi.js", + "homepage": "https://github.com/kevinswiber/postman2openapi", + "types": "postman2openapi.d.ts" +} \ No newline at end of file diff --git a/justfile b/justfile index ab82f3a..651b4cf 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -prepare: build-release build-nodejs build-web test +prepare: test build-release build-web build: cargo build --all @@ -13,12 +13,14 @@ build-lib: cargo build build-cli: cargo build --package postman2openapi-cli -build-web: - wasm-pack build --release --out-dir ./web/wasm --target bundler +build-web: build-js npm install --prefix ./web npm run build --prefix ./web -build-nodejs: - wasm-pack build --release --out-dir ./nodejs --target nodejs +build-js: + cargo build --target wasm32-unknown-unknown --release + wasm-bindgen --out-dir ./js ./target/wasm32-unknown-unknown/release/postman2openapi.wasm + wasm-snip --snip-rust-panicking-code -o ./js/postman2openapi_bg.wasm ./js/postman2openapi_bg.wasm + wasm-opt -Oz -o ./js/postman2openapi_bg.wasm ./js/postman2openapi_bg.wasm build-devcontainer-image: NEEDS_BUILDER=$(docker buildx ls | grep -q postman2openapi; echo $?); \ if [[ "$NEEDS_BUILDER" = "1" ]]; then docker buildx create --name postman2openapi --bootstrap --use; \ diff --git a/src/backends/mod.rs b/src/backends/mod.rs new file mode 100644 index 0000000..ec4844d --- /dev/null +++ b/src/backends/mod.rs @@ -0,0 +1 @@ +pub(crate) mod openapi3_0; diff --git a/src/backends/openapi3_0.rs b/src/backends/openapi3_0.rs new file mode 100644 index 0000000..b1bb2fc --- /dev/null +++ b/src/backends/openapi3_0.rs @@ -0,0 +1,1264 @@ +use crate::core::{ + capture_collection_variables, capture_openapi_path_variables, Backend, CreateOperationParams, + State, +}; +use crate::formats::openapi::v3_0::{ + self as openapi3, ObjectOrReference, Parameter, SecurityRequirement, +}; +use crate::formats::postman::{self, AuthType}; +use convert_case::{Case, Casing}; +use indexmap::{IndexMap, IndexSet}; +use std::borrow::Cow; +use std::collections::BTreeMap; + +pub(crate) struct OpenApi30Backend<'a> { + pub(crate) oas: &'a mut openapi3::Spec, + pub(crate) operation_ids: BTreeMap, +} + +impl<'a> OpenApi30Backend<'a> { + pub(crate) fn generate( + name: Cow<'a, str>, + description: Option>, + ) -> openapi3::Spec { + openapi3::Spec { + openapi: String::from("3.0.3"), + info: openapi3::Info { + license: None, + contact: Some(openapi3::Contact::default()), + description: description.map(|s| s.to_string()), + terms_of_service: None, + version: String::from("1.0.0"), + title: name.to_string(), + }, + components: None, + external_docs: None, + paths: IndexMap::new(), + security: None, + servers: Some(Vec::::new()), + tags: Some(IndexSet::::new()), + } + } + + pub(crate) fn create_path_parameters( + state: &mut State, + resolved_segments: &[String], + postman_variables: &Option>, + ) -> Option>> { + let params: Vec> = resolved_segments + .iter() + .flat_map(|segment| { + capture_openapi_path_variables(segment.as_str()) + .map(|captures| { + captures + .iter() + .map(|capture| { + let var = &capture.value; + let mut param = Parameter { + name: var.to_string(), + location: "path".to_owned(), + required: Some(true), + ..Parameter::default() + }; + + let mut schema = openapi3::Schema { + schema_type: Some("string".to_string()), + ..Default::default() + }; + if let Some(path_val) = &postman_variables { + if let Some(p) = path_val.iter().find(|p| match &p.key { + Some(k) => k == var, + _ => false, + }) { + param.description = + p.description.as_ref().map(|d| d.into()); + if let Some(pval) = &p.value { + if let Some(pval_val) = pval.as_str() { + schema.example = Some(serde_json::Value::String( + state + .variables + .resolve(Cow::Borrowed(pval_val)), + )); + } + } + } + } + param.schema = Some(schema); + openapi3::ObjectOrReference::Object(param) + }) + .collect() + }) + .unwrap_or(vec![]) + }) + .collect(); + + if !params.is_empty() { + Some(params) + } else { + None + } + } + + pub(crate) fn create_query_parameters( + state: &mut State, + query_params: &[postman::QueryParam], + ) -> Option>> { + let mut keys = vec![]; + let params = query_params + .iter() + .filter_map(|qp| match &qp.key { + Some(key) => { + if keys.contains(&key) { + return None; + } + + keys.push(key); + let param = Parameter { + name: key.to_string(), + description: qp.description.as_ref().map(|d| d.into()), + location: "query".to_string(), + schema: Some(openapi3::Schema { + schema_type: Some("string".to_string()), + example: qp.value.clone().map(|pval| { + serde_json::Value::String(state.variables.resolve(pval)) + }), + ..openapi3::Schema::default() + }), + ..Parameter::default() + }; + + Some(openapi3::ObjectOrReference::Object(param)) + } + None => None, + }) + .collect::>>(); + + if !params.is_empty() { + Some(params) + } else { + None + } + } + + pub(crate) fn create_request_body( + state: &mut State, + body: &postman::Body, + op: &mut openapi3::Operation, + name: Cow<'a, str>, + ct: Option, + ) { + let mut content_type = ct; + let request_body = match op.request_body.as_mut() { + Some(ObjectOrReference::Object(request_body)) => request_body, + _ => { + op.request_body = Some(ObjectOrReference::Object(openapi3::RequestBody::default())); + match op.request_body.as_mut() { + Some(ObjectOrReference::Object(request_body)) => request_body, + _ => unreachable!(), + } + } + }; + + let default_media_type = openapi3::MediaType::default(); + + if let Some(mode) = &body.mode { + match mode { + postman::Mode::Raw => { + content_type = Some("application/octet-stream".to_string()); + if let Some(raw) = body.raw.clone() { + let resolved_body = state.variables.resolve(raw); + let example_val; + + //set content type based on options or inference. + match serde_json::from_str(&resolved_body) { + Ok(v) => match v { + serde_json::Value::Object(_) | serde_json::Value::Array(_) => { + content_type = Some("application/json".to_string()); + let content = { + let ct = content_type.as_ref().unwrap(); + if !request_body.content.contains_key(ct) { + request_body + .content + .insert(ct.clone(), default_media_type.clone()); + } + + request_body.content.get_mut(ct).unwrap() + }; + + if let Some(schema) = Self::create_schema(&v) { + content.schema = + Some(openapi3::ObjectOrReference::Object(schema)); + } + example_val = v; + } + _ => { + example_val = serde_json::Value::String(resolved_body); + } + }, + _ => { + content_type = Some("text/plain".to_string()); + if let Some(options) = body.options.clone() { + if let Some(raw_options) = options.raw { + if raw_options.language.is_some() { + content_type = match raw_options.language.unwrap() { + Cow::Borrowed("xml") => { + Some("application/xml".to_string()) + } + Cow::Borrowed("json") => { + Some("application/json".to_string()) + } + Cow::Borrowed("html") => { + Some("text/html".to_string()) + } + _ => Some("text/plain".to_string()), + } + } + } + } + example_val = serde_json::Value::String(resolved_body); + } + } + + let content = { + let ct = content_type.as_ref().unwrap(); + if !request_body.content.contains_key(ct) { + request_body + .content + .insert(ct.clone(), default_media_type.clone()); + } + + request_body.content.get_mut(ct).unwrap() + }; + + let examples = content.examples.clone().unwrap_or( + openapi3::MediaTypeExample::Examples { + examples: BTreeMap::new(), + }, + ); + + let example = openapi3::Example { + summary: None, + description: None, + value: Some(example_val), + }; + + if let openapi3::MediaTypeExample::Examples { examples: mut ex } = examples + { + ex.insert(name.to_string(), ObjectOrReference::Object(example)); + content.examples = + Some(openapi3::MediaTypeExample::Examples { examples: ex }); + } + } + } + postman::Mode::Urlencoded => { + content_type = Some("application/x-www-form-urlencoded".to_string()); + let content = { + let ct = content_type.as_ref().unwrap(); + if !request_body.content.contains_key(ct) { + request_body + .content + .insert(ct.clone(), default_media_type.clone()); + } + + request_body.content.get_mut(ct).unwrap() + }; + if let Some(urlencoded) = &body.urlencoded { + let mut oas_data = serde_json::Map::new(); + for i in urlencoded { + if let Some(v) = &i.value { + let value = serde_json::Value::String(v.to_string()); + oas_data.insert(i.key.to_string(), value); + } + } + let oas_obj = serde_json::Value::Object(oas_data); + if let Some(schema) = Self::create_schema(&oas_obj) { + content.schema = Some(openapi3::ObjectOrReference::Object(schema)); + } + + let examples = content.examples.clone().unwrap_or( + openapi3::MediaTypeExample::Examples { + examples: BTreeMap::new(), + }, + ); + + let example = openapi3::Example { + summary: None, + description: None, + value: Some(oas_obj), + }; + + if let openapi3::MediaTypeExample::Examples { examples: mut ex } = examples + { + ex.insert(name.to_string(), ObjectOrReference::Object(example)); + content.examples = + Some(openapi3::MediaTypeExample::Examples { examples: ex }); + } + } + } + postman::Mode::Formdata => { + content_type = Some("multipart/form-data".to_string()); + let content = { + let ct = content_type.as_ref().unwrap(); + if !request_body.content.contains_key(ct) { + request_body + .content + .insert(ct.clone(), default_media_type.clone()); + } + + request_body.content.get_mut(ct).unwrap() + }; + + let mut schema = openapi3::Schema { + schema_type: Some("object".to_string()), + ..Default::default() + }; + let mut properties = BTreeMap::::new(); + + if let Some(formdata) = &body.formdata { + for i in formdata { + if let Some(t) = i.form_parameter_type.clone() { + let is_binary = t == "file"; + if let Some(v) = &i.value { + let value = serde_json::Value::String(v.to_string()); + let prop_schema = Self::create_schema(&value); + if let Some(mut prop_schema) = prop_schema { + if is_binary { + prop_schema.format = Some("binary".to_string()); + } + prop_schema.description = + i.description.as_ref().map(|d| d.into()); + properties.insert(i.key.to_string(), prop_schema); + } + } else { + let mut prop_schema = openapi3::Schema { + schema_type: Some("string".to_string()), + description: i.description.as_ref().map(|d| d.into()), + ..Default::default() + }; + if is_binary { + prop_schema.format = Some("binary".to_string()); + } + properties.insert(i.key.to_string(), prop_schema); + } + } + // NOTE: Postman doesn't store the content type of multipart files. :( + } + schema.properties = Some(properties); + content.schema = Some(openapi3::ObjectOrReference::Object(schema)); + } + } + + postman::Mode::GraphQl => { + content_type = Some("application/json".to_string()); + let content = { + let ct = content_type.as_ref().unwrap(); + if !request_body.content.contains_key(ct) { + request_body + .content + .insert(ct.clone(), default_media_type.clone()); + } + + request_body.content.get_mut(ct).unwrap() + }; + + // The schema is the same for every GraphQL request. + content.schema = Some(ObjectOrReference::Object(openapi3::Schema { + schema_type: Some("object".to_owned()), + properties: Some(BTreeMap::from([ + ( + "query".to_owned(), + openapi3::Schema { + schema_type: Some("string".to_owned()), + ..openapi3::Schema::default() + }, + ), + ( + "variables".to_owned(), + openapi3::Schema { + schema_type: Some("object".to_owned()), + ..openapi3::Schema::default() + }, + ), + ])), + ..openapi3::Schema::default() + })); + + if let Some(postman::GraphQlBody::GraphQlBodyClass(graphql)) = &body.graphql { + if let Some(query) = &graphql.query { + let mut example_map = serde_json::Map::new(); + example_map + .insert("query".to_owned(), query.clone().to_string().into()); + if let Some(vars) = &graphql.variables { + if let Ok(vars) = serde_json::from_str::(vars) { + example_map.insert("variables".to_owned(), vars); + } + } + + let example = openapi3::MediaTypeExample::Example { + example: serde_json::Value::Object(example_map), + }; + content.examples = Some(example); + } + } + } + _ => content_type = Some("application/octet-stream".to_string()), + } + } + + if content_type.is_none() { + content_type = Some("application/octet-stream".to_string()); + request_body + .content + .insert(content_type.unwrap(), default_media_type); + } + } + + pub(crate) fn create_schema(value: &serde_json::Value) -> Option { + match value { + serde_json::Value::Object(m) => { + let mut schema = openapi3::Schema { + schema_type: Some("object".to_string()), + ..Default::default() + }; + + let mut properties = BTreeMap::::new(); + + for (key, val) in m.iter() { + if let Some(v) = Self::create_schema(val) { + properties.insert(key.to_string(), v); + } + } + + schema.properties = Some(properties); + Some(schema) + } + serde_json::Value::Array(a) => { + let mut schema = openapi3::Schema { + schema_type: Some("array".to_string()), + ..Default::default() + }; + + let mut item_schema = openapi3::Schema::default(); + + for n in 0..a.len() { + if let Some(i) = a.get(n) { + if let Some(i) = Self::create_schema(i) { + if n == 0 { + item_schema = i; + } else { + item_schema = Self::merge_schemas(item_schema, &i); + } + } + } + } + + schema.items = Some(Box::new(item_schema)); + schema.example = Some(value.clone()); + + Some(schema) + } + serde_json::Value::String(_) => { + let schema = openapi3::Schema { + schema_type: Some("string".to_string()), + example: Some(value.clone()), + ..Default::default() + }; + Some(schema) + } + serde_json::Value::Number(_) => { + let schema = openapi3::Schema { + schema_type: Some("number".to_string()), + example: Some(value.clone()), + ..Default::default() + }; + Some(schema) + } + serde_json::Value::Bool(_) => { + let schema = openapi3::Schema { + schema_type: Some("boolean".to_string()), + example: Some(value.clone()), + ..Default::default() + }; + Some(schema) + } + serde_json::Value::Null => { + let schema = openapi3::Schema { + nullable: Some(true), + example: Some(value.clone()), + ..Default::default() + }; + Some(schema) + } + } + } + + pub(crate) fn merge_schemas( + mut original: openapi3::Schema, + new: &openapi3::Schema, + ) -> openapi3::Schema { + // If the new schema has a nullable Option but the original doesn't, + // set the original nullable to the new one. + if original.nullable.is_none() && new.nullable.is_some() { + original.nullable = new.nullable; + } + + // If both original and new have a nullable Option, + // If any of their values is true, set to true. + if let Some(original_nullable) = original.nullable { + if let Some(new_nullable) = new.nullable { + if new_nullable != original_nullable { + original.nullable = Some(true); + } + } + } + + if let Some(ref mut any_of) = original.any_of { + any_of.push(openapi3::ObjectOrReference::Object(new.clone())); + return original; + } + + // Reset the schema type. + if original.schema_type.is_none() && new.schema_type.is_some() && new.any_of.is_none() { + original.schema_type = new.schema_type.clone(); + } + + // If both types are objects, merge the schemas of each property. + if let Some(t) = &original.schema_type { + if let "object" = t.as_str() { + if let Some(original_properties) = &mut original.properties { + if let Some(new_properties) = &new.properties { + for (key, val) in original_properties.iter_mut() { + if let Some(v) = new_properties.get(key) { + let prop = v; + *val = Self::merge_schemas(val.clone(), prop); + } + } + + for (key, val) in new_properties.iter() { + if !original_properties.contains_key(key) { + original_properties.insert(key.to_string(), val.clone()); + } + } + } + } + } + } + + if let Some(ref original_type) = original.schema_type { + if let Some(ref new_type) = new.schema_type { + if new_type != original_type { + let cloned = original.clone(); + original.schema_type = None; + original.properties = None; + original.items = None; + original.any_of = Some(vec![ + openapi3::ObjectOrReference::Object(cloned), + openapi3::ObjectOrReference::Object(new.clone()), + ]); + } + } + } + + original + } + + fn create_operation_security( + &mut self, + state: &mut State, + auth: &postman::Auth, + ) -> Option)>> { + self.create_security_items(state, auth, false) + } + + fn create_security_items( + &mut self, + state: &mut State, + auth: &postman::Auth, + add_to_root: bool, + ) -> Option)>> { + if self.oas.components.is_none() { + self.oas.components = Some(openapi3::Components::default()); + } + if self + .oas + .components + .as_ref() + .unwrap() + .security_schemes + .is_none() + { + self.oas.components.as_mut().unwrap().security_schemes = Some(BTreeMap::new()); + } + let security_schemes = self + .oas + .components + .as_mut() + .unwrap() + .security_schemes + .as_mut() + .unwrap(); + let security = match auth.auth_type { + AuthType::Noauth => Some(None), + AuthType::Basic => { + let scheme = openapi3::SecurityScheme::Http { + scheme: "basic".to_string(), + bearer_format: None, + }; + let name = "basicAuth".to_string(); + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, vec![]))) + } + AuthType::Digest => { + let scheme = openapi3::SecurityScheme::Http { + scheme: "digest".to_string(), + bearer_format: None, + }; + let name = "digestAuth".to_string(); + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, vec![]))) + } + AuthType::Bearer => { + let scheme = openapi3::SecurityScheme::Http { + scheme: "bearer".to_string(), + bearer_format: None, + }; + let name = "bearerAuth".to_string(); + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, vec![]))) + } + AuthType::Jwt => { + let scheme = openapi3::SecurityScheme::Http { + scheme: "bearer".to_string(), + bearer_format: Some("jwt".to_string()), + }; + let name = "jwtBearerAuth".to_string(); + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, vec![]))) + } + AuthType::Apikey => { + let name = "apiKey".to_string(); + if let Some(apikey) = &auth.apikey { + let scheme = openapi3::SecurityScheme::ApiKey { + name: state + .variables + .resolve(apikey.key.clone().unwrap_or(Cow::Borrowed("Authorization"))), + location: match apikey.location { + postman::ApiKeyLocation::Header => "header".to_string(), + postman::ApiKeyLocation::Query => "query".to_string(), + }, + }; + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + } else { + let scheme = openapi3::SecurityScheme::ApiKey { + name: "Authorization".to_string(), + location: "header".to_string(), + }; + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + } + Some(Some((name, vec![]))) + } + AuthType::Oauth2 => { + let name = "oauth2".to_string(); + if let Some(oauth2) = &auth.oauth2 { + let mut flows: openapi3::Flows = Default::default(); + let scopes = BTreeMap::from_iter( + oauth2 + .scope + .clone() + .unwrap_or_default() + .iter() + .map(|s| state.variables.resolve(Cow::Borrowed(s))) + .map(|s| (s.to_string(), s.to_string())), + ); + let authorization_url = state.variables.resolve(Cow::Borrowed( + oauth2.auth_url.as_ref().unwrap_or(&"".to_string()), + )); + let token_url = state.variables.resolve(Cow::Borrowed( + oauth2.access_token_url.as_ref().unwrap_or(&"".to_string()), + )); + let refresh_url = oauth2 + .refresh_token_url + .as_ref() + .map(|url| state.variables.resolve(Cow::Borrowed(url))); + match oauth2.grant_type { + postman::Oauth2GrantType::AuthorizationCode + | postman::Oauth2GrantType::AuthorizationCodeWithPkce => { + flows.authorization_code = Some(openapi3::AuthorizationCodeFlow { + authorization_url, + token_url, + refresh_url, + scopes, + }); + } + postman::Oauth2GrantType::ClientCredentials => { + flows.client_credentials = Some(openapi3::ClientCredentialsFlow { + token_url, + refresh_url, + scopes, + }); + } + postman::Oauth2GrantType::PasswordCredentials => { + flows.password = Some(openapi3::PasswordFlow { + token_url, + refresh_url, + scopes, + }); + } + postman::Oauth2GrantType::Implicit => { + flows.implicit = Some(openapi3::ImplicitFlow { + authorization_url, + refresh_url, + scopes, + }); + } + } + let scheme = openapi3::SecurityScheme::OAuth2 { + flows: Box::new(flows), + }; + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, oauth2.scope.clone().unwrap_or_default()))) + } else { + let scheme = openapi3::SecurityScheme::OAuth2 { + flows: Default::default(), + }; + security_schemes.insert(name.clone(), ObjectOrReference::Object(scheme)); + Some(Some((name, vec![]))) + } + } + _ => None, + }; + + let security_requirement = match security.clone() { + Some(Some((name, scopes))) => Some(SecurityRequirement { + requirement: Some(BTreeMap::from([(name, scopes)])), + }), + Some(None) => Some(SecurityRequirement { requirement: None }), + _ => None, + }; + + if add_to_root { + if let Some(security_requirement) = security_requirement { + if self.oas.security.is_none() { + self.oas.security = Some(vec![security_requirement]); + } else { + self.oas + .security + .as_mut() + .unwrap() + .push(security_requirement); + } + } + } + + security + } +} + +impl<'a> Backend<'a> for OpenApi30Backend<'a> { + fn create_server(&mut self, state: &mut State, url: &postman::UrlClass, parts: &[Cow]) { + let host = parts.join("."); + let mut proto = "".to_string(); + if let Some(protocol) = &url.protocol { + proto = format!("{protocol}://", protocol = protocol); + } + if let Some(s) = &mut self.oas.servers { + let mut server_url = format!("{proto}{host}"); + server_url = state.variables.resolve(Cow::Borrowed(&server_url)); + if !s.iter_mut().any(|srv| srv.url == server_url) { + let server = openapi3::Server { + url: server_url, + description: None, + variables: None, + }; + s.push(server); + } + } + } + + fn create_tag(&mut self, _state: &mut State, name: Cow, description: Option>) { + if let Some(t) = &mut self.oas.tags { + let mut tag = openapi3::Tag { + name: name.to_string(), + description: description.map(|d| d.into()), + }; + + let mut i: usize = 0; + while t.contains(&tag) { + i += 1; + tag.name = format!("{tagName}{i}", tagName = tag.name); + } + + t.insert(tag); + }; + } + + fn create_operation<'cp: 'a>(&mut self, state: &mut State, params: CreateOperationParams<'cp>) { + let CreateOperationParams { + auth, + item, + request, + request_name, + path_elements: paths, + url, + } = params; + + let sr = if let Some(auth) = auth { + let security = self.create_operation_security(state, auth); + match security { + Some(Some((name, scopes))) => Some(SecurityRequirement { + requirement: Some(BTreeMap::from([(name, scopes)])), + }), + Some(None) => Some(SecurityRequirement { requirement: None }), + _ => None, + } + } else { + None + }; + + let empty_paths = vec![]; + let resolved_segments = paths + .unwrap_or(&empty_paths) + .iter() + .map(|segment| { + let mut seg = match segment { + postman::PathElement::PathClass(c) => c.value.clone().unwrap_or_default(), + postman::PathElement::String(c) => c.clone(), + }; + seg = Cow::Owned(state.variables.resolve_with_credits_and_replace_fn( + seg, + state.variables.replace_credits, + //|s| VARIABLE_RE.replace_all(&s, "{$1}").to_string(), + |s| { + let captures = capture_collection_variables(&s); + let mut newstr = s.clone(); + if let Some(captures) = captures { + for capture in captures { + newstr = newstr.replace( + format!("{{{{{value}}}}}", value = capture.value).as_str(), + format!("{{{value}}}", value = capture.value).as_str(), + ); + } + } + newstr + }, + )); + if !seg.is_empty() { + match &seg[0..1] { + ":" => format!("{{{}}}", &seg[1..]), + _ => seg.to_string(), + } + } else { + seg.to_string() + } + }) + .collect::>(); + let segments = "/".to_string() + &resolved_segments.join("/"); + + // TODO: Because of variables, we can actually get duplicate paths. + // - /admin/{subresource}/{subresourceId} + // - /admin/{subresource2}/{subresource2Id} + // Throw a warning? + if !self.oas.paths.contains_key(&segments) { + self.oas + .paths + .insert(segments.clone(), openapi3::PathItem::default()); + } + + let path = self.oas.paths.get_mut(&segments).unwrap(); + let method = match &request.method { + Some(m) => m.to_lowercase(), + None => "get".to_string(), + }; + let op_ref = match method.as_str() { + "get" => &mut path.get, + "post" => &mut path.post, + "put" => &mut path.put, + "delete" => &mut path.delete, + "patch" => &mut path.patch, + "options" => &mut path.options, + "trace" => &mut path.trace, + _ => &mut path.get, + }; + let is_merge = op_ref.is_some(); + if op_ref.is_none() { + *op_ref = Some(openapi3::Operation::default()); + } + let op = op_ref.as_mut().unwrap(); + + path.parameters = Self::create_path_parameters(state, &resolved_segments, &url.variable); + if let Some(sr) = sr { + if let Some(op_security) = &mut op.security { + if !op_security.contains(&sr) { + op_security.push(sr); + } + } else { + op.security = Some(vec![sr]); + } + } + + if !is_merge { + let mut op_id = request_name + .chars() + .map(|c| match c { + 'A'..='Z' | 'a'..='z' | '0'..='9' => c, + _ => ' ', + }) + .collect::() + .from_case(Case::Title) + .to_case(Case::Camel); + + match self.operation_ids.get_mut(&op_id) { + Some(v) => { + *v += 1; + op_id = format!("{op_id}{v}"); + } + None => { + self.operation_ids.insert(op_id.clone(), 0); + } + } + + op.operation_id = Some(op_id); + } + + if let Some(qp) = &url.query { + if let Some(mut query_params) = Self::create_query_parameters(state, qp) { + match &op.parameters { + Some(params) => { + let mut cloned = params.clone(); + for p1 in &mut query_params { + if let ObjectOrReference::Object(p1) = p1 { + let found = cloned.iter_mut().find(|p2| { + if let ObjectOrReference::Object(p2) = p2 { + p2.location == p1.location && p2.name == p1.name + } else { + false + } + }); + if let Some(ObjectOrReference::Object(p2)) = found { + p2.schema = Some(Self::merge_schemas( + p2.schema.clone().unwrap(), + &p1.schema.clone().unwrap(), + )); + } else { + cloned.push(ObjectOrReference::Object(p1.clone())); + } + } + } + op.parameters = Some(cloned); + } + None => op.parameters = Some(query_params), + }; + } + } + + let mut content_type: Option = None; + + if let Some(postman::HeaderUnion::HeaderArray(headers)) = &request.header { + for header in headers.iter() { + let key = header.key.to_lowercase(); + if key == "accept" || key == "authorization" { + continue; + } + if key == "content-type" { + let content_type_parts: Vec<&str> = header.value.split(';').collect(); + content_type = Some(content_type_parts[0].to_owned()); + } else { + let param = Parameter { + location: "header".to_owned(), + name: header.key.to_string(), + description: header.description.as_ref().map(|d| d.into()), + schema: Some(openapi3::Schema { + schema_type: Some("string".to_owned()), + example: Some(serde_json::Value::String(header.value.to_owned())), + ..openapi3::Schema::default() + }), + ..Parameter::default() + }; + + if op.parameters.is_none() { + op.parameters = Some(vec![ObjectOrReference::Object(param)]); + } else { + let params = op.parameters.as_mut().unwrap(); + let mut has_pushed = false; + for p in params { + if let ObjectOrReference::Object(p) = p { + if p.name == param.name && p.location == param.location { + if let Some(schema) = &p.schema { + has_pushed = true; + p.schema = Some(Self::merge_schemas( + schema.clone(), + ¶m.schema.clone().unwrap(), + )); + } + } + } + } + if !has_pushed { + op.parameters + .as_mut() + .unwrap() + .push(ObjectOrReference::Object(param)); + } + } + } + } + } + + if let Some(body) = &request.body { + Self::create_request_body(state, body, op, request_name.clone(), content_type); + } + + if !is_merge { + let description = match request.description.as_ref().map(|d| d.into()) { + Some(desc) => Some(desc), + None => Some(request_name.to_string()), + }; + + op.summary = Some(request_name.to_string()); + op.description = description; + } + + if !state.hierarchy.is_empty() { + op.tags = Some(state.hierarchy.iter().map(|s| s.to_string()).collect()); + } + + if let Some(responses) = &item.response { + for r in responses.iter().flatten() { + if let Some(or) = &r.original_request { + if let Some(body) = &or.body { + content_type = Some("text/plain".to_string()); + if let Some(options) = body.options.clone() { + if let Some(raw_options) = options.raw { + if raw_options.language.is_some() { + content_type = match raw_options.language.unwrap() { + Cow::Borrowed("xml") => Some("application/xml".to_string()), + Cow::Borrowed("json") => { + Some("application/json".to_string()) + } + Cow::Borrowed("html") => Some("text/html".to_string()), + _ => Some("text/plain".to_string()), + } + } + } + } + Self::create_request_body( + state, + body, + op, + request_name.clone(), + content_type, + ); + } + } + let mut oas_response = openapi3::Response::default(); + let mut response_media_types = BTreeMap::::new(); + + if let Some(name) = &r.name { + oas_response.description = Some(name.to_string()); + } + if let Some(postman::Headers::UnionArray(headers)) = &r.header { + let mut oas_headers = + BTreeMap::>::new(); + for h in headers { + if let postman::HeaderElement::Header(hdr) = h { + if hdr.value.is_empty() || hdr.key.to_lowercase() == "content-type" { + continue; + } + let mut oas_header = openapi3::Header::default(); + let header_schema = openapi3::Schema { + schema_type: Some("string".to_string()), + example: Some(serde_json::Value::String(hdr.value.to_string())), + ..Default::default() + }; + oas_header.schema = Some(header_schema); + + oas_headers.insert( + hdr.key.to_string(), + openapi3::ObjectOrReference::Object(oas_header), + ); + } + } + if !oas_headers.is_empty() { + oas_response.headers = Some(oas_headers); + } + } + let mut response_content = openapi3::MediaType::default(); + if let Some(raw) = &r.body { + let mut response_content_type: Option = None; + let resolved_body = state.variables.resolve(raw.clone()); + let example_val; + + match serde_json::from_str(&resolved_body) { + Ok(v) => match v { + serde_json::Value::Object(_) | serde_json::Value::Array(_) => { + response_content_type = Some("application/json".to_string()); + if let Some(schema) = Self::create_schema(&v) { + response_content.schema = + Some(openapi3::ObjectOrReference::Object(schema)); + } + example_val = v; + } + _ => { + example_val = serde_json::Value::String(resolved_body); + } + }, + _ => { + // TODO: Check if XML, HTML, JavaScript + response_content_type = Some("text/plain".to_string()); + example_val = serde_json::Value::String(resolved_body); + } + } + let mut example_map = + BTreeMap::>::new(); + + let ex = openapi3::Example { + summary: None, + description: None, + value: Some(example_val), + }; + + let example_name = match &r.name { + Some(n) => n.to_string(), + None => "".to_string(), + }; + + example_map.insert(example_name, openapi3::ObjectOrReference::Object(ex)); + let example = openapi3::MediaTypeExample::Examples { + examples: example_map, + }; + + response_content.examples = Some(example); + + if response_content_type.is_none() { + response_content_type = Some("application/octet-stream".to_string()); + } + + response_media_types + .insert(response_content_type.clone().unwrap(), response_content); + } + oas_response.content = Some(response_media_types); + + if let Some(code) = &r.code { + if let Some(existing_response) = op.responses.get_mut(&code.to_string()) { + let new_response = oas_response.clone(); + if let Some(name) = &new_response.description { + existing_response.description = Some( + existing_response + .description + .clone() + .unwrap_or("".to_string()) + + " / " + + name, + ); + } + + if let Some(headers) = new_response.headers { + let mut cloned_headers = headers.clone(); + for (key, val) in headers { + cloned_headers.insert(key, val); + } + existing_response.headers = Some(cloned_headers); + } + + let mut existing_content = + existing_response.content.clone().unwrap_or_default(); + for (media_type, new_content) in new_response.content.unwrap() { + if let Some(existing_response_content) = + existing_content.get_mut(&media_type) + { + if let Some(openapi3::ObjectOrReference::Object(existing_schema)) = + existing_response_content.schema.clone() + { + if let Some(openapi3::ObjectOrReference::Object(new_schema)) = + new_content.schema + { + existing_response_content.schema = + Some(openapi3::ObjectOrReference::Object( + Self::merge_schemas(existing_schema, &new_schema), + )) + } + } + + if let Some(openapi3::MediaTypeExample::Examples { + examples: existing_examples, + }) = &mut existing_response_content.examples + { + let new_example_map = match new_content.examples.unwrap() { + openapi3::MediaTypeExample::Examples { examples } => { + examples.clone() + } + _ => BTreeMap::::new(), + }; + for (key, value) in new_example_map.iter() { + existing_examples.insert(key.clone(), value.clone()); + } + } + } + } + existing_response.content = Some(existing_content.clone()); + } else { + op.responses.insert(code.to_string(), oas_response); + } + } + } + } + + if !op.responses.contains_key("200") + && !op.responses.contains_key("201") + && !op.responses.contains_key("202") + && !op.responses.contains_key("203") + && !op.responses.contains_key("204") + && !op.responses.contains_key("205") + && !op.responses.contains_key("206") + && !op.responses.contains_key("207") + && !op.responses.contains_key("208") + && !op.responses.contains_key("226") + { + op.responses.insert( + "200".to_string(), + openapi3::Response { + description: Some("".to_string()), + ..openapi3::Response::default() + }, + ); + } + } + + fn create_security(&mut self, state: &mut State, auth: &postman::Auth) { + self.create_security_items(state, auth, true); + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_path_parameters() { + let mut state = State::default(); + let postman_variables = Some(vec![postman::Variable { + key: Some(Cow::Borrowed("test")), + value: Some(serde_json::Value::String("test_value".to_string())), + description: None, + ..postman::Variable::default() + }]); + let path_params = ["/test/".to_string(), "{test_value}".to_string()]; + let params = + OpenApi30Backend::create_path_parameters(&mut state, &path_params, &postman_variables); + assert_eq!(params.unwrap().len(), 1); + } + + #[test] + fn test_generate_query_parameters() { + let mut state = State::default(); + let query_params = vec![postman::QueryParam { + key: Some(Cow::Borrowed("test")), + value: Some(Cow::Borrowed("{{test}}")), + description: None, + ..postman::QueryParam::default() + }]; + let params = OpenApi30Backend::create_query_parameters(&mut state, &query_params); + assert_eq!(params.unwrap().len(), 1); + } +} diff --git a/src/core.rs b/src/core.rs new file mode 100644 index 0000000..6209e23 --- /dev/null +++ b/src/core.rs @@ -0,0 +1,270 @@ +use std::{borrow::Cow, collections::BTreeMap}; + +use crate::formats::postman; + +#[cfg(not(target_arch = "wasm32"))] +pub type Map = indexmap::IndexMap; +#[cfg(target_arch = "wasm32")] +pub type Map = js_sys::Map; +#[cfg(not(target_arch = "wasm32"))] +pub type Set = indexmap::IndexSet; +#[cfg(target_arch = "wasm32")] +pub type Set = js_sys::Set; + +pub static VAR_REPLACE_CREDITS: usize = 20; + +enum CaptureState { + Start, + VariableOpen, + Variable, + VariableClose, +} + +pub struct Capture<'a> { + pub start: usize, + pub end: usize, + pub value: Cow<'a, str>, +} + +pub fn capture_openapi_path_variables(s: &str) -> Option>> { + let mut captures = Vec::new(); + let mut state = CaptureState::Start; + let mut state_start = 0; + s.chars().enumerate().for_each(|(i, c)| { + state = match state { + CaptureState::Start => match c { + '{' => { + state_start = i + 1; + CaptureState::Variable + } + _ => CaptureState::Start, + }, + CaptureState::Variable => match c { + '}' => { + captures.push(Capture { + start: state_start, + end: i - 1, + value: Cow::Borrowed(&s[state_start..i]), + }); + CaptureState::Start + } + '{' => CaptureState::VariableOpen, + _ => CaptureState::Variable, + }, + _ => CaptureState::Start, + } + }); + + if !captures.is_empty() { + Some(captures) + } else { + None + } +} + +pub fn capture_collection_variables(s: &str) -> Option>> { + let mut captures = Vec::new(); + let mut state = CaptureState::Start; + let mut state_start = 0; + s.chars().enumerate().for_each(|(i, c)| { + state = match state { + CaptureState::Start => match c { + '{' => CaptureState::VariableOpen, + _ => CaptureState::Start, + }, + CaptureState::VariableOpen => match c { + '{' => { + state_start = i + 1; + CaptureState::Variable + } + _ => CaptureState::Start, + }, + CaptureState::Variable => match c { + '}' => CaptureState::VariableClose, + '{' => CaptureState::VariableOpen, + _ => CaptureState::Variable, + }, + CaptureState::VariableClose => match c { + '}' => { + captures.push(Capture { + start: state_start, + end: i - 2, + value: Cow::Borrowed(&s[state_start..i - 1]), + }); + CaptureState::Start + } + '{' => CaptureState::VariableOpen, + _ => CaptureState::Start, + }, + } + }); + + if !captures.is_empty() { + Some(captures) + } else { + None + } +} + +#[derive(Default)] +pub struct State<'a> { + pub auth_stack: Vec<&'a postman::Auth<'a>>, + pub hierarchy: Vec>, + pub variables: Variables<'a>, +} + +#[derive(Debug, Clone)] +pub struct CreateOperationParams<'a> { + pub auth: Option<&'a postman::Auth<'a>>, + pub item: &'a postman::Items<'a>, + pub request: &'a postman::RequestClass<'a>, + pub request_name: Cow<'a, str>, + pub path_elements: Option<&'a Vec>>, + pub url: &'a postman::UrlClass<'a>, +} + +#[derive(Debug, Default, Clone)] +pub struct Variables<'a> { + pub map: BTreeMap, serde_json::value::Value>, + pub replace_credits: usize, +} + +impl<'a> Variables<'a> { + pub fn resolve(&self, segment: Cow<'a, str>) -> String { + self.resolve_with_credits(segment, self.replace_credits) + } + + pub fn resolve_with_credits( + &self, + segment: Cow<'a, str>, + sub_replace_credits: usize, + ) -> String { + self.resolve_with_credits_and_replace_fn(segment, sub_replace_credits, |s| s) + } + + pub fn resolve_with_credits_and_replace_fn( + &self, + segment: Cow<'a, str>, + sub_replace_credits: usize, + replace_fn: fn(String) -> String, + ) -> String { + let s = segment.to_string(); + + if sub_replace_credits == 0 { + return s; + } + + if let Some(cap) = capture_collection_variables(&s) { + for capture in cap { + if let Some(v) = self.map.get(capture.value.as_ref()) { + if let Some(v2) = v.as_str() { + return self.resolve_with_credits( + Cow::Owned(s.replace( + format!("{{{{{value}}}}}", value = capture.value).as_str(), + v2, + )), + sub_replace_credits - 1, + ); + } + } + } + } + + replace_fn(s) + } +} + +pub trait Converter { + fn convert_collection<'a, T: Backend<'a>>( + &mut self, + backend: &mut T, + state: &mut State<'a>, + items: &'a [postman::Items], + ); + fn convert_folder<'a, T: Backend<'a>>( + &mut self, + backend: &mut T, + state: &mut State<'a>, + items: &'a [postman::Items], + name: Cow<'a, str>, + description: Option>, + ); + fn convert_request<'a, T: Backend<'a>>( + &mut self, + backend: &mut T, + state: &mut State<'a>, + item: &'a postman::Items, + name: Cow<'a, str>, + ); +} + +pub trait Backend<'a> { + fn create_server(&mut self, state: &mut State, url: &postman::UrlClass, parts: &[Cow]); + fn create_tag(&mut self, state: &mut State, name: Cow, description: Option>); + fn create_operation<'cp: 'a>(&mut self, state: &mut State, params: CreateOperationParams<'cp>); + fn create_security(&mut self, state: &mut State, auth: &postman::Auth); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_captures_one_collection_variable() { + let captures = capture_collection_variables("{{foo}}").unwrap(); + assert_eq!(captures.len(), 1); + assert_eq!(captures[0].start, 2); + assert_eq!(captures[0].end, 4); + assert_eq!(captures[0].value, "foo"); + } + + #[test] + fn it_captures_many_collection_variables() { + let captures = capture_collection_variables("{{foo}}{{bar}}").unwrap(); + assert_eq!(captures.len(), 2); + + assert_eq!(captures[0].start, 2); + assert_eq!(captures[0].end, 4); + assert_eq!(captures[0].value, "foo"); + + assert_eq!(captures[1].start, 9); + assert_eq!(captures[1].end, 11); + assert_eq!(captures[1].value, "bar"); + } + + #[test] + fn it_only_captures_nested_collection_variables() { + let captures = capture_collection_variables("{{foo{{bar}}}}{{{{bar}}foo}}").unwrap(); + assert_eq!(captures.len(), 2); + + assert_eq!(captures[0].start, 7); + assert_eq!(captures[0].end, 9); + assert_eq!(captures[0].value, "bar"); + + assert_eq!(captures[1].start, 18); + assert_eq!(captures[1].end, 20); + assert_eq!(captures[1].value, "bar"); + } + + #[test] + fn it_captures_one_openapi_path_variable() { + let captures = capture_openapi_path_variables("{foo}").unwrap(); + assert_eq!(captures.len(), 1); + assert_eq!(captures[0].start, 1); + assert_eq!(captures[0].end, 3); + assert_eq!(captures[0].value, "foo"); + } + + #[test] + fn it_captures_multiple_openapi_path_variables() { + let captures = capture_openapi_path_variables("{foo}/{bar}").unwrap(); + assert_eq!(captures.len(), 2); + assert_eq!(captures[0].start, 1); + assert_eq!(captures[0].end, 3); + assert_eq!(captures[0].value, "foo"); + + assert_eq!(captures[1].start, 7); + assert_eq!(captures[1].end, 9); + assert_eq!(captures[1].value, "bar"); + } +} diff --git a/src/formats/mod.rs b/src/formats/mod.rs new file mode 100644 index 0000000..7788de7 --- /dev/null +++ b/src/formats/mod.rs @@ -0,0 +1,2 @@ +pub mod openapi; +pub mod postman; diff --git a/src/openapi/data/v3.0/api-with-examples.yaml b/src/formats/openapi/data/v3.0/api-with-examples.yaml similarity index 100% rename from src/openapi/data/v3.0/api-with-examples.yaml rename to src/formats/openapi/data/v3.0/api-with-examples.yaml diff --git a/src/openapi/data/v3.0/callback-example.yaml b/src/formats/openapi/data/v3.0/callback-example.yaml similarity index 100% rename from src/openapi/data/v3.0/callback-example.yaml rename to src/formats/openapi/data/v3.0/callback-example.yaml diff --git a/src/openapi/data/v3.0/link-example.yaml b/src/formats/openapi/data/v3.0/link-example.yaml similarity index 100% rename from src/openapi/data/v3.0/link-example.yaml rename to src/formats/openapi/data/v3.0/link-example.yaml diff --git a/src/openapi/data/v3.0/petstore-expanded.yaml b/src/formats/openapi/data/v3.0/petstore-expanded.yaml similarity index 100% rename from src/openapi/data/v3.0/petstore-expanded.yaml rename to src/formats/openapi/data/v3.0/petstore-expanded.yaml diff --git a/src/openapi/data/v3.0/petstore.yaml b/src/formats/openapi/data/v3.0/petstore.yaml similarity index 100% rename from src/openapi/data/v3.0/petstore.yaml rename to src/formats/openapi/data/v3.0/petstore.yaml diff --git a/src/openapi/data/v3.0/uspto.yaml b/src/formats/openapi/data/v3.0/uspto.yaml similarity index 100% rename from src/openapi/data/v3.0/uspto.yaml rename to src/formats/openapi/data/v3.0/uspto.yaml diff --git a/src/openapi/error.rs b/src/formats/openapi/error.rs similarity index 67% rename from src/openapi/error.rs rename to src/formats/openapi/error.rs index 643c82f..c2863fb 100644 --- a/src/openapi/error.rs +++ b/src/formats/openapi/error.rs @@ -1,6 +1,5 @@ //! Error types -use semver::{Error as SemVerError, Version}; use serde_json::Error as JsonError; #[cfg(not(target_arch = "wasm32"))] use serde_yaml::Error as YamlError; @@ -17,10 +16,6 @@ pub enum Error { Yaml(YamlError), #[error("{0}")] Serialize(JsonError), - #[error("{0}")] - SemVerError(SemVerError), - #[error("Unsupported spec file version ({0})")] - UnsupportedSpecFileVersion(Version), } #[cfg(target_arch = "wasm32")] @@ -30,10 +25,6 @@ pub enum Error { Io(IoError), #[error("{0}")] Serialize(JsonError), - #[error("{0}")] - SemVerError(SemVerError), - #[error("Unsupported spec file version ({0})")] - UnsupportedSpecFileVersion(Version), } impl From for Error { @@ -54,9 +45,3 @@ impl From for Error { Error::Serialize(e) } } - -impl From for Error { - fn from(e: SemVerError) -> Self { - Error::SemVerError(e) - } -} diff --git a/src/openapi/mod.rs b/src/formats/openapi/mod.rs similarity index 93% rename from src/openapi/mod.rs rename to src/formats/openapi/mod.rs index eb66d80..0f8734f 100644 --- a/src/openapi/mod.rs +++ b/src/formats/openapi/mod.rs @@ -20,8 +20,7 @@ pub mod error; pub mod v3_0; pub use error::Error; - -const MINIMUM_OPENAPI30_VERSION: &str = ">= 3.0"; +use serde::{Deserialize, Serialize}; pub type Result = StdResult; @@ -84,7 +83,7 @@ mod tests { println!(" Saving string to {:?}...", path); std::fs::create_dir_all(&path).unwrap(); let full_filename = path.as_ref().to_path_buf().join(filename); - let mut f = File::create(&full_filename).unwrap(); + let mut f = File::create(full_filename).unwrap(); f.write_all(data.as_bytes()).unwrap(); } @@ -114,7 +113,7 @@ mod tests { // File -> `String` -> `serde_yaml::Value` -> `serde_json::Value` -> `String` // Read the original file to string - let spec_yaml_str = read_to_string(&input_file) + let spec_yaml_str = read_to_string(input_file) .unwrap_or_else(|e| panic!("failed to read contents of {:?}: {}", input_file, e)); // Convert YAML string to JSON string let spec_json_str = convert_yaml_str_to_json(&spec_yaml_str); @@ -123,7 +122,7 @@ mod tests { // File -> `Spec` -> `serde_json::Value` -> `String` // Parse the input file - let parsed_spec = from_path(&input_file).unwrap(); + let parsed_spec = from_path(input_file).unwrap(); // Convert to serde_json::Value let parsed_spec_json = serde_json::to_value(parsed_spec).unwrap(); // Convert to a JSON string @@ -152,7 +151,7 @@ mod tests { // Just tests if the deserialization does not blow up. But does not test correctness #[test] fn can_deserialize() { - for entry in fs::read_dir("src/openapi/data/v3.0").unwrap() { + for entry in fs::read_dir("src/formats/openapi/data/v3.0").unwrap() { let path = entry.unwrap().path(); // cargo test -- --nocapture to see this message println!("Testing if {:?} is deserializable", path); @@ -167,7 +166,7 @@ mod tests { .iter() .collect(); - for entry in fs::read_dir("src/openapi/data/v3.0").unwrap() { + for entry in fs::read_dir("src/formats/openapi/data/v3.0").unwrap() { let entry = entry.unwrap(); let path = entry.path(); @@ -187,7 +186,7 @@ mod tests { #[test] fn can_deserialize_one_of_v3() { - let openapi = from_path("src/openapi/data/v3.0/petstore-expanded.yaml").unwrap(); + let openapi = from_path("src/formats/openapi/data/v3.0/petstore-expanded.yaml").unwrap(); let OpenApi::V3_0(spec) = openapi; let components = spec.components.unwrap(); let schemas = components.schemas.unwrap(); diff --git a/src/openapi/v3_0/components.rs b/src/formats/openapi/v3_0/components.rs similarity index 98% rename from src/openapi/v3_0/components.rs rename to src/formats/openapi/v3_0/components.rs index 5f0d27b..bf58f60 100644 --- a/src/openapi/v3_0/components.rs +++ b/src/formats/openapi/v3_0/components.rs @@ -1,3 +1,5 @@ +use serde::{Deserialize, Serialize}; + use super::schema::{ Callback, Example, Header, Link, Parameter, RequestBody, Response, Schema, SecurityScheme, }; diff --git a/src/openapi/v3_0/mod.rs b/src/formats/openapi/v3_0/mod.rs similarity index 100% rename from src/openapi/v3_0/mod.rs rename to src/formats/openapi/v3_0/mod.rs diff --git a/src/openapi/v3_0/schema.rs b/src/formats/openapi/v3_0/schema.rs similarity index 98% rename from src/openapi/v3_0/schema.rs rename to src/formats/openapi/v3_0/schema.rs index 1b045d9..0377d90 100644 --- a/src/openapi/v3_0/schema.rs +++ b/src/formats/openapi/v3_0/schema.rs @@ -1,30 +1,13 @@ //! Schema specification for [OpenAPI 3.0.0](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md) use indexmap::{IndexMap, IndexSet}; +use serde::{Deserialize, Serialize}; use std::{ collections::{BTreeMap, HashMap}, hash::{Hash, Hasher}, }; -use super::{ - super::Error, - super::Result, - super::MINIMUM_OPENAPI30_VERSION, - components::{Components, ObjectOrReference}, -}; - -impl Spec { - pub fn validate_version(&self) -> Result { - let spec_version = &self.openapi; - let sem_ver = semver::Version::parse(spec_version)?; - let required_version = semver::VersionReq::parse(MINIMUM_OPENAPI30_VERSION).unwrap(); - if required_version.matches(&sem_ver) { - Ok(sem_ver) - } else { - Err(Error::UnsupportedSpecFileVersion(sem_ver)) - } - } -} +use super::components::{Components, ObjectOrReference}; /// top level document #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] diff --git a/src/postman/mod.rs b/src/formats/postman/mod.rs similarity index 63% rename from src/postman/mod.rs rename to src/formats/postman/mod.rs index 525496c..7cbdf28 100644 --- a/src/postman/mod.rs +++ b/src/formats/postman/mod.rs @@ -1,86 +1,88 @@ -use serde::{Deserialize, Deserializer}; +use std::borrow::Cow; -extern crate serde_json; +use serde::{Deserialize, Deserializer, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] -pub struct Spec { - #[serde(rename = "auth")] - pub auth: Option, +pub struct Spec<'a> { + #[serde(borrow, rename = "auth")] + pub auth: Option>, - #[serde(rename = "event")] - pub event: Option>, + #[serde(borrow, rename = "event")] + pub event: Option>>, - #[serde(rename = "info")] - pub info: Information, + #[serde(borrow, rename = "info")] + pub info: Information<'a>, /// Items are the basic unit for a Postman collection. You can think of them as corresponding /// to a single API endpoint. Each Item has one request and may have multiple API responses /// associated with it. - #[serde(rename = "item")] - pub item: Vec, + #[serde(borrow, rename = "item")] + pub item: Vec>, - #[serde(rename = "variable")] - pub variable: Option>, + #[serde(borrow, rename = "variable")] + pub variable: Option>>, } /// Represents authentication helpers provided by Postman #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Auth { +pub struct Auth<'a> { /// The attributes for [AWS /// Auth](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html). - #[serde(rename = "awsv4")] - pub awsv4: Option, + #[serde(borrow, rename = "awsv4")] + pub awsv4: Option>, /// The attributes for [Basic /// Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). - #[serde(rename = "basic")] - pub basic: Option, + #[serde(borrow, rename = "basic")] + pub basic: Option>, /// The helper attributes for [Bearer Token /// Authentication](https://tools.ietf.org/html/rfc6750) - #[serde(rename = "bearer")] - pub bearer: Option, + #[serde(borrow, rename = "bearer")] + pub bearer: Option>, - #[serde(rename = "jwt")] - pub jwt: Option, + #[serde(borrow, rename = "jwt")] + pub jwt: Option>, /// The attributes for [Digest /// Authentication](https://en.wikipedia.org/wiki/Digest_access_authentication). - #[serde(rename = "digest")] - pub digest: Option, + #[serde(borrow, rename = "digest")] + pub digest: Option>, /// The attributes for [Hawk Authentication](https://github.com/hueniverse/hawk) - #[serde(rename = "hawk")] - pub hawk: Option, + #[serde(borrow, rename = "hawk")] + pub hawk: Option>, #[serde(rename = "noauth")] pub noauth: Option, /// The attributes for [NTLM /// Authentication](https://msdn.microsoft.com/en-us/library/cc237488.aspx) - #[serde(rename = "ntlm")] - pub ntlm: Option, + #[serde(borrow, rename = "ntlm")] + pub ntlm: Option>, /// The attributes for [Oauth2](https://oauth.net/1/) - #[serde(rename = "oauth1")] - pub oauth1: Option, + #[serde(borrow, rename = "oauth1")] + pub oauth1: Option>, /// Helper attributes for [Oauth2](https://oauth.net/2/) #[serde(rename = "oauth2")] pub oauth2: Option, - #[serde(rename = "apikey")] - pub apikey: Option, + #[serde(borrow, rename = "apikey")] + pub apikey: Option>, #[serde(rename = "type")] pub auth_type: AuthType, } #[derive(Clone, Debug, Serialize, PartialEq, Eq)] -pub struct ApiKey { - pub key: Option, +pub struct ApiKey<'a> { + #[serde(borrow)] + pub key: Option>, pub location: ApiKeyLocation, - pub value: Option, + #[serde(borrow)] + pub value: Option>, } #[derive(Clone, Debug, Serialize, PartialEq, Eq)] @@ -89,7 +91,7 @@ pub enum ApiKeyLocation { Query, } -impl<'de> Deserialize<'de> for ApiKey { +impl<'de: 'a, 'a> Deserialize<'de> for ApiKey<'a> { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -102,16 +104,16 @@ impl<'de> Deserialize<'de> for ApiKey { if let AuthAttributeUnion::AuthAttribute21(v) = deserialized { for item in v { if let Some(serde_json::Value::String(str)) = item.value { - match item.key.as_str() { - "key" => key = Some(str), - "in" => { + match item.key { + Cow::Borrowed("key") => key = Some(Cow::Owned(str)), + Cow::Borrowed("in") => { location = match str.as_str() { "query" => ApiKeyLocation::Query, "header" => ApiKeyLocation::Header, _ => ApiKeyLocation::Header, } } - "value" => value = Some(str), + Cow::Borrowed("value") => value = Some(Cow::Owned(str)), _ => {} } } @@ -159,8 +161,8 @@ impl<'de> Deserialize<'de> for Oauth2 { if let AuthAttributeUnion::AuthAttribute21(v) = deserialized { for item in v { if let Some(serde_json::Value::String(str)) = item.value { - match item.key.as_str() { - "grantType" => { + match item.key { + Cow::Borrowed("grantType") => { grant_type = match str.as_str() { "authorization_code" => Oauth2GrantType::AuthorizationCode, "authorization_code_with_pkce" => { @@ -172,15 +174,17 @@ impl<'de> Deserialize<'de> for Oauth2 { _ => Oauth2GrantType::AuthorizationCode, } } - "accessTokenUrl" => access_token_url = Some(str), - "addTokenTo" => add_token_to = Some(str), - "authUrl" => auth_url = Some(str), - "clientId" => client_id = Some(str), - "clientSecret" => client_secret = Some(str), - "refreshTokenUrl" => refresh_token_url = Some(str), - "scope" => scope = Some(str.split(' ').map(|s| s.to_string()).collect()), - "state" => state = Some(str), - "tokenName" => token_name = Some(str), + Cow::Borrowed("accessTokenUrl") => access_token_url = Some(str), + Cow::Borrowed("addTokenTo") => add_token_to = Some(str), + Cow::Borrowed("authUrl") => auth_url = Some(str), + Cow::Borrowed("clientId") => client_id = Some(str), + Cow::Borrowed("clientSecret") => client_secret = Some(str), + Cow::Borrowed("refreshTokenUrl") => refresh_token_url = Some(str), + Cow::Borrowed("scope") => { + scope = Some(str.split(' ').map(|s| s.to_string()).collect()) + } + Cow::Borrowed("state") => state = Some(str), + Cow::Borrowed("tokenName") => token_name = Some(str), _ => {} } } @@ -213,12 +217,12 @@ pub enum Oauth2GrantType { /// Represents an attribute for any authorization method provided by Postman. For example /// `username` and `password` are set as auth attributes for Basic Authentication method. #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct AuthAttribute { - #[serde(rename = "key")] - pub key: String, +pub struct AuthAttribute<'a> { + #[serde(borrow, rename = "key")] + pub key: Cow<'a, str>, - #[serde(rename = "type")] - pub auth_type: Option, + #[serde(borrow, rename = "type")] + pub auth_type: Option>, #[serde(rename = "value")] pub value: Option, @@ -226,8 +230,8 @@ pub struct AuthAttribute { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] #[serde(untagged)] -pub enum AuthAttributeUnion { - AuthAttribute21(Vec), +pub enum AuthAttributeUnion<'a> { + AuthAttribute21(#[serde(borrow)] Vec>), AuthAttribute20(Option), } @@ -236,131 +240,131 @@ pub enum AuthAttributeUnion { /// /// Defines a script associated with an associated event name #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct Event { +pub struct Event<'a> { /// Indicates whether the event is disabled. If absent, the event is assumed to be enabled. #[serde(rename = "disabled")] pub disabled: Option, /// A unique identifier for the enclosing event. - #[serde(rename = "id")] - pub id: Option, + #[serde(borrow, rename = "id")] + pub id: Option>, /// Can be set to `test` or `prerequest` for test scripts or pre-request scripts respectively. #[serde(rename = "listen")] - pub listen: String, + pub listen: Cow<'a, str>, - #[serde(rename = "script")] - pub script: Option