├── .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 [](https://github.com/softprops/recap/actions/workflows/main.yml) [](LICENSE) [](https://crates.io/crates/recap) [](http://docs.rs/recap) [](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"
--------------------------------------------------------------------------------