-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add wrapper type for JSONPath querying
- Loading branch information
1 parent
4bc61bf
commit 48bdd73
Showing
7 changed files
with
126 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
//! Utilities for querying HTTP response data | ||
use crate::http::ResponseContent; | ||
use derive_more::{Display, FromStr}; | ||
use serde::{Deserialize, Serialize}; | ||
use serde_json_path::{ExactlyOneError, JsonPath}; | ||
use thiserror::Error; | ||
|
||
/// A wrapper around a JSONPath. This combines some common behavior, and will | ||
/// make it easy to swap out the query language in the future if necessary. | ||
#[derive(Clone, Debug, Display, FromStr, Serialize, Deserialize)] | ||
#[serde(transparent)] | ||
pub struct Query(JsonPath); | ||
|
||
#[derive(Debug, Error)] | ||
pub enum QueryError { | ||
/// Got either 0 or 2+ results for JSON path query | ||
#[error("Expected exactly one result from query")] | ||
InvalidResult { | ||
#[from] | ||
#[source] | ||
error: ExactlyOneError, | ||
}, | ||
} | ||
|
||
impl Query { | ||
/// Apply a query to some content, returning a string. The query should | ||
/// return a single result. If it's a scalar, that will be stringified. If | ||
/// it's an array/object, it'll be converted back into its input format, | ||
/// then stringified. | ||
pub fn query_to_string( | ||
&self, | ||
value: &dyn ResponseContent, | ||
) -> Result<String, QueryError> { | ||
let content_type = value.content_type(); | ||
|
||
// All content types get converted to JSON for querying, then converted | ||
// back. This is fucky but we need *some* common format | ||
let json_value = value.to_json(); | ||
let filtered = self.0.query(&json_value).exactly_one()?; | ||
|
||
// If we got a scalar value, use that. Otherwise convert back to the | ||
// input content type to re-stringify | ||
let stringified = match filtered { | ||
serde_json::Value::Null => "".into(), | ||
serde_json::Value::Number(n) => n.to_string(), | ||
serde_json::Value::Bool(b) => b.to_string(), | ||
serde_json::Value::String(s) => s.clone(), | ||
serde_json::Value::Array(_) | serde_json::Value::Object(_) => { | ||
content_type.parse_json(filtered).to_string() | ||
} | ||
}; | ||
|
||
Ok(stringified) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::{http::Json, util::assert_err}; | ||
use rstest::rstest; | ||
use serde_json::json; | ||
|
||
#[rstest] | ||
#[case("$", json(json!({"test": "hi!"})), r#"{"test":"hi!"}"#)] | ||
#[case("$.test", json(json!({"test": "hi!"})), "hi!")] | ||
#[case("$.test", json(json!({"test": 3})), "3")] | ||
#[case("$.test", json(json!({"test": true})), "true")] | ||
fn test_query_to_string( | ||
#[case] query: &str, | ||
#[case] content: Box<dyn ResponseContent>, | ||
#[case] expected: &str, | ||
) { | ||
let query = Query::from_str(query).unwrap(); | ||
let out = query.query_to_string(&*content).unwrap(); | ||
assert_eq!(out, expected); | ||
} | ||
|
||
#[rstest] | ||
#[case("$[*]", json(json!([1, 2])), "Expected exactly one result")] | ||
#[case("$[*]", json(json!([])), "Expected exactly one result")] | ||
fn test_query_to_string_error( | ||
#[case] query: &str, | ||
#[case] content: Box<dyn ResponseContent>, | ||
#[case] expected_err: &str, | ||
) { | ||
let query = Query::from_str(query).unwrap(); | ||
assert_err!(query.query_to_string(&*content), expected_err); | ||
} | ||
|
||
/// Helper to create JSON content | ||
fn json(value: serde_json::Value) -> Box<dyn ResponseContent> { | ||
Box::new(Json::from(value)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters