use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Cardinality {
OneToOne,
OneToMany,
ManyToOne,
ManyToMany,
}
impl Cardinality {
pub fn default_navigation(&self) -> NavigationHint {
match self {
Cardinality::OneToOne => NavigationHint::Inline,
Cardinality::ManyToOne => NavigationHint::Link,
Cardinality::OneToMany => NavigationHint::Nested,
Cardinality::ManyToMany => NavigationHint::Nested,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum NavigationHint {
Inline,
Link,
Tab,
Nested,
Hidden,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RelationshipDef {
pub name: String,
pub target: String,
pub cardinality: Cardinality,
pub navigation: NavigationHint,
#[serde(skip_serializing_if = "Option::is_none")]
pub foreign_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub inverse: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl RelationshipDef {
pub fn new(
name: impl Into<String>,
target: impl Into<String>,
cardinality: Cardinality,
) -> Self {
Self {
name: name.into(),
target: target.into(),
navigation: cardinality.default_navigation(),
cardinality,
foreign_key: None,
inverse: None,
description: None,
}
}
pub fn foreign_key(mut self, fk: impl Into<String>) -> Self {
self.foreign_key = Some(fk.into());
self
}
pub fn inverse(mut self, inverse: impl Into<String>) -> Self {
self.inverse = Some(inverse.into());
self
}
pub fn navigation(mut self, hint: NavigationHint) -> Self {
self.navigation = hint;
self
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn relationship_def_new_sets_defaults() {
let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne);
assert_eq!(rel.name, "customer");
assert_eq!(rel.target, "customer");
assert_eq!(rel.cardinality, Cardinality::ManyToOne);
assert_eq!(rel.navigation, NavigationHint::Link);
assert!(rel.foreign_key.is_none());
assert!(rel.inverse.is_none());
assert!(rel.description.is_none());
}
#[test]
fn relationship_def_builder_chain() {
let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne)
.foreign_key("customer_id")
.inverse("orders")
.navigation(NavigationHint::Tab)
.description("Customer who placed this order");
assert_eq!(rel.name, "customer");
assert_eq!(rel.target, "customer");
assert_eq!(rel.cardinality, Cardinality::ManyToOne);
assert_eq!(rel.navigation, NavigationHint::Tab);
assert_eq!(rel.foreign_key.as_deref(), Some("customer_id"));
assert_eq!(rel.inverse.as_deref(), Some("orders"));
assert_eq!(
rel.description.as_deref(),
Some("Customer who placed this order")
);
}
#[test]
fn cardinality_default_navigation() {
assert_eq!(
Cardinality::OneToOne.default_navigation(),
NavigationHint::Inline
);
assert_eq!(
Cardinality::ManyToOne.default_navigation(),
NavigationHint::Link
);
assert_eq!(
Cardinality::OneToMany.default_navigation(),
NavigationHint::Nested
);
assert_eq!(
Cardinality::ManyToMany.default_navigation(),
NavigationHint::Nested
);
}
#[test]
fn relationship_def_serde_round_trip() {
let rel = RelationshipDef::new("customer", "customer", Cardinality::ManyToOne)
.foreign_key("customer_id")
.inverse("orders")
.navigation(NavigationHint::Link)
.description("Customer who placed this order");
let json = serde_json::to_string(&rel).unwrap();
let parsed: RelationshipDef = serde_json::from_str(&json).unwrap();
assert_eq!(rel, parsed);
}
#[test]
fn relationship_def_json_omits_none_fields() {
let rel = RelationshipDef::new("items", "item", Cardinality::OneToMany);
let json = serde_json::to_string(&rel).unwrap();
assert!(!json.contains("foreign_key"));
assert!(!json.contains("inverse"));
assert!(!json.contains("description"));
assert!(json.contains("name"));
assert!(json.contains("target"));
assert!(json.contains("cardinality"));
assert!(json.contains("navigation"));
}
#[test]
fn cardinality_serde_values() {
assert_eq!(
serde_json::to_string(&Cardinality::OneToOne).unwrap(),
r#""one_to_one""#
);
assert_eq!(
serde_json::to_string(&Cardinality::OneToMany).unwrap(),
r#""one_to_many""#
);
assert_eq!(
serde_json::to_string(&Cardinality::ManyToOne).unwrap(),
r#""many_to_one""#
);
assert_eq!(
serde_json::to_string(&Cardinality::ManyToMany).unwrap(),
r#""many_to_many""#
);
for card in [
Cardinality::OneToOne,
Cardinality::OneToMany,
Cardinality::ManyToOne,
Cardinality::ManyToMany,
] {
let json = serde_json::to_string(&card).unwrap();
let parsed: Cardinality = serde_json::from_str(&json).unwrap();
assert_eq!(card, parsed);
}
}
#[test]
fn navigation_hint_serde_values() {
assert_eq!(
serde_json::to_string(&NavigationHint::Inline).unwrap(),
r#""inline""#
);
assert_eq!(
serde_json::to_string(&NavigationHint::Link).unwrap(),
r#""link""#
);
assert_eq!(
serde_json::to_string(&NavigationHint::Tab).unwrap(),
r#""tab""#
);
assert_eq!(
serde_json::to_string(&NavigationHint::Nested).unwrap(),
r#""nested""#
);
assert_eq!(
serde_json::to_string(&NavigationHint::Hidden).unwrap(),
r#""hidden""#
);
for hint in [
NavigationHint::Inline,
NavigationHint::Link,
NavigationHint::Tab,
NavigationHint::Nested,
NavigationHint::Hidden,
] {
let json = serde_json::to_string(&hint).unwrap();
let parsed: NavigationHint = serde_json::from_str(&json).unwrap();
assert_eq!(hint, parsed);
}
}
#[test]
fn relationship_def_json_schema() {
let schema = schemars::schema_for!(RelationshipDef);
let value = schema.to_value();
let props = value
.get("properties")
.expect("RelationshipDef schema must have properties");
let obj = props.as_object().unwrap();
assert!(obj.contains_key("name"), "missing 'name' property");
assert!(obj.contains_key("target"), "missing 'target' property");
assert!(
obj.contains_key("cardinality"),
"missing 'cardinality' property"
);
assert!(
obj.contains_key("navigation"),
"missing 'navigation' property"
);
assert!(
obj.contains_key("foreign_key"),
"missing 'foreign_key' property"
);
assert!(obj.contains_key("inverse"), "missing 'inverse' property");
}
}