Skip to content

Commit

Permalink
Move importers into their own crate
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasPickering committed Nov 25, 2024
1 parent 6940a6c commit 2c2be56
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 145 deletions.
25 changes: 24 additions & 1 deletion Cargo.lock

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

13 changes: 9 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,27 @@ env-lock = "0.1.0"
futures = "0.3.28"
indexmap = {version = "2.0.0", default-features = false}
itertools = "0.13.0"
mime = "0.3.17"
pretty_assertions = "1.4.0"
ratatui = {version = "0.28.0", default-features = false}
reqwest = {version = "0.12.5", default-features = false}
rstest = {version = "0.21.0", default-features = false}
serde = {version = "1.0.204", default-features = false}
serde_json = {version = "1.0.120", default-features = false, features = ["preserve_order"]}
serde_json_path = "0.7.1"
serde_test = "1.0.176"
serde_yaml = {version = "0.9.0", default-features = false}
slumber_cli = {path = "./crates/cli", version = "2.3.0" }
slumber_config = {path = "./crates/config", version = "2.3.0" }
slumber_core = {path = "./crates/core", version = "2.3.0" }
slumber_tui = {path = "./crates/tui", version = "2.3.0" }
slumber_cli = {path = "./crates/cli", version = "2.3.0"}
slumber_config = {path = "./crates/config", version = "2.3.0"}
slumber_core = {path = "./crates/core", version = "2.3.0"}
slumber_import = {path = "./crates/import", version = "2.3.0"}
slumber_tui = {path = "./crates/tui", version = "2.3.0"}
strum = {version = "0.26.3", default-features = false}
thiserror = "1.0.63"
tokio = {version = "1.39.2", default-features = false}
tracing = "0.1.40"
uuid = {version = "1.10.0", default-features = false}
winnow = "0.6.16"

[dependencies]
anyhow = {workspace = true, features = ["backtrace"]}
Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde = {workspace = true}
serde_yaml = {workspace = true}
slumber_config = {workspace = true}
slumber_core = {workspace = true}
slumber_import = {workspace = true}
tracing = {workspace = true}

[dev-dependencies]
Expand Down
7 changes: 4 additions & 3 deletions crates/cli/src/commands/import.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::{GlobalArgs, Subcommand};
use anyhow::Context;
use clap::{Parser, ValueEnum};
use slumber_core::collection::Collection;
use std::{
fs::File,
io::{self, Write},
Expand Down Expand Up @@ -34,8 +33,10 @@ 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)?,
Format::Insomnia => {
slumber_import::from_insomnia(&self.input_file)?
}
Format::Openapi => slumber_import::from_openapi(&self.input_file)?,
};

// Write the output
Expand Down
13 changes: 6 additions & 7 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ version = "2.3.0"
# Rely on parent for rust-version

[dependencies]
anyhow = "1.0.0"
anyhow = {workspace = true}
async-trait = "0.1.81"
bytes = {workspace = true, features = ["serde"]}
chrono = {workspace = true, features = ["clock", "serde", "std"]}
Expand All @@ -20,25 +20,24 @@ dirs = {workspace = true}
futures = {workspace = true}
indexmap = {workspace = true, features = ["serde"]}
itertools = {workspace = true}
mime = "0.3.17"
openapiv3 = "2.0.0"
mime = {workspace = true}
regex = {version = "1.10.5", default-features = false}
reqwest = {workspace = true, features = ["multipart", "rustls-tls", "rustls-tls-native-roots"]}
rmp-serde = "1.1.2"
rstest = {workspace = true, optional = true}
rusqlite = {version = "0.31.0", default-features = false, features = ["bundled", "chrono", "uuid"]}
rusqlite_migration = "1.2.0"
serde = {workspace = true, features = ["derive"]}
serde_json = {version = "1.0.120", default-features = false, features = ["preserve_order"]}
serde_json = {workspace = true}
serde_json_path = {workspace = true}
serde_yaml = {workspace = true}
strum = {workspace = true, features = ["derive"]}
thiserror = "1.0.63"
thiserror = {workspace = true}
tokio = {workspace = true, features = ["fs", "process"]}
tracing = "0.1.0"
tracing = {workspace = true}
url = {version = "2.0.0", features = ["serde"]}# Inherited from reqwest
uuid = {workspace = true, features = ["serde", "v4"]}
winnow = "0.6.16"
winnow = {workspace = true}

[dev-dependencies]
env-lock = {workspace = true}
Expand Down
2 changes: 0 additions & 2 deletions crates/core/src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
//! possible
mod cereal;
mod insomnia;
mod models;
mod openapi;
mod recipe_tree;

pub use cereal::HasId;
Expand Down
13 changes: 1 addition & 12 deletions crates/core/src/collection/cereal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use serde::{
ser::Error as _,
Deserialize, Deserializer, Serialize, Serializer,
};
use std::{fmt::Display, hash::Hash, str::FromStr};
use std::{hash::Hash, str::FromStr};

/// A type that has an `id` field. This is ripe for a derive macro, maybe a fun
/// project some day?
Expand Down Expand Up @@ -106,17 +106,6 @@ where
Ok(map)
}

/// Deserialize a value using its `FromStr` implementation
pub fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: Display,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(D::Error::custom)
}

/// Deserialize a profile mapping. This also enforces that only one profile is
/// marked as default
pub fn deserialize_profiles<'de, D>(
Expand Down
37 changes: 37 additions & 0 deletions crates/import/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[package]
authors = ["Lucas Pickering <[email protected]>"]
description = "Import from other formats to Slumber collections. Not intended for external use."
edition = "2021"
homepage = "https://slumber.lucaspickering.me"
keywords = ["rest", "http", "terminal", "tui"]
license = "MIT"
name = "slumber_import"
repository = "https://github.com/LucasPickering/slumber"
version = "2.3.0"
# Rely on parent for rust-version

[dependencies]
anyhow = {workspace = true}
indexmap = {workspace = true, features = ["serde"]}
itertools = {workspace = true}
mime = {workspace = true}
openapiv3 = "2.0.0"
reqwest = {workspace = true}
serde = {workspace = true}
serde_json = {workspace = true}
serde_yaml = {workspace = true}
slumber_core = {workspace = true}
strum = {workspace = true}
thiserror = {workspace = true}
tracing = {workspace = true}
winnow = {workspace = true}

[dev-dependencies]
pretty_assertions = {workspace = true}
rstest = {workspace = true}
serde_test = {workspace = true}

[features]

[package.metadata.release]
tag = false
Original file line number Diff line number Diff line change
@@ -1,77 +1,76 @@
//! Import request collections from Insomnia. Based on the Insomnia v4 export
//! format
use crate::{
use anyhow::{anyhow, Context};
use indexmap::IndexMap;
use itertools::Itertools;
use mime::Mime;
use reqwest::header;
use serde::{de::Error as _, Deserialize, Deserializer};
use slumber_core::{
collection::{
self, cereal::deserialize_from_str, Chain, ChainId, ChainSource,
Collection, Folder, HasId, Method, Profile, ProfileId, Recipe,
RecipeBody, RecipeId, RecipeNode, RecipeTree, SelectorMode,
self, Chain, ChainId, ChainSource, Collection, Folder, HasId, Method,
Profile, ProfileId, Recipe, RecipeBody, RecipeId, RecipeNode,
RecipeTree, SelectorMode,
},
http::content_type::ContentType,
template::{Identifier, Template},
util::NEW_ISSUE_LINK,
};
use anyhow::{anyhow, Context};
use indexmap::IndexMap;
use itertools::Itertools;
use mime::Mime;
use reqwest::header;
use serde::{Deserialize, Deserializer};
use std::{collections::HashMap, fs::File, path::Path};
use std::{
collections::HashMap, fmt::Display, fs::File, path::Path, str::FromStr,
};
use tracing::{debug, error, info, warn};

impl Collection {
/// Convert an Insomnia exported collection into the slumber format. This
/// supports YAML *or* JSON input.
///
/// This is not async because it's only called by the CLI, where we don't
/// care about blocking. It keeps the code simpler.
pub fn from_insomnia(
insomnia_file: impl AsRef<Path>,
) -> anyhow::Result<Self> {
let insomnia_file = insomnia_file.as_ref();
// First, deserialize into the insomnia format
info!(file = ?insomnia_file, "Loading Insomnia collection");
warn!(
"The Insomnia importer is approximate. Some features are missing \
/// Convert an Insomnia exported collection into the slumber format. This
/// supports YAML *or* JSON input.
///
/// This is not async because it's only called by the CLI, where we don't
/// care about blocking. It keeps the code simpler.
pub fn from_insomnia(
insomnia_file: impl AsRef<Path>,
) -> anyhow::Result<Collection> {
let insomnia_file = insomnia_file.as_ref();
// First, deserialize into the insomnia format
info!(file = ?insomnia_file, "Loading Insomnia collection");
warn!(
"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: {NEW_ISSUE_LINK}"
);
let file = File::open(insomnia_file).context(format!(
"Error opening Insomnia collection file {insomnia_file:?}"
);
let file = File::open(insomnia_file).context(format!(
"Error opening Insomnia collection file {insomnia_file:?}"
))?;
// The format can be YAML or JSON, so we can just treat it all as YAML
let mut insomnia: Insomnia =
serde_yaml::from_reader(file).context(format!(
"Error deserializing Insomnia collection file {insomnia_file:?}"
))?;
// The format can be YAML or JSON, so we can just treat it all as YAML
let mut insomnia: Insomnia =
serde_yaml::from_reader(file).context(format!(
"Error deserializing Insomnia collection file {insomnia_file:?}"
))?;

// Match Insomnia's visual order. This isn't entirely accurate because
// Insomnia reorders folders/requests according to the tree structure,
// but it should get us the right order within each layer
insomnia.resources.sort_by_key(Resource::sort_key);

let Grouped {
workspace_id,
environments,
request_groups,
requests,
} = Grouped::group(insomnia)?;

// Convert everything we care about
let profiles = build_profiles(&workspace_id, environments);
let chains = build_chains(&requests);
let recipes =
build_recipe_tree(&workspace_id, request_groups, requests)?;

Ok(Collection {
profiles,
recipes,
chains,
_ignore: serde::de::IgnoredAny,
})
}

// Match Insomnia's visual order. This isn't entirely accurate because
// Insomnia reorders folders/requests according to the tree structure,
// but it should get us the right order within each layer
insomnia.resources.sort_by_key(Resource::sort_key);

let Grouped {
workspace_id,
environments,
request_groups,
requests,
} = Grouped::group(insomnia)?;

// Convert everything we care about
let profiles = build_profiles(&workspace_id, environments);
let chains = build_chains(&requests);
let recipes = build_recipe_tree(&workspace_id, request_groups, requests)?;

Ok(Collection {
profiles,
recipes,
chains,
_ignore: serde::de::IgnoredAny,
})
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -575,6 +574,17 @@ fn build_recipe_tree(
Ok(RecipeTree::new(tree)?)
}

/// Deserialize a value using its `FromStr` implementation
fn deserialize_from_str<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: Display,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(D::Error::custom)
}

/// For some fucked reason, Insomnia uses empty map instead of `null` for empty
/// values in some cases. This function deserializes that to a regular Option.
fn deserialize_shitty_option<'de, T, D>(
Expand Down Expand Up @@ -603,12 +613,12 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::test_data_dir;
use indexmap::indexmap;
use pretty_assertions::assert_eq;
use rstest::rstest;
use serde::de::DeserializeOwned;
use serde_test::{assert_de_tokens, assert_de_tokens_error, Token};
use slumber_core::test_util::test_data_dir;
use std::{fmt::Debug, path::PathBuf};

const INSOMNIA_FILE: &str = "insomnia.json";
Expand All @@ -622,8 +632,7 @@ mod tests {
#[rstest]
fn test_insomnia_import(test_data_dir: PathBuf) {
let imported =
Collection::from_insomnia(test_data_dir.join(INSOMNIA_FILE))
.unwrap();
from_insomnia(test_data_dir.join(INSOMNIA_FILE)).unwrap();
let expected =
Collection::load(&test_data_dir.join(INSOMNIA_IMPORTED_FILE))
.unwrap();
Expand Down
Loading

0 comments on commit 2c2be56

Please sign in to comment.