use std::borrow::Cow;
use std::fmt;
use std::marker::PhantomData;
use std::str::FromStr;
use serde::de::Visitor;
use serde::{de, Deserialize, Deserializer};
use serde_derive::Serialize;
use crate::errors;
use crate::errors::*;
use crate::model::io::IOType;
use crate::model::name::Name;
use crate::model::validation::Validate;
#[derive(Hash, Debug, PartialEq, Ord, PartialOrd, Eq, Clone, Default, Serialize, Deserialize)]
pub struct Route {
string: String,
}
impl fmt::Display for Route {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.string)
}
}
impl FromStr for Route {
type Err = errors::Error;
fn from_str(s: &str) -> Result<Self> {
Ok(Route {
string: s.to_string()
})
}
}
impl From<&str> for Route {
fn from(string: &str) -> Self {
Route {
string: string.to_string()
}
}
}
impl From<String> for Route {
fn from(string: String) -> Self {
Route {
string
}
}
}
impl From<&Name> for Route {
fn from(name: &Name) -> Self {
Route {
string: name.to_string()
}
}
}
pub fn route_string<'de, T, D>(deserializer: D) -> std::result::Result<T, D::Error>
where
T: Deserialize<'de> + FromStr<Err = Error>,
D: Deserializer<'de>,
{
struct RouteString<T>(PhantomData<fn() -> T>);
impl<'de, Route> Visitor<'de> for RouteString<Route>
where
Route: Deserialize<'de> + FromStr<Err = Error>,
{
type Value = Route;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("String")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Route, E>
where E: de::Error {
#[allow(clippy::unwrap_used)]
Ok(FromStr::from_str(value).unwrap())
}
}
deserializer.deserialize_any(RouteString(PhantomData))
}
pub fn route_or_route_array<'de, D>(deserializer: D) -> std::result::Result<Vec<Route>, D::Error>
where
D: Deserializer<'de>,
{
struct StringOrVec(PhantomData<Vec<Route>>);
impl<'de> de::Visitor<'de> for StringOrVec {
type Value = Vec<Route>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Route or Array of Routes")
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where E: de::Error {
#[allow(clippy::unwrap_used)]
Ok(vec![FromStr::from_str(value).unwrap()])
}
fn visit_seq<S>(self, mut visitor: S) -> std::result::Result<Self::Value, S::Error>
where S: de::SeqAccess<'de> {
let mut vec: Vec<Route> = Vec::new();
while let Some(element) = visitor.next_element::<String>()? {
vec.push(Route::from(element));
}
Ok(vec)
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}
#[derive(Debug, PartialEq)]
pub enum RouteType {
FlowInput(Name, Route),
FlowOutput(Name),
SubProcess(Name, Route)
}
impl Route {
pub fn sub_route_of(&self, other: &Route) -> Option<Route> {
if self.string == other.string {
return Some(Route::from(""));
}
self.string.strip_prefix(&format!("{other}/")).map(Route::from)
}
pub fn insert<R: AsRef<str>>(&mut self, sub_route: R) -> &Self {
self.string.insert_str(0, sub_route.as_ref());
self
}
pub fn extend(&mut self, sub_route: &Route) -> &Self {
if !sub_route.is_empty() {
if !self.string.ends_with('/') && !sub_route.string.starts_with('/') {
self.string.push('/');
}
self.string.push_str(&sub_route.string);
}
self
}
pub fn pop(&self) -> (Cow<Route>, Option<Route>) {
let mut segments: Vec<&str> = self.string.split('/').collect();
let sub_route = segments.pop();
match sub_route {
None => (Cow::Borrowed(self), None),
Some("") => (Cow::Borrowed(self), None),
Some(sr) => (
Cow::Owned(Route::from(segments.join("/"))),
Some(Route::from(sr)),
),
}
}
pub fn without_trailing_array_index(&self) -> (Cow<Route>, usize, bool) {
let mut parts: Vec<&str> = self.string.split('/').collect();
if let Some(last_part) = parts.pop() {
if let Ok(index) = last_part.parse::<usize>() {
let route_without_number = parts.join("/");
return (Cow::Owned(Route::from(route_without_number)), index, true);
}
}
(Cow::Borrowed(self), 0, false)
}
pub fn is_array_selector(&self) -> bool {
if self.string.is_empty() {
return false;
}
let mut parts: Vec<&str> = self.string.split('/').collect();
if let Some(last_part) = parts.pop() {
return last_part.parse::<usize>().is_ok();
}
false
}
pub fn depth(&self) -> usize {
if self.string.is_empty() {
return 0;
}
self.string.split('/').count()
}
pub fn is_empty(&self) -> bool {
self.string.is_empty()
}
pub fn parse_subroute(&self) -> Result<RouteType> {
let segments: Vec<&str> = self.string.split('/').collect();
match segments[0] {
"input" => Ok(RouteType::FlowInput(segments[1].into(),
segments[2..].join("/").into())),
"output" => Ok(RouteType::FlowOutput(segments[1].into())),
"" => bail!("Invalid Route in connection - must be an input, output or sub-process name"),
process_name => Ok(RouteType::SubProcess(process_name.into(),
segments[1..].join("/").into())),
}
}
pub fn parent(&self, name: &Name) -> String {
self.string.strip_suffix(&format!("/{}", name.as_str()))
.unwrap_or(&self.string).to_string()
}
}
impl AsRef<str> for Route {
fn as_ref(&self) -> &str {
self.string.as_str()
}
}
impl Validate for Route {
fn validate(&self) -> Result<()> {
if self.string.is_empty() {
bail!("Route '{}' is invalid - a route must specify an input, output or subprocess by name", self);
}
if self.string.parse::<usize>().is_ok() {
bail!("Route '{}' is invalid - cannot be an integer", self);
}
Ok(())
}
}
pub trait HasRoute {
fn route(&self) -> &Route;
fn route_mut(&mut self) -> &mut Route;
}
pub trait SetRoute {
fn set_routes_from_parent(&mut self, parent: &Route);
}
#[allow(clippy::upper_case_acronyms)]
pub trait SetIORoutes {
fn set_io_routes_from_parent(&mut self, parent: &Route, io_type: IOType);
}
#[cfg(test)]
mod test {
use crate::model::name::Name;
use crate::model::route::RouteType;
use crate::model::validation::Validate;
use super::Route;
#[test]
fn test_invalid_connection_route() {
match Route::from("").parse_subroute() {
Ok(_) => panic!("Connection route should not be valid"),
Err(e) => assert!(e.to_string()
.contains("Invalid Route in connection"))
}
}
#[test]
fn test_parse_valid_input() {
let route = Route::from("input/string");
assert_eq!(route.parse_subroute().expect("Could not find input"),
RouteType::FlowInput(Name::from("string"),
Route::default()));
}
#[test]
fn test_parse_valid_output() {
let route = Route::from("output/string");
assert_eq!(route.parse_subroute().expect("Could not find input"),
RouteType::FlowOutput(Name::from("string")));
}
#[test]
fn test_parse_valid_subprocess() {
let route = Route::from("sub-process");
assert_eq!(route.parse_subroute().expect("Could not find input"),
RouteType::SubProcess(Name::from("sub-process"), Route::default()));
}
#[test]
fn test_from_string() {
let route = Route::from("my-route".to_string());
assert_eq!(route, Route::from("my-route"));
}
#[test]
fn test_from_ref_string() {
let route = Route::from(&format!("{}{}", "my-route", "/subroute"));
assert_eq!(route, Route::from("my-route/subroute"));
}
#[test]
fn test_from_name() {
let name = Name::from("my-route-name");
assert_eq!(Route::from(&name), Route::from("my-route-name"));
}
#[test]
fn test_route_pop() {
let original = Route::from("/root/function/output/subroute");
let (level_up, sub) = original.pop();
assert_eq!(
level_up.into_owned(),
Route::from("/root/function/output")
);
assert_eq!(sub, Some(Route::from("subroute")));
}
#[test]
fn test_root_route_pop() {
let original = Route::from("/");
let (level_up, sub) = original.pop();
assert_eq!(level_up.into_owned(), Route::from("/"));
assert_eq!(sub, None);
}
#[test]
fn test_empty_route_pop() {
let original = Route::from("");
let (level_up, sub) = original.pop();
assert_eq!(level_up.into_owned(), Route::from(""));
assert_eq!(sub, None);
}
#[test]
fn no_path_no_change() {
let route = Route::from("");
let (new_route, _num, trailing_number) = route.without_trailing_array_index();
assert_eq!(new_route.into_owned(), Route::default());
assert!(!trailing_number);
}
#[test]
fn just_slash_no_change() {
let route = Route::from("/");
let (new_route, _num, trailing_number) = route.without_trailing_array_index();
assert_eq!(new_route.into_owned(), Route::from("/"));
assert!(!trailing_number);
}
#[test]
fn no_trailing_number_no_change() {
let route = Route::from("/output1");
let (new_route, _num, trailing_number) = route.without_trailing_array_index();
assert_eq!(new_route.into_owned(), route);
assert!(!trailing_number);
}
#[test]
fn detect_array_at_output_root() {
let route = Route::from("/0");
let (new_route, num, trailing_number) = route.without_trailing_array_index();
assert_eq!(new_route.into_owned(), Route::from(""));
assert_eq!(num, 0);
assert!(trailing_number);
}
#[test]
fn detect_array_at_output_subroute() {
let route = Route::from("/array_output/0");
let (new_route, num, trailing_number) = route.without_trailing_array_index();
assert_eq!(new_route.into_owned(), Route::from("/array_output"));
assert_eq!(num, 0);
assert!(trailing_number);
}
#[test]
fn valid_process_route() {
let route = Route::from("sub_process/i1");
assert!(route.validate().is_ok());
}
#[test]
fn valid_process_route_with_subroute() {
let route = Route::from("sub_process/i1/sub_route");
assert!(route.validate().is_ok());
}
#[test]
fn valid_input_route() {
let route = Route::from("input/i1");
assert!(route.validate().is_ok());
}
#[test]
fn valid_input_route_with_subroute() {
let route = Route::from("input/i1/sub_route");
assert!(route.validate().is_ok());
}
#[test]
fn valid_output_route() {
let route = Route::from("output/i1");
assert!(route.validate().is_ok());
}
#[test]
fn valid_output_route_with_subroute() {
let route = Route::from("output/i1/sub_route");
assert!(route.validate().is_ok());
}
#[test]
fn validate_invalid_empty_route() {
let route = Route::from("");
assert!(route.validate().is_err());
}
#[test]
fn validate_invalid_route() {
let route = Route::from("123");
assert!(route.validate().is_err());
}
#[test]
fn subroute_equal_route() {
let route = Route::from("/root/function");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_some())
}
#[test]
fn subroute_distinct_route() {
let route = Route::from("/root/function");
assert!(route.sub_route_of(&Route::from("/root/foo")).is_none())
}
#[test]
fn subroute_extended_name_route() {
let route = Route::from("/root/function_foo");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_none())
}
#[test]
fn is_a_subroute() {
let route = Route::from("/root/function/input");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_some())
}
#[test]
fn is_a_sub_subroute() {
let route = Route::from("/root/function/input/element");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_some())
}
#[test]
fn is_array_element_subroute() {
let route = Route::from("/root/function/1");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_some())
}
#[test]
fn is_array_element_sub_subroute() {
let route = Route::from("/root/function/input/1");
assert!(route
.sub_route_of(&Route::from("/root/function"))
.is_some())
}
#[test]
fn extend_empty_route() {
let mut route = Route::default();
route.extend(&Route::from("sub"));
assert_eq!(route, Route::from("/sub"));
}
#[test]
fn extend_root_route() {
let mut route = Route::from("/");
route.extend(&Route::from("sub"));
assert_eq!(route, Route::from("/sub"));
}
#[test]
fn extend_route() {
let mut route = Route::from("/root/function");
route.extend(&Route::from("sub"));
assert_eq!(route, Route::from("/root/function/sub"));
}
#[test]
fn extend_route_with_nothing() {
let mut route = Route::from("/root/function");
route.extend(&Route::from(""));
assert_eq!(route, Route::from("/root/function"));
}
}