feat(versionable): add transparent mode for newtype structs

This commit is contained in:
Nicolas Sarlin
2024-10-04 14:26:38 +02:00
committed by Nicolas Sarlin
parent 543b39951b
commit 51da8fe735
5 changed files with 691 additions and 373 deletions

View File

@@ -7,8 +7,10 @@
mod associated;
mod dispatch_type;
mod transparent;
mod version_type;
mod versionize_attribute;
mod versionize_impl;
use dispatch_type::DispatchType;
use proc_macro::TokenStream;
@@ -51,6 +53,7 @@ pub(crate) const SEND_TRAIT_NAME: &str = "::core::marker::Send";
pub(crate) const STATIC_LIFETIME_NAME: &str = "'static";
use associated::AssociatingTrait;
use versionize_impl::VersionizeImplementor;
use crate::version_type::VersionType;
use crate::versionize_attribute::VersionizeAttribute;
@@ -131,21 +134,24 @@ pub fn derive_versions_dispatch(input: TokenStream) -> TokenStream {
pub fn derive_versionize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let attributes = syn_unwrap!(
VersionizeAttribute::parse_from_attributes_list(&input.attrs).and_then(|attr_opt| attr_opt
.ok_or_else(|| syn::Error::new(
Span::call_site(),
"Missing `versionize` attribute for `Versionize`",
)))
);
let attributes = syn_unwrap!(VersionizeAttribute::parse_from_attributes_list(
&input.attrs
));
let implementor = syn_unwrap!(VersionizeImplementor::new(
attributes,
&input.data,
Span::call_site()
));
// If we apply a type conversion before the call to versionize, the type that implements
// the `Version` trait is the target type and not Self
let version_trait_impl: Option<proc_macro2::TokenStream> = if attributes.needs_conversion() {
None
} else {
Some(impl_version_trait(&input))
};
let version_trait_impl: Option<proc_macro2::TokenStream> =
if implementor.is_directly_versioned() {
Some(impl_version_trait(&input))
} else {
None
};
// Parse the name of the traits that we will implement
let versionize_trait: Path = parse_const_str(VERSIONIZE_TRAIT_NAME);
@@ -162,28 +168,28 @@ pub fn derive_versionize(input: TokenStream) -> TokenStream {
let (_, ty_generics, _) = input.generics.split_for_impl();
// Generates the associated types required by the traits
let versioned_type = attributes.versioned_type(&lifetime, &input.generics);
let versioned_owned_type = attributes.versioned_owned_type(&input.generics);
let versioned_type = implementor.versioned_type(&lifetime, &input.generics);
let versioned_owned_type = implementor.versioned_owned_type(&input.generics);
let versioned_type_where_clause =
attributes.versioned_type_where_clause(&lifetime, &input.generics);
implementor.versioned_type_where_clause(&lifetime, &input.generics);
let versioned_owned_type_where_clause =
attributes.versioned_owned_type_where_clause(&input.generics);
implementor.versioned_owned_type_where_clause(&input.generics);
// If the original type has some generics, we need to add bounds on them for
// the traits impl
let versionize_trait_where_clause =
syn_unwrap!(attributes.versionize_trait_where_clause(&input.generics));
syn_unwrap!(implementor.versionize_trait_where_clause(&input.generics));
let versionize_owned_trait_where_clause =
syn_unwrap!(attributes.versionize_owned_trait_where_clause(&input.generics));
syn_unwrap!(implementor.versionize_owned_trait_where_clause(&input.generics));
let unversionize_trait_where_clause =
syn_unwrap!(attributes.unversionize_trait_where_clause(&input.generics));
syn_unwrap!(implementor.unversionize_trait_where_clause(&input.generics));
let trait_impl_generics = input.generics.split_for_impl().0;
let versionize_body = attributes.versionize_method_body();
let versionize_owned_body = attributes.versionize_owned_method_body();
let versionize_body = implementor.versionize_method_body();
let versionize_owned_body = implementor.versionize_owned_method_body();
let unversionize_arg_name = Ident::new("versioned", Span::call_site());
let unversionize_body = attributes.unversionize_method_body(&unversionize_arg_name);
let unversionize_body = implementor.unversionize_method_body(&unversionize_arg_name);
let unversionize_error: Path = parse_const_str(UNVERSIONIZE_ERROR_NAME);
quote! {

View File

@@ -0,0 +1,58 @@
use proc_macro2::Span;
use syn::punctuated::Punctuated;
use syn::{Data, Field, Ident, Token, Type};
/// A transparent struct that is versioned using its inner field
pub(crate) struct TransparentStruct {
pub(crate) inner_type: Type,
pub(crate) kind: TransparentStructKind,
}
/// Transparent struct can be either newtypes or regular structs with a single field.
pub(crate) enum TransparentStructKind {
NewType,
SingleField(Ident),
}
impl TransparentStruct {
/// Parse the type declaration to find the target versioned type when the `transparent`
/// attribute is used.
pub(crate) fn new(decla: &Data, base_span: Span) -> syn::Result<Self> {
let error = || {
syn::Error::new(
base_span,
"'transparent' attribute is only supported for single field structs",
)
};
match decla {
Data::Struct(stru) => match &stru.fields {
syn::Fields::Named(named_fields) => {
Self::from_fields(&named_fields.named).map_err(|_| error())
}
syn::Fields::Unnamed(unnamed_fields) => {
Self::from_fields(&unnamed_fields.unnamed).map_err(|_| error())
}
syn::Fields::Unit => Err(error()),
},
Data::Enum(_) => Err(error()),
Data::Union(_) => Err(error()),
}
}
/// Get the single element inside a transparent struct
fn from_fields(fields: &Punctuated<Field, Token![,]>) -> Result<Self, ()> {
if fields.len() != 1 {
Err(())
} else {
let field = fields.first().unwrap();
let inner_type = field.ty.clone();
let kind = match &field.ident {
Some(ident) => TransparentStructKind::SingleField(ident.clone()),
None => TransparentStructKind::NewType,
};
Ok(Self { inner_type, kind })
}
}
}

View File

@@ -1,39 +1,41 @@
//! This module handles the parsing of the parameters of the proc macro, found in the
//! `#[versionize(...)]` attribute
use proc_macro2::Span;
use quote::{quote, ToTokens};
use quote::ToTokens;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{
parse_quote, Attribute, Expr, GenericArgument, GenericParam, Generics, Ident, Lifetime, Lit,
Meta, Path, PathArguments, Token, TraitBound, Type, TypeParam, WhereClause,
};
use crate::{
add_lifetime_where_clause, add_trait_where_clause, add_where_lifetime_bound_to_generics,
parse_const_str, DISPATCH_TRAIT_NAME, ERROR_TRAIT_NAME, FROM_TRAIT_NAME, INTO_TRAIT_NAME,
SEND_TRAIT_NAME, STATIC_LIFETIME_NAME, SYNC_TRAIT_NAME, TRY_INTO_TRAIT_NAME,
UNVERSIONIZE_ERROR_NAME, UNVERSIONIZE_TRAIT_NAME, VERSIONIZE_OWNED_TRAIT_NAME,
};
use syn::{Attribute, Expr, Lit, Meta, Path, Token};
/// Name of the attribute used to give arguments to the `Versionize` macro
const VERSIONIZE_ATTR_NAME: &str = "versionize";
pub(crate) struct ClassicVersionizeAttribute {
dispatch_enum: Path,
}
pub(crate) enum ConversionType {
Direct,
Try,
}
pub(crate) struct ConvertVersionizeAttribute {
conversion_target: Path,
conversion_type: ConversionType,
}
/// Transparent mode can also be activated using `#[repr(transparent)]`
const REPR_ATTR_NAME: &str = "repr";
/// Represent the parsed `#[versionize(...)]` attribute
pub(crate) enum VersionizeAttribute {
Classic(ClassicVersionizeAttribute),
Convert(ConvertVersionizeAttribute),
Transparent,
}
/// The "classic" variant of the versionize attribute: `#[versionize(MyTypeVersions)]`
pub(crate) struct ClassicVersionizeAttribute {
pub(crate) dispatch_enum: Path,
}
/// A versionize attribute with a type conversion: `#[versionize(convert = "SerializableMyType")]`
/// or `#[versionize(from = "SerializableMyType", into = "SerializableMyType")]`
pub(crate) struct ConvertVersionizeAttribute {
pub(crate) conversion_target: Path,
pub(crate) conversion_type: ConversionType,
}
/// Tell if the conversion can fail or not
pub(crate) enum ConversionType {
Direct,
Try,
}
#[derive(Default)]
@@ -44,10 +46,27 @@ struct VersionizeAttributeBuilder {
from: Option<Path>,
try_from: Option<Path>,
into: Option<Path>,
transparent: bool,
}
impl VersionizeAttributeBuilder {
fn build(self, base_span: &Span) -> syn::Result<VersionizeAttribute> {
fn build(self, base_span: Span) -> syn::Result<VersionizeAttribute> {
if self.transparent {
if self.dispatch_enum.is_some()
|| self.convert.is_some()
|| self.try_convert.is_some()
|| self.from.is_some()
|| self.into.is_some()
{
return Err(syn::Error::new(
base_span,
"'transparent' does not accept any other parameters",
));
} else {
return Ok(VersionizeAttribute::Transparent);
}
}
let convert_is_try = self.try_convert.is_some() || self.try_from.is_some();
// User should not use `from` and `try_from` at the same time
let from_target = match (self.from, self.try_from) {
@@ -129,10 +148,9 @@ should have the same value",
}))
} else {
Ok(VersionizeAttribute::Classic(ClassicVersionizeAttribute {
dispatch_enum: self.dispatch_enum.ok_or(syn::Error::new(
*base_span,
"Missing dispatch enum argument",
))?,
dispatch_enum: self
.dispatch_enum
.ok_or(syn::Error::new(base_span, "Missing dispatch enum argument"))?,
}))
}
}
@@ -143,17 +161,30 @@ impl VersionizeAttribute {
/// `DispatchType` is the name of the type holding the dispatch enum.
/// Return an error if no `versionize` attribute has been found, if multiple attributes are
/// present on the same struct or if the attribute is malformed.
pub(crate) fn parse_from_attributes_list(
attributes: &[Attribute],
) -> syn::Result<Option<Self>> {
pub(crate) fn parse_from_attributes_list(attributes: &[Attribute]) -> syn::Result<Self> {
let version_attributes: Vec<&Attribute> = attributes
.iter()
.filter(|attr| attr.path().is_ident(VERSIONIZE_ATTR_NAME))
.collect();
let repr_attributes: Vec<&Attribute> = attributes
.iter()
.filter(|attr| attr.path().is_ident(REPR_ATTR_NAME))
.collect();
match version_attributes.as_slice() {
[] => Ok(None),
[attr] => Self::parse_from_attribute(attr).map(Some),
[] => {
// transparent mode can also be enabled via `#[repr(transparent)]`
if let Some(attr) = repr_attributes.first() {
Self::parse_from_attribute(attr)
} else {
Err(syn::Error::new(
Span::call_site(),
"Missing `versionize` attribute for `Versionize`",
))
}
}
[attr] => Self::parse_from_attribute(attr),
[_, attr2, ..] => Err(syn::Error::new(
attr2.span(),
"Multiple `versionize` attributes found",
@@ -173,11 +204,19 @@ impl VersionizeAttribute {
let mut attribute_builder = VersionizeAttributeBuilder::default();
for meta in nested.iter() {
match meta {
Meta::Path(dispatch_enum) => {
if attribute_builder.dispatch_enum.is_some() {
Meta::Path(path) => {
// parse versionize(transparent)
if path.is_ident("transparent") {
if attribute_builder.transparent {
return Err(Self::default_error(meta.span()));
} else {
attribute_builder.transparent = true;
}
// parse versionize(MyTypeVersions)
} else if attribute_builder.dispatch_enum.is_some() {
return Err(Self::default_error(meta.span()));
} else {
attribute_builder.dispatch_enum = Some(dispatch_enum.clone());
attribute_builder.dispatch_enum = Some(path.clone());
}
}
Meta::NameValue(name_value) => {
@@ -221,7 +260,7 @@ impl VersionizeAttribute {
attribute_builder.into =
Some(parse_path_ignore_quotes(&name_value.value)?);
}
// parse versionize(dispatch = "Type")
// parse versionize(dispatch = "MyTypeVersions")
} else if name_value.path.is_ident("dispatch") {
if attribute_builder.dispatch_enum.is_some() {
return Err(Self::default_error(meta.span()));
@@ -237,284 +276,7 @@ impl VersionizeAttribute {
}
}
attribute_builder.build(&attribute.span())
}
pub(crate) fn needs_conversion(&self) -> bool {
match self {
VersionizeAttribute::Classic(_) => false,
VersionizeAttribute::Convert(_) => true,
}
}
/// Return the associated type used in the `Versionize` trait: `MyType::Versioned<'vers>`
///
/// If the type is directly versioned, this will be a type generated by the `VersionDispatch`.
///
/// If we have a conversion before the versioning, we re-use the versioned_owned type of the
/// conversion target. The versioned_owned is needed because the conversion will create a new
/// value, so we can't just use a reference.
pub(crate) fn versioned_type(
&self,
lifetime: &Lifetime,
input_generics: &Generics,
) -> proc_macro2::TokenStream {
match self {
VersionizeAttribute::Classic(attr) => {
let (_, ty_generics, _) = input_generics.split_for_impl();
let dispatch_trait: Path = parse_const_str(DISPATCH_TRAIT_NAME);
let dispatch_enum_path = &attr.dispatch_enum;
quote! {
<#dispatch_enum_path #ty_generics as
#dispatch_trait<Self>>::Ref<#lifetime>
}
}
VersionizeAttribute::Convert(_) => {
// If we want to apply a conversion before the call to versionize we need to use the
// "owned" alternative of the dispatch enum to be able to store the
// conversion result.
self.versioned_owned_type(input_generics)
}
}
}
/// Return the where clause for `MyType::Versioned<'vers>`. if `MyType` has generics, this means
/// adding a 'vers lifetime bound on them.
pub(crate) fn versioned_type_where_clause(
&self,
lifetime: &Lifetime,
input_generics: &Generics,
) -> Option<WhereClause> {
let mut generics = input_generics.clone();
add_where_lifetime_bound_to_generics(&mut generics, lifetime);
let (_, _, where_clause) = generics.split_for_impl();
where_clause.cloned()
}
/// Return the associated type used in the `VersionizeOwned` trait: `MyType::VersionedOwned`
///
/// If the type is directly versioned, this will be a type generated by the `VersionDispatch`.
///
/// If we have a conversion before the versioning, we re-use the versioned_owned type of the
/// conversion target.
pub(crate) fn versioned_owned_type(
&self,
input_generics: &Generics,
) -> proc_macro2::TokenStream {
let (_, ty_generics, _) = input_generics.split_for_impl();
match self {
VersionizeAttribute::Classic(attr) => {
let dispatch_trait: Path = parse_const_str(DISPATCH_TRAIT_NAME);
let dispatch_enum_path = &attr.dispatch_enum;
quote! {
<#dispatch_enum_path #ty_generics as
#dispatch_trait<Self>>::Owned
}
}
VersionizeAttribute::Convert(convert_attr) => {
let convert_type_path = &convert_attr.conversion_target;
let versionize_owned_trait: Path = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
quote! {
<#convert_type_path as #versionize_owned_trait>::VersionedOwned
}
}
}
}
/// Return the where clause for `MyType::VersionedOwned`.
///
/// This is simply the where clause of the input type.
pub(crate) fn versioned_owned_type_where_clause(
&self,
input_generics: &Generics,
) -> Option<WhereClause> {
match self {
VersionizeAttribute::Classic(_) => input_generics.split_for_impl().2.cloned(),
VersionizeAttribute::Convert(convert_attr) => {
extract_generics(&convert_attr.conversion_target)
.split_for_impl()
.2
.cloned()
}
}
}
/// Return the where clause needed to implement the Versionize trait.
///
/// This is the same as the one for the VersionizeOwned, with an additional "Clone" bound in the
/// case where we need to perform a conversion before the versioning.
pub(crate) fn versionize_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
// The base bounds for the owned traits are also used for the ref traits
let mut generics = input_generics.clone();
if self.needs_conversion() {
// The versionize method takes a ref. We need to own the input type in the conversion
// case to apply `From<Input> for Target`. This adds a `Clone` bound to have
// a better error message if the input type is not Clone.
add_trait_where_clause(&mut generics, [&parse_quote! { Self }], &["Clone"])?;
}
self.versionize_owned_trait_where_clause(&generics)
}
/// Return the where clause needed to implement the VersionizeOwned trait.
///
/// If the type is directly versioned, the bound states that the argument points to a valid
/// DispatchEnum for this type. This is done by adding a bound on this argument to
/// `VersionsDisaptch<Self>`.
///
/// If there is a conversion, the target of the conversion should implement `VersionizeOwned`
/// and `From<Self>`.
pub(crate) fn versionize_owned_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
let mut generics = input_generics.clone();
match self {
VersionizeAttribute::Classic(attr) => {
let dispatch_generics = generics.clone();
let dispatch_ty_generics = dispatch_generics.split_for_impl().1;
let dispatch_enum_path = &attr.dispatch_enum;
add_trait_where_clause(
&mut generics,
[&parse_quote!(#dispatch_enum_path #dispatch_ty_generics)],
&[format!("{}<Self>", DISPATCH_TRAIT_NAME,)],
)?;
}
VersionizeAttribute::Convert(convert_attr) => {
let convert_type_path = &convert_attr.conversion_target;
add_trait_where_clause(
&mut generics,
[&parse_quote!(#convert_type_path)],
&[
VERSIONIZE_OWNED_TRAIT_NAME,
&format!("{}<Self>", FROM_TRAIT_NAME),
],
)?;
}
}
Ok(generics.split_for_impl().2.cloned())
}
/// Return the where clause for the `Unversionize` trait.
///
/// If the versioning is direct, this is the same bound as the one used for `VersionizeOwned`.
///
/// If there is a conversion, the target of the conversion need to implement `Unversionize` and
/// `Into` or `TryInto<T, E>`, with `E: Error + Send + Sync + 'static`
pub(crate) fn unversionize_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
match self {
VersionizeAttribute::Classic(_) => {
self.versionize_owned_trait_where_clause(input_generics)
}
VersionizeAttribute::Convert(convert_attr) => {
let mut generics = input_generics.clone();
let convert_type_path = &convert_attr.conversion_target;
let into_trait = match convert_attr.conversion_type {
ConversionType::Direct => format!("{}<Self>", INTO_TRAIT_NAME),
ConversionType::Try => {
// Doing a TryFrom requires that the error
// impl Error + Send + Sync + 'static
let try_into_trait: Path = parse_const_str(TRY_INTO_TRAIT_NAME);
add_trait_where_clause(
&mut generics,
[&parse_quote!(<#convert_type_path as #try_into_trait<Self>>::Error)],
&[ERROR_TRAIT_NAME, SYNC_TRAIT_NAME, SEND_TRAIT_NAME],
)?;
add_lifetime_where_clause(
&mut generics,
[&parse_quote!(<#convert_type_path as #try_into_trait<Self>>::Error)],
&[STATIC_LIFETIME_NAME],
)?;
format!("{}<Self>", TRY_INTO_TRAIT_NAME)
}
};
add_trait_where_clause(
&mut generics,
[&parse_quote!(#convert_type_path)],
&[
UNVERSIONIZE_TRAIT_NAME,
&format!("{}<Self>", FROM_TRAIT_NAME),
&into_trait,
],
)?;
Ok(generics.split_for_impl().2.cloned())
}
}
}
/// Return the body of the versionize method.
pub(crate) fn versionize_method_body(&self) -> proc_macro2::TokenStream {
let versionize_owned_trait: TraitBound = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
match self {
VersionizeAttribute::Classic(_) => {
quote! {
self.into()
}
}
VersionizeAttribute::Convert(convert_attr) => {
let convert_type_path = with_turbofish(&convert_attr.conversion_target);
quote! {
#versionize_owned_trait::versionize_owned(#convert_type_path::from(self.to_owned()))
}
}
}
}
/// Return the body of the versionize_owned method.
pub(crate) fn versionize_owned_method_body(&self) -> proc_macro2::TokenStream {
let versionize_owned_trait: TraitBound = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
match self {
VersionizeAttribute::Classic(_) => {
quote! {
self.into()
}
}
VersionizeAttribute::Convert(convert_attr) => {
let convert_type_path = with_turbofish(&convert_attr.conversion_target);
quote! {
#versionize_owned_trait::versionize_owned(#convert_type_path::from(self))
}
}
}
}
/// Return the body of the unversionize method.
pub(crate) fn unversionize_method_body(&self, arg_name: &Ident) -> proc_macro2::TokenStream {
let error: Type = parse_const_str(UNVERSIONIZE_ERROR_NAME);
match self {
VersionizeAttribute::Classic(_) => {
quote! { #arg_name.try_into() }
}
VersionizeAttribute::Convert(convert_attr) => {
let target = with_turbofish(&convert_attr.conversion_target);
match convert_attr.conversion_type {
ConversionType::Direct => {
quote! { #target::unversionize(#arg_name).map(|value| value.into()) }
}
ConversionType::Try => {
let target_name = format!("{}", target.to_token_stream());
quote! { #target::unversionize(#arg_name).and_then(|value| TryInto::<Self>::try_into(value)
.map_err(|e| #error::conversion(#target_name, e)))
}
}
}
}
}
attribute_builder.build(attribute.span())
}
}
@@ -536,37 +298,3 @@ fn parse_path_ignore_quotes(value: &Expr) -> syn::Result<Path> {
)),
}
}
/// Return the same type but with generics that use the turbofish syntax. Converts
/// `MyStruct<T>` into `MyStruct::<T>`
fn with_turbofish(path: &Path) -> Path {
let mut with_turbo = path.clone();
for segment in with_turbo.segments.iter_mut() {
if let PathArguments::AngleBracketed(generics) = &mut segment.arguments {
generics.colon2_token = Some(Token![::](generics.span()));
}
}
with_turbo
}
/// Extract the generics inside a type
fn extract_generics(path: &Path) -> Generics {
let mut generics = Generics::default();
if let Some(last_segment) = path.segments.last() {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
for arg in &args.args {
if let GenericArgument::Type(Type::Path(type_path)) = arg {
if let Some(ident) = type_path.path.get_ident() {
let param = TypeParam::from(ident.clone());
generics.params.push(GenericParam::Type(param));
}
}
}
}
}
generics
}

View File

@@ -0,0 +1,440 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::spanned::Spanned;
use syn::{
parse_quote, Data, GenericArgument, GenericParam, Generics, Ident, Lifetime, Path,
PathArguments, Token, TraitBound, Type, TypeParam, TypePath, WhereClause,
};
use crate::transparent::{TransparentStruct, TransparentStructKind};
use crate::versionize_attribute::{
ClassicVersionizeAttribute, ConversionType, ConvertVersionizeAttribute, VersionizeAttribute,
};
use crate::{
add_lifetime_where_clause, add_trait_where_clause, add_where_lifetime_bound_to_generics,
parse_const_str, DISPATCH_TRAIT_NAME, ERROR_TRAIT_NAME, FROM_TRAIT_NAME, INTO_TRAIT_NAME,
SEND_TRAIT_NAME, STATIC_LIFETIME_NAME, SYNC_TRAIT_NAME, TRY_INTO_TRAIT_NAME,
UNVERSIONIZE_ERROR_NAME, UNVERSIONIZE_TRAIT_NAME, VERSIONIZE_OWNED_TRAIT_NAME,
VERSIONIZE_TRAIT_NAME,
};
pub(crate) enum VersionizeImplementor {
Classic(ClassicVersionizeAttribute),
Convert(ConvertVersionizeAttribute),
Transparent(TransparentStruct),
}
impl VersionizeImplementor {
pub(crate) fn new(
attributes: VersionizeAttribute,
decla: &Data,
base_span: Span,
) -> syn::Result<Self> {
match attributes {
VersionizeAttribute::Classic(classic) => Ok(Self::Classic(classic)),
VersionizeAttribute::Convert(convert) => Ok(Self::Convert(convert)),
VersionizeAttribute::Transparent => {
Ok(Self::Transparent(TransparentStruct::new(decla, base_span)?))
}
}
}
/// Checks if the type should have a "true" Versionize implementation or if the implementation
/// is delegated to another type
pub(crate) fn is_directly_versioned(&self) -> bool {
match self {
Self::Classic(_) => true,
Self::Convert(_) => false,
Self::Transparent(_) => false,
}
}
/// Return the associated type used in the `Versionize` trait: `MyType::Versioned<'vers>`
///
/// If the type is directly versioned, this will be a type generated by the `VersionDispatch`.
///
/// If we have a conversion before the versioning, we re-use the versioned_owned type of the
/// conversion target. The versioned_owned is needed because the conversion will create a new
/// value, so we can't just use a reference.
pub(crate) fn versioned_type(
&self,
lifetime: &Lifetime,
input_generics: &Generics,
) -> proc_macro2::TokenStream {
match self {
Self::Classic(attr) => {
let (_, ty_generics, _) = input_generics.split_for_impl();
let dispatch_trait: Path = parse_const_str(DISPATCH_TRAIT_NAME);
let dispatch_enum_path = &attr.dispatch_enum;
quote! {
<#dispatch_enum_path #ty_generics as
#dispatch_trait<Self>>::Ref<#lifetime>
}
}
Self::Convert(_) => {
// If we want to apply a conversion before the call to versionize we need to use the
// "owned" alternative of the dispatch enum to be able to store the
// conversion result.
self.versioned_owned_type(input_generics)
}
Self::Transparent(transparent) => {
let versionize_trait: Path = parse_const_str(VERSIONIZE_TRAIT_NAME);
let inner_type = &transparent.inner_type;
quote! { <#inner_type as #versionize_trait>::Versioned<#lifetime>}
}
}
}
/// Return the where clause for `MyType::Versioned<'vers>`. if `MyType` has generics, this means
/// adding a 'vers lifetime bound on them.
pub(crate) fn versioned_type_where_clause(
&self,
lifetime: &Lifetime,
input_generics: &Generics,
) -> Option<WhereClause> {
let mut generics = input_generics.clone();
add_where_lifetime_bound_to_generics(&mut generics, lifetime);
let (_, _, where_clause) = generics.split_for_impl();
where_clause.cloned()
}
/// Return the associated type used in the `VersionizeOwned` trait: `MyType::VersionedOwned`
///
/// If the type is directly versioned, this will be a type generated by the `VersionDispatch`.
///
/// If we have a conversion before the versioning, we re-use the versioned_owned type of the
/// conversion target.
pub(crate) fn versioned_owned_type(
&self,
input_generics: &Generics,
) -> proc_macro2::TokenStream {
let (_, ty_generics, _) = input_generics.split_for_impl();
match self {
Self::Classic(attr) => {
let dispatch_trait: Path = parse_const_str(DISPATCH_TRAIT_NAME);
let dispatch_enum_path = &attr.dispatch_enum;
quote! {
<#dispatch_enum_path #ty_generics as
#dispatch_trait<Self>>::Owned
}
}
Self::Convert(convert_attr) => {
let convert_type_path = &convert_attr.conversion_target;
let versionize_owned_trait: Path = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
quote! {
<#convert_type_path as #versionize_owned_trait>::VersionedOwned
}
}
Self::Transparent(transparent) => {
let versionize_owned_trait: Path = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
let inner_type = &transparent.inner_type;
quote! { <#inner_type as #versionize_owned_trait>::VersionedOwned }
}
}
}
/// Return the where clause for `MyType::VersionedOwned`.
///
/// This is simply the where clause of the input type.
pub(crate) fn versioned_owned_type_where_clause(
&self,
input_generics: &Generics,
) -> Option<WhereClause> {
match self {
Self::Classic(_) => input_generics.split_for_impl().2.cloned(),
Self::Convert(convert_attr) => extract_generics(&convert_attr.conversion_target)
.split_for_impl()
.2
.cloned(),
Self::Transparent(_) => input_generics.split_for_impl().2.cloned(),
}
}
/// Return the where clause needed to implement the Versionize trait.
///
/// This is the same as the one for the VersionizeOwned, with an additional "Clone" bound in the
/// case where we need to perform a conversion before the versioning.
pub(crate) fn versionize_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
// The base bounds for the owned traits are also used for the ref traits
let mut generics = input_generics.clone();
match self {
VersionizeImplementor::Classic(_) => {
self.versionize_owned_trait_where_clause(&generics)
}
VersionizeImplementor::Convert(_) => {
// The versionize method takes a ref. We need to own the input type in the
// conversion case to apply `From<Input> for Target`. This adds a
// `Clone` bound to have a better error message if the input type is
// not Clone.
add_trait_where_clause(&mut generics, [&parse_quote! { Self }], &["Clone"])?;
self.versionize_owned_trait_where_clause(&generics)
}
VersionizeImplementor::Transparent(transparent) => {
add_trait_where_clause(
&mut generics,
[&transparent.inner_type],
&[VERSIONIZE_TRAIT_NAME],
)?;
Ok(generics.split_for_impl().2.cloned())
}
}
}
/// Return the where clause needed to implement the VersionizeOwned trait.
///
/// If the type is directly versioned, the bound states that the argument points to a valid
/// DispatchEnum for this type. This is done by adding a bound on this argument to
/// `VersionsDisaptch<Self>`.
///
/// If there is a conversion, the target of the conversion should implement `VersionizeOwned`
/// and `From<Self>`.
pub(crate) fn versionize_owned_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
let mut generics = input_generics.clone();
match self {
Self::Classic(attr) => {
let dispatch_generics = generics.clone();
let dispatch_ty_generics = dispatch_generics.split_for_impl().1;
let dispatch_enum_path = &attr.dispatch_enum;
add_trait_where_clause(
&mut generics,
[&parse_quote!(#dispatch_enum_path #dispatch_ty_generics)],
&[format!("{}<Self>", DISPATCH_TRAIT_NAME,)],
)?;
}
Self::Convert(convert_attr) => {
let convert_type_path = &convert_attr.conversion_target;
add_trait_where_clause(
&mut generics,
[&parse_quote!(#convert_type_path)],
&[
VERSIONIZE_OWNED_TRAIT_NAME,
&format!("{}<Self>", FROM_TRAIT_NAME),
],
)?;
}
Self::Transparent(transparent) => {
add_trait_where_clause(
&mut generics,
[&transparent.inner_type],
&[VERSIONIZE_OWNED_TRAIT_NAME],
)?;
}
}
Ok(generics.split_for_impl().2.cloned())
}
/// Return the where clause for the `Unversionize` trait.
///
/// If the versioning is direct, this is the same bound as the one used for `VersionizeOwned`.
///
/// If there is a conversion, the target of the conversion need to implement `Unversionize` and
/// `Into` or `TryInto<T, E>`, with `E: Error + Send + Sync + 'static`
pub(crate) fn unversionize_trait_where_clause(
&self,
input_generics: &Generics,
) -> syn::Result<Option<WhereClause>> {
match self {
Self::Classic(_) => self.versionize_owned_trait_where_clause(input_generics),
Self::Convert(convert_attr) => {
let mut generics = input_generics.clone();
let convert_type_path = &convert_attr.conversion_target;
let into_trait = match convert_attr.conversion_type {
ConversionType::Direct => format!("{}<Self>", INTO_TRAIT_NAME),
ConversionType::Try => {
// Doing a TryFrom requires that the error
// impl Error + Send + Sync + 'static
let try_into_trait: Path = parse_const_str(TRY_INTO_TRAIT_NAME);
add_trait_where_clause(
&mut generics,
[&parse_quote!(<#convert_type_path as #try_into_trait<Self>>::Error)],
&[ERROR_TRAIT_NAME, SYNC_TRAIT_NAME, SEND_TRAIT_NAME],
)?;
add_lifetime_where_clause(
&mut generics,
[&parse_quote!(<#convert_type_path as #try_into_trait<Self>>::Error)],
&[STATIC_LIFETIME_NAME],
)?;
format!("{}<Self>", TRY_INTO_TRAIT_NAME)
}
};
add_trait_where_clause(
&mut generics,
[&parse_quote!(#convert_type_path)],
&[
UNVERSIONIZE_TRAIT_NAME,
&format!("{}<Self>", FROM_TRAIT_NAME),
&into_trait,
],
)?;
Ok(generics.split_for_impl().2.cloned())
}
Self::Transparent(transparent) => {
let mut generics = input_generics.clone();
add_trait_where_clause(
&mut generics,
[&transparent.inner_type],
&[UNVERSIONIZE_TRAIT_NAME],
)?;
Ok(generics.split_for_impl().2.cloned())
}
}
}
/// Return the body of the versionize method.
pub(crate) fn versionize_method_body(&self) -> proc_macro2::TokenStream {
match self {
Self::Classic(_) => {
quote! {
self.into()
}
}
Self::Convert(convert_attr) => {
let versionize_owned_trait: TraitBound =
parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
let convert_type_path = with_turbofish(&convert_attr.conversion_target);
quote! {
#versionize_owned_trait::versionize_owned(#convert_type_path::from(self.to_owned()))
}
}
Self::Transparent(transparent) => match &transparent.kind {
TransparentStructKind::NewType => {
quote! {
self.0.versionize()
}
}
TransparentStructKind::SingleField(field_name) => {
quote! {
self.#field_name.versionize()
}
}
},
}
}
/// Return the body of the versionize_owned method.
pub(crate) fn versionize_owned_method_body(&self) -> proc_macro2::TokenStream {
let versionize_owned_trait: TraitBound = parse_const_str(VERSIONIZE_OWNED_TRAIT_NAME);
match self {
Self::Classic(_) => {
quote! {
self.into()
}
}
Self::Convert(convert_attr) => {
let convert_type_path = with_turbofish(&convert_attr.conversion_target);
quote! {
#versionize_owned_trait::versionize_owned(#convert_type_path::from(self))
}
}
Self::Transparent(transparent) => match &transparent.kind {
TransparentStructKind::NewType => {
quote! {
self.0.versionize_owned()
}
}
TransparentStructKind::SingleField(field_name) => {
quote! {
self.#field_name.versionize_owned()
}
}
},
}
}
/// Return the body of the unversionize method.
pub(crate) fn unversionize_method_body(&self, arg_name: &Ident) -> proc_macro2::TokenStream {
let error: Type = parse_const_str(UNVERSIONIZE_ERROR_NAME);
match self {
Self::Classic(_) => {
quote! { #arg_name.try_into() }
}
Self::Convert(convert_attr) => {
let target = with_turbofish(&convert_attr.conversion_target);
match convert_attr.conversion_type {
ConversionType::Direct => {
quote! { #target::unversionize(#arg_name).map(|value| value.into()) }
}
ConversionType::Try => {
let target_name = format!("{}", target.to_token_stream());
quote! { #target::unversionize(#arg_name).and_then(|value| TryInto::<Self>::try_into(value)
.map_err(|e| #error::conversion(#target_name, e)))
}
}
}
}
Self::Transparent(transparent) => {
let inner = match &transparent.inner_type {
Type::Path(path) => Type::Path(TypePath {
qself: path.qself.clone(),
path: with_turbofish(&path.path),
}),
inner => inner.clone(),
};
match &transparent.kind {
TransparentStructKind::NewType => {
quote! {
#inner::unversionize(#arg_name).map(Self)
}
}
TransparentStructKind::SingleField(field_name) => {
quote! {
Ok(Self { #field_name: #inner::unversionize(#arg_name)? })
}
}
}
}
}
}
}
/// Return the same type but with generics that use the turbofish syntax. Converts
/// `MyStruct<T>` into `MyStruct::<T>`
fn with_turbofish(path: &Path) -> Path {
let mut with_turbo = path.clone();
for segment in with_turbo.segments.iter_mut() {
if let PathArguments::AngleBracketed(generics) = &mut segment.arguments {
generics.colon2_token = Some(Token![::](generics.span()));
}
}
with_turbo
}
/// Extract the generics inside a type
fn extract_generics(path: &Path) -> Generics {
let mut generics = Generics::default();
if let Some(last_segment) = path.segments.last() {
if let PathArguments::AngleBracketed(args) = &last_segment.arguments {
for arg in &args.args {
if let GenericArgument::Type(Type::Path(type_path)) = arg {
if let Some(ident) = type_path.path.get_ident() {
let param = TypeParam::from(ident.clone());
generics.params.push(GenericParam::Type(param));
}
}
}
}
}
generics
}

View File

@@ -0,0 +1,86 @@
//! In this example, we use the `transparent` attribute so that the versioning of the outer struct
//! will simply defer to the inner type. This is useful for wrapper types that shouldn't be
//! represented in serialized data. This only works for "newtype" struct or structs with only one
//! field.
use std::convert::Infallible;
use tfhe_versionable::{Unversionize, Upgrade, Version, Versionize, VersionsDispatch};
// The Wrapper that should be skipped. Also work with a single field regular struct:
//
// struct MyStructWrapper<T> { inner: MyStruct<T> };
#[derive(Versionize)]
#[versionize(transparent)] // Also works with `#[repr(transparent)]`
struct MyStructWrapper<T>(MyStruct<T>);
// The inner struct that is versioned.
#[derive(Versionize)]
#[versionize(MyStructVersions)]
struct MyStruct<T> {
attr: T,
builtin: u32,
}
#[derive(Version)]
struct MyStructV0 {
builtin: u32,
}
impl<T: Default> Upgrade<MyStruct<T>> for MyStructV0 {
type Error = Infallible;
fn upgrade(self) -> Result<MyStruct<T>, Self::Error> {
Ok(MyStruct {
attr: T::default(),
builtin: self.builtin,
})
}
}
#[derive(VersionsDispatch)]
#[allow(unused)]
enum MyStructVersions<T> {
V0(MyStructV0),
V1(MyStruct<T>),
}
mod v0 {
use tfhe_versionable::{Versionize, VersionsDispatch};
// This struct cannot change as it is not itself versioned. If you ever make a change that
// should impact the serialized layout of the data, you need to update all the types that use
// it.
#[derive(Versionize)]
#[versionize(transparent)]
pub(super) struct MyStructWrapper(pub(super) MyStruct);
#[derive(Versionize)]
#[versionize(MyStructVersions)]
pub(super) struct MyStruct {
pub(super) builtin: u32,
}
#[derive(VersionsDispatch)]
#[allow(unused)]
pub(super) enum MyStructVersions {
V0(MyStruct),
}
}
fn main() {
let value = 1234;
let ms = v0::MyStructWrapper(v0::MyStruct { builtin: value });
let serialized = bincode::serialize(&ms.versionize()).unwrap();
let unserialized =
MyStructWrapper::<u64>::unversionize(bincode::deserialize(&serialized).unwrap()).unwrap();
assert_eq!(unserialized.0.builtin, value);
}
#[test]
fn test() {
main()
}