Skip to content

Commit

Permalink
Add new cgp-field crate for deriving HasField field accessors (#19)
Browse files Browse the repository at this point in the history
* Add boilerplate for new cgp-field crates

* Add HasField trait

* Add symbol! macro

* Implement derive_fields macro

* Add test for symbol

* Test derive HasField

* Re-export cgp-field in cgp-core

* Reformat imports

* Make derive_field a derive macro

* Auto derive HasField for contexts that implement Deref

* Fix clippy

* Fix test
  • Loading branch information
soareschen authored Jul 4, 2024
1 parent b76801f commit 778fb3f
Show file tree
Hide file tree
Showing 22 changed files with 375 additions and 13 deletions.
35 changes: 29 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ members = [
"crates/cgp-component",
"crates/cgp-component-macro",
"crates/cgp-component-macro-lib",
"crates/cgp-field",
"crates/cgp-field-macro",
"crates/cgp-field-macro-lib",
"crates/cgp-error",
"crates/cgp-error-eyre",
"crates/cgp-error-std",
Expand All @@ -32,6 +35,9 @@ cgp-sync = { path = "./crates/cgp-sync" }
cgp-component = { path = "./crates/cgp-component" }
cgp-component-macro = { path = "./crates/cgp-component-macro" }
cgp-component-macro-lib = { path = "./crates/cgp-component-macro-lib" }
cgp-field = { path = "./crates/cgp-field" }
cgp-field-macro = { path = "./crates/cgp-field-macro" }
cgp-field-macro-lib = { path = "./crates/cgp-field-macro-lib" }
cgp-error = { path = "./crates/cgp-error" }
cgp-run = { path = "./crates/cgp-run" }
cgp-inner = { path = "./crates/cgp-inner" }
7 changes: 1 addition & 6 deletions crates/cgp-component-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,4 @@ proc-macro = true

[dependencies]
cgp-component-macro-lib = { version = "0.1.0" }

syn = { version = "2.0.37", features = [ "full" ] }
quote = "1.0.33"
proc-macro2 = "1.0.67"
itertools = "0.11.0"
prettyplease = "0.2.20"
proc-macro2 = "1.0.67"
1 change: 1 addition & 0 deletions crates/cgp-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ all-features = true
cgp-async = { version = "0.1.0" }
cgp-component = { version = "0.1.0" }
cgp-error = { version = "0.1.0" }
cgp-field = { version = "0.1.0" }
cgp-run = { version = "0.1.0" }
cgp-inner = { version = "0.1.0" }
5 changes: 4 additions & 1 deletion crates/cgp-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
pub mod prelude;

pub use cgp_async::{async_trait, Async};
pub use {cgp_component as component, cgp_error as error, cgp_inner as inner, cgp_run as run};
pub use {
cgp_component as component, cgp_error as error, cgp_field as field, cgp_inner as inner,
cgp_run as run,
};
1 change: 1 addition & 0 deletions crates/cgp-core/src/prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub use cgp_component::{
define_components, delegate_components, derive_component, DelegateComponent, HasComponents,
};
pub use cgp_error::{CanRaiseError, HasErrorType};
pub use cgp_field::{symbol, Char, HasField};
23 changes: 23 additions & 0 deletions crates/cgp-field-macro-lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "cgp-field-macro-lib"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
rust-version = { workspace = true }
readme = "README.md"
keywords = ["context-generic programming"]
description = """
Context-generic programming core macros
"""

[package.metadata.docs.rs]
all-features = true

[dependencies]
syn = { version = "2.0.37", features = [ "full" ] }
quote = "1.0.33"
proc-macro2 = "1.0.67"
itertools = "0.11.0"
prettyplease = "0.2.20"
58 changes: 58 additions & 0 deletions crates/cgp-field-macro-lib/src/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{parse_quote, Fields, ItemImpl, ItemStruct};

use crate::symbol::symbol_from_string;

pub fn derive_has_field_impls(item_struct: &ItemStruct) -> Vec<ItemImpl> {
let struct_ident = &item_struct.ident;

let (impl_generics, ty_generics, where_clause) = item_struct.generics.split_for_impl();

let mut item_impls = Vec::new();

if let Fields::Named(fields) = &item_struct.fields {
for field in fields.named.iter() {
let field_ident = field.ident.as_ref().unwrap();

let field_symbol = symbol_from_string(&field_ident.to_string());

let field_type = &field.ty;

let item_impl: ItemImpl = parse_quote! {
impl #impl_generics HasField< #field_symbol >
for #struct_ident #ty_generics
#where_clause
{
type Field = #field_type;

fn get_field(
&self,
key: ::core::marker::PhantomData< #field_symbol >,
) -> &Self::Field
{
&self. #field_ident
}
}
};

item_impls.push(item_impl);
}
}

item_impls
}

pub fn derive_fields(input: TokenStream) -> TokenStream {
let item_struct: ItemStruct = syn::parse2(input).unwrap();

let item_impls = derive_has_field_impls(&item_struct);

let mut output = TokenStream::new();

for item_impl in item_impls {
output.extend(item_impl.to_token_stream());
}

output
}
8 changes: 8 additions & 0 deletions crates/cgp-field-macro-lib/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
pub mod field;
pub mod symbol;

#[cfg(test)]
mod tests;

pub use field::derive_fields;
pub use symbol::make_symbol;
23 changes: 23 additions & 0 deletions crates/cgp-field-macro-lib/src/symbol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{parse_quote, LitStr, Type};

pub fn symbol_from_string(value: &str) -> Type {
let char_types = <Punctuated<Type, Comma>>::from_iter(
value
.chars()
.map(|c: char| -> Type { parse_quote!( Char< #c > ) }),
);

parse_quote!( ( #char_types ) )
}

pub fn make_symbol(input: TokenStream) -> TokenStream {
let literal: LitStr = syn::parse2(input).unwrap();

let symbol = symbol_from_string(&literal.value());

symbol.to_token_stream()
}
87 changes: 87 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/field.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use quote::quote;

use crate::field::derive_fields;
use crate::tests::helper::equal::equal_token_stream;

#[test]
fn test_basic_derive_fields() {
let derived = derive_fields(quote! {
pub struct Foo {
pub bar: Bar,
pub baz: Baz,
}
});

let expected = quote! {
impl HasField<(Char<'b'>, Char<'a'>, Char<'r'>)> for Foo {
type Field = Bar;

fn get_field(
&self,
key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'r'>)>,
) -> &Self::Field {
&self.bar
}
}

impl HasField<(Char<'b'>, Char<'a'>, Char<'z'>)> for Foo {
type Field = Baz;

fn get_field(
&self,
key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'z'>)>,
) -> &Self::Field {
&self.baz
}
}
};

assert!(equal_token_stream(&derived, &expected));
}

#[test]
fn test_generic_derive_fields() {
let derived = derive_fields(quote! {
pub struct Foo<FooParamA, FooParamB: Clone>
where
FooParamA: Eq,
{
pub bar: Bar<FooParamA>,
pub baz: Baz<String>,
}
});

let expected = quote! {
impl<FooParamA, FooParamB: Clone> HasField<(Char<'b'>, Char<'a'>, Char<'r'>)>
for Foo<FooParamA, FooParamB>
where
FooParamA: Eq,
{
type Field = Bar<FooParamA>;

fn get_field(
&self,
key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'r'>)>,
) -> &Self::Field {
&self.bar
}
}

impl<FooParamA, FooParamB: Clone> HasField<(Char<'b'>, Char<'a'>, Char<'z'>)>
for Foo<FooParamA, FooParamB>
where
FooParamA: Eq,
{
type Field = Baz<String>;

fn get_field(
&self,
key: ::core::marker::PhantomData<(Char<'b'>, Char<'a'>, Char<'z'>)>,
) -> &Self::Field {
&self.baz
}
}
};

assert!(equal_token_stream(&derived, &expected));
}
7 changes: 7 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/helper/equal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use proc_macro2::TokenStream;

use crate::tests::helper::format::format_token_stream;

pub fn equal_token_stream(left: &TokenStream, right: &TokenStream) -> bool {
format_token_stream(left) == format_token_stream(right)
}
7 changes: 7 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/helper/format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use prettyplease::unparse;
use proc_macro2::TokenStream;
use syn::parse_file;

pub fn format_token_stream(stream: &TokenStream) -> String {
unparse(&parse_file(&stream.to_string()).unwrap())
}
2 changes: 2 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/helper/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod equal;
pub mod format;
3 changes: 3 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod field;
pub mod helper;
pub mod symbol;
31 changes: 31 additions & 0 deletions crates/cgp-field-macro-lib/src/tests/symbol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use quote::quote;

use crate::symbol::make_symbol;
use crate::tests::helper::equal::equal_token_stream;

#[test]
fn test_symbol_macro() {
let symbol = make_symbol(quote!("hello_world"));

let derived = quote! {
type Symbol = #symbol;
};

let expected = quote! {
type Symbol = (
Char<'h'>,
Char<'e'>,
Char<'l'>,
Char<'l'>,
Char<'o'>,
Char<'_'>,
Char<'w'>,
Char<'o'>,
Char<'r'>,
Char<'l'>,
Char<'d'>,
);
};

assert!(equal_token_stream(&derived, &expected));
}
Loading

0 comments on commit 778fb3f

Please sign in to comment.