use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ParameterMetadata {
pub name: String,
pub source: ParameterSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_type: Option<String>,
pub required: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema: Option<Value>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ParameterSource {
Path,
Query,
Header,
Cookie,
}
impl std::fmt::Display for ParameterSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Path => write!(f, "path"),
Self::Query => write!(f, "query"),
Self::Header => write!(f, "header"),
Self::Cookie => write!(f, "cookie"),
}
}
}
impl std::str::FromStr for ParameterSource {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"path" => Ok(Self::Path),
"query" => Ok(Self::Query),
"header" => Ok(Self::Header),
"cookie" => Ok(Self::Cookie),
_ => Err(format!("Unknown parameter source: {}", s)),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExtractedRouteMetadata {
pub parameters: Vec<ParameterMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_schema: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub response_schema: Option<Value>,
}
pub struct MetadataBuilder {
parameters: Vec<ParameterMetadata>,
request_schema: Option<Value>,
response_schema: Option<Value>,
}
impl Default for MetadataBuilder {
fn default() -> Self {
Self::new()
}
}
impl MetadataBuilder {
pub fn new() -> Self {
Self {
parameters: Vec::new(),
request_schema: None,
response_schema: None,
}
}
pub fn with_parameter(mut self, param: ParameterMetadata) -> Self {
self.parameters.push(param);
self
}
pub fn with_parameters(mut self, params: Vec<ParameterMetadata>) -> Self {
self.parameters.extend(params);
self
}
pub fn with_request_schema(mut self, schema: Option<Value>) -> Self {
self.request_schema = schema;
self
}
pub fn with_response_schema(mut self, schema: Option<Value>) -> Self {
self.response_schema = schema;
self
}
pub fn build(self) -> ExtractedRouteMetadata {
ExtractedRouteMetadata {
parameters: self.parameters,
request_schema: self.request_schema,
response_schema: self.response_schema,
}
}
}
pub fn extract_path_parameters(path: &str) -> Vec<ParameterMetadata> {
let mut params = Vec::new();
let mut in_brace = false;
let mut current_param = String::new();
for ch in path.chars() {
match ch {
'{' => in_brace = true,
'}' => {
if !current_param.is_empty() {
params.push(ParameterMetadata {
name: current_param.clone(),
source: ParameterSource::Path,
schema_type: Some("string".to_string()),
required: true,
schema: None,
});
current_param.clear();
}
in_brace = false;
}
_ if in_brace => current_param.push(ch),
_ => {}
}
}
params
}
pub fn parse_parameter_schema(schema: &Value) -> Result<Vec<ParameterMetadata>, String> {
let mut params = Vec::new();
let Some(props) = schema.get("properties").and_then(|p| p.as_object()) else {
return Ok(params);
};
let required: Vec<String> = schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default();
for (param_name, param_schema) in props {
let is_required = required.contains(param_name);
let source = param_schema
.get("source")
.and_then(|s| s.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(ParameterSource::Query);
let schema_type = param_schema.get("type").and_then(|t| t.as_str()).map(String::from);
params.push(ParameterMetadata {
name: param_name.clone(),
source,
schema_type,
required: is_required,
schema: Some(param_schema.clone()),
});
}
Ok(params)
}
pub fn validate_metadata(metadata: &ExtractedRouteMetadata) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
for param in &metadata.parameters {
if param.name.is_empty() {
errors.push("Parameter name cannot be empty".to_string());
}
if let Some(schema) = ¶m.schema {
if !schema.is_object() {
errors.push(format!("Parameter schema for '{}' must be an object", param.name));
}
}
}
if let Some(schema) = &metadata.request_schema {
if !schema.is_object() {
errors.push("Request schema must be an object".to_string());
}
}
if let Some(schema) = &metadata.response_schema {
if !schema.is_object() {
errors.push("Response schema must be an object".to_string());
}
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
pub fn merge_parameters(
path_params: Vec<ParameterMetadata>,
schema: Option<&Value>,
) -> Result<Vec<ParameterMetadata>, String> {
let mut merged: HashMap<String, ParameterMetadata> = HashMap::new();
for param in path_params {
merged.insert(param.name.clone(), param);
}
if let Some(schema_obj) = schema {
let schema_params = parse_parameter_schema(schema_obj)?;
for param in schema_params {
merged.insert(param.name.clone(), param);
}
}
Ok(merged.into_values().collect())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_extract_path_parameters() {
let params = extract_path_parameters("/users/{user_id}/posts/{post_id}");
assert_eq!(params.len(), 2);
assert_eq!(params[0].name, "user_id");
assert_eq!(params[1].name, "post_id");
assert!(params.iter().all(|p| p.source == ParameterSource::Path));
}
#[test]
fn test_extract_no_path_parameters() {
let params = extract_path_parameters("/users");
assert!(params.is_empty());
}
#[test]
fn test_parse_parameter_schema() {
let schema = json!({
"type": "object",
"properties": {
"name": {"type": "string", "source": "query"},
"age": {"type": "integer", "source": "query"}
},
"required": ["name"]
});
let params = parse_parameter_schema(&schema).unwrap();
assert_eq!(params.len(), 2);
let name_param = params.iter().find(|p| p.name == "name").unwrap();
assert!(name_param.required);
let age_param = params.iter().find(|p| p.name == "age").unwrap();
assert!(!age_param.required);
}
#[test]
fn test_merge_parameters() {
let path_params = vec![ParameterMetadata {
name: "user_id".to_string(),
source: ParameterSource::Path,
schema_type: Some("string".to_string()),
required: true,
schema: None,
}];
let schema = json!({
"type": "object",
"properties": {
"limit": {"type": "integer", "source": "query"}
},
"required": []
});
let merged = merge_parameters(path_params, Some(&schema)).unwrap();
assert_eq!(merged.len(), 2);
assert!(merged.iter().any(|p| p.name == "user_id"));
assert!(merged.iter().any(|p| p.name == "limit"));
}
#[test]
fn test_parameter_source_display() {
assert_eq!(ParameterSource::Path.to_string(), "path");
assert_eq!(ParameterSource::Query.to_string(), "query");
assert_eq!(ParameterSource::Header.to_string(), "header");
assert_eq!(ParameterSource::Cookie.to_string(), "cookie");
}
#[test]
fn test_parameter_source_from_str() {
assert_eq!("path".parse(), Ok(ParameterSource::Path));
assert_eq!("query".parse(), Ok(ParameterSource::Query));
}
#[test]
fn test_validate_metadata_success() {
let metadata = ExtractedRouteMetadata {
parameters: vec![ParameterMetadata {
name: "id".to_string(),
source: ParameterSource::Path,
schema_type: Some("string".to_string()),
required: true,
schema: None,
}],
request_schema: None,
response_schema: None,
};
assert!(validate_metadata(&metadata).is_ok());
}
#[test]
fn test_validate_metadata_empty_param_name() {
let metadata = ExtractedRouteMetadata {
parameters: vec![ParameterMetadata {
name: String::new(),
source: ParameterSource::Query,
schema_type: None,
required: false,
schema: None,
}],
request_schema: None,
response_schema: None,
};
let result = validate_metadata(&metadata);
assert!(result.is_err());
}
}