use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParameterIn {
Query,
Header,
Path,
Cookie,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParameterStyle {
Matrix,
Label,
Form,
Simple,
SpaceDelimited,
PipeDelimited,
DeepObject,
}
#[derive(Debug, Clone, Copy)]
pub enum ParameterValue<'a> {
Scalar(&'a str),
Array(&'a [&'a str]),
Object(&'a [(&'a str, &'a str)]),
}
#[derive(Debug, thiserror::Error)]
pub enum EncodeError {
#[error("style {style:?} is not valid for location {location:?}")]
InvalidLocation {
style: ParameterStyle,
location: ParameterIn,
},
#[error("style {style:?} cannot encode {shape}")]
InvalidShape {
style: ParameterStyle,
shape: &'static str,
},
#[error("style {style:?} with explode={explode} is undefined for {shape}")]
UndefinedCombination {
style: ParameterStyle,
explode: bool,
shape: &'static str,
},
}
pub fn encode_parameter(
dst: &mut String,
name: &str,
value: ParameterValue<'_>,
style: ParameterStyle,
explode: bool,
location: ParameterIn,
first: &mut bool,
) -> Result<(), EncodeError> {
validate_location(style, location)?;
match style {
ParameterStyle::Matrix => encode_matrix(dst, name, value, explode),
ParameterStyle::Label => encode_label(dst, name, value, explode),
ParameterStyle::Form => encode_form(dst, name, value, explode, first),
ParameterStyle::Simple => encode_simple(dst, name, value, explode),
ParameterStyle::SpaceDelimited => {
encode_delimited(dst, name, value, explode, first, "%20", style)
}
ParameterStyle::PipeDelimited => {
encode_delimited(dst, name, value, explode, first, "|", style)
}
ParameterStyle::DeepObject => encode_deep_object(dst, name, value, explode, first),
}
}
const UNRESERVED: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'!')
.add(b'"')
.add(b'#')
.add(b'$')
.add(b'%')
.add(b'&')
.add(b'\'')
.add(b'(')
.add(b')')
.add(b'*')
.add(b'+')
.add(b',')
.add(b'/')
.add(b':')
.add(b';')
.add(b'<')
.add(b'=')
.add(b'>')
.add(b'?')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
fn pct(out: &mut String, s: &str) {
for chunk in utf8_percent_encode(s, UNRESERVED) {
out.push_str(chunk);
}
}
fn validate_location(style: ParameterStyle, location: ParameterIn) -> Result<(), EncodeError> {
let ok = match style {
ParameterStyle::Matrix | ParameterStyle::Label => location == ParameterIn::Path,
ParameterStyle::Form => {
matches!(location, ParameterIn::Query | ParameterIn::Cookie)
}
ParameterStyle::Simple => {
matches!(location, ParameterIn::Path | ParameterIn::Header)
}
ParameterStyle::SpaceDelimited
| ParameterStyle::PipeDelimited
| ParameterStyle::DeepObject => location == ParameterIn::Query,
};
if ok {
Ok(())
} else {
Err(EncodeError::InvalidLocation { style, location })
}
}
fn shape_str(value: &ParameterValue<'_>) -> &'static str {
match value {
ParameterValue::Scalar(_) => "scalar",
ParameterValue::Array(_) => "array",
ParameterValue::Object(_) => "object",
}
}
fn encode_matrix(
dst: &mut String,
name: &str,
value: ParameterValue<'_>,
explode: bool,
) -> Result<(), EncodeError> {
match value {
ParameterValue::Scalar(v) => {
dst.push(';');
pct(dst, name);
dst.push('=');
pct(dst, v);
}
ParameterValue::Array(items) => {
if items.is_empty() {
return Ok(());
}
if explode {
for item in items {
dst.push(';');
pct(dst, name);
dst.push('=');
pct(dst, item);
}
} else {
dst.push(';');
pct(dst, name);
dst.push('=');
join_pct(dst, items.iter().copied(), ",");
}
}
ParameterValue::Object(props) => {
if props.is_empty() {
return Ok(());
}
if explode {
for (k, v) in props {
dst.push(';');
pct(dst, k);
dst.push('=');
pct(dst, v);
}
} else {
dst.push(';');
pct(dst, name);
dst.push('=');
join_pct_pairs(dst, props.iter().copied(), ",", ",");
}
}
}
Ok(())
}
fn encode_label(
dst: &mut String,
_name: &str,
value: ParameterValue<'_>,
explode: bool,
) -> Result<(), EncodeError> {
match value {
ParameterValue::Scalar(v) => {
dst.push('.');
pct(dst, v);
}
ParameterValue::Array(items) => {
if items.is_empty() {
return Ok(());
}
dst.push('.');
let sep = if explode { "." } else { "," };
join_pct(dst, items.iter().copied(), sep);
}
ParameterValue::Object(props) => {
if props.is_empty() {
return Ok(());
}
dst.push('.');
if explode {
join_pct_pairs(dst, props.iter().copied(), "=", ".");
} else {
join_pct_pairs(dst, props.iter().copied(), ",", ",");
}
}
}
Ok(())
}
fn encode_form(
dst: &mut String,
name: &str,
value: ParameterValue<'_>,
explode: bool,
first: &mut bool,
) -> Result<(), EncodeError> {
let prefix = |dst: &mut String, first: &mut bool| {
dst.push(if *first { '?' } else { '&' });
*first = false;
};
match value {
ParameterValue::Scalar(v) => {
prefix(dst, first);
pct(dst, name);
dst.push('=');
pct(dst, v);
}
ParameterValue::Array(items) => {
if items.is_empty() {
return Ok(());
}
if explode {
for item in items {
prefix(dst, first);
pct(dst, name);
dst.push('=');
pct(dst, item);
}
} else {
prefix(dst, first);
pct(dst, name);
dst.push('=');
join_pct(dst, items.iter().copied(), ",");
}
}
ParameterValue::Object(props) => {
if props.is_empty() {
return Ok(());
}
if explode {
for (k, v) in props {
prefix(dst, first);
pct(dst, k);
dst.push('=');
pct(dst, v);
}
} else {
prefix(dst, first);
pct(dst, name);
dst.push('=');
join_pct_pairs(dst, props.iter().copied(), ",", ",");
}
}
}
Ok(())
}
fn encode_simple(
dst: &mut String,
_name: &str,
value: ParameterValue<'_>,
explode: bool,
) -> Result<(), EncodeError> {
match value {
ParameterValue::Scalar(v) => pct(dst, v),
ParameterValue::Array(items) => join_pct(dst, items.iter().copied(), ","),
ParameterValue::Object(props) => {
let kv_sep = if explode { "=" } else { "," };
join_pct_pairs(dst, props.iter().copied(), kv_sep, ",");
}
}
Ok(())
}
fn encode_delimited(
dst: &mut String,
name: &str,
value: ParameterValue<'_>,
explode: bool,
first: &mut bool,
join: &str,
style: ParameterStyle,
) -> Result<(), EncodeError> {
match value {
ParameterValue::Array(items) => {
if items.is_empty() {
return Ok(());
}
if explode {
for item in items {
dst.push(if *first { '?' } else { '&' });
*first = false;
pct(dst, name);
dst.push('=');
pct(dst, item);
}
} else {
dst.push(if *first { '?' } else { '&' });
*first = false;
pct(dst, name);
dst.push('=');
join_pct(dst, items.iter().copied(), join);
}
Ok(())
}
other => Err(EncodeError::InvalidShape {
style,
shape: shape_str(&other),
}),
}
}
fn encode_deep_object(
dst: &mut String,
name: &str,
value: ParameterValue<'_>,
explode: bool,
first: &mut bool,
) -> Result<(), EncodeError> {
let ParameterValue::Object(props) = value else {
return Err(EncodeError::InvalidShape {
style: ParameterStyle::DeepObject,
shape: shape_str(&value),
});
};
if !explode {
return Err(EncodeError::UndefinedCombination {
style: ParameterStyle::DeepObject,
explode,
shape: "object",
});
}
for (k, v) in props {
dst.push(if *first { '?' } else { '&' });
*first = false;
pct(dst, name);
dst.push('[');
pct(dst, k);
dst.push(']');
dst.push('=');
pct(dst, v);
}
Ok(())
}
fn join_pct<'a, I: IntoIterator<Item = &'a str>>(out: &mut String, items: I, sep: &str) {
let mut first = true;
for item in items {
if !first {
out.push_str(sep);
}
first = false;
pct(out, item);
}
}
fn join_pct_pairs<'a, I: IntoIterator<Item = (&'a str, &'a str)>>(
out: &mut String,
pairs: I,
kv_sep: &str,
pair_sep: &str,
) {
let mut first = true;
for (k, v) in pairs {
if !first {
out.push_str(pair_sep);
}
first = false;
pct(out, k);
out.push_str(kv_sep);
pct(out, v);
}
}
#[derive(Debug, thiserror::Error)]
pub enum SerializeEncodeError {
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("value shape is not encodable as a parameter: {reason}")]
UnsupportedShape {
reason: &'static str,
},
#[error(transparent)]
Encode(#[from] EncodeError),
}
pub fn encode_serialized<T>(
dst: &mut String,
name: &str,
value: &T,
style: ParameterStyle,
explode: bool,
location: ParameterIn,
first: &mut bool,
) -> Result<(), SerializeEncodeError>
where
T: serde::Serialize + ?Sized,
{
let json = serde_json::to_value(value)?;
encode_serialized_value(dst, name, &json, style, explode, location, first)
}
pub fn encode_serialized_value(
dst: &mut String,
name: &str,
json: &serde_json::Value,
style: ParameterStyle,
explode: bool,
location: ParameterIn,
first: &mut bool,
) -> Result<(), SerializeEncodeError> {
use serde_json::Value;
if json.is_null() {
return Ok(());
}
match json {
Value::Null => Ok(()),
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
let scalar = scalar_to_string(json);
encode_parameter(
dst,
name,
ParameterValue::Scalar(&scalar),
style,
explode,
location,
first,
)?;
Ok(())
}
Value::Array(items) => {
let mut owned: Vec<String> = Vec::with_capacity(items.len());
for item in items {
if !is_primitive(item) {
return Err(SerializeEncodeError::UnsupportedShape {
reason: "array element is not a primitive",
});
}
if item.is_null() {
continue;
}
owned.push(scalar_to_string(item));
}
let refs: Vec<&str> = owned.iter().map(String::as_str).collect();
encode_parameter(
dst,
name,
ParameterValue::Array(&refs),
style,
explode,
location,
first,
)?;
Ok(())
}
Value::Object(map) => {
let mut owned: Vec<(String, String)> = Vec::with_capacity(map.len());
for (k, v) in map {
if !is_primitive(v) {
return Err(SerializeEncodeError::UnsupportedShape {
reason: "object property value is not a primitive",
});
}
if v.is_null() {
continue;
}
owned.push((k.clone(), scalar_to_string(v)));
}
let refs: Vec<(&str, &str)> = owned
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
encode_parameter(
dst,
name,
ParameterValue::Object(&refs),
style,
explode,
location,
first,
)?;
Ok(())
}
}
}
fn is_primitive(v: &serde_json::Value) -> bool {
matches!(
v,
serde_json::Value::Null
| serde_json::Value::Bool(_)
| serde_json::Value::Number(_)
| serde_json::Value::String(_)
)
}
fn scalar_to_string(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Bool(b) => b.to_string(),
serde_json::Value::Number(n) => n.to_string(),
_ => v.to_string(),
}
}
#[cfg(test)]
mod test;