atuin_common/string/
non_blank.rs1use std::borrow::Cow;
4
5use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
6use serde::{Deserialize, Deserializer, Serialize};
7
8#[derive(
11 Clone,
12 PartialEq,
13 Eq,
14 Hash,
15 PartialOrd,
16 Ord,
17 Serialize,
18 derive_more::AsRef,
19 derive_more::Debug,
20 derive_more::Deref,
21 derive_more::Display,
22)]
23#[as_ref(forward)]
24#[debug("{_0:?}")]
25#[deref(forward)]
26#[display("{_0}")]
27pub struct NonBlank<T = String>(T);
28
29pub type NonBlankString = NonBlank<String>;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
33#[error("string is blank")]
34pub struct Blank;
35
36impl<T: AsRef<str>> NonBlank<T> {
37 pub fn new(inner: T) -> Result<Self, Blank> {
39 if inner.as_ref().trim().is_empty() {
40 Err(Blank)
41 } else {
42 Ok(Self(inner))
43 }
44 }
45
46 pub fn as_str(&self) -> &str {
48 self.0.as_ref()
49 }
50
51 pub fn into_inner(self) -> T {
52 self.0
53 }
54}
55
56impl<'de, T: AsRef<str> + Deserialize<'de>> Deserialize<'de> for NonBlank<T> {
57 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
58 Self::new(T::deserialize(deserializer)?).map_err(serde::de::Error::custom)
59 }
60}
61
62impl<T> JsonSchema for NonBlank<T> {
63 fn schema_name() -> Cow<'static, str> {
64 "NonBlank".into()
65 }
66
67 fn inline_schema() -> bool {
68 true
69 }
70
71 fn json_schema(_: &mut SchemaGenerator) -> Schema {
72 json_schema!({ "type": "string", "minLength": 1 })
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use rstest::rstest;
79 use schemars::JsonSchema;
80 use serde::Deserialize;
81 use serde_json::json;
82
83 use super::*;
84
85 #[derive(Deserialize, JsonSchema)]
86 struct Params {
87 query: NonBlankString,
88 }
89
90 #[rstest]
91 #[case::plain("disk")]
92 #[case::padded(" disk full \n")]
93 fn holds_the_text_verbatim(#[case] input: &str) {
94 let borrowed: NonBlank<&str> = NonBlank::new(input).unwrap();
95 assert_eq!(borrowed.as_str(), input);
96 assert_eq!(&*borrowed, input);
97 assert_eq!(borrowed.to_string(), input);
98 assert_eq!(NonBlankString::new(input.to_owned()).unwrap().as_str(), input);
99 let params: Params = serde_json::from_value(json!({"query": input})).unwrap();
100 assert_eq!(params.query.as_str(), input);
101 assert_eq!(serde_json::to_value(¶ms.query).unwrap(), json!(input));
102 assert_eq!(params.query.into_inner(), input);
103 }
104
105 #[rstest]
106 #[case::empty("")]
107 #[case::whitespace(" \t\n")]
108 fn rejects_blank_text(#[case] input: &str) {
109 assert_eq!(NonBlank::new(input), Err(Blank));
110 assert!(serde_json::from_value::<Params>(json!({"query": input})).is_err());
111 }
112
113 #[rstest]
114 fn json_schema_requires_a_character() {
115 let schema = schemars::schema_for!(Params);
116 let query = &schema.as_value()["properties"]["query"];
117 assert_eq!(query["type"], "string");
118 assert_eq!(query["minLength"], 1);
119 assert_eq!(schema.as_value()["required"], json!(["query"]));
120 }
121}