p2panda_rs/schema/
field_types.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3use std::fmt::Display;
4use std::str::FromStr;
5
6use once_cell::sync::Lazy;
7use regex::Regex;
8
9use crate::operation::OperationValue;
10use crate::schema::error::FieldTypeError;
11use crate::schema::SchemaId;
12
13/// Valid field types for publishing an application schema.
14///
15/// Implements conversion to `OperationValue`:
16///
17/// ```
18/// # use p2panda_rs::operation::{OperationFields, OperationValue};
19/// # use p2panda_rs::schema::FieldType;
20/// let mut field_definition = OperationFields::new();
21/// field_definition.insert("name", "document_title".into());
22/// field_definition.insert("type", FieldType::String.into());
23/// ```
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub enum FieldType {
26    /// Defines a boolean field.
27    Boolean,
28
29    /// Defines a bytes field.
30    Bytes,
31
32    /// Defines an integer number field.
33    Integer,
34
35    /// Defines a floating point number field.
36    Float,
37
38    /// Defines a text string field.
39    String,
40
41    /// Defines a [`Relation`][`crate::operation::Relation`] field that references the given
42    /// schema.
43    Relation(SchemaId),
44
45    /// Defines a [`RelationList`][`crate::operation::RelationList`] field that references the
46    /// given schema.
47    RelationList(SchemaId),
48
49    /// Defines a [`PinnedRelation`][`crate::operation::PinnedRelation`] field that references the
50    /// given schema.
51    PinnedRelation(SchemaId),
52
53    /// Defines a [`PinnedRelationList`][`crate::operation::PinnedRelationList`] field that
54    /// references the given schema.
55    PinnedRelationList(SchemaId),
56}
57
58/// Returns string representation of this field type.
59impl Display for FieldType {
60    // Note: This automatically implements the `to_string` function as well.
61    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62        let field_type_str = match self {
63            FieldType::Boolean => "bool".to_string(),
64            FieldType::Bytes => "bytes".to_string(),
65            FieldType::Integer => "int".to_string(),
66            FieldType::Float => "float".to_string(),
67            FieldType::String => "str".to_string(),
68            FieldType::Relation(schema_id) => format!("relation({})", schema_id),
69            FieldType::RelationList(schema_id) => {
70                format!("relation_list({})", schema_id)
71            }
72            FieldType::PinnedRelation(schema_id) => {
73                format!("pinned_relation({})", schema_id)
74            }
75            FieldType::PinnedRelationList(schema_id) => {
76                format!("pinned_relation_list({})", schema_id)
77            }
78        };
79
80        write!(f, "{}", field_type_str)
81    }
82}
83
84impl From<FieldType> for OperationValue {
85    fn from(field_type: FieldType) -> OperationValue {
86        OperationValue::String(field_type.to_string())
87    }
88}
89
90impl FromStr for FieldType {
91    type Err = FieldTypeError;
92
93    fn from_str(s: &str) -> Result<Self, Self::Err> {
94        // Match non-parametric field types on their plain text name
95        let text_match = match s {
96            "bool" => Ok(FieldType::Boolean),
97            "int" => Ok(FieldType::Integer),
98            "float" => Ok(FieldType::Float),
99            "str" => Ok(FieldType::String),
100            "bytes" => Ok(FieldType::Bytes),
101            _ => Err(FieldTypeError::InvalidFieldType(s.into())),
102        };
103
104        if text_match.is_ok() {
105            return text_match;
106        }
107
108        // Matches a field type name, followed by an optional group in parentheses that contains
109        // the referenced schema for relation field types
110        static RELATION_REGEX: Lazy<Regex> = Lazy::new(|| {
111            // Unwrap as we checked the regular expression for correctness
112            Regex::new(r"(\w+)(\((.+)\))?").unwrap()
113        });
114
115        // @TODO: This might panic if input is invalid?
116        let groups = RELATION_REGEX.captures(s).unwrap();
117        let relation_type = groups.get(1).map(|group_match| group_match.as_str());
118        let schema_id = groups.get(3).map(|group_match| group_match.as_str());
119
120        match (relation_type, schema_id) {
121            (Some("relation"), Some(schema_id)) => {
122                Ok(FieldType::Relation(SchemaId::from_str(schema_id)?))
123            }
124            (Some("relation_list"), Some(schema_id)) => {
125                Ok(FieldType::RelationList(SchemaId::from_str(schema_id)?))
126            }
127            (Some("pinned_relation"), Some(schema_id)) => {
128                Ok(FieldType::PinnedRelation(SchemaId::from_str(schema_id)?))
129            }
130            (Some("pinned_relation_list"), Some(schema_id)) => Ok(FieldType::PinnedRelationList(
131                SchemaId::from_str(schema_id)?,
132            )),
133            _ => Err(FieldTypeError::InvalidFieldType(s.into())),
134        }
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use crate::schema::SchemaId;
141
142    use super::FieldType;
143
144    #[test]
145    fn to_string() {
146        assert_eq!(FieldType::Boolean.to_string(), "bool");
147        assert_eq!(FieldType::Integer.to_string(), "int");
148        assert_eq!(FieldType::Float.to_string(), "float");
149        assert_eq!(FieldType::String.to_string(), "str");
150        assert_eq!(FieldType::Bytes.to_string(), "bytes");
151        assert_eq!(
152            FieldType::Relation(SchemaId::SchemaFieldDefinition(1)).to_string(),
153            "relation(schema_field_definition_v1)"
154        );
155        assert_eq!(
156            FieldType::RelationList(SchemaId::SchemaFieldDefinition(1)).to_string(),
157            "relation_list(schema_field_definition_v1)"
158        );
159        assert_eq!(
160            FieldType::PinnedRelation(SchemaId::SchemaFieldDefinition(1)).to_string(),
161            "pinned_relation(schema_field_definition_v1)"
162        );
163        assert_eq!(
164            FieldType::PinnedRelationList(SchemaId::SchemaFieldDefinition(1)).to_string(),
165            "pinned_relation_list(schema_field_definition_v1)"
166        );
167    }
168
169    #[test]
170    fn from_str() {
171        assert_eq!(FieldType::Boolean, "bool".parse().unwrap());
172        assert_eq!(FieldType::Integer, "int".parse().unwrap());
173        assert_eq!(FieldType::Float, "float".parse().unwrap());
174        assert_eq!(FieldType::String, "str".parse().unwrap());
175        assert_eq!(FieldType::Bytes, "bytes".parse().unwrap());
176        assert_eq!(
177            FieldType::Relation(SchemaId::SchemaFieldDefinition(1)),
178            "relation(schema_field_definition_v1)".parse().unwrap()
179        );
180        assert_eq!(
181            FieldType::RelationList(SchemaId::SchemaFieldDefinition(1)),
182            "relation_list(schema_field_definition_v1)".parse().unwrap()
183        );
184        assert_eq!(
185            FieldType::PinnedRelation(SchemaId::SchemaFieldDefinition(1)),
186            "pinned_relation(schema_field_definition_v1)"
187                .parse()
188                .unwrap()
189        );
190        assert_eq!(
191            FieldType::PinnedRelationList(SchemaId::SchemaFieldDefinition(1)),
192            "pinned_relation_list(schema_field_definition_v1)"
193                .parse()
194                .unwrap()
195        );
196
197        let invalid = "relation(no_no_no)".parse::<FieldType>();
198        assert_eq!(
199            invalid.unwrap_err().to_string(),
200            "encountered invalid hash while parsing application schema id: invalid hex encoding \
201            in hash string"
202        );
203    }
204
205    #[test]
206    fn invalid_type_string() {
207        assert!("poopy".parse::<FieldType>().is_err());
208    }
209}