feat(domain): add field definition types (FieldType, FieldDefinition)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:11:28 +02:00
parent da2db11a30
commit 2242ff5ef1
3 changed files with 127 additions and 1 deletions
+120
View File
@@ -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<AuthorityKind> },
}
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<VocabularyId>, Option<AuthorityKind>) {
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<VocabularyId>,
authority_kind: Option<AuthorityKind>,
) -> Option<Self> {
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<String>,
pub labels: Vec<LocalizedLabel>,
}
/// 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<String>,
pub labels: Vec<LocalizedLabel>,
}
#[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);
}
}