From 2242ff5ef1815ee1bc7d0d199b45fc001d721348 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 10:11:28 +0200 Subject: [PATCH] feat(domain): add field definition types (FieldType, FieldDefinition) Co-Authored-By: Claude Sonnet 4.6 --- crates/domain/src/field_definition.rs | 120 ++++++++++++++++++++++++++ crates/domain/src/id.rs | 4 + crates/domain/src/lib.rs | 4 +- 3 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 crates/domain/src/field_definition.rs diff --git a/crates/domain/src/field_definition.rs b/crates/domain/src/field_definition.rs new file mode 100644 index 0000000..c834b21 --- /dev/null +++ b/crates/domain/src/field_definition.rs @@ -0,0 +1,120 @@ +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); + } +} diff --git a/crates/domain/src/id.rs b/crates/domain/src/id.rs index f7cc745..6f7be8d 100644 --- a/crates/domain/src/id.rs +++ b/crates/domain/src/id.rs @@ -68,6 +68,10 @@ id_newtype!( /// Identifier for a catalogue object (or group of objects). ObjectId ); +id_newtype!( + /// Identifier for a flexible-field definition. + FieldDefinitionId +); #[cfg(test)] mod tests { diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 8c46507..509f870 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -2,6 +2,7 @@ mod audit; mod authority; +mod field_definition; mod id; mod label; mod object; @@ -9,7 +10,8 @@ mod vocabulary; pub use audit::{AuditAction, AuditActor, AuditEntry, FieldChange, NewAuditEvent}; pub use authority::{Authority, AuthorityKind, AuthorityRef, NewAuthority}; -pub use id::{AuthorityId, ObjectId, OrgId, TermId, VocabularyId}; +pub use field_definition::{FieldDefinition, FieldType, NewFieldDefinition}; +pub use id::{AuthorityId, FieldDefinitionId, ObjectId, OrgId, TermId, VocabularyId}; pub use label::{LocalizedLabel, pick_label}; pub use object::{CatalogueObject, ObjectInput, Visibility}; pub use vocabulary::{NewTerm, Term, TermRef, Vocabulary};