use crate::{AuthorityKind, FieldDefinitionId, LocalizedLabel, VocabularyId}; /// The type of a flexible field, carrying its binding where applicable. /// /// Type-driven: a `Term` always names its vocabulary; a non-term never carries one. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FieldType { Text, LocalizedText, Integer, Date, Boolean, Term { vocabulary_id: VocabularyId }, Authority { kind: Option }, } impl FieldType { /// The stored discriminant string. pub fn kind_str(&self) -> &'static str { match self { FieldType::Text => "text", FieldType::LocalizedText => "localized_text", FieldType::Integer => "integer", FieldType::Date => "date", FieldType::Boolean => "boolean", FieldType::Term { .. } => "term", FieldType::Authority { .. } => "authority", } } /// Decompose into the three stored columns: `(data_type, vocabulary_id, authority_kind)`. pub fn to_parts(&self) -> (&'static str, Option, Option) { match self { FieldType::Term { vocabulary_id } => ("term", Some(*vocabulary_id), None), FieldType::Authority { kind } => ("authority", None, *kind), other => (other.kind_str(), None, None), } } /// Reconstruct from the stored columns. `None` for an unknown or inconsistent combo /// (e.g. `term` without a vocabulary). pub fn from_parts( data_type: &str, vocabulary_id: Option, authority_kind: Option, ) -> Option { match data_type { "text" => Some(FieldType::Text), "localized_text" => Some(FieldType::LocalizedText), "integer" => Some(FieldType::Integer), "date" => Some(FieldType::Date), "boolean" => Some(FieldType::Boolean), "term" => vocabulary_id.map(|vocabulary_id| FieldType::Term { vocabulary_id }), "authority" => Some(FieldType::Authority { kind: authority_kind, }), _ => None, } } } /// A registered flexible field, with its multilingual display labels. #[derive(Debug, Clone, PartialEq, Eq)] pub struct FieldDefinition { pub id: FieldDefinitionId, pub key: String, pub field_type: FieldType, pub required: bool, pub group_key: Option, pub labels: Vec, } /// A field definition to be created. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NewFieldDefinition { pub key: String, pub field_type: FieldType, pub required: bool, pub group_key: Option, pub labels: Vec, } #[cfg(test)] mod tests { use super::*; #[test] fn field_type_round_trips_through_parts() { let v = VocabularyId::new(); let cases = [ FieldType::Text, FieldType::LocalizedText, FieldType::Integer, FieldType::Date, FieldType::Boolean, FieldType::Term { vocabulary_id: v }, FieldType::Authority { kind: Some(AuthorityKind::Person), }, FieldType::Authority { kind: None }, ]; for ft in cases { let (data_type, vocabulary_id, authority_kind) = ft.to_parts(); assert_eq!( FieldType::from_parts(data_type, vocabulary_id, authority_kind), Some(ft) ); } } #[test] fn term_without_vocabulary_is_invalid() { assert_eq!(FieldType::from_parts("term", None, None), None); } #[test] fn unknown_type_is_none() { assert_eq!(FieldType::from_parts("blob", None, None), None); } }