Skip to content

Commit 014bd57

Browse files
committed
chore: merge detrim into repo
1 parent d1089d1 commit 014bd57

File tree

9 files changed

+730
-0
lines changed

9 files changed

+730
-0
lines changed

crates/detrim/CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
3+
## Unreleased
4+
5+
## 0.1.4
6+
7+
- No significant changes since `0.1.3`.
8+
9+
## 0.1.3
10+
11+
- Fix `cow_str` when expecting to borrow from input data.
12+
13+
## 0.1.2
14+
15+
- Add `hashset_string()` function behind on-by-default `std` crate feature.
16+
- Fix `string_non_empty()` and `option_string_non_empty()` functions when encountering `null`s.
17+
18+
## 0.1.1
19+
20+
- Add `option_string()` function.
21+
- Add `vec_string()` function.
22+
23+
## 0.1.0
24+
25+
- Add `string()` function.
26+
- Add `string_non_empty()` function.
27+
- Add `option_string_non_empty()` function.

crates/detrim/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "detrim"
3+
version = "0.1.4"
4+
description = "Automatic string trimming with serde"
5+
categories = ["encoding", "no-std"]
6+
keywords = ["deserialization", "utilities", "serde"]
7+
authors.workspace = true
8+
repository.workspace = true
9+
license.workspace = true
10+
edition.workspace = true
11+
rust-version.workspace = true
12+
13+
[package.metadata.docs.rs]
14+
all-features = true
15+
rustdoc-args = ["--cfg", "docsrs"]
16+
17+
[features]
18+
default = ["std"]
19+
std = []
20+
21+
[dependencies]
22+
serde = { version = "1", default-features = false, features = ["alloc"] }
23+
24+
[dev-dependencies]
25+
serde = { version = "1", features = ["std", "derive"] }
26+
serde_json = "1"
27+
28+
[lints]
29+
workspace = true

crates/detrim/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# `detrim`
2+
3+
<!-- prettier-ignore-start -->
4+
5+
[![crates.io](https://img.shields.io/crates/v/detrim?label=latest)](https://crates.io/crates/detrim)
6+
[![Documentation](https://docs.rs/detrim/badge.svg?version=0.1.4)](https://docs.rs/detrim/0.1.4)
7+
[![dependency status](https://deps.rs/crate/detrim/0.1.4/status.svg)](https://deps.rs/crate/detrim/0.1.4)
8+
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/detrim.svg)
9+
<br />
10+
[![CI](https://github.com/x52dev/detrim/actions/workflows/ci.yml/badge.svg)](https://github.com/x52dev/detrim/actions/workflows/ci.yml)
11+
[![codecov](https://codecov.io/gh/x52dev/detrim/branch/main/graph/badge.svg)](https://codecov.io/gh/x52dev/detrim)
12+
![Version](https://img.shields.io/badge/rustc-1.56.1+-ab6000.svg)
13+
[![Download](https://img.shields.io/crates/d/detrim.svg)](https://crates.io/crates/detrim)
14+
15+
<!-- prettier-ignore-end -->
16+
17+
<!-- cargo-rdme start -->
18+
19+
**De**serialization **trim**ming for strings in serde models.
20+
21+
## Examples
22+
23+
```rust
24+
#[derive(Debug, serde::Deserialize)]
25+
struct Form {
26+
#[serde(deserialize_with = "detrim::string")]
27+
name: String,
28+
}
29+
30+
let form = serde_json::from_str::<Form>(r#"{ "name": "ferris" }"#).unwrap();
31+
assert_eq!(form.name, "ferris");
32+
33+
let form = serde_json::from_str::<Form>(r#"{ "name": " ferris " }"#).unwrap();
34+
assert_eq!(form.name, "ferris");
35+
```
36+
37+
<!-- cargo-rdme end -->

crates/detrim/src/cow_str.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use alloc::{
2+
borrow::{Cow, ToOwned as _},
3+
str,
4+
string::String,
5+
vec::Vec,
6+
};
7+
use core::fmt;
8+
9+
use serde::{de, Deserializer};
10+
11+
/// Trims a CoW string during deserialization.
12+
pub fn cow_str<'a, 'de: 'a, D: Deserializer<'de>>(de: D) -> Result<Cow<'a, str>, D::Error> {
13+
struct CowStrVisitor;
14+
15+
impl<'a> de::Visitor<'a> for CowStrVisitor {
16+
type Value = Cow<'a, str>;
17+
18+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
19+
formatter.write_str("a string")
20+
}
21+
22+
fn visit_str<E: de::Error>(self, val: &str) -> Result<Self::Value, E> {
23+
Ok(Cow::Owned(val.trim().to_owned()))
24+
}
25+
26+
fn visit_borrowed_str<E: de::Error>(self, val: &'a str) -> Result<Self::Value, E> {
27+
Ok(Cow::Borrowed(val.trim()))
28+
}
29+
30+
fn visit_string<E: de::Error>(self, val: String) -> Result<Self::Value, E> {
31+
Ok(Cow::Owned(val.trim().to_owned()))
32+
}
33+
34+
fn visit_bytes<E: de::Error>(self, val: &[u8]) -> Result<Self::Value, E> {
35+
match str::from_utf8(val) {
36+
Ok(val) => Ok(Cow::Owned(val.trim().to_owned())),
37+
Err(_) => Err(de::Error::invalid_value(de::Unexpected::Bytes(val), &self)),
38+
}
39+
}
40+
41+
fn visit_borrowed_bytes<E: de::Error>(self, val: &'a [u8]) -> Result<Self::Value, E> {
42+
match str::from_utf8(val) {
43+
Ok(val) => Ok(Cow::Borrowed(val.trim())),
44+
Err(_) => Err(de::Error::invalid_value(de::Unexpected::Bytes(val), &self)),
45+
}
46+
}
47+
48+
fn visit_byte_buf<E: de::Error>(self, val: Vec<u8>) -> Result<Self::Value, E> {
49+
match String::from_utf8(val) {
50+
Ok(val) => Ok(Cow::Owned(val.trim().to_owned())),
51+
Err(err) => Err(de::Error::invalid_value(
52+
de::Unexpected::Bytes(&err.into_bytes()),
53+
&self,
54+
)),
55+
}
56+
}
57+
}
58+
59+
de.deserialize_str(CowStrVisitor)
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use serde::Deserialize;
65+
66+
use super::*;
67+
68+
#[test]
69+
fn cow_str() {
70+
#[derive(Debug, Deserialize, PartialEq, Eq)]
71+
struct Foo<'a> {
72+
#[serde(borrow, deserialize_with = "super::cow_str")]
73+
foo: Cow<'a, str>,
74+
}
75+
76+
impl<'a> Foo<'a> {
77+
fn new(foo: impl Into<Cow<'a, str>>) -> Self {
78+
Self { foo: foo.into() }
79+
}
80+
}
81+
82+
serde_json::from_str::<Foo<'static>>(r#"{ "foo": 1 }"#).unwrap_err();
83+
serde_json::from_str::<Foo<'static>>(r#"{ "foo": true }"#).unwrap_err();
84+
85+
assert_eq!(
86+
Foo::new(""),
87+
serde_json::from_str(r#"{ "foo": "" }"#).unwrap(),
88+
);
89+
assert_eq!(
90+
Foo::new(""),
91+
serde_json::from_str(r#"{ "foo": " " }"#).unwrap(),
92+
);
93+
assert_eq!(
94+
Foo::new("bar"),
95+
serde_json::from_str(r#"{ "foo": "bar" }"#).unwrap(),
96+
);
97+
assert_eq!(
98+
Foo::new("bar"),
99+
serde_json::from_str(r#"{ "foo": " bar" }"#).unwrap(),
100+
);
101+
assert_eq!(
102+
Foo::new("bar"),
103+
serde_json::from_str(r#"{ "foo": " bar" }"#).unwrap(),
104+
);
105+
assert_eq!(
106+
Foo::new("bar"),
107+
serde_json::from_str(r#"{ "foo": "bar " }"#).unwrap(),
108+
);
109+
assert_eq!(
110+
Foo::new("bar"),
111+
serde_json::from_str(r#"{ "foo": " bar " }"#).unwrap(),
112+
);
113+
}
114+
115+
#[test]
116+
fn cow_str_allows_borrows() {
117+
#[derive(Debug, Deserialize, PartialEq, Eq)]
118+
struct Foo<'a> {
119+
#[serde(borrow, deserialize_with = "super::cow_str")]
120+
foo: Cow<'a, str>,
121+
}
122+
123+
// borrowed when no trimming is needed
124+
let source = br#"{ "foo": "bar" }"#.to_vec();
125+
let json = serde_json::from_slice::<Foo<'_>>(&source).unwrap();
126+
assert!(matches!(&json.foo, Cow::Borrowed(_)));
127+
assert_eq!(json.foo, "bar");
128+
129+
// borrowed and trimmed
130+
let source = br#"{ "foo": " bar " }"#.to_vec();
131+
let json = serde_json::from_slice::<Foo<'_>>(&source).unwrap();
132+
assert!(matches!(&json.foo, Cow::Borrowed(_)));
133+
assert_eq!(json.foo, "bar");
134+
135+
// owned and trimmed when escape sequences need processing
136+
let source = br#"{ "foo": " b\\ar " }"#.to_vec();
137+
let json = serde_json::from_slice::<Foo<'_>>(&source).unwrap();
138+
assert!(matches!(&json.foo, Cow::Owned(_)));
139+
assert_eq!(json.foo, "b\\ar");
140+
}
141+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::{
2+
borrow::ToOwned as _, collections::HashSet, iter::FromIterator as _, string::String, vec::Vec,
3+
};
4+
5+
use serde::{Deserialize as _, Deserializer};
6+
7+
/// Trims set of strings during deserialization.
8+
///
9+
/// Strings are deduplicated _after_ being trimmed (i.e., differences in extraneous whitespace are
10+
/// handled).
11+
pub fn hashset_string<'a, D: Deserializer<'a>>(de: D) -> Result<HashSet<String>, D::Error> {
12+
let mut set = Vec::<String>::deserialize(de)?;
13+
14+
for item in &mut set {
15+
#[allow(clippy::assigning_clones)]
16+
{
17+
*item = item.trim().to_owned();
18+
}
19+
}
20+
21+
Ok(HashSet::from_iter(set))
22+
}
23+
24+
#[cfg(test)]
25+
mod tests {
26+
use serde::Deserialize;
27+
28+
use super::*;
29+
30+
#[test]
31+
fn hashset_string() {
32+
#[derive(Debug, Deserialize, PartialEq, Eq)]
33+
struct Foo {
34+
#[serde(deserialize_with = "super::hashset_string")]
35+
foo: HashSet<String>,
36+
}
37+
38+
impl Foo {
39+
fn new(foo: impl IntoIterator<Item = impl Into<String>>) -> Self {
40+
Self {
41+
foo: foo.into_iter().map(Into::into).collect(),
42+
}
43+
}
44+
}
45+
46+
serde_json::from_str::<Foo>(r#"{ "foo": 1 }"#).unwrap_err();
47+
serde_json::from_str::<Foo>(r#"{ "foo": "" }"#).unwrap_err();
48+
49+
assert_eq!(
50+
Foo::new([""; 0]),
51+
serde_json::from_str(r#"{ "foo": [] }"#).unwrap(),
52+
);
53+
assert_eq!(
54+
Foo::new([""]),
55+
serde_json::from_str(r#"{ "foo": [""] }"#).unwrap(),
56+
);
57+
assert_eq!(
58+
Foo::new([""]),
59+
serde_json::from_str(r#"{ "foo": [" "] }"#).unwrap(),
60+
);
61+
assert_eq!(
62+
Foo::new(["bar"]),
63+
serde_json::from_str(r#"{ "foo": ["bar"] }"#).unwrap(),
64+
);
65+
assert_eq!(
66+
Foo::new(["bar"]),
67+
serde_json::from_str(r#"{ "foo": [" bar"] }"#).unwrap(),
68+
);
69+
assert_eq!(
70+
Foo::new(["bar"]),
71+
serde_json::from_str(r#"{ "foo": [" bar"] }"#).unwrap(),
72+
);
73+
assert_eq!(
74+
Foo::new(["bar"]),
75+
serde_json::from_str(r#"{ "foo": ["bar "] }"#).unwrap(),
76+
);
77+
assert_eq!(
78+
Foo::new(["bar"]),
79+
serde_json::from_str(r#"{ "foo": [" bar "] }"#).unwrap(),
80+
);
81+
assert_eq!(
82+
Foo::new(["bar"]),
83+
serde_json::from_str(r#"{ "foo": [" bar ", " bar "] }"#).unwrap(),
84+
);
85+
assert_eq!(
86+
Foo::new(["bar"]),
87+
serde_json::from_str(r#"{ "foo": [" bar ", " bar"] }"#).unwrap(),
88+
);
89+
}
90+
}

crates/detrim/src/lib.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//! **De**serialization **trim**ming for strings in serde models.
2+
//!
3+
//! # Examples
4+
//!
5+
//! ```
6+
//! #[derive(Debug, serde::Deserialize)]
7+
//! struct Form {
8+
//! #[serde(deserialize_with = "detrim::string")]
9+
//! name: String,
10+
//! }
11+
//!
12+
//! let form = serde_json::from_str::<Form>(r#"{ "name": "ferris" }"#).unwrap();
13+
//! assert_eq!(form.name, "ferris");
14+
//!
15+
//! let form = serde_json::from_str::<Form>(r#"{ "name": " ferris " }"#).unwrap();
16+
//! assert_eq!(form.name, "ferris");
17+
//! ```
18+
19+
#![cfg_attr(not(feature = "std"), no_std)]
20+
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
21+
22+
extern crate alloc;
23+
24+
mod cow_str;
25+
#[cfg(feature = "std")]
26+
mod hashset_string;
27+
mod string;
28+
mod string_non_empty;
29+
mod vec_string;
30+
31+
#[cfg(feature = "std")]
32+
pub use crate::hashset_string::hashset_string;
33+
pub use crate::{
34+
cow_str::cow_str,
35+
string::{option_string, str, string},
36+
string_non_empty::{option_string_non_empty, string_non_empty},
37+
vec_string::vec_string,
38+
};

0 commit comments

Comments
 (0)