use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
pub fn expand(
template: &str,
substitutions: &HashMap<String, Value>,
) -> Result<String, StdUriTemplateError> {
expand_impl(template, substitutions)
}
#[derive(Debug, Clone)]
pub enum Value {
String(String),
Bool(bool),
Integer(i64),
Float(f64),
List(Vec<Value>),
Map(Vec<(String, Value)>),
}
#[derive(Debug)]
pub struct StdUriTemplateError {
message: String,
}
impl fmt::Display for StdUriTemplateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for StdUriTemplateError {}
impl StdUriTemplateError {
fn new(message: String) -> Self {
StdUriTemplateError { message }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Operator {
NoOp,
Plus,
Hash,
Dot,
Slash,
Semicolon,
QuestionMark,
Amp,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum SubstitutionType {
Empty,
String,
List,
Map,
}
fn validate_literal(c: char, col: usize) -> Result<(), StdUriTemplateError> {
match c {
'+' | '#' | '/' | ';' | '?' | '&' | ' ' | '!' | '=' | '$' | '|' | '*' | ':' | '~'
| '-' => Err(StdUriTemplateError::new(format!(
"Illegal character identified in the token at col:{}",
col
))),
_ => Ok(()),
}
}
fn get_max_char(buffer: &str, col: usize) -> Result<i32, StdUriTemplateError> {
if buffer.is_empty() {
return Ok(-1);
}
buffer.parse::<i32>().map_err(|_| {
StdUriTemplateError::new(format!("Cannot parse max chars at col:{}", col))
})
}
fn get_operator(
c: char,
token: &mut String,
col: usize,
) -> Result<Operator, StdUriTemplateError> {
match c {
'+' => Ok(Operator::Plus),
'#' => Ok(Operator::Hash),
'.' => Ok(Operator::Dot),
'/' => Ok(Operator::Slash),
';' => Ok(Operator::Semicolon),
'?' => Ok(Operator::QuestionMark),
'&' => Ok(Operator::Amp),
_ => {
validate_literal(c, col)?;
token.push(c);
Ok(Operator::NoOp)
}
}
}
fn expand_impl(
template: &str,
substitutions: &HashMap<String, Value>,
) -> Result<String, StdUriTemplateError> {
let mut result = String::with_capacity(template.len() * 2);
let mut to_token = false;
let mut token = String::new();
let mut operator: Option<Operator> = None;
let mut composite = false;
let mut to_max_char_buffer = false;
let mut max_char_buffer = String::with_capacity(3);
let mut first_token = true;
for (i, character) in template.chars().enumerate() {
match character {
'{' => {
to_token = true;
token.clear();
first_token = true;
}
'}' => {
if to_token {
let max_char = get_max_char(&max_char_buffer, i)?;
let expanded = expand_token(
operator.unwrap_or(Operator::NoOp),
&token,
composite,
max_char,
first_token,
substitutions,
&mut result,
i,
)?;
if expanded && first_token {
first_token = false;
}
to_token = false;
token.clear();
operator = None;
composite = false;
to_max_char_buffer = false;
max_char_buffer.clear();
} else {
return Err(StdUriTemplateError::new(format!(
"Failed to expand token, invalid at col:{}",
i
)));
}
}
',' if to_token => {
let max_char = get_max_char(&max_char_buffer, i)?;
let expanded = expand_token(
operator.unwrap_or(Operator::NoOp),
&token,
composite,
max_char,
first_token,
substitutions,
&mut result,
i,
)?;
if expanded && first_token {
first_token = false;
}
token.clear();
composite = false;
to_max_char_buffer = false;
max_char_buffer.clear();
}
_ => {
if to_token {
if operator.is_none() {
operator = Some(get_operator(character, &mut token, i)?);
} else if to_max_char_buffer {
if character.is_ascii_digit() {
max_char_buffer.push(character);
} else {
return Err(StdUriTemplateError::new(format!(
"Illegal character identified in the token at col:{}",
i
)));
}
} else {
match character {
':' => {
to_max_char_buffer = true;
max_char_buffer.clear();
}
'*' => {
composite = true;
}
_ => {
validate_literal(character, i)?;
token.push(character);
}
}
}
} else {
result.push(character);
}
}
}
}
if !to_token {
Ok(result)
} else {
Err(StdUriTemplateError::new("Unterminated token".to_string()))
}
}
fn add_prefix(op: Operator, result: &mut String) {
match op {
Operator::Hash => result.push('#'),
Operator::Dot => result.push('.'),
Operator::Slash => result.push('/'),
Operator::Semicolon => result.push(';'),
Operator::QuestionMark => result.push('?'),
Operator::Amp => result.push('&'),
_ => {}
}
}
fn add_separator(op: Operator, result: &mut String) {
match op {
Operator::Dot => result.push('.'),
Operator::Slash => result.push('/'),
Operator::Semicolon => result.push(';'),
Operator::QuestionMark | Operator::Amp => result.push('&'),
_ => result.push(','),
}
}
fn add_value(op: Operator, token: &str, value: &str, result: &mut String, max_char: i32) {
match op {
Operator::Plus | Operator::Hash => {
add_expanded_value(None, value, result, max_char, false);
}
Operator::QuestionMark | Operator::Amp => {
result.push_str(token);
result.push('=');
add_expanded_value(None, value, result, max_char, true);
}
Operator::Semicolon => {
result.push_str(token);
add_expanded_value(Some("="), value, result, max_char, true);
}
Operator::Dot | Operator::Slash | Operator::NoOp => {
add_expanded_value(None, value, result, max_char, true);
}
}
}
fn add_value_element(op: Operator, _token: &str, value: &str, result: &mut String, max_char: i32) {
match op {
Operator::Plus | Operator::Hash => {
add_expanded_value(None, value, result, max_char, false);
}
Operator::QuestionMark
| Operator::Amp
| Operator::Semicolon
| Operator::Dot
| Operator::Slash
| Operator::NoOp => {
add_expanded_value(None, value, result, max_char, true);
}
}
}
fn is_iprivate(cp: char) -> bool {
(0xE000..=0xF8FF).contains(&(cp as u32))
}
fn is_ucschar(cp: char) -> bool {
let code = cp as u32;
(0xA0..=0xD7FF).contains(&code)
|| (0xF900..=0xFDCF).contains(&code)
|| (0xFDF0..=0xFFEF).contains(&code)
}
fn is_unreserved(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '.' || c == '_' || c == '~'
}
fn percent_encode_char(c: char, result: &mut String) {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
for byte in encoded.as_bytes() {
result.push('%');
result.push(to_hex_digit(byte >> 4));
result.push(to_hex_digit(byte & 0x0F));
}
}
fn url_encode_char(c: char, result: &mut String) {
if is_unreserved(c) {
result.push(c);
} else {
percent_encode_char(c, result);
}
}
fn to_hex_digit(nibble: u8) -> char {
match nibble {
0..=9 => (b'0' + nibble) as char,
10..=15 => (b'A' + nibble - 10) as char,
_ => unreachable!(),
}
}
fn add_expanded_value(
prefix: Option<&str>,
value: &str,
result: &mut String,
max_char: i32,
replace_reserved: bool,
) {
let max = if max_char != -1 {
max_char as usize
} else {
usize::MAX
};
let mut to_reserved = false;
let mut reserved_buffer = String::with_capacity(3);
let mut to_append = String::with_capacity(12);
let mut prefix_pending = prefix;
for character in value.chars().take(max) {
if let Some(p) = prefix_pending.take() {
result.push_str(p);
}
if character == '%' && !replace_reserved {
to_reserved = true;
reserved_buffer.clear();
}
to_append.clear();
if replace_reserved || is_ucschar(character) || is_iprivate(character) {
url_encode_char(character, &mut to_append);
} else if !character.is_ascii() {
percent_encode_char(character, &mut to_append);
} else {
to_append.push(character);
}
if to_reserved {
reserved_buffer.push_str(&to_append);
if reserved_buffer.len() == 3 {
let is_encoded = is_valid_percent_encoded(&reserved_buffer);
if is_encoded {
result.push_str(&reserved_buffer);
} else {
result.push_str("%25");
result.push_str(&reserved_buffer[1..]);
}
to_reserved = false;
reserved_buffer.clear();
}
} else if character == ' ' {
result.push_str("%20");
} else if character == '%' {
result.push_str("%25");
} else {
result.push_str(&to_append);
}
}
if to_reserved {
result.push_str("%25");
result.push_str(&reserved_buffer[1..]);
}
}
fn is_valid_percent_encoded(s: &str) -> bool {
let b = s.as_bytes();
b.len() == 3 && b[0] == b'%' && b[1].is_ascii_hexdigit() && b[2].is_ascii_hexdigit()
}
fn get_substitution_type(
value: Option<&Value>,
_col: usize,
) -> Result<SubstitutionType, StdUriTemplateError> {
match value {
None => Ok(SubstitutionType::Empty),
Some(v) => match v {
Value::String(_) | Value::Bool(_) | Value::Integer(_) | Value::Float(_) => {
Ok(SubstitutionType::String)
}
Value::List(_) => Ok(SubstitutionType::List),
Value::Map(_) => Ok(SubstitutionType::Map),
},
}
}
fn is_empty(subst_type: SubstitutionType, value: &Value) -> bool {
match subst_type {
SubstitutionType::String => false,
SubstitutionType::List => {
if let Value::List(l) = value {
l.is_empty()
} else {
true
}
}
SubstitutionType::Map => {
if let Value::Map(m) = value {
m.is_empty()
} else {
true
}
}
SubstitutionType::Empty => true,
}
}
fn convert_native_types(value: &Value) -> Result<Cow<'_, str>, StdUriTemplateError> {
match value {
Value::String(s) => Ok(Cow::Borrowed(s)),
Value::Bool(b) => Ok(Cow::Owned(b.to_string())),
Value::Integer(i) => Ok(Cow::Owned(i.to_string())),
Value::Float(f) => {
if *f == (*f as i64) as f64 && f.is_finite() {
Ok(Cow::Owned((*f as i64).to_string()))
} else {
Ok(Cow::Owned(f.to_string()))
}
}
Value::List(_) | Value::Map(_) => Err(StdUriTemplateError::new(format!(
"Illegal class passed as substitution, found {:?}",
value
))),
}
}
#[allow(clippy::too_many_arguments)]
fn expand_token(
operator: Operator,
token: &str,
composite: bool,
max_char: i32,
first_token: bool,
substitutions: &HashMap<String, Value>,
result: &mut String,
col: usize,
) -> Result<bool, StdUriTemplateError> {
if token.is_empty() {
return Err(StdUriTemplateError::new(format!(
"Found an empty token at col:{}",
col
)));
}
let value = substitutions.get(token);
let subst_type = get_substitution_type(value, col)?;
if subst_type == SubstitutionType::Empty {
return Ok(false);
}
let value = value.unwrap();
if is_empty(subst_type, value) {
return Ok(false);
}
if first_token {
add_prefix(operator, result);
} else {
add_separator(operator, result);
}
match subst_type {
SubstitutionType::String => {
add_string_value(operator, token, value, result, max_char)?;
}
SubstitutionType::List => {
add_list_value(operator, token, value, result, max_char, composite)?;
}
SubstitutionType::Map => {
add_map_value(operator, token, value, result, max_char, composite)?;
}
SubstitutionType::Empty => {}
}
Ok(true)
}
fn add_string_value(
operator: Operator,
token: &str,
value: &Value,
result: &mut String,
max_char: i32,
) -> Result<(), StdUriTemplateError> {
let s = convert_native_types(value)?;
add_value(operator, token, &s, result, max_char);
Ok(())
}
fn add_list_value(
operator: Operator,
token: &str,
value: &Value,
result: &mut String,
max_char: i32,
composite: bool,
) -> Result<(), StdUriTemplateError> {
if let Value::List(list) = value {
let mut first = true;
for v in list {
let s = convert_native_types(v)?;
if first {
add_value(operator, token, &s, result, max_char);
first = false;
} else if composite {
add_separator(operator, result);
add_value(operator, token, &s, result, max_char);
} else {
result.push(',');
add_value_element(operator, token, &s, result, max_char);
}
}
}
Ok(())
}
fn add_map_value(
operator: Operator,
token: &str,
value: &Value,
result: &mut String,
max_char: i32,
composite: bool,
) -> Result<(), StdUriTemplateError> {
if max_char != -1 {
return Err(StdUriTemplateError::new(
"Value trimming is not allowed on Maps".to_string(),
));
}
if let Value::Map(map) = value {
let mut first = true;
for (key, val) in map {
let v = convert_native_types(val)?;
if composite {
if !first {
add_separator(operator, result);
}
add_value_element(operator, token, key, result, max_char);
result.push('=');
} else {
if first {
add_value(operator, token, key, result, max_char);
} else {
result.push(',');
add_value_element(operator, token, key, result, max_char);
}
result.push(',');
}
add_value_element(operator, token, &v, result, max_char);
first = false;
}
}
Ok(())
}