Skip to main content

parse_rust_storage/
schema.rs

1//! Field types, and the per-class shape a transform needs.
2//!
3//! This lives in `parse-rust-storage` rather than `parse-rust-schema` because both adapters need it to
4//! lower a value, and the transform cannot be written without knowing which fields are Pointers.
5//! `parse-rust-schema` will own inference, validation and CLP on top of these types.
6
7use indexmap::IndexMap;
8
9/// A Parse field type.
10///
11/// `Pointer` and `Relation` carry their target class because the wire form does
12/// (`Pointer<_User>`), and because the Mongo transform needs the target to build the
13/// `"<Class>$<id>"` storage form.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum FieldType {
16    String,
17    Number,
18    Boolean,
19    Date,
20    Object,
21    Array,
22    GeoPoint,
23    File,
24    Bytes,
25    Polygon,
26    Pointer {
27        target_class: String,
28    },
29    Relation {
30        target_class: String,
31    },
32    /// Not a user-settable field type. `ACL` is a real column in the Parse schema, and
33    /// `mongoSchemaFieldsToParseSchemaFields` injects it unconditionally.
34    Acl,
35}
36
37impl FieldType {
38    /// The wire rendering, which is part of the error-message contract.
39    ///
40    /// `typeToString` produces `Pointer<_User>` for parametric types
41    /// (`SchemaController.js:697-705`), and that exact string appears inside
42    /// `schema mismatch for <Class>.<field>; expected <expected> but got <got>`. Getting the
43    /// rendering wrong changes an error a client may match on.
44    pub fn to_wire_string(&self) -> String {
45        match self {
46            FieldType::String => "String".into(),
47            FieldType::Number => "Number".into(),
48            FieldType::Boolean => "Boolean".into(),
49            FieldType::Date => "Date".into(),
50            FieldType::Object => "Object".into(),
51            FieldType::Array => "Array".into(),
52            FieldType::GeoPoint => "GeoPoint".into(),
53            FieldType::File => "File".into(),
54            FieldType::Bytes => "Bytes".into(),
55            FieldType::Polygon => "Polygon".into(),
56            FieldType::Acl => "ACL".into(),
57            FieldType::Pointer { target_class } => format!("Pointer<{target_class}>"),
58            FieldType::Relation { target_class } => format!("Relation<{target_class}>"),
59        }
60    }
61
62    pub fn is_pointer(&self) -> bool {
63        matches!(self, FieldType::Pointer { .. })
64    }
65}
66
67/// The shape of one class: what a transform needs to lower or raise a document.
68///
69/// Order-preserving, because `_SCHEMA` documents are compared in golden files and because the
70/// order fields were added is the order upstream writes them.
71#[derive(Debug, Clone, Default)]
72pub struct ClassSchema {
73    pub class_name: String,
74    pub fields: IndexMap<String, FieldType>,
75}
76
77impl ClassSchema {
78    pub fn new(class_name: impl Into<String>) -> Self {
79        Self {
80            class_name: class_name.into(),
81            fields: IndexMap::new(),
82        }
83    }
84
85    pub fn with_field(mut self, name: impl Into<String>, ty: FieldType) -> Self {
86        self.fields.insert(name.into(), ty);
87        self
88    }
89
90    pub fn field(&self, name: &str) -> Option<&FieldType> {
91        self.fields.get(name)
92    }
93
94    /// Is this field stored under a `_p_` prefix?
95    ///
96    /// Note the deliberate narrowness: only a *declared* Pointer field is prefixed. A pointer
97    /// value written to a field the schema does not know about is not prefixed by
98    /// `transformKey`, because that function consults the schema and nothing else.
99    pub fn is_pointer_field(&self, name: &str) -> bool {
100        self.field(name).is_some_and(FieldType::is_pointer)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn parametric_types_render_with_angle_brackets() {
110        assert_eq!(
111            FieldType::Pointer {
112                target_class: "_User".into()
113            }
114            .to_wire_string(),
115            "Pointer<_User>"
116        );
117        assert_eq!(
118            FieldType::Relation {
119                target_class: "Post".into()
120            }
121            .to_wire_string(),
122            "Relation<Post>"
123        );
124        assert_eq!(FieldType::String.to_wire_string(), "String");
125    }
126
127    #[test]
128    fn only_declared_pointer_fields_are_prefixed() {
129        let s = ClassSchema::new("Post").with_field(
130            "author",
131            FieldType::Pointer {
132                target_class: "_User".into(),
133            },
134        );
135        assert!(s.is_pointer_field("author"));
136        assert!(!s.is_pointer_field("undeclared"));
137    }
138}