8000 chore: replace parse boilerplate with a proc macro by jdrouet · Pull Request #11 · jolimail/mrml-core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content
This repository was archived by the owner on Jul 25, 2023. It is now read-only.

chore: replace parse boilerplate with a proc macro #11

8000
Merged
merged 22 commits into from
Jan 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
633cf1e
chore(print-macro): create a macro to implement print trait for param…
jdrouet Jan 15, 2023
ec95849
chore(print-macro): handle children
jdrouet Jan 15, 2023
35e221d
chore(print-macro): handle when children is text
jdrouet Jan 15, 2023
17d4873
chore(print-macro): create MrmlPrintChildren macro
jdrouet Jan 15, 2023
5086760
chore(print-macro): handle optional attributes
jdrouet Jan 16, 2023
9315e9c
chore(print-macro): handle struct as children
jdrouet Jan 16, 2023
fcd7f7e
chore: apply clippy suggestions
jdrouet Jan 16, 2023
dc7e9dc
chore(macro-print): fix code format
jdrouet Jan 17, 2023
98a8c34
chore(parse-macro): init package
jdrouet Jan 16, 2023
91dc187
chore(parse-macro): create first derive with element having only a St…
jdrouet Jan 16, 2023
5bf7562
chore(parse-macro): use macro on mj-title
jdrouet Jan 16, 2023
f51c91b
chore(parse-macro): handle attributes parsing on mj-spacer
jdrouet Jan 16, 2023
15a33dc
chore(parse-macro): use on mj-image
jdrouet Jan 16, 2023
820a35e
chore(parse-macro): handle attributes as struct
jdrouet Jan 16, 2023
79f3162
chore(parse-macro): use macro on mj-breakpoint
jdrouet Jan 17, 2023
7a34f9d
chore(parse-macro): use macro on mj-button and handle comments and text
jdrouet Jan 17, 2023
502db00
chore(parse-macro): create macro for parsing children
jdrouet Jan 17, 2023
c131aac
chore(parse-macro): use children macro everywhere
jdrouet Jan 17, 2023
4a29b07
chore(parse-macro): apply clippy fixes
jdrouet Jan 17, 2023
a7a31b8
Merge branch 'main' into macro-parse
jdrouet Jan 17, 2023
b9c0cef
Merge branch 'main' into macro-parse
jdrouet Jan 17, 2023
acc1863
merge branch 'main' into macro-parse
jdrouet Jan 17, 2023
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
170 changes: 96 additions & 74 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ travis-ci = { repository = "jdrouet/mrml", branch = "main" }
[features]
default = ["json", "parse", "print", "render"]
json = ["serde", "serde_json"]
parse = ["xmlparser"]
parse = ["mrml-parse-macros", "xmlparser"]
print = ["mrml-print-macros"]
render = ["rand"]
orderedmap = ["indexmap", "rustc-hash"]
Expand All @@ -35,14 +35,21 @@ serde_json = { version = "1.0", optional = true }
xmlparser = { version = "0.13", optional = true }
indexmap = { version = "1.9", features = ["serde-1"], optional = true }
rustc-hash = { version = "1.1", optional = true }

# macros
mrml-macros = { path = "./lib/mrml-macros" }
mrml-parse-macros = { path = "./lib/mrml-parse-macros", optional = true }
mrml-print-macros = { path = "./lib/mrml-print-macros", optional = true }

[dev-dependencies]
criterion = "0.4"

[workspace]
members = ["./lib/mrml-macros", "./lib/mrml-print-macros"]
members = [
"./lib/mrml-macros",
"./lib/mrml-parse-macros",
"./lib/mrml-print-macros",
]

[[bench]]
name = "basic"
Expand Down
16 changes: 16 additions & 0 deletions lib/mrml-parse-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "mrml-parse-macros"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
proc-macro = true

[dependencies]
darling = "0.14"
Inflector = "0.11"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full", "fold"] }
101 changes: 101 additions & 0 deletions lib/mrml-parse-macros/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use crate::common::{as_path, get_fields, is_option};
use inflector::cases::kebabcase::to_kebab_case;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Field, Ident};

fn create_builder_struct_field(field: &Field) -> Option<proc_macro2::TokenStream> {
if let Some(ref field_ident) = field.ident {
if as_path(field).map(is_option).unwrap_or(false) {
let ty = &field.ty;
Some(quote! {
#field_ident: #ty,
})
} else {
let ty = &field.ty;
Some(quote! {
#field_ident: Option<#ty>,
})
}
} else {
None
}
}

fn create_builder_build_field(field: &Field) -> Option<proc_macro2::TokenStream> {
if let Some(ref field_ident) = field.ident {
if as_path(field).map(is_option).unwrap_or(false) {
Some(quote! {
#field_ident: self.#field_ident,
})
} else {
Some(quote! {
#field_ident: self.#field_ident
.ok_or_else(|| crate::prelude::parse::Error::MissingAttribute(stringify!(#field_ident)))?,
})
}
} else {
None
}
}

fn create_builder_insert_field(field: &Field) -> Option<proc_macro2::TokenStream> {
if let Some(ref field_ident) = field.ident {
let attribute = to_kebab_case(&field_ident.to_string());
Some(quote! {
#attribute => {
self.#field_ident = Some(value.to_string());
}
})
} else {
None
}
}

fn create_builder(ast: &DeriveInput) -> proc_macro2::TokenStream {
let ident = &ast.ident;

let builder_name = format!("{ident}Builder");
let builder_ident = Ident::new(&builder_name, ident.span());

let struct_fields = get_fields(ast)
.iter()
.filter_map(create_builder_struct_field);

let build_fields = get_fields(ast)
.iter()
.filter_map(create_builder_build_field);

let insert_fields = get_fields(ast)
.iter()
.filter_map(create_builder_insert_field);

quote! {
#[derive(Debug, Default)]
struct #builder_ident {
#(#struct_fields)*
}

impl #builder_ident {
fn build(self) -> Result<#ident, crate::prelude::parse::Error> {
Ok(#ident {
#(#build_fields)*
})
}

fn insert<'a>(&mut self, name: xmlparser::StrSpan<'a>, value: xmlparser::StrSpan<'a>) -> Result<(), crate::prelude::parse::Error> {
match name.as_str() {
#(#insert_fields)*
_ => return Err(crate::prelude::parse::Error::UnexpectedAttribute(name.start())),
};
Ok(())
}
}
}
}

pub fn derive(input: TokenStream) -> TokenStream {
let ast: DeriveInput = parse_macro_input!(input as DeriveInput);

create_builder(&ast).into()
}
6 changes: 6 additions & 0 deletions lib/mrml-parse-macros/src/children.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use proc_macro::TokenStream;
use quote::quote;

pub fn derive(_input: TokenStream) -> TokenStream {
quote! {}.into()
}
137 changes: 137 additions & 0 deletions lib/mrml-parse-macros/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use proc_macro2::Span;
use syn::{
punctuated::Punctuated, token::Comma, Data, DataStruct, DeriveInput, Field, Fields,
FieldsNamed, GenericArgument, Path, PathArguments, Type, TypePath,
};

pub fn is_vec(path: &Path) -> bool {
path.segments
.first()
// TODO make sure that it's a Vec<T>
.map(|s| s.ident == "Vec")
.unwrap_or(false)
}

fn get_vec_type(path: &Path) -> Type {
let res = &path.segments.first().unwrap().arguments;
let res = if let PathArguments::AngleBracketed(arg) = res {
arg
} else {
panic!("expected path arguments of kind angle bracketed {res:?}");
};
let res = res.args.first().unwrap();
let res = if let GenericArgument::Type(ty) = res {
ty
} else {
panic!("expected generic argument of kind Type {res:?}");
};
res.to_owned()
}

pub fn is_map(path: &Path) -> bool {
path.segments
.first()
// TODO make sure that it's a Vec<String, String>
.map(|s| s.ident == "Map")
.unwrap_or(false)
}

pub fn is_option(path: &Path) -> bool {
path.segments
.first()
// TODO make sure that it's a Option<T>
.map(|s| s.ident == "Option")
.unwrap_or(false)
}

pub fn as_data_struct(ast: &DeriveInput) -> Option<&DataStruct> {
if let Data::Struct(inner) = &(ast.data) {
Some(inner)
} else {
None
}
}

pub fn as_fields_named(input: &DataStruct) -> Option<&FieldsNamed> {
if let Fields::Named(inner) = &input.fields {
Some(inner)
} else {
None
}
}

pub fn get_fields(ast: &DeriveInput) -> &Punctuated<Field, Comma> {
as_data_struct(ast)
.and_then(as_fields_named)
.map(|f| &f.named)
.expect("MrmlParseComponent only supports structs.")
}

pub fn get_children_field(ast: &DeriveInput) -> Option<&Field> {
get_fields(ast).into_iter().find(|f| {
f.ident
.as_ref()
.map(|id| *id == "children")
.unwrap_or(false)
})
}

pub enum ChildrenKind {
String,
List(Type),
None,
}

pub fn get_children_kind(ast: &DeriveInput) -> ChildrenKind {
if let Some(field) = get_children_field(ast) {
match &field.ty {
Type::Path(TypePath { path, .. }) if path.is_ident("String") => ChildrenKind::String,
Type::Path(TypePath { path, .. }) if is_vec(path) => {
ChildrenKind::List(get_vec_type(path))
}
_ => panic!("Incompatible type for children field {:?}", field.ident),
}
} else {
ChildrenKind::None
}
}

pub fn get_attributes_field(ast: &DeriveInput) -> Option<&Field> {
get_fields(ast).into_iter().find(|f| {
f.ident
.as_ref()
.map(|id| *id == "attributes")
.unwrap_or(false)
})
}

pub fn as_path(field: &Field) -> Option<&Path> {
match &field.ty {
Type::Path(TypePath { path, .. }) => Some(path),
_ => None,
}
}

pub fn get_attributes_kind(ast: &DeriveInput) -> AttributesKind {
if let Some(field) = get_attributes_field(ast) {
match &field.ty {
Type::Path(TypePath { path, .. }) if is_map(path) => AttributesKind::Map,
Type::Path(TypePath { path, .. }) => {
if let Some(ref ident) = path.get_ident() {
AttributesKind::Struct(ident.to_string(), ident.span())
} else {
AttributesKind::None
}
}
_ => AttributesKind::None,
}
} else {
AttributesKind::None
}
}

pub enum AttributesKind {
Map,
Struct(String, Span),
None,
}
Loading
0