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, }, /// An authority reference. `kind: None` accepts any authority kind; /// `Some(kind)` constrains to that kind. 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. Returns `None` for an unknown /// discriminant or an inconsistent combination — a scalar type carrying any /// binding, a `term` without a vocabulary (or with an authority kind), or an /// `authority` carrying a vocabulary. pub fn from_parts( data_type: &str, vocabulary_id: Option, authority_kind: Option, ) -> Option { let scalar = vocabulary_id.is_none() && authority_kind.is_none(); match data_type { "text" if scalar => Some(FieldType::Text), "localized_text" if scalar => Some(FieldType::LocalizedText), "integer" if scalar => Some(FieldType::Integer), "date" if scalar => Some(FieldType::Date), "boolean" if scalar => Some(FieldType::Boolean), "term" => match vocabulary_id { Some(vocabulary_id) if authority_kind.is_none() => { Some(FieldType::Term { vocabulary_id }) } _ => None, }, "authority" if vocabulary_id.is_none() => 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); } #[test] fn stray_binding_on_scalar_type_is_rejected() { let v = VocabularyId::new(); assert_eq!(FieldType::from_parts("text", Some(v), None), None); assert_eq!( FieldType::from_parts("boolean", None, Some(AuthorityKind::Person)), None ); } #[test] fn term_or_authority_with_wrong_binding_is_rejected() { let v = VocabularyId::new(); assert_eq!( FieldType::from_parts("term", Some(v), Some(AuthorityKind::Person)), None ); assert_eq!(FieldType::from_parts("authority", Some(v), None), None); } }