Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for openapiv3 #233

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased] - ReleaseDate

### Added

- Initial support for importing collections from an OpenAPIv3 specification [#106](https://github.com/LucasPickering/slumber/issues/106)
- Currently only OpenAPI 3.0 (not 3.1) is supported. Please try this out and give feedback if anything doesn't work.

### Changed

- Allow escaping keys in templates [#149](https://github.com/LucasPickering/slumber/issues/149)
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ indexmap = {version = "^2.0.1", features = ["serde"]}
itertools = "^0.12.0"
mime = "^0.3.17"
notify = {version = "^6.1.1", default-features = false, features = ["macos_fsevent"]}
openapiv3 = "2.0.0"
persisted = {version = "^0.1.0", features = ["serde"]}
ratatui = {version = "^0.26.0", features = ["serde", "unstable-rendered-line-info"]}
reqwest = {version = "^0.12.4", default-features = false, features = ["multipart", "rustls-tls"]}
Expand Down
4 changes: 2 additions & 2 deletions docs/src/cli/import.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `slumber import`

Generate a Slumber collection file based on an external format. Currently the only supported format is Insomnia, but more are planned.
Generate a Slumber collection file based on an external format.

See `slumber import --help` for more options.

Expand All @@ -27,10 +27,10 @@ slumber import insomnia insomnia.json slumber.yml
Supported formats:

- Insomnia
- OpenAPI v3

Requested formats:

- [OpenAPI](https://github.com/LucasPickering/slumber/issues/106)
- [JetBrains HTTP](https://github.com/LucasPickering/slumber/issues/122)

If you'd like another format supported, please [open an issue](https://github.com/LucasPickering/slumber/issues/new).
6 changes: 6 additions & 0 deletions src/cli/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,21 @@ pub struct ImportCommand {
}

#[derive(Copy, Clone, Debug, ValueEnum)]
#[allow(rustdoc::bare_urls)]
enum Format {
/// Insomnia export format (JSON or YAML)
Insomnia,
/// OpenAPI v3.0 (JSON or YAML) v3.1 not supported but may work
/// https://spec.openapis.org/oas/v3.0.3
Openapi,
}

impl Subcommand for ImportCommand {
async fn execute(self, _global: GlobalArgs) -> anyhow::Result<ExitCode> {
// Load the input
let collection = match self.format {
Format::Insomnia => Collection::from_insomnia(&self.input_file)?,
Format::Openapi => Collection::from_openapi(&self.input_file)?,
};

// Write the output
Expand Down
1 change: 1 addition & 0 deletions src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
mod cereal;
mod insomnia;
mod models;
mod openapi;
mod recipe_tree;

pub use cereal::HasId;
Expand Down
39 changes: 17 additions & 22 deletions src/collection/insomnia.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
use crate::{
collection::{
self, cereal::deserialize_from_str, Chain, ChainId, ChainSource,
Collection, Folder, HasId, JsonBody, Method, Profile, ProfileId,
Recipe, RecipeBody, RecipeId, RecipeNode, RecipeTree,
Collection, Folder, HasId, Method, Profile, ProfileId, Recipe,
RecipeBody, RecipeId, RecipeNode, RecipeTree,
},
template::{Identifier, Template},
util::NEW_ISSUE_LINK,
};
use anyhow::{anyhow, Context};
use indexmap::IndexMap;
Expand All @@ -16,7 +17,7 @@ use mime::Mime;
use reqwest::header;
use serde::{Deserialize, Deserializer};
use std::{collections::HashMap, fs::File, path::Path};
use tracing::{debug, info, warn};
use tracing::{debug, error, info, warn};

impl Collection {
/// Convert an Insomnia exported collection into the slumber format. This
Expand All @@ -34,8 +35,7 @@ impl Collection {
"The Insomnia importer is approximate. Some features are missing \
and it most likely will not give you an equivalent collection. If \
you would like to request support for a particular Insomnia \
feature, please open an issue: \
https://github.com/LucasPickering/slumber/issues/new"
feature, please open an issue: {NEW_ISSUE_LINK}"
);
let file = File::open(insomnia_file).context(format!(
"Error opening Insomnia collection file {insomnia_file:?}"
Expand Down Expand Up @@ -253,7 +253,7 @@ impl Grouped {
Resource::ApiSpec => {}
// Anything unknown should give a warning
Resource::Other { id, kind } => {
warn!("Ignoring resource `{id}` of unknown type `{kind}`");
error!("Ignoring resource `{id}` of unknown type `{kind}`");
}
}
}
Expand Down Expand Up @@ -333,7 +333,7 @@ impl From<Request> for RecipeNode {
.map(RecipeBody::try_from)
.transpose()
.inspect_err(|error| {
warn!(
error!(
"Error importing body for request `{id}`: {error}",
id = request.id
)
Expand All @@ -346,7 +346,7 @@ impl From<Request> for RecipeNode {
request.authentication.and_then(|authentication| {
let result = authentication.try_into();
if let Err(kind) = &result {
warn!(
error!(
"Ignoring authentication of unknown type `{kind}` \
for request `{}`",
request.id
Expand Down Expand Up @@ -379,15 +379,13 @@ impl TryFrom<Body> for RecipeBody {

fn try_from(body: Body) -> anyhow::Result<Self> {
let body = if body.mime_type == mime::APPLICATION_JSON {
// Parse JSON to our own JSON equivalent
let json: JsonBody<String> =
serde_json::from_str::<serde_json::Value>(
body.try_text()?.as_str(),
)
.context("Error parsing body as JSON")?
.into();
// Convert each string into a template *without* parsing
RecipeBody::Json(json.map(Template::raw))
// Parse to JSON
let json = serde_json::from_str::<serde_json::Value>(
body.try_text()?.as_str(),
)
.context("Error parsing body as JSON")?;
// Convert to our own body
RecipeBody::untemplated_json(json)
} else if body.mime_type == mime::APPLICATION_WWW_FORM_URLENCODED {
RecipeBody::FormUrlencoded(
body.params.into_iter().map(FormParam::into).collect(),
Expand Down Expand Up @@ -511,7 +509,7 @@ fn build_chains(requests: &[Request]) -> IndexMap<ChainId, Chain> {
if let FormParamKind::File = param.kind {
let id: ChainId = Identifier::escape(&param.id).into();
let Some(path) = &param.file_name else {
warn!(
error!(
"Form param `{}` is of type `file` \
but missing `file_name` field",
param.id
Expand Down Expand Up @@ -585,10 +583,7 @@ fn build_recipe_tree(
}

let tree = build_tree(&mut children_map, workspace_id)?;

RecipeTree::new(tree).map_err(|duplicate_id| {
anyhow!("Duplicate folder/recipe ID `{duplicate_id}`")
})
Ok(RecipeTree::new(tree)?)
}

/// For some fucked reason, Insomnia uses empty map instead of `null` for empty
Expand Down
8 changes: 8 additions & 0 deletions src/collection/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ pub enum RecipeBody {
FormMultipart(IndexMap<String, Template>),
}

impl RecipeBody {
/// Build a JSON body *without* parsing the internal strings as templates.
/// Useful for importing from external formats.
pub fn untemplated_json(value: serde_json::Value) -> Self {
Self::Json(JsonBody::<String>::from(value).map(Template::raw))
}
}

#[cfg(test)]
impl From<&str> for RecipeBody {
fn from(template: &str) -> Self {
Expand Down
Loading
Loading