Skip to content
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
52 changes: 52 additions & 0 deletions gotcha/tests/test_path_params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,58 @@ fn test_path_tuple_parameter() {
}
}

#[cfg(feature = "openapi")]
mod path_struct {
use either::Either;
use gotcha::{ParameterProvider, Path, Schematic};
use oas::{Parameter, ParameterIn, Referenceable};

// Only the derived Schematic impl is exercised; the fields are never read directly.
#[allow(dead_code)]
#[derive(Schematic)]
struct ConnectionPath {
user_id: String,
connection_id: String,
}

fn assert_per_field_params(params: &[Parameter]) {
assert_eq!(params.len(), 2, "each struct field should become one parameter");
for (param, name) in params.iter().zip(["user_id", "connection_id"]) {
assert_eq!(param.name, name);
assert!(matches!(param._in, ParameterIn::Path));
assert_eq!(param.required, Some(true));
let Some(Referenceable::Data(schema)) = &param.schema else {
panic!("parameter '{name}' should carry an inline schema");
};
assert_eq!(schema._type.as_deref(), Some("string"), "parameter '{name}' should have the field's type");
assert!(!schema.extras.contains_key("$ref"), "parameter '{name}' must not be a $ref to the whole struct");
}
}

#[test]
fn struct_fields_become_parameters() {
let url = "/users/{user_id}/connections/{connection_id}".to_string();
let result = <Path<ConnectionPath> as ParameterProvider>::generate(url);
match result {
Either::Left(params) => assert_per_field_params(&params),
Either::Right(_) => panic!("Path<ConnectionPath> should generate parameters, not a request body"),
}
}

#[test]
fn struct_fields_become_parameters_inside_a_collection_scope() {
// Spec assembly generates every operation inside a collection scope, where a derived
// struct's generate_schema() returns a bare `$ref`. Parameters must still come out
// per-field — not one parameter holding the whole object's `$ref`, dropping the rest.
let url = "/users/{user_id}/connections/{connection_id}".to_string();
let (result, _schemas) = gotcha_core::registry::collect(|| <Path<ConnectionPath> as ParameterProvider>::generate(url));
match result {
Either::Left(params) => assert_per_field_params(&params),
Either::Right(_) => panic!("Path<ConnectionPath> should generate parameters, not a request body"),
}
}
}

#[cfg(feature = "openapi")]
#[test]
fn test_multiple_path_params() {
Expand Down
23 changes: 20 additions & 3 deletions gotcha_core/src/parameter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,29 @@ impl<T1: Schematic, T2: Schematic> ParameterProvider for Path<(T1, T2)> {

impl<T: Schematic> ParameterProvider for Path<T> {
fn generate(url: String) -> Either<Vec<Parameter>, RequestBody> {
// Case 1: a struct extractor — each field becomes one path parameter. Read the fields
// directly (as `Query` does) instead of scraping `properties` out of the generated
// schema: during spec assembly a collection scope is active, so a derived struct's
// `generate_schema()` returns a bare `$ref` carrying no properties at all.
let fields = T::fields();
if !fields.is_empty() {
return Either::Left(
fields
.into_iter()
.map(|(name, schema)| {
let desc = schema.schema.description.clone();
build_param(name.to_string(), ParameterIn::Path, schema.required, schema.schema, desc)
})
.collect(),
);
}

let mut ret = vec![];
let mut schema = T::generate_schema();

// Check if this is a struct with properties or a simple type
// Check if this is a hand-written impl exposing properties or a simple type
if let Some(mut properties) = schema.schema.extras.remove("properties") {
// Case 1: Struct with properties - each property becomes a path parameter
// Case 2: object schema with properties - each property becomes a path parameter
if let Some(properties) = properties.as_object_mut() {
properties.iter_mut().for_each(|(key, value)| {
let schema = serde_json::from_value(value.clone()).unwrap();
Expand All @@ -98,7 +115,7 @@ impl<T: Schematic> ParameterProvider for Path<T> {
})
}
} else {
// Case 2: Simple type like Uuid - extract parameter name from URL
// Case 3: Simple type like Uuid - extract parameter name from URL
// Since axum 0.8 a captured segment is written `{name}`, matching OpenAPI's own syntax.
let pattern = regex::Regex::new(r"\{([^}]+)\}").unwrap();
let param_names_in_path: Vec<String> = pattern.captures_iter(&url).map(|digits| digits.get(1).unwrap().as_str().to_string()).collect();
Expand Down
Loading