cipherstash_config/table/
path.rs

1use crate::errors::ConfigError;
2use serde::{
3    de::Deserialize,
4    ser::{Serialize, Serializer},
5};
6
7/// Represents a (possibly fully qualified) table.
8/// It is specified by an optional schema and a table name.
9#[derive(Debug, Clone, Eq)]
10pub struct TablePath(pub Option<String>, pub String);
11
12impl TablePath {
13    pub fn unqualified(relname: impl Into<String>) -> Self {
14        Self(None, relname.into())
15    }
16
17    /// Qualify the path with the given schema
18    /// Note that if `schemaname` is an empty string, it will be set to `None`.
19    /// This is handy as Query ASTs often represent null schema as an empty string.
20    pub fn qualified(schemaname: impl Into<String>, relname: impl Into<String>) -> Self {
21        let schemaname: String = schemaname.into();
22        if schemaname.is_empty() {
23            Self(None, relname.into())
24        } else {
25            Self(Some(schemaname), relname.into())
26        }
27    }
28
29    pub fn as_string(&self) -> String {
30        match self {
31            Self(None, field) => field.to_string(),
32            Self(Some(relation), field) => format!("{}.{}", relation, field),
33        }
34    }
35}
36
37impl TryFrom<&str> for TablePath {
38    type Error = ConfigError;
39
40    fn try_from(path: &str) -> Result<Self, Self::Error> {
41        let tokens: Vec<&str> = path.split('.').collect();
42
43        match tokens.len() {
44            0 => Err(ConfigError::InvalidPath(path.to_string())),
45            1 => Ok(TablePath::unqualified(tokens[0])),
46            2 => Ok(TablePath::qualified(tokens[0], tokens[1])),
47            _ => Err(ConfigError::UnexpectedQualifier(
48                tokens[0].to_string(),
49                path.to_string(),
50            )),
51        }
52    }
53}
54
55impl PartialEq for TablePath {
56    fn eq(&self, other: &Self) -> bool {
57        if self.1 != other.1 {
58            return false;
59        };
60        let schema = self.0.as_ref().zip(other.0.as_ref());
61
62        match schema {
63            None => true,
64            Some((a, b)) if a == b => true,
65            _ => false,
66        }
67    }
68}
69
70impl PartialEq<&[String]> for TablePath {
71    fn eq(&self, other: &&[String]) -> bool {
72        match other.len() {
73            0 => false,
74            1 => self.1 == *other[0],
75            2 => {
76                self.0
77                    .as_ref()
78                    .map(|schema| *schema == other[0])
79                    .unwrap_or(false)
80                    && self.1 == *other[1]
81            }
82            _ => false,
83        }
84    }
85}
86
87impl Serialize for TablePath {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        serializer.serialize_str(&self.as_string())
93    }
94}
95
96impl<'de> Deserialize<'de> for TablePath {
97    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98    where
99        D: serde::Deserializer<'de>,
100    {
101        let s: String = Deserialize::deserialize(deserializer)?;
102        TablePath::try_from(s.as_str()).map_err(serde::de::Error::custom)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn rel_from(path_str: &str) -> Result<TablePath, ConfigError> {
111        TablePath::try_from(path_str)
112    }
113
114    fn check_equal(subject: &str, target: &str) -> Result<(), Box<dyn std::error::Error>> {
115        let subject: TablePath = subject.try_into()?;
116        let target: TablePath = target.try_into()?;
117        assert_eq!(subject, target);
118
119        Ok(())
120    }
121
122    #[test]
123    fn unqualified() {
124        assert!(matches!(
125            TablePath::unqualified("users"),
126            TablePath(None, str) if str == *"users"
127        ))
128    }
129
130    #[test]
131    fn qualified() {
132        assert!(matches!(
133            TablePath::qualified("public", "users"),
134            TablePath(Some(schema), table) if table == *"users" && schema == *"public"
135        ))
136    }
137
138    #[test]
139    fn qualified_by_empty_schema() {
140        // Actually unqualified 😎
141        assert!(matches!(
142            TablePath::qualified("", "users"),
143            TablePath(None, table) if table == *"users"
144        ))
145    }
146
147    #[test]
148    fn test_from_conversion() -> Result<(), Box<dyn std::error::Error>> {
149        assert_eq!(rel_from("users")?, TablePath::unqualified("users"));
150        assert_eq!(
151            rel_from("public.users")?,
152            TablePath::qualified("public", "users")
153        );
154        assert!(rel_from("foo.bar.wee").is_err());
155        // TODO
156        //assert!(matches!(rel_from("foo.f/d"), Err(_)));
157
158        Ok(())
159    }
160
161    #[test]
162    fn equivalence_exact() -> Result<(), Box<dyn std::error::Error>> {
163        check_equal("users", "users")?;
164        check_equal("public.users", "public.users")?;
165
166        Ok(())
167    }
168
169    #[test]
170    fn equivalence_partial_target() -> Result<(), Box<dyn std::error::Error>> {
171        check_equal("public.users", "users")?;
172
173        Ok(())
174    }
175
176    #[test]
177    fn equivalence_partial_subject() -> Result<(), Box<dyn std::error::Error>> {
178        check_equal("users", "public.users")?;
179
180        Ok(())
181    }
182
183    #[test]
184    fn equalivalence_slice() -> Result<(), Box<dyn std::error::Error>> {
185        assert_eq!(TablePath::unqualified("users"), &["users".to_string()][..]);
186        assert_eq!(
187            TablePath::qualified("public", "users"),
188            &["users".to_string()][..]
189        );
190        assert_eq!(
191            TablePath::qualified("public", "users"),
192            &["public".to_string(), "users".to_string()][..]
193        );
194        assert_ne!(TablePath::unqualified("users"), &[][..]);
195        assert_ne!(TablePath::unqualified("users"), &["foo".to_string()][..]);
196        assert_ne!(
197            TablePath::unqualified("users"),
198            &["foo".to_string(), "users".to_string()][..]
199        );
200        assert_ne!(
201            TablePath::qualified("public", "users"),
202            &["foo".to_string(), "users".to_string()][..]
203        );
204        assert_ne!(
205            TablePath::qualified("public", "users"),
206            &["foo".to_string(), "public".to_string(), "users".to_string()][..]
207        );
208
209        Ok(())
210    }
211}