diff --git a/Cargo.toml b/Cargo.toml index 18ffc877..fde23a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,6 +133,9 @@ version = "0.2" [dev-dependencies.wasm-bindgen-test] version = "0.3" +[dev-dependencies.trybuild] +version = "1.0.52" + [target.'cfg(windows)'.dev-dependencies.winapi] version = "0.3" features = ["combaseapi"] diff --git a/benches/macros/invalid_parse.rs b/benches/macros/invalid_parse.rs new file mode 100644 index 00000000..1f4330f0 --- /dev/null +++ b/benches/macros/invalid_parse.rs @@ -0,0 +1,29 @@ +use uuid::{uuid, Uuid}; + +const _: Uuid = uuid!(""); +const _: Uuid = uuid!("!"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E45"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BBF-329BF39FA1E4"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BGBF-329BF39FA1E4"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faaXB6BFF329BF39FA1E4"); +const _: Uuid = uuid!("F9168C5E-CEB-24fa-eB6BFF32-BF39FA1E4"); +const _: Uuid = uuid!("01020304-1112-2122-3132-41424344"); +const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c88"); +const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0cg8"); +const _: Uuid = uuid!("67e5504410b1426%9247bb680e5fe0c8"); + +// Test error reporting +const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c"); +const _: Uuid = uuid!("67e550X410b1426f9247bb680e5fe0cd"); +const _: Uuid = uuid!("67e550-4105b1426f9247bb680e5fe0c"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BF1-02BF39FA1E4"); + + +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BBF-329BF39FA1E4"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BGBF-329BF39FA1E4"); +const _: Uuid = uuid!("01020304-1112-2122-3132-41424344"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4"); + +fn main() {} \ No newline at end of file diff --git a/benches/macros/invalid_parse.stderr b/benches/macros/invalid_parse.stderr new file mode 100644 index 00000000..563d2aa4 --- /dev/null +++ b/benches/macros/invalid_parse.stderr @@ -0,0 +1,125 @@ +error: invalid length: expected one of [36, 32], found 0 + --> benches/macros/invalid_parse.rs:3:23 + | +3 | const _: Uuid = uuid!(""); + | ^^ + +error: invalid length: expected one of [36, 32], found 1 + --> benches/macros/invalid_parse.rs:4:23 + | +4 | const _: Uuid = uuid!("!"); + | ^^^ + +error: invalid length: expected one of [36, 32], found 37 + --> benches/macros/invalid_parse.rs:5:23 + | +5 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E45"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid length: expected one of [36, 32], found 35 + --> benches/macros/invalid_parse.rs:6:23 + | +6 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BBF-329BF39FA1E4"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found G at 20 + --> benches/macros/invalid_parse.rs:7:44 + | +7 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BGBF-329BF39FA1E4"); + | ^ + +error: invalid number of groups: expected one of [1, 5], found 4 + --> benches/macros/invalid_parse.rs:8:23 + | +8 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid length: expected one of [36, 32], found 18 + --> benches/macros/invalid_parse.rs:9:23 + | +9 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa"); + | ^^^^^^^^^^^^^^^^^^^^ + +error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found X at 18 + --> benches/macros/invalid_parse.rs:10:42 + | +10 | const _: Uuid = uuid!("F9168C5E-CEB2-4faaXB6BFF329BF39FA1E4"); + | ^ + +error: invalid group length: expected 4, found 3 in group 1 + --> benches/macros/invalid_parse.rs:11:33 + | +11 | const _: Uuid = uuid!("F9168C5E-CEB-24fa-eB6BFF32-BF39FA1E4"); + | ^^^ + +error: invalid group length: expected 12, found 8 in group 4 + --> benches/macros/invalid_parse.rs:12:48 + | +12 | const _: Uuid = uuid!("01020304-1112-2122-3132-41424344"); + | ^^^^^^^^ + +error: invalid length: expected one of [36, 32], found 33 + --> benches/macros/invalid_parse.rs:13:23 + | +13 | const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c88"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid length: expected one of [36, 32], found 33 + --> benches/macros/invalid_parse.rs:14:23 + | +14 | const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0cg8"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found % at 15 + --> benches/macros/invalid_parse.rs:15:39 + | +15 | const _: Uuid = uuid!("67e5504410b1426%9247bb680e5fe0c8"); + | ^ + +error: invalid length: expected one of [36, 32], found 31 + --> benches/macros/invalid_parse.rs:18:23 + | +18 | const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found X at 6 + --> benches/macros/invalid_parse.rs:19:30 + | +19 | const _: Uuid = uuid!("67e550X410b1426f9247bb680e5fe0cd"); + | ^ + +error: invalid group length: expected 8, found 6 in group 0 + --> benches/macros/invalid_parse.rs:20:24 + | +20 | const _: Uuid = uuid!("67e550-4105b1426f9247bb680e5fe0c"); + | ^^^^^^ + +error: invalid group length: expected 4, found 5 in group 3 + --> benches/macros/invalid_parse.rs:21:43 + | +21 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BF1-02BF39FA1E4"); + | ^^^^^ + +error: invalid length: expected one of [36, 32], found 35 + --> benches/macros/invalid_parse.rs:24:23 + | +24 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BBF-329BF39FA1E4"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found G at 20 + --> benches/macros/invalid_parse.rs:25:44 + | +25 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-BGBF-329BF39FA1E4"); + | ^ + +error: invalid group length: expected 12, found 8 in group 4 + --> benches/macros/invalid_parse.rs:26:48 + | +26 | const _: Uuid = uuid!("01020304-1112-2122-3132-41424344"); + | ^^^^^^^^ + +error: invalid number of groups: expected one of [1, 5], found 4 + --> benches/macros/invalid_parse.rs:27:23 + | +27 | const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/benches/macros/renamed.rs b/benches/macros/renamed.rs new file mode 100644 index 00000000..87a5dd61 --- /dev/null +++ b/benches/macros/renamed.rs @@ -0,0 +1,11 @@ +use ::uuid::{uuid as id, Uuid as Id}; + +mod uuid { + struct MyType; +} + +struct Uuid; + +const _: Id = id!("67e55044-10b1-426f-9247-bb680e5fe0c8"); + +fn main() {} \ No newline at end of file diff --git a/benches/macros/valid_parse.rs b/benches/macros/valid_parse.rs new file mode 100644 index 00000000..31ab620c --- /dev/null +++ b/benches/macros/valid_parse.rs @@ -0,0 +1,20 @@ +use uuid::{uuid, Uuid}; + +const _: Uuid = uuid!("00000000000000000000000000000000"); +const _: Uuid = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); +const _: Uuid = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); +const _: Uuid = uuid!("F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4"); +const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c8"); +const _: Uuid = uuid!("01020304-1112-2122-3132-414243444546"); +const _: Uuid = uuid!("urn:uuid:67e55044-10b1-426f-9247-bb680e5fe0c8"); + +// Nil +const _: Uuid = uuid!("00000000000000000000000000000000"); +const _: Uuid = uuid!("00000000-0000-0000-0000-000000000000"); + +// valid hyphenated +const _: Uuid = uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8"); +// valid short +const _: Uuid = uuid!("67e5504410b1426f9247bb680e5fe0c8"); + +fn main() {} \ No newline at end of file diff --git a/benches/parse_str.rs b/benches/parse_str.rs index e53ba242..3ec45227 100644 --- a/benches/parse_str.rs +++ b/benches/parse_str.rs @@ -48,3 +48,12 @@ fn parse_invalid_group_len(b: &mut Bencher) { fn parse_invalid_groups(b: &mut Bencher) { b.iter(|| Uuid::parse_str("F9168C5E-CEB2-4faa-B6BFF329BF39FA1E4")); } + +#[cfg(feature = "macros")] +#[test] +fn test_valid_macro() { + let t = trybuild::TestCases::new(); + t.pass("benches/macros/valid_parse.rs"); + t.pass("benches/macros/renamed.rs"); + t.compile_fail("benches/macros/invalid_parse.rs"); +} \ No newline at end of file diff --git a/macros/Cargo.toml b/macros/Cargo.toml index 75f04119..d0c03c7b 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -7,3 +7,6 @@ edition = "2018" proc-macro = true [dependencies] +syn = "1.0.80" +quote = "1.0.10" +proc-macro2 = "1.0.29" diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 396a74a4..1ff05ffd 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,17 +1,81 @@ -use std; +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, quote_spanned}; +use std::fmt; +use syn::spanned::Spanned; + +#[cfg(any(feature = "std", test))] +#[macro_use] +extern crate std; + +#[cfg(all(not(feature = "std"), not(test)))] +#[macro_use] +extern crate core as std; #[path = "../../shared/error.rs"] -#[allow(dead_code)] mod error; #[path = "../../shared/parser.rs"] -#[allow(dead_code)] mod parser; -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +#[proc_macro] +pub fn parse_lit(input: TokenStream) -> TokenStream { + build_uuid(input.clone()).unwrap_or_else(|e| { + let msg = e.to_string(); + let ts = TokenStream2::from(input); + let span = match e { + Error::UuidParse(error::Error( + error::ErrorKind::InvalidCharacter { index, .. }, + )) => { + let mut s = proc_macro2::Literal::string(""); + s.set_span(ts.span()); + s.subspan(index + 1..=index + 1).unwrap() + } + Error::UuidParse(error::Error( + error::ErrorKind::InvalidGroupLength { found, group, .. }, + )) => { + let start = + parser::GROUP_LENS.iter().take(group).sum::() + + group + + 1; + let mut s = proc_macro2::Literal::string(""); + s.set_span(ts.span()); + s.subspan(start..start + found).unwrap() + } + _ => ts.span(), + }; + TokenStream::from(quote_spanned! {span=> + compile_error!(#msg) + }) + }) +} + +enum Error { + NonStringLiteral, + UuidParse(error::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Error::NonStringLiteral => f.write_str("expected string literal"), + Error::UuidParse(ref e) => write!(f, "{}", e), + } } } + +fn build_uuid(input: TokenStream) -> Result { + let string = match syn::parse::(input) { + Ok(syn::Lit::Str(literal)) => literal.value(), + _ => return Err(Error::NonStringLiteral), + }; + + let bytes = parser::parse_str(&string).map_err(Error::UuidParse)?; + + let tokens = bytes + .iter() + .map(|byte| quote! { #byte, }) + .collect::(); + + Ok(quote! {[#tokens]}.into()) +} diff --git a/shared/parser.rs b/shared/parser.rs index 49342eaf..fde0bb40 100644 --- a/shared/parser.rs +++ b/shared/parser.rs @@ -26,7 +26,7 @@ fn len_matches_any(len: usize, crits: &[usize]) -> bool { const ACC_GROUP_LENS: [usize; 5] = [8, 12, 16, 20, 32]; // Length of each hyphenated group in hex digits. -const GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12]; +pub(super) const GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12]; pub fn parse_str(mut input: &str) -> Result<[u8; 16], Error> { // Ensure length is valid for any of the supported formats diff --git a/src/lib.rs b/src/lib.rs index 45e3978e..a048ca87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,8 +207,10 @@ mod v5; mod winapi_support; #[cfg(feature = "macros")] -#[doc(inline)] -pub use uuid_macros::*; +#[macro_use] +mod macros; +#[cfg(feature = "macros")] +pub extern crate uuid_macros; use crate::std::convert; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..f4380d47 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,53 @@ +/// Parse [`Uuid`][uuid::Uuid]s from string literals at compile time. +/// ## Usage +/// This macro transforms the string literal representation of a +/// [`Uuid`][uuid::Uuid] into the bytes representation, raising a compilation +/// error if it cannot properly be parsed. +/// +/// ## Examples +/// Setting a global constant: +/// ``` +/// # use uuid::{uuid, Uuid}; +/// pub const SCHEMA_ATTR_CLASS: Uuid = uuid!("00000000-0000-0000-0000-ffff00000000"); +/// pub const SCHEMA_ATTR_UUID: Uuid = uuid!("00000000-0000-0000-0000-ffff00000001"); +/// pub const SCHEMA_ATTR_NAME: Uuid = uuid!("00000000-0000-0000-0000-ffff00000002"); +/// ``` +/// Defining a local variable: +/// ``` +/// # use uuid::{uuid, Uuid}; +/// let uuid: Uuid = uuid!("urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4"); +/// ``` +/// ## Compilation Failures +/// Invalid UUIDs are rejected: +/// ```ignore +/// # use uuid::{uuid, Uuid}; +/// let uuid: Uuid = uuid!("F9168C5E-ZEB2-4FAA-B6BF-329BF39FA1E4"); +/// ``` +/// Provides the following compilation error: +/// ```txt +/// error: invalid character: expected an optional prefix of `urn:uuid:` followed by 0123456789abcdefABCDEF-, found Z at 9 +/// | +/// | let id: Uuid = uuid!("F9168C5E-ZEB2-4FAA-B6BF-329BF39FA1E4"); +/// | ^ +/// ``` +/// Tokens that aren't string literals are also rejected: +/// ```ignore +/// # use uuid::{uuid, Uuid}; +/// let uuid_str: &str = "550e8400e29b41d4a716446655440000"; +/// let uuid: Uuid = uuid!(uuid_str); +/// ``` +/// Provides the following compilation error: +/// ```txt +/// error: expected string literal +/// | +/// | let uuid: Uuid = uuid!(uuid_str); +/// | ^^^^^^^^ +/// ``` +/// +/// [uuid::Uuid]: https://docs.rs/uuid/*/uuid/struct.Uuid.html +#[macro_export] +macro_rules! uuid { + ($uuid:tt) => {{ + $crate::Uuid::from_bytes($crate::uuid_macros::parse_lit!($uuid)) + }}; +}