From 6e27288f43579b02bdd4c89002e95d7d48deb2f8 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 10:15:07 +0200 Subject: [PATCH] fix(domain): make FieldType::from_parts a strict inverse; reject stray bindings --- crates/domain/src/field_definition.rs | 57 +++++++++++++++++++++------ 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/crates/domain/src/field_definition.rs b/crates/domain/src/field_definition.rs index c834b21..45d83ef 100644 --- a/crates/domain/src/field_definition.rs +++ b/crates/domain/src/field_definition.rs @@ -10,8 +10,14 @@ pub enum FieldType { Integer, Date, Boolean, - Term { vocabulary_id: VocabularyId }, - Authority { kind: Option }, + Term { + vocabulary_id: VocabularyId, + }, + /// An authority reference. `kind: None` accepts any authority kind; + /// `Some(kind)` constrains to that kind. + Authority { + kind: Option, + }, } impl FieldType { @@ -37,21 +43,30 @@ impl FieldType { } } - /// Reconstruct from the stored columns. `None` for an unknown or inconsistent combo - /// (e.g. `term` without a vocabulary). + /// 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" => 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 { + "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, @@ -117,4 +132,24 @@ mod tests { 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); + } }