use std::fmt;
use serde::{Deserialize, Serialize};
use crate::config::host::Host;
mod v1;
pub const ROUTE_SCHEMA_VERSION: u32 = 2;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StoredRoute {
pub schema_version: u32,
pub host: Host,
}
impl StoredRoute {
#[allow(dead_code)] pub fn new(host: Host) -> Self {
Self {
schema_version: ROUTE_SCHEMA_VERSION,
host,
}
}
pub fn key(&self) -> String {
self.host.name.to_lowercase()
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct StoredRouteError {
pub host: String,
pub error: String,
}
#[derive(Debug, Default)]
pub struct RoutesSnapshot {
pub revision: u64,
pub routes: Vec<Result<StoredRoute, StoredRouteError>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ReplicaStatus {
pub id: String,
pub revision: u64,
pub applied_at: u64,
pub version: String,
pub hostname: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum StoreError {
Unavailable(String),
Invalid(String),
}
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StoreError::Unavailable(err) => write!(f, "store unavailable: {err}"),
StoreError::Invalid(err) => write!(f, "invalid data: {err}"),
}
}
}
impl std::error::Error for StoreError {}
pub fn decode_stored_route(host: &str, json: &str) -> Result<StoredRoute, StoredRouteError> {
let error = |error: String| StoredRouteError {
host: host.to_string(),
error,
};
let version: SchemaVersion =
serde_json::from_str(json).map_err(|err| error(format!("invalid route: {err}")))?;
match version.schema_version {
1 => v1::decode_v1(json)
.map(StoredRoute::new)
.map_err(|err| error(format!("invalid route: {err}"))),
ROUTE_SCHEMA_VERSION => {
serde_json::from_str(json).map_err(|err| error(format!("invalid route: {err}")))
}
version => Err(error(unsupported_version(version))),
}
}
pub fn check_schema_version(stored: StoredRoute) -> Result<StoredRoute, StoredRouteError> {
if stored.schema_version > ROUTE_SCHEMA_VERSION {
return Err(StoredRouteError {
host: stored.key(),
error: unsupported_version(stored.schema_version),
});
}
Ok(stored)
}
#[derive(Deserialize)]
struct SchemaVersion {
schema_version: u32,
}
fn unsupported_version(version: u32) -> String {
format!(
"unsupported schema version {version} (this version of proksi supports up to {ROUTE_SCHEMA_VERSION})"
)
}
#[cfg(test)]
mod tests {
use crate::routing::test_utils::host;
use super::{check_schema_version, decode_stored_route, StoredRoute};
#[test]
fn test_decode_stored_route() {
let stored = StoredRoute::new(host("a.test", "10.0.0.1"));
let json = serde_json::to_string(&stored).unwrap();
assert_eq!(decode_stored_route("a.test", &json), Ok(stored));
}
#[test]
fn test_decode_invalid_stored_route() {
let err = decode_stored_route("a.test", "{ not json").unwrap_err();
assert_eq!(err.host, "a.test");
assert!(err.error.starts_with("invalid route"), "{}", err.error);
}
#[test]
fn test_decode_schema_version_1_route_as_host() {
let json = serde_json::json!({
"schema_version": 1,
"route": { "host": "a.test", "upstreams": [{ "ip": "10.0.0.1", "port": 3000 }] },
})
.to_string();
let stored = decode_stored_route("a.test", &json).unwrap();
assert_eq!(stored.schema_version, 2);
assert_eq!(stored.host.name, "a.test");
assert_eq!(stored.host.locations[0].path, "/*");
}
#[test]
fn test_decode_newer_schema_version() {
let json = serde_json::json!({
"schema_version": 3,
"host": { "name": "a.test", "something_new": true },
})
.to_string();
let err = decode_stored_route("a.test", &json).unwrap_err();
assert_eq!(
err.error,
"unsupported schema version 3 (this version of proksi supports up to 2)"
);
}
#[test]
fn test_check_schema_version() {
let mut stored = StoredRoute::new(host("a.test", "10.0.0.1"));
assert!(check_schema_version(stored.clone()).is_ok());
stored.schema_version = 3;
assert!(check_schema_version(stored).is_err());
}
#[test]
fn test_stored_route_key_is_lowercase() {
assert_eq!(StoredRoute::new(host("A.Test", "10.0.0.1")).key(), "a.test");
}
}