├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── recap-derive ├── Cargo.toml └── src │ └── lib.rs ├── recap ├── Cargo.toml ├── examples │ └── log.rs └── src │ └── lib.rs └── rustfmt.toml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Did something not work as expected? 4 | --- 5 | 6 | 7 | 8 | ## 🐛 Bug description 9 | Describe your issue in detail. 10 | 11 | #### 🤔 Expected Behavior 12 | 13 | 14 | #### 👟 Steps to reproduce 15 | 16 | 17 | #### 🌍 Your environment 18 | 19 | 20 | recap version: 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 💡 3 | about: Suggest a new idea for recap 4 | --- 5 | 6 | 7 | 8 | ## 💡 Feature description 9 | 10 | 11 | #### 💻 Basic example 12 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## What did you implement: 8 | 9 | 12 | 13 | Closes: #xxx 14 | 15 | #### How did you verify your change: 16 | 17 | #### What (if anything) would need to be called out in the CHANGELOG for the next release: -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "*.md" 7 | branches: 8 | - master 9 | tags: 10 | - "**" 11 | pull_request: 12 | paths-ignore: 13 | - "*.md" 14 | branches: 15 | - master 16 | 17 | env: 18 | CARGO_TERM_COLOR: always 19 | 20 | jobs: 21 | codestyle: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Set up Rust 25 | uses: hecrj/setup-rust-action@v1 26 | with: 27 | components: rustfmt 28 | rust-version: nightly 29 | - uses: actions/checkout@v2 30 | - run: cargo fmt --all -- --check 31 | 32 | lint: 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Set up Rust 36 | uses: hecrj/setup-rust-action@v1 37 | with: 38 | components: clippy 39 | - uses: actions/checkout@v2 40 | - run: cargo clippy --all-targets -- -D clippy::all 41 | 42 | compile: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Set up Rust 46 | uses: hecrj/setup-rust-action@v1 47 | - uses: actions/checkout@v2 48 | - run: cargo check --all 49 | 50 | test: 51 | needs: [codestyle, lint, compile] 52 | strategy: 53 | matrix: 54 | rust: [stable, beta, nightly] 55 | runs-on: ubuntu-latest 56 | 57 | continue-on-error: ${{ matrix.rust != 'stable' }} 58 | 59 | steps: 60 | - name: Setup Rust 61 | uses: hecrj/setup-rust-action@v1 62 | with: 63 | rust-version: ${{ matrix.rust }} 64 | - name: Checkout 65 | uses: actions/checkout@v2 66 | - name: Test 67 | run: cargo test 68 | 69 | publish-docs: 70 | if: github.ref == 'refs/heads/master' 71 | runs-on: ubuntu-latest 72 | needs: [test] 73 | steps: 74 | - name: Set up Rust 75 | uses: hecrj/setup-rust-action@v1 76 | - uses: actions/checkout@v2 77 | - name: Generate Docs 78 | run: | 79 | cargo doc --no-deps 80 | echo "" > target/doc/index.html 81 | - name: Publish 82 | uses: peaceiris/actions-gh-pages@v3 83 | with: 84 | github_token: ${{ secrets.GITHUB_TOKEN }} 85 | publish_dir: ./target/doc 86 | 87 | publish-crate: 88 | if: startsWith(github.ref, 'refs/tags/') 89 | runs-on: ubuntu-latest 90 | needs: [test] 91 | steps: 92 | - name: Set up Rust 93 | uses: hecrj/setup-rust-action@v1 94 | - uses: actions/checkout@v2 95 | - name: Publish 96 | run: | 97 | pushd recap-derive 98 | cargo publish --token ${{ secrets.CRATES_TOKEN }} 99 | popd 100 | # eventual consistency dictates we wait a bit before publishing 101 | # a crate that depends on the above 102 | sleep 20 103 | pushd recap 104 | cargo publish --token ${{ secrets.CRATES_TOKEN }} 105 | popd 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.2 2 | 3 | * Introduce derived `pub fn is_match(txt: &str) -> bool` associated fn on proc macro'd struct 4 | 5 | # 0.1.1 6 | 7 | * Improve turnaround time by validating recap regex correctness at compile time. Validates regex syntax and arity of named capture groups 8 | 9 | # 0.1.0 10 | 11 | * initial release -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "recap", 4 | "recap-derive" 5 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Doug Tangren 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # recap [![Main](https://github.com/softprops/recap/actions/workflows/main.yml/badge.svg)](https://github.com/softprops/recap/actions/workflows/main.yml) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![crates.io](https://img.shields.io/crates/v/recap.svg)](https://crates.io/crates/recap) [![Released API docs](https://docs.rs/recap/badge.svg)](http://docs.rs/recap) [![Master API docs](https://img.shields.io/badge/docs-master-green.svg)](https://softprops.github.io/recap) 2 | 3 | > deserialize named capture groups into typesafe structs 4 | 5 | Recap is provides what [envy](https://crates.io/crates/envy) provides for environment variables, for[ named capture groups](https://www.regular-expressions.info/named.html). Named regex capture groups are like any other regex capture group but have the extra property that they are associated with name. i.e `(?Psome-pattern)` 6 | 7 | ## 🤔 who is this for 8 | 9 | You may find this crate useful for cases where your application needs to extract information from string input provided by a third party that has a loosely structured format. 10 | 11 | A common usecase for this is when you are dealing with log file data that was not stored in a particular structured format like JSON, but rather in a format that can be represented with a pattern. 12 | 13 | You may also find this useful parsing other loosely formatted data patterns. 14 | 15 | This crate would be less appropriate for cases where your input is provided in a more structured format, like JSON. 16 | I recommend using a crate like [`serde-json`](https://crates.io/crates/serde_json) for those cases instead. 17 | 18 | ## 📦 install 19 | 20 | Add the following to your `Cargo.toml` file. 21 | 22 | ```toml 23 | [dependencies] 24 | recap = "0.1" 25 | ``` 26 | 27 | ## 🤸 usage 28 | 29 | A typical recap usage looks like the following. Assuming your Rust program looks something like this... 30 | 31 | > 💡 These examples use Serde's [derive feature](https://serde.rs/derive.html) 32 | 33 | ```rust 34 | use recap::Recap; 35 | use serde::Deserialize; 36 | use std::error::Error; 37 | 38 | #[derive(Debug, Deserialize, Recap)] 39 | #[recap(regex = r#"(?x) 40 | (?P\d+) 41 | \s+ 42 | (?Ptrue|false) 43 | \s+ 44 | (?P\S+) 45 | "#)] 46 | struct LogEntry { 47 | foo: usize, 48 | bar: bool, 49 | baz: String, 50 | } 51 | 52 | fn main() -> Result<(), Box> { 53 | let logs = r#"1 true hello 54 | 2 false world"#; 55 | 56 | for line in logs.lines() { 57 | let entry: LogEntry = line.parse()?; 58 | println!("{:#?}", entry); 59 | } 60 | 61 | Ok(()) 62 | } 63 | 64 | ``` 65 | 66 | > 👭 Consider this crate a cousin of [envy](https://github.com/softprops/envy), a crate for deserializing environment variables into typesafe structs. 67 | 68 | Doug Tangren (softprops) 2019 69 | -------------------------------------------------------------------------------- /recap-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "recap-derive" 3 | version = "0.1.2" 4 | authors = ["softprops "] 5 | description = "Derives FromStr impl for types which are then deserialized using recap" 6 | license = "MIT" 7 | keywords = ["regex", "serde"] 8 | readme = "../README.md" 9 | documentation = "https://docs.rs/recap-derive" 10 | homepage = "https://github.com/softprops/recap" 11 | repository = "https://github.com/softprops/recap" 12 | edition = "2021" 13 | 14 | [lib] 15 | proc-macro = true 16 | 17 | [badges] 18 | coveralls = { repository = "softprops/recap" } 19 | maintenance = { status = "actively-developed" } 20 | travis-ci = { repository = "softprops/recap" } 21 | 22 | [dependencies] 23 | proc-macro2 = "1" 24 | quote = "1" 25 | regex = "1.2" 26 | syn = "1" 27 | -------------------------------------------------------------------------------- /recap-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use proc_macro::TokenStream; 4 | use proc_macro2::Span; 5 | use quote::quote; 6 | use regex::Regex; 7 | use syn::{ 8 | parse_macro_input, Data::Struct, DataStruct, DeriveInput, Fields, Ident, Lit, Meta, NestedMeta, 9 | }; 10 | 11 | #[proc_macro_derive(Recap, attributes(recap))] 12 | pub fn derive_recap(item: TokenStream) -> TokenStream { 13 | let item = parse_macro_input!(item as DeriveInput); 14 | let regex = extract_regex(&item).expect( 15 | r#"Unable to resolve recap regex. 16 | Make sure your structure has declared an attribute in the form: 17 | #[derive(Deserialize, Recap)] 18 | #[recap(regex ="your-pattern-here")] 19 | struct YourStruct { ... } 20 | "#, 21 | ); 22 | 23 | validate(&item, ®ex); 24 | 25 | let item_ident = &item.ident; 26 | let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl(); 27 | 28 | let has_lifetimes = item.generics.lifetimes().count() > 0; 29 | let impl_from_str = if !has_lifetimes { 30 | quote! { 31 | impl #impl_generics std::str::FromStr for #item_ident #ty_generics #where_clause { 32 | type Err = recap::Error; 33 | fn from_str(s: &str) -> Result { 34 | recap::lazy_static! { 35 | static ref RE: recap::Regex = recap::Regex::new(#regex) 36 | .expect("Failed to compile regex"); 37 | } 38 | 39 | recap::from_captures(&RE, s) 40 | } 41 | } 42 | } 43 | } else { 44 | quote! {} 45 | }; 46 | 47 | let lifetimes = item.generics.lifetimes(); 48 | let also_lifetimes = item.generics.lifetimes(); 49 | let impl_inner = quote! { 50 | impl #impl_generics std::convert::TryFrom<& #(#lifetimes)* str> for #item_ident #ty_generics #where_clause { 51 | type Error = recap::Error; 52 | fn try_from(s: & #(#also_lifetimes)* str) -> Result { 53 | recap::lazy_static! { 54 | static ref RE: recap::Regex = recap::Regex::new(#regex) 55 | .expect("Failed to compile regex"); 56 | } 57 | 58 | recap::from_captures(&RE, s) 59 | } 60 | } 61 | #impl_from_str 62 | }; 63 | 64 | let impl_matcher = quote! { 65 | impl #impl_generics #item_ident #ty_generics #where_clause { 66 | /// Recap derived method. Returns true when some input text 67 | /// matches the regex associated with this type 68 | pub fn is_match(input: &str) -> bool { 69 | recap::lazy_static! { 70 | static ref RE: recap::Regex = recap::Regex::new(#regex) 71 | .expect("Failed to compile regex"); 72 | } 73 | RE.is_match(input) 74 | } 75 | } 76 | }; 77 | 78 | let injector = Ident::new( 79 | &format!("RECAP_IMPL_FOR_{}", item.ident.to_string()), 80 | Span::call_site(), 81 | ); 82 | 83 | let out = quote! { 84 | const #injector: () = { 85 | extern crate recap; 86 | #impl_inner 87 | #impl_matcher 88 | }; 89 | }; 90 | 91 | out.into() 92 | } 93 | 94 | fn validate( 95 | item: &DeriveInput, 96 | regex: &str, 97 | ) { 98 | let regex = Regex::new(regex).unwrap_or_else(|err| { 99 | panic!( 100 | "Invalid regular expression provided for `{}`\n{}", 101 | &item.ident, err 102 | ) 103 | }); 104 | let caps = regex.capture_names().flatten().count(); 105 | let fields = match &item.data { 106 | Struct(DataStruct { 107 | fields: Fields::Named(fs), 108 | .. 109 | }) => fs.named.len(), 110 | _ => panic!("Recap regex can only be applied to Structs with named fields"), 111 | }; 112 | if caps != fields { 113 | panic!( 114 | "Recap could not derive a `FromStr` impl for `{}`.\n\t\t > Expected regex with {} named capture groups to align with struct fields but found {}", 115 | item.ident, fields, caps 116 | ); 117 | } 118 | } 119 | 120 | fn extract_regex(item: &DeriveInput) -> Option { 121 | item.attrs 122 | .iter() 123 | .flat_map(syn::Attribute::parse_meta) 124 | .filter_map(|x| match x { 125 | Meta::List(y) => Some(y), 126 | _ => None, 127 | }) 128 | .filter(|x| x.path.is_ident("recap")) 129 | .flat_map(|x| x.nested.into_iter()) 130 | .filter_map(|x| match x { 131 | NestedMeta::Meta(y) => Some(y), 132 | _ => None, 133 | }) 134 | .filter_map(|x| match x { 135 | Meta::NameValue(y) => Some(y), 136 | _ => None, 137 | }) 138 | .find(|x| x.path.is_ident("regex")) 139 | .and_then(|x| match x.lit { 140 | Lit::Str(y) => Some(y.value()), 141 | _ => None, 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /recap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "recap" 3 | version = "0.1.2" 4 | authors = ["softprops "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "Deserialize typed structures from regex captures" 8 | keywords = ["regex", "serde"] 9 | readme = "../README.md" 10 | documentation = "https://docs.rs/recap" 11 | homepage = "https://github.com/softprops/recap" 12 | repository = "https://github.com/softprops/recap" 13 | 14 | [badges] 15 | coveralls = { repository = "softprops/recap" } 16 | maintenance = { status = "actively-developed" } 17 | travis-ci = { repository = "softprops/recap" } 18 | 19 | [dependencies] 20 | envy = "0.4" 21 | lazy_static = "1.3" 22 | recap-derive = { version = "0.1.2", path = "../recap-derive", optional = true } 23 | regex = "1.2" 24 | serde = { version = "1.0", features = ["derive"] } 25 | 26 | [features] 27 | default = ["derive"] 28 | derive = ["recap-derive"] -------------------------------------------------------------------------------- /recap/examples/log.rs: -------------------------------------------------------------------------------- 1 | use recap::Recap; 2 | use serde::Deserialize; 3 | use std::error::Error; 4 | 5 | #[derive(Debug, Deserialize, Recap)] 6 | #[recap(regex = r#"(?x) 7 | (?P\d+) 8 | \s+ 9 | (?Ptrue|false) 10 | \s+ 11 | (?P\S+) 12 | "#)] 13 | #[allow(dead_code)] 14 | struct LogEntry { 15 | foo: usize, 16 | bar: bool, 17 | baz: String, 18 | } 19 | 20 | fn main() -> Result<(), Box> { 21 | let logs = r#"1 true hello 22 | 2 false world"#; 23 | 24 | for line in logs.lines() { 25 | if LogEntry::is_match(line) { 26 | let entry: LogEntry = line.parse()?; 27 | println!("{:#?}", entry); 28 | } 29 | } 30 | 31 | Ok(()) 32 | } 33 | -------------------------------------------------------------------------------- /recap/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Recap deserializes structures from regex [named capture groups](https://www.regular-expressions.info/named.html) 2 | //! extracted from strings. 3 | //! 4 | //! You may find this crate useful for cases where input is provided as a raw string in a loosely structured format. 5 | //! A common use case for this is when you're dealing with log file data that was not stored in a particular structed format 6 | //! like JSON but rather in a format that can be represented with a pattern. 7 | //! 8 | //! Recap is provides what [envy](https://crates.io/crates/envy) provides environment variables for named regex capture groups 9 | //! 10 | //! 11 | //! # Examples 12 | //! 13 | //! Below is an example that derives a `FromStr` for your type that will 14 | //! parse into the struct using named capture groups 15 | //! 16 | //! ```rust 17 | //! use recap::Recap; 18 | //! use serde::Deserialize; 19 | //! use std::error::Error; 20 | //! 21 | //! #[derive(Debug, Deserialize, PartialEq, Recap)] 22 | //! #[recap(regex=r#"(?P\S+)\s(?P\S+)"#)] 23 | //! struct Example { 24 | //! foo: String, 25 | //! bar: String, 26 | //! } 27 | //! 28 | //! fn main() -> Result<(), Box> { 29 | //! 30 | //! assert_eq!( 31 | //! "hello there".parse::()?, 32 | //! Example { 33 | //! foo: "hello".into(), 34 | //! bar: "there".into() 35 | //! } 36 | //! ); 37 | //! 38 | //! Ok(()) 39 | //! } 40 | //! ``` 41 | //! 42 | //! You can also use Recap with Serde's zero-copy deserialization: 43 | //! 44 | //! ```rust 45 | //! use recap::Recap; 46 | //! use serde::Deserialize; 47 | //! use std::convert::TryInto; 48 | //! use std::error::Error; 49 | //! 50 | //! #[derive(Debug, Deserialize, PartialEq, Recap)] 51 | //! #[recap(regex=r#"(?P\S+)\s(?P\S+)"#)] 52 | //! struct Example<'a> { 53 | //! foo: &'a str, 54 | //! bar: &'a str, 55 | //! } 56 | //! 57 | //! fn main() -> Result<(), Box> { 58 | //! let input = "hello there"; 59 | //! let result: Example = input.try_into()?; 60 | //! assert_eq!( 61 | //! result, 62 | //! Example { 63 | //! foo: "hello", 64 | //! bar: "there" 65 | //! } 66 | //! ); 67 | //! 68 | //! Ok(()) 69 | //! } 70 | //! ``` 71 | //! 72 | //! You can also use recap by using the generic function `from_captures` in which 73 | //! case you'll be reponsible for bringing your only Regex reference. 74 | //! 75 | //! 💡 For convenience the [regex](https://crates.io/crates/regex) crate's [`Regex`](https://docs.rs/regex/latest/regex/struct.Regex.html) 76 | //! type is re-exported 77 | //! 78 | //! ```rust 79 | //! use recap::{Regex, from_captures}; 80 | //! use serde::Deserialize; 81 | //! use std::error::Error; 82 | //! 83 | //! #[derive(Debug, Deserialize, PartialEq)] 84 | //! struct Example { 85 | //! foo: String, 86 | //! bar: String, 87 | //! } 88 | //! 89 | //! fn main() -> Result<(), Box> { 90 | //! let pattern = Regex::new( 91 | //! r#"(?P\S+)\s(?P\S+)"# 92 | //! )?; 93 | //! 94 | //! let example: Example = from_captures( 95 | //! &pattern, "hello there" 96 | //! )?; 97 | //! 98 | //! assert_eq!( 99 | //! example, 100 | //! Example { 101 | //! foo: "hello".into(), 102 | //! bar: "there".into() 103 | //! } 104 | //! ); 105 | //! 106 | //! Ok(()) 107 | //! } 108 | //! ``` 109 | pub use regex::Regex; 110 | use serde::de::{ 111 | self, 112 | value::{BorrowedStrDeserializer, MapDeserializer, SeqDeserializer}, 113 | Deserialize, IntoDeserializer, 114 | }; 115 | 116 | // used in derive crate output 117 | // to derive a static for compiled 118 | // regex 119 | #[cfg(feature = "derive")] 120 | #[doc(hidden)] 121 | pub use lazy_static::lazy_static; 122 | 123 | // Re-export for #[derive(Recap)] 124 | #[cfg(feature = "derive")] 125 | #[allow(unused_imports)] 126 | #[macro_use] 127 | extern crate recap_derive; 128 | #[cfg(feature = "derive")] 129 | #[doc(hidden)] 130 | pub use recap_derive::*; 131 | 132 | /// A type which encapsulates recap errors 133 | pub type Error = envy::Error; 134 | type Result = envy::Result; 135 | 136 | struct Vars<'a, Iter>(Iter) 137 | where 138 | Iter: IntoIterator; 139 | 140 | struct Val<'a>(&'a str, &'a str); 141 | 142 | impl<'a: 'de, 'de> IntoDeserializer<'de, Error> for Val<'a> { 143 | type Deserializer = Self; 144 | 145 | fn into_deserializer(self) -> Self::Deserializer { 146 | self 147 | } 148 | } 149 | 150 | struct VarName<'a>(&'a str); 151 | 152 | impl<'a: 'de, 'de> IntoDeserializer<'de, Error> for VarName<'a> { 153 | type Deserializer = Self; 154 | 155 | fn into_deserializer(self) -> Self::Deserializer { 156 | self 157 | } 158 | } 159 | 160 | impl<'a, Iter: Iterator> Iterator for Vars<'a, Iter> { 161 | type Item = (VarName<'a>, Val<'a>); 162 | 163 | fn next(&mut self) -> Option { 164 | self.0.next().map(|(k, v)| (VarName(k), Val(k, v))) 165 | } 166 | } 167 | 168 | macro_rules! forward_parsed_values { 169 | ($($ty:ident => $method:ident,)*) => { 170 | $( 171 | fn $method(self, visitor: V) -> Result 172 | where V: de::Visitor<'de> 173 | { 174 | match self.1.parse::<$ty>() { 175 | Ok(val) => val.into_deserializer().$method(visitor), 176 | Err(e) => Err(de::Error::custom(format_args!("{} while parsing value '{}' provided by {}", e, self.1, self.0))) 177 | } 178 | } 179 | )* 180 | } 181 | } 182 | 183 | impl<'a: 'de, 'de> de::Deserializer<'de> for Val<'a> { 184 | type Error = Error; 185 | fn deserialize_any( 186 | self, 187 | visitor: V, 188 | ) -> Result 189 | where 190 | V: de::Visitor<'de>, 191 | { 192 | BorrowedStrDeserializer::new(self.1).deserialize_any(visitor) 193 | } 194 | 195 | fn deserialize_seq( 196 | self, 197 | visitor: V, 198 | ) -> Result 199 | where 200 | V: de::Visitor<'de>, 201 | { 202 | let values = self.1.split(',').map(|v| Val(self.0, v)); 203 | SeqDeserializer::new(values).deserialize_seq(visitor) 204 | } 205 | 206 | fn deserialize_option( 207 | self, 208 | visitor: V, 209 | ) -> Result 210 | where 211 | V: de::Visitor<'de>, 212 | { 213 | visitor.visit_some(self) 214 | } 215 | 216 | forward_parsed_values! { 217 | bool => deserialize_bool, 218 | u8 => deserialize_u8, 219 | u16 => deserialize_u16, 220 | u32 => deserialize_u32, 221 | u64 => deserialize_u64, 222 | i8 => deserialize_i8, 223 | i16 => deserialize_i16, 224 | i32 => deserialize_i32, 225 | i64 => deserialize_i64, 226 | f32 => deserialize_f32, 227 | f64 => deserialize_f64, 228 | } 229 | 230 | #[inline] 231 | fn deserialize_newtype_struct( 232 | self, 233 | _: &'static str, 234 | visitor: V, 235 | ) -> Result 236 | where 237 | V: serde::de::Visitor<'de>, 238 | { 239 | visitor.visit_newtype_struct(self) 240 | } 241 | 242 | fn deserialize_enum( 243 | self, 244 | _name: &'static str, 245 | _variants: &'static [&'static str], 246 | visitor: V, 247 | ) -> Result 248 | where 249 | V: de::Visitor<'de>, 250 | { 251 | visitor.visit_enum(self.1.into_deserializer()) 252 | } 253 | 254 | serde::forward_to_deserialize_any! { 255 | char str string unit 256 | bytes byte_buf map unit_struct tuple_struct 257 | identifier tuple ignored_any 258 | struct 259 | } 260 | } 261 | 262 | impl<'a: 'de, 'de> de::Deserializer<'de> for VarName<'a> { 263 | type Error = Error; 264 | fn deserialize_any( 265 | self, 266 | visitor: V, 267 | ) -> Result 268 | where 269 | V: de::Visitor<'de>, 270 | { 271 | self.0.into_deserializer().deserialize_any(visitor) 272 | } 273 | 274 | #[inline] 275 | fn deserialize_newtype_struct( 276 | self, 277 | _: &'static str, 278 | visitor: V, 279 | ) -> Result 280 | where 281 | V: serde::de::Visitor<'de>, 282 | { 283 | visitor.visit_newtype_struct(self) 284 | } 285 | 286 | serde::forward_to_deserialize_any! { 287 | char str string unit seq option 288 | bytes byte_buf map unit_struct tuple_struct 289 | identifier tuple ignored_any enum 290 | struct bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 291 | } 292 | } 293 | 294 | /// A deserializer for env vars 295 | struct Deserializer<'a, 'de: 'a, Iter: Iterator> { 296 | inner: MapDeserializer<'de, Vars<'a, Iter>, Error>, 297 | } 298 | 299 | impl<'a, 'de: 'a, Iter: Iterator> Deserializer<'a, 'de, Iter> { 300 | fn new(vars: Iter) -> Self { 301 | Deserializer { 302 | inner: MapDeserializer::new(Vars(vars)), 303 | } 304 | } 305 | } 306 | 307 | impl<'a: 'de, 'de, Iter: Iterator> de::Deserializer<'de> 308 | for Deserializer<'a, 'de, Iter> 309 | { 310 | type Error = Error; 311 | fn deserialize_any( 312 | self, 313 | visitor: V, 314 | ) -> Result 315 | where 316 | V: de::Visitor<'de>, 317 | { 318 | self.deserialize_map(visitor) 319 | } 320 | 321 | fn deserialize_map( 322 | self, 323 | visitor: V, 324 | ) -> Result 325 | where 326 | V: de::Visitor<'de>, 327 | { 328 | visitor.visit_map(self.inner) 329 | } 330 | 331 | serde::forward_to_deserialize_any! { 332 | bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string unit seq 333 | bytes byte_buf unit_struct tuple_struct 334 | identifier tuple ignored_any option newtype_struct enum 335 | struct 336 | } 337 | } 338 | 339 | /// Deserializes a type based on an iterable of `(&str, &str)` 340 | /// representing keys and values 341 | fn from_iter<'a, Iter, T>(iter: Iter) -> Result 342 | where 343 | T: de::Deserialize<'a>, 344 | Iter: IntoIterator, 345 | { 346 | T::deserialize(Deserializer::new(iter.into_iter())) 347 | } 348 | 349 | /// Deserialize a type from named regex capture groups 350 | /// 351 | /// See module level documentation for examples 352 | pub fn from_captures<'a, D>( 353 | re: &'a Regex, 354 | input: &'a str, 355 | ) -> Result 356 | where 357 | D: Deserialize<'a>, 358 | { 359 | let caps = re.captures(input).ok_or_else(|| { 360 | envy::Error::Custom(format!("No captures resolved in string '{}'", input)) 361 | })?; 362 | from_iter( 363 | re.capture_names() 364 | .map(|maybe_name| { 365 | maybe_name.and_then(|name| caps.name(name).map(|val| (name, val.as_str()))) 366 | }) 367 | .flatten(), 368 | ) 369 | } 370 | 371 | #[cfg(test)] 372 | mod tests { 373 | use super::{from_captures, Regex}; 374 | use serde::Deserialize; 375 | use std::error::Error; 376 | 377 | #[derive(Debug, PartialEq, Deserialize)] 378 | struct LogEntry { 379 | foo: String, 380 | bar: String, 381 | baz: String, 382 | } 383 | 384 | #[derive(Debug, PartialEq, Deserialize)] 385 | struct LogEntryOptional { 386 | foo: String, 387 | bar: String, 388 | baz: Option, 389 | } 390 | 391 | #[derive(Debug, PartialEq, Deserialize)] 392 | struct LogEntryBorrowed<'a> { 393 | foo: &'a str, 394 | bar: &'a str, 395 | baz: &'a str, 396 | } 397 | 398 | #[test] 399 | fn deserializes_matching_captures_optional() -> Result<(), Box> { 400 | assert_eq!( 401 | from_captures::( 402 | &Regex::new( 403 | r#"(?x) 404 | (?P\S+) 405 | \s+ 406 | (?P\S+) 407 | \s+ 408 | (?P\S+)? 409 | "# 410 | )?, 411 | "one two " 412 | )?, 413 | LogEntryOptional { 414 | foo: "one".into(), 415 | bar: "two".into(), 416 | baz: None 417 | } 418 | ); 419 | 420 | Ok(()) 421 | } 422 | 423 | #[test] 424 | fn deserializes_matching_captures() -> Result<(), Box> { 425 | assert_eq!( 426 | from_captures::( 427 | &Regex::new( 428 | r#"(?x) 429 | (?P\S+) 430 | \s+ 431 | (?P\S+) 432 | \s+ 433 | (?P\S+) 434 | "# 435 | )?, 436 | "one two three" 437 | )?, 438 | LogEntry { 439 | foo: "one".into(), 440 | bar: "two".into(), 441 | baz: "three".into() 442 | } 443 | ); 444 | 445 | Ok(()) 446 | } 447 | 448 | #[test] 449 | fn deserializes_zero_copy() -> Result<(), Box> { 450 | let input = "one two three"; 451 | assert_eq!( 452 | from_captures::( 453 | &Regex::new( 454 | r#"(?x) 455 | (?P\S+) 456 | \s+ 457 | (?P\S+) 458 | \s+ 459 | (?P\S+) 460 | "# 461 | )?, 462 | input 463 | )?, 464 | LogEntryBorrowed { 465 | foo: "one", 466 | bar: "two", 467 | baz: "three" 468 | } 469 | ); 470 | 471 | Ok(()) 472 | } 473 | 474 | #[test] 475 | fn fails_without_captures() -> Result<(), Box> { 476 | let result = from_captures::(&Regex::new("test")?, "one two three"); 477 | match result { 478 | Ok(_) => panic!("should have failed"), 479 | // enum variants on type aliases are experimental 480 | Err(err) => assert_eq!( 481 | err.to_string(), 482 | "No captures resolved in string \'one two three\'" 483 | ), 484 | } 485 | 486 | Ok(()) 487 | } 488 | 489 | #[test] 490 | fn fails_with_unmatched_captures() -> Result<(), Box> { 491 | let result = from_captures::(&Regex::new(".+")?, "one two three"); 492 | match result { 493 | Ok(_) => panic!("should have failed"), 494 | // enum variants on type aliases are experimental 495 | Err(err) => assert_eq!(err.to_string(), "missing value for field foo"), 496 | } 497 | 498 | Ok(()) 499 | } 500 | } 501 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#fn_args_layout 2 | fn_args_layout = "Vertical" 3 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#imports_granularity 4 | imports_granularity = "Crate" --------------------------------------------------------------------------------