degen_sql/
tiny_safe_string.rs

1use serde::{Deserialize, Serialize, Serializer, Deserializer};
2use std::fmt;
3use std::str::FromStr;
4use serde::de::{self, Visitor};
5use std::ops::Deref;
6
7#[cfg_attr(feature = "utoipa-schema", derive(utoipa::ToSchema))]
8#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
9pub struct TinySafeString(String);
10
11impl TinySafeString {
12    /// Creates a new TinySafeString if the input string only contains alphanumeric characters
13    pub fn new(s: &str) -> Result<Self, String> {
14        if s.chars().all(|c| c.is_alphanumeric() || c == '_') {
15            Ok(TinySafeString(s.to_string()))
16        } else {
17            Err(format!("Invalid string: '{}'. Only alphanumeric characters and underscores are allowed.", s))
18        }
19    }
20
21    /// Get the inner string value
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25
26    /// Validates if a string meets the safety requirements
27    pub fn is_valid(s: &str) -> bool {
28        s.chars().all(|c| c.is_alphanumeric() || c == '_')
29    }
30
31    /// Convert to SQL-safe column name
32    pub fn to_sql_string(&self) -> &str {
33        &self.0
34    }
35}
36
37impl Deref for TinySafeString {
38    type Target = str;
39
40    fn deref(&self) -> &Self::Target {
41        &self.0
42    }
43}
44
45impl AsRef<str> for TinySafeString {
46    fn as_ref(&self) -> &str {
47        &self.0
48    }
49}
50
51impl FromStr for TinySafeString {
52    type Err = String;
53
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        TinySafeString::new(s)
56    }
57}
58
59/*
60impl<'a> TryFrom<&'a str> for TinySafeString {
61    type Error = String;
62
63    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
64        TinySafeString::new(s)
65    }
66}*/
67
68impl TryFrom<String> for TinySafeString {
69    type Error = String;
70
71    fn try_from(s: String) -> Result<Self, Self::Error> {
72        TinySafeString::new(&s)
73    }
74}
75
76impl From<TinySafeString> for String {
77    fn from(value: TinySafeString) -> Self {
78        value.0
79    }
80}
81
82// Custom serialization for TinySafeString
83impl Serialize for TinySafeString {
84    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85    where
86        S: Serializer,
87    {
88        serializer.serialize_str(&self.0)
89    }
90}
91
92// Custom deserialization for TinySafeString
93impl<'de> Deserialize<'de> for TinySafeString {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: Deserializer<'de>,
97    {
98        struct TinySafeStringVisitor;
99        
100        impl<'de> Visitor<'de> for TinySafeStringVisitor {
101            type Value = TinySafeString;
102            
103            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
104                formatter.write_str("a string containing only alphanumeric characters and underscores")
105            }
106            
107            fn visit_str<E>(self, value: &str) -> Result<TinySafeString, E>
108            where
109                E: de::Error,
110            {
111                TinySafeString::new(value).map_err(E::custom)
112            }
113        }
114        
115        deserializer.deserialize_str(TinySafeStringVisitor)
116    }
117}
118
119// Implement Display for TinySafeString
120impl fmt::Display for TinySafeString {
121    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122        write!(f, "{}", self.0)
123    }
124}
125
126// Implement From<&str> for easy conversion in code, but with a panic for invalid strings
127// Use TryFrom for safe conversion instead
128impl From<&str> for TinySafeString {
129    fn from(s: &str) -> Self {
130        Self::new(s).unwrap_or_else(|e| panic!("{}", e))
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use crate::pagination::PaginationData;
137use crate::pagination::ColumnSortDir;
138use super::*;
139    use serde_json::json;
140
141    #[test]
142    fn test_valid_strings() {
143        assert!(TryInto::<TinySafeString>::try_into("hello").is_ok() );
144        assert!(TinySafeString::new("hello123").is_ok());
145        assert!(TinySafeString::new("HELLO").is_ok());
146        assert!(TinySafeString::new("hello_world").is_ok());
147        assert!(TinySafeString::new("_underscore").is_ok());
148    }
149
150    #[test]
151    fn test_invalid_strings() {
152        assert!(TinySafeString::new("hello world").is_err());
153        assert!(TinySafeString::new("hello-world").is_err());
154        assert!(TinySafeString::new("hello;drop table").is_err());
155        assert!(TinySafeString::new("SELECT * FROM").is_err());
156        assert!(TinySafeString::new("hello'world").is_err());
157    }
158
159    #[test]
160    fn test_serialization() {
161        let safe_str = TinySafeString::new("hello123").unwrap();
162        let serialized = serde_json::to_string(&safe_str).unwrap();
163        assert_eq!(serialized, "\"hello123\"");
164    }
165
166    #[test]
167    fn test_deserialization() {
168        let json_str = "\"hello123\"";
169        let safe_str: TinySafeString = serde_json::from_str(json_str).unwrap();
170        assert_eq!(safe_str.as_str(), "hello123");
171    }
172
173    #[test]
174    fn test_invalid_deserialization() {
175        let json_str = "\"hello world\"";
176        let result: Result<TinySafeString, _> = serde_json::from_str(json_str);
177        assert!(result.is_err());
178    }
179
180    #[test]
181    fn test_pagination_data() {
182        let json_data = json!({
183            "page": 2,
184            "page_size": 20,
185            "sort_by": "created_at",
186            "sort_dir": "asc"
187        });
188
189        let pagination_data: PaginationData = serde_json::from_value(json_data).unwrap();
190        assert_eq!(pagination_data.page, Some(2));
191        assert_eq!(pagination_data.page_size, Some(20));
192        assert_eq!(pagination_data.sort_by.as_ref().unwrap().as_str(), "created_at");
193        assert!(matches!(pagination_data.sort_dir.as_ref().unwrap(), ColumnSortDir::Asc));
194    }
195}