add Optics proc macro
parent
0ec197ff1e
commit
2bf792d0f5
|
@ -3,6 +3,8 @@ name = "bad-optics"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
[workspace]
|
||||||
|
members = ["bad-optics-derive"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
bad-optics-derive = { path = "./bad-optics-derive" }
|
||||||
|
|
|
@ -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"
|
|
@ -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()
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! field_lens {
|
macro_rules! field_lens {
|
||||||
($type:ident, $field:ident) => {
|
($type:ident, $field:ident) => {
|
||||||
lens(
|
$crate::lenses::lens(
|
||||||
|v: $type| v.$field,
|
|v: $type| v.$field,
|
||||||
|mut u: $type, v| {
|
|mut u: $type, v| {
|
||||||
u.$field = v;
|
u.$field = v;
|
||||||
|
|
|
@ -7,6 +7,8 @@ pub mod prisms;
|
||||||
pub mod traversals;
|
pub mod traversals;
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
|
pub use bad_optics_derive::Optics;
|
||||||
|
|
||||||
pub use crate::combinations::*;
|
pub use crate::combinations::*;
|
||||||
|
|
||||||
pub use crate::lenses::*;
|
pub use crate::lenses::*;
|
||||||
|
|
Loading…
Reference in New Issue