diff --git a/Cargo.toml b/Cargo.toml index 8436323..9edab6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "cqrs", "cqrs-codegen", + "cqrs-codegen/impl", "cqrs-core", "cqrs-postgres", "cqrs-proptest", diff --git a/cqrs-codegen/.gitignore b/cqrs-codegen/.gitignore new file mode 100644 index 0000000..edc25b1 --- /dev/null +++ b/cqrs-codegen/.gitignore @@ -0,0 +1 @@ +/.watch-cqrs-codegen-impl diff --git a/cqrs-codegen/Cargo.toml b/cqrs-codegen/Cargo.toml index f8ce58c..8368c01 100644 --- a/cqrs-codegen/Cargo.toml +++ b/cqrs-codegen/Cargo.toml @@ -3,7 +3,7 @@ name = "cqrs-codegen" version = "0.1.0-dev" edition = "2018" # authors = -description = "Code generation for CQRS/ES" +description = "Code generation interface for CQRS/ES" # license = "Apache-2.0" readme = "../README.md" # documentation = "https://docs.rs/cqrs-core" @@ -12,11 +12,14 @@ readme = "../README.md" [lib] proc-macro = true +[features] +default = ["watt"] +no-watt = ["cqrs-codegen-impl", "syn"] + [dependencies] -proc-macro2 = "1.0.6" -quote = "1.0.2" -syn = "1.0.6" -synstructure = "0.12.1" +cqrs-codegen-impl = { version = "0.1.0-dev", path = "./impl", optional = true } +syn = { version = "1.0.7", optional = true } +watt = { version = "0.3.0", optional = true } [dev-dependencies] cqrs = { version = "0.3", path = "../cqrs" } diff --git a/cqrs-codegen/build.rs b/cqrs-codegen/build.rs new file mode 100644 index 0000000..79ba6df --- /dev/null +++ b/cqrs-codegen/build.rs @@ -0,0 +1,147 @@ +use std::{ + env, + error::Error, + fs::{self, OpenOptions}, + io::Write as _, + path::Path, + process, +}; + +fn main() -> Result<(), Box> { + let watt = env::var("CARGO_FEATURE_WATT"); + let no_watt = env::var("CARGO_FEATURE_NO_WATT"); + + if watt.is_ok() && no_watt.is_ok() { + panic!( + "Both 'watt' and 'no-watt' features specified; \ + exactly one of the two features have to be specified", + ); + } else if watt.is_err() && no_watt.is_err() { + panic!( + "Neither 'watt', nor 'no-watt' feature specified; \ + exactly one of the two features have to be specified", + ); + } + + if no_watt.is_ok() { + return Ok(()); + } + + let codegen_wasm_exists = Path::new("./src/codegen.wasm").is_file(); + let impl_exists = Path::new("./impl").is_dir(); + + if !codegen_wasm_exists && !impl_exists { + panic!( + "Neither './src/codegen.wasm' file, nor './impl/' directory exist, \ + so it's impossible to build cqrs-codegen with 'watt' feature", + ); + } + + let watch = Path::new("./.watch-cqrs-codegen-impl").exists(); + + if watch { + if impl_exists { + // Have to explicitly exclude 'codegen.wasm', cause otherwise each + // rebuild triggers 'rerun-if-changed' and forces rebuild on next + // run (which triggers 'rerun-if-changed'...). + rerun_if_changed_recursive_with_exceptions("./src", &["codegen.wasm"])?; + rerun_if_changed_recursive("./impl/src")?; + } else { + println!( + "cargo:warning='./.watch-cqrs-codegen-impl' file exists, \ + but './impl/' directory doesn't; \ + './src/codegen.wasm' won't be rebuilt", + ); + } + } + + if (!codegen_wasm_exists || watch) && impl_exists { + let root = env::current_dir()?; + + env::set_current_dir("./impl")?; + + fs::copy("./Cargo.toml", "./Cargo.toml.orig")?; + + writeln!( + OpenOptions::new().append(true).open("./Cargo.toml")?, + "[workspace]", + )?; + + let status = process::Command::new(env::var("CARGO")?) + .args(&[ + "build", + "--release", + "--target", + "wasm32-unknown-unknown", + "--features", + "watt", + "--target-dir", + "target", + ]) + .status()?; + + if !status.success() { + panic!( + "cargo-build for cqrs-codegen-impl returned \ + non-zero status code", + ); + } + + fs::copy("./Cargo.toml.orig", "./Cargo.toml")?; + + // Removing ./Cargo.toml.orig is not critical, + // so result is explicitly ignored. + drop(fs::remove_file("./Cargo.toml.orig")); + + env::set_current_dir(root)?; + + fs::copy( + "./impl/target/wasm32-unknown-unknown/release/cqrs_codegen_impl.wasm", + "./src/codegen.wasm", + )?; + } + + Ok(()) +} + +fn rerun_if_changed(path: &str) { + println!("cargo:rerun-if-changed={}", path); +} + +fn rerun_if_changed_recursive

(path: P) -> Result<(), Box> +where + P: AsRef, +{ + rerun_if_changed_recursive_with_exceptions(path, &[]) +} + +fn rerun_if_changed_recursive_with_exceptions

( + path: P, + exceptions: &[&str], +) -> Result<(), Box> +where + P: AsRef, +{ + for path in fs::read_dir(path)? { + let path = path?; + + if exceptions + .iter() + .any(|&exception| path.file_name() == exception) + { + continue; + } + + let path_type = path.file_type()?; + + let path = path.path(); + + if path_type.is_dir() { + rerun_if_changed_recursive(path)?; + } else if path_type.is_file() { + rerun_if_changed(path.to_str().ok_or("Failed to convert PathBuf to &str")?); + } + } + + Ok(()) +} diff --git a/cqrs-codegen/impl/.gitignore b/cqrs-codegen/impl/.gitignore new file mode 100644 index 0000000..042776a --- /dev/null +++ b/cqrs-codegen/impl/.gitignore @@ -0,0 +1,2 @@ +/Cargo.lock +/target/ diff --git a/cqrs-codegen/impl/Cargo.toml b/cqrs-codegen/impl/Cargo.toml new file mode 100644 index 0000000..d5e986e --- /dev/null +++ b/cqrs-codegen/impl/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "cqrs-codegen-impl" +version = "0.1.0-dev" +edition = "2018" +# authors = +description = "Code generation implementation for CQRS/ES" +# license = "Apache-2.0" +readme = "../README.md" +# documentation = "https://docs.rs/cqrs-core" +# repository = "/~https://github.com/cq-rs/cqrs" + +[lib] +crate-type = ["rlib", "cdylib"] + +[features] +default = [] +watt = [] + +[dependencies] +proc-macro2 = "1.0.6" +quote = "1.0.2" +syn = "1.0.6" +synstructure = { git = "/~https://github.com/ffuugoo/synstructure", default-features = false } + +[patch.crates-io] +proc-macro2 = { git = "/~https://github.com/dtolnay/watt" } diff --git a/cqrs-codegen/src/event/event.rs b/cqrs-codegen/impl/src/event/event.rs similarity index 100% rename from cqrs-codegen/src/event/event.rs rename to cqrs-codegen/impl/src/event/event.rs diff --git a/cqrs-codegen/src/event/mod.rs b/cqrs-codegen/impl/src/event/mod.rs similarity index 97% rename from cqrs-codegen/src/event/mod.rs rename to cqrs-codegen/impl/src/event/mod.rs index afde654..13fb158 100644 --- a/cqrs-codegen/src/event/mod.rs +++ b/cqrs-codegen/impl/src/event/mod.rs @@ -12,9 +12,9 @@ use synstructure::Structure; use crate::util::{self, TryInto as _}; -pub(crate) use event::derive; -pub(crate) use registered_event::derive as registered_derive; -pub(crate) use versioned_event::derive as versioned_derive; +pub use event::derive as event_derive; +pub use registered_event::derive as registered_event_derive; +pub use versioned_event::derive as versioned_event_derive; /// Name of the attribute, used for this family of derives. const ATTR_NAME: &str = "event"; diff --git a/cqrs-codegen/src/event/registered_event.rs b/cqrs-codegen/impl/src/event/registered_event.rs similarity index 97% rename from cqrs-codegen/src/event/registered_event.rs rename to cqrs-codegen/impl/src/event/registered_event.rs index e66b12f..264f47f 100644 --- a/cqrs-codegen/src/event/registered_event.rs +++ b/cqrs-codegen/impl/src/event/registered_event.rs @@ -30,6 +30,8 @@ fn derive_struct(input: syn::DeriveInput) -> Result { /// Implements [`crate::derive_registered_event`] macro expansion for enums /// via [`synstructure`]. fn derive_enum(input: syn::DeriveInput) -> Result { + util::assert_attr_does_not_exist(&input.attrs, super::ATTR_NAME)?; + let mut structure = Structure::try_new(&input)?; super::render_enum_proxy_method_calls( diff --git a/cqrs-codegen/src/event/versioned_event.rs b/cqrs-codegen/impl/src/event/versioned_event.rs similarity index 100% rename from cqrs-codegen/src/event/versioned_event.rs rename to cqrs-codegen/impl/src/event/versioned_event.rs diff --git a/cqrs-codegen/impl/src/lib.rs b/cqrs-codegen/impl/src/lib.rs new file mode 100644 index 0000000..ae9a433 --- /dev/null +++ b/cqrs-codegen/impl/src/lib.rs @@ -0,0 +1,38 @@ +mod event; +mod util; + +use proc_macro2::TokenStream; + +#[cfg(not(feature = "watt"))] +/// Re-exports proc macro `$fn` as is. +macro_rules! export { + ($fn:ident) => { + pub use event::$fn; + }; +} + +#[cfg(feature = "watt")] +/// Re-exports proc macro `$fn` via WASM ABI. +macro_rules! export { + ($fn:ident) => { + #[no_mangle] + pub extern "C" fn $fn(input: TokenStream) -> TokenStream { + expand(syn::parse2(input), event::$fn) + } + }; +} + +/// Performs expansion of a given proc macro implementation. +pub fn expand>( + input: syn::Result, + macro_impl: fn(syn::DeriveInput) -> syn::Result, +) -> TS { + match input.and_then(|input| macro_impl(input)) { + Ok(res) => res.into(), + Err(err) => err.to_compile_error().into(), + } +} + +export!(event_derive); +export!(registered_event_derive); +export!(versioned_event_derive); diff --git a/cqrs-codegen/src/util.rs b/cqrs-codegen/impl/src/util.rs similarity index 100% rename from cqrs-codegen/src/util.rs rename to cqrs-codegen/impl/src/util.rs diff --git a/cqrs-codegen/src/codegen.wasm b/cqrs-codegen/src/codegen.wasm new file mode 100755 index 0000000..648ea57 Binary files /dev/null and b/cqrs-codegen/src/codegen.wasm differ diff --git a/cqrs-codegen/src/lib.rs b/cqrs-codegen/src/lib.rs index 47cb107..66199b7 100644 --- a/cqrs-codegen/src/lib.rs +++ b/cqrs-codegen/src/lib.rs @@ -1,10 +1,31 @@ extern crate proc_macro; -mod event; -mod util; - use proc_macro::TokenStream; -use syn::parse::Result; + +#[cfg(all(not(feature = "watt"), feature = "no-watt"))] +/// Imports proc macro from implementation crate as is. +macro_rules! import { + ($input:expr, $fn:ident) => { + cqrs_codegen_impl::expand(syn::parse($input), cqrs_codegen_impl::$fn) + }; +} + +#[cfg(all(feature = "watt", not(feature = "no-watt")))] +/// Imports proc macro from implementation crate via WASM ABI. +macro_rules! import { + ($input:expr, $fn:ident) => { + wasm::MACRO.proc_macro(stringify!($fn), $input) + }; +} + +#[cfg(all(feature = "watt", not(feature = "no-watt")))] +mod wasm { + /// Generated WASM of implementation crate. + static WASM: &[u8] = include_bytes!("codegen.wasm"); + + /// Callable interface of the generated [`WASM`]. + pub static MACRO: watt::WasmMacro = watt::WasmMacro::new(WASM); +} /// Derives [`cqrs::Event`] implementation for structs and enums. /// @@ -48,8 +69,8 @@ use syn::parse::Result; /// } /// ``` #[proc_macro_derive(Event, attributes(event))] -pub fn derive_event(input: TokenStream) -> TokenStream { - expand(input, event::derive) +pub fn event_derive(input: TokenStream) -> TokenStream { + import!(input, event_derive) } /// Derives [`cqrs::RegisteredEvent`] implementation for structs and enums. @@ -92,8 +113,8 @@ pub fn derive_event(input: TokenStream) -> TokenStream { /// } /// ``` #[proc_macro_derive(RegisteredEvent)] -pub fn derive_registered_event(input: TokenStream) -> TokenStream { - expand(input, event::registered_derive) +pub fn registered_event_derive(input: TokenStream) -> TokenStream { + import!(input, registered_event_derive) } /// Derives [`cqrs::VersionedEvent`] implementation for structs and enums. @@ -138,16 +159,6 @@ pub fn derive_registered_event(input: TokenStream) -> TokenStream { /// } /// ``` #[proc_macro_derive(VersionedEvent, attributes(event))] -pub fn derive_versioned_event(input: TokenStream) -> TokenStream { - expand(input, event::versioned_derive) -} - -type MacroImpl = fn(syn::DeriveInput) -> Result; - -/// Expands given input [`TokenStream`] with a given macro implementation. -fn expand(input: TokenStream, macro_impl: MacroImpl) -> TokenStream { - match syn::parse(input).and_then(|i| macro_impl(i)) { - Ok(res) => res.into(), - Err(err) => err.to_compile_error().into(), - } +pub fn versioned_event_derive(input: TokenStream) -> TokenStream { + import!(input, versioned_event_derive) }