#![warn(missing_docs)]
use semver::Version;
use std::fmt;
const LEGACY_CLIENT_ID_PREFIX: &str = "Tetsy";
const TETSY_CLIENT_ID_PREFIX: &str = "Tetsy-Vapory";
lazy_static! {
static ref TETSY_CLIENT_LARGE_REQUESTS_VERSION: Version = Version::parse("2.4.0").unwrap();
}
#[derive(Clone,Debug,PartialEq,Eq,Serialize)]
pub struct TetsyClientData {
name: String,
identity: Option<String>,
semver: Version,
os: String,
compiler: String,
can_handle_large_requests: bool,
}
impl TetsyClientData {
fn new(
name: String,
identity: Option<String>,
semver: Version,
os: String,
compiler: String,
) -> Self {
let can_handle_large_requests = &semver >= &TETSY_CLIENT_LARGE_REQUESTS_VERSION;
TetsyClientData {
name: name,
identity: identity,
semver: semver,
os: os,
compiler: compiler,
can_handle_large_requests: can_handle_large_requests,
}
}
fn name(&self) -> &str {
self.name.as_str()
}
fn identity(&self) -> Option<&str> {
self.identity.as_ref().map(String::as_str)
}
fn semver(&self) -> &Version {
&self.semver
}
fn os(&self) -> &str {
self.os.as_str()
}
fn compiler(&self) -> &str {
self.compiler.as_str()
}
fn can_handle_large_requests(&self) -> bool {
self.can_handle_large_requests
}
}
#[derive(Clone,Debug,Eq,PartialEq,Serialize)]
pub enum ClientVersion {
TetsyClient(
TetsyClientData
),
TetsyUnknownFormat(String),
Other(String),
}
impl Default for ClientVersion {
fn default() -> Self {
ClientVersion::Other("".to_owned())
}
}
pub trait ClientCapabilities {
fn can_handle_large_requests(&self) -> bool;
fn accepts_service_transaction(&self) -> bool;
}
impl ClientCapabilities for ClientVersion {
fn can_handle_large_requests(&self) -> bool {
match self {
ClientVersion::TetsyClient(data) => data.can_handle_large_requests(),
ClientVersion::TetsyUnknownFormat(_) => false,
ClientVersion::Other(_) => true
}
}
fn accepts_service_transaction(&self) -> bool {
match self {
ClientVersion::TetsyClient(_) => true,
ClientVersion::TetsyUnknownFormat(_) => true,
ClientVersion::Other(_) => false
}
}
}
fn is_tetsy(client_id: &str) -> bool {
client_id.starts_with(LEGACY_CLIENT_ID_PREFIX) || client_id.starts_with(TETSY_CLIENT_ID_PREFIX)
}
fn parse_tetsy_format(client_version: &str) -> Result<TetsyClientData, ()> {
const TETSY_ID_STRING_MINIMUM_TOKENS: usize = 4;
let tokens: Vec<&str> = client_version.split("/").collect();
if tokens.len() < TETSY_ID_STRING_MINIMUM_TOKENS {
return Err(())
}
let name = tokens[0];
let identity = if tokens.len() - 3 > 1 {
Some(tokens[1..(tokens.len() - 3)].join("/"))
} else {
None
};
let compiler = tokens[tokens.len() - 1];
let os = tokens[tokens.len() - 2];
get_number_from_version(tokens[tokens.len() - 3])
.and_then(|v| Version::parse(v).ok())
.map(|semver| TetsyClientData::new(
name.to_owned(),
identity,
semver,
os.to_owned(),
compiler.to_owned(),
))
.ok_or(())
}
impl<T> From<T> for ClientVersion
where T: AsRef<str> {
fn from(client_version: T) -> Self {
let client_version_str: &str = client_version.as_ref();
if !is_tetsy(client_version_str) {
return ClientVersion::Other(client_version_str.to_owned());
}
if let Ok(data) = parse_tetsy_format(client_version_str) {
ClientVersion::TetsyClient(data)
} else {
ClientVersion::TetsyUnknownFormat(client_version_str.to_owned())
}
}
}
fn format_tetsy_version_string(client_version: &TetsyClientData, f: &mut fmt::Formatter) -> std::fmt::Result {
let name = client_version.name();
let semver = client_version.semver();
let os = client_version.os();
let compiler = client_version.compiler();
match client_version.identity() {
None => write!(f, "{}/v{}/{}/{}", name, semver, os, compiler),
Some(identity) => write!(f, "{}/{}/v{}/{}/{}", name, identity, semver, os, compiler),
}
}
impl fmt::Display for ClientVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
match self {
ClientVersion::TetsyClient(data) => format_tetsy_version_string(data, f),
ClientVersion::TetsyUnknownFormat(id) => write!(f, "{}", id),
ClientVersion::Other(id) => write!(f, "{}", id)
}
}
}
fn get_number_from_version(version: &str) -> Option<&str> {
if version.starts_with("v") {
return version.get(1..);
}
None
}
#[cfg(test)]
pub mod tests {
use super::*;
const TETSY_CLIENT_SEMVER: &str = "2.4.0";
const TETSY_CLIENT_OLD_SEMVER: &str = "2.2.0";
const TETSY_CLIENT_OS: &str = "linux";
const TETSY_CLIENT_COMPILER: &str = "rustc";
const TETSY_CLIENT_IDENTITY: &str = "ExpanseSOLO";
const TETSY_CLIENT_MULTITOKEN_IDENTITY: &str = "ExpanseSOLO/abc/v1.2.3";
fn make_default_version_string() -> String {
format!(
"{}/v{}/{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
TETSY_CLIENT_COMPILER
)
}
fn make_default_long_version_string() -> String {
format!(
"{}/{}/v{}/{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_IDENTITY,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
TETSY_CLIENT_COMPILER
)
}
fn make_multitoken_identity_long_version_string() -> String {
format!(
"{}/{}/v{}/{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_MULTITOKEN_IDENTITY,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
TETSY_CLIENT_COMPILER
)
}
fn make_old_semver_version_string() -> String {
format!(
"{}/v{}/{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_OLD_SEMVER,
TETSY_CLIENT_OS,
TETSY_CLIENT_COMPILER
)
}
#[test]
pub fn client_version_when_from_empty_string_then_default() {
let default = ClientVersion::default();
assert_eq!(ClientVersion::from(""), default);
}
#[test]
pub fn get_number_from_version_when_valid_then_number() {
let version_string = format!("v{}", TETSY_CLIENT_SEMVER);
assert_eq!(get_number_from_version(&version_string).unwrap(), TETSY_CLIENT_SEMVER);
}
#[test]
pub fn client_version_when_str_tetsy_format_and_valid_then_all_fields_match() {
let client_version_string = make_default_version_string();
if let ClientVersion::TetsyClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
assert_eq!(client_version.name(), TETSY_CLIENT_ID_PREFIX);
assert_eq!(*client_version.semver(), Version::parse(TETSY_CLIENT_SEMVER).unwrap());
assert_eq!(client_version.os(), TETSY_CLIENT_OS);
assert_eq!(client_version.compiler(), TETSY_CLIENT_COMPILER);
} else {
panic!("shouldn't be here");
}
}
#[test]
pub fn client_version_when_str_tetsy_long_format_and_valid_then_all_fields_match() {
let client_version_string = make_default_long_version_string();
if let ClientVersion::TetsyClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
assert_eq!(client_version.name(), TETSY_CLIENT_ID_PREFIX);
assert_eq!(client_version.identity().unwrap(), TETSY_CLIENT_IDENTITY);
assert_eq!(*client_version.semver(), Version::parse(TETSY_CLIENT_SEMVER).unwrap());
assert_eq!(client_version.os(), TETSY_CLIENT_OS);
assert_eq!(client_version.compiler(), TETSY_CLIENT_COMPILER);
} else {
panic!("shouldnt be here");
}
}
#[test]
pub fn client_version_when_str_tetsy_long_format_and_valid_and_identity_multiple_tokens_then_all_fields_match() {
let client_version_string = make_multitoken_identity_long_version_string();
if let ClientVersion::TetsyClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
assert_eq!(client_version.name(), TETSY_CLIENT_ID_PREFIX);
assert_eq!(client_version.identity().unwrap(), TETSY_CLIENT_MULTITOKEN_IDENTITY);
assert_eq!(*client_version.semver(), Version::parse(TETSY_CLIENT_SEMVER).unwrap());
assert_eq!(client_version.os(), TETSY_CLIENT_OS);
assert_eq!(client_version.compiler(), TETSY_CLIENT_COMPILER);
} else {
panic!("shouldnt be here");
}
}
#[test]
pub fn client_version_when_string_tetsy_format_and_valid_then_all_fields_match() {
let client_version_string: String = make_default_version_string();
if let ClientVersion::TetsyClient(client_version) = ClientVersion::from(client_version_string.as_str()) {
assert_eq!(client_version.name(), TETSY_CLIENT_ID_PREFIX);
assert_eq!(*client_version.semver(), Version::parse(TETSY_CLIENT_SEMVER).unwrap());
assert_eq!(client_version.os(), TETSY_CLIENT_OS);
assert_eq!(client_version.compiler(), TETSY_CLIENT_COMPILER);
} else {
panic!("shouldn't be here");
}
}
#[test]
pub fn client_version_when_tetsy_format_and_invalid_then_equals_tetsy_unknown_client_version_string() {
let client_version_string = format!(
"{}/{}/{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
TETSY_CLIENT_COMPILER);
let client_version = ClientVersion::from(client_version_string.as_str());
let tetsy_unknown = ClientVersion::TetsyUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, tetsy_unknown);
}
#[test]
pub fn client_version_when_tetsy_format_without_identity_and_missing_compiler_field_then_equals_tetsy_unknown_client_version_string() {
let client_version_string = format!(
"{}/v{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
);
let client_version = ClientVersion::from(client_version_string.as_str());
let tetsy_unknown = ClientVersion::TetsyUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, tetsy_unknown);
}
#[test]
pub fn client_version_when_tetsy_format_with_identity_and_missing_compiler_field_then_equals_tetsy_unknown_client_version_string() {
let client_version_string = format!(
"{}/{}/v{}/{}",
TETSY_CLIENT_ID_PREFIX,
TETSY_CLIENT_IDENTITY,
TETSY_CLIENT_SEMVER,
TETSY_CLIENT_OS,
);
let client_version = ClientVersion::from(client_version_string.as_str());
let tetsy_unknown = ClientVersion::TetsyUnknownFormat(client_version_string.to_string());
assert_eq!(client_version, tetsy_unknown);
}
#[test]
pub fn client_version_when_not_tetsy_format_and_valid_then_other_with_client_version_string() {
let client_version_string = "Gvap/main.jnode.network/v1.8.21-stable-9dc5d1a9/linux";
let client_version = ClientVersion::from(client_version_string);
assert_eq!(client_version, ClientVersion::Other(client_version_string.to_string()));
}
#[test]
pub fn client_version_when_tetsy_format_and_valid_then_to_string_equal() {
let client_version_string: String = make_default_version_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert_eq!(client_version.to_string(), client_version_string);
}
#[test]
pub fn client_version_when_other_then_to_string_equal_input_string() {
let client_version_string: String = "Other".to_string();
let client_version = ClientVersion::from("Other");
assert_eq!(client_version.to_string(), client_version_string);
}
#[test]
pub fn client_capabilities_when_tetsy_old_version_then_handles_large_requests_false() {
let client_version_string: String = make_old_semver_version_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert!(!client_version.can_handle_large_requests());
}
#[test]
pub fn client_capabilities_when_tetsy_beta_version_then_not_handles_large_requests_true() {
let client_version_string: String = format!(
"{}/v{}/{}/{}",
"Tetsy-Vapory",
"2.4.0-beta",
"x86_64-linux-gnu",
"rustc1.31.1")
.to_string();
let client_version = ClientVersion::from(client_version_string.as_str());
assert!(!client_version.can_handle_large_requests());
}
#[test]
pub fn client_version_when_to_owned_then_both_objects_equal() {
let client_version_string: String = make_old_semver_version_string();
let origin = ClientVersion::from(client_version_string.as_str());
let borrowed = &origin;
let owned = origin.to_owned();
assert_eq!(*borrowed, owned);
}
#[test]
fn client_version_accepts_service_transaction_for_different_versions() {
assert!(!ClientVersion::from("Gvap").accepts_service_transaction());
assert!(ClientVersion::from("Tetsy-Vapory/v2.6.0/linux/rustc").accepts_service_transaction());
assert!(ClientVersion::from("Tetsy-Vapory/ABCDEFGH/v2.7.3/linux/rustc").accepts_service_transaction());
}
#[test]
fn is_tetsy_when_tetsy_then_true() {
let client_id = format!("{}/", TETSY_CLIENT_ID_PREFIX);
assert!(is_tetsy(&client_id));
}
#[test]
fn is_tetsy_when_empty_then_false() {
let client_id = "";
assert!(!is_tetsy(&client_id));
}
#[test]
fn is_tetsy_when_other_then_false() {
let client_id = "other";
assert!(!is_tetsy(&client_id));
}
}