Skip to content

Commit 0e70545

Browse files
authored
Merge pull request #10 from tomoikey/serde
support serde_json
2 parents 64efce4 + 721df36 commit 0e70545

File tree

8 files changed

+180
-6
lines changed

8 files changed

+180
-6
lines changed

Cargo.toml

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ repository = "https://github.com/tomoikey/refined-type"
66
readme = "README.md"
77
categories = ["development-tools"]
88
license = "MIT"
9-
version = "0.3.6"
9+
version = "0.3.7"
1010
edition = "2021"
1111

1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

1414
[dependencies]
15-
regex = "1.10.3"
15+
regex = "1.10.3"
16+
serde = { version = "1.0.197", features = ["derive"] }
17+
serde_json = "1.0.114"
18+
19+
[dev-dependencies]
20+
anyhow = "1.0.80"

README.md

+57-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type NonEmptyString = Refined<NonEmptyStringRule>;
1313

1414
fn main() {
1515
let hello_world = NonEmptyString::new("hello world".to_string());
16-
assert_eq!(hello_world.unwrap().deref(), "hello world");
16+
assert_eq!(hello_world.unwrap().into_value(), "hello world");
1717

1818
let empty_string = NonEmptyString::new("".to_string());
1919
assert!(empty_string.is_err());
@@ -186,6 +186,62 @@ fn main() {
186186
}
187187
```
188188

189+
# JSON
190+
`refined_type` is compatible with `serde_json`. This ensures type-safe communication and eliminates the need to write new validation processes. All you need to do is implement a set of rules once and implement `serde`’s `Serialize` and `Deserialize`.
191+
### Serialize
192+
```rust
193+
type NonEmptyString = Refined<NonEmptyStringRule>;
194+
195+
#[derive(Serialize)]
196+
struct Human {
197+
name: NonEmptyString,
198+
age: u8,
199+
}
200+
201+
fn main() -> anyhow::Result<()> {
202+
let john = Human {
203+
name: NonEmptyString::new("john".to_string())?,
204+
age: 8,
205+
};
206+
207+
let actual = json!(john);
208+
let expected = json! {{
209+
"name": "john",
210+
"age": 8
211+
}};
212+
assert_eq!(actual, expected);
213+
Ok(())
214+
}
215+
```
216+
217+
### Deserialize
218+
```rust
219+
type NonEmptyString = Refined<NonEmptyStringRule>;
220+
221+
#[derive(Debug, Eq, PartialEq, Deserialize)]
222+
struct Human {
223+
name: NonEmptyString,
224+
age: u8,
225+
}
226+
227+
fn test_refined_deserialize_json_ok_struct() -> anyhow::Result<()> {
228+
let json = json! {{
229+
"name": "john",
230+
"age": 8
231+
}}
232+
.to_string();
233+
234+
let actual = serde_json::from_str::<Human>(&json)?;
235+
236+
let expected = Human {
237+
name: NonEmptyString::new("john".to_string())?,
238+
age: 8,
239+
};
240+
assert_eq!(actual, expected);
241+
Ok(())
242+
}
243+
```
244+
189245
# Tips
190246
Directly writing `And`, `Or`, `Not` or `Refined` can often lead to a decrease in readability.
191247
Therefore, using **type aliases** can help make your code clearer.

src/refined.rs

+109-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::result::Error;
22
use crate::rule::Rule;
3+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
34
use std::fmt::{Display, Formatter};
45
use std::marker::PhantomData;
56
use std::ops::Deref;
@@ -17,7 +18,7 @@ use std::ops::Deref;
1718
/// let empty_string_result = Refined::<NonEmptyStringRule>::new("".to_string());
1819
/// assert!(empty_string_result.is_err())
1920
/// ```
20-
#[derive(Debug, PartialEq, Eq, Clone)]
21+
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
2122
pub struct Refined<RULE>
2223
where
2324
RULE: Rule,
@@ -26,6 +27,35 @@ where
2627
_rule: PhantomData<RULE>,
2728
}
2829

30+
impl<RULE, T> Serialize for Refined<RULE>
31+
where
32+
RULE: Rule<Item = T>,
33+
T: Serialize,
34+
{
35+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
36+
where
37+
S: Serializer,
38+
{
39+
self.value.serialize(serializer)
40+
}
41+
}
42+
43+
impl<'de, RULE, T> Deserialize<'de> for Refined<RULE>
44+
where
45+
RULE: Rule<Item = T>,
46+
T: Deserialize<'de>,
47+
{
48+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49+
where
50+
D: Deserializer<'de>,
51+
{
52+
use serde::de::Error;
53+
let item: T = Deserialize::deserialize(deserializer)?;
54+
let refined = Refined::new(item).map_err(|e| Error::custom(e.to_string()))?;
55+
Ok(refined)
56+
}
57+
}
58+
2959
impl<RULE, T> Refined<RULE>
3060
where
3161
RULE: Rule<Item = T>,
@@ -72,6 +102,8 @@ mod test {
72102
use crate::refined::Refined;
73103
use crate::result::Error;
74104
use crate::rule::NonEmptyStringRule;
105+
use serde::{Deserialize, Serialize};
106+
use serde_json::json;
75107

76108
#[test]
77109
fn test_refined_non_empty_string_ok() -> Result<(), Error<String>> {
@@ -93,4 +125,80 @@ mod test {
93125
assert_eq!(format!("{}", non_empty_string), "Hello");
94126
Ok(())
95127
}
128+
129+
#[test]
130+
fn test_refined_serialize_json_string() -> anyhow::Result<()> {
131+
let non_empty_string = Refined::<NonEmptyStringRule>::new("hello".to_string())?;
132+
133+
let actual = json!(non_empty_string);
134+
let expected = json!("hello");
135+
assert_eq!(actual, expected);
136+
Ok(())
137+
}
138+
139+
#[test]
140+
fn test_refined_serialize_json_struct() -> anyhow::Result<()> {
141+
type NonEmptyString = Refined<NonEmptyStringRule>;
142+
#[derive(Serialize)]
143+
struct Human {
144+
name: NonEmptyString,
145+
age: u8,
146+
}
147+
148+
let john = Human {
149+
name: NonEmptyString::new("john".to_string())?,
150+
age: 8,
151+
};
152+
153+
let actual = json!(john);
154+
let expected = json! {{
155+
"name": "john",
156+
"age": 8
157+
}};
158+
assert_eq!(actual, expected);
159+
Ok(())
160+
}
161+
162+
#[test]
163+
fn test_refined_deserialize_json_ok_string() -> anyhow::Result<()> {
164+
let json = json!("hello").to_string();
165+
let non_empty_string: Refined<NonEmptyStringRule> = serde_json::from_str(&json)?;
166+
167+
let actual = non_empty_string.into_value();
168+
let expected = "hello";
169+
assert_eq!(actual, expected);
170+
Ok(())
171+
}
172+
173+
#[test]
174+
fn test_refined_deserialize_json_ok_struct() -> anyhow::Result<()> {
175+
type NonEmptyString = Refined<NonEmptyStringRule>;
176+
#[derive(Debug, Eq, PartialEq, Deserialize)]
177+
struct Human {
178+
name: NonEmptyString,
179+
age: u8,
180+
}
181+
let json = json! {{
182+
"name": "john",
183+
"age": 8
184+
}}
185+
.to_string();
186+
187+
let actual = serde_json::from_str::<Human>(&json)?;
188+
189+
let expected = Human {
190+
name: NonEmptyString::new("john".to_string())?,
191+
age: 8,
192+
};
193+
assert_eq!(actual, expected);
194+
Ok(())
195+
}
196+
197+
#[test]
198+
fn test_refined_deserialize_json_err() -> anyhow::Result<()> {
199+
let json = json!("").to_string();
200+
let result = serde_json::from_str::<Refined<NonEmptyStringRule>>(&json);
201+
assert!(result.is_err());
202+
Ok(())
203+
}
96204
}

src/result.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ use std::fmt::{Debug, Display, Formatter};
22

33
/// A type indicating a failure to convert to `Refined`
44
#[derive(Debug)]
5-
pub struct Error<T> {
5+
pub struct Error<T: Sized> {
66
message: String,
77
target: T,
88
}
99

10+
impl<T> std::error::Error for Error<T> where T: Debug {}
11+
1012
impl<T> Error<T> {
1113
pub fn new(message: impl Into<String>, target: T) -> Self {
1214
Self {

src/rule/composer/and.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::marker::PhantomData;
1313
/// let actual = NonEmptyAlphabetString::validate("Hello".to_string());
1414
/// assert!(actual.is_ok_and(|n| n.as_str() == "Hello"));
1515
/// ```
16+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1617
pub struct And<RULE1, RULE2> {
1718
_rule1: PhantomData<RULE1>,
1819
_rule2: PhantomData<RULE2>,

src/rule/composer/not.rs

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use std::marker::PhantomData;
1313
/// assert!(NonEmptyString::validate("non empty".to_string()).is_ok());
1414
/// assert!(NonEmptyString::validate("".to_string()).is_err());
1515
/// ```
16+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1617
pub struct Not<RULE> {
1718
_rule: PhantomData<RULE>,
1819
}

src/rule/composer/or.rs

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::marker::PhantomData;
1414
/// assert!(EmptyOrAlphabetString::validate("alphabet".to_string()).is_ok());
1515
/// assert!(EmptyOrAlphabetString::validate("1".to_string()).is_err());
1616
/// ```
17+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
1718
pub struct Or<RULE1, RULE2> {
1819
_rule1: PhantomData<RULE1>,
1920
_rule2: PhantomData<RULE2>,

src/rule/empty.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::marker::PhantomData;
1616
/// assert!(Empty::<u8>::validate(0).is_ok());
1717
/// assert!(Empty::<u8>::validate(1).is_err());
1818
/// ```
19-
#[derive(Default)]
19+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
2020
pub struct Empty<T> {
2121
_phantom_data: PhantomData<T>,
2222
}

0 commit comments

Comments
 (0)