backbone_core/
violation.rs1use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
22pub struct Violation {
23 pub path: String,
26 pub code: String,
28 #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
30 pub params: serde_json::Map<String, serde_json::Value>,
31 pub message: String,
33}
34
35impl Violation {
36 pub fn new(path: impl Into<String>, code: impl Into<String>, message: impl Into<String>) -> Self {
37 Self { path: path.into(), code: code.into(), params: serde_json::Map::new(), message: message.into() }
38 }
39
40 pub fn with_param(mut self, name: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
42 self.params.insert(name.into(), value.into());
43 self
44 }
45}
46
47pub fn batch_item_path(index: usize, path: &str) -> String {
50 if path.is_empty() {
51 index.to_string()
52 } else {
53 format!("{index}/{path}")
54 }
55}
56
57pub fn code_from_constraint(constraint: &str) -> String {
66 for prefix in ["ck_", "uq_", "ex_", "tg_"] {
67 if let Some(rest) = constraint.strip_prefix(prefix) {
68 if let Some((_, code)) = rest.split_once("__") {
69 if !code.is_empty() {
70 return code.to_string();
71 }
72 }
73 }
74 }
75 constraint.to_string()
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn a_generated_constraint_name_carries_its_rule_code() {
84 assert_eq!(code_from_constraint("ck_journals__unbalanced"), "unbalanced");
85 assert_eq!(code_from_constraint("uq_accounts__account_code_taken"), "account_code_taken");
86 assert_eq!(code_from_constraint("ex_tax_rates__overlapping_validity"), "overlapping_validity");
87 assert_eq!(code_from_constraint("fiscal_period_status_transition"), "fiscal_period_status_transition");
89 assert_eq!(code_from_constraint("ck_legacy"), "ck_legacy");
91 }
92
93 #[test]
94 fn a_violation_serializes_without_empty_params() {
95 let v = Violation::new("status", "field_not_writable", "`status` changes only through its verbs");
96 assert_eq!(
97 serde_json::to_value(&v).unwrap(),
98 serde_json::json!({ "path": "status", "code": "field_not_writable", "message": "`status` changes only through its verbs" })
99 );
100 let v = Violation::new("name", "max_length", "too long").with_param("max", 255);
101 assert_eq!(serde_json::to_value(&v).unwrap()["params"], serde_json::json!({ "max": 255 }));
102 }
103}