Skip to content

Commit b1a9fe9

Browse files
committed
compile-time check of regexes, utility macros
1 parent d42a518 commit b1a9fe9

File tree

10 files changed

+432
-56
lines changed

10 files changed

+432
-56
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
<a name="v2.0.0"></a>
2+
### v2.0.0 - 2021-05-17
3+
- regular expressions are now checked at compile time
4+
- regex_is_match!
5+
- regex_find!
6+
- regex_captures!
7+
18
<a name="v1.1.0"></a>
29
### v1.1.0 - 2021-05-08
310
- no more complementary import needed

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
[package]
22
name = "lazy-regex"
3-
version = "1.1.0"
3+
version = "2.0.0"
44
authors = ["Canop <[email protected]>"]
55
edition = "2018"
6-
description = "a macro to reduce regex creation boilerplate"
6+
description = "lazy static regular expressions checked at compile time"
77
keywords = ["macro", "lazy", "static", "regex"]
88
license = "MIT"
99
categories = ["text-processing"]
@@ -12,5 +12,12 @@ readme = "README.md"
1212

1313
[dependencies]
1414
once_cell = "1.7"
15-
regex = "1.4"
15+
regex = "1.5"
1616

17+
[dependencies.proc_macros]
18+
package = "lazy-regex-proc_macros"
19+
path = "src/proc_macros"
20+
version = "2.0.0"
21+
22+
[workspace]
23+
members = ["src/proc_macros", "examples/regexes"]

README.md

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,82 @@
11
# lazy-regex
2-
a macro for when you're tired of the regex creation boilerplate
32

4-
## What it does
3+
Use the `regex!` macro to build regexes:
54

6-
It's a shortcut to write static lazily compiled regular expressions as is usually done with lazy_static or once_cell.
5+
* they're checked at compile time
6+
* they're wrapped in `once_cell` lazy static initializers so that they're compiled only once
7+
* they can hold flags with a familiar suffix syntax: `let case_insensitive_regex = regex!("ab*"i);`
8+
* regex creation is less verbose
79

8-
It lets you replace
10+
This macro builds normal instances of `regex::Regex` so all the usual features are available.
911

12+
You may also use shortcut macros for testing a match or capturing groups as substrings:
13+
14+
* `regex_is_match!`
15+
* `regex_find!`
16+
* `regex_captures!`
17+
18+
# Build Regexes
1019

1120
```
12-
fn some_helper_function(text: &str) -> bool {
13-
lazy_static! {
14-
static ref RE: Regex = Regex::new("...").unwrap();
15-
}
16-
RE.is_match(text)
17-
}
18-
```
21+
use lazy_regex::regex;
1922
20-
with
23+
// build a simple regex
24+
let r = regex!("sa+$");
25+
assert_eq!(r.is_match("Saa"), false);
2126
27+
// build a regex with flag(s)
28+
let r = regex!("sa+$"i);
29+
assert_eq!(r.is_match("Saa"), true);
2230
23-
```
24-
fn some_helper_function(text: &str) -> bool {
25-
regex!("...").is_match(text)
26-
}
27-
```
31+
// supported regex flags: 'i', 'm', 's', 'x', 'U'
32+
// see https://docs.rs/regex/1.5.4/regex/struct.RegexBuilder.html
2833
29-
The first code comes from the regex documentation.
34+
// you can use a raw literal
35+
let r = regex!(r#"^"+$"#);
36+
assert_eq!(r.is_match("\"\""), true);
3037
38+
// or a raw literal with flag(s)
39+
let r = regex!(r#"^\s*("[a-t]*"\s*)+$"#i);
40+
assert_eq!(r.is_match(r#" "Aristote" "Platon" "#), true);
3141
32-
## FAQ
42+
// this line wouldn't compile:
43+
// let r = regex!("(unclosed");
3344
34-
### Is it really useful ?
45+
```
3546

36-
Regarding the binary, it's as using lazy_static or once_cell.
37-
It just makes some code a little easier to read. You're judge.
47+
# Test a match
3848

39-
### Can I have several `regex!` in the same function ? On the same Line ?
49+
```
50+
use lazy_regex::regex_is_match;
4051
41-
Yes, no problem.
52+
let b = regex_is_match!("[ab]+", "car");
53+
assert_eq!(b, true);
54+
```
4255

43-
### It hides the `unwrap()`, isn't it concerning ?
56+
# Extract a value
4457

45-
Not so much in my opinion as the macro only accepts a litteral: you won't hide a failure occuring on a dynamic string.
58+
```
59+
use lazy_regex::regex_find;
4660
47-
### I'd like to have flags too
61+
let f_word = regex_find!(r#"\bf\w+\b"#, "The fox jumps.").unwrap();
62+
assert_eq!(f_word, "fox");
63+
```
4864

49-
You mean something like `regex!("somestring", "i")` ? Cool. I was just waiting for somebody's else to ask for it. Create an issue and I'll see if I can easily wrap `RegexBuilder` to handle flags ala JavaScript.
65+
# Capture
5066

51-
### What's the licence ?
67+
```
68+
use lazy_regex::regex_captures;
69+
70+
let (_, letter) = regex_captures!(r#"([a-z])\d+"#i, "form A42").unwrap();
71+
assert_eq!(letter, "A");
72+
73+
let (whole, name, version) = regex_captures!(
74+
r#"(\w+)-([0-9.]+)"#, // a literal regex
75+
"This is lazy_regex-2.0!", // any expression
76+
).unwrap();
77+
assert_eq!(whole, "lazy_regex-2.0");
78+
assert_eq!(name, "lazy_regex");
79+
assert_eq!(version, "2.0");
80+
```
5281

53-
It's MIT. No attribution is needed.
82+
The size of the tupple is checked at compile time and ensures you have the right number of capturing groups.

examples/regexes/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
/target/
4+
5+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7+
Cargo.lock
8+
9+
# These are backup files generated by rustfmt
10+
**/*.rs.bk

examples/regexes/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "regexes"
3+
version = "1.0.0"
4+
authors = ["dystroy <[email protected]>"]
5+
edition = "2018"
6+
description = "An example for lazy-regex"
7+
license = "MIT"
8+
readme = "README.md"
9+
10+
[dependencies]
11+
lazy-regex = { path = "../.." }
12+
regex = "1.4"

examples/regexes/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
You may launch this example in this repository with
3+
4+
```
5+
cargo run
6+
```
7+
8+
and that would do nothing.
9+
10+
If you want some log, set the env var `SMALL_APP_LOG`, for example with
11+
12+
```
13+
SMALL_APP_LOG=info cargo run
14+
```
15+
16+
or
17+
18+
```
19+
SMALL_APP_LOG=debug cargo run
20+
```
21+
22+
A `small-app.log` file would thus be created.

examples/regexes/src/main.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use {
2+
lazy_regex::*,
3+
};
4+
5+
fn example_builds() {
6+
7+
// build a simple regex
8+
let r = regex!("sa+$");
9+
assert_eq!(r.is_match("Saa"), false);
10+
11+
// build a regex with flag(s)
12+
let r = regex!("sa+$"i);
13+
assert_eq!(r.is_match("Saa"), true);
14+
15+
// you can use a raw literal
16+
let r = regex!(r#"^"+$"#);
17+
assert_eq!(r.is_match("\"\""), true);
18+
19+
// and a raw literal with flag(s)
20+
let r = regex!(r#"^\s*("[a-t]*"\s*)+$"#i);
21+
assert_eq!(r.is_match(r#" "Aristote" "Platon" "#), true);
22+
23+
// this line wouldn't compile:
24+
// let r = regex!("(unclosed");
25+
26+
}
27+
28+
fn example_is_match() {
29+
let b = regex_is_match!("[ab]+", "car");
30+
assert_eq!(b, true);
31+
}
32+
33+
fn example_captures() {
34+
let (whole, name, version) = regex_captures!(
35+
r#"(\w+)-([0-9.]+)"#, // a literal regex
36+
"This is lazy_regex-2.0!", // any expression
37+
).unwrap();
38+
assert_eq!(whole, "lazy_regex-2.0");
39+
assert_eq!(name, "lazy_regex");
40+
assert_eq!(version, "2.0");
41+
}
42+
43+
fn main() {
44+
45+
// the regular expressions will be built only once
46+
for _ in 0..10 {
47+
example_builds();
48+
}
49+
50+
example_is_match();
51+
52+
for _ in 0..10 {
53+
example_captures();
54+
}
55+
}

src/lib.rs

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,97 @@
11
/*!
22
3-
This crate introduces the `regex!` macro which is a shortcut to write static lazily compiled regular expressions as is usually done with lazy_static or once_cell.
3+
Use the [regex!] macro to build regexes:
44
5-
It lets you replace
5+
* they're checked at compile time
6+
* they're wrapped in `once_cell` lazy static initializers so that they're compiled only once
7+
* they can hold flags with a familiar suffix syntax: `let case_insensitive_regex = regex!("ab*"i);`
8+
* regex creation is less verbose
69
10+
This macro builds normal instances of [regex::Regex] so all the usual features are available.
711
8-
```not-executable
9-
fn some_helper_function(text: &str) -> bool {
10-
lazy_static! {
11-
static ref RE: Regex = Regex::new("...").unwrap();
12-
}
13-
RE.is_match(text)
14-
}
12+
You may also use shortcut macros for testing a match or capturing groups as substrings:
1513
14+
* [regex_is_match!]
15+
* [regex_find!]
16+
* [regex_captures!]
17+
18+
# Build Regexes
19+
20+
```
21+
use lazy_regex::regex;
22+
23+
// build a simple regex
24+
let r = regex!("sa+$");
25+
assert_eq!(r.is_match("Saa"), false);
26+
27+
// build a regex with flag(s)
28+
let r = regex!("sa+$"i);
29+
assert_eq!(r.is_match("Saa"), true);
30+
31+
32+
// you can use a raw literal
33+
let r = regex!(r#"^"+$"#);
34+
assert_eq!(r.is_match("\"\""), true);
35+
36+
// or a raw literal with flag(s)
37+
let r = regex!(r#"^\s*("[a-t]*"\s*)+$"#i);
38+
assert_eq!(r.is_match(r#" "Aristote" "Platon" "#), true);
39+
40+
// this line wouldn't compile because the regex is invalid:
41+
// let r = regex!("(unclosed");
42+
43+
```
44+
Supported regex flags: 'i', 'm', 's', 'x', 'U'.
45+
46+
See [regex::RegexBuilder].
47+
48+
# Test a match
49+
50+
```
51+
use lazy_regex::regex_is_match;
52+
53+
let b = regex_is_match!("[ab]+", "car");
54+
assert_eq!(b, true);
55+
```
56+
57+
# Extract a value
58+
59+
```
60+
use lazy_regex::regex_find;
61+
62+
let f_word = regex_find!(r#"\bf\w+\b"#, "The fox jumps.").unwrap();
63+
assert_eq!(f_word, "fox");
1664
```
1765
18-
with
66+
# Capture
1967
68+
```
69+
use lazy_regex::regex_captures;
70+
71+
let (_, letter) = regex_captures!(r#"([a-z])\d+"#i, "form A42").unwrap();
72+
assert_eq!(letter, "A");
2073
21-
```not-executable
22-
fn some_helper_function(text: &str) -> bool {
23-
regex!("...").is_match(text)
24-
}
74+
let (whole, name, version) = regex_captures!(
75+
r#"(\w+)-([0-9.]+)"#, // a literal regex
76+
"This is lazy_regex-2.0!", // any expression
77+
).unwrap();
78+
assert_eq!(whole, "lazy_regex-2.0");
79+
assert_eq!(name, "lazy_regex");
80+
assert_eq!(version, "2.0");
2581
```
2682
83+
There's no limit to the size of the tupple.
84+
It's checked at compile time to ensure you have the right number of capturing groups.
85+
2786
*/
2887

29-
pub use once_cell;
88+
pub use {
89+
once_cell,
90+
proc_macros::{
91+
regex,
92+
regex_captures,
93+
regex_find,
94+
regex_is_match,
95+
},
96+
};
3097

31-
#[macro_export]
32-
macro_rules! regex {
33-
($s: literal) => {{
34-
use lazy_regex::once_cell::sync::OnceCell;
35-
static RE: OnceCell::<regex::Regex> = OnceCell::new();
36-
RE.get_or_init(|| regex::Regex::new($s).unwrap())
37-
}};
38-
}

src/proc_macros/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "lazy-regex-proc_macros"
3+
version = "2.0.0"
4+
authors = ["Canop <[email protected]>"]
5+
edition = "2018"
6+
7+
[dependencies]
8+
syn = "1.0"
9+
proc-macro2 = "1.0"
10+
quote = "1.0"
11+
regex = "1.4"
12+
13+
[lib]
14+
proc-macro = true
15+
path = "mod.rs"

0 commit comments

Comments
 (0)