mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-12-23 10:11:54 +00:00
Add proc macros for Hint and edge (#63)
* Add proc-macro crate with two macros * Let cargo recalculate the Cargo.lock * Add tests and refactor some code to allow testing also the impl for parse_hint_helper_attrs now preserves order (which is essential for testing)
This commit is contained in:
parent
b7f18dfaa8
commit
5f565aeb74
8 changed files with 539 additions and 28 deletions
63
Cargo.lock
generated
63
Cargo.lock
generated
|
|
@ -58,9 +58,20 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bitflags",
|
||||
"graphite-document-core",
|
||||
"graphite-proc-macros",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphite-proc-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"graphite-editor-core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "graphite-renderer-core"
|
||||
version = "0.1.0"
|
||||
|
|
@ -78,18 +89,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.49"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821"
|
||||
checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kurbo"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb348d766edbac91ba1eb83020d96f4f8867924d194393083c15a51f185e6a82"
|
||||
checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
|
@ -111,9 +122,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
version = "1.0.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
|
@ -135,9 +146,9 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.64"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f"
|
||||
checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -152,9 +163,9 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.72"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe"
|
||||
checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"wasm-bindgen-macro",
|
||||
|
|
@ -162,9 +173,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.72"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3"
|
||||
checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"lazy_static",
|
||||
|
|
@ -177,9 +188,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.22"
|
||||
version = "0.4.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468"
|
||||
checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"js-sys",
|
||||
|
|
@ -189,9 +200,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.72"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b"
|
||||
checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
|
|
@ -199,9 +210,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.72"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d"
|
||||
checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -212,15 +223,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.72"
|
||||
version = "0.2.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa"
|
||||
checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f002ea97b5abdb19aafd48cbb5a0a7f6931cf36ea05a0a46ccc95d9f4c2cf43"
|
||||
checksum = "e972e914de63aa53bd84865e54f5c761bd274d48e5be3a6329a662c0386aa67a"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
|
@ -232,9 +243,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.22"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a6c0bd3933daf64c78fc25a7452530f79fa7e21f77fa03d608d1e988a66735"
|
||||
checksum = "ea6153a8f9bf24588e9f25c87223414fff124049f68d3a442a0f0eab4768a8b6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -242,9 +253,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.49"
|
||||
version = "0.3.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310"
|
||||
checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
|
|
|
|||
|
|
@ -15,3 +15,7 @@ bitflags = "1.2.1"
|
|||
[dependencies.document-core]
|
||||
path = "../document"
|
||||
package = "graphite-document-core"
|
||||
|
||||
[dependencies.proc-macros]
|
||||
path = "../proc-macro"
|
||||
package = "graphite-proc-macros"
|
||||
|
|
|
|||
5
core/editor/src/hint.rs
Normal file
5
core/editor/src/hint.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
pub trait Hint {
|
||||
fn hints(&self) -> HashMap<String, String>;
|
||||
}
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
mod color;
|
||||
mod dispatcher;
|
||||
mod error;
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
pub mod hint;
|
||||
pub mod tools;
|
||||
pub mod workspace;
|
||||
|
||||
|
|
|
|||
19
core/proc-macro/Cargo.toml
Normal file
19
core/proc-macro/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "graphite-proc-macros"
|
||||
version = "0.1.0"
|
||||
authors = ["Graphite Authors <contact@graphite.design>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.26"
|
||||
syn = "1.0.68"
|
||||
quote = "1.0.9"
|
||||
|
||||
[dev-dependencies.editor-core]
|
||||
path = "../editor"
|
||||
package = "graphite-editor-core"
|
||||
62
core/proc-macro/src/helpers.rs
Normal file
62
core/proc-macro/src/helpers.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
use proc_macro2::Ident;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{Path, PathArguments, PathSegment, Token};
|
||||
|
||||
/// Returns `Ok(Vec<T>)` if all items are `Ok(T)`, else returns a combination of every error encountered (not just the first one)
|
||||
pub fn fold_error_iter<T>(iter: impl Iterator<Item = syn::Result<T>>) -> syn::Result<Vec<T>> {
|
||||
iter.fold(Ok(vec![]), |acc, x| match acc {
|
||||
Ok(mut v) => x.map(|x| {
|
||||
v.push(x);
|
||||
v
|
||||
}),
|
||||
Err(mut e) => match x {
|
||||
Ok(_) => Err(e),
|
||||
Err(e2) => {
|
||||
e.combine(e2);
|
||||
Err(e)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates the path `left::right` from the idents `left` and `right`
|
||||
pub fn two_path(left_ident: Ident, right_ident: Ident) -> Path {
|
||||
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
|
||||
segments.push(PathSegment {
|
||||
ident: left_ident,
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
segments.push(PathSegment {
|
||||
ident: right_ident,
|
||||
arguments: PathArguments::None,
|
||||
});
|
||||
|
||||
Path { leading_colon: None, segments }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use quote::ToTokens;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
#[test]
|
||||
fn test_fold_error_iter() {
|
||||
let res = fold_error_iter(vec![Ok(()), Ok(())].into_iter());
|
||||
assert!(res.is_ok());
|
||||
|
||||
let _span = quote::quote! { "" }.span();
|
||||
let res = fold_error_iter(vec![Ok(()), Err(syn::Error::new(_span, "err1")), Err(syn::Error::new(_span, "err2"))].into_iter());
|
||||
assert!(res.is_err());
|
||||
let err = res.unwrap_err();
|
||||
let mut check_err = syn::Error::new(_span, "err1");
|
||||
check_err.combine(syn::Error::new(_span, "err2"));
|
||||
assert_eq!(err.to_compile_error().to_string(), check_err.to_compile_error().to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_path() {
|
||||
let _span = quote::quote! { "" }.span();
|
||||
assert_eq!(two_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b");
|
||||
}
|
||||
}
|
||||
264
core/proc-macro/src/lib.rs
Normal file
264
core/proc-macro/src/lib.rs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
mod helpers;
|
||||
mod structs;
|
||||
|
||||
use crate::helpers::{fold_error_iter, two_path};
|
||||
use crate::structs::{AttrInnerKeyStringMap, AttrInnerSingleString};
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use syn::{parse_macro_input, Attribute, Data, DeriveInput, LitStr, Variant};
|
||||
|
||||
fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec<LitStr>, Vec<LitStr>)> {
|
||||
fold_error_iter(
|
||||
attrs
|
||||
.iter()
|
||||
.filter(|a| a.path.get_ident().map_or(false, |i| i == "hint"))
|
||||
.map(|attr| syn::parse2::<AttrInnerKeyStringMap>(attr.tokens.clone())),
|
||||
)
|
||||
.and_then(|v: Vec<AttrInnerKeyStringMap>| {
|
||||
fold_error_iter(AttrInnerKeyStringMap::multi_into_iter(v).map(|(k, mut v)| match v.len() {
|
||||
0 => panic!("internal error: a key without values was somehow inserted into the hashmap"),
|
||||
1 => {
|
||||
let single_val = v.pop().unwrap();
|
||||
Ok((LitStr::new(&k.to_string(), Span::call_site()), single_val))
|
||||
}
|
||||
_ => {
|
||||
// the first value is ok, the other ones should error
|
||||
let after_first = v.into_iter().skip(1);
|
||||
// this call to fold_error_iter will always return Err with a combined error
|
||||
fold_error_iter(after_first.map(|lit| Err(syn::Error::new(lit.span(), format!("value for key {} was already given", k))))).map(|_: Vec<()>| unreachable!())
|
||||
}
|
||||
}))
|
||||
})
|
||||
.map(|v| v.into_iter().unzip())
|
||||
}
|
||||
|
||||
fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
|
||||
let input = syn::parse2::<DeriveInput>(input_item)?;
|
||||
|
||||
let ident = input.ident;
|
||||
|
||||
match input.data {
|
||||
Data::Enum(data) => {
|
||||
let variants = data.variants.iter().map(|var: &Variant| two_path(ident.clone(), var.ident.clone())).collect::<Vec<_>>();
|
||||
|
||||
let hint_result = fold_error_iter(data.variants.into_iter().map(|var: Variant| parse_hint_helper_attrs(&var.attrs)));
|
||||
|
||||
hint_result.map(|hints: Vec<(Vec<LitStr>, Vec<LitStr>)>| {
|
||||
let (keys, values): (Vec<Vec<LitStr>>, Vec<Vec<LitStr>>) = hints.into_iter().unzip();
|
||||
let cap: Vec<usize> = keys.iter().map(|v| v.len()).collect();
|
||||
|
||||
quote::quote! {
|
||||
impl Hint for #ident {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
match self {
|
||||
#(
|
||||
#variants { .. } => {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
|
||||
#(
|
||||
hm.insert(#keys.to_string(), #values.to_string());
|
||||
)*
|
||||
hm
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Data::Struct(_) | Data::Union(_) => {
|
||||
let hint_result = parse_hint_helper_attrs(&input.attrs);
|
||||
|
||||
hint_result.map(|(keys, values)| {
|
||||
let cap = keys.len();
|
||||
|
||||
quote::quote! {
|
||||
impl Hint for #ident {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
|
||||
#(
|
||||
hm.insert(#keys.to_string(), #values.to_string());
|
||||
)*
|
||||
hm
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the `Hint` trait
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use graphite_proc_macros::Hint;
|
||||
/// # use editor_core::hint::Hint;
|
||||
///
|
||||
/// #[derive(Hint)]
|
||||
/// pub enum StateMachine {
|
||||
/// #[hint(rmb = "foo", lmb = "bar")]
|
||||
/// Ready,
|
||||
/// #[hint(alt = "baz")]
|
||||
/// RMBDown,
|
||||
/// // no hint (also ok)
|
||||
/// LMBDown
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(Hint, attributes(hint))]
|
||||
pub fn derive_hint(input_item: TokenStream) -> TokenStream {
|
||||
TokenStream::from(derive_hint_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
/// The `edge` proc macro does nothing, it is intended for use with an external tool
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
/// match (example_tool_state, event) {
|
||||
/// (ToolState::Ready, Event::MouseDown(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||
/// #[edge("LMB Down")]
|
||||
/// ToolState::Pending
|
||||
/// }
|
||||
/// (SelectToolState::Pending, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||
/// #[edge("LMB Up: Select Object")]
|
||||
/// SelectToolState::Ready
|
||||
/// }
|
||||
/// (SelectToolState::Pending, Event::MouseMove(x,y)) => {
|
||||
/// #[edge("Mouse Move")]
|
||||
/// SelectToolState::TransformSelected
|
||||
/// }
|
||||
/// (SelectToolState::TransformSelected, Event::MouseMove(x,y)) => {
|
||||
/// #[egde("Mouse Move")]
|
||||
/// SelectToolState::TransformSelected
|
||||
/// }
|
||||
/// (SelectToolState::TransformSelected, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||
/// #[edge("LMB Up")]
|
||||
/// SelectToolState::Ready
|
||||
/// }
|
||||
/// (state, _) => {
|
||||
/// // Do nothing
|
||||
/// state
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// to make sure that only `#[edge("string")]` is allowed
|
||||
let _verify = parse_macro_input!(attr as AttrInnerSingleString);
|
||||
|
||||
item
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn ts_assert_eq(l: TokenStream2, r: TokenStream2) {
|
||||
// not sure if this is the best way of doing things but if two TokenStreams are equal, their `to_string` is also equal
|
||||
// so there are at least no false negatives
|
||||
assert_eq!(l.to_string(), r.to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derive_hint() {
|
||||
let res = derive_hint_impl(quote::quote! {
|
||||
#[hint(key1="val1",key2="val2",)]
|
||||
struct S { a: u8, b: String, c: bool }
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
ts_assert_eq(
|
||||
res.unwrap(),
|
||||
quote::quote! {
|
||||
impl Hint for S {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
|
||||
hm.insert("key1".to_string(), "val1".to_string());
|
||||
hm.insert("key2".to_string(), "val2".to_string());
|
||||
hm
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let res = derive_hint_impl(quote::quote! {
|
||||
enum E {
|
||||
#[hint(key1="val1",key2="val2",)]
|
||||
S { a: u8, b: String, c: bool },
|
||||
#[hint(key3="val3")]
|
||||
X,
|
||||
Y
|
||||
}
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
ts_assert_eq(
|
||||
res.unwrap(),
|
||||
quote::quote! {
|
||||
impl Hint for E {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
match self {
|
||||
E::S { .. } => {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
|
||||
hm.insert("key1".to_string(), "val1".to_string());
|
||||
hm.insert("key2".to_string(), "val2".to_string());
|
||||
hm
|
||||
}
|
||||
E::X { .. } => {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(1usize);
|
||||
hm.insert("key3".to_string(), "val3".to_string());
|
||||
hm
|
||||
}
|
||||
E::Y { .. } => {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
|
||||
hm
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let res = derive_hint_impl(quote::quote! {
|
||||
union NoHint {}
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
ts_assert_eq(
|
||||
res.unwrap(),
|
||||
quote::quote! {
|
||||
impl Hint for NoHint {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
|
||||
hm
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let res = derive_hint_impl(quote::quote! {
|
||||
#[hint(a="1", a="2")]
|
||||
struct S;
|
||||
});
|
||||
assert!(res.is_err());
|
||||
|
||||
let res = derive_hint_impl(quote::quote! {
|
||||
#[hint(a="1")]
|
||||
#[hint(b="2")]
|
||||
struct S;
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
ts_assert_eq(
|
||||
res.unwrap(),
|
||||
quote::quote! {
|
||||
impl Hint for S {
|
||||
fn hints(&self) -> ::std::collections::HashMap<String, String> {
|
||||
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
|
||||
hm.insert("a".to_string(), "1".to_string());
|
||||
hm.insert("b".to_string(), "2".to_string());
|
||||
hm
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// note: edge needs no testing since AttrInnerSingleString has testing and that's all you'd need to test with edge
|
||||
}
|
||||
144
core/proc-macro/src/structs.rs
Normal file
144
core/proc-macro/src/structs.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
use proc_macro2::Ident;
|
||||
use std::collections::HashMap;
|
||||
use syn::parse::{Parse, ParseStream};
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::token::Paren;
|
||||
use syn::{parenthesized, LitStr, Token};
|
||||
|
||||
/// Parses `("some text")`
|
||||
pub struct AttrInnerSingleString {
|
||||
_paren_token: Paren,
|
||||
pub content: LitStr,
|
||||
}
|
||||
|
||||
impl Parse for AttrInnerSingleString {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
let _paren_token = parenthesized!(content in input);
|
||||
Ok(Self {
|
||||
_paren_token,
|
||||
content: content.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `key="value"`
|
||||
pub struct KeyEqString {
|
||||
key: Ident,
|
||||
_eq_token: Token![=],
|
||||
lit: LitStr,
|
||||
}
|
||||
|
||||
impl Parse for KeyEqString {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
Ok(Self {
|
||||
key: input.parse()?,
|
||||
_eq_token: input.parse()?,
|
||||
lit: input.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `(key="value", key="value", …)`
|
||||
pub struct AttrInnerKeyStringMap {
|
||||
_paren_token: Paren,
|
||||
parts: Punctuated<KeyEqString, Token![,]>,
|
||||
}
|
||||
|
||||
impl Parse for AttrInnerKeyStringMap {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let content;
|
||||
let _paren_token = parenthesized!(content in input);
|
||||
Ok(Self {
|
||||
_paren_token,
|
||||
parts: Punctuated::parse_terminated(&content)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AttrInnerKeyStringMap {
|
||||
pub fn multi_into_iter(iter: impl IntoIterator<Item = Self>) -> impl Iterator<Item = (Ident, Vec<LitStr>)> {
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
let mut res = Vec::<(Ident, Vec<LitStr>)>::new();
|
||||
let mut idx = HashMap::<Ident, usize>::new();
|
||||
|
||||
for part in iter.into_iter().flat_map(|x: Self| x.parts) {
|
||||
match idx.entry(part.key) {
|
||||
Entry::Occupied(occ) => {
|
||||
res[*occ.get()].1.push(part.lit);
|
||||
}
|
||||
Entry::Vacant(vac) => {
|
||||
let ident = vac.key().clone();
|
||||
vac.insert(res.len());
|
||||
res.push((ident, vec![part.lit]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn attr_inner_single_string() {
|
||||
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
|
||||
("a string literal")
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
assert_eq!(res.ok().unwrap().content.value(), "a string literal");
|
||||
|
||||
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
|
||||
wrong, "stuff"
|
||||
});
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_eq_string() {
|
||||
let res = syn::parse2::<KeyEqString>(quote::quote! {
|
||||
key="value"
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
let res = res.ok().unwrap();
|
||||
assert_eq!(res.key, "key");
|
||||
assert_eq!(res.lit.value(), "value");
|
||||
|
||||
let res = syn::parse2::<KeyEqString>(quote::quote! {
|
||||
wrong, "stuff"
|
||||
});
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attr_inner_key_string_map() {
|
||||
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
|
||||
(key="value", key2="value2")
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
let res = res.ok().unwrap();
|
||||
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
|
||||
assert_eq!(item.key, k);
|
||||
assert_eq!(item.lit.value(), v);
|
||||
}
|
||||
|
||||
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
|
||||
(key="value", key2="value2",)
|
||||
});
|
||||
assert!(res.is_ok());
|
||||
let res = res.ok().unwrap();
|
||||
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
|
||||
assert_eq!(item.key, k);
|
||||
assert_eq!(item.lit.value(), v);
|
||||
}
|
||||
|
||||
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
|
||||
wrong, "stuff"
|
||||
});
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue