2022-02-08 16:59:38 +00:00
|
|
|
// Copyright 2022 Oxide Computer Company
|
2021-10-17 17:40:22 +00:00
|
|
|
|
2022-03-08 22:31:56 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2021-10-02 18:40:03 +00:00
|
|
|
use proc_macro2::TokenStream;
|
|
|
|
use quote::{format_ident, quote};
|
2021-07-01 08:41:42 +00:00
|
|
|
|
2021-10-17 17:40:22 +00:00
|
|
|
use crate::{Error, Result};
|
|
|
|
|
2021-07-01 08:41:42 +00:00
|
|
|
#[derive(Eq, PartialEq, Clone, Debug)]
|
|
|
|
enum Component {
|
|
|
|
Constant(String),
|
|
|
|
Parameter(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Eq, PartialEq, Clone, Debug)]
|
2021-12-10 02:15:24 +00:00
|
|
|
pub struct PathTemplate {
|
2021-07-01 08:41:42 +00:00
|
|
|
components: Vec<Component>,
|
|
|
|
}
|
|
|
|
|
2021-12-10 02:15:24 +00:00
|
|
|
impl PathTemplate {
|
2022-03-08 22:31:56 +00:00
|
|
|
pub fn compile(&self, rename: HashMap<&String, &String>) -> TokenStream {
|
2021-10-02 18:40:03 +00:00
|
|
|
let mut fmt = String::new();
|
|
|
|
fmt.push_str("{}");
|
2021-07-01 08:41:42 +00:00
|
|
|
for c in self.components.iter() {
|
2021-10-02 18:40:03 +00:00
|
|
|
fmt.push('/');
|
2021-07-01 08:41:42 +00:00
|
|
|
match c {
|
2021-10-02 18:40:03 +00:00
|
|
|
Component::Constant(n) => fmt.push_str(n),
|
|
|
|
Component::Parameter(_) => fmt.push_str("{}"),
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-02 18:40:03 +00:00
|
|
|
|
|
|
|
let components = self.components.iter().filter_map(|component| {
|
|
|
|
if let Component::Parameter(n) = &component {
|
2022-03-08 22:31:56 +00:00
|
|
|
let param = format_ident!(
|
|
|
|
"{}",
|
|
|
|
rename
|
|
|
|
.get(&n)
|
|
|
|
.expect(&format!("missing path name mapping {}", n)),
|
|
|
|
);
|
2021-10-02 18:40:03 +00:00
|
|
|
Some(quote! {
|
2022-02-08 16:59:38 +00:00
|
|
|
progenitor_client::encode_path(&#param.to_string())
|
2021-10-02 18:40:03 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
None
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
2021-10-02 18:40:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
quote! {
|
|
|
|
let url = format!(#fmt, self.baseurl, #(#components,)*);
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
2021-09-18 04:17:21 +00:00
|
|
|
|
|
|
|
pub fn names(&self) -> Vec<String> {
|
|
|
|
self.components
|
|
|
|
.iter()
|
|
|
|
.filter_map(|c| match c {
|
|
|
|
Component::Parameter(name) => Some(name.to_string()),
|
|
|
|
Component::Constant(_) => None,
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
|
2021-12-10 02:15:24 +00:00
|
|
|
pub fn parse(t: &str) -> Result<PathTemplate> {
|
2021-07-01 08:41:42 +00:00
|
|
|
enum State {
|
|
|
|
Start,
|
|
|
|
ConstantOrParameter,
|
|
|
|
Parameter,
|
2021-07-02 00:11:48 +00:00
|
|
|
ParameterSlash,
|
2021-07-01 08:41:42 +00:00
|
|
|
Constant,
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut s = State::Start;
|
|
|
|
let mut a = String::new();
|
|
|
|
let mut components = Vec::new();
|
|
|
|
|
|
|
|
for c in t.chars() {
|
|
|
|
match s {
|
|
|
|
State::Start => {
|
|
|
|
if c == '/' {
|
|
|
|
s = State::ConstantOrParameter;
|
|
|
|
} else {
|
2021-10-17 17:40:22 +00:00
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"path must start with a slash".to_string(),
|
|
|
|
));
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
State::ConstantOrParameter => {
|
|
|
|
if c == '/' || c == '}' {
|
2021-10-17 17:40:22 +00:00
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"expected a constant or parameter".to_string(),
|
|
|
|
));
|
2021-07-01 08:41:42 +00:00
|
|
|
} else if c == '{' {
|
|
|
|
s = State::Parameter;
|
|
|
|
} else {
|
|
|
|
s = State::Constant;
|
|
|
|
a.push(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
State::Constant => {
|
|
|
|
if c == '/' {
|
|
|
|
components.push(Component::Constant(a));
|
|
|
|
a = String::new();
|
|
|
|
s = State::ConstantOrParameter;
|
|
|
|
} else if c == '{' || c == '}' {
|
2021-10-17 17:40:22 +00:00
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"unexpected parameter".to_string(),
|
|
|
|
));
|
2021-07-01 08:41:42 +00:00
|
|
|
} else {
|
|
|
|
a.push(c);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
State::Parameter => {
|
|
|
|
if c == '}' {
|
|
|
|
components.push(Component::Parameter(a));
|
|
|
|
a = String::new();
|
2021-07-02 00:11:48 +00:00
|
|
|
s = State::ParameterSlash;
|
2021-07-01 08:41:42 +00:00
|
|
|
} else if c == '/' || c == '{' {
|
2021-10-17 17:40:22 +00:00
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"unexpected parameter".to_string(),
|
|
|
|
));
|
2021-07-01 08:41:42 +00:00
|
|
|
} else {
|
|
|
|
a.push(c);
|
|
|
|
}
|
|
|
|
}
|
2021-07-02 00:11:48 +00:00
|
|
|
State::ParameterSlash => {
|
|
|
|
if c == '/' {
|
|
|
|
s = State::ConstantOrParameter;
|
|
|
|
} else {
|
2021-10-17 17:40:22 +00:00
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"expected a slah after parameter".to_string(),
|
|
|
|
));
|
2021-07-02 00:11:48 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
match s {
|
2021-10-17 17:40:22 +00:00
|
|
|
State::Start => {
|
|
|
|
return Err(Error::InvalidPath("empty path".to_string()))
|
|
|
|
}
|
2021-07-02 00:11:48 +00:00
|
|
|
State::ConstantOrParameter | State::ParameterSlash => (),
|
2021-07-01 08:41:42 +00:00
|
|
|
State::Constant => components.push(Component::Constant(a)),
|
2021-10-17 17:40:22 +00:00
|
|
|
State::Parameter => {
|
|
|
|
return Err(Error::InvalidPath(
|
|
|
|
"unterminated parameter".to_string(),
|
|
|
|
))
|
|
|
|
}
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
|
2021-12-10 02:15:24 +00:00
|
|
|
Ok(PathTemplate { components })
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ToString for PathTemplate {
|
|
|
|
fn to_string(&self) -> std::string::String {
|
|
|
|
self.components
|
|
|
|
.iter()
|
|
|
|
.map(|component| match component {
|
|
|
|
Component::Constant(s) => s.clone(),
|
|
|
|
Component::Parameter(s) => format!("{{{}}}", s),
|
|
|
|
})
|
|
|
|
.fold(String::new(), |a, b| a + "/" + &b)
|
|
|
|
}
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
2022-03-08 22:31:56 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2021-12-10 02:15:24 +00:00
|
|
|
use super::{parse, Component, PathTemplate};
|
2021-07-01 08:41:42 +00:00
|
|
|
|
|
|
|
#[test]
|
2022-02-08 16:59:38 +00:00
|
|
|
fn basic() {
|
2021-07-01 08:41:42 +00:00
|
|
|
let trials = vec![
|
2021-07-02 00:12:16 +00:00
|
|
|
(
|
|
|
|
"/info",
|
2021-12-10 02:15:24 +00:00
|
|
|
PathTemplate {
|
2021-07-02 00:12:16 +00:00
|
|
|
components: vec![Component::Constant("info".into())],
|
|
|
|
},
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"/measure/{number}",
|
2021-12-10 02:15:24 +00:00
|
|
|
PathTemplate {
|
2021-07-02 00:12:16 +00:00
|
|
|
components: vec![
|
|
|
|
Component::Constant("measure".into()),
|
|
|
|
Component::Parameter("number".into()),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
),
|
|
|
|
(
|
|
|
|
"/one/{two}/three",
|
2021-12-10 02:15:24 +00:00
|
|
|
PathTemplate {
|
2021-07-02 00:12:16 +00:00
|
|
|
components: vec![
|
|
|
|
Component::Constant("one".into()),
|
|
|
|
Component::Parameter("two".into()),
|
|
|
|
Component::Constant("three".into()),
|
|
|
|
],
|
|
|
|
},
|
|
|
|
),
|
2021-07-01 08:41:42 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
for (path, want) in trials.iter() {
|
2022-02-08 16:59:38 +00:00
|
|
|
match parse(path) {
|
|
|
|
Ok(t) => assert_eq!(&t, want),
|
|
|
|
Err(e) => panic!("path {} {}", path, e),
|
|
|
|
}
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-18 04:17:21 +00:00
|
|
|
#[test]
|
2022-02-08 16:59:38 +00:00
|
|
|
fn names() {
|
2021-09-18 04:17:21 +00:00
|
|
|
let trials = vec![
|
|
|
|
("/info", vec![]),
|
|
|
|
("/measure/{number}", vec!["number".to_string()]),
|
|
|
|
(
|
|
|
|
"/measure/{one}/{two}/and/{three}/yeah",
|
|
|
|
vec!["one".to_string(), "two".to_string(), "three".to_string()],
|
|
|
|
),
|
|
|
|
];
|
|
|
|
|
|
|
|
for (path, want) in trials.iter() {
|
2022-02-08 16:59:38 +00:00
|
|
|
match parse(path) {
|
|
|
|
Ok(t) => assert_eq!(&t.names(), want),
|
|
|
|
Err(e) => panic!("path {} {}", path, e),
|
|
|
|
}
|
2021-09-18 04:17:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-01 08:41:42 +00:00
|
|
|
#[test]
|
2022-02-08 16:59:38 +00:00
|
|
|
fn compile() {
|
2022-03-08 22:31:56 +00:00
|
|
|
let mut rename = HashMap::new();
|
|
|
|
let number = "number".to_string();
|
|
|
|
rename.insert(&number, &number);
|
2022-02-08 16:59:38 +00:00
|
|
|
let t = parse("/measure/{number}").unwrap();
|
2022-03-08 22:31:56 +00:00
|
|
|
let out = t.compile(rename);
|
2021-10-02 18:40:03 +00:00
|
|
|
let want = quote::quote! {
|
|
|
|
let url = format!("{}/measure/{}",
|
|
|
|
self.baseurl,
|
2022-02-08 16:59:38 +00:00
|
|
|
progenitor_client::encode_path(&number.to_string()),
|
2021-10-02 18:40:03 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
assert_eq!(want.to_string(), out.to_string());
|
2021-07-01 08:41:42 +00:00
|
|
|
}
|
|
|
|
}
|