use crate::analysis::{
attribute_parser,
expect_analysis::{self, ExpectMode},
type_analysis,
};
use crate::debug::CallStackDebug;
use crate::field::FieldProcessingContext;
use quote::{ToTokens, quote};
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum FieldOptionality {
Optional,
Required,
}
impl Default for FieldOptionality {
fn default() -> Self {
Self::Required
}
}
impl std::fmt::Display for FieldOptionality {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Optional => write!(f, "Optional"),
Self::Required => write!(f, "Required"),
}
}
}
impl FieldOptionality {
#[allow(unused)]
pub fn new(is_optional: bool) -> Self {
if is_optional {
Self::Optional
} else {
Self::Required
}
}
}
impl FieldOptionality {
pub fn from_field_context(ctx: &FieldProcessingContext, field: &syn::Field) -> Self {
let _trace = CallStackDebug::with_context(
"analysis::optionality::FieldOptionality",
"from_field_context",
ctx.struct_name,
ctx.field_name,
&[],
);
if let Ok(proto_meta) = attribute_parser::ProtoFieldMeta::from_field(field)
&& proto_meta.has_explicit_optionality()
{
_trace.decision(
"explicit_optionality_flage",
"explicit annotation takes absolute precedence",
);
if proto_meta.is_proto_optional() {
Self::Optional
} else {
Self::Required
}
} else if let Some(inferred) = Self::infer_from_patterns(ctx, field) {
inferred
} else {
Self::emit_ambiguity_error(ctx);
Self::Required
}
}
fn infer_from_patterns(ctx: &FieldProcessingContext, field: &syn::Field) -> Option<Self> {
let _trace = CallStackDebug::with_context(
"analysis::optionality::FieldOptionality",
"infer_from_patterns",
ctx.struct_name,
ctx.field_name,
&[],
);
let field_type = &field.ty;
if type_analysis::is_option_type(field_type) {
_trace.decision("is_option", "Option<T> in Rust → optional proto field");
return Some(Self::Optional);
}
if type_analysis::is_vec_type(field_type) {
_trace.decision("is_vec", " Vec<T> → required repeated proto field");
return Some(Self::Required);
}
if Self::has_explicit_optional_usage_indicators(ctx, field) {
_trace.decision(
"has_explicit_usage_indicators",
"Fields with explicit expect() or default() → optional proto field",
);
return Some(Self::Optional);
}
if type_analysis::is_primitive_type(field_type) {
_trace.decision("is_primitive", "Primitive -> required proto field");
return Some(Self::Required);
}
if type_analysis::is_enum_type(field_type) {
_trace.decision("is_enum", "Enum -> required proto field");
return Some(Self::Required);
}
if Self::is_custom_type_without_optional_indicators(ctx, field_type) {
_trace.decision(
"custom_type_required",
"Custom type without optional indicators → required proto field",
);
return Some(Self::Required);
}
if Self::is_newtype_wrapper(field_type) {
if Self::has_explicit_optional_usage_indicators(ctx, field) {
_trace.decision(
"newtype_with_usage",
"Newtype with explicit expect/default → optional",
);
return Some(Self::Optional);
} else {
_trace.decision(
"newtype_without_usage",
"Newtype without indicators → required",
);
return Some(Self::Required);
}
}
_trace.error("? AMBIGUOUS: Requires explicit #[proto(optional = true/false)]");
None
}
fn has_explicit_optional_usage_indicators(
ctx: &FieldProcessingContext,
field: &syn::Field,
) -> bool {
let has_explicit_expect = expect_analysis::has_expect_panic_syntax(field)
|| (ctx.expect_mode != ExpectMode::None && Self::has_expect_attribute_on_field(field));
let has_explicit_default =
Self::has_default_fn_attribute(field) || Self::has_any_default_attribute(field);
let result = has_explicit_expect || has_explicit_default;
if result {
CallStackDebug::new(
"analysis::optionality::FieldOptionality",
"has_explicit_optional_usage_indicators",
ctx.struct_name,
ctx.field_name,
)
.checkpoint_data(
"explicit_usage_found",
&[
("has_explicit_expect", &has_explicit_expect.to_string()),
("has_explicit_default", &has_explicit_default.to_string()),
],
);
}
result
}
fn is_custom_type_without_optional_indicators(
ctx: &FieldProcessingContext,
field_type: &syn::Type,
) -> bool {
if let syn::Type::Path(type_path) = field_type {
let segments = &type_path.path.segments;
if segments.len() == 1 {
let is_primitive = type_analysis::is_primitive_type(field_type);
let is_std_type = Self::is_std_type(field_type);
let is_proto_type = type_analysis::is_proto_type(field_type, ctx.proto_module);
let is_custom = !is_primitive && !is_std_type && !is_proto_type;
if is_custom {
CallStackDebug::new(
"analysis::optionality::FieldOptionality",
"is_custom_type_without_optional_indicators",
ctx.struct_name,
ctx.field_name,
)
.checkpoint_data(
"custom_type_detected",
&[
("type_name", &segments[0].ident.to_string()),
("is_primitive", &is_primitive.to_string()),
("is_std_type", &is_std_type.to_string()),
("is_proto_type", &is_proto_type.to_string()),
],
);
}
is_custom
} else {
false
}
} else {
false
}
}
fn has_expect_attribute_on_field(field: &syn::Field) -> bool {
if let Ok(proto_meta) = attribute_parser::ProtoFieldMeta::from_field(field) {
proto_meta.expect
} else {
false
}
}
#[allow(unused)]
fn has_optional_usage_indicators(ctx: &FieldProcessingContext, field: &syn::Field) -> bool {
let has_expect = !matches!(ctx.expect_mode, ExpectMode::None)
|| expect_analysis::has_expect_panic_syntax(field);
let has_default = ctx.has_default
|| Self::has_default_fn_attribute(field)
|| Self::has_any_default_attribute(field);
has_expect || has_default
}
fn has_default_fn_attribute(field: &syn::Field) -> bool {
if let Ok(proto_meta) = attribute_parser::ProtoFieldMeta::from_field(field) {
proto_meta.default_fn.is_some()
} else {
false
}
}
fn has_any_default_attribute(field: &syn::Field) -> bool {
attribute_parser::ProtoFieldMeta::from_field(field)
.map(|proto_meta| {
proto_meta.default_fn.is_some() ||
field.attrs.iter().any(|attr| {
attr.path().is_ident("proto") &&
attr.to_token_stream().to_string().contains("default")
})
})
.unwrap_or(false)
}
#[allow(unused)]
fn is_newtype_wrapper(field_type: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = field_type {
let segments = &type_path.path.segments;
segments.len() == 1
&& !type_analysis::is_primitive_type(field_type)
&& !Self::is_std_type(field_type)
} else {
false
}
}
#[allow(unused)]
fn get_newtype_inner_type(field_type: &syn::Type) -> Option<syn::Type> {
if let syn::Type::Path(type_path) = field_type {
if type_path.path.segments.len() == 1 {
return Some(syn::parse_quote!(u64)); }
}
None
}
fn is_std_type(field_type: &syn::Type) -> bool {
matches!(
quote!(#field_type).to_string().as_str(),
"String" | "Vec" | "HashMap" | "HashSet" | "BTreeMap" | "BTreeSet"
)
}
fn emit_ambiguity_error(ctx: &FieldProcessingContext) {
panic!(
"Cannot infer optionality for field '{}.{}' of type '{}'. \
Add explicit annotation: #[proto(proto_optional)] or #[proto(proto_required)]",
ctx.struct_name,
ctx.field_name,
quote!(ctx.field_type)
);
}
pub fn is_optional(self) -> bool {
matches!(self, Self::Optional)
}
pub fn is_required(self) -> bool {
matches!(self, Self::Required)
}
}