Skip to main content

miniextendr_macros/
s7_conversion.rs

1//! Feed S7 conversion markers and attributes into the same registration plan.
2
3use crate::miniextendr_impl::{ClassSystem, ParsedMethod, ReceiverKind};
4use crate::return_wrap::ConversionKind;
5use syn::{GenericArgument, PathArguments, Type};
6
7pub(crate) fn configure(
8    method: &mut ParsedMethod,
9    original: &syn::ImplItemFn,
10    system: ClassSystem,
11    implementing_type: &syn::Ident,
12) -> syn::Result<()> {
13    let marker = method.return_wrap.as_ref().and_then(|wrap| wrap.conversion);
14    let attrs = &mut method.method_attrs;
15    if marker.is_none() && attrs.s7.convert_to.is_none() && attrs.s7.convert_from.is_none() {
16        return Ok(());
17    }
18    let error = |message| syn::Error::new_spanned(&original.sig, message);
19    if system != ClassSystem::S7 {
20        return Err(error(
21            "S7 conversion markers and attributes require an inherent S7 impl",
22        ));
23    }
24    if attrs.serialize || attrs.unwrap_in_r {
25        return Err(error(
26            "S7 class conversion cannot be combined with serialize or unwrap_in_r",
27        ));
28    }
29    if attrs.s7.convert_to.is_some() && attrs.s7.convert_from.is_some() {
30        return Err(error(
31            "cannot specify both `convert_from` and `convert_to` on the same method; convert_from is for static methods, convert_to is for instance methods",
32        ));
33    }
34    let kind = marker.unwrap_or_else(|| {
35        if attrs.s7.convert_to.is_some() {
36            ConversionKind::To
37        } else {
38            ConversionKind::From
39        }
40    });
41    let params: Vec<_> = original
42        .sig
43        .inputs
44        .iter()
45        .filter_map(|arg| {
46            if let syn::FnArg::Typed(param) = arg {
47                Some(param)
48            } else {
49                None
50            }
51        })
52        .collect();
53    match kind {
54        ConversionKind::To => {
55            if method.env == ReceiverKind::None || !params.is_empty() {
56                return Err(error(
57                    "convert_to requires an instance method with no additional parameters",
58                ));
59            }
60            if attrs.s7.convert_from.is_some() {
61                return Err(error("ConvertTo disagrees with the convert_from attribute"));
62            }
63        }
64        ConversionKind::From => {
65            if method.env != ReceiverKind::None || params.len() != 1 {
66                return Err(error(
67                    "convert_from requires a static method with exactly one source parameter",
68                ));
69            }
70            if attrs.s7.convert_to.is_some() {
71                return Err(error("ConvertFrom disagrees with the convert_to attribute"));
72            }
73        }
74    }
75    if method.return_wrap.is_none() {
76        method.return_wrap =
77            crate::return_wrap::resolve(&method.sig.output, Some(ClassSystem::S7))?.0;
78    }
79    let wrap = method
80        .return_wrap
81        .as_mut()
82        .expect("a conversion has a named return payload");
83    if wrap.system() != ClassSystem::S7 {
84        return Err(error(
85            "S7 conversion cannot select another return class system",
86        ));
87    }
88    if wrap.has_vector() {
89        return Err(error(
90            "S7 conversion returns one class instance, not a vector of class instances",
91        ));
92    }
93    match kind {
94        ConversionKind::To => {
95            if marker.is_some() {
96                let target = if wrap.target() == "Self" {
97                    crate::naming::ident_name(implementing_type)
98                } else {
99                    wrap.target().to_owned()
100                };
101                if attrs
102                    .s7
103                    .convert_to
104                    .as_ref()
105                    .is_some_and(|name| name != &target)
106                {
107                    return Err(error("ConvertTo target and convert_to attribute disagree"));
108                }
109                attrs.s7.convert_to = Some(target);
110            }
111            let target = attrs
112                .s7
113                .convert_to
114                .as_ref()
115                .expect("convert_to target was selected");
116            wrap.set_target(crate::r_class_formatter::class_ref_or_verbatim(target));
117        }
118        ConversionKind::From => {
119            if wrap.target() != "Self"
120                && wrap.target() != crate::naming::ident_name(implementing_type)
121            {
122                return Err(error(
123                    "convert_from must return Self or the enclosing class type",
124                ));
125            }
126            if marker.is_some() {
127                let source = source_class(&params[0].ty).ok_or_else(|| error("ConvertFrom needs a named source class parameter, such as `&Source` or `ExternalPtr<Source>`"))?;
128                let source = if source == "Self" {
129                    crate::naming::ident_name(implementing_type)
130                } else {
131                    source
132                };
133                if attrs
134                    .s7
135                    .convert_from
136                    .as_ref()
137                    .is_some_and(|name| name != &source)
138                {
139                    return Err(error(
140                        "ConvertFrom source parameter and convert_from attribute disagree",
141                    ));
142                }
143                attrs.s7.convert_from = Some(source);
144            }
145            wrap.set_target("Self".to_owned());
146        }
147    }
148    wrap.conversion = Some(kind);
149    Ok(())
150}
151
152fn source_class(ty: &Type) -> Option<String> {
153    if let Type::Reference(reference) = ty {
154        return source_class(&reference.elem);
155    }
156    let Type::Path(path) = ty else { return None };
157    let segment = path.path.segments.last()?;
158    if segment.ident == "ExternalPtr"
159        || segment.ident == "ExternalPtrRef"
160        || segment.ident == "ExternalPtrMut"
161    {
162        let PathArguments::AngleBracketed(args) = &segment.arguments else {
163            return None;
164        };
165        let inner = args.args.iter().find_map(|arg| match arg {
166            GenericArgument::Type(inner) => Some(inner),
167            _ => None,
168        })?;
169        return source_class(inner);
170    }
171    if !#[allow(non_exhaustive_omitted_patterns)] match segment.arguments {
    PathArguments::None => true,
    _ => false,
}matches!(segment.arguments, PathArguments::None) {
172        return None;
173    }
174    Some(crate::naming::ident_name(&segment.ident))
175}