difftreelog
feat versioned structs helper
in: master
3 files changed
crates/struct-versioning/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/struct-versioning/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "struct-versioning"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+syn = { version = "1.0", features = ["full"] }
+quote = "1.0.15"
+
+[lib]
+proc-macro = true
crates/struct-versioning/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/struct-versioning/src/lib.rs
@@ -0,0 +1,325 @@
+use proc_macro::TokenStream;
+use quote::format_ident;
+use syn::{
+ parse::{Parse, ParseStream},
+ Token, LitInt, parse_macro_input, ItemStruct, Error, Fields, Result, Field,
+ punctuated::Punctuated, Expr, parenthesized,
+};
+use quote::quote;
+
+mod kw {
+ syn::custom_keyword!(version);
+ syn::custom_keyword!(first_version);
+ syn::custom_keyword!(versions);
+ syn::custom_keyword!(upper);
+}
+
+struct VersionedAttrs {
+ current_version: u32,
+ first_version: u32,
+ upper: bool,
+ versions: bool,
+}
+
+/// #[versioned(version = 2)]
+impl Parse for VersionedAttrs {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let mut current_version = None::<u32>;
+ let mut first_version = None::<u32>;
+ let mut upper = false;
+ let mut versions = false;
+
+ loop {
+ if input.is_empty() {
+ break;
+ }
+ let lookahead = input.lookahead1();
+ if lookahead.peek(kw::version) {
+ input.parse::<kw::version>()?;
+ input.parse::<Token![=]>()?;
+ let t = input.parse::<LitInt>()?;
+ if current_version.is_some() {
+ return Err(Error::new_spanned(t, "version is already set"));
+ }
+ current_version = Some(t.base10_parse()?)
+ } else if lookahead.peek(kw::first_version) {
+ input.parse::<kw::first_version>()?;
+ input.parse::<Token![=]>()?;
+ let t = input.parse::<LitInt>()?;
+ if first_version.is_some() {
+ return Err(Error::new_spanned(t, "first version is already set"));
+ }
+ first_version = Some(t.base10_parse()?)
+ } else if lookahead.peek(kw::upper) {
+ input.parse::<kw::upper>()?;
+ upper = true;
+ } else if lookahead.peek(kw::versions) {
+ input.parse::<kw::version>()?;
+ versions = true;
+ } else {
+ return Err(lookahead.error())
+ }
+
+ if input.is_empty() {
+ break
+ } else if input.peek(Token![,]) {
+ input.parse::<Token![,]>()?;
+ continue
+ } else {
+ return Err(input.error("unexpected token"))
+ }
+ }
+ let first_version = first_version.unwrap_or(1);
+ let current_version = current_version.unwrap_or(first_version);
+
+ if current_version == 0 || first_version == 0 || first_version > current_version {
+ return Err(Error::new(input.span(), "1 <= first_version <= version"))
+ }
+
+ Ok(Self {
+ current_version,
+ first_version,
+ upper,
+ versions,
+ })
+ }
+}
+
+/// #[version(..3)] - field vas removed in version 3 (i.e it was exist on version 2, but doesn't on version 3)
+/// #[version(3..)] - field has appeared in version 3
+/// #[version(2..4)] - field was on versions 2, 3
+/// #[version(1..2, upper(old_field + 1))] - when updating struct from old version to new - calculate new field value from passed expression
+struct VersionAttr {
+ since: u32,
+ before: Option<u32>,
+
+ upper: Option<Expr>,
+}
+impl VersionAttr {
+ fn exists_on(&self, version: u32) -> bool {
+ version >= self.since && self.before.map_or(true, |before| version < before)
+ }
+}
+impl Parse for VersionAttr {
+ fn parse(input: ParseStream) -> Result<Self> {
+ let mut since = None::<u32>;
+ let mut before = None::<u32>;
+ let lookahead = input.lookahead1();
+
+ if lookahead.peek(LitInt) {
+ let t: LitInt = input.parse()?;
+ since = Some(t.base10_parse()?);
+ } else if !lookahead.peek(Token![..]) {
+ return Err(lookahead.error());
+ }
+ let range = input.parse::<Token![..]>()?;
+ let lookahead = input.lookahead1();
+ if lookahead.peek(LitInt) {
+ let t: LitInt = input.parse()?;
+ before = Some(t.base10_parse()?);
+ } else if !input.is_empty() && !lookahead.peek(Token![,]) {
+ return Err(lookahead.error());
+ }
+
+ let upper = if input.peek(Token![,]) {
+ input.parse::<Token![,]>()?;
+ input.parse::<kw::upper>()?;
+ let expr;
+ parenthesized!(expr in input);
+
+ Some(Expr::parse(&expr)?)
+ } else {None};
+
+ if since.is_none() && before.is_none() {
+ return Err(Error::new_spanned(
+ range,
+ "noop range, remove this version attribute",
+ ));
+ }
+ Ok(Self {
+ since: since.unwrap_or(1),
+ before,
+ upper,
+ })
+ }
+}
+impl Default for VersionAttr {
+ fn default() -> Self {
+ Self {
+ since: 1,
+ before: None,
+ upper: None,
+ }
+ }
+}
+
+/// Generate versioned variants of a struct
+///
+/// `#[versioned(version = 1[, first_version = 1][, upper][, versions])]`
+/// - *version* - current version of a struct
+/// - *first_version* - allows to skip generation of structs, which predates first supported version
+/// - *upper* - generate From impls, which converts old version of structs to new
+/// - *versions* - generate enum, which contains all possible versions of struct
+///
+/// Each field may have version attribute
+/// `#[version([1]..[2][, upper(old)])]`
+/// - *1* - version, on which this field is appeared
+/// - *2* - version, in which this field was removed
+/// (i.e if set to 2, this field was exist on version 1, and no longer exist on version 2)
+/// - *upper* - code, which should be executed to transform old value to new/create new value
+#[proc_macro_attribute]
+pub fn versioned(attr: TokenStream, input: TokenStream) -> TokenStream {
+ let attr = parse_macro_input!(attr as VersionedAttrs);
+ let input = parse_macro_input!(input as ItemStruct);
+
+ let fields = match input.fields {
+ Fields::Named(named) => named.named,
+ _ => {
+ return Error::new_spanned(input, "expected named fields")
+ .into_compile_error()
+ .into()
+ }
+ };
+ let fields = fields
+ .iter()
+ .map(|field| {
+ let version_attr = match field.attrs.iter().find(|a| a.path.is_ident("version")) {
+ Some(v) => v.parse_args::<VersionAttr>()?,
+ None => return Ok((VersionAttr::default(), field.clone())),
+ };
+ let mut field = field.clone();
+ field.attrs.retain(|a| !a.path.is_ident("version"));
+ Ok((version_attr, field))
+ })
+ .collect::<Result<Vec<(VersionAttr, Field)>>>();
+ let fields = match fields {
+ Ok(f) => f,
+ Err(e) => return e.into_compile_error().into(),
+ };
+
+ let attrs = input.attrs;
+ let vis = input.vis;
+ let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
+ let mut out = Vec::new();
+ for version in attr.first_version..=attr.current_version {
+ let name = format_ident!("{}Version{}", &input.ident, version);
+ let current_fields = fields
+ .iter()
+ .filter_map(|(ver, field)| ver.exists_on(version).then(|| field));
+
+ let mut doc = Vec::new();
+ if version > attr.first_version {
+ doc.push(" # Versioning".into());
+ doc.push(format!(" Changes between {} and {}:", version - 1, version));
+ for (ver, field) in fields.iter() {
+ match (ver.exists_on(version - 1), ver.exists_on(version)) {
+ (true, false) => {
+ let ty = &field.ty;
+ doc.push(format!(" - {}: {} was removed", field.ident.as_ref().unwrap(), quote!{#ty}))
+ }
+ (false, true) => {
+ let ty = &field.ty;
+ doc.push(format!(" - [`{}`]: {} was added", field.ident.as_ref().unwrap(), quote!{#ty}))
+ }
+ _ => {},
+ }
+ }
+ }
+
+ let upper = if attr.upper && version > attr.first_version {
+ let prev_version = format_ident!("{}Version{}", &input.ident, version - 1);
+ let removed_fields = fields.iter()
+ .filter(|(v, _)| v.exists_on(version - 1) && !v.exists_on(version))
+ .map(|(_, f)| f.ident.as_ref().unwrap())
+ .collect::<Vec<_>>();
+ let added_fields = fields.iter()
+ .filter(|(v, _)| !v.exists_on(version - 1) && v.exists_on(version))
+ .map(|(v, f)| {
+ let name = f.ident.as_ref().unwrap();
+ let value = v.upper.clone().unwrap_or_else(|| Expr::Verbatim(Error::new_spanned(f, "missing upper declaration").to_compile_error()));
+ quote! { #name: #value }
+ });
+ let passed_fields = fields.iter()
+ .filter(|(v, _)| v.exists_on(version - 1) && v.exists_on(version))
+ .map(|(_, f)| f.ident.as_ref().unwrap())
+ .collect::<Vec<_>>();
+ // let added_fields = fields;
+ quote! {
+ impl #impl_generics From<#prev_version #ty_generics> for #name #ty_generics #where_clause {
+ fn from(old: #prev_version #ty_generics) -> Self {
+ let #prev_version {
+ #(#removed_fields,)*
+ #(#passed_fields,)*
+ } = old;
+ #(let _ = &#removed_fields;)*
+ Self {
+ #(#added_fields,)*
+ #(#passed_fields,)*
+ }
+ }
+ }
+ }
+ } else {quote!{}};
+
+ out.push(quote! {
+ #(#attrs)*
+ #(#[doc = #doc])*
+ #vis struct #name #impl_generics #where_clause {
+ #(#current_fields,)*
+ }
+
+ #upper
+ });
+ }
+
+ let ident = &input.ident;
+ let last_version = format_ident!("{}Version{}", input.ident, attr.current_version);
+
+ let versions = if attr.versions {
+ let name = format_ident!("{}Versions", input.ident);
+ let versions = (attr.first_version..=attr.current_version).map(|v| {
+ let var_name = format_ident!("V{}", v);
+ let var_type = format_ident!("{}Version{}", input.ident, v);
+ quote! {
+ #var_name(#var_type),
+ }
+ });
+ let versions = (attr.first_version..=attr.current_version).map(|this_version| {
+ let var_name = format_ident!("V{}", this_version);
+ let stages = (this_version+1..=attr.current_version).map(|v| {
+ let var_type = format_ident!("{}Version{}", input.ident, v);
+ quote! {
+ let v = #var_type::from(v);
+ }
+ });
+ quote! {
+ #var_name(v) => {
+ #(#stages)*
+ v
+ }
+ }
+ }).collect::<Vec<_>>();
+ quote! {
+ #vis enum #name {
+ #(#versions)*
+ }
+ impl struct_versioning::Versions for #name {
+ type Last = #last_version;
+ fn up(self) -> Self::Last {
+ match self {
+ #(#versions)*
+ }
+ }
+ }
+ }
+ } else {quote!{}};
+
+ quote! {
+ #(#out)*
+
+ #vis type #ident #ty_generics = #last_version #ty_generics;
+
+ #versions
+ }
+ .into()
+}
crates/struct-versioning/tests/upper.rsdiffbeforeafterbothno content