Skip to content
Open
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
731 changes: 449 additions & 282 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ futures = "0.3.31"
futures-core = "0.3.31"
heck = "0.5.0"
http = "1.4.0"
httpmock = "0.8.0"
hyper = "1.8.1"
indexmap = "2.13.0"
openapiv3 = "2.2.0"
Expand Down
17 changes: 17 additions & 0 deletions progenitor-client/src/progenitor_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,23 @@ pub fn encode_path(pc: &str) -> String {
percent_encoding::utf8_percent_encode(pc, PATH_SET).to_string()
}

#[doc(hidden)]
/// Serialize a query parameter value into decoded `(key, value)` pairs using
/// the same rules as [`QueryParam`].
///
/// Intended for generated clients and mocks; values are not percent-encoded.
pub fn query_param_pairs<T>(name: &str, value: &T) -> Result<Vec<(String, String)>, String>
where
T: Serialize,
{
let query =
serde_urlencoded::to_string(QueryParam::new(name, value)).map_err(|err| err.to_string())?;
if query.is_empty() {
return Ok(Vec::new());
}
serde_urlencoded::from_str::<Vec<(String, String)>>(&query).map_err(|err| err.to_string())
}

#[doc(hidden)]
pub trait RequestBuilderExt<E> {
fn form_urlencoded<T: Serialize + ?Sized>(self, body: &T) -> Result<RequestBuilder, Error<E>>;
Expand Down
111 changes: 110 additions & 1 deletion progenitor-client/tests/client_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
error::Error,
};

use progenitor_client::{encode_path, QueryParam};
use progenitor_client::{encode_path, query_param_pairs, QueryParam};
use serde::Serialize;

#[test]
Expand All @@ -24,6 +24,11 @@ fn encode_query_param<T: Serialize>(param_name: &str, value: &T) -> Result<Strin
Ok(url.query().unwrap().to_owned())
}

/// Helper to unwrap `query_param_pairs` for test assertions.
fn collect_query_pairs<T: Serialize>(param_name: &str, value: &T) -> Vec<(String, String)> {
query_param_pairs(param_name, value).expect("failed to serialize query param pairs")
}

#[test]
fn test_query_scalars() {
let value = "xyz".to_string();
Expand All @@ -39,6 +44,18 @@ fn test_query_scalars() {
assert_eq!(result, "param_name=-0.05");
}

/// query_param_pairs mirrors QueryParam for scalar values.
#[test]
fn test_query_param_pairs_scalars() {
let value = "xyz".to_string();
let result = collect_query_pairs("param_name", &value);
assert_eq!(result, vec![("param_name".to_string(), "xyz".to_string())]);

let value = 42;
let result = collect_query_pairs("param_name", &value);
assert_eq!(result, vec![("param_name".to_string(), "42".to_string())]);
}

#[test]
fn test_query_arrays() {
let value = vec!["a", "b", "c"];
Expand All @@ -65,6 +82,44 @@ fn test_query_arrays() {
);
}

/// query_param_pairs repeats keys for array and set values.
#[test]
fn test_query_param_pairs_arrays() {
let value = vec!["a", "b", "c"];
let result = collect_query_pairs("paramName", &value);
assert_eq!(
result,
vec![
("paramName".to_string(), "a".to_string()),
("paramName".to_string(), "b".to_string()),
("paramName".to_string(), "c".to_string()),
]
);

let value = ["a", "b", "c"].into_iter().collect::<BTreeSet<_>>();
let result = collect_query_pairs("paramName", &value);
assert_eq!(
result,
vec![
("paramName".to_string(), "a".to_string()),
("paramName".to_string(), "b".to_string()),
("paramName".to_string(), "c".to_string()),
]
);

let value = ["a", "b", "c"].into_iter().collect::<HashSet<_>>();
let mut result = collect_query_pairs("paramName", &value);
result.sort();
assert_eq!(
result,
vec![
("paramName".to_string(), "a".to_string()),
("paramName".to_string(), "b".to_string()),
("paramName".to_string(), "c".to_string()),
]
);
}

#[test]
fn test_query_object() {
#[derive(Serialize)]
Expand All @@ -81,6 +136,40 @@ fn test_query_object() {
assert_eq!(result, "a=a+value&b=b+value");
}

/// query_param_pairs emits object fields and map entries as pairs.
#[test]
fn test_query_param_pairs_object_and_map() {
#[derive(Serialize)]
struct Value {
a: String,
b: String,
}
let value = Value {
a: "a value".to_string(),
b: "b value".to_string(),
};
let result = collect_query_pairs("ignored", &value);
assert_eq!(
result,
vec![
("a".to_string(), "a value".to_string()),
("b".to_string(), "b value".to_string()),
]
);

let value = [("a", "a value"), ("b", "b value")]
.into_iter()
.collect::<BTreeMap<_, _>>();
let result = collect_query_pairs("ignored", &value);
assert_eq!(
result,
vec![
("a".to_string(), "a value".to_string()),
("b".to_string(), "b value".to_string()),
]
);
}

#[test]
fn test_query_map() {
let value = [("a", "a value"), ("b", "b value")]
Expand Down Expand Up @@ -115,6 +204,18 @@ fn test_query_enum_external() {
encode_query_param("ignored", &value).expect_err("variant not supported");
}

/// query_param_pairs rejects unsupported enum variants.
#[test]
fn test_query_param_pairs_invalid_variant() {
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum Value {
Tuple(u64, u64),
}
let value = Value::Tuple(1, 2);
assert!(query_param_pairs("ignored", &value).is_err());
}

#[test]
fn test_query_enum_internal() {
#[derive(Serialize)]
Expand Down Expand Up @@ -219,3 +320,11 @@ fn test_query_option() {
let result = encode_query_param("paramName", &value).unwrap();
assert_eq!(result, "paramName=42");
}

/// query_param_pairs yields no pairs for empty options.
#[test]
fn test_query_param_pairs_empty() {
let value = Option::<u64>::None;
let result = collect_query_pairs("ignored", &value);
assert!(result.is_empty());
}
1 change: 1 addition & 0 deletions progenitor-impl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dropshot = { workspace = true }
expectorate = { workspace = true }
futures = { workspace = true }
http = { workspace = true }
httpmock = { workspace = true }
hyper = { workspace = true }
progenitor-client = { workspace = true }
reqwest = { workspace = true }
Expand Down
26 changes: 22 additions & 4 deletions progenitor-impl/src/httpmock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ impl Generator {

use #crate_path::*;

/// Apply decoded query parameter pairs to the matcher.
fn apply_query_param_pairs(
mut when: ::httpmock::When,
pairs: &[(String, String)],
) -> ::httpmock::When {
for (key, value) in pairs {
when = when.query_param(key, value);
}
when
}

#(
pub struct #when(::httpmock::When);
#when_impl
Expand Down Expand Up @@ -189,7 +200,12 @@ impl Generator {
OperationParameterKind::Query(true) => (
true,
quote! {
Self(self.0.query_param(#api_name, value.to_string()))
let expected_pairs = ::progenitor_client::query_param_pairs(
#api_name,
&value,
)
.expect("failed to serialize query param");
Self(apply_query_param_pairs(self.0, &expected_pairs))
},
),
OperationParameterKind::Header(true) => (
Expand All @@ -203,10 +219,12 @@ impl Generator {
false,
quote! {
if let Some(value) = value.into() {
Self(self.0.query_param(
let expected_pairs = ::progenitor_client::query_param_pairs(
#api_name,
value.to_string(),
))
&value,
)
.expect("failed to serialize query param");
Self(apply_query_param_pairs(self.0, &expected_pairs))
} else {
Self(self.0.query_param_missing(#api_name))
}
Expand Down
20 changes: 18 additions & 2 deletions progenitor-impl/tests/output/src/buildomat_httpmock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ pub mod operations {
#![doc = r" its inner type with a call to `into_inner()`. This can"]
#![doc = r" be used to explicitly deviate from permitted values."]
use crate::buildomat_builder::*;
#[doc = r" Apply decoded query parameter pairs to the matcher."]
fn apply_query_param_pairs(
mut when: ::httpmock::When,
pairs: &[(String, String)],
) -> ::httpmock::When {
for (key, value) in pairs {
when = when.query_param(key, value);
}

when
}

pub struct ControlHoldWhen(::httpmock::When);
impl ControlHoldWhen {
pub fn new(inner: ::httpmock::When) -> Self {
Expand Down Expand Up @@ -208,7 +220,9 @@ pub mod operations {
T: Into<Option<u32>>,
{
if let Some(value) = value.into() {
Self(self.0.query_param("minseq", value.to_string()))
let expected_pairs = ::progenitor_client::query_param_pairs("minseq", &value)
.expect("failed to serialize query param");
Self(apply_query_param_pairs(self.0, &expected_pairs))
} else {
Self(self.0.query_param_missing("minseq"))
}
Expand Down Expand Up @@ -756,7 +770,9 @@ pub mod operations {
T: Into<Option<&'a types::GetThingOrThingsId>>,
{
if let Some(value) = value.into() {
Self(self.0.query_param("id", value.to_string()))
let expected_pairs = ::progenitor_client::query_param_pairs("id", &value)
.expect("failed to serialize query param");
Self(apply_query_param_pairs(self.0, &expected_pairs))
} else {
Self(self.0.query_param_missing("id"))
}
Expand Down
16 changes: 15 additions & 1 deletion progenitor-impl/tests/output/src/cli_gen_httpmock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ pub mod operations {
#![doc = r" its inner type with a call to `into_inner()`. This can"]
#![doc = r" be used to explicitly deviate from permitted values."]
use crate::cli_gen_builder::*;
#[doc = r" Apply decoded query parameter pairs to the matcher."]
fn apply_query_param_pairs(
mut when: ::httpmock::When,
pairs: &[(String, String)],
) -> ::httpmock::When {
for (key, value) in pairs {
when = when.query_param(key, value);
}

when
}

pub struct UnoWhen(::httpmock::When);
impl UnoWhen {
pub fn new(inner: ::httpmock::When) -> Self {
Expand All @@ -19,7 +31,9 @@ pub mod operations {
}

pub fn gateway(self, value: &str) -> Self {
Self(self.0.query_param("gateway", value.to_string()))
let expected_pairs = ::progenitor_client::query_param_pairs("gateway", &value)
.expect("failed to serialize query param");
Self(apply_query_param_pairs(self.0, &expected_pairs))
}

pub fn body(self, value: &types::UnoBody) -> Self {
Expand Down
Loading