156 lines
4.9 KiB
Rust
156 lines
4.9 KiB
Rust
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<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. 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<VocabularyId>,
|
|
authority_kind: Option<AuthorityKind>,
|
|
) -> Option<Self> {
|
|
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<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);
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|