├── reformation ├── tests │ ├── nested_record.txt │ ├── containers.rs │ ├── enum.rs │ └── struct.rs ├── src │ ├── containers.rs │ └── lib.rs ├── Cargo.toml ├── examples │ ├── enum_.rs │ └── derive.rs └── benches │ ├── generic_overhead.rs │ └── kirby_user_agent.rs ├── .gitignore ├── Cargo.toml ├── failing_examples ├── Cargo.toml └── src │ └── bin │ ├── generics.rs │ └── intentional_compiler_errors.rs ├── .travis.yml ├── reformation_derive ├── src │ ├── syn_helpers.rs │ ├── reformation_attribute.rs │ ├── format.rs │ └── lib.rs └── Cargo.toml ├── LICENSE ├── Changelog.md └── README.md /reformation/tests/nested_record.txt: -------------------------------------------------------------------------------- 1 | Apparitions 2 | Ghost: 1, 2, 3, 4, 5, 6, 7 3 | Specter: 3, 9 4 | Lost soul: 0, 334, 21 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /reformation_derive/target 3 | /reformation/target 4 | /failing_examples/target 5 | **/*.rs.bk 6 | /.idea 7 | reparse.iml 8 | Cargo.lock 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "reformation", 4 | "reformation_derive", 5 | ] 6 | 7 | default-members = [ 8 | "reformation", 9 | "reformation_derive", 10 | ] 11 | -------------------------------------------------------------------------------- /failing_examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "failing_examples" 3 | version = "0.1.0" 4 | authors = ["hukumka "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | reformation = {path="../reformation"} 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | rust: 4 | - stable 5 | - beta 6 | - nightly 7 | matrix: 8 | allow_failures: 9 | - rust: nightly 10 | script: 11 | - cargo build --verbose --all 12 | - cargo test --verbose --all 13 | - cd reformation_derive 14 | - cargo test --verbose --all -------------------------------------------------------------------------------- /reformation_derive/src/syn_helpers.rs: -------------------------------------------------------------------------------- 1 | use syn::{Expr, Lit}; 2 | 3 | pub fn expr_lit(x: &Expr) -> Option<&Lit> { 4 | if let Expr::Lit(ref i) = x { 5 | Some(&i.lit) 6 | } else { 7 | None 8 | } 9 | } 10 | 11 | pub fn expr_bool_lit(x: &Expr) -> Option { 12 | if let Some(Lit::Bool(ref c)) = expr_lit(x) { 13 | Some(c.value) 14 | } else { 15 | None 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /reformation_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reformation_derive" 3 | version = "0.5.2" 4 | authors = ["hukumka "] 5 | edition = "2018" 6 | description = "reformation derive crate" 7 | 8 | license = "MIT" 9 | repository = "https://github.com/hukumka/reformation" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | regex = "1.4" 16 | syn = "1.0" 17 | quote = "1.0" 18 | proc-macro2 = "1.0" 19 | lazy_static = "1.4" 20 | -------------------------------------------------------------------------------- /reformation/src/containers.rs: -------------------------------------------------------------------------------- 1 | use crate::ParseOverride; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | pub struct VecDelimited(pub Vec); 5 | 6 | impl<'a, T: ParseOverride<'a>, const SEP: &'static str> ParseOverride<'a> for VecDelimited{ 7 | fn parse_override(s: &'a str) -> Result{ 8 | let data: Result, _> = s.split(SEP) 9 | .map(|s| { 10 | println!("{:?}", s); 11 | T::parse_override(s) 12 | }) 13 | .collect(); 14 | Ok(Self(data?)) 15 | } 16 | } -------------------------------------------------------------------------------- /reformation/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "reformation" 3 | version = "0.5.3" 4 | authors = ["hukumka "] 5 | edition = "2018" 6 | description = "Parsing via regular expressions" 7 | 8 | readme = "../README.md" 9 | license = "MIT" 10 | repository = "https://github.com/hukumka/reformation" 11 | keywords = ["parsing", "regex"] 12 | categories = ["parsing"] 13 | 14 | [badges] 15 | travis-ci = { repository = "hukumka/reformation" } 16 | 17 | [features] 18 | containers = [] 19 | 20 | [dependencies] 21 | # pub 22 | once_cell = "1.5" 23 | regex = "1.4" 24 | # private 25 | reformation_derive = {path="../reformation_derive", version="0.5.2"} 26 | derive_more = "0.99" 27 | 28 | [dev-dependencies] 29 | criterion = "0.3" 30 | lazy_static = "1.4" 31 | 32 | [[bench]] 33 | name = "kirby_user_agent" 34 | harness = false 35 | 36 | [[bench]] 37 | name = "generic_overhead" 38 | harness = false 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Arnold Pavel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /reformation/tests/containers.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature="containers")] 2 | mod tests{ 3 | use reformation::containers::VecDelimited; 4 | use reformation::Reformation; 5 | 6 | #[derive(Reformation)] 7 | #[reformation(r"{header}\r\n{rows}")] 8 | struct Nested<'a>{ 9 | header: &'a str, 10 | #[reformation(r"(?s).*")] 11 | rows: VecDelimited, "\r\n"> 12 | } 13 | 14 | #[derive(Reformation, Debug, PartialEq)] 15 | #[reformation("{name}: {numbers}")] 16 | struct Record<'a>{ 17 | name: &'a str, 18 | #[reformation(r"[^\n]*")] 19 | numbers: VecDelimited, 20 | } 21 | 22 | #[test] 23 | fn test_nested_record(){ 24 | let data = include_str!("nested_record.txt"); 25 | let nested = Nested::parse(data).unwrap(); 26 | assert_eq!(nested.header, "Apparitions"); 27 | assert_eq!(nested.rows.0.len(), 3); 28 | let rows = nested.rows.0; 29 | assert_eq!(rows[0], Record{ 30 | name: "Ghost", 31 | numbers: VecDelimited(vec![1, 2, 3, 4, 5, 6, 7]) 32 | }); 33 | assert_eq!(rows[1], Record{ 34 | name: "Specter", 35 | numbers: VecDelimited(vec![3, 9]), 36 | }); 37 | assert_eq!(rows[2], Record{ 38 | name: "Lost soul", 39 | numbers: VecDelimited(vec![0, 334, 21]), 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /reformation/tests/enum.rs: -------------------------------------------------------------------------------- 1 | use reformation::Reformation; 2 | 3 | #[derive(Reformation, PartialEq, Debug)] 4 | enum A { 5 | #[reformation("variant1")] 6 | V1, 7 | #[reformation("variant2{}")] 8 | V2(usize), 9 | } 10 | 11 | #[derive(Reformation, PartialEq, Debug)] 12 | enum B { 13 | #[reformation("{}")] 14 | One(u8), 15 | #[reformation("{}..{}", no_regex = true)] 16 | Pair(u8, u8), 17 | 18 | // variant that cannot be produced by parse function 19 | #[allow(dead_code)] 20 | NeverParsed(Vec), 21 | } 22 | 23 | #[test] 24 | fn test_base_enum() -> Result<(), reformation::Error> { 25 | dbg!(A::regex_str()); 26 | let a = A::parse("variant1")?; 27 | assert_eq!(a, A::V1); 28 | let b = A::parse("variant259")?; 29 | assert_eq!(b, A::V2(59)); 30 | Ok(()) 31 | } 32 | 33 | #[test] 34 | fn test_enum_separation() -> Result<(), reformation::Error> { 35 | dbg!(B::regex_str()); 36 | let a = B::parse("8")?; 37 | assert_eq!(a, B::One(8)); 38 | let b = B::parse("1..88")?; 39 | assert_eq!(b, B::Pair(1, 88)); 40 | Ok(()) 41 | } 42 | 43 | #[derive(Reformation)] 44 | #[reformation(no_regex=true)] 45 | enum LifetimeInEnum<'a>{ 46 | #[reformation("A")] 47 | A, 48 | #[reformation("B({})")] 49 | B(usize), 50 | #[reformation("C({})")] 51 | C(&'a str), 52 | #[reformation("D({_value})")] 53 | D { 54 | _value: &'a str 55 | }, 56 | } 57 | 58 | -------------------------------------------------------------------------------- /failing_examples/src/bin/generics.rs: -------------------------------------------------------------------------------- 1 | use reformation::Reformation; 2 | 3 | // This example is expected to work, since type std::marker::Marker 4 | // implements default, but as macro bounds T: Reformation for all generated 5 | // methods, it does not compile. 6 | #[derive(Reformation)] 7 | #[reformation("{x}")] 8 | struct A { 9 | x: usize, 10 | _marker: std::marker::PhantomData, 11 | } 12 | 13 | #[derive(Reformation)] 14 | #[reformation(r"A\({x}\)")] 15 | struct A2 { 16 | x: T, 17 | } 18 | 19 | // Sometimes macro cannot determine correct trait bounds by itself. 20 | // In such sorrowful cases one can explicitly specify where clause 21 | // for generated trait implementations. 22 | // By default bound T: Reformation is put on every generic argument, 23 | // but this is not desired behaviour here. By using `override_where=""` 24 | // we remove such bound. 25 | #[derive(Reformation)] 26 | #[reformation("{x}", override_where = "")] 27 | struct B { 28 | x: usize, 29 | _marker: std::marker::PhantomData, 30 | } 31 | 32 | #[derive(Reformation)] 33 | #[reformation("{x}", override_where = "where T1: Reformation<'input>")] 34 | struct C { 35 | x: T1, 36 | y: Vec, // constructed with Default::default method 37 | } 38 | 39 | fn main() { 40 | let x = A2::::parse("A(32)").unwrap(); 41 | assert_eq!(x.x, 32); 42 | 43 | let y = B::>::parse("11").unwrap(); 44 | assert_eq!(y.x, 11); 45 | 46 | let z = C::::parse("44").unwrap(); 47 | assert_eq!(z.x, 44); 48 | assert_eq!(z.y, vec![]); 49 | 50 | let x: A> = "13".parse().unwrap(); 51 | } 52 | -------------------------------------------------------------------------------- /reformation/examples/enum_.rs: -------------------------------------------------------------------------------- 1 | use reformation::Reformation; 2 | 3 | #[derive(Reformation, Debug)] 4 | enum Color { 5 | #[reformation("red")] 6 | Red, 7 | #[reformation("green")] 8 | Green, 9 | #[reformation(r"grey\({}\)")] 10 | Grey(i32), 11 | #[reformation("yellow")] 12 | Yellow, 13 | #[reformation(r"blue={}, {}")] 14 | Blue(i32, i32), 15 | } 16 | 17 | #[derive(Reformation, Debug, PartialEq)] 18 | #[reformation(no_regex = true)] 19 | enum Superman { 20 | #[reformation("Bird({})")] 21 | Bird(f32), 22 | #[reformation("Plane({})")] 23 | Plane(f32), 24 | } 25 | 26 | #[derive(Reformation, Debug, PartialEq)] 27 | #[reformation(no_regex = true, slack = true)] 28 | enum MiniList { 29 | #[reformation("[]")] 30 | Zero, 31 | #[reformation("[{}]")] 32 | One(i32), 33 | #[reformation("[{}, {}]")] 34 | Two(i32, i32), 35 | #[reformation("[{}, {}, {}]")] 36 | Three(i32, i32, i32), 37 | } 38 | 39 | fn main() { 40 | let c = Color::parse("grey(64)").unwrap(); 41 | println!("{:?}", c); 42 | 43 | let c = Color::parse("blue=11, -23").unwrap(); 44 | println!("{:?}", c); 45 | 46 | let s = Superman::parse("Bird(-1.3)").unwrap(); 47 | assert_eq!(s, Superman::Bird(-1.3)); 48 | 49 | let l = MiniList::parse("[34]").unwrap(); 50 | assert_eq!(l, MiniList::One(34)); 51 | 52 | let l = MiniList::parse("[34,22]").unwrap(); 53 | assert_eq!(l, MiniList::Two(34, 22)); 54 | let l = MiniList::parse("[34, 22]").unwrap(); 55 | assert_eq!(l, MiniList::Two(34, 22)); 56 | 57 | let l = MiniList::parse("[]").unwrap(); 58 | assert_eq!(l, MiniList::Zero); 59 | } 60 | -------------------------------------------------------------------------------- /failing_examples/src/bin/intentional_compiler_errors.rs: -------------------------------------------------------------------------------- 1 | use reformation::Reformation; 2 | 3 | // Should not compile without reformation attribute 4 | #[derive(Reformation)] 5 | struct A; 6 | 7 | // Should not compile, since format string is not correct regular expression 8 | #[derive(Reformation)] 9 | #[reformation("(")] 10 | struct B; 11 | 12 | // Format string does not cover fields, which does not present in struct 13 | #[derive(Reformation)] 14 | #[reformation("{x}")] 15 | struct C {} 16 | 17 | // Format string features unnamed argument 18 | // TODO: figure out why span is broken 19 | #[derive(Reformation)] 20 | #[reformation("{}")] 21 | struct D { 22 | a: i32, 23 | } 24 | 25 | // Format string does not contain enough arguments 26 | #[derive(Reformation)] 27 | #[reformation("")] 28 | struct E(usize); 29 | 30 | // Format string must not contain named arguments 31 | #[derive(Reformation)] 32 | #[reformation("{x}")] 33 | struct F(usize); 34 | 35 | // Format string contains to many arguments 36 | #[derive(Reformation)] 37 | #[reformation("{} {}")] 38 | struct G(usize); 39 | 40 | // Format string does not cover all variants 41 | #[derive(Reformation)] 42 | #[reformation("(RED|GREEN)")] 43 | enum H { 44 | RED, 45 | GREEN, 46 | BLUE, 47 | } 48 | 49 | // Format string does not cover variant values 50 | #[derive(Reformation)] 51 | #[reformation("({})")] 52 | enum J { 53 | A(i32, i32), 54 | } 55 | 56 | #[derive(Reformation)] 57 | #[reformation("{a}")] 58 | struct K { 59 | #[reformation("(")] 60 | a: usize, 61 | } 62 | 63 | #[derive(Reformation)] 64 | #[reformation("{")] 65 | struct L { 66 | #[reformation("(")] 67 | a: usize, 68 | } 69 | 70 | fn main() {} 71 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### 0.5.3 2 | + Implement `Error` trait for `reformation::Error`. (credit: nertpinx) 3 | 4 | ### 0.5.2 5 | + Add `fromstr` mode to implement `FromStr` trait for struct. 6 | 7 | ### 0.5.1 8 | + Allow enums to have variant which cannot be parsed into. 9 | + Make regex override work for types implementing `ParseOverride` instead of `ReformationPrimitive`, 10 | removing latter, as its no longer needed. 11 | + Fixed buf with default not handled correctly 12 | 13 | ## 0.5.0 14 | + Remove old enum syntax. 15 | + Remove need for magical lifetime name and 16 | add support for multiple lifetimes for 17 | in place parsing. 18 | + Remove lazy_static public dependancy. 19 | + Add base for generics support. 20 | 21 | ### 0.4.1 22 | + Fix bug with enums not being put into group 23 | + Hide clippy lint on evaluation order 24 | 25 | ## 0.4.0 26 | + Change trait to support types containing references (such as &str) 27 | + Allow override of regular expression of primitive via attribute 28 | + Add benchmark to prove that it is zero-cost abstraction 29 | + Add per-variant regex declaration for enum 30 | 31 | ### 0.3.1 32 | + Fix failing test 33 | 34 | ### 0.3.0 35 | + Add compiler error for incorrect regex 36 | + Add compiler error for attempts to use named capture groups 37 | + Add compiler error for cases then format string does not match struct/enum declaration. 38 | 39 | ### 0.2.3 40 | + Add slack mode 41 | + Implement Reformation for char 42 | + Enum support 43 | + Tuple struct support 44 | 45 | ### 0.2.2 46 | + Auto replacement of capturing groups with non-capturing 47 | + Allow to omit items implementing default trait from format string 48 | + Add no_regex mode 49 | 50 | ### 0.2.1 51 | + Now able to use derived struct as build brick for other structs 52 | + Fixed macro naming for correct import 53 | 54 | ## 0.2.0 55 | + Changed Reformation trait signature 56 | + Add derive macro 57 | 58 | ## 0.1.0 59 | + Add simplest macro capable of some parsings. No derive yet 60 | -------------------------------------------------------------------------------- /reformation/examples/derive.rs: -------------------------------------------------------------------------------- 1 | extern crate reformation; 2 | 3 | use reformation::Reformation; 4 | 5 | #[derive(Debug, Reformation)] 6 | #[reformation(r"Vec\{{{x}, {y}, {z}\}}", slack = true)] 7 | struct Vec { 8 | x: f32, 9 | y: f32, 10 | z: f32, 11 | } 12 | 13 | // note that capture group (,|;) is replaced with non-capturing (:?,|;) in order 14 | // to avoid accidental break of expression. Note that named capture groups 15 | // (?Pexpr) will still cause logical error and hopefully panic. 16 | #[derive(Debug, Reformation)] 17 | #[reformation(r"Rect\{{{a}(,|;)\s+{b}\}}")] 18 | struct Rect { 19 | a: Vec, 20 | b: Vec, 21 | 22 | // Note what zero does not appear in format string, but 23 | // initialized from `Default` trait implementation 24 | zero: usize, 25 | } 26 | 27 | // One may choose to use plain format syntax (with no regular expressions) 28 | // by providing `no_regex=true` mode 29 | #[derive(Debug, Reformation)] 30 | #[reformation(r"(\|)({left_eye}_{right_eye})(|/)", no_regex = true)] 31 | struct Crab { 32 | left_eye: String, 33 | right_eye: String, 34 | } 35 | 36 | #[derive(Debug, Reformation)] 37 | #[reformation(r"hm( vec might be here: {})?")] 38 | struct Ovec(Option); 39 | 40 | fn main() { 41 | let a = Vec::parse("Vec{1, 2, 3}").unwrap(); 42 | println!("{:?}", a); 43 | 44 | // Even through such structs can be combined, but do not overuse it, since it will produce horrific regular expressions 45 | println!("{:?}", Rect::regex_str()); 46 | 47 | // Vec regex is in slack mode, allowing arbitrary amount of spaces after coma. 48 | let r = Rect::parse("Rect{Vec{1, 1, 0}; Vec{-3.e-5, 0.03,3}}").unwrap(); 49 | println!("{:?}", r); 50 | 51 | let pirate_crab = Crab::parse(r"(\|)(x_^)(|/)").unwrap(); 52 | println!("{:?}", pirate_crab); 53 | 54 | let b = Ovec::parse("hm vec might be here: Vec{0, 0, 0}").unwrap(); 55 | println!("{:?}", b); 56 | 57 | let c = Ovec::parse("hm").unwrap(); 58 | println!("{:?}", c); 59 | } 60 | -------------------------------------------------------------------------------- /reformation/benches/generic_overhead.rs: -------------------------------------------------------------------------------- 1 | /// Since static variables created for one per lexical function: 2 | /// ``` 3 | /// fn gen(){ 4 | /// static X: i32 = 0; 5 | /// } 6 | /// // gen:: and gen:: will share same static variable 7 | /// ``` 8 | /// instance of regex automata is held inside `HashMap` protected by 9 | /// `RwLock`. 10 | /// 11 | /// This creates overhead for accessing it in each call of `Reformation::parse` 12 | /// 13 | /// But, on the other hand, wrapping generic structure inside concrete one will 14 | /// eliminate this overhead, since `reformation` constructs regex automation 15 | /// on per struct basis, which means `AIntErase::parse` never uses automation 16 | /// of `AInt::`. Also, unless `AInt::::parse` is called at least once 17 | /// such automation is never constructed. 18 | /// 19 | /// This benchmark aims to give general idea of how much overhead it could cause. 20 | #[macro_use] 21 | extern crate criterion; 22 | 23 | use criterion::{Criterion, Fun}; 24 | use reformation::Reformation; 25 | 26 | 27 | // This struct being strait up parsed 28 | #[derive(Reformation, Debug, PartialEq)] 29 | #[reformation("{value}")] 30 | struct AInt{ 31 | value: i32 32 | } 33 | 34 | #[derive(Reformation, Debug, PartialEq)] 35 | #[reformation("{value}")] 36 | struct A{ 37 | value: T 38 | } 39 | 40 | // Erase generic to remove overhead 41 | #[derive(Reformation, Debug, PartialEq)] 42 | #[reformation("{}")] 43 | struct AIntErase(A); 44 | 45 | fn parse(inputs: &Vec<&str>) { 46 | for i in inputs{ 47 | criterion::black_box(AInt::parse(i).unwrap()); 48 | } 49 | } 50 | 51 | fn parse_generic(inputs: &Vec<&str>) { 52 | for i in inputs{ 53 | criterion::black_box(A::::parse(i).unwrap()); 54 | } 55 | } 56 | 57 | fn parse_generic_erased(inputs: &Vec<&str>) { 58 | for i in inputs{ 59 | criterion::black_box(AIntErase::parse(i).unwrap()); 60 | } 61 | } 62 | 63 | fn compare(c: &mut Criterion) { 64 | let inputs = vec!["134", "-13", "9999932", "0", "3"]; 65 | let a_i32 = Fun::new("parse_int", |b, i| b.iter(|| parse(i))); 66 | let a_gen = Fun::new("parse::", |b, i| b.iter(|| parse_generic(i))); 67 | let a_gen_erased = Fun::new("parse:: with erased generic", |b, i| b.iter(|| parse_generic_erased(i))); 68 | c.bench_functions("reformation parse gen", vec![a_i32, a_gen, a_gen_erased], inputs); 69 | } 70 | 71 | criterion_group!(benches, compare); 72 | criterion_main!(benches); 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/hukumka/reformation.svg?branch=master)](https://travis-ci.org/hukumka/reformation) 2 | 3 | # reformation 4 | 5 | Parsing via regular expressions using format syntax 6 | 7 | Derive will require attribute reformation to specify format string, 8 | which will be treated as format string -> regular expression string 9 | 10 | Types implementing `Reformation` by default: 11 | 12 | + signed integers: `i8` `i16` `i32` `i64` `i128` `isize` 13 | + unsigned integers: `u8` `u16` `u32` `u64` `u128` `usize` 14 | + floats: `f32` `f64` 15 | + `String`, &str 16 | + `char` 17 | 18 | ### Structs 19 | 20 | ```rust 21 | use reformation::Reformation; 22 | 23 | #[derive(Reformation, Debug)] 24 | #[reformation(r"{year}-{month}-{day} {hour}:{minute}")] 25 | struct Date{ 26 | year: u16, 27 | month: u8, 28 | day: u8, 29 | hour: u8, 30 | minute: u8, 31 | } 32 | 33 | fn main(){ 34 | let date = Date::parse("2018-12-22 20:23").unwrap(); 35 | 36 | assert_eq!(date.year, 2018); 37 | assert_eq!(date.month, 12); 38 | assert_eq!(date.day, 22); 39 | assert_eq!(date.hour, 20); 40 | assert_eq!(date.minute, 23); 41 | } 42 | ``` 43 | 44 | ### Tuple Structs 45 | 46 | ```rust 47 | use reformation::Reformation; 48 | 49 | #[derive(Reformation)] 50 | #[reformation(r"{} -> {}")] 51 | struct Predicate(Empty, char); 52 | 53 | #[derive(Reformation, Debug, PartialEq)] 54 | #[reformation(r"Empty")] 55 | struct Empty; 56 | 57 | fn main(){ 58 | let p = Predicate::parse("Empty -> X").unwrap(); 59 | assert_eq!(p.0, Empty); 60 | assert_eq!(p.1, 'X'); 61 | } 62 | ``` 63 | 64 | ### Enums 65 | ```rust 66 | use reformation::Reformation; 67 | 68 | #[derive(Reformation, Eq, PartialEq, Debug)] 69 | enum Ant{ 70 | #[reformation(r"Queen\({}\)")] 71 | Queen(String), 72 | #[reformation(r"Worker\({}\)")] 73 | Worker(i32), 74 | #[reformation(r"Warrior")] 75 | Warrior 76 | } 77 | 78 | fn main(){ 79 | let queen = Ant::parse("Queen(We are swarm)").unwrap(); 80 | assert_eq!(queen, Ant::Queen("We are swarm".to_string())); 81 | 82 | let worker = Ant::parse("Worker(900000)").unwrap(); 83 | assert_eq!(worker, Ant::Worker(900000)); 84 | 85 | let warrior = Ant::parse("Warrior").unwrap(); 86 | assert_eq!(warrior, Ant::Warrior); 87 | } 88 | ``` 89 | 90 | ### In place parsing 91 | ```rust 92 | use reformation::Reformation; 93 | 94 | #[derive(Reformation, Eq, PartialEq, Debug)] 95 | #[reformation("{a} {b}")] 96 | struct InPlace<'a, 'b>{ 97 | #[reformation("[a-z]*")] 98 | a: &'a str, 99 | #[reformation("[a-z]*")] 100 | b: &'b str, 101 | } 102 | 103 | fn main(){ 104 | // Then parsed from &'x str value will have type 105 | // InPlace<'x, 'x> 106 | let inplace = InPlace::parse("aval bval").unwrap(); 107 | assert_eq!(inplace, InPlace{a: "aval", b: "bval"}) 108 | } 109 | ``` 110 | 111 | ### Modes 112 | 113 | Order, in which modes are specified does not matter. 114 | 115 | #### fromstr 116 | 117 | Generate implementation of `FromStr` trait. 118 | 119 | Not compatible with lifetime annotated structs. 120 | 121 | ```rust 122 | use reformation::Reformation; 123 | 124 | #[derive(Reformation, Debug)] 125 | #[reformation(r"{year}-{month}-{day} {hour}:{minute}", fromstr = true)] 126 | struct Date{ 127 | year: u16, 128 | month: u8, 129 | day: u8, 130 | hour: u8, 131 | minute: u8, 132 | } 133 | 134 | fn main() -> Result<(), Box> { 135 | let date: Date = "2018-12-22 20:23".parse()?; 136 | 137 | assert_eq!(date.year, 2018); 138 | assert_eq!(date.month, 12); 139 | assert_eq!(date.day, 22); 140 | assert_eq!(date.hour, 20); 141 | assert_eq!(date.minute, 23); 142 | 143 | Ok(()) 144 | } 145 | ``` 146 | 147 | #### no_regex 148 | 149 | Makes format string behave as regular string (in contrast with being regular expression), 150 | by escaping all special regex characters. 151 | 152 | ```rust 153 | use reformation::Reformation; 154 | 155 | #[derive(Reformation, Debug)] 156 | #[reformation("Vec{{{x}, {y}}}", no_regex=true)] 157 | struct Vec{ 158 | x: i32, 159 | y: i32, 160 | } 161 | 162 | fn main(){ 163 | let v= Vec::parse("Vec{-1, 1}").unwrap(); 164 | assert_eq!(v.x, -1); 165 | assert_eq!(v.y, 1); 166 | } 167 | ``` 168 | 169 | #### slack 170 | 171 | Allow arbitrary number of spaces after separators: ',', ';', ':'. For separator to be recognized 172 | as slack, it must be followed by at least one space in format string. 173 | 174 | ```rust 175 | use reformation::Reformation; 176 | 177 | #[derive(Reformation, Debug)] 178 | #[reformation(r"Vec\{{{x}, {y}\}}", slack=true)] 179 | struct Vec{ 180 | x: i32, 181 | y: i32, 182 | } 183 | 184 | fn main(){ 185 | let v = Vec::parse("Vec{-1,1}").unwrap(); 186 | assert_eq!(v.x, -1); 187 | assert_eq!(v.y, 1); 188 | 189 | let r = Vec::parse("Vec{15, 2}").unwrap(); 190 | assert_eq!(r.x, 15); 191 | assert_eq!(r.y, 2); 192 | } 193 | ``` 194 | 195 | License: MIT 196 | -------------------------------------------------------------------------------- /reformation/tests/struct.rs: -------------------------------------------------------------------------------- 1 | use reformation::{Error, ParseOverride, Reformation}; 2 | 3 | #[derive(Debug, Reformation, PartialEq)] 4 | #[reformation(r"Vec\{{{x}, {y}, {z}\}}", slack = true)] 5 | struct Vect { 6 | x: f32, 7 | y: f32, 8 | z: f32, 9 | } 10 | 11 | #[test] 12 | fn test_vec() { 13 | let real = Vect::parse("Vec{1, 2, 3}"); 14 | let expected = Vect { 15 | x: 1.0, 16 | y: 2.0, 17 | z: 3.0, 18 | }; 19 | assert_eq!(real, Ok(expected)); 20 | } 21 | 22 | // note that capture group (,|;) is replaced with non-capturing (:?,|;) in order 23 | // to avoid accidental break of expression. Note that named capture groups 24 | // (?Pexpr) will still cause logical error and hopefully panic. 25 | #[derive(Debug, Reformation, PartialEq)] 26 | #[reformation(r"Rect\{{{a}(,|;)\s+{b}\}}")] 27 | struct Rect { 28 | a: Vect, 29 | b: Vect, 30 | 31 | // Note what zero does not appear in format string, but 32 | // initialized from `Default` trait implementation 33 | zero: usize, 34 | } 35 | 36 | #[test] 37 | fn test_rect() { 38 | let real = Rect::parse("Rect{Vec{1, 1, 0}; Vec{-3.e-5, 0.03,3}}"); 39 | let expected = Rect { 40 | a: Vect { 41 | x: 1.0, 42 | y: 1.0, 43 | z: 0.0, 44 | }, 45 | b: Vect { 46 | x: -3.0e-5, 47 | y: 0.03, 48 | z: 3.0, 49 | }, 50 | zero: 0, 51 | }; 52 | assert_eq!(real, Ok(expected)); 53 | } 54 | 55 | #[derive(Reformation, PartialEq, Debug)] 56 | #[reformation("{b}, {a}")] 57 | struct Order { 58 | a: i32, 59 | b: i32, 60 | } 61 | 62 | #[test] 63 | fn test_order() { 64 | let real = Order::parse("1, 3"); 65 | let expected = Order { a: 3, b: 1 }; 66 | assert_eq!(real, Ok(expected)); 67 | } 68 | 69 | #[derive(Reformation, PartialEq, Debug)] 70 | #[reformation("unit")] 71 | struct Unit; 72 | 73 | #[derive(Reformation, PartialEq, Debug)] 74 | #[reformation("{a}, {b}")] 75 | struct InPlace<'a, 'b> { 76 | a: &'a str, 77 | b: &'b str, 78 | } 79 | 80 | #[derive(Reformation, PartialEq, Debug)] 81 | #[reformation("{}, {}")] 82 | struct Generic(T, Option); 83 | 84 | #[test] 85 | fn in_place() { 86 | let ab = InPlace::parse("wqr, asdfg"); 87 | assert_eq!( 88 | ab, 89 | Ok(InPlace { 90 | a: "wqr", 91 | b: "asdfg" 92 | }) 93 | ) 94 | } 95 | 96 | #[test] 97 | fn test_unit() { 98 | let u = Unit::parse("unit"); 99 | assert_eq!(u, Ok(Unit)); 100 | let u = Unit::parse("u"); 101 | assert!(u.is_err()); 102 | } 103 | 104 | #[test] 105 | fn test_generic() { 106 | let a = Generic::::parse("12, "); 107 | assert_eq!(a, Ok(Generic::(12, None))); 108 | let a = Generic::::parse("stringoo, strongo"); 109 | assert_eq!( 110 | a, 111 | Ok(Generic::( 112 | "stringoo".to_string(), 113 | Some("strongo".to_string()) 114 | )) 115 | ); 116 | } 117 | 118 | #[derive(Reformation, PartialEq, Debug)] 119 | #[reformation("{a} {b}")] 120 | struct Override { 121 | #[reformation(r".")] 122 | a: i32, 123 | 124 | #[reformation(r"\d( \d)*")] 125 | b: VecWrapper, 126 | } 127 | 128 | #[derive(Debug, PartialEq)] 129 | struct VecWrapper(Vec); 130 | 131 | impl<'a> ParseOverride<'a> for VecWrapper { 132 | fn parse_override(s: &'a str) -> Result { 133 | let v: Result, Error> = s 134 | .split_whitespace() 135 | .map(|i| i.parse::().map_err(|e| Error::Other(e.to_string()))) 136 | .collect(); 137 | v.map(|v| VecWrapper(v)) 138 | } 139 | } 140 | 141 | #[test] 142 | fn test_override() { 143 | let a = Override::parse("13 1"); 144 | assert_eq!(r"(.) (\d(?: \d)*)", Override::regex_str()); 145 | if a.is_ok() { 146 | panic!("{:?}", a) 147 | } 148 | let b = Override::parse("1 2 3 9 0"); 149 | assert_eq!( 150 | b, 151 | Ok(Override { 152 | a: 1, 153 | b: VecWrapper(vec![2, 3, 9, 0]) 154 | }) 155 | ); 156 | } 157 | 158 | #[derive(Reformation, Debug, PartialEq)] 159 | #[reformation("{a}: {c}")] 160 | struct ManyDefaults { 161 | a: i32, 162 | b: i32, 163 | c: i32, 164 | d: i32, 165 | e: i32, 166 | } 167 | 168 | #[test] 169 | fn test_default() { 170 | let x = ManyDefaults::parse("1: 2").unwrap(); 171 | assert_eq!( 172 | x, 173 | ManyDefaults { 174 | a: 1, 175 | b: 0, 176 | c: 2, 177 | d: 0, 178 | e: 0, 179 | } 180 | ) 181 | } 182 | 183 | #[derive(Reformation, Debug, PartialEq)] 184 | #[reformation("{}:{}:{}", fromstr = true)] 185 | struct Time(i32, i32, i32); 186 | 187 | #[test] 188 | fn test_fromstr() { 189 | let x: Time = "1:2:3".parse().unwrap(); 190 | assert_eq!(x, Time(1, 2, 3)); 191 | } 192 | 193 | #[derive(Reformation, Debug, PartialEq)] 194 | #[reformation("$", no_regex = true)] 195 | struct Dollar; 196 | 197 | #[test] 198 | fn test_dollar() { 199 | let x = Dollar::parse("$").unwrap(); 200 | assert_eq!(x, Dollar); 201 | } 202 | -------------------------------------------------------------------------------- /reformation_derive/src/reformation_attribute.rs: -------------------------------------------------------------------------------- 1 | use crate::syn_helpers::expr_bool_lit; 2 | use lazy_static::lazy_static; 3 | use proc_macro2::Span; 4 | use regex::{escape, Regex}; 5 | use syn::parse::{Parse, ParseStream}; 6 | use syn::punctuated::Punctuated; 7 | use syn::{AttrStyle, Attribute, Expr, Ident, Lit}; 8 | 9 | /// Struct representing attribute `#[reformation(...)]` 10 | #[derive(Clone)] 11 | pub struct ReformationAttribute { 12 | pub span: Span, 13 | pub regex_string: Option, 14 | 15 | pub slack: bool, 16 | pub no_regex: bool, 17 | pub fromstr: bool, 18 | } 19 | 20 | impl ReformationAttribute { 21 | fn new(span: Span) -> Self { 22 | Self { 23 | span, 24 | regex_string: None, 25 | slack: false, 26 | no_regex: false, 27 | fromstr: false, 28 | } 29 | } 30 | 31 | pub fn combine(&self, other: &Self) -> Self { 32 | let s = other 33 | .regex_string 34 | .clone() 35 | .or_else(|| self.regex_string.clone()); 36 | Self { 37 | span: other.span, 38 | regex_string: s, 39 | fromstr: false, // Only makes sence on top level #TODO 40 | slack: other.slack | self.slack, 41 | no_regex: other.no_regex | self.no_regex, 42 | } 43 | } 44 | 45 | pub fn substr_mode(&self) -> impl Fn(&str) -> String + '_ { 46 | move |s| { 47 | let s = if self.no_regex { 48 | escape(s) 49 | } else { 50 | no_capturing_groups(s) 51 | }; 52 | if self.slack { 53 | slack(&s) 54 | } else { 55 | s 56 | } 57 | } 58 | } 59 | 60 | /// Parse ReformationAttribute from set of attributes on DeriveInput 61 | pub fn parse(span: Span, attrs: &[Attribute]) -> syn::Result { 62 | let attr = if let Some(a) = Self::find_attribute(attrs) { 63 | a 64 | } else { 65 | return Ok(Self::new(span)); 66 | }; 67 | let tts = &attr.tokens; 68 | let stream_str = quote!(#tts).to_string(); 69 | let res: Self = syn::parse_str(&stream_str).map_err(|e| syn::Error::new(span, e))?; 70 | Ok(res) 71 | } 72 | 73 | pub fn find_attribute(attrs: &[Attribute]) -> Option<&Attribute> { 74 | attrs.iter().find(|a| is_reformation_attr(a)) 75 | } 76 | 77 | pub fn regex(&self) -> syn::Result<&str> { 78 | self.regex_string 79 | .as_ref() 80 | .map(|s| s.as_str()) 81 | .ok_or_else(|| syn::Error::new(self.span, "No format string specified by attribute")) 82 | } 83 | } 84 | 85 | impl Parse for ReformationAttribute { 86 | fn parse(input: ParseStream) -> syn::Result { 87 | let content; 88 | parenthesized!(content in input); 89 | let params: Punctuated = content.parse_terminated(Mode::parse)?; 90 | let mut res = Self::new(Span::call_site()); 91 | for mode in params { 92 | res.apply(mode)?; 93 | } 94 | Ok(res) 95 | } 96 | } 97 | 98 | enum Mode { 99 | Str(String), 100 | BoolParam(Ident), 101 | } 102 | 103 | impl Parse for Mode { 104 | fn parse(input: ParseStream) -> syn::Result { 105 | let lookahead = input.lookahead1(); 106 | if lookahead.peek(Ident) { 107 | let ident: Ident = input.parse()?; 108 | let _eq: Token![=] = input.parse()?; 109 | let true_: Expr = input.parse()?; 110 | expect_true(ident.span(), &ident.to_string(), &true_)?; 111 | Ok(Mode::BoolParam(ident)) 112 | } else { 113 | let regex: Lit = input.parse()?; 114 | match regex { 115 | Lit::Str(s) => Ok(Mode::Str(s.value())), 116 | _ => Err(syn::Error::new_spanned(regex, "Expected string literal.")), 117 | } 118 | } 119 | } 120 | } 121 | 122 | impl ReformationAttribute { 123 | fn apply(&mut self, mode: Mode) -> syn::Result<()> { 124 | match mode { 125 | Mode::BoolParam(ident) => match ident.to_string().as_str() { 126 | "no_regex" => { 127 | self.no_regex = true; 128 | Ok(()) 129 | } 130 | "slack" => { 131 | self.slack = true; 132 | Ok(()) 133 | } 134 | "fromstr" => { 135 | self.fromstr = true; 136 | Ok(()) 137 | } 138 | _ => Err(syn::Error::new( 139 | self.span, 140 | format!("Unknown mode: {:?}", &ident.to_string()), 141 | )), 142 | }, 143 | Mode::Str(s) => { 144 | self.regex_string = Some(s); 145 | Ok(()) 146 | } 147 | } 148 | } 149 | } 150 | 151 | fn expect_true(span: Span, name: &str, value: &Expr) -> syn::Result<()> { 152 | if expr_bool_lit(value) != Some(true) { 153 | Err(syn::Error::new( 154 | span, 155 | format!( 156 | "Error expected `true` for mode `{}`, found `{}`", 157 | name, 158 | quote! {value}.to_string() 159 | ), 160 | )) 161 | } else { 162 | Ok(()) 163 | } 164 | } 165 | 166 | fn is_reformation_attr(a: &Attribute) -> bool { 167 | let pound = &a.pound_token; 168 | let path = &a.path; 169 | let style_cmp = match a.style { 170 | AttrStyle::Outer => true, 171 | _ => false, 172 | }; 173 | quote!(#pound).to_string() == "#" && style_cmp && quote!(#path).to_string() == "reformation" 174 | } 175 | 176 | fn no_capturing_groups(input: &str) -> String { 177 | let mut prev = None; 178 | let mut res = String::new(); 179 | let mut iter = input.chars().peekable(); 180 | while let Some(c) = iter.next() { 181 | if prev != Some('\\') && c == '(' && iter.peek() != Some(&'?') { 182 | res.push_str("(?:"); 183 | } else { 184 | res.push(c); 185 | } 186 | prev = Some(c); 187 | } 188 | res 189 | } 190 | 191 | fn slack(input: &str) -> String { 192 | lazy_static! { 193 | static ref RE: Regex = Regex::new(r"([,:;])\s+").unwrap(); 194 | } 195 | RE.replace_all(input, r"$1\s*").to_string() 196 | } 197 | -------------------------------------------------------------------------------- /reformation_derive/src/format.rs: -------------------------------------------------------------------------------- 1 | use std::iter::Peekable; 2 | use std::str::CharIndices; 3 | 4 | use std::error::Error; 5 | use std::fmt; 6 | 7 | #[derive(Debug)] 8 | pub struct Format { 9 | substrings: Vec, 10 | arguments: Vec, 11 | } 12 | 13 | #[derive(Debug, Eq, PartialEq)] 14 | enum Argument { 15 | Positional(usize), 16 | Named(String), 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum FormatError { 21 | NoClosing, 22 | NoOpening(usize), 23 | } 24 | 25 | impl fmt::Display for FormatError { 26 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 27 | match self { 28 | FormatError::NoClosing => { 29 | write!(f, "FormatError: Bracket was opened, but never closed") 30 | } 31 | FormatError::NoOpening(i) => write!( 32 | f, 33 | "FormatError: Bracket was closed at {}, but no matching opening bracket found", 34 | i 35 | ), 36 | } 37 | } 38 | } 39 | 40 | impl Error for FormatError {} 41 | 42 | struct FormatBuilder<'a> { 43 | format: Format, 44 | string: &'a str, 45 | iter: Peekable>, 46 | positional_count: usize, 47 | } 48 | 49 | impl Format { 50 | pub fn build(string: &str) -> Result { 51 | let mut res = FormatBuilder::new(string).build()?; 52 | res.map_substrings(|x| x.replace("{{", "{").replace("}}", "}")); 53 | Ok(res) 54 | } 55 | 56 | pub fn no_arguments(&self) -> bool { 57 | self.arguments.is_empty() 58 | } 59 | 60 | pub fn map_substrings String>(&mut self, map: T) { 61 | for i in &mut self.substrings { 62 | *i = map(&i); 63 | } 64 | } 65 | 66 | pub fn named_arguments<'a>(&'a self) -> impl Iterator + 'a { 67 | self.arguments.iter().filter_map(|a| match a { 68 | Argument::Named(s) => Some(s.clone()), 69 | _ => None, 70 | }) 71 | } 72 | 73 | pub fn positional_arguments(&self) -> usize { 74 | self.arguments 75 | .iter() 76 | .filter_map(|a| match a { 77 | Argument::Positional(_) => Some(()), 78 | _ => None, 79 | }) 80 | .count() 81 | } 82 | 83 | pub fn linearize(&self) -> String { 84 | let escape = |s: &str| s.replace("{", "{{").replace("}", "}}"); 85 | let mut res = String::new(); 86 | for (s, _) in self.substrings.iter().zip(&self.arguments) { 87 | res.push_str(&escape(s)); 88 | res.push_str("{}"); 89 | } 90 | res.push_str(&escape(self.substrings.last().unwrap())); 91 | res 92 | } 93 | } 94 | 95 | impl ToString for Argument { 96 | #[inline] 97 | fn to_string(&self) -> String { 98 | match self { 99 | Argument::Named(s) => format!("{{{}}}", s), 100 | Argument::Positional(_) => "{}".to_string(), 101 | } 102 | } 103 | } 104 | 105 | impl<'a> FormatBuilder<'a> { 106 | fn new(string: &'a str) -> Self { 107 | Self { 108 | format: Format { 109 | substrings: vec![], 110 | arguments: vec![], 111 | }, 112 | string, 113 | iter: string.char_indices().peekable(), 114 | positional_count: 0, 115 | } 116 | } 117 | 118 | fn build(mut self) -> Result { 119 | let mut substr_start = 0; 120 | while let Some((i, c)) = self.iter.next() { 121 | let is_argument = c == '{' 122 | && self 123 | .iter 124 | .peek() // check if '{' is not part of "{{" escaping 125 | .map(|(_, c)| c) 126 | != Some(&'{'); 127 | 128 | let is_closing = c == '}' 129 | && self 130 | .iter 131 | .peek() // check if '{' is not part of "{{" escaping 132 | .map(|(_, c)| c) 133 | != Some(&'}'); 134 | 135 | if is_argument { 136 | let substr = self.string.get(substr_start..i).unwrap().to_string(); 137 | self.format.substrings.push(substr); 138 | let arg = self.parse_argument()?; 139 | self.format.arguments.push(arg); 140 | 141 | if let Some((i, _)) = self.iter.peek() { 142 | substr_start = *i; 143 | } else { 144 | substr_start = self.string.len(); 145 | } 146 | } else if is_closing { 147 | return Err(FormatError::NoOpening(i)); 148 | } else if c == '{' || c == '}' { 149 | self.iter.next(); 150 | } 151 | } 152 | let last_substr = self.string.get(substr_start..).unwrap().to_string(); 153 | self.format.substrings.push(last_substr); 154 | 155 | Ok(self.format) 156 | } 157 | 158 | fn parse_argument(&mut self) -> Result { 159 | let start; 160 | if let Some((i, c)) = self.iter.next() { 161 | start = i; 162 | if c == '}' { 163 | // positional argument 164 | let res = Argument::Positional(self.positional_count); 165 | self.positional_count += 1; 166 | return Ok(res); 167 | } 168 | } else { 169 | return Err(FormatError::NoClosing); 170 | } 171 | 172 | let end = self 173 | .iter 174 | .find(|(_, x)| x == &'}') 175 | .ok_or_else(|| FormatError::NoClosing)? 176 | .0; 177 | 178 | let name = self.string.get(start..end).unwrap().to_string(); 179 | Ok(Argument::Named(name)) 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | mod tests { 185 | use super::*; 186 | 187 | #[test] 188 | fn test_format_new() { 189 | let f = Format::build("a = {}, b = {}").unwrap(); 190 | assert_eq!(f.substrings, &["a = ", ", b = ", ""]); 191 | assert_eq!( 192 | f.arguments, 193 | &[Argument::Positional(0), Argument::Positional(1),] 194 | ); 195 | 196 | let f = Format::build("Vec{{ {x}, {}, {z}, {}}}").unwrap(); 197 | assert_eq!(f.substrings, &["Vec{ ", ", ", ", ", ", ", "}"]); 198 | assert_eq!( 199 | f.arguments, 200 | &[ 201 | Argument::Named("x".to_string()), 202 | Argument::Positional(0), 203 | Argument::Named("z".to_string()), 204 | Argument::Positional(1), 205 | ] 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /reformation/benches/kirby_user_agent.rs: -------------------------------------------------------------------------------- 1 | // Comparison of original kirby (https://github.com/rubytogether/kirby) user agent parsing 2 | // and it reimplementation using reformation. 3 | // Copyright notice: https://github.com/rubytogether/kirby/blob/main/LICENSE 4 | 5 | #[macro_use] 6 | extern crate criterion; 7 | 8 | use criterion::{Criterion, Fun}; 9 | use lazy_static::lazy_static; 10 | use reformation::Reformation; 11 | use regex::Regex; 12 | 13 | fn parse_re(s: &str) -> Option { 14 | if let Ok(u) = UserAgent1::parse(s) { 15 | Some(UserAgent { 16 | bundler: Some(u.bundler.0), 17 | rubygems: u.rubygems.0, 18 | ruby: Some(u.ruby.0), 19 | platform: Some(u.platform), 20 | command: Some(u.command), 21 | jruby: u.jruby.map(|x| x.0), 22 | truffleruby: u.truffleruby.map(|x| x.0), 23 | options: u.options, 24 | ci: u.ci, 25 | gemstash: u.gemstash.map(|x| x.0), 26 | }) 27 | } else if let Ok(u) = UserAgent2::parse(s) { 28 | Some(UserAgent { 29 | rubygems: u.rubygems.0, 30 | ruby: Some(u.ruby.0), 31 | platform: Some(u.platform), 32 | gemstash: u.gemstash.map(|x| x.0), 33 | ..Default::default() 34 | }) 35 | } else if let Ok(u) = UserAgent3::parse(s) { 36 | Some(UserAgent { 37 | rubygems: u.rubygems.0, 38 | ..Default::default() 39 | }) 40 | } else { 41 | None 42 | } 43 | } 44 | 45 | #[derive(Reformation)] 46 | #[reformation( 47 | "\ 48 | bundler/{bundler} rubygems/{rubygems} ruby/{ruby} \\({platform}\\) command/{command}\ 49 | ( jruby/{jruby})?( truffleruby/{truffleruby})?( options/{options})?( ci/{ci})? \ 50 | [a-f0-9]{{16}}( Gemstash/{gemstash})?\ 51 | " 52 | )] 53 | struct UserAgent1<'a> { 54 | bundler: Version<'a>, 55 | rubygems: Version<'a>, 56 | ruby: Version<'a>, 57 | 58 | #[reformation(r"[^)]*")] // use regex ([^)]*) to parse platform field instead of standard one. 59 | platform: &'a str, 60 | 61 | command: &'a str, 62 | jruby: Option>, 63 | truffleruby: Option>, 64 | options: Option<&'a str>, 65 | ci: Option<&'a str>, 66 | gemstash: Option>, 67 | } 68 | 69 | #[derive(Reformation)] 70 | #[reformation(r"(Ruby, )?RubyGems/{rubygems} {platform} Ruby/{ruby} \(.*?\)( jruby| truffleruby| rbx)?( Gemstash/{gemstash})?")] 71 | struct UserAgent2<'a> { 72 | rubygems: Version<'a>, 73 | ruby: Version<'a>, 74 | #[reformation(".*")] 75 | // only to produce identical regex as kirby. Removing it actually result in slight performance improvement 76 | platform: &'a str, 77 | gemstash: Option>, 78 | } 79 | 80 | #[derive(Reformation)] 81 | #[reformation(r"Ruby, Gems {rubygems}")] 82 | struct UserAgent3<'a> { 83 | rubygems: Version<'a>, 84 | } 85 | 86 | // Instead of repeating such regex as attribute for each field, I decided to 87 | // specify it via type 88 | #[derive(Reformation)] 89 | #[reformation("{}")] 90 | struct Version<'a>(#[reformation(r"[0-9a-zA-Z.\-]+")] &'a str); 91 | 92 | // Original 93 | 94 | #[derive(PartialEq, Debug, Default)] 95 | pub struct UserAgent<'a> { 96 | pub bundler: Option<&'a str>, 97 | pub rubygems: &'a str, 98 | pub ruby: Option<&'a str>, 99 | pub platform: Option<&'a str>, 100 | pub command: Option<&'a str>, 101 | pub options: Option<&'a str>, 102 | pub jruby: Option<&'a str>, 103 | pub truffleruby: Option<&'a str>, 104 | pub ci: Option<&'a str>, 105 | pub gemstash: Option<&'a str>, 106 | } 107 | 108 | pub fn parse(a: &str) -> Option { 109 | lazy_static! { 110 | // Here is the named regex. The regex created below does not include names, because that interface has borrowing issues 😬 111 | // \Abundler/(?[0-9a-zA-Z.\-]+) rubygems/(?[0-9a-zA-Z.\-]+) ruby/(?[0-9a-zA-Z.\-]+) \((?.*)\) command/(.*?)(?: jruby/(?[0-9a-zA-Z.\-]+))?(?: truffleruby/(?[0-9a-zA-Z.\-]+))?(?: options/(?.*?))?(?: ci/(?.*?))? ([a-f0-9]{16})(?: Gemstash/(?[0-9a-zA-Z.\-]+))?\z 112 | static ref br: Regex = Regex::new(r"\Abundler/([0-9a-zA-Z.\-]+) rubygems/([0-9a-zA-Z.\-]+) ruby/([0-9a-zA-Z.\-]+) \(([^)]*)\) command/(.*?)(?: jruby/([0-9a-zA-Z.\-]+))?(?: truffleruby/([0-9a-zA-Z.\-]+))?(?: options/(.*?))?(?: ci/(.*?))? [a-f0-9]{16}(?: Gemstash/([0-9a-zA-Z.\-]+))?\z").unwrap(); 113 | static ref rr: Regex = Regex::new(r"\A(?:Ruby, )?RubyGems/([0-9a-z.\-]+) (.*) Ruby/([0-9a-z.\-]+) \(.*?\)(?: jruby| truffleruby| rbx)?(?: Gemstash/([0-9a-z.\-]+))?\z").unwrap(); 114 | static ref gr: Regex = Regex::new(r"\ARuby, Gems ([0-9a-z.\-]+)\z").unwrap(); 115 | } 116 | 117 | let mut bl = br.capture_locations(); 118 | let mut rl = rr.capture_locations(); 119 | let mut gl = gr.capture_locations(); 120 | 121 | if let Some(_) = br.captures_read(&mut bl, a) { 122 | return Some(UserAgent { 123 | bundler: match bl.get(1) { 124 | Some(loc) => Some(&a[loc.0..loc.1]), 125 | _ => None, 126 | }, 127 | rubygems: match bl.get(2) { 128 | Some(loc) => &a[loc.0..loc.1], 129 | _ => panic!("parse failed on {:?}", a), 130 | }, 131 | ruby: match bl.get(3) { 132 | Some(loc) => Some(&a[loc.0..loc.1]), 133 | _ => None, 134 | }, 135 | platform: match bl.get(4) { 136 | Some(loc) => Some(&a[loc.0..loc.1]), 137 | _ => None, 138 | }, 139 | command: match bl.get(5) { 140 | Some(loc) => Some(&a[loc.0..loc.1]), 141 | _ => None, 142 | }, 143 | jruby: match bl.get(6) { 144 | Some(loc) => Some(&a[loc.0..loc.1]), 145 | _ => None, 146 | }, 147 | truffleruby: match bl.get(7) { 148 | Some(loc) => Some(&a[loc.0..loc.1]), 149 | _ => None, 150 | }, 151 | options: match bl.get(8) { 152 | Some(loc) => Some(&a[loc.0..loc.1]), 153 | _ => None, 154 | }, 155 | ci: match bl.get(9) { 156 | Some(loc) => Some(&a[loc.0..loc.1]), 157 | _ => None, 158 | }, 159 | gemstash: match bl.get(11) { 160 | Some(loc) => Some(&a[loc.0..loc.1]), 161 | _ => None, 162 | }, 163 | }); 164 | } else if let Some(_) = rr.captures_read(&mut rl, a) { 165 | return Some(UserAgent { 166 | bundler: None, 167 | rubygems: match rl.get(1) { 168 | Some(loc) => &a[loc.0..loc.1], 169 | _ => panic!("parse failed on {:?}", a), 170 | }, 171 | ruby: match rl.get(3) { 172 | Some(loc) => Some(&a[loc.0..loc.1]), 173 | _ => None, 174 | }, 175 | platform: match rl.get(2) { 176 | Some(loc) => Some(&a[loc.0..loc.1]), 177 | _ => None, 178 | }, 179 | command: None, 180 | jruby: None, 181 | truffleruby: None, 182 | options: None, 183 | ci: None, 184 | gemstash: match rl.get(4) { 185 | Some(loc) => Some(&a[loc.0..loc.1]), 186 | _ => None, 187 | }, 188 | }); 189 | } else if let Some(_) = gr.captures_read(&mut gl, a) { 190 | return Some(UserAgent { 191 | bundler: None, 192 | rubygems: match gl.get(1) { 193 | Some(loc) => &a[loc.0..loc.1], 194 | _ => panic!("parse failed on {:?}", a), 195 | }, 196 | ruby: None, 197 | platform: None, 198 | command: None, 199 | jruby: None, 200 | truffleruby: None, 201 | options: None, 202 | ci: None, 203 | gemstash: None, 204 | }); 205 | } else { 206 | return None; 207 | } 208 | } 209 | 210 | fn reformation_parse(inputs: &Vec<&str>) { 211 | for i in inputs { 212 | let r = parse_re(*i).unwrap(); 213 | criterion::black_box(r); 214 | } 215 | } 216 | 217 | fn kirby_parse(inputs: &Vec<&str>) { 218 | for i in inputs { 219 | let r = parse(i).unwrap(); 220 | criterion::black_box(r); 221 | } 222 | } 223 | 224 | fn compare(c: &mut Criterion) { 225 | test_parse(); 226 | let inputs = vec![ 227 | "bundler/1.16.1 rubygems/2.6.11 ruby/2.4.1 (x86_64-pc-linux-gnu) command/install options/no_install,mirror.https://rubygems.org/,mirror.https://rubygems.org/.fallback_timeout/,path 59dbf8e99fa09c0a", 228 | "bundler/1.12.5 rubygems/2.6.10 ruby/2.3.1 (x86_64-pc-linux-gnu) command/install options/orig_path 95ac718b0e500f41", 229 | "bundler/1.16.1 rubygems/2.7.6 ruby/2.5.1 (x86_64-pc-linux-gnu) command/install rbx/3.105 options/no_install,git.allow_insecure,build.nokogiri,jobs,path,app_config,silence_root_warning,bin,gemfile e710485d04febb1e", 230 | "bundler/1.12.5 rubygems/2.6.10 ruby/2.3.1 (x86_64-pc-linux-gnu) command/install options/orig_path 95ac718b0e500f41", 231 | "bundler/1.15.4 rubygems/2.6.14 ruby/2.4.2 (x86_64-w64-mingw32) command/install options/ 6e8fa23dbf26d4ff Gemstash/1.1.0", 232 | "bundler/1.16.2 rubygems/2.7.6 ruby/2.5.0 (x86_64-Oracle Corporation-linux) command/install jruby/9.2.1.0-SNAPSHOT options/no_install,retry,jobs,gemfile ci/travis,ci fe5e45257d515f1f", 233 | "bundler/1.5.1 rubygems/2.2.0 ruby/2.1.0 (x86_64-unknown-linux-gnu) command/install fe5e45257d515f1f", 234 | "Ruby, Gems 1.1.1", 235 | "Ruby, RubyGems/1.3.7 x86_64-linux Ruby/1.9.2 (2010-08-18 patchlevel 0)", 236 | "Ruby, RubyGems/2.6.6 x86_64-linux Ruby/2.3.1 (2018-01-06 patchlevel 0) rbx", 237 | ]; 238 | let re = Fun::new("Reformation", |b, i| b.iter(|| reformation_parse(i))); 239 | let kr = Fun::new("Kirby", |b, i| b.iter(|| kirby_parse(i))); 240 | c.bench_functions("reformation parse", vec![re, kr], inputs); 241 | } 242 | 243 | criterion_group!(benches, compare); 244 | criterion_main!(benches); 245 | 246 | fn test_parse() { 247 | assert_eq!( 248 | parse_re("bundler/1.12.5 rubygems/2.6.10 ruby/2.3.1 (x86_64-pc-linux-gnu) command/install options/orig_path 95ac718b0e500f41"), 249 | Some(UserAgent { 250 | bundler: Some("1.12.5"), 251 | rubygems: "2.6.10", 252 | ruby: Some("2.3.1"), 253 | platform: Some("x86_64-pc-linux-gnu"), 254 | command: Some("install"), 255 | options: Some("orig_path"), 256 | jruby: None, 257 | truffleruby: None, 258 | ci: None, 259 | gemstash: None, 260 | }) 261 | ); 262 | 263 | assert_eq!( 264 | parse_re("Ruby, RubyGems/2.4.8 x86_64-linux Ruby/2.1.6 (2015-04-13 patchlevel 336)"), 265 | Some(UserAgent { 266 | bundler: None, 267 | rubygems: "2.4.8", 268 | ruby: Some("2.1.6"), 269 | platform: Some("x86_64-linux"), 270 | command: None, 271 | options: None, 272 | jruby: None, 273 | truffleruby: None, 274 | ci: None, 275 | gemstash: None, 276 | }) 277 | ); 278 | 279 | assert_eq!( 280 | parse_re("Ruby, Gems 1.1.1"), 281 | Some(UserAgent { 282 | bundler: None, 283 | rubygems: "1.1.1", 284 | ruby: None, 285 | platform: None, 286 | command: None, 287 | options: None, 288 | jruby: None, 289 | truffleruby: None, 290 | ci: None, 291 | gemstash: None, 292 | }) 293 | ); 294 | } 295 | -------------------------------------------------------------------------------- /reformation/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Parsing via regular expressions using format syntax 2 | //! 3 | //! Derive will require attribute reformation to specify format string, 4 | //! which will be treated as format string -> regular expression string 5 | //! 6 | //! Types implementing `Reformation` by default: 7 | //! 8 | //! + signed integers: `i8` `i16` `i32` `i64` `i128` `isize` 9 | //! + unsigned integers: `u8` `u16` `u32` `u64` `u128` `usize` 10 | //! + floats: `f32` `f64` 11 | //! + `String`, &str 12 | //! + `char` 13 | //! 14 | //! ## Structs 15 | //! 16 | //! ``` 17 | //! use reformation::Reformation; 18 | //! 19 | //! #[derive(Reformation, Debug)] 20 | //! #[reformation(r"{year}-{month}-{day} {hour}:{minute}")] 21 | //! struct Date{ 22 | //! year: u16, 23 | //! month: u8, 24 | //! day: u8, 25 | //! hour: u8, 26 | //! minute: u8, 27 | //! } 28 | //! 29 | //! fn main(){ 30 | //! let date = Date::parse("2018-12-22 20:23").unwrap(); 31 | //! 32 | //! assert_eq!(date.year, 2018); 33 | //! assert_eq!(date.month, 12); 34 | //! assert_eq!(date.day, 22); 35 | //! assert_eq!(date.hour, 20); 36 | //! assert_eq!(date.minute, 23); 37 | //! } 38 | //! ``` 39 | //! 40 | //! ## Tuple Structs 41 | //! 42 | //! ``` 43 | //! use reformation::Reformation; 44 | //! 45 | //! #[derive(Reformation)] 46 | //! #[reformation(r"{} -> {}")] 47 | //! struct Predicate(Empty, char); 48 | //! 49 | //! #[derive(Reformation, Debug, PartialEq)] 50 | //! #[reformation(r"Empty")] 51 | //! struct Empty; 52 | //! 53 | //! fn main(){ 54 | //! let p = Predicate::parse("Empty -> X").unwrap(); 55 | //! assert_eq!(p.0, Empty); 56 | //! assert_eq!(p.1, 'X'); 57 | //! } 58 | //! ``` 59 | //! 60 | //! ## Enums 61 | //! ``` 62 | //! use reformation::Reformation; 63 | //! 64 | //! #[derive(Reformation, Eq, PartialEq, Debug)] 65 | //! enum Ant{ 66 | //! #[reformation(r"Queen\({}\)")] 67 | //! Queen(String), 68 | //! #[reformation(r"Worker\({}\)")] 69 | //! Worker(i32), 70 | //! #[reformation(r"Warrior")] 71 | //! Warrior 72 | //! } 73 | //! 74 | //! fn main(){ 75 | //! let queen = Ant::parse("Queen(We are swarm)").unwrap(); 76 | //! assert_eq!(queen, Ant::Queen("We are swarm".to_string())); 77 | //! 78 | //! let worker = Ant::parse("Worker(900000)").unwrap(); 79 | //! assert_eq!(worker, Ant::Worker(900000)); 80 | //! 81 | //! let warrior = Ant::parse("Warrior").unwrap(); 82 | //! assert_eq!(warrior, Ant::Warrior); 83 | //! } 84 | //! ``` 85 | //! 86 | //! ## In place parsing 87 | //! ``` 88 | //! use reformation::Reformation; 89 | //! 90 | //! #[derive(Reformation, Eq, PartialEq, Debug)] 91 | //! #[reformation("{a} {b}")] 92 | //! struct InPlace<'a, 'b>{ 93 | //! #[reformation("[a-z]*")] 94 | //! a: &'a str, 95 | //! #[reformation("[a-z]*")] 96 | //! b: &'b str, 97 | //! } 98 | //! 99 | //! fn main(){ 100 | //! // Then parsed from &'x str value will have type 101 | //! // InPlace<'x, 'x> 102 | //! println!("{}", InPlace::regex_str()); 103 | //! let inplace = InPlace::parse("aval bval").unwrap(); 104 | //! assert_eq!(inplace, InPlace{a: "aval", b: "bval"}) 105 | //! } 106 | //! ``` 107 | //! 108 | //! ## Modes 109 | //! 110 | //! Order, in which modes are specified does not matter. 111 | //! 112 | //! ### fromstr 113 | //! 114 | //! Generate implementation of `FromStr` trait. 115 | //! 116 | //! Not compatible with lifetime annotated structs. 117 | //! 118 | //! ``` 119 | //! use reformation::Reformation; 120 | //! 121 | //! #[derive(Reformation, Debug)] 122 | //! #[reformation(r"{year}-{month}-{day} {hour}:{minute}", fromstr = true)] 123 | //! struct Date{ 124 | //! year: u16, 125 | //! month: u8, 126 | //! day: u8, 127 | //! hour: u8, 128 | //! minute: u8, 129 | //! } 130 | //! 131 | //! fn main() -> Result<(), Box> { 132 | //! let date: Date = "2018-12-22 20:23".parse()?; 133 | //! 134 | //! assert_eq!(date.year, 2018); 135 | //! assert_eq!(date.month, 12); 136 | //! assert_eq!(date.day, 22); 137 | //! assert_eq!(date.hour, 20); 138 | //! assert_eq!(date.minute, 23); 139 | //! 140 | //! Ok(()) 141 | //! } 142 | //! ``` 143 | //! 144 | //! ### no_regex 145 | //! 146 | //! Makes format string behave as regular string (in contrast with being regular expression), 147 | //! by escaping all special regex characters. 148 | //! 149 | //! ``` 150 | //! use reformation::Reformation; 151 | //! 152 | //! #[derive(Reformation, Debug)] 153 | //! #[reformation("Vec{{{x}, {y}}}", no_regex=true)] 154 | //! struct Vec{ 155 | //! x: i32, 156 | //! y: i32, 157 | //! } 158 | //! 159 | //! fn main(){ 160 | //! let v= Vec::parse("Vec{-1, 1}").unwrap(); 161 | //! assert_eq!(v.x, -1); 162 | //! assert_eq!(v.y, 1); 163 | //! } 164 | //! ``` 165 | //! 166 | //! ### slack 167 | //! 168 | //! Allow arbitrary number of spaces after separators: ',', ';', ':'. For separator to be recognized 169 | //! as slack, it must be followed by at least one space in format string. 170 | //! 171 | //! 172 | //! ``` 173 | //! use reformation::Reformation; 174 | //! 175 | //! #[derive(Reformation, Debug)] 176 | //! #[reformation(r"Vec\{{{x}, {y}\}}", slack=true)] 177 | //! struct Vec{ 178 | //! x: i32, 179 | //! y: i32, 180 | //! } 181 | //! 182 | //! fn main(){ 183 | //! let v = Vec::parse("Vec{-1,1}").unwrap(); 184 | //! assert_eq!(v.x, -1); 185 | //! assert_eq!(v.y, 1); 186 | //! 187 | //! let r = Vec::parse("Vec{15, 2}").unwrap(); 188 | //! assert_eq!(r.x, 15); 189 | //! assert_eq!(r.y, 2); 190 | //! } 191 | //! ``` 192 | 193 | #![cfg_attr(feature="containers", feature(const_generics))] 194 | 195 | #[macro_use] 196 | extern crate derive_more; 197 | 198 | pub use once_cell::sync::OnceCell; 199 | pub use reformation_derive::*; 200 | pub use regex::{CaptureLocations, Error as RegexError, Regex}; 201 | use std::collections::HashMap; 202 | use std::sync::RwLock; 203 | 204 | #[cfg(feature="containers")] 205 | pub mod containers; 206 | 207 | /// Declares how object can be parsed from `&'a str` 208 | /// with possibility of in place parsing 209 | pub trait ParseOverride<'t>: Sized { 210 | fn parse_override(input: &'t str) -> Result; 211 | } 212 | 213 | pub trait Reformation<'t>: Sized { 214 | /// regular expression for matching this struct 215 | fn regex_str() -> &'static str; 216 | 217 | /// number of used capture groups. 218 | // Can be calculated from regex_str, but 219 | // setting explicit value by hand avoids 220 | // any extra cost and can be inlined in nested structs, but 221 | // more error prone. 222 | fn captures_count() -> usize; 223 | 224 | /// create instance of function from captures with given offset 225 | fn from_captures<'a>(c: &Captures<'a, 't>, offset: usize) -> Result; 226 | 227 | /// parse struct from str 228 | fn parse(input: &'t str) -> Result; 229 | } 230 | 231 | impl<'t, T: Reformation<'t>> ParseOverride<'t> for T { 232 | fn parse_override(input: &'t str) -> Result { 233 | ::parse(input) 234 | } 235 | } 236 | 237 | macro_rules! group_impl_parse_primitive{ 238 | ($re: expr, $($name: ty),*) => { 239 | $(group_impl_parse_primitive!{@single $re, $name})* 240 | }; 241 | 242 | (@single $re: expr, $name: ty) => { 243 | impl<'t> Reformation<'t> for $name{ 244 | #[inline] 245 | fn regex_str() -> &'static str{ 246 | $re 247 | } 248 | 249 | #[inline] 250 | fn captures_count() -> usize{ 251 | 1 252 | } 253 | 254 | #[inline] 255 | fn from_captures<'a>(c: &Captures<'a, 't>, offset: usize) -> Result{ 256 | let res = c.get(offset) 257 | .ok_or_else(|| Error::DoesNotContainGroup)? 258 | .parse::<$name>() 259 | .map_err(|e| Error::Other(e.to_string()))?; 260 | Ok(res) 261 | } 262 | 263 | #[inline] 264 | fn parse(input: &'t str) -> Result{ 265 | let res = input.parse::<$name>().map_err(|e| Error::Other(e.to_string()))?; 266 | Ok(res) 267 | } 268 | } 269 | }; 270 | } 271 | 272 | type CallMap = HashMap String, &'static T>; 273 | 274 | pub struct GenericStaticStr { 275 | map: RwLock>, 276 | } 277 | 278 | impl Default for GenericStaticStr{ 279 | fn default() -> Self{ 280 | Self::new() 281 | } 282 | } 283 | 284 | impl GenericStaticStr { 285 | pub fn new() -> Self { 286 | Self { 287 | map: RwLock::new(HashMap::new()), 288 | } 289 | } 290 | 291 | pub fn call_once T>( 292 | &'static self, 293 | key: fn() -> String, 294 | map: F, 295 | ) -> &'static T { 296 | { 297 | let read = self.map.read().unwrap(); 298 | if let Some(v) = read.get(&key) { 299 | return v; 300 | } 301 | } 302 | { 303 | let mut write = self.map.write().unwrap(); 304 | // double check that value still was not inserted to avoid 305 | // memory leaks. 306 | // Box::leak ing one value per key is completely ok since it is 307 | // meant for this value to live as long as program, and then it 308 | // would be freed. But if two processes will try to initialize 309 | // same key both might be sure in absence of value and leak it twice. 310 | if let Some(v) = write.get(&key) { 311 | return v; 312 | } 313 | let value = Box::new(map(&key())); 314 | let value = Box::leak(value); 315 | write.insert(key, value); 316 | value 317 | } 318 | } 319 | } 320 | 321 | #[derive(Copy, Clone)] 322 | /// Wrapper to get captures of regular expression 323 | #[derive(Debug)] 324 | pub struct Captures<'a, 't> { 325 | captures: &'a CaptureLocations, 326 | input: &'t str, 327 | } 328 | 329 | impl<'a, 't> Captures<'a, 't> { 330 | #[inline] 331 | pub fn new(captures: &'a CaptureLocations, input: &'t str) -> Self { 332 | Self { captures, input } 333 | } 334 | 335 | #[inline] 336 | /// Get string corresponding to `id` capture group 337 | pub fn get(&self, id: usize) -> Option<&'t str> { 338 | self.captures.get(id).map(|(a, b)| &self.input[a..b]) 339 | } 340 | } 341 | 342 | #[derive(Debug, Display, Eq, PartialEq)] 343 | pub enum Error { 344 | NoRegexMatch(NoRegexMatch), 345 | DoesNotContainGroup, 346 | #[display(fmt = "{:?}", "_0")] 347 | Other(String), 348 | } 349 | 350 | impl std::error::Error for Error {} 351 | 352 | #[derive(Debug, Display, Eq, PartialEq)] 353 | #[display( 354 | fmt = "No regex match: regex {:?} does not match string {:?}", 355 | format, 356 | request 357 | )] 358 | pub struct NoRegexMatch { 359 | pub format: &'static str, 360 | pub request: String, 361 | } 362 | group_impl_parse_primitive! {r"(\d+)", u8, u16, u32, u64, u128, usize} 363 | group_impl_parse_primitive! {r"([\+-]?\d+)", i8, i16, i32, i64, i128, isize} 364 | group_impl_parse_primitive! {r"((?:[\+-]?\d+(?:.\d*)?|.\d+)(?:[eE][\+-]?\d+)?)", f32, f64} 365 | group_impl_parse_primitive! {r"(.*)", String} 366 | group_impl_parse_primitive! {r"(.)", char} 367 | 368 | impl<'t, T: Reformation<'t>> Reformation<'t> for Option { 369 | #[inline] 370 | fn regex_str() -> &'static str { 371 | fn generate_string<'a, T: Reformation<'a>>() -> String { 372 | T::regex_str().to_string() + "?" 373 | } 374 | static RE: OnceCell> = OnceCell::new(); 375 | let re = RE.get_or_init(GenericStaticStr::new); 376 | re.call_once(generate_string::, |x: &str| x.to_string()) 377 | .as_str() 378 | } 379 | 380 | #[inline] 381 | fn captures_count() -> usize { 382 | T::captures_count() 383 | } 384 | 385 | #[inline] 386 | fn from_captures<'a>(captures: &Captures<'a, 't>, offset: usize) -> Result { 387 | if captures.get(offset).is_some() { 388 | T::from_captures(captures, offset).map(Some) 389 | } else { 390 | Ok(None) 391 | } 392 | } 393 | 394 | #[inline] 395 | fn parse(input: &'t str) -> Result { 396 | match T::parse(input) { 397 | Ok(x) => Ok(Some(x)), 398 | Err(Error::DoesNotContainGroup) => Ok(None), 399 | Err(e) => Err(e), 400 | } 401 | } 402 | } 403 | 404 | impl<'t> Reformation<'t> for &'t str { 405 | #[inline] 406 | fn regex_str() -> &'static str { 407 | "(.*?)" 408 | } 409 | 410 | #[inline] 411 | fn captures_count() -> usize { 412 | 1 413 | } 414 | 415 | #[inline] 416 | fn from_captures<'a>(captures: &Captures<'a, 't>, offset: usize) -> Result { 417 | let res = captures 418 | .get(offset) 419 | .ok_or_else(|| Error::DoesNotContainGroup)?; 420 | Ok(res) 421 | } 422 | 423 | #[inline] 424 | fn parse(input: &'t str) -> Result { 425 | Ok(input) 426 | } 427 | } 428 | 429 | #[cfg(test)] 430 | mod tests { 431 | use super::*; 432 | 433 | #[test] 434 | fn test_float_parse() { 435 | // test regular expression for floating point numbers 436 | let re = regex::Regex::new(&format!("^{}$", f32::regex_str())).unwrap(); 437 | // positive 438 | assert!(check_float_capture(&re, "10")); 439 | assert!(check_float_capture(&re, "10.2")); 440 | assert!(check_float_capture(&re, "10.")); 441 | assert!(check_float_capture(&re, "0.34")); 442 | assert!(check_float_capture(&re, "00.34")); 443 | assert!(check_float_capture(&re, ".34")); 444 | assert!(check_float_capture(&re, ".34e2")); 445 | assert!(check_float_capture(&re, ".34e+2")); 446 | assert!(check_float_capture(&re, ".34e-2")); 447 | assert!(check_float_capture(&re, "-0.34e-2")); 448 | assert!(check_float_capture(&re, "5e-2")); 449 | assert!(check_float_capture(&re, "5.e-2")); // should this pass? 450 | 451 | // negative 452 | assert!(!re.is_match("5..")); 453 | assert!(!re.is_match(".")); 454 | assert!(!re.is_match("--4.")); 455 | assert!(!re.is_match("-.0")); 456 | } 457 | 458 | fn check_float_capture(r: ®ex::Regex, s: &str) -> bool { 459 | r.captures(s) 460 | .map(|c| c.len() == 2 && c.get(1).map(|x| x.as_str()) == Some(s)) 461 | .unwrap_or(false) 462 | } 463 | } 464 | -------------------------------------------------------------------------------- /reformation_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | 3 | #[macro_use] 4 | extern crate quote; 5 | #[macro_use] 6 | extern crate syn; 7 | 8 | extern crate proc_macro; 9 | 10 | mod format; 11 | mod reformation_attribute; 12 | mod syn_helpers; 13 | 14 | use crate::format::Format; 15 | use crate::reformation_attribute::ReformationAttribute; 16 | use proc_macro2::{Span, TokenStream}; 17 | use std::collections::HashMap; 18 | use syn::spanned::Spanned; 19 | 20 | #[proc_macro_derive(Reformation, attributes(reformation))] 21 | pub fn reformation_derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 22 | let ds = parse_macro_input!(item as syn::DeriveInput); 23 | let expanded = match reformation_derive_do(ds) { 24 | Ok(ok) => ok, 25 | Err(errors) => errors.to_compile_error(), 26 | }; 27 | proc_macro::TokenStream::from(expanded) 28 | } 29 | 30 | fn reformation_derive_do(mut ds: syn::DeriveInput) -> syn::Result { 31 | let ds = DeriveInput::new(&mut ds)?; 32 | Ok(ds.impl_all()) 33 | } 34 | 35 | struct DeriveInput<'a> { 36 | input: &'a syn::DeriveInput, 37 | data: InputData<'a>, 38 | attr: ReformationAttribute, 39 | } 40 | 41 | enum InputData<'a> { 42 | Struct(Item<'a>), 43 | Enum(Vec>), 44 | } 45 | 46 | struct Item<'a> { 47 | format: String, 48 | name: TokenStream, 49 | defaults: Vec<&'a syn::Field>, 50 | fields: Vec<&'a syn::Field>, 51 | } 52 | 53 | impl<'a> InputData<'a> { 54 | 55 | 56 | fn new_enum( 57 | name: &TokenStream, 58 | attrs: &ReformationAttribute, 59 | enum_: &'a syn::DataEnum, 60 | ) -> syn::Result { 61 | let variants: syn::Result> = enum_ 62 | .variants 63 | .iter() 64 | .filter_map(|v| { 65 | ReformationAttribute::find_attribute(&v.attrs)?; // Variants with no attribute should be unparseable 66 | let new_attrs = match ReformationAttribute::parse(v.span(), &v.attrs) { 67 | Ok(a) => a, 68 | Err(e) => { 69 | return Some(Err(e)); 70 | } 71 | }; 72 | let mut attrs = attrs.clone(); 73 | attrs.regex_string = None; 74 | let new_attrs = attrs.combine(&new_attrs); 75 | let ident = &v.ident; 76 | let name = quote! {#name::#ident}; 77 | Some(Item::from_fields(name, &new_attrs, &v.fields)) 78 | }) 79 | .collect(); 80 | Ok(InputData::Enum(variants?)) 81 | } 82 | } 83 | 84 | impl<'a> Item<'a> { 85 | fn from_fields( 86 | name: TokenStream, 87 | attrs: &ReformationAttribute, 88 | fields: &'a syn::Fields, 89 | ) -> syn::Result { 90 | match fields { 91 | syn::Fields::Named(_) => Self::named(name, attrs, fields), 92 | syn::Fields::Unnamed(_) => Self::unnamed(name, attrs, fields), 93 | syn::Fields::Unit => Self::empty(name, attrs), 94 | } 95 | } 96 | 97 | fn named( 98 | name: TokenStream, 99 | attrs: &ReformationAttribute, 100 | fields: &'a syn::Fields, 101 | ) -> syn::Result { 102 | let format = format_from_attribute(attrs)?; 103 | if format.positional_arguments() > 0 { 104 | return Err(syn::Error::new( 105 | attrs.span, 106 | "Reformation error: arguments must be named", 107 | )); 108 | } 109 | let mut map: HashMap = fields 110 | .iter() 111 | .map(|f| (f.ident.as_ref().unwrap().to_string(), f)) 112 | .collect(); 113 | let fields: syn::Result> = format 114 | .named_arguments() 115 | .map(|s| { 116 | map.remove(&s) 117 | .ok_or_else(|| syn::Error::new(attrs.span, format!("No field named '{}'", s))) 118 | }) 119 | .collect(); 120 | let defaults: Vec<_> = map.into_iter().map(|(_, v)| v).collect(); 121 | let fields = fields?; 122 | Ok(Self { 123 | format: format.linearize(), 124 | fields, 125 | defaults, 126 | name, 127 | }) 128 | } 129 | 130 | fn unnamed( 131 | name: TokenStream, 132 | attrs: &ReformationAttribute, 133 | fields: &'a syn::Fields, 134 | ) -> syn::Result { 135 | let format = format_from_attribute(attrs)?; 136 | if format.named_arguments().next().is_some() { 137 | return Err(syn::Error::new( 138 | attrs.span, 139 | "Reformation error: arguments must be positional", 140 | )); 141 | } 142 | let fields: Vec<_> = fields.iter().collect(); 143 | if format.positional_arguments() != fields.len() { 144 | return Err(syn::Error::new( 145 | attrs.span, 146 | "Reformation error: wrong number of arguments", 147 | )); 148 | } 149 | Ok(Self { 150 | name, 151 | defaults: vec![], 152 | format: format.linearize(), 153 | fields, 154 | }) 155 | } 156 | 157 | fn empty(name: TokenStream, attrs: &ReformationAttribute) -> syn::Result { 158 | let format = format_from_attribute(attrs)?; 159 | if format.no_arguments() { 160 | let format = format.linearize(); 161 | Ok(Self { 162 | name, 163 | format, 164 | defaults: vec![], 165 | fields: vec![], 166 | }) 167 | } else { 168 | Err(syn::Error::new( 169 | attrs.span, 170 | "Reformation error: no arguments expected in format string for Unit.", 171 | )) 172 | } 173 | } 174 | } 175 | 176 | impl<'a> DeriveInput<'a> { 177 | fn new(derive_input: &'a syn::DeriveInput) -> syn::Result> { 178 | let attr = ReformationAttribute::parse(derive_input.span(), &derive_input.attrs)?; 179 | let ident = &derive_input.ident; 180 | let name = quote! { #ident }; 181 | if attr.fromstr && derive_input.generics.lifetimes().next().is_some() { 182 | return Err(syn::Error::new(ident.span(), "FromStr can only be implemented by `T: 'static` types.")) 183 | } 184 | let data = match derive_input.data { 185 | syn::Data::Struct(ref s) => { 186 | let item = Item::from_fields(name, &attr, &s.fields)?; 187 | Ok(InputData::Struct(item)) 188 | } 189 | syn::Data::Enum(ref e) => InputData::new_enum(&name, &attr, &e), 190 | syn::Data::Union(_) => Err(syn::Error::new( 191 | ident.span(), 192 | "Reformation does not support unions", 193 | )), 194 | }?; 195 | Ok(DeriveInput{ 196 | data, 197 | input: derive_input, 198 | attr 199 | }) 200 | } 201 | 202 | fn unique_lifetime(&self, syffix: &str) -> syn::Lifetime { 203 | let last = self 204 | .input 205 | .generics 206 | .lifetimes() 207 | .map(|x| x.lifetime.ident.to_string()) 208 | .max() 209 | .unwrap_or_default(); 210 | 211 | let total = format!("'{}_{}", last, syffix); 212 | syn::Lifetime::new(&total, Span::call_site()) 213 | } 214 | 215 | /// Generate ```TokenStream``` with implementation of ```Reformation``` for `input` 216 | fn impl_all(&self) -> TokenStream { 217 | let ident = &self.input.ident; 218 | 219 | // Add lifetime " 'input: 'a + 'b + ... " to impl_gen 220 | let mut impl_gen = self.input.generics.clone(); 221 | let mut lifetimes = impl_gen.lifetimes(); 222 | let first = lifetimes.next().into_iter(); 223 | let input_lifetime = self.unique_lifetime("input"); 224 | let input = parse_quote!(#input_lifetime #(: #first)* #(+ #lifetimes)* ); 225 | impl_gen.params.push(syn::GenericParam::Lifetime(input)); 226 | // add 'input bound on all generic types 227 | for t in impl_gen.type_params_mut() { 228 | t.bounds 229 | .push(parse_quote!(::reformation::Reformation<#input_lifetime>)) 230 | } 231 | let (impl_gen, _, where_clause) = impl_gen.split_for_impl(); 232 | let (_, type_gen, _) = self.input.generics.split_for_impl(); 233 | 234 | let regex_str = self.regex_str_quote(); 235 | let count = self.captures_count_quote(); 236 | let from_captures = self.impl_from_captures_quote(); 237 | let parse = self.parse_quote(); 238 | 239 | let fromstr = if self.attr.fromstr { 240 | self.fromstr_quote() 241 | } else { 242 | quote! {} 243 | }; 244 | 245 | let res = quote! { 246 | #[allow(clippy::eval_order_dependence)] 247 | impl #impl_gen ::reformation::Reformation<#input_lifetime> for #ident #type_gen #where_clause{ 248 | #regex_str 249 | #count 250 | #from_captures 251 | #parse 252 | } 253 | 254 | #fromstr 255 | }; 256 | res 257 | } 258 | 259 | fn fromstr_quote(&self) -> TokenStream { 260 | let ident = &self.input.ident; 261 | let (impl_gen, type_gen, where_clause) = self.input.generics.split_for_impl(); 262 | 263 | quote! { 264 | impl #impl_gen std::str::FromStr for #ident #type_gen #where_clause { 265 | type Err = ::reformation::Error; 266 | fn from_str(s: &str) -> Result { 267 | Self::parse(s) 268 | } 269 | } 270 | } 271 | } 272 | 273 | fn regex_str_quote(&self) -> TokenStream { 274 | let fmt = match self.data { 275 | InputData::Struct(ref i) => { 276 | let substr = i.substrings(); 277 | let fmt = &i.format; 278 | quote! { 279 | format!(#fmt, #(#substr),*) 280 | } 281 | } 282 | InputData::Enum(ref e) => { 283 | let substr = e.iter().flat_map(|i| i.substrings()); 284 | let fmt = Self::enum_format_str(&e); 285 | quote! { 286 | format!(#fmt, #(#substr),*) 287 | } 288 | } 289 | }; 290 | 291 | let gen = self.generic_type_params(); 292 | let static_ = self.maybe_generic_static( 293 | quote! {String}, 294 | quote! {create_regex}, 295 | quote! {|x: &str| x.to_string()}, 296 | ); 297 | let res = quote! { 298 | fn regex_str() -> &'static str{ 299 | fn create_regex #gen () -> String { 300 | #fmt 301 | } 302 | #static_.as_str() 303 | } 304 | }; 305 | res 306 | } 307 | 308 | fn generic_type_params(&self) -> TokenStream { 309 | let mut generics = self.input.generics.clone(); 310 | let mut lifetimes = generics.lifetimes(); 311 | let first = lifetimes.next().into_iter(); 312 | let input_lifetime = self.unique_lifetime("input"); 313 | let input = parse_quote!(#input_lifetime #(: #first)* #(+ #lifetimes)* ); 314 | generics.params.push(syn::GenericParam::Lifetime(input)); 315 | let input_lifetime = self.unique_lifetime("input"); 316 | for t in generics.type_params_mut() { 317 | t.bounds 318 | .push(parse_quote!(::reformation::Reformation<#input_lifetime>)) 319 | } 320 | let (impl_gen, _, _) = generics.split_for_impl(); 321 | quote! { #impl_gen } 322 | } 323 | 324 | fn maybe_generic_static( 325 | &self, 326 | type_: TokenStream, 327 | key: TokenStream, 328 | map: TokenStream, 329 | ) -> TokenStream { 330 | if self.input.generics.type_params().next().is_some() { 331 | let (_, ty, _) = self.input.generics.split_for_impl(); 332 | let turbo = ty.as_turbofish(); 333 | quote! { 334 | { 335 | static RE: ::reformation::OnceCell<::reformation::GenericStaticStr<#type_>> = ::reformation::OnceCell::new(); 336 | let re = RE.get_or_init(||{ 337 | ::reformation::GenericStaticStr::new() 338 | }); 339 | re.call_once(#key #turbo, #map) 340 | } 341 | } 342 | } else { 343 | // No generics, fall back to regular statics for better perfomance 344 | quote! { 345 | static RE: ::reformation::OnceCell<#type_> = ::reformation::OnceCell::new(); 346 | RE.get_or_init(||{ 347 | (#map)(&#key()) 348 | }) 349 | } 350 | } 351 | } 352 | 353 | fn enum_format_str(items: &[Item]) -> String { 354 | let mut fmt = "(?:(".to_string(); 355 | for i in items { 356 | fmt += &i.format; 357 | fmt += ")|("; 358 | } 359 | // After we added last group we added separator ")|(" 360 | // where "|(" must be removed 361 | fmt.pop(); 362 | fmt.pop(); 363 | fmt += ")"; 364 | fmt 365 | } 366 | 367 | fn captures_count_quote(&self) -> TokenStream { 368 | let count = match self.data { 369 | InputData::Struct(ref i) => i.size_quote(), 370 | InputData::Enum(ref e) => { 371 | let counts = e.iter().flat_map(|i| i.captures_count()); 372 | let variant_count = e.len(); 373 | quote! { 374 | { 375 | let mut count = #variant_count; 376 | #(count += #counts;)* 377 | count 378 | } 379 | } 380 | } 381 | }; 382 | quote! { 383 | #[inline] 384 | fn captures_count() -> usize { 385 | #count 386 | } 387 | } 388 | } 389 | 390 | fn impl_from_captures_quote(&self) -> TokenStream { 391 | let body = match self.data { 392 | InputData::Struct(ref i) => i.impl_from_captures("e! {captures}, "e! {offset}), 393 | InputData::Enum(ref variants) => { 394 | let items = variants 395 | .iter() 396 | .map(|i| i.impl_from_captures("e! {captures}, "e! {offset})); 397 | let sizes = variants.iter().map(|i| i.size_quote()); 398 | quote! { 399 | #( 400 | if captures.get(offset).is_some() { 401 | offset += 1; 402 | return #items; 403 | }else{ 404 | offset += #sizes + 1; 405 | } 406 | )* 407 | Err(::reformation::Error::DoesNotContainGroup) 408 | } 409 | } 410 | }; 411 | let input_lifetime = self.unique_lifetime("input"); 412 | let cap_lifetime = self.unique_lifetime("cap"); 413 | let res = quote! { 414 | fn from_captures<#cap_lifetime>(captures: &::reformation::Captures<#cap_lifetime, #input_lifetime>, mut offset: usize) -> Result { 415 | #body 416 | } 417 | }; 418 | res 419 | } 420 | 421 | fn parse_quote(&self) -> TokenStream { 422 | let ident = &self.input.ident; 423 | let (_, ty_gen, _) = self.input.generics.split_for_impl(); 424 | let name = quote! { #ident #ty_gen}; 425 | let input_lifetime = self.unique_lifetime("input"); 426 | 427 | let gen = self.generic_type_params(); 428 | let static_ = self.maybe_generic_static( 429 | quote! {::reformation::Regex}, 430 | quote! {create_regex}, 431 | quote! {|x: &str| ::reformation::Regex::new(x).unwrap()}, 432 | ); 433 | let static_ = quote! { 434 | { 435 | fn create_regex #gen () -> String { 436 | format!(r"\A{}\z", <#name as ::reformation::Reformation>::regex_str()) 437 | } 438 | #static_ 439 | } 440 | }; 441 | 442 | quote! { 443 | fn parse(string: &#input_lifetime str) -> Result{ 444 | use ::reformation::{Regex, Captures, Error}; 445 | let re = #static_; 446 | let mut loc = re.capture_locations(); 447 | if re.captures_read(&mut loc, string).is_some(){ 448 | let captures = Captures::new(&loc, string); 449 | Self::from_captures(&captures, 1) 450 | }else{ 451 | Err( 452 | Error::NoRegexMatch(::reformation::NoRegexMatch{ 453 | format: Self::regex_str(), 454 | request: string.to_string(), 455 | }) 456 | ) 457 | } 458 | } 459 | } 460 | } 461 | 462 | } 463 | 464 | impl<'a> Item<'a> { 465 | fn substrings<'b>(&'b self) -> impl Iterator + 'b { 466 | self.fields.iter().map(|f| { 467 | let ty = &f.ty; 468 | let r = Self::field_regex_override(f); 469 | match r { 470 | Ok(Some(regex)) => quote! { format!("({})", #regex) }, 471 | Ok(None) => quote! {<#ty as ::reformation::Reformation>::regex_str()}, 472 | Err(e) => e.to_compile_error(), 473 | } 474 | }) 475 | } 476 | 477 | fn field_regex_override(field: &syn::Field) -> syn::Result> { 478 | if field.attrs.is_empty() { 479 | return Ok(None); 480 | } 481 | let attr = ReformationAttribute::parse(field.span(), &field.attrs)?; 482 | let res = attr.regex().ok().map(|s| (attr.substr_mode())(s)); 483 | Ok(res) 484 | } 485 | 486 | fn size_quote(&self) -> TokenStream { 487 | if self.fields.is_empty() { 488 | return quote! {0}; 489 | } 490 | let count = self.captures_count(); 491 | quote! {{ 492 | let mut count = 0; 493 | #(count += #count;)* 494 | count 495 | }} 496 | } 497 | 498 | fn captures_count<'b>(&'b self) -> impl Iterator + 'b { 499 | self.fields.iter().map(|f| { 500 | let ty = &f.ty; 501 | if Item::field_regex_override(f).unwrap().is_some() { 502 | quote! {1} 503 | } else { 504 | quote! {<#ty as ::reformation::Reformation>::captures_count()} 505 | } 506 | }) 507 | } 508 | 509 | /// Returns token stream representing code which construct `Item` from captures 510 | /// 511 | /// + `captures` - name of captures variable 512 | /// + `counter` - name of counter variable 513 | fn impl_from_captures(&self, captures: &TokenStream, counter: &TokenStream) -> TokenStream { 514 | let constr = &self.name; 515 | if self.fields.is_empty() { 516 | return quote! { 517 | Ok(#constr) 518 | }; 519 | } 520 | 521 | let item_from_captures = self.fields.iter() 522 | .map(|f|{ 523 | let type1 = &f.ty; 524 | let type2 = &f.ty; 525 | let counter1 = &counter; 526 | let counter2 = &counter; 527 | if Item::field_regex_override(f).unwrap().is_some(){ 528 | quote! { 529 | { 530 | let str = #captures.get(#counter1) 531 | .ok_or_else(|| ::reformation::Error::DoesNotContainGroup)?; 532 | let x = <#type1 as ::reformation::ParseOverride>::parse_override(str)?; 533 | #counter2 += 1; 534 | x 535 | } 536 | } 537 | }else{ 538 | quote! { 539 | { 540 | let x = <#type1 as ::reformation::Reformation>::from_captures(#captures, #counter1)?; 541 | #counter2 += <#type2 as ::reformation::Reformation>::captures_count(); 542 | x 543 | } 544 | } 545 | } 546 | }); 547 | 548 | let defaults = self.defaults.iter().map(|x| &x.ident); 549 | if self.fields[0].ident.is_some() { 550 | let names = self.fields.iter().map(|f| f.ident.as_ref().unwrap()); 551 | quote! { 552 | Ok(#constr{ 553 | #( 554 | #names: #item_from_captures, 555 | )* 556 | #(#defaults: Default::default(),)* 557 | }) 558 | } 559 | } else { 560 | quote! { 561 | Ok(#constr( #( 562 | #item_from_captures, 563 | )* )) 564 | } 565 | } 566 | } 567 | } 568 | 569 | fn format_from_attribute(attr: &ReformationAttribute) -> syn::Result { 570 | let mut format = Format::build(&attr.regex()?).map_err(|e| syn::Error::new(attr.span, e))?; 571 | format.map_substrings(attr.substr_mode()); 572 | Ok(format) 573 | } 574 | --------------------------------------------------------------------------------