cipherstash_config/table/
path.rs1use crate::errors::ConfigError;
2use serde::{
3 de::Deserialize,
4 ser::{Serialize, Serializer},
5};
6
7#[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 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 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 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}