Skip to content

Commit

Permalink
Filter request history by profile
Browse files Browse the repository at this point in the history
Closes #74
  • Loading branch information
LucasPickering committed Dec 20, 2023
1 parent 18fca8a commit 518e766
Show file tree
Hide file tree
Showing 16 changed files with 311 additions and 125 deletions.
1 change: 1 addition & 0 deletions slumber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ profiles:
name: Request Fails
data:
host: http://localhost:5000
username: !template "xX{{chains.username}}Xx"
user_guid: abc123

chains:
Expand Down
28 changes: 13 additions & 15 deletions src/cli/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,18 @@ impl Subcommand for RequestCommand {
let mut collection = RequestCollection::load(collection_path).await?;

// Find profile and recipe by ID
let profile_data = self
let profile = self
.profile
.map::<anyhow::Result<_>, _>(|id| {
let profile =
collection.profiles.swap_remove(&id).ok_or_else(|| {
anyhow!(
"No profile with ID `{id}`; options are: {}",
collection.profiles.keys().join(", ")
)
})?;
Ok(profile.data)
.map(|profile_id| {
collection.profiles.swap_remove(&profile_id).ok_or_else(|| {
anyhow!(
"No profile with ID `{profile_id}`; options are: {}",
collection.profiles.keys().join(", ")
)
})
})
.transpose()?
.unwrap_or_default();
.transpose()?;

let recipe = collection
.recipes
.swap_remove(&self.request_id)
Expand All @@ -77,10 +75,10 @@ impl Subcommand for RequestCommand {
let request = RequestBuilder::new(
recipe,
TemplateContext {
profile: profile_data,
overrides,
chains: collection.chains,
profile,
chains: collection.chains.clone(),
database: database.clone(),
overrides,
prompter: Box::new(CliPrompter),
},
)
Expand Down
134 changes: 109 additions & 25 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! responses.
use crate::{
collection::RequestRecipeId,
collection::{ProfileId, RequestRecipeId},
http::{RequestId, RequestRecord},
util::{Directory, ResultExt},
};
Expand Down Expand Up @@ -48,11 +48,13 @@ pub struct Database {
/// A unique ID for a collection. This is generated when the collection is
/// inserted into the DB.
#[derive(Copy, Clone, Debug, Display)]
#[cfg_attr(test, derive(Eq, Hash, PartialEq))]
pub struct CollectionId(Uuid);

impl Database {
/// Load the database. This will perform first-time setup, so this should
/// only be called at the main session entrypoint.
/// Load the database. This will perform migrations, but can be called from
/// anywhere in the app. The migrations will run on first connection, and
/// not after that.
pub fn load() -> anyhow::Result<Self> {
let path = Self::path()?;
info!(?path, "Loading database");
Expand Down Expand Up @@ -96,6 +98,7 @@ impl Database {
"CREATE TABLE requests (
id UUID PRIMARY KEY NOT NULL,
collection_id UUID NOT NULL,
profile_id TEXT,
recipe_id TEXT NOT NULL,
start_time TEXT NOT NULL,
end_time TEXT NOT NULL,
Expand Down Expand Up @@ -275,20 +278,26 @@ impl CollectionDatabase {
.map(PathBuf::from)
}

/// Get the most recent request+response for a recipe, or `None` if there
/// has never been one received.
/// Get the most recent request+response for a profile+recipe, or `None` if
/// there has never been one received. If the given profile is `None`, match
/// all requests that have no associated profile.
pub fn get_last_request(
&self,
profile_id: Option<&ProfileId>,
recipe_id: &RequestRecipeId,
) -> anyhow::Result<Option<RequestRecord>> {
self.database
.connection()
.query_row(
// `IS` needed for profile_id so `None` will match `NULL`
"SELECT * FROM requests
WHERE collection_id = :collection_id AND recipe_id = :recipe_id
WHERE collection_id = :collection_id
AND profile_id IS :profile_id
AND recipe_id = :recipe_id
ORDER BY start_time DESC LIMIT 1",
named_params! {
":collection_id": self.collection_id,
":profile_id": profile_id,
":recipe_id": recipe_id,
},
|row| row.try_into(),
Expand Down Expand Up @@ -316,18 +325,20 @@ impl CollectionDatabase {
requests (
id,
collection_id,
profile_id,
recipe_id,
start_time,
end_time,
request,
response,
status_code
)
VALUES (:id, :collection_id, :recipe_id, :start_time,
:end_time, :request, :response, :status_code)",
VALUES (:id, :collection_id, :profile_id, :recipe_id,
:start_time, :end_time, :request, :response, :status_code)",
named_params! {
":id": record.id,
":collection_id": self.collection_id,
":profile_id": &record.request.profile_id,
":recipe_id": &record.request.recipe_id,
":start_time": &record.start_time,
":end_time": &record.end_time,
Expand Down Expand Up @@ -431,6 +442,18 @@ impl FromSql for CollectionId {
}
}

impl ToSql for ProfileId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.deref().to_sql()
}
}

impl FromSql for ProfileId {
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
Ok(String::column_result(value)?.into())
}
}

impl ToSql for RequestId {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.0.to_sql()
Expand Down Expand Up @@ -533,6 +556,7 @@ mod tests {
use super::*;
use crate::factory::*;
use factori::create;
use std::collections::HashMap;

#[test]
fn test_merge() {
Expand All @@ -544,6 +568,7 @@ mod tests {

let record1 = create!(RequestRecord);
let record2 = create!(RequestRecord);
let profile_id = record1.request.profile_id.as_ref();
let recipe_id = &record1.request.recipe_id;
let ui_key = "key1";
collection1.insert_request(&record1).unwrap();
Expand All @@ -553,15 +578,23 @@ mod tests {

// Sanity checks
assert_eq!(
collection1.get_last_request(recipe_id).unwrap().unwrap().id,
collection1
.get_last_request(profile_id, recipe_id)
.unwrap()
.unwrap()
.id,
record1.id
);
assert_eq!(
collection1.get_ui::<_, String>(ui_key).unwrap(),
Some("value1".into())
);
assert_eq!(
collection2.get_last_request(recipe_id).unwrap().unwrap().id,
collection2
.get_last_request(profile_id, recipe_id)
.unwrap()
.unwrap()
.id,
record2.id
);
assert_eq!(
Expand All @@ -574,7 +607,11 @@ mod tests {

// Collection 2 values should've overwritten
assert_eq!(
collection1.get_last_request(recipe_id).unwrap().unwrap().id,
collection1
.get_last_request(profile_id, recipe_id)
.unwrap()
.unwrap()
.id,
record2.id
);
assert_eq!(
Expand Down Expand Up @@ -602,24 +639,71 @@ mod tests {
.into_collection(Path::new("README.md"))
.unwrap();

let record1 = create!(RequestRecord);
let record2 = create!(RequestRecord);
collection1.insert_request(&record1).unwrap();
collection2.insert_request(&record2).unwrap();

// Make sure the two have a conflicting recipe ID, which should be
// de-conflicted via the collection ID
assert_eq!(record1.request.recipe_id, record2.request.recipe_id);
let recipe_id = &record1.request.recipe_id;
// We separate requests by 3 columns. Create multiple of each column to
// make sure we filter by each column correctly
let collections = [collection1, collection2];

// Store the created request ID for each cell in the matrix, so we can
// compare to what the DB spits back later
let mut request_ids: HashMap<
(CollectionId, Option<ProfileId>, RequestRecipeId),
RequestId,
> = Default::default();

// Create and insert each request
for collection in &collections {
for profile_id in [None, Some("profile1"), Some("profile2")] {
for recipe_id in ["recipe1", "recipe2"] {
let recipe_id: RequestRecipeId = recipe_id.into();
let profile_id = profile_id.map(ProfileId::from);
let request = create!(
Request,
profile_id: profile_id.clone(),
recipe_id: recipe_id.clone(),
);
let record = create!(RequestRecord, request: request);
collection.insert_request(&record).unwrap();
request_ids.insert(
(collection.collection_id(), profile_id, recipe_id),
record.id,
);
}
}
}

assert_eq!(
collection1.get_last_request(recipe_id).unwrap().unwrap().id,
record1.id
);
assert_eq!(
collection2.get_last_request(recipe_id).unwrap().unwrap().id,
record2.id
);
// Try to find each inserted recipe individually. Also try some
// expected non-matches
for collection in &collections {
for profile_id in [None, Some("profile1"), Some("extra_profile")] {
for recipe_id in ["recipe1", "extra_recipe"] {
let collection_id = collection.collection_id();
let profile_id = profile_id.map(ProfileId::from);
let recipe_id = recipe_id.into();

// Leave the Option here so a non-match will trigger a handy
// assertion error
let record_id = collection
.get_last_request(profile_id.as_ref(), &recipe_id)
.unwrap()
.map(|record| record.id);
let expected_id = request_ids.get(&(
collection_id,
profile_id.clone(),
recipe_id.clone(),
));

assert_eq!(
record_id.as_ref(),
expected_id,
"Request mismatch for collection = {collection_id}, \
profile = {profile_id:?}, recipe = {recipe_id}"
);
}
}
}
}

/// Test UI state storage and retrieval
Expand Down
19 changes: 17 additions & 2 deletions src/factory.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
collection::{Chain, ChainSource, RequestRecipeId},
collection::{Chain, ChainSource, Profile, ProfileId, RequestRecipeId},
db::CollectionDatabase,
http::{Body, Request, RequestId, RequestRecord, Response},
template::{Prompt, Prompter, Template, TemplateContext},
Expand All @@ -9,9 +9,18 @@ use factori::{create, factori};
use indexmap::IndexMap;
use reqwest::{header::HeaderMap, Method, StatusCode};

factori!(Profile, {
default {
id = "profile1".into(),
name = None,
data = Default::default(),
}
});

factori!(Request, {
default {
id = RequestId::new(),
profile_id = None,
recipe_id = "recipe1".into(),
method = Method::GET,
url = "/url".into(),
Expand Down Expand Up @@ -59,7 +68,7 @@ factori!(Chain, {

factori!(TemplateContext, {
default {
profile = Default::default()
profile = None
chains = Default::default()
prompter = Box::<TestPrompter>::default(),
database = CollectionDatabase::testing()
Expand Down Expand Up @@ -91,6 +100,12 @@ impl Prompter for TestPrompter {
}

// Some helpful conversion implementations
impl From<&str> for ProfileId {
fn from(value: &str) -> Self {
value.to_owned().into()
}
}

impl From<&str> for RequestRecipeId {
fn from(value: &str) -> Self {
value.to_owned().into()
Expand Down
6 changes: 5 additions & 1 deletion src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ impl RequestBuilder {
pub fn new(
recipe: RequestRecipe,
template_context: TemplateContext,
) -> RequestBuilder {
) -> Self {
debug!(recipe_id = %recipe.id, "Building request from recipe");
let request_id = RequestId::new();

Expand Down Expand Up @@ -280,6 +280,10 @@ impl RequestBuilder {

Ok(Request {
id: self.id,
profile_id: template_context
.profile
.as_ref()
.map(|profile| profile.id.clone()),
recipe_id: recipe.id.clone(),
method,
url,
Expand Down
4 changes: 3 additions & 1 deletion src/http/record.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! HTTP-related data types
use crate::{
collection::RequestRecipeId,
collection::{ProfileId, RequestRecipeId},
http::{parse, ContentType},
util::ResultExt,
};
Expand Down Expand Up @@ -93,6 +93,8 @@ impl RequestRecord {
pub struct Request {
/// Unique ID for this request. Private to prevent mutation
pub id: RequestId,
/// The profile used to render this request (for historical context)
pub profile_id: Option<ProfileId>,
/// The recipe used to generate this request (for historical context)
pub recipe_id: RequestRecipeId,

Expand Down
Loading

0 comments on commit 518e766

Please sign in to comment.