proof_of_sql_parser/
resource_id.rs

1//! This file defines the resource identifier type.
2use crate::{impl_serde_from_str, sql::ResourceIdParser, Identifier, ParseError, ParseResult};
3use alloc::{
4    format,
5    string::{String, ToString},
6    vec::Vec,
7};
8use core::{
9    fmt::{self, Display},
10    str::FromStr,
11};
12use sqlparser::ast::Ident;
13
14/// Unique resource identifier, like `schema.object_name`.
15#[derive(Debug, PartialEq, Eq, Clone, Hash, Copy)]
16pub struct ResourceId {
17    schema: Identifier,
18    object_name: Identifier,
19}
20
21impl ResourceId {
22    /// Constructor for [`ResourceId`]s.
23    #[must_use]
24    pub fn new(schema: Identifier, object_name: Identifier) -> Self {
25        Self {
26            schema,
27            object_name,
28        }
29    }
30
31    /// Constructor for [`ResourceId`]s.
32    ///
33    /// # Errors
34    /// Fails if the provided `schema/object_name` strings aren't valid postgres-style
35    /// identifiers (excluding dollar signs).
36    /// These identifiers are defined here:
37    /// <https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS>.
38    pub fn try_new(schema: &str, object_name: &str) -> ParseResult<Self> {
39        let schema = Identifier::try_new(schema)?;
40        let object_name = Identifier::try_new(object_name)?;
41
42        Ok(ResourceId {
43            schema,
44            object_name,
45        })
46    }
47
48    /// The schema identifier of this [`ResourceId`].
49    #[must_use]
50    pub fn schema(&self) -> Identifier {
51        self.schema
52    }
53
54    /// The `object_name` identifier of this [`ResourceId`].
55    #[must_use]
56    pub fn object_name(&self) -> Identifier {
57        self.object_name
58    }
59
60    /// Conversion to string in the format used in `KeyDB`.
61    ///
62    /// Space and time APIs accept a `.` separator in resource ids.
63    /// However, when a resource id is stored in `KeyDB`, or used as a key, a `:` separator is used.
64    /// This method differs from [`ToString::to_string`] by using the latter format.
65    ///
66    /// Furthermore, while space and time APIs accept lowercase resource identifiers,
67    /// all resource identifiers are stored internally in uppercase.
68    /// This method performs that transformation as well.
69    /// For more information, see
70    /// <https://space-and-time.atlassian.net/wiki/spaces/SE/pages/4947974/Gateway+Storage+Overview#Database-Resources>.
71    #[must_use]
72    pub fn storage_format(&self) -> String {
73        let ResourceId {
74            schema,
75            object_name,
76        } = self;
77
78        let schema = schema.name().to_string().to_uppercase();
79        let object_name = object_name.name().to_string().to_uppercase();
80
81        format!("{schema}:{object_name}")
82    }
83}
84
85impl Display for ResourceId {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        let ResourceId {
88            schema,
89            object_name,
90        } = self;
91
92        formatter.write_fmt(format_args!("{schema}.{object_name}"))
93    }
94}
95
96impl FromStr for ResourceId {
97    type Err = ParseError;
98
99    fn from_str(string: &str) -> ParseResult<Self> {
100        let (schema, object_name) = ResourceIdParser::new().parse(string).map_err(|e| {
101            ParseError::ResourceIdParseError {
102                error: format!("{e:?}"),
103            }
104        })?;
105
106        // use unsafe `Identifier::new` to prevent double parsing the ids
107        Ok(ResourceId {
108            schema: Identifier::new(schema),
109            object_name: Identifier::new(object_name),
110        })
111    }
112}
113impl_serde_from_str!(ResourceId);
114
115impl TryFrom<Vec<Ident>> for ResourceId {
116    type Error = ParseError;
117
118    fn try_from(identifiers: Vec<Ident>) -> ParseResult<Self> {
119        if identifiers.len() != 2 {
120            return Err(ParseError::ResourceIdParseError {
121                error: "Expected exactly two identifiers for ResourceId".to_string(),
122            });
123        }
124
125        let schema = Identifier::try_from(identifiers[0].clone())?;
126        let object_name = Identifier::try_from(identifiers[1].clone())?;
127        Ok(ResourceId::new(schema, object_name))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn try_new_resource_id() {
137        let resource_id =
138            ResourceId::try_new("G00d_identifier", "_can_start_with_underscore").unwrap();
139        assert_eq!(resource_id.schema().name(), "g00d_identifier");
140        assert_eq!(
141            resource_id.object_name().name(),
142            "_can_start_with_underscore"
143        );
144    }
145
146    #[test]
147    fn resource_id_from_str() {
148        let resource_id =
149            ResourceId::from_str("G00d_identifier._can_start_with_underscore").unwrap();
150        assert_eq!(resource_id.schema().name(), "g00d_identifier");
151        assert_eq!(
152            resource_id.object_name().name(),
153            "_can_start_with_underscore"
154        );
155    }
156
157    #[test]
158    fn try_new_resource_id_with_additional_characters_fails() {
159        assert!(ResourceId::try_new("GOOD_IDENTIFIER", "GOOD_IDENTIFIER.").is_err());
160        assert!(ResourceId::try_new("GOOD_IDENTIFIER.", "GOOD_IDENTIFIER").is_err());
161        assert!(ResourceId::try_new("BAD$IDENTIFIER", "GOOD_IDENTIFIER").is_err());
162        assert!(ResourceId::try_new("GOOD_IDENTIFIER", "BAD IDENTIFIER").is_err());
163    }
164
165    #[test]
166    fn we_can_parse_valid_resource_ids_with_white_spaces_at_beginning_or_end() {
167        let resource_id =
168            ResourceId::from_str("      GOOD_IDENTIFIER._can_start_with_underscore   ").unwrap();
169        assert_eq!(resource_id.schema().name(), "good_identifier");
170        assert_eq!(
171            resource_id.object_name().name(),
172            "_can_start_with_underscore"
173        );
174
175        let resource_id = ResourceId::try_new(
176            "      GOOD_IDENTIFIER     ",
177            "      _can_start_with_underscore   ",
178        )
179        .unwrap();
180        assert_eq!(resource_id.schema().name(), "good_identifier");
181        assert_eq!(
182            resource_id.object_name().name(),
183            "_can_start_with_underscore"
184        );
185    }
186
187    #[test]
188    fn display_resource_id() {
189        assert_eq!(
190            ResourceId::try_new("GOOD_IDENTIFIER", "good_identifier")
191                .unwrap()
192                .to_string(),
193            "good_identifier.good_identifier"
194        );
195
196        assert_eq!(
197            ResourceId::try_new("g00d_identifier", "_can_Start_with_underscore")
198                .unwrap()
199                .to_string(),
200            "g00d_identifier._can_start_with_underscore"
201        );
202    }
203
204    #[test]
205    fn resource_id_storage_format() {
206        assert_eq!(
207            ResourceId::try_new("GOOD_IDENTIFIER", "good_identifier")
208                .unwrap()
209                .storage_format(),
210            "GOOD_IDENTIFIER:GOOD_IDENTIFIER"
211        );
212        assert_eq!(
213            ResourceId::try_new("g00d_identifier", "_can_Start_with_underscore")
214                .unwrap()
215                .storage_format(),
216            "G00D_IDENTIFIER:_CAN_START_WITH_UNDERSCORE"
217        );
218    }
219
220    #[test]
221    fn invalid_resource_id_parsing_fails() {
222        assert!(ResourceId::from_str("GOOD_IDENTIFIER").is_err());
223        assert!(ResourceId::from_str("GOOD_IDENTIFIER:GOOD_IDENTIFIER").is_err());
224        assert!(ResourceId::from_str("BAD$IDENTIFIER.GOOD_IDENTIFIER").is_err());
225        assert!(ResourceId::from_str("GOOD_IDENTIFIER.BAD_IDENT!FIER").is_err());
226        assert!(ResourceId::from_str("GOOD_IDENTIFIER.BAD IDENTIFIER").is_err());
227        assert!(ResourceId::from_str("GOOD_IDENTIFIER.13AD_IDENTIFIER").is_err());
228        assert!(ResourceId::from_str("13AD_IDENTIFIER.GOOD_IDENTIFIER").is_err());
229        assert!(ResourceId::from_str("GOOD_IDENTIFIER.").is_err());
230        assert!(ResourceId::from_str(".GOOD_IDENTIFIER").is_err());
231        assert!(ResourceId::from_str(".").is_err());
232        assert!(ResourceId::from_str("GOOD_IDENTIFIER").is_err());
233        assert!(ResourceId::from_str("GOOD_IDENTIFIER.GOOD_IDENTIFIER.GOOD_IDENTIFIER").is_err());
234    }
235
236    #[test]
237    fn resource_id_serializes_to_string() {
238        let resource_id = ResourceId::try_new("GOOD_IDENTIFIER", "good_identifier").unwrap();
239        let serialized = serde_json::to_string(&resource_id).unwrap();
240        assert_eq!(serialized, r#""good_identifier.good_identifier""#);
241    }
242
243    #[test]
244    fn resource_id_deserializes_from_string() {
245        let resource_id = ResourceId::try_new("GOOD_IDENTIFIER", "good_identifier").unwrap();
246        let deserialized: ResourceId =
247            serde_json::from_str(r#""good_identifier.good_identifier""#).unwrap();
248        assert_eq!(resource_id, deserialized);
249    }
250
251    #[test]
252    fn resource_id_fails_to_deserialize_with_invalid_identifier() {
253        let deserialized: Result<ResourceId, _> =
254            serde_json::from_str(r#""good_identifier.bad!identifier"#);
255        assert!(deserialized.is_err());
256    }
257
258    #[test]
259    fn test_try_from_vec_ident() {
260        let identifiers = alloc::vec![Ident::new("schema_name"), Ident::new("object_name")];
261        let resource_id = ResourceId::try_from(identifiers).unwrap();
262        assert_eq!(resource_id.schema().name(), "schema_name");
263        assert_eq!(resource_id.object_name().name(), "object_name");
264
265        let invalid_identifiers = alloc::vec![Ident::new("only_one_ident")];
266        assert!(ResourceId::try_from(invalid_identifiers).is_err());
267    }
268}