use crate::debug::CallStackDebug;
use crate::field::info::RustFieldInfo;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum CustomConversionStrategy {
FromFn(String),
IntoFn(String),
Bidirectional(String, String),
}
impl CustomConversionStrategy {
pub fn from_field_info(name: &syn::Ident, rust: &RustFieldInfo) -> Option<Self> {
let _trace = CallStackDebug::with_context(
"conversion::custom_strategy::CustomConversionStrategy",
"from_field_info",
name,
&rust.field_name,
&[
("from_proto_fn", &format!("{:?}", rust.from_proto_fn)),
("to_proto_fn", &format!("{:?}", rust.to_proto_fn)),
],
);
let strategy = match (&rust.from_proto_fn, &rust.to_proto_fn) {
(Some(from_fn), Some(into_fn)) => {
Some(Self::Bidirectional(from_fn.clone(), into_fn.clone()))
}
(Some(from_fn), None) => Some(Self::FromFn(from_fn.clone())),
(None, Some(into_fn)) => Some(Self::IntoFn(into_fn.clone())),
(None, None) => None,
};
_trace.checkpoint_data(
"custom_conversion_strategy",
&[("strategy", &format!("{:?}", strategy))],
);
strategy
}
#[cfg(test)]
pub fn from_proto_fn(&self) -> Option<&str> {
match self {
Self::FromFn(fn_name) | Self::Bidirectional(fn_name, _) => Some(fn_name),
Self::IntoFn(_) => None,
}
}
#[cfg(test)]
pub fn to_proto_fn(&self) -> Option<&str> {
match self {
Self::IntoFn(fn_name) | Self::Bidirectional(_, fn_name) => Some(fn_name),
Self::FromFn(_) => None,
}
}
#[cfg(test)]
pub fn has_from_proto_fn(&self) -> bool {
matches!(self, Self::FromFn(_) | Self::Bidirectional(_, _))
}
#[cfg(test)]
pub fn has_to_proto_fn(&self) -> bool {
matches!(self, Self::IntoFn(_) | Self::Bidirectional(_, _))
}
#[cfg(test)]
pub fn is_bidirectional(&self) -> bool {
matches!(self, Self::Bidirectional(_, _))
}
pub fn validate(&self) -> Result<(), String> {
let validate_path = |path: &str, direction: &str| -> Result<(), String> {
if path.trim().is_empty() {
return Err(format!(
"Custom {} function path cannot be empty",
direction
));
}
Ok(())
};
match self {
Self::FromFn(path) => validate_path(path, "proto_to_rust"),
Self::IntoFn(path) => validate_path(path, "rust_to_proto"),
Self::Bidirectional(from_path, into_path) => {
validate_path(from_path, "proto_to_rust")?;
validate_path(into_path, "rust_to_proto")?;
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mock_rust_field_with_fns(
from_proto_fn: Option<String>,
to_proto_fn: Option<String>,
) -> RustFieldInfo {
let field_name: syn::Ident = syn::parse_str("test_field").unwrap();
let field_type: syn::Type = syn::parse_str("String").unwrap();
RustFieldInfo {
field_name,
field_type,
is_option: false,
is_vec: false,
is_primitive: true,
is_custom: false,
is_enum: false,
has_transparent: false,
has_default: false,
expect_mode: crate::analysis::expect_analysis::ExpectMode::None,
has_proto_ignore: false,
from_proto_fn,
to_proto_fn,
}
}
#[test]
fn test_custom_strategy_detection() {
let field_name: syn::Ident = syn::parse_str("test_field").unwrap();
let rust =
mock_rust_field_with_fns(Some("from_proto".to_string()), Some("to_proto".to_string()));
let strategy = CustomConversionStrategy::from_field_info(&field_name, &rust);
assert_eq!(
strategy,
Some(CustomConversionStrategy::Bidirectional(
"from_proto".to_string(),
"to_proto".to_string()
))
);
let rust = mock_rust_field_with_fns(Some("from_proto".to_string()), None);
let strategy = CustomConversionStrategy::from_field_info(&field_name, &rust);
assert_eq!(
strategy,
Some(CustomConversionStrategy::FromFn("from_proto".to_string()))
);
let rust = mock_rust_field_with_fns(None, Some("to_proto".to_string()));
let strategy = CustomConversionStrategy::from_field_info(&field_name, &rust);
assert_eq!(
strategy,
Some(CustomConversionStrategy::IntoFn("to_proto".to_string()))
);
let rust = mock_rust_field_with_fns(None, None);
let strategy = CustomConversionStrategy::from_field_info(&field_name, &rust);
assert_eq!(strategy, None);
}
#[test]
fn test_function_name_extraction() {
let bidirectional =
CustomConversionStrategy::Bidirectional("from_fn".to_string(), "into_fn".to_string());
assert_eq!(bidirectional.from_proto_fn(), Some("from_fn"));
assert_eq!(bidirectional.to_proto_fn(), Some("into_fn"));
assert!(bidirectional.has_from_proto_fn());
assert!(bidirectional.has_to_proto_fn());
assert!(bidirectional.is_bidirectional());
let from_only = CustomConversionStrategy::FromFn("from_fn".to_string());
assert_eq!(from_only.from_proto_fn(), Some("from_fn"));
assert_eq!(from_only.to_proto_fn(), None);
assert!(from_only.has_from_proto_fn());
assert!(!from_only.has_to_proto_fn());
assert!(!from_only.is_bidirectional());
let into_only = CustomConversionStrategy::IntoFn("into_fn".to_string());
assert_eq!(into_only.from_proto_fn(), None);
assert_eq!(into_only.to_proto_fn(), Some("into_fn"));
assert!(!into_only.has_from_proto_fn());
assert!(into_only.has_to_proto_fn());
assert!(!into_only.is_bidirectional());
}
#[test]
fn test_validation() {
let valid = CustomConversionStrategy::FromFn("valid_function_name".to_string());
assert!(valid.validate().is_ok());
let empty = CustomConversionStrategy::FromFn("".to_string());
assert!(empty.validate().is_err());
let whitespace = CustomConversionStrategy::IntoFn(" ".to_string());
assert!(whitespace.validate().is_err());
let bidirectional_valid =
CustomConversionStrategy::Bidirectional("from_fn".to_string(), "into_fn".to_string());
assert!(bidirectional_valid.validate().is_ok());
let bidirectional_invalid =
CustomConversionStrategy::Bidirectional("from_fn".to_string(), "".to_string());
assert!(bidirectional_invalid.validate().is_err());
}
}