use std::fmt::Display;
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
use crate::error::Error;
use crate::http::Method;
#[derive(Debug)]
#[non_exhaustive]
pub struct Route {
pub id: &'static str,
pub service: &'static str,
pub method: Method,
pub path: &'static str,
pub pattern: &'static str,
pub account_scoped: bool,
pub resource_type: &'static str,
pub params: &'static [RouteParam],
pub idempotent: bool,
pub readonly: bool,
pub pagination: Pagination,
pub retry: Option<Retry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct RouteParam {
pub name: &'static str,
pub role: ParamRole,
pub kind: ParamKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParamRole {
Parent,
Recording,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParamKind {
String,
Bool,
Int32,
Int64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Pagination {
None,
Link {
page_param: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Retry {
pub max: u32,
pub base_delay_ms: u64,
pub retry_on: &'static [u16],
}
const PATH_SEGMENT: &AsciiSet = &NON_ALPHANUMERIC
.remove(b'-')
.remove(b'_')
.remove(b'.')
.remove(b'~');
impl Route {
pub fn fill(&self, account_id: Option<&str>, values: &[&dyn Display]) -> String {
match self.try_fill(account_id, values) {
Ok(path) => path,
Err(error) => panic!("{error}"),
}
}
pub fn try_fill(
&self,
account_id: Option<&str>,
values: &[&dyn Display],
) -> Result<String, Error> {
if values.len() != self.params.len() {
return Err(Error::usage(format!(
"{} takes {} path parameters, got {}",
self.id,
self.params.len(),
values.len()
)));
}
if account_id.is_some() != self.account_scoped {
return Err(Error::usage(format!(
"{} {} an account",
self.id,
if self.account_scoped {
"needs"
} else {
"does not take"
}
)));
}
for value in values {
let value = value.to_string();
if value.is_empty() || value == "." || value == ".." {
return Err(Error::usage(format!(
"{}: {value:?} is not a path parameter value",
self.id
)));
}
}
let mut path = self.path.to_string();
if let Some(account_id) = account_id {
path = path.replace("{accountId}", &encode(account_id));
}
for (param, value) in self.params.iter().zip(values) {
path = path.replace(&format!("{{{}}}", param.name), &encode(&value.to_string()));
}
Ok(path)
}
pub fn recognize(&self, path: &str) -> Option<Vec<(&'static str, String)>> {
let pattern_segments: Vec<&str> = self.pattern.split('/').collect();
let path_segments: Vec<&str> = path.split('/').collect();
if pattern_segments.len() != path_segments.len() {
return None;
}
let mut params = Vec::new();
for (pattern, actual) in pattern_segments.iter().zip(&path_segments) {
if let Some(name) = pattern
.strip_prefix('{')
.and_then(|rest| rest.strip_suffix('}'))
{
if actual.is_empty() {
return None;
}
let name = if name == "accountId" {
"accountId"
} else {
self.params.iter().find(|param| param.name == name)?.name
};
let value = percent_decode_str(actual).decode_utf8().ok()?;
params.push((name, value.into_owned()));
} else if pattern != actual {
return None;
}
}
Some(params)
}
pub fn retry(&self) -> Option<Retry> {
self.retry
}
}
pub(crate) fn encode(value: &str) -> String {
utf8_percent_encode(value, PATH_SEGMENT).to_string()
}