From 9d17672a7694fbe4fe37ae07467ff6b29e48aec6 Mon Sep 17 00:00:00 2001 From: soup Date: Sat, 4 May 2024 10:00:44 -0400 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 173 +++++++++++++ Cargo.toml | 29 +++ flake.lock | 97 ++++++++ flake.nix | 26 ++ proc-macros/easy-default/Cargo.toml | 15 ++ proc-macros/easy-default/core/Cargo.toml | 10 + proc-macros/easy-default/core/src/lib.rs | 213 ++++++++++++++++ proc-macros/easy-default/src/lib.rs | 9 + proc-macros/easy-from/Cargo.toml | 15 ++ proc-macros/easy-from/core/Cargo.toml | 10 + proc-macros/easy-from/core/src/lib.rs | 214 +++++++++++++++++ proc-macros/easy-from/src/lib.rs | 9 + proc-macros/easy-get/Cargo.toml | 15 ++ proc-macros/easy-get/core/Cargo.toml | 12 + proc-macros/easy-get/core/src/lib.rs | 280 ++++++++++++++++++++++ proc-macros/easy-get/src/lib.rs | 9 + proc-macros/impl-for-refs/Cargo.toml | 15 ++ proc-macros/impl-for-refs/core/Cargo.toml | 10 + proc-macros/impl-for-refs/core/src/lib.rs | 108 +++++++++ proc-macros/impl-for-refs/src/lib.rs | 9 + rustfmt.toml | 5 + src/lib.rs | 244 +++++++++++++++++++ 23 files changed, 1528 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 proc-macros/easy-default/Cargo.toml create mode 100644 proc-macros/easy-default/core/Cargo.toml create mode 100644 proc-macros/easy-default/core/src/lib.rs create mode 100644 proc-macros/easy-default/src/lib.rs create mode 100644 proc-macros/easy-from/Cargo.toml create mode 100644 proc-macros/easy-from/core/Cargo.toml create mode 100644 proc-macros/easy-from/core/src/lib.rs create mode 100644 proc-macros/easy-from/src/lib.rs create mode 100644 proc-macros/easy-get/Cargo.toml create mode 100644 proc-macros/easy-get/core/Cargo.toml create mode 100644 proc-macros/easy-get/core/src/lib.rs create mode 100644 proc-macros/easy-get/src/lib.rs create mode 100644 proc-macros/impl-for-refs/Cargo.toml create mode 100644 proc-macros/impl-for-refs/core/Cargo.toml create mode 100644 proc-macros/impl-for-refs/core/src/lib.rs create mode 100644 proc-macros/impl-for-refs/src/lib.rs create mode 100644 rustfmt.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9d8e828 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,173 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "e" +version = "1.0.0" +dependencies = [ + "e-easy-default", + "e-easy-from", + "e-easy-get", + "e-impl-for-refs", +] + +[[package]] +name = "e-easy-default" +version = "1.0.0" +dependencies = [ + "e-easy-default-core", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-easy-default-core" +version = "1.0.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-easy-from" +version = "1.0.0" +dependencies = [ + "e-easy-from-core", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-easy-from-core" +version = "1.0.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-easy-get" +version = "1.0.0" +dependencies = [ + "e-easy-get-core", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-easy-get-core" +version = "1.0.0" +dependencies = [ + "e-easy-default", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-impl-for-refs" +version = "1.0.0" +dependencies = [ + "e-impl-for-refs-core", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "e-impl-for-refs-core" +version = "1.0.0" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2bff39d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "e" +version = "1.0.0" +edition = "2021" + +[workspace] +members = [ + "proc-macros/easy-default/core", + "proc-macros/easy-default", + "proc-macros/easy-from/core", + "proc-macros/easy-from", + "proc-macros/easy-get/core", + "proc-macros/easy-get", + "proc-macros/impl-for-refs/core", + "proc-macros/impl-for-refs", +] +resolver = "2" + +[dependencies] +e-easy-default = { path = "proc-macros/easy-default" } +e-easy-from = { path = "proc-macros/easy-from" } +e-easy-get = { path = "proc-macros/easy-get" } +e-impl-for-refs = { path = "proc-macros/impl-for-refs" } + +[workspace.dependencies] +proc-macro-error = "1.0" +proc-macro2 = "1" +syn = { version = "2", features = ["full"] } +quote = "1" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8218615 --- /dev/null +++ b/flake.lock @@ -0,0 +1,97 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1710483719, + "narHash": "sha256-Ev/hJ59IAA3dWfTB3CWxMv/V/owO1yKyq0nwsek/d9o=", + "owner": "nix-community", + "repo": "fenix", + "rev": "d0439c495e5cd13ff252ade520ca620f52abb40b", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1707347730, + "narHash": "sha256-0etC/exQIaqC9vliKhc3eZE2Mm2wgLa0tj93ZF/egvM=", + "path": "/nix/store/l6ymqz1y4fiy57xh6f3y23zwp0i9lr4f-source", + "rev": "6832d0d99649db3d65a0e15fa51471537b2c56a6", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1710430493, + "narHash": "sha256-KfmUsf/d62ANcFhSTR3BDIpk2ww0AcxXdi9lpZJ5UtQ=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "14558af15ee3d471bf8f4212f7609ae1f9647bc5", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..71e051b --- /dev/null +++ b/flake.nix @@ -0,0 +1,26 @@ +{ + inputs.nixpkgs.url = "nixpkgs"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = { self, nixpkgs, flake-utils, fenix }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + fenix' = fenix.packages.${system}; + nightly = fenix'.default; + stable = fenix'.stable; + in { + devShells.default = pkgs.mkShell { + packages = [ (fenix'.combine [ + (stable.withComponents [ + "cargo" "rustc" "rust-src" "rust-analyzer" "clippy" + ]) + nightly.rustfmt + ]) pkgs.cargo-watch ]; + }; + }); +} diff --git a/proc-macros/easy-default/Cargo.toml b/proc-macros/easy-default/Cargo.toml new file mode 100644 index 0000000..6446261 --- /dev/null +++ b/proc-macros/easy-default/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "e-easy-default" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } + +e-easy-default-core = { path = "./core" } + +[lib] +proc-macro = true \ No newline at end of file diff --git a/proc-macros/easy-default/core/Cargo.toml b/proc-macros/easy-default/core/Cargo.toml new file mode 100644 index 0000000..46cb1ad --- /dev/null +++ b/proc-macros/easy-default/core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "e-easy-default-core" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } diff --git a/proc-macros/easy-default/core/src/lib.rs b/proc-macros/easy-default/core/src/lib.rs new file mode 100644 index 0000000..bc2e7fc --- /dev/null +++ b/proc-macros/easy-default/core/src/lib.rs @@ -0,0 +1,213 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Fields, ItemStruct, Meta}; + +pub fn easy_default_core(input: TokenStream) -> TokenStream { + let input: ItemStruct = match syn::parse2(input) { + Ok(i) => i, + Err(e) => return e.to_compile_error(), + }; + + let struct_name = input.ident; + let (ipl, ty, _) = input.generics.split_for_impl(); + + match input.fields { + Fields::Named(n) => { + let mut exprs = Vec::new(); + + for field in n.named { + let default_expr = field.attrs.iter().find_map(|a| { + let default_expr = match &a.meta { + Meta::List(list) => match list.path.get_ident() { + Some(i) if i == "default" => &list.tokens, + _ => return None, + }, + _ => return None, + }; + + Some(default_expr) + }); + + let ident = field.ident.expect("named field should have name"); + match default_expr { + Some(expr) => exprs.push(quote! { + #ident: #expr, + }), + None => exprs.push(quote! { + #ident: ::core::default::Default::default(), + }), + } + } + + let output = quote! { + impl #ipl ::core::default::Default for #struct_name #ty { + fn default() -> Self { + Self { + #(#exprs)* + } + } + } + }; + + output + }, + Fields::Unnamed(u) => { + let mut exprs = Vec::new(); + + for field in u.unnamed { + let default_expr = field.attrs.iter().find_map(|a| { + let default_expr = match &a.meta { + Meta::List(list) => match list.path.get_ident() { + Some(i) if i == "default" => &list.tokens, + _ => return None, + }, + _ => return None, + }; + + Some(default_expr) + }); + + match default_expr { + Some(expr) => exprs.push(quote! { + #expr + }), + None => exprs.push(quote! { + ::core::default::Default::default() + }), + } + } + + let output = quote! { + impl #ipl ::core::default::Default for #struct_name #ty { + fn default() -> Self { + Self(#(#exprs),*) + } + } + }; + + output + }, + Fields::Unit => quote! { + impl #ipl ::core::default::Default for #struct_name #ty { + fn default() -> Self { + Self + } + } + }, + } +} + +#[cfg(test)] +mod test { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::easy_default_core; + + #[test] + fn named_fields_struct() { + let before = quote! { + #[derive(EasyDefault)] + struct Test { + #[default("Test".to_string())] + foo: String, + bar: String, + } + }; + + let after = easy_default_core(before); + assert_eq!( + after.to_string(), + quote! { + impl ::core::default::Default for Test { + fn default() -> Self { + Self { + foo: "Test".to_string(), + bar: ::core::default::Default::default(), + } + } + } + } + .to_string() + ); + } + + #[test] + fn tuple_struct() { + let before = quote! { + #[derive(EasyDefault)] + struct Test(#[default(5)] u64); + }; + + let after = easy_default_core(before); + assert_eq!( + after.to_string(), + quote! { + impl ::core::default::Default for Test { + fn default() -> Self { + Self(5) + } + } + } + .to_string() + ); + } + + #[test] + fn unit_struct() { + let before = quote! { + #[derive(EasyDefault)] + struct Test; + }; + + let after = easy_default_core(before); + assert_eq!( + after.to_string(), + quote! { + impl ::core::default::Default for Test { + fn default() -> Self { + Self + } + } + } + .to_string(), + ); + } + + fn assert_output(input: TokenStream, expected: TokenStream) { + let after = easy_default_core(input); + assert_eq!(after.to_string(), expected.to_string(),) + } + + #[test] + fn other1() { + assert_output( + quote! { + pub struct ObjectMeta { + #[default(ObjectKind::Orphan)] + pub kind: ObjectKind, + #[default(OffsetDateTime::now_utc())] + pub created_at: OffsetDateTime, + #[default(OffsetDateTime::now_utc())] + pub updated_at: OffsetDateTime, + pub name: String, + pub description: String, + } + }, + quote! { + impl ::core::default::Default for ObjectMeta { + fn default() -> Self { + Self { + kind: ObjectKind::Orphan, + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + name: ::core::default::Default::default(), + description: ::core::default::Default::default(), + } + } + } + + }, + ) + } +} diff --git a/proc-macros/easy-default/src/lib.rs b/proc-macros/easy-default/src/lib.rs new file mode 100644 index 0000000..eb4f3f1 --- /dev/null +++ b/proc-macros/easy-default/src/lib.rs @@ -0,0 +1,9 @@ +use e_easy_default_core::easy_default_core; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; + +#[proc_macro_error] +#[proc_macro_derive(EasyDefault, attributes(default))] +pub fn easy_default(input: TokenStream) -> TokenStream { + easy_default_core(input.into()).into() +} diff --git a/proc-macros/easy-from/Cargo.toml b/proc-macros/easy-from/Cargo.toml new file mode 100644 index 0000000..af7a27b --- /dev/null +++ b/proc-macros/easy-from/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "e-easy-from" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } + +e-easy-from-core = { path = "./core" } + +[lib] +proc-macro = true \ No newline at end of file diff --git a/proc-macros/easy-from/core/Cargo.toml b/proc-macros/easy-from/core/Cargo.toml new file mode 100644 index 0000000..24ee3f5 --- /dev/null +++ b/proc-macros/easy-from/core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "e-easy-from-core" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } diff --git a/proc-macros/easy-from/core/src/lib.rs b/proc-macros/easy-from/core/src/lib.rs new file mode 100644 index 0000000..244a76c --- /dev/null +++ b/proc-macros/easy-from/core/src/lib.rs @@ -0,0 +1,214 @@ +use proc_macro2::TokenStream; +use quote::{quote, TokenStreamExt}; +use syn::{spanned::Spanned, Fields, Ident, ItemEnum, ItemStruct, Meta}; + +fn handle_enum(e: ItemEnum) -> TokenStream { + let name = &e.ident; + + let mut stream: TokenStream = e + .variants + .iter() + .filter_map(|variant| { + let variant_name = &variant.ident; + + let has_from_attr = variant.attrs.iter().any(|a| { + let path = match &a.meta { + Meta::Path(p) => p, + _ => return false, + }; + + path.get_ident().is_some_and(|i| *i == "from") + }); + + if !has_from_attr { + return None; + } + + match &variant.fields { + Fields::Named(_n) => todo!(), + Fields::Unnamed(u) => { + match u.unnamed.iter().collect::>().as_slice() { + &[] => todo!(), + &[field] => { + let ty = &field.ty; + Some(quote! { + impl ::core::convert::From<#ty> for #name { + fn from(value: #ty) -> Self { + Self::#variant_name(value) + } + } + }) + }, + _sl => todo!(), + } + }, + Fields::Unit => todo!(), + } + }) + .collect(); + + let has_convert_infallible = e.attrs.iter().any(|a| match &a.meta { + Meta::List(list) => { + if list.path.get_ident().is_some_and(|i| *i == "from") { + let ident: Ident = match syn::parse2(list.tokens.clone()) { + Ok(i) => i, + _ => return false, + }; + + ident == "infallible" + } else { + false + } + }, + _ => false, + }); + + if has_convert_infallible { + stream.append_all(quote! { + impl ::core::convert::From<::core::convert::Infallible> for #name { + fn from(value: ::core::convert::Infallible) -> Self { + unreachable!() + } + } + }); + } + + stream +} + +fn handle_struct(s: ItemStruct) -> TokenStream { + match &s.fields { + Fields::Unnamed(u) => { + if u.unnamed.len() != 1 { + proc_macro_error::abort!( + s.span(), + "Tuple struct must have single field" + ); + } + + let field = u.unnamed.first().expect("Should be len 1"); + let ty = &field.ty; + let name = &s.ident; + let generics = &s.generics; + + quote! { + impl #generics ::core::convert::From<#ty> for #name #generics { + fn from(value: #ty) -> Self { + Self(value) + } + } + } + }, + _ => todo!(), + } +} + +pub fn easy_from_core(input: TokenStream) -> TokenStream { + match syn::parse2(input.clone()) { + Ok(e) => handle_enum(e), + Err(_) => match syn::parse2(input) { + Ok(s) => handle_struct(s), + Err(e) => e.to_compile_error(), + }, + } +} + +#[cfg(test)] +mod test { + use quote::quote; + + use crate::easy_from_core; + + #[test] + fn test_enum_all_fields_covered() { + let input = quote! { + #[derive(EasyFrom)] + enum Foo { + #[from] + Bar(u64), + + #[from] + Baz(String), + } + }; + let output = easy_from_core(input).to_string(); + let expected = quote! { + impl ::core::convert::From for Foo { + fn from(value: u64) -> Self { + Self::Bar(value) + } + } + + impl ::core::convert::From for Foo { + fn from(value: String) -> Self { + Self::Baz(value) + } + } + } + .to_string(); + + assert_eq!(output, expected) + } + + #[test] + fn test_enum_some_fields_covered() { + let input = quote! { + #[derive(EasyFrom)] + enum Foo { + #[from] + Bar(u64), + Baz(String), + } + }; + let output = easy_from_core(input).to_string(); + let expected = quote! { + impl ::core::convert::From for Foo { + fn from(value: u64) -> Self { + Self::Bar(value) + } + } + } + .to_string(); + + assert_eq!(output, expected) + } + + #[test] + fn test_enum_from_infallible() { + let input = quote! { + #[derive(EasyFrom)] + #[from(infallible)] + enum Never {} + }; + let output = easy_from_core(input).to_string(); + let expected = quote! { + impl ::core::convert::From<::core::convert::Infallible> for Never { + fn from(value: ::core::convert::Infallible) -> Self { + unreachable!() + } + } + } + .to_string(); + + assert_eq!(output, expected) + } + + #[test] + fn test_newtype_struct() { + let input = quote! { + #[derive(EasyFrom)] + struct NewType(#[from] T); + }; + let output = easy_from_core(input).to_string(); + let expected = quote! { + impl ::core::convert::From for NewType { + fn from(value: T) -> Self { + Self(value) + } + } + } + .to_string(); + + assert_eq!(output, expected) + } +} diff --git a/proc-macros/easy-from/src/lib.rs b/proc-macros/easy-from/src/lib.rs new file mode 100644 index 0000000..9bb3439 --- /dev/null +++ b/proc-macros/easy-from/src/lib.rs @@ -0,0 +1,9 @@ +use e_easy_from_core::easy_from_core; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; + +#[proc_macro_error] +#[proc_macro_derive(EasyFrom, attributes(from))] +pub fn easy_from(input: TokenStream) -> TokenStream { + easy_from_core(input.into()).into() +} diff --git a/proc-macros/easy-get/Cargo.toml b/proc-macros/easy-get/Cargo.toml new file mode 100644 index 0000000..3f3f18b --- /dev/null +++ b/proc-macros/easy-get/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "e-easy-get" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } + +e-easy-get-core = { path = "./core" } + +[lib] +proc-macro = true \ No newline at end of file diff --git a/proc-macros/easy-get/core/Cargo.toml b/proc-macros/easy-get/core/Cargo.toml new file mode 100644 index 0000000..6919ab7 --- /dev/null +++ b/proc-macros/easy-get/core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "e-easy-get-core" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } + +e-easy-default = { path = "../../easy-default" } diff --git a/proc-macros/easy-get/core/src/lib.rs b/proc-macros/easy-get/core/src/lib.rs new file mode 100644 index 0000000..b0208e7 --- /dev/null +++ b/proc-macros/easy-get/core/src/lib.rs @@ -0,0 +1,280 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens, TokenStreamExt}; +use syn::{ + Attribute, Field, Fields, FieldsNamed, Generics, Ident, ItemStruct, LitStr, + Meta, +}; + +#[derive(Default)] +enum Visibility { + #[default] + Inherited, + Pub, +} + +impl ToTokens for Visibility { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Visibility::Pub => tokens.append_all(quote! { pub }), + Visibility::Inherited => (), + } + } +} + +#[derive(Default)] +struct GetOptions { + visibility: Visibility, + rename: Option, +} +impl GetOptions { + fn from_attr(attr: &Attribute) -> syn::Result { + let mut out = Self::default(); + attr.parse_nested_meta(|nm| { + if nm.path.is_ident("pub") { + out.visibility = Visibility::Pub; + } + + if nm.path.is_ident("rename") { + let name = nm.value()?; + let name: LitStr = name.parse()?; + + out.rename = Some(name.value()); + } + + Ok(()) + })?; + + Ok(out) + } +} + +fn named_fields( + name: Ident, + generics: Generics, + n: FieldsNamed, +) -> TokenStream { + let (ipl, ty, _) = generics.split_for_impl(); + + n.named + .into_iter() + .flat_map(|field| { + let field_name = + field.ident.as_ref().expect("named should have ident"); + let field_ty = &field.ty; + + fn options_for( + attr_name: &str, + field: &Field, + ) -> Option> { + field.attrs.iter().find_map(|a| match &a.meta { + Meta::List(list) => list + .path + .get_ident() + .filter(|i| *i == attr_name) + .map(|_| GetOptions::from_attr(a)), + Meta::Path(p) => p + .get_ident() + .filter(|i| *i == attr_name) + .map(|_| Ok(GetOptions::default())), + _ => None, + }) + } + + let get = options_for("get", &field); + + fn output_for( + options: Option>, + field_name: &Ident, + f: impl FnOnce(Ident, Visibility) -> TokenStream, + ) -> Option { + match options { + Some(Ok(get)) => { + let GetOptions { + visibility, rename, .. + } = get; + + let fn_name = if let Some(rename) = rename { + format_ident!("{}", rename) + } else { + field_name.clone() + }; + + Some(f(fn_name, visibility)) + }, + Some(Err(e)) => Some(e.into_compile_error()), + _ => None, + } + } + + let get_out = output_for(get, field_name, |fn_name, vis| { + quote! { + impl #ipl #name #ty { + #vis fn #fn_name(&self) -> #field_ty { + self.#field_name + } + } + } + }); + + let get_ref = options_for("get_ref", &field); + + let get_ref_out = + output_for(get_ref, field_name, |fn_name, vis| { + quote! { + impl #ipl #name #ty { + #vis fn #fn_name(&self) -> &#field_ty { + &self.#field_name + } + } + } + }); + + let get_mut = options_for("get_mut", &field); + let get_mut_renamed = get_mut + .as_ref() + .is_some_and(|r| r.as_ref().is_ok_and(|o| o.rename.is_some())); + let get_mut_out = + output_for(get_mut, field_name, |fn_name, vis| { + let fn_name = if get_mut_renamed { + fn_name + } else { + format_ident!("{}_mut", fn_name) + }; + + quote! { + impl #ipl #name #ty { + #vis fn #fn_name(&mut self) -> &mut #field_ty { + &mut self.#field_name + } + } + } + }); + + get_out.into_iter().chain(get_ref_out).chain(get_mut_out) + }) + .collect() +} + +pub fn e_easy_get_core(input: TokenStream) -> TokenStream { + let item: ItemStruct = match syn::parse2(input) { + Ok(i) => i, + Err(e) => return e.into_compile_error(), + }; + + match item.fields { + Fields::Named(n) => named_fields(item.ident, item.generics, n), + _ => todo!("aaaaaa"), + } +} + +#[cfg(test)] +mod test { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::e_easy_get_core; + + fn expect_output(input: TokenStream, output: TokenStream) { + assert_eq!(e_easy_get_core(input).to_string(), output.to_string()) + } + + #[test] + fn test_get_pub() { + expect_output( + quote! { + #[derive(EasyGet)] + pub struct Foo { + #[get(pub)] + bar: usize, + } + }, + quote! { + impl Foo { + pub fn bar(&self) -> usize { + self.bar + } + } + }, + ); + } + + #[test] + fn test_get_generic() { + expect_output( + quote! { + #[derive(EasyGet)] + pub struct Foo { + #[get(pub)] + bar: T, + } + }, + quote! { + impl Foo { + pub fn bar(&self) -> T { + self.bar + } + } + }, + ); + } + + #[test] + fn test_get_ref_generic_default() { + expect_output( + quote! { + #[derive(EasyGet)] + pub struct Foo { + #[get_ref(pub)] + bar: T, + } + }, + quote! { + impl Foo { + pub fn bar(&self) -> &T { + &self.bar + } + } + }, + ); + } + + #[test] + fn test_rename() { + expect_output( + quote! { + #[derive(EasyGet)] + pub struct Foo { + #[get_ref(pub, rename = "baz")] + bar: T, + } + }, + quote! { + impl Foo { + pub fn baz(&self) -> &T { + &self.bar + } + } + }, + ) + } + + #[test] + fn test_get_mut() { + expect_output( + quote! { + #[derive(EasyGet)] + pub struct Foo { + #[get_mut(pub)] + bar: T, + } + }, + quote! { + impl Foo { + pub fn bar_mut(&mut self) -> &mut T { + &mut self.bar + } + } + }, + ) + } +} diff --git a/proc-macros/easy-get/src/lib.rs b/proc-macros/easy-get/src/lib.rs new file mode 100644 index 0000000..2e49ff1 --- /dev/null +++ b/proc-macros/easy-get/src/lib.rs @@ -0,0 +1,9 @@ +use e_easy_get_core::e_easy_get_core; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; + +#[proc_macro_error] +#[proc_macro_derive(EasyGet, attributes(get, get_ref, get_mut))] +pub fn easy_get(input: TokenStream) -> TokenStream { + e_easy_get_core(input.into()).into() +} diff --git a/proc-macros/impl-for-refs/Cargo.toml b/proc-macros/impl-for-refs/Cargo.toml new file mode 100644 index 0000000..95be47b --- /dev/null +++ b/proc-macros/impl-for-refs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "e-impl-for-refs" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } + +e-impl-for-refs-core = { path = "./core" } + +[lib] +proc-macro = true \ No newline at end of file diff --git a/proc-macros/impl-for-refs/core/Cargo.toml b/proc-macros/impl-for-refs/core/Cargo.toml new file mode 100644 index 0000000..e981e26 --- /dev/null +++ b/proc-macros/impl-for-refs/core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "e-impl-for-refs-core" +version = "1.0.0" +edition = "2021" + +[dependencies] +proc-macro-error = { workspace = true } +proc-macro2 = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } diff --git a/proc-macros/impl-for-refs/core/src/lib.rs b/proc-macros/impl-for-refs/core/src/lib.rs new file mode 100644 index 0000000..093b28f --- /dev/null +++ b/proc-macros/impl-for-refs/core/src/lib.rs @@ -0,0 +1,108 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::{FnArg, ItemTrait, Pat}; + +pub fn impl_for_refs_core( + _attrs: TokenStream, + item: TokenStream, +) -> TokenStream { + let trt: ItemTrait = match syn::parse2(item.clone()) { + Ok(e) => e, + Err(e) => return e.into_compile_error(), + }; + + let trt_name = &trt.ident; + + let fns = trt + .items + .iter() + .filter_map(|i| match i { + syn::TraitItem::Fn(f) => { + if f.default.is_some() { + return None; + } + + let sig = &f.sig; + let fn_name = &f.sig.ident; + let args = sig.inputs.iter().map(|i| match i { + FnArg::Receiver(_s) => { + quote!((&**self)) + }, + FnArg::Typed(t) => match t.pat.as_ref() { + Pat::Ident(e) => quote!(#e), + _ => todo!(), + }, + }); + + Some(quote! { + #sig { + #trt_name::#fn_name(#(#args),*) + } + }) + }, + _ => None, + }) + .collect::(); + + quote! { + #item + + impl<'a, T> #trt_name for &'a T where T: ?Sized + #trt_name { + #fns + } + + impl<'a, T> #trt_name for &'a mut T where T: ?Sized + #trt_name { + #fns + } + } +} + +#[cfg(test)] +mod test { + use proc_macro2::TokenStream; + use quote::quote; + + use crate::impl_for_refs_core; + + fn expect_output( + attrs: TokenStream, + input: TokenStream, + output: TokenStream, + ) { + assert_eq!( + impl_for_refs_core(attrs, input).to_string(), + output.to_string() + ) + } + + #[test] + fn test_works() { + expect_output( + quote!(), + quote! { + #[impl_for_refs] + trait Test { + fn test(&self) -> usize; + } + }, + quote! { + #[impl_for_refs] + trait Test { + fn test(&self) -> usize; + } + + impl<'a, T> Test for &'a T where T: ?Sized + Test { + fn test(&self) -> usize { + Test::test((&**self)) + } + } + + impl<'a, T> Test for &'a mut T where T: ?Sized + Test { + fn test(&self) -> usize { + Test::test((&**self)) + } + } + }, + ) + } +} diff --git a/proc-macros/impl-for-refs/src/lib.rs b/proc-macros/impl-for-refs/src/lib.rs new file mode 100644 index 0000000..f384d3a --- /dev/null +++ b/proc-macros/impl-for-refs/src/lib.rs @@ -0,0 +1,9 @@ +use e_impl_for_refs_core::impl_for_refs_core; +use proc_macro::TokenStream; +use proc_macro_error::proc_macro_error; + +#[proc_macro_error] +#[proc_macro_attribute] +pub fn impl_for_refs(attrs: TokenStream, item: TokenStream) -> TokenStream { + impl_for_refs_core(attrs.into(), item.into()).into() +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7000591 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +edition = "2021" +hard_tabs = true +match_block_trailing_comma = true +max_width = 80 +empty_item_single_line = false diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4a2be26 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,244 @@ +/* +#[cfg(derive)] +pub mod derive { + pub use e_easy_default::EasyDefault; + pub use e_easy_from::EasyFrom; + pub use e_easy_get::EasyGet; +} + +#[cfg(meta)] +pub mod meta { + pub use e_impl_for_refs::impl_for_refs; +} + +pub mod args { + use std::error::Error; + + use e_easy_get::EasyGet; + + #[derive(Debug, EasyGet, Eq, PartialEq)] + pub struct MissingParamFor { + #[get_ref(pub)] + arg_name: T, + } + impl core::fmt::Display for MissingParamFor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } + } + impl Error for MissingParamFor { + } + + pub enum Arg { + /// An arg that starts with a single `-` character. If multiple + /// characters follow the `-`, they will be returned as multiple, + /// separate Shorts + Short(char), + /// An arg that starts with `--` + Long(S), + /// An arg that has no preceding dashes + Param(S), + } + + enum ArgParserState { + ParsingSingle(std::vec::IntoIter), + UseIterator, + } + + pub struct ArgParser { + iterator: I, + state: ArgParserState, + stored: Option, + } + + impl ArgParser + where + I: Iterator, + S: AsRef, + { + pub fn from_iterator(iterator: I) -> Self { + Self { + iterator, + state: ArgParserState::UseIterator, + stored: None, + } + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Option> { + loop { + break match &mut self.state { + ArgParserState::ParsingSingle(it) => { + if let Some(c) = it.next() { + Some(Arg::Short(c)) + } else { + self.state = ArgParserState::UseIterator; + continue; + } + }, + ArgParserState::UseIterator => { + self.stored = Some(self.iterator.next()?); + + let stored = self.stored.as_ref().unwrap(); + let next = stored.as_ref(); + if let Some(long) = next.strip_prefix("--") { + Some(Arg::Long(long)) + } else if let Some(short) = next.strip_prefix('-') { + let mut chars = short.chars(); + let out = chars.next()?; + let chars = chars.collect::>().into_iter(); + self.state = ArgParserState::ParsingSingle(chars); + + Some(Arg::Short(out)) + } else { + Some(Arg::Param(next)) + } + }, + }; + } + } + + pub fn next_param_for( + &mut self, + arg_name: T, + ) -> Result<&str, MissingParamFor> { + match self.next() { + Some(Arg::Param(p)) => Ok(p), + _ => Err(MissingParamFor { arg_name }), + } + } + } + + impl ArgParser { + pub fn from_env() -> Self { + let it = std::env::args(); + + Self::from_iterator(it) + } + } + + #[cfg(test)] + mod test { + use std::num::ParseIntError; + + use e_easy_default::EasyDefault; + use e_easy_from::EasyFrom; + + use crate::args::{Arg, ArgParser, MissingParamFor}; + + #[test] + fn test_arg_parser() { + #[derive(EasyDefault, Debug, PartialEq, Eq)] + struct Args { + #[default("Hello world".to_string())] + string: String, + #[default(false)] + bool: bool, + #[default(8)] + u64: u64, + } + + #[derive(EasyFrom, Debug, PartialEq, Eq)] + #[from(infallible)] + enum Error { + #[from] + MissingParamFor(MissingParamFor<&'static str>), + #[from] + IntParseError(ParseIntError), + } + + fn parse_args(strs: &[&str]) -> Result { + let mut parser = ArgParser::from_iterator(strs.iter()); + let mut args = Args::default(); + + while let Some(arg) = parser.next() { + match arg { + Arg::Long("string") => { + args.string = + parser.next_param_for("string")?.to_string() + }, + Arg::Long("bool") => { + args.bool = true; + }, + Arg::Long("u64") => { + args.u64 = parser.next_param_for("u64")?.parse()? + }, + _ => {}, + } + } + + Ok(args) + } + + assert_eq!( + parse_args(&["--string"]), + Err(Error::MissingParamFor(MissingParamFor { + arg_name: "string" + })) + ) + } + } +} + +pub mod str { + use std::fmt::Display; + + #[derive( + Default, Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd, Debug, + )] + pub struct NonEmptyStr<'a>(&'a str); + impl<'a> AsRef for NonEmptyStr<'a> { + fn as_ref(&self) -> &str { + self.0 + } + } + impl<'a> Display for NonEmptyStr<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } + } + + pub trait StrExt { + fn non_empty(&self) -> Option; + } + + impl StrExt for T + where + T: AsRef, + { + fn non_empty(&self) -> Option { + let s = self.as_ref(); + + if s.is_empty() { + None + } else { + Some(NonEmptyStr(s)) + } + } + } +} +*/ + +use core::hash::{Hash, Hasher}; + +pub fn default() -> T { + T::default() +} + +pub fn hash(value: U) -> u64 { + let mut hasher: T = default(); + value.hash(&mut hasher); + hasher.finish() +} + +pub struct DisplayFn core::fmt::Result>( + pub F, +); +impl core::fmt::Display for DisplayFn +where + F: Fn(&mut core::fmt::Formatter<'_>) -> core::fmt::Result, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0(f) + } +}