diff --git a/Cargo.toml b/Cargo.toml index 17aa85a03..c2256eec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ anyhow = "1.0.63" async-entry = "0.3.1" byte-unit = "4.0.12" bytes = "1.0" +chrono = { version = "0.4" } clap = { version = "4.1.11", features = ["derive", "env"] } derive_more = { version="0.99.9" } futures = "0.3" @@ -29,6 +30,7 @@ pretty_assertions = "1.0.0" proc-macro2 = "1.0" quote = "1.0" rand = "0.8" +semver = "1.0.14" serde = { version="1.0.114", features=["derive", "rc"]} serde_json = "1.0.57" syn = "2.0" diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 179a73beb..ba2273899 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -16,9 +16,11 @@ repository = { workspace = true } proc-macro = true [dependencies] +chrono = { workspace = true } proc-macro2 = { workspace = true } quote = { workspace = true } -syn = { workspace = true, features = ["full"] } +semver = { workspace = true } +syn = { workspace = true, features = ["full", "extra-traits"] } [features] diff --git a/macros/src/lib.rs b/macros/src/lib.rs index a64bc54f7..1499d8bc7 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,7 +1,11 @@ #![doc = include_str!("lib_readme.md")] +mod since; +pub(crate) mod utils; + use proc_macro::TokenStream; use quote::quote; +use since::Since; use syn::parse2; use syn::parse_macro_input; use syn::parse_str; @@ -103,3 +107,53 @@ fn add_send_bounds(item: TokenStream) -> TokenStream { _ => panic!("add_async_trait can only be used with traits"), } } + +/// Add a `Since` line of doc, such as `/// Since: 1.0.0`. +/// +/// `#[since(version = "1.0.0")]` generates: +/// ```rust,ignore +/// /// Since: 1.0.0 +/// ``` +/// +/// `#[since(version = "1.0.0", date = "2021-01-01")]` generates: +/// ```rust,ignore +/// /// Since: 1.0.0, Date(2021-01-01) +/// ``` +/// +/// - The `version` must be a valid semver string. +/// - The `date` must be a valid date string in the format `yyyy-mm-dd`. +/// +/// ### Example +/// +/// ```rust,ignore +/// /// Foo function +/// /// +/// /// Does something. +/// #[since(version = "1.0.0")] +/// fn foo() {} +/// ``` +/// +/// The above code will be transformed into: +/// +/// ```rust,ignore +/// /// Foo function +/// /// +/// /// Does something. +/// /// +/// /// Since: 1.0.0 +/// fn foo() {} +/// ``` +#[proc_macro_attribute] +pub fn since(args: TokenStream, item: TokenStream) -> TokenStream { + let tokens = do_since(args, item.clone()); + match tokens { + Ok(x) => x, + Err(e) => utils::token_stream_with_error(item, e), + } +} + +fn do_since(args: TokenStream, item: TokenStream) -> Result { + let since = Since::new(args)?; + let tokens = since.append_since_doc(item)?; + Ok(tokens) +} diff --git a/macros/src/lib_readme.md b/macros/src/lib_readme.md index d16ae3387..0e560c210 100644 --- a/macros/src/lib_readme.md +++ b/macros/src/lib_readme.md @@ -1,8 +1,10 @@ Supporting utils for [Openraft](https://crates.io/crates/openraft). +# `add_async_trait` + `#[add_async_trait]` adds `Send` bounds to an async trait. -# Example +## Example ``` #[openraft_macros::add_async_trait] @@ -17,4 +19,30 @@ The above code will be transformed into: trait MyTrait { fn my_method(&self) -> impl Future> + Send; } -``` \ No newline at end of file +``` + + +# `since` + +`#[since(version = "1.0.0")]` adds a doc line `/// Since: 1.0.0`. + +## Example + +```rust,ignore +/// Foo function +/// +/// Does something. +#[since(version = "1.0.0")] +fn foo() {} +``` + +The above code will be transformed into: + +```rust,ignore +/// Foo function +/// +/// Does something. +/// +/// Since: 1.0.0 +fn foo() {} +``` diff --git a/macros/src/since.rs b/macros/src/since.rs new file mode 100644 index 000000000..0e45c6664 --- /dev/null +++ b/macros/src/since.rs @@ -0,0 +1,198 @@ +use std::str::FromStr; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::parse::Parser; +use syn::spanned::Spanned; + +use crate::utils; + +pub struct Since { + pub(crate) version: Option, + pub(crate) date: Option, +} + +impl Since { + /// Build a `Since` struct from the given attribute arguments. + pub(crate) fn new(args: TokenStream) -> Result { + let mut since = Since { + version: None, + date: None, + }; + + type AttributeArgs = syn::punctuated::Punctuated; + + let parsed_args = AttributeArgs::parse_terminated.parse(args.clone())?; + + for arg in parsed_args { + match arg { + syn::Meta::NameValue(namevalue) => { + let q = namevalue + .path + .get_ident() + .ok_or_else(|| syn::Error::new_spanned(&namevalue, "Must have specified ident"))?; + + let ident = q.to_string().to_lowercase(); + + match ident.as_str() { + "version" => { + since.set_version(namevalue.value.clone(), Spanned::span(&namevalue.value))?; + } + + "date" => { + since.set_date(namevalue.value.clone(), Spanned::span(&namevalue.value))?; + } + + name => { + let msg = format!( + "Unknown attribute {} is specified; expected one of: `version`, `date`", + name, + ); + return Err(syn::Error::new_spanned(namevalue, msg)); + } + } + } + other => { + return Err(syn::Error::new_spanned(other, "Unknown attribute inside the macro")); + } + } + } + + if since.version.is_none() { + return Err(syn::Error::new_spanned( + proc_macro2::TokenStream::from(args), + "Missing `version` attribute", + )); + } + + Ok(since) + } + /// Append a `since` doc such as `Since: 1.0.0` to the bottom of the doc section. + pub(crate) fn append_since_doc(self, item: TokenStream) -> Result { + let item = proc_macro2::TokenStream::from(item); + + // Present docs to skip, in order to append `since` at bottom of doc section. + let mut present_docs = vec![]; + + // Tokens left after present docs. + let mut last_non_doc = vec![]; + + let mut it = item.clone().into_iter(); + loop { + let Some(curr) = it.next() else { + break; + }; + let Some(next) = it.next() else { + last_non_doc.push(curr); + break; + }; + + if utils::is_doc(&curr, &next) { + present_docs.push(curr); + present_docs.push(next); + } else { + last_non_doc.push(curr); + last_non_doc.push(next); + break; + } + } + + let since_docs_str = if present_docs.is_empty() { + format!(r#"#[doc = " {}"]"#, self.to_doc_string()) + } else { + // If there are already docs, insert a blank line. + format!(r#"#[doc = ""] #[doc = " {}"]"#, self.to_doc_string()) + }; + let since_docs = proc_macro2::TokenStream::from_str(&since_docs_str).unwrap(); + + let present_docs: proc_macro2::TokenStream = present_docs.into_iter().collect(); + let last_non_docs: proc_macro2::TokenStream = last_non_doc.into_iter().collect(); + + // Other non doc tokens. + let other: proc_macro2::TokenStream = it.collect(); + + let tokens = quote! { + #present_docs + #since_docs + #last_non_docs + #other + }; + Ok(tokens.into()) + } + + /// Build doc string: `Since: 1.0.0, Date(2021-01-01)` or `Since: 1.0.0` + fn to_doc_string(&self) -> String { + let mut s = String::new(); + s.push_str("Since: "); + + if let Some(version) = &self.version { + s.push_str(version.as_str()); + } + if let Some(date) = &self.date { + s.push_str(&format!(", Date({})", date)); + } + s + } + + pub(crate) fn set_version(&mut self, ver_lit: syn::Expr, span: Span) -> Result<(), syn::Error> { + if self.version.is_some() { + return Err(syn::Error::new(span, "`version` set multiple times.")); + } + + let ver = Self::parse_str(ver_lit, "version", span)?; + + semver::Version::parse(&ver) + .map_err(|_e| syn::Error::new(span, format!("`version`(`{}`) is not valid semver.", ver)))?; + + self.version = Some(ver); + + Ok(()) + } + + pub(crate) fn set_date(&mut self, date_lit: syn::Expr, span: Span) -> Result<(), syn::Error> { + if self.date.is_some() { + return Err(syn::Error::new(span, "`date` set multiple times.")); + } + + let date_str = Self::parse_str(date_lit, "date", span)?; + + chrono::NaiveDate::parse_from_str(&date_str, "%Y-%m-%d").map_err(|_e| { + syn::Error::new( + span, + format!( + "`date`(`{}`) is not valid date string. Expected format: yyyy-mm-dd", + date_str + ), + ) + })?; + + self.date = Some(date_str); + + Ok(()) + } + + /// Extract string from `foo` or `"foo"` + fn parse_str(expr: syn::Expr, field: &str, span: Span) -> Result { + let s = match expr { + syn::Expr::Lit(s) => match s.lit { + syn::Lit::Str(s) => s.value(), + syn::Lit::Verbatim(s) => s.to_string(), + _ => { + return Err(syn::Error::new( + span, + format!("Failed to parse value of `{}` as string.", field), + )) + } + }, + syn::Expr::Verbatim(s) => s.to_string(), + _ => { + return Err(syn::Error::new( + span, + format!("Failed to parse value of `{}` as string.", field), + )) + } + }; + Ok(s) + } +} diff --git a/macros/src/utils.rs b/macros/src/utils.rs new file mode 100644 index 000000000..64c0c4262 --- /dev/null +++ b/macros/src/utils.rs @@ -0,0 +1,49 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenTree; + +/// Check if the next two token is a doc attribute, such as `#[doc = "foo"]`. +/// +/// An doc attribute is composed of a `#` token and a `Group` token with a `Bracket` delimiter: +/// ```ignore +/// Punct { ch: '#', }, +/// Group { +/// delimiter: Bracket, +/// stream: TokenStream [ +/// Ident { ident: "doc", }, +/// Punct { ch: '=', }, +/// Literal { kind: Str, symbol: " Doc", }, +/// ], +/// }, +/// ``` +pub(crate) fn is_doc(curr: &TokenTree, next: &TokenTree) -> bool { + let TokenTree::Punct(p) = curr else { + return false; + }; + + if p.as_char() != '#' { + return false; + } + + let TokenTree::Group(g) = &next else { + return false; + }; + + if g.delimiter() != proc_macro2::Delimiter::Bracket { + return false; + } + let first = g.stream().into_iter().next(); + let Some(first) = first else { + return false; + }; + + let TokenTree::Ident(i) = first else { + return false; + }; + + i == "doc" +} + +pub(crate) fn token_stream_with_error(mut item: TokenStream, e: syn::Error) -> TokenStream { + item.extend(TokenStream::from(e.into_compile_error())); + item +} diff --git a/macros/tests/test_since.rs b/macros/tests/test_since.rs new file mode 100644 index 000000000..9de1a3c9c --- /dev/null +++ b/macros/tests/test_since.rs @@ -0,0 +1,30 @@ +use openraft_macros::since; + +/// Doc +/// +/// By default, `Send` bounds will be added to the trait and to the return bounds of any async +/// functions defined withing the trait. +/// +/// If the `singlethreaded` feature is enabled, the trait definition remains the same without any +/// added `Send` bounds. +/// +/// # Example +/// +/// - list +#[since(version = "1.0.0", date = "2021-01-01")] +#[allow(dead_code)] +const A: i32 = 0; + +#[since(version = "1.0.0")] +#[allow(dead_code)] +fn foo() {} + +/* + +#[since(version = "1.0.0..0")] +fn bad_semver() {} + +#[since(version = "1.0.0", date = "2021-01--")] +fn bad_date() {} + + */ diff --git a/openraft/src/raft/message/client_write.rs b/openraft/src/raft/message/client_write.rs index 00e0a34e4..fe606ce80 100644 --- a/openraft/src/raft/message/client_write.rs +++ b/openraft/src/raft/message/client_write.rs @@ -1,6 +1,8 @@ use std::fmt; use std::fmt::Debug; +use openraft_macros::since; + use crate::display_ext::DisplayOptionExt; use crate::error::ClientWriteError; use crate::LogId; @@ -32,6 +34,7 @@ where C: RaftTypeConfig { /// Create a new instance of `ClientWriteResponse`. #[allow(dead_code)] + #[since(version = "0.10.0")] pub(crate) fn new_app_response(log_id: LogId, data: C::R) -> Self { Self { log_id, @@ -40,15 +43,18 @@ where C: RaftTypeConfig } } + #[since(version = "0.10.0")] pub fn log_id(&self) -> &LogId { &self.log_id } + #[since(version = "0.10.0")] pub fn response(&self) -> &C::R { &self.data } /// Return membership config if the log entry is a change-membership entry. + #[since(version = "0.10.0")] pub fn membership(&self) -> &Option> { &self.membership }