Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ jobs:
cd no-std-examples
nix develop .#no-std -c cargo run --features=box --bin no-std-box
nix develop .#no-std -c cargo run --features=option --bin no-std-option

- name: Test with catalyst
nix develop .#ci -c check-catalyst
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Cargo.lock
.direnv/
struct-patch/examples
no-std-examples/target
complex-example/target
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ members = [
"lib",
"derive",
]
exclude = [ "no-std-examples" ]
exclude = [
"no-std-examples",
"complex-example",
]

[workspace.package]
authors = ["Antonio Yang <yanganto@gmail.com>"]
Expand Down
20 changes: 20 additions & 0 deletions complex-example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[workspace]
resolver = "2"
members = [
"substrate",
"catalyst",
]
[workspace.dependencies]
struct-patch = { path = "../lib", features = ["catalyst"] }

[workspace.package]
authors = ["Antonio Yang <yanganto@gmail.com>"]
version = "0.11.0"
edition = "2021"
categories = ["development-tools"]
keywords = ["struct", "patch", "macro", "derive", "overlay"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/yanganto/struct-patch/"
description = "A library that helps you implement partial updates for your structs."
rust-version = "1.61.0"
16 changes: 16 additions & 0 deletions complex-example/catalyst/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "catalyst"
authors.workspace = true
version.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
description.workspace = true
rust-version.workspace = true

[dependencies]
struct-patch.workspace = true
substrate = { path = "../substrate" }
22 changes: 22 additions & 0 deletions complex-example/catalyst/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use struct_patch::Catalyst;
use substrate::Base;

#[derive(Catalyst)]
// #[catalyst(bind = Base)]
// #[complex(name = Complex)]
// #[complex(attribute(derive(Default)))]
struct Amyloid {
extra_bool: bool,
extra_string: String,
extra_option: Option<usize>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn complex_works() {
let complex = AmyloidComplex { };
}
}
15 changes: 15 additions & 0 deletions complex-example/substrate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "substrate"
authors.workspace = true
version.workspace = true
edition.workspace = true
categories.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
description.workspace = true
rust-version.workspace = true

[dependencies]
struct-patch.workspace = true
19 changes: 19 additions & 0 deletions complex-example/substrate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use struct_patch::Substrate;

#[derive(Substrate)]
pub struct Base {
field_bool: bool,
field_string: String,
field_option: Option<usize>,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn expose_works() {
assert_eq!(Base::expose(),
r#""{\"named\":[{\"ident\":\"field_bool\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"bool\"}]}}},{\"ident\":\"field_string\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"String\"}]}}},{\"ident\":\"field_option\",\"colon_token\":true,\"ty\":{\"path\":{\"segments\":[{\"ident\":\"Option\",\"arguments\":{\"angle_bracketed\":{\"args\":[{\"type\":{\"path\":{\"segments\":[{\"ident\":\"usize\"}]}}}]}}}]}}}]}""#);
}
}
2 changes: 2 additions & 0 deletions derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["parsing"] }
syn-serde = { version = "0.3.2", features = ["json"], optional = true }

[features]
status = []
op = []
merge = []
nesting = []
catalyst = [ "syn-serde" ]

[dev-dependencies]
pretty_assertions_sorted = "1.2.3"
152 changes: 152 additions & 0 deletions derive/src/catalyst.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
extern crate proc_macro;
use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens};
use std::str::FromStr;
use syn::{
meta::ParseNestedMeta, parenthesized, spanned::Spanned, DeriveInput, Error, Lit, LitStr,
Result, Type,
};
use syn_serde::json;

pub(crate) struct Catalyst {
visibility: syn::Visibility,
struct_name: Ident,
complex_struct_name: Ident,
generics: syn::Generics,
attributes: Vec<TokenStream>,
fields: syn::Fields,
bind: String,
}

const CATALYST: &str = "patch";
const COMPLEX: &str = "complex";
const BIND: &str = "bind";
const NAME: &str = "name";
const ATTRIBUTE: &str = "attribute";

impl Catalyst {
/// let catalyst bind the substrate and generate the token stream for complex
pub fn to_token_stream(&self) -> Result<TokenStream> {
let Catalyst {
visibility,
struct_name,
complex_struct_name,
generics,
attributes,
fields: _fields,
bind: _bind,
} = self;

// let active_site = json::to_string(fields);

let mapped_attributes = attributes
.iter()
.map(|a| {
quote! {
#[#a]
}
})
.collect::<Vec<_>>();

Ok(quote! {
#(#mapped_attributes)*
#visibility struct #complex_struct_name #generics {
}
})
}
/// Parse the Catalyst struct
pub fn from_ast(
DeriveInput {
ident,
data,
generics,
attrs,
vis,
}: syn::DeriveInput,
) -> Result<Catalyst> {
let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = data {
fields
} else {
return Err(syn::Error::new(
ident.span(),
"Catalyst derive only use for struct",
));
};
let mut name = None;
let mut attributes = vec![];
let mut bind = String::new();

for attr in attrs {
// TODO
// Fix not cross
// O complex(name = ..); X catalyst(name = ..)
// O catalyst(bind = ..); X complex(bind = ..)
if attr.path().to_string().as_str() != CATALYST
|| attr.path().to_string().as_str() != COMPLEX
{
continue;
}

if let syn::Meta::List(meta) = &attr.meta {
if meta.tokens.is_empty() {
continue;
}
}

attr.parse_nested_meta(|meta| {
let path = meta.path.to_string();
match path.as_str() {
NAME => {
if let Some(lit) = crate::get_lit_str(path, &meta)? {
if name.is_some() {
return Err(meta
.error("The name attribute can't be defined more than once"));
}
name = Some(lit.parse()?);
}
}
ATTRIBUTE => {
let content;
parenthesized!(content in meta.input);
let attribute: TokenStream = content.parse()?;
attributes.push(attribute);
}
BIND => {
// TODO
}
_ => {
return Err(meta.error(format_args!(
"unknown catalyst container attribute `{}`",
path.replace(' ', "")
)));
}
}
Ok(())
})?;
}

Ok(Catalyst {
visibility: vis,
complex_struct_name: name.unwrap_or({
let ts = TokenStream::from_str(&format!("{}Complex", &ident,)).unwrap();
let lit = LitStr::new(&ts.to_string(), Span::call_site());
lit.parse()?
}),
struct_name: ident,
generics,
attributes,
fields,
bind,
})
}
}

trait ToStr {
fn to_string(&self) -> String;
}

impl ToStr for syn::Path {
fn to_string(&self) -> String {
self.to_token_stream().to_string()
}
}
58 changes: 58 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
extern crate proc_macro;
#[cfg(feature = "catalyst")]
mod catalyst;
mod filler;
mod patch;
#[cfg(feature = "catalyst")]
mod substrate;

#[cfg(feature = "catalyst")]
use catalyst::Catalyst;
use filler::Filler;
use patch::Patch;
#[cfg(feature = "catalyst")]
use substrate::Substrate;

use syn::meta::ParseNestedMeta;
use syn::spanned::Spanned;
Expand Down Expand Up @@ -34,6 +42,26 @@ pub fn derive_filler(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
.into()
}

#[cfg(feature = "catalyst")]
#[proc_macro_derive(Substrate, attributes(substrate))]
pub fn derive_substrate(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
Substrate::from_ast(syn::parse_macro_input!(item as syn::DeriveInput))
.unwrap()
.to_token_stream()
.unwrap()
.into()
}

#[cfg(feature = "catalyst")]
#[proc_macro_derive(Catalyst, attributes(catalyst))]
pub fn derive_catalyst(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
Catalyst::from_ast(syn::parse_macro_input!(item as syn::DeriveInput))
.unwrap()
.to_token_stream()
.unwrap()
.into()
}

fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result<Option<syn::Lit>> {
let expr: syn::Expr = meta.value()?.parse()?;
let mut value = &expr;
Expand All @@ -52,3 +80,33 @@ fn get_lit(attr_name: String, meta: &ParseNestedMeta) -> syn::Result<Option<syn:
))
}
}

fn get_lit_str(attr_name: String, meta: &ParseNestedMeta) -> syn::Result<Option<syn::LitStr>> {
let expr: syn::Expr = meta.value()?.parse()?;
let mut value = &expr;
while let syn::Expr::Group(e) = value {
value = &e.expr;
}
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = value
{
let suffix = lit.suffix();
if !suffix.is_empty() {
return Err(Error::new(
lit.span(),
format!("unexpected suffix `{}` on string literal", suffix),
));
}
Ok(Some(lit.clone()))
} else {
Err(Error::new(
expr.span(),
format!(
"expected serde {} attribute to be a string: `{} = \"...\"`",
attr_name, attr_name
),
))
}
}
Loading
Loading