use std::{cmp::Ordering, collections::BTreeMap, str::FromStr};
use crate::{
backend::BackendId,
shared::{Duration, Fraction, Regex},
Hostname, Name, Service,
};
use serde::{Deserialize, Serialize};
#[cfg(feature = "typeinfo")]
use junction_typeinfo::TypeInfo;
#[doc(hidden)]
pub mod tags {
pub const GENERATED_BY: &str = "junctionlabs.io/generated-by";
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(try_from = "String", into = "String")]
pub enum HostnameMatch {
Subdomain(Hostname),
Exact(Hostname),
}
impl HostnameMatch {
pub fn matches(&self, hostname: &Hostname) -> bool {
self.matches_str_validated(hostname)
}
pub fn matches_str(&self, s: &str) -> bool {
if Hostname::validate(s.as_bytes()).is_err() {
return false;
}
self.matches_str_validated(s)
}
fn matches_str_validated(&self, s: &str) -> bool {
match self {
HostnameMatch::Subdomain(d) => {
let (subdomain, domain) = s.split_at(s.len() - d.len());
domain == &d[..] && subdomain.ends_with('.')
}
HostnameMatch::Exact(e) => s == e.as_ref(),
}
}
}
#[cfg(feature = "typeinfo")]
impl junction_typeinfo::TypeInfo for HostnameMatch {
fn kind() -> junction_typeinfo::Kind {
junction_typeinfo::Kind::String
}
}
impl From<Hostname> for HostnameMatch {
fn from(hostname: Hostname) -> Self {
Self::Exact(hostname)
}
}
impl std::fmt::Display for HostnameMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HostnameMatch::Subdomain(hostname) => write!(f, "*.{hostname}"),
HostnameMatch::Exact(hostname) => f.write_str(hostname),
}
}
}
impl FromStr for HostnameMatch {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s.strip_prefix("*.") {
Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
None => Self::Exact(Hostname::from_str(s)?),
})
}
}
impl TryFrom<String> for HostnameMatch {
type Error = crate::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
Ok(match s.strip_prefix("*.") {
Some(hostname) => Self::Subdomain(Hostname::from_str(hostname)?),
None => Self::Exact(Hostname::try_from(s)?),
})
}
}
impl From<HostnameMatch> for String {
fn from(value: HostnameMatch) -> Self {
match value {
HostnameMatch::Subdomain(_) => value.to_string(),
HostnameMatch::Exact(inner) => inner.0.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct Route {
pub id: Name,
#[serde(default)]
pub tags: BTreeMap<String, String>,
#[serde(default)]
pub hostnames: Vec<HostnameMatch>,
#[serde(default)]
pub ports: Vec<u16>,
#[serde(default)]
pub rules: Vec<RouteRule>,
}
impl Route {
pub fn passthrough_route(id: Name, service: Service) -> Route {
Route {
id,
hostnames: vec![service.hostname().into()],
ports: vec![],
tags: Default::default(),
rules: vec![RouteRule {
matches: vec![RouteMatch {
path: Some(PathMatch::empty_prefix()),
..Default::default()
}],
backends: vec![BackendRef {
service,
port: None,
weight: 1,
}],
..Default::default()
}],
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<Name>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub matches: Vec<RouteMatch>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[doc(hidden)]
pub filters: Vec<RouteFilter>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeouts: Option<RouteTimeouts>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retry: Option<RouteRetry>,
#[serde(default)]
pub backends: Vec<BackendRef>,
}
impl PartialOrd for RouteRule {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RouteRule {
fn cmp(&self, other: &Self) -> Ordering {
let mut self_matches: Vec<_> = self.matches.iter().collect();
self_matches.sort();
let mut self_matches = self_matches.iter().rev();
let mut other_matches: Vec<_> = other.matches.iter().collect();
other_matches.sort();
let mut other_matches = other_matches.iter().rev();
loop {
match (self_matches.next(), other_matches.next()) {
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(a), Some(b)) => match a.cmp(b) {
Ordering::Equal => {}
ord => return ord,
},
}
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteTimeouts {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request: Option<Duration>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
alias = "backendRequest"
)]
pub backend_request: Option<Duration>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteMatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathMatch>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub headers: Vec<HeaderMatch>,
#[serde(default, skip_serializing_if = "Vec::is_empty", alias = "queryParams")]
pub query_params: Vec<QueryParamMatch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<Method>,
}
impl PartialOrd for RouteMatch {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RouteMatch {
fn cmp(&self, other: &Self) -> Ordering {
match self.path.cmp(&other.path) {
Ordering::Equal => (),
cmp => return cmp,
}
match (&self.method, &other.method) {
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
_ => (),
}
match self.headers.len().cmp(&other.headers.len()) {
Ordering::Equal => (),
cmp => return cmp,
}
self.query_params.len().cmp(&other.query_params.len())
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type")]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum PathMatch {
#[serde(alias = "prefix")]
Prefix { value: String },
#[serde(alias = "regularExpression", alias = "regular_expression")]
RegularExpression { value: Regex },
#[serde(untagged)]
Exact { value: String },
}
impl PathMatch {
pub fn empty_prefix() -> Self {
Self::Prefix {
value: String::new(),
}
}
}
impl PartialOrd for PathMatch {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathMatch {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Exact { value: v1 }, Self::Exact { value: v2 }) => v1.len().cmp(&v2.len()),
(Self::Exact { .. }, _) => Ordering::Greater,
(Self::Prefix { .. }, Self::Exact { .. }) => Ordering::Less,
(Self::Prefix { value: v1 }, Self::Prefix { value: v2 }) => v1.len().cmp(&v2.len()),
(Self::Prefix { .. }, _) => Ordering::Greater,
(Self::RegularExpression { value: v1 }, Self::RegularExpression { value: v2 }) => {
v1.as_str().len().cmp(&v2.as_str().len())
}
(Self::RegularExpression { .. }, _) => Ordering::Less,
}
}
}
pub type HeaderName = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
#[serde(tag = "type", deny_unknown_fields)]
pub enum HeaderMatch {
#[serde(
alias = "regex",
alias = "regular_expression",
alias = "regularExpression"
)]
RegularExpression { name: String, value: Regex },
#[serde(untagged)]
Exact { name: String, value: String },
}
impl HeaderMatch {
pub fn name(&self) -> &str {
match self {
HeaderMatch::RegularExpression { name, .. } => name,
HeaderMatch::Exact { name, .. } => name,
}
}
pub fn is_match(&self, header_value: &str) -> bool {
match self {
HeaderMatch::RegularExpression { value, .. } => value.is_match(header_value),
HeaderMatch::Exact { value, .. } => value == header_value,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields, tag = "type")]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum QueryParamMatch {
#[serde(
alias = "regex",
alias = "regular_expression",
alias = "regularExpression"
)]
RegularExpression { name: String, value: Regex },
#[serde(untagged)]
Exact { name: String, value: String },
}
impl QueryParamMatch {
pub fn name(&self) -> &str {
match self {
QueryParamMatch::RegularExpression { name, .. } => name,
QueryParamMatch::Exact { name, .. } => name,
}
}
pub fn is_match(&self, param_value: &str) -> bool {
match self {
QueryParamMatch::RegularExpression { value, .. } => value.is_match(param_value),
QueryParamMatch::Exact { value, .. } => value == param_value,
}
}
}
pub type Method = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type", deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum RouteFilter {
RequestHeaderModifier {
#[serde(alias = "requestHeaderModifier")]
request_header_modifier: HeaderFilter,
},
ResponseHeaderModifier {
#[serde(alias = "responseHeaderModifier")]
response_header_modifier: HeaderFilter,
},
RequestMirror {
#[serde(alias = "requestMirror")]
request_mirror: RequestMirrorFilter,
},
RequestRedirect {
#[serde(alias = "requestRedirect")]
request_redirect: RequestRedirectFilter,
},
URLRewrite {
#[serde(alias = "urlRewrite")]
url_rewrite: UrlRewriteFilter,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct HeaderFilter {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub set: Vec<HeaderValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub add: Vec<HeaderValue>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remove: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct HeaderValue {
pub name: HeaderName,
pub value: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type", deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub enum PathModifier {
ReplaceFullPath {
#[serde(alias = "replaceFullPath")]
replace_full_path: String,
},
ReplacePrefixMatch {
#[serde(alias = "replacePrefixMatch")]
replace_prefix_match: String,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RequestRedirectFilter {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scheme: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<Name>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathModifier>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
#[serde(default, skip_serializing_if = "Option::is_none", alias = "statusCode")]
pub status_code: Option<u16>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct UrlRewriteFilter {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<Hostname>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathModifier>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RequestMirrorFilter {
pub percent: Option<i32>,
pub fraction: Option<Fraction>,
pub backend: Service,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct RouteRetry {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub codes: Vec<u16>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attempts: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backoff: Option<Duration>,
}
const fn default_weight() -> u32 {
1
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "typeinfo", derive(TypeInfo))]
pub struct BackendRef {
#[serde(flatten)]
pub service: Service,
pub port: Option<u16>,
#[serde(default = "default_weight")]
pub weight: u32,
}
impl BackendRef {
#[doc(hidden)]
pub fn into_backend_id(&self, default_port: u16) -> BackendId {
let port = self.port.unwrap_or(default_port);
BackendId {
service: self.service.clone(),
port,
}
}
#[doc(hidden)]
pub fn as_backend_id(&self) -> Option<BackendId> {
let port = self.port?;
Some(BackendId {
service: self.service.clone(),
port,
})
}
#[cfg(feature = "xds")]
pub(crate) fn name(&self) -> String {
let mut buf = String::new();
self.write_name(&mut buf).unwrap();
buf
}
#[cfg(feature = "xds")]
fn write_name(&self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
self.service.write_name(w)?;
if let Some(port) = self.port {
write!(w, ":{port}")?;
}
Ok(())
}
}
impl FromStr for BackendRef {
type Err = crate::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, port) = super::parse_port(s)?;
let backend = Service::from_str(name)?;
Ok(Self {
service: backend,
port,
weight: default_weight(),
})
}
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use rand::seq::SliceRandom;
use serde::de::DeserializeOwned;
use serde_json::json;
use super::*;
use crate::{
http::{HeaderMatch, RouteRule},
shared::Regex,
Service,
};
#[test]
fn test_hostname_match() {
let exact_matcher = HostnameMatch::from_str("foo.bar").unwrap();
let subdomain_matcher = HostnameMatch::from_str("*.foo.bar").unwrap();
for invalid_hostname in [
"",
"*",
".*",
".",
"!@#@!#!@",
"foo....bar",
".foo.bar",
"...foo.bar",
] {
assert!(!exact_matcher.matches_str(invalid_hostname));
assert!(!subdomain_matcher.matches_str(invalid_hostname));
}
for not_matching in ["blahfoo.bar", "bfoo.bar", "bar.foo"] {
assert!(!exact_matcher.matches_str(not_matching));
assert!(!subdomain_matcher.matches_str(not_matching));
}
assert!(exact_matcher.matches_str("foo.bar"));
assert!(!subdomain_matcher.matches_str("foo.bar"));
assert!(!exact_matcher.matches_str("blah.foo.bar"));
assert!(subdomain_matcher.matches_str("blah.foo.bar"));
assert!(subdomain_matcher.matches_str("b.foo.bar"));
}
#[test]
fn test_hostname_match_json() {
let json_value = json!(["foo.bar.baz", "*.foo.bar.baz",]);
let matchers = vec![
HostnameMatch::Exact(Hostname::from_static("foo.bar.baz")),
HostnameMatch::Subdomain(Hostname::from_static("foo.bar.baz")),
];
assert_eq!(
serde_json::from_value::<Vec<HostnameMatch>>(json_value.clone()).unwrap(),
matchers,
);
assert_eq!(serde_json::to_value(&matchers).unwrap(), json_value,);
}
#[test]
fn test_header_matcher_json() {
let test_json = json!([
{ "name":"bar", "type" : "RegularExpression", "value": ".*foo"},
{ "name":"bar", "value": "a literal"},
]);
let obj: Vec<HeaderMatch> = serde_json::from_value(test_json.clone()).unwrap();
assert_eq!(
obj,
vec![
HeaderMatch::RegularExpression {
name: "bar".to_string(),
value: Regex::from_str(".*foo").unwrap(),
},
HeaderMatch::Exact {
name: "bar".to_string(),
value: "a literal".to_string(),
}
]
);
let output_json = serde_json::to_value(&obj).unwrap();
assert_eq!(test_json, output_json);
}
#[test]
fn test_retry_policy_json() {
let test_json = json!({
"codes":[ 1, 2 ],
"attempts": 3,
"backoff": 60.0,
});
let obj: RouteRetry = serde_json::from_value(test_json.clone()).unwrap();
let output_json = serde_json::to_value(obj).unwrap();
assert_eq!(test_json, output_json);
}
#[test]
fn test_route_rule_json() {
let test_json = json!({
"matches":[
{
"method": "GET",
"path": { "value": "foo" },
"headers": [
{"name":"ian", "value": "foo"},
{"name": "bar", "type":"RegularExpression", "value": ".*foo"}
]
},
{
"query_params": [
{"name":"ian", "value": "foo"},
{"name": "bar", "type":"RegularExpression", "value": ".*foo"}
]
}
],
"filters":[{
"type": "URLRewrite",
"url_rewrite":{
"hostname":"ian.com",
"path": {"type":"ReplacePrefixMatch", "replace_prefix_match":"/"}
}
}],
"backends":[
{
"type": "kube",
"name": "timeout-svc",
"namespace": "foo",
"port": 80,
"weight": 1,
}
],
"timeouts": {
"request": 1.0,
}
});
let obj: RouteRule = serde_json::from_value(test_json.clone()).unwrap();
let output_json = serde_json::to_value(&obj).unwrap();
assert_eq!(test_json, output_json);
}
#[test]
fn test_route_json() {
assert_deserialize(
json!({
"id": "sweet-potato",
"hostnames": ["foo.bar.svc.cluster.local"],
"rules": [
{
"backends": [
{
"type": "kube",
"name": "foo",
"namespace": "bar",
"port": 80,
}
],
}
]
}),
Route {
id: Name::from_static("sweet-potato"),
hostnames: vec![Hostname::from_static("foo.bar.svc.cluster.local").into()],
ports: vec![],
tags: Default::default(),
rules: vec![RouteRule {
name: None,
matches: vec![],
filters: vec![],
timeouts: None,
retry: None,
backends: vec![BackendRef {
service: Service::kube("bar", "foo").unwrap(),
port: Some(80),
weight: 1,
}],
}],
},
);
}
#[test]
fn test_route_json_missing_fields() {
assert_deserialize_err::<Route>(json!({
"uhhhh": ["foo.bar"],
"rules": [
{
"matches": [],
}
]
}));
}
#[track_caller]
fn assert_deserialize<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
json: serde_json::Value,
expected: T,
) {
let actual: T = serde_json::from_value(json).unwrap();
assert_eq!(expected, actual);
}
#[track_caller]
fn assert_deserialize_err<T: DeserializeOwned + PartialEq + std::fmt::Debug>(
json: serde_json::Value,
) -> serde_json::Error {
serde_json::from_value::<T>(json).unwrap_err()
}
#[test]
fn test_path_match() {
arbtest::arbtest(|u| {
let s1: String = u.arbitrary()?;
let s2: String = u.arbitrary()?;
let m1 = PathMatch::Exact { value: s1.clone() };
let m2 = PathMatch::Exact { value: s2.clone() };
assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));
Ok(())
});
arbtest::arbtest(|u| {
let s1: String = u.arbitrary()?;
let s2: String = u.arbitrary()?;
let m1 = PathMatch::Prefix { value: s1.clone() };
let m2 = PathMatch::Prefix { value: s2.clone() };
assert_eq!(s1.len().cmp(&s2.len()), m1.cmp(&m2));
Ok(())
});
arbtest::arbtest(|u| {
let m1 = PathMatch::Exact {
value: u.arbitrary()?,
};
let m2 = PathMatch::Prefix {
value: u.arbitrary()?,
};
assert!(m1 > m2);
Ok(())
});
}
#[test]
fn test_order_route_match() {
let path_match = RouteMatch {
path: Some(PathMatch::Exact {
value: "/potato".to_string(),
}),
..Default::default()
};
let method_match = RouteMatch {
method: Some("PUT".to_string()),
..Default::default()
};
let header_match = RouteMatch {
headers: vec![HeaderMatch::Exact {
name: "x-user".to_string(),
value: "a-user".to_string(),
}],
..Default::default()
};
let query_match = RouteMatch {
query_params: vec![QueryParamMatch::Exact {
name: "q".to_string(),
value: "value".to_string(),
}],
..Default::default()
};
assert_eq!(
vec![&query_match, &header_match, &method_match, &path_match],
shuffle_and_sort([&path_match, &query_match, &header_match, &method_match]),
);
let m1 = RouteMatch {
path: Some(PathMatch::Exact {
value: "fooooooooooo".to_string(),
}),
query_params: query_match.query_params.clone(),
..Default::default()
};
let m2 = RouteMatch {
path: Some(PathMatch::Exact {
value: "foo".to_string(),
}),
query_params: query_match.query_params.clone(),
..Default::default()
};
assert!(m1 > m2, "should tie break by comparing path_match");
let m1 = RouteMatch {
path: path_match.path.clone(),
query_params: query_match.query_params.clone(),
..Default::default()
};
let m2 = RouteMatch {
path: path_match.path.clone(),
..Default::default()
};
assert!(m1 > m2, "should tie-break with query params");
let m1 = RouteMatch {
method: Some("GET".to_string()),
query_params: query_match.query_params.clone(),
..Default::default()
};
let m2 = RouteMatch {
method: Some("PUT".to_string()),
..Default::default()
};
assert!(m1 > m2, "should tie-break with query params");
}
#[test]
fn test_order_route_rule() {
let path_match = RouteMatch {
path: Some(PathMatch::Exact {
value: "/potato".to_string(),
}),
..Default::default()
};
let header_match = RouteMatch {
headers: vec![HeaderMatch::Exact {
name: "x-user".to_string(),
value: "a-user".to_string(),
}],
..Default::default()
};
let query_match = RouteMatch {
query_params: vec![QueryParamMatch::Exact {
name: "q".to_string(),
value: "value".to_string(),
}],
..Default::default()
};
let r1 = RouteRule {
matches: vec![path_match.clone()],
..Default::default()
};
let r2 = RouteRule {
matches: vec![header_match.clone()],
..Default::default()
};
assert!(r1 > r2);
assert!(r2 < r1);
let r1 = RouteRule {
matches: vec![path_match.clone()],
..Default::default()
};
let r2 = RouteRule {
matches: vec![path_match.clone(), header_match.clone()],
..Default::default()
};
assert!(r1 < r2);
assert!(r2 > r1);
let r1 = RouteRule {
matches: vec![query_match.clone()],
..Default::default()
};
let r2 = RouteRule {
matches: vec![],
..Default::default()
};
assert!(r2 < r1);
assert!(r1 > r2);
}
fn shuffle_and_sort<T: Ord>(xs: impl IntoIterator<Item = T>) -> Vec<T> {
let mut rng = rand::thread_rng();
let mut v: Vec<_> = xs.into_iter().collect();
v.shuffle(&mut rng);
v.sort();
v
}
}