diff --git a/Cargo.toml b/Cargo.toml index e5f1841..4b6e9e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,8 @@ name = "bad-optics" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[workspace] +members = ["bad-optics-derive"] [dependencies] +bad-optics-derive = { path = "./bad-optics-derive" } diff --git a/bad-optics-derive/Cargo.toml b/bad-optics-derive/Cargo.toml new file mode 100644 index 0000000..e8c8797 --- /dev/null +++ b/bad-optics-derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "bad-optics-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = {version = "1.0", features = ["extra-traits"]} +quote = "1.0" +proc-macro2 = "1.0" diff --git a/bad-optics-derive/src/lib.rs b/bad-optics-derive/src/lib.rs new file mode 100644 index 0000000..2066d38 --- /dev/null +++ b/bad-optics-derive/src/lib.rs @@ -0,0 +1,117 @@ +use std::collections::HashMap; + +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{parse_macro_input, DataStruct, DeriveInput, Field, Ident, Lit, Meta, Type, Visibility}; + +// TODO add attributes to rename the lens/module/skip making lenses/idk + +#[proc_macro_derive(Optics, attributes(mod_name))] +pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + let name = &input.ident; + let mod_name = Ident::new(&get_mod_name(&input), Span::call_site()); + + let expanded = match input.data { + syn::Data::Struct(s) => expand_struct(s, name, &mod_name), + syn::Data::Enum(_) => todo!("not yet implemented for prisms"), + syn::Data::Union(_) => panic!("this macro does not work on unions"), + }; + + // Hand the output tokens back to the compiler + proc_macro::TokenStream::from(expanded) +} + +fn get_mod_name(input: &DeriveInput) -> String { + for i in &input.attrs { + if let Ok(Meta::NameValue(meta)) = i.parse_meta() { + if let Some(ident) = meta.path.get_ident() { + if ident == "mod_name" { + if let Lit::Str(a) = meta.lit { + return a.value(); + } + } + } + } + } + + input.ident.to_string().to_lowercase() +} + +fn expand_struct(data: DataStruct, name: &Ident, mod_name: &Ident) -> TokenStream { + let fields = match &data.fields { + syn::Fields::Named(n) => n.named.iter(), + syn::Fields::Unnamed(_) => todo!(), + syn::Fields::Unit => todo!(), + } + .filter(|f| matches!(f.vis, Visibility::Public(_))); + + let lens_funcs = fields + .clone() + .map(|field| { + let fname = field.ident.as_ref().unwrap(); + let ty = &field.ty; + quote! { + pub fn #fname() -> + bad_optics::lenses::Lens> + { + bad_optics::field_lens!(#name, #fname) + } + } + }) + .collect::(); + + let group_impls = group_by_type(fields) + .into_iter() + .map(|(ty, fields)| { + let lenses = fields + .into_iter() + .map(|field| { + let fname = field.ident.unwrap(); + quote! { + bad_optics::field_lens!(#name, #fname), + } + }) + .collect::(); + + quote! { + impl Lenses<#ty> { + pub fn get() -> + Vec>> + { + vec![ + #lenses + ] + } + } + } + }) + .collect::(); + + quote! { + pub mod #mod_name { + use super::*; + + #lens_funcs + + pub struct Lenses(std::marker::PhantomData); + #group_impls + } + } +} + +fn group_by_type<'a>(fields: impl Iterator) -> Vec<(Type, Vec)> { + let mut map = HashMap::>::new(); + + for field in fields { + if let Some(f) = map.get_mut(&field.ty) { + f.push(field.clone()); + } else { + map.insert(field.ty.clone(), vec![field.clone()]); + } + } + + map.into_iter().collect() +} diff --git a/examples/derive.rs b/examples/derive.rs new file mode 100644 index 0000000..b9df645 --- /dev/null +++ b/examples/derive.rs @@ -0,0 +1,46 @@ +use bad_optics::prelude::Optics; + +// the Optics derive macro will implement lenses for every public field in the struct +// it makes a module named whatever the struct is called, but in lower case +// you can rename the generated module by adding `#[mod_name = "other_name"]` to the struct + +#[derive(Optics, Clone, Debug)] +pub struct MyStruct { + pub field1: String, + pub field2: String, + + pub field3: u8, + _field4: u8, +} + +fn main() { + let o = MyStruct { + field1: "first field".to_string(), + field2: "second field".to_string(), + field3: 12, + _field4: 1, + }; + + // we can manually get lenses for each field + // note that it's a function that returns a lens + let field1 = mystruct::field1(); + let field2 = mystruct::field2(); + + // the lenses work normally as any other lens :) + assert_eq!(field1(o.clone()), "first field"); + assert_eq!(field2(o.clone()), "second field"); + + // we can get a vec with all the lenses that match a type + let string_lenses = mystruct::Lenses::::get(); + assert_eq!(string_lenses.len(), 2); + + // since _field4 is private, there's no lens for it + let vec_string_lenses = mystruct::Lenses::::get(); + assert_eq!(vec_string_lenses.len(), 1); + + let mut o = o; + for lens in string_lenses { + o = lens(o, |s| s.to_ascii_uppercase()); + } + dbg!(o); +} diff --git a/src/lenses/fields.rs b/src/lenses/fields.rs index d3b38f6..de6c16b 100644 --- a/src/lenses/fields.rs +++ b/src/lenses/fields.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! field_lens { ($type:ident, $field:ident) => { - lens( + $crate::lenses::lens( |v: $type| v.$field, |mut u: $type, v| { u.$field = v; diff --git a/src/lib.rs b/src/lib.rs index ba16e95..c320aa8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,8 @@ pub mod prisms; pub mod traversals; pub mod prelude { + pub use bad_optics_derive::Optics; + pub use crate::combinations::*; pub use crate::lenses::*;