use super::Version;
use crate::error::CoreError;
use std::collections::BTreeSet;
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ClientCapabilities {
pub preferred_version: Option<Version>,
pub supportedversions: Vec<Version>,
pub required_features: BTreeSet<String>,
pub optional_features: BTreeSet<String>,
pub client_type: String,
pub clientversion: Version,
}
impl ClientCapabilities {
pub fn new(client_type: String, clientversion: Version) -> Self {
Self {
preferred_version: None,
supportedversions: Vec::new(),
required_features: BTreeSet::new(),
optional_features: BTreeSet::new(),
client_type,
clientversion,
}
}
pub fn with_preferred_version(mut self, version: Version) -> Self {
self.preferred_version = Some(version);
self
}
pub fn with_supported_version(mut self, version: Version) -> Self {
self.supportedversions.push(version);
self
}
pub fn require_feature(mut self, feature: &str) -> Self {
self.required_features.insert(feature.to_string());
self
}
pub fn prefer_feature(mut self, feature: &str) -> Self {
self.optional_features.insert(feature.to_string());
self
}
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct NegotiationResult {
pub negotiated_version: Version,
pub available_features: BTreeSet<String>,
pub unsupported_features: BTreeSet<String>,
pub status: NegotiationStatus,
pub metadata: NegotiationMetadata,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NegotiationStatus {
Success,
SuccessWithWarnings,
PartialSuccess,
Failed,
FeaturesMissing,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct NegotiationMetadata {
pub consideredversions: Vec<Version>,
pub selection_reason: String,
pub warnings: Vec<String>,
pub algorithm: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NegotiationStrategy {
PreferLatest,
PreferClientPreference,
PreferStable,
PreferFeatureRich,
Custom,
}
pub struct VersionNegotiator {
strategy: NegotiationStrategy,
featurematrix: FeatureMatrix,
}
impl VersionNegotiator {
pub fn new() -> Self {
Self {
strategy: NegotiationStrategy::PreferLatest,
featurematrix: FeatureMatrix::new(),
}
}
pub fn with_strategy(mut self, strategy: NegotiationStrategy) -> Self {
self.strategy = strategy;
self
}
pub fn negotiate(
&self,
client_capabilities: &ClientCapabilities,
serverversions: &[&Version],
) -> Result<NegotiationResult, CoreError> {
let mut metadata = NegotiationMetadata {
consideredversions: serverversions.iter().map(|v| (*v).clone()).collect(),
selection_reason: String::new(),
warnings: Vec::new(),
algorithm: format!("{:?}", self.strategy),
};
let compatibleversions =
self.find_compatibleversions(client_capabilities, serverversions, &mut metadata)?;
if compatibleversions.is_empty() {
return Ok(NegotiationResult {
negotiated_version: Version::new(0, 0, 0),
available_features: BTreeSet::new(),
unsupported_features: client_capabilities.required_features.clone(),
status: NegotiationStatus::Failed,
metadata,
});
}
let selected_version =
self.apply_strategy(&compatibleversions, client_capabilities, &mut metadata)?;
let (available_features, unsupported_features, status) = self.check_feature_compatibility(
&selected_version,
client_capabilities,
&mut metadata,
)?;
metadata.selection_reason = format!(
"Selected {} using {:?} strategy",
selected_version, self.strategy
);
Ok(NegotiationResult {
negotiated_version: selected_version,
available_features,
unsupported_features,
status,
metadata,
})
}
fn find_compatibleversions(
&self,
client_capabilities: &ClientCapabilities,
serverversions: &[&Version],
metadata: &mut NegotiationMetadata,
) -> Result<Vec<Version>, CoreError> {
let mut compatible = Vec::new();
for server_version in serverversions {
if client_capabilities.supportedversions.is_empty()
|| client_capabilities
.supportedversions
.contains(server_version)
{
compatible.push((*server_version).clone());
} else {
metadata.warnings.push(format!(
"Version {server_version} not in client's supported list"
));
}
}
Ok(compatible)
}
fn apply_strategy(
&self,
compatibleversions: &[Version],
client_capabilities: &ClientCapabilities,
metadata: &mut NegotiationMetadata,
) -> Result<Version, CoreError> {
match self.strategy {
NegotiationStrategy::PreferLatest => {
let mut versions = compatibleversions.to_vec();
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
}
NegotiationStrategy::PreferClientPreference => {
if let Some(ref preferred) = client_capabilities.preferred_version {
if compatibleversions.contains(preferred) {
return Ok(preferred.clone());
}
metadata.warnings.push(
"Client preferred _version not available, falling back to latest"
.to_string(),
);
}
let mut versions = compatibleversions.to_vec();
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
}
NegotiationStrategy::PreferStable => {
let stableversions: Vec<_> = compatibleversions
.iter()
.filter(|v| v.is_stable())
.cloned()
.collect();
if !stableversions.is_empty() {
let mut versions = stableversions;
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
} else {
let mut versions = compatibleversions.to_vec();
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
}
}
NegotiationStrategy::PreferFeatureRich => {
let mut versions = compatibleversions.to_vec();
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
}
NegotiationStrategy::Custom => {
let mut versions = compatibleversions.to_vec();
versions.sort();
versions.reverse();
Ok(versions.into_iter().next().expect("Operation failed"))
}
}
}
fn check_feature_compatibility(
&self,
selected_version: &Version,
client_capabilities: &ClientCapabilities,
_metadata: &mut NegotiationMetadata,
) -> Result<(BTreeSet<String>, BTreeSet<String>, NegotiationStatus), CoreError> {
let available_features = self.featurematrix.get_supported_features(selected_version);
let unsupported_features: BTreeSet<String> = client_capabilities
.required_features
.difference(&available_features)
.cloned()
.collect();
let status = if unsupported_features.is_empty() {
NegotiationStatus::Success
} else {
NegotiationStatus::FeaturesMissing
};
Ok((available_features, unsupported_features, status))
}
}
impl Default for VersionNegotiator {
fn default() -> Self {
Self::new()
}
}
struct FeatureMatrix {
version_features: std::collections::HashMap<Version, BTreeSet<String>>,
}
impl FeatureMatrix {
fn new() -> Self {
Self {
version_features: std::collections::HashMap::new(),
}
}
fn get_supported_features(&self, version: &Version) -> BTreeSet<String> {
self.version_features
.get(version)
.cloned()
.unwrap_or_else(BTreeSet::new)
}
#[allow(dead_code)]
fn set_version_features(&mut self, version: Version, features: BTreeSet<String>) {
self.version_features.insert(version, features);
}
}
pub struct ClientRequirementsBuilder {
capabilities: ClientCapabilities,
}
impl ClientRequirementsBuilder {
pub fn new(client_type: &str, clientversion: Version) -> Self {
Self {
capabilities: ClientCapabilities::new(client_type.to_string(), clientversion),
}
}
pub fn preferred_version(mut self, version: Version) -> Self {
self.capabilities.preferred_version = Some(version);
self
}
pub fn supportversions(mut self, versions: Vec<Version>) -> Self {
self.capabilities.supportedversions = versions;
self
}
pub fn require_feature(mut self, feature: &str) -> Self {
self.capabilities
.required_features
.insert(feature.to_string());
self
}
pub fn prefer_feature(mut self, feature: &str) -> Self {
self.capabilities
.optional_features
.insert(feature.to_string());
self
}
pub fn build(self) -> ClientCapabilities {
self.capabilities
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_capabilities_builder() {
let capabilities = ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.preferred_version(Version::new(2, 1, 0))
.supportversions(vec![Version::new(2, 0, 0), Version::new(2, 1, 0)])
.require_feature("feature1")
.prefer_feature("feature2")
.build();
assert_eq!(capabilities.client_type, "test_client");
assert_eq!(capabilities.preferred_version, Some(Version::new(2, 1, 0)));
assert!(capabilities.required_features.contains("feature1"));
assert!(capabilities.optional_features.contains("feature2"));
}
#[test]
fn test_version_negotiation_prefer_latest() {
let negotiator = VersionNegotiator::new().with_strategy(NegotiationStrategy::PreferLatest);
let client_capabilities =
ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.supportversions(vec![
Version::new(1, 0, 0),
Version::new(1, 1, 0),
Version::new(2, 0, 0),
])
.build();
let v1 = Version::new(1, 0, 0);
let v2 = Version::new(1, 1, 0);
let v3 = Version::new(2, 0, 0);
let serverversions = vec![&v1, &v2, &v3];
let result = negotiator
.negotiate(&client_capabilities, &serverversions)
.expect("Operation failed");
assert_eq!(result.negotiated_version, Version::new(2, 0, 0));
assert_eq!(result.status, NegotiationStatus::Success);
}
#[test]
fn test_version_negotiation_prefer_client() {
let negotiator =
VersionNegotiator::new().with_strategy(NegotiationStrategy::PreferClientPreference);
let client_capabilities =
ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.preferred_version(Version::new(1, 1, 0))
.supportversions(vec![
Version::new(1, 0, 0),
Version::new(1, 1, 0),
Version::new(2, 0, 0),
])
.build();
let v1 = Version::new(1, 0, 0);
let v2 = Version::new(1, 1, 0);
let v3 = Version::new(2, 0, 0);
let serverversions = vec![&v1, &v2, &v3];
let result = negotiator
.negotiate(&client_capabilities, &serverversions)
.expect("Operation failed");
assert_eq!(result.negotiated_version, Version::new(1, 1, 0));
}
#[test]
fn test_version_negotiation_prefer_stable() {
let negotiator = VersionNegotiator::new().with_strategy(NegotiationStrategy::PreferStable);
let client_capabilities =
ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.supportversions(vec![
Version::new(1, 0, 0),
Version::parse("2.0.0-alpha").expect("Operation failed"),
Version::new(1, 1, 0),
])
.build();
let v1 = Version::new(1, 0, 0);
let v2 = Version::parse("2.0.0-alpha").expect("Operation failed");
let v3 = Version::new(1, 1, 0);
let serverversions = vec![&v1, &v2, &v3];
let result = negotiator
.negotiate(&client_capabilities, &serverversions)
.expect("Operation failed");
assert_eq!(result.negotiated_version, Version::new(1, 1, 0));
}
#[test]
fn test_no_compatible_version() {
let negotiator = VersionNegotiator::new();
let client_capabilities =
ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.supportversions(vec![Version::new(3, 0, 0)])
.build();
let v1 = Version::new(1, 0, 0);
let v2 = Version::new(2, 0, 0);
let serverversions = vec![&v1, &v2];
let result = negotiator
.negotiate(&client_capabilities, &serverversions)
.expect("Operation failed");
assert_eq!(result.status, NegotiationStatus::Failed);
}
#[test]
fn test_negotiation_metadata() {
let negotiator = VersionNegotiator::new();
let client_capabilities =
ClientRequirementsBuilder::new("test_client", Version::new(1, 0, 0))
.supportversions(vec![Version::new(1, 0, 0)])
.build();
let v1 = Version::new(1, 0, 0);
let serverversions = vec![&v1];
let result = negotiator
.negotiate(&client_capabilities, &serverversions)
.expect("Operation failed");
assert!(!result.metadata.selection_reason.is_empty());
assert_eq!(result.metadata.consideredversions.len(), 1);
}
}