use crate::{Error, Result};
use serde::Deserialize;
use std::path::Path;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VersionSource {
UserFlag,
SSTableMetadata,
DatasetMetadata,
Unknown,
}
impl VersionSource {
pub fn precedence(&self) -> u8 {
match self {
VersionSource::UserFlag => 0,
VersionSource::SSTableMetadata => 1,
VersionSource::DatasetMetadata => 2,
VersionSource::Unknown => 255,
}
}
pub fn description(&self) -> &'static str {
match self {
VersionSource::UserFlag => "User-provided flag (--cassandra-version)",
VersionSource::SSTableMetadata => "SSTable metadata",
VersionSource::DatasetMetadata => "Dataset metadata.yml",
VersionSource::Unknown => "Unknown (no version information available)",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedVersion {
pub version: Option<String>,
pub source: VersionSource,
}
impl ResolvedVersion {
pub fn new(version: Option<String>, source: VersionSource) -> Self {
Self { version, source }
}
pub fn is_known(&self) -> bool {
self.version.is_some()
}
pub fn version_or_unknown(&self) -> &str {
self.version.as_deref().unwrap_or("unknown")
}
}
#[derive(Debug, Clone, Deserialize)]
struct DatasetMetadata {
cassandra_version: Option<String>,
}
pub struct VersionHintResolver;
impl VersionHintResolver {
pub async fn resolve(
user_version: Option<String>,
sstable_path: &Path,
platform: Arc<crate::Platform>,
) -> Result<ResolvedVersion> {
if let Some(version) = user_version {
return Ok(ResolvedVersion::new(Some(version), VersionSource::UserFlag));
}
if let Some(version) = Self::parse_sstable_metadata(sstable_path, platform.clone()).await? {
return Ok(ResolvedVersion::new(
Some(version),
VersionSource::SSTableMetadata,
));
}
if let Some(version) = Self::parse_dataset_metadata(sstable_path, platform).await? {
return Ok(ResolvedVersion::new(
Some(version),
VersionSource::DatasetMetadata,
));
}
Ok(ResolvedVersion::new(None, VersionSource::Unknown))
}
async fn parse_sstable_metadata(
_sstable_path: &Path,
_platform: Arc<crate::Platform>,
) -> Result<Option<String>> {
Ok(None)
}
async fn parse_dataset_metadata(
sstable_path: &Path,
platform: Arc<crate::Platform>,
) -> Result<Option<String>> {
let search_paths = [
sstable_path.to_path_buf(),
sstable_path.parent().unwrap_or(sstable_path).to_path_buf(),
sstable_path
.parent()
.and_then(|p| p.parent())
.unwrap_or(sstable_path)
.to_path_buf(),
];
for base_path in &search_paths {
let metadata_path = base_path.join("metadata.yml");
if !platform.fs().exists(&metadata_path).await? {
continue;
}
match platform.fs().read_file(&metadata_path).await {
Ok(contents) => {
let contents_str = String::from_utf8(contents).map_err(|e| {
Error::parse(format!(
"metadata.yml at {} is not valid UTF-8: {}",
metadata_path.display(),
e
))
})?;
let metadata: DatasetMetadata =
serde_yaml::from_str(&contents_str).map_err(|e| {
Error::parse(format!(
"Failed to parse metadata.yml at {}: {}",
metadata_path.display(),
e
))
})?;
if let Some(version) = metadata.cassandra_version {
return Ok(Some(version));
}
continue;
}
Err(e) => {
match &e {
Error::Io(io_err) if io_err.kind() == std::io::ErrorKind::NotFound => {
continue;
}
_ => {
return Err(e);
}
}
}
}
}
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
use std::sync::Arc;
use tempfile::TempDir;
#[test]
fn test_version_source_precedence() {
assert!(VersionSource::UserFlag.precedence() < VersionSource::SSTableMetadata.precedence());
assert!(
VersionSource::SSTableMetadata.precedence()
< VersionSource::DatasetMetadata.precedence()
);
assert!(VersionSource::DatasetMetadata.precedence() < VersionSource::Unknown.precedence());
}
#[test]
fn test_version_source_description() {
assert_eq!(
VersionSource::UserFlag.description(),
"User-provided flag (--cassandra-version)"
);
assert_eq!(
VersionSource::SSTableMetadata.description(),
"SSTable metadata"
);
assert_eq!(
VersionSource::DatasetMetadata.description(),
"Dataset metadata.yml"
);
assert_eq!(
VersionSource::Unknown.description(),
"Unknown (no version information available)"
);
}
#[test]
fn test_resolved_version_is_known() {
let known = ResolvedVersion::new(Some("5.0".to_string()), VersionSource::UserFlag);
assert!(known.is_known());
let unknown = ResolvedVersion::new(None, VersionSource::Unknown);
assert!(!unknown.is_known());
}
#[test]
fn test_resolved_version_or_unknown() {
let known = ResolvedVersion::new(Some("5.0".to_string()), VersionSource::UserFlag);
assert_eq!(known.version_or_unknown(), "5.0");
let unknown = ResolvedVersion::new(None, VersionSource::Unknown);
assert_eq!(unknown.version_or_unknown(), "unknown");
}
#[tokio::test]
async fn test_user_flag_precedence() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let resolved =
VersionHintResolver::resolve(Some("5.0-user".to_string()), temp_dir.path(), platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::UserFlag);
assert_eq!(resolved.version, Some("5.0-user".to_string()));
assert!(resolved.is_known());
}
#[tokio::test]
async fn test_unknown_when_no_sources() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let resolved = VersionHintResolver::resolve(None, temp_dir.path(), platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::Unknown);
assert_eq!(resolved.version, None);
assert!(!resolved.is_known());
assert_eq!(resolved.version_or_unknown(), "unknown");
}
#[tokio::test]
async fn test_metadata_yml_parsing() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let metadata_content = "cassandra_version: \"5.0\"\nkeyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
platform
.fs()
.write_file(&metadata_path, metadata_content.as_bytes())
.await
.unwrap();
let resolved = VersionHintResolver::resolve(None, temp_dir.path(), platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::DatasetMetadata);
assert_eq!(resolved.version, Some("5.0".to_string()));
}
#[tokio::test]
async fn test_metadata_yml_parent_directory() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let metadata_content = "cassandra_version: \"4.0\"\nkeyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
platform
.fs()
.write_file(&metadata_path, metadata_content.as_bytes())
.await
.unwrap();
let sstable_dir = temp_dir.path().join("sstables");
platform.fs().create_dir(&sstable_dir).await.unwrap();
let resolved = VersionHintResolver::resolve(None, &sstable_dir, platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::DatasetMetadata);
assert_eq!(resolved.version, Some("4.0".to_string()));
}
#[tokio::test]
async fn test_metadata_yml_invalid_yaml() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let metadata_path = temp_dir.path().join("metadata.yml");
platform
.fs()
.write_file(&metadata_path, b"invalid: yaml: syntax: error:")
.await
.unwrap();
let result = VersionHintResolver::resolve(None, temp_dir.path(), platform).await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Failed to parse metadata.yml"));
}
#[tokio::test]
async fn test_metadata_yml_missing_version_field() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let metadata_content = "keyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
platform
.fs()
.write_file(&metadata_path, metadata_content.as_bytes())
.await
.unwrap();
let resolved = VersionHintResolver::resolve(None, temp_dir.path(), platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::Unknown);
assert_eq!(resolved.version, None);
}
#[tokio::test]
async fn test_user_flag_overrides_metadata_yml() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let metadata_content = "cassandra_version: \"5.0\"\nkeyspaces: []\n";
let metadata_path = temp_dir.path().join("metadata.yml");
platform
.fs()
.write_file(&metadata_path, metadata_content.as_bytes())
.await
.unwrap();
let resolved = VersionHintResolver::resolve(
Some("4.0-override".to_string()),
temp_dir.path(),
platform,
)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::UserFlag);
assert_eq!(resolved.version, Some("4.0-override".to_string()));
}
#[tokio::test]
async fn test_not_found_error_robustness() {
let temp_dir = TempDir::new().unwrap();
let config = Config::default();
let platform = Arc::new(crate::Platform::new(&config).await.unwrap());
let resolved = VersionHintResolver::resolve(None, temp_dir.path(), platform)
.await
.unwrap();
assert_eq!(resolved.source, VersionSource::Unknown);
assert_eq!(resolved.version, None);
}
}