add Optics proc macro

This commit is contained in:
annieversary 2021-11-15 13:11:46 +00:00
parent 0ec197ff1e
commit 2bf792d0f5
6 changed files with 181 additions and 2 deletions

View file

@ -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" }

View file

@ -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"

View file

@ -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::lenses::lens::FuncLens<#name, #ty>>
{
bad_optics::field_lens!(#name, #fname)
}
}
})
.collect::<TokenStream>();
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::<TokenStream>();
quote! {
impl Lenses<#ty> {
pub fn get() ->
Vec<bad_optics::lenses::Lens<bad_optics::lenses::lens::FuncLens<#name, #ty>>>
{
vec![
#lenses
]
}
}
}
})
.collect::<TokenStream>();
quote! {
pub mod #mod_name {
use super::*;
#lens_funcs
pub struct Lenses<T>(std::marker::PhantomData<T>);
#group_impls
}
}
}
fn group_by_type<'a>(fields: impl Iterator<Item = &'a Field>) -> Vec<(Type, Vec<Field>)> {
let mut map = HashMap::<Type, Vec<Field>>::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()
}

46
examples/derive.rs Normal file
View file

@ -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::<String>::get();
assert_eq!(string_lenses.len(), 2);
// since _field4 is private, there's no lens for it
let vec_string_lenses = mystruct::Lenses::<u8>::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);
}

View file

@ -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;

View file

@ -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::*;