1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::BTreeSet;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct DictEnumItem<T>
7where
8 T: Copy + 'static,
9{
10 pub code: &'static str,
11 pub label: &'static str,
12 pub description: &'static str,
13 pub raw_value: T,
14 pub meta_json: Option<&'static str>,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct DictionarySpec {
20 pub code: String,
21 pub name: String,
22 pub description: Option<String>,
23 pub scope: String,
24 pub raw_value_kind: RawValueKind,
25 #[serde(default)]
26 pub open_enum: bool,
27 pub unknown_variant: Option<String>,
28 #[serde(default)]
29 pub sort_index: i64,
30 #[serde(default)]
31 pub items: Vec<DictionaryItemSpec>,
32}
33
34impl DictionarySpec {
35 pub fn from_json_str(input: &str) -> Result<Self, DictSpecError> {
36 let spec = serde_json::from_str::<Self>(input)?;
37 spec.validate()?;
38 Ok(spec)
39 }
40
41 pub fn to_pretty_json_string(&self) -> Result<String, DictSpecError> {
42 self.validate()?;
43 Ok(serde_json::to_string_pretty(self)?)
44 }
45
46 pub fn validate(&self) -> Result<(), DictSpecError> {
47 ensure_non_empty("code", &self.code)?;
48 ensure_non_empty("name", &self.name)?;
49 ensure_non_empty("scope", &self.scope)?;
50 if self.open_enum {
51 ensure_non_empty(
52 "unknownVariant",
53 self.unknown_variant.as_deref().unwrap_or("Other"),
54 )?;
55 }
56 if self.items.is_empty() {
57 return Err(DictSpecError::Validation(
58 "items cannot be empty".to_string(),
59 ));
60 }
61
62 let mut item_codes = BTreeSet::new();
63 let mut int_values = BTreeSet::new();
64 let mut text_values = BTreeSet::new();
65 for item in &self.items {
66 item.validate(self.raw_value_kind)?;
67 if !item_codes.insert(item.code.clone()) {
68 return Err(DictSpecError::Validation(format!(
69 "duplicate item code: {}",
70 item.code
71 )));
72 }
73 match self.raw_value_kind {
74 RawValueKind::Int => {
75 let value = item.raw_int_value.expect("validated raw_int_value");
76 if !int_values.insert(value) {
77 return Err(DictSpecError::Validation(format!(
78 "duplicate rawIntValue: {value}"
79 )));
80 }
81 }
82 RawValueKind::String => {
83 let value = item
84 .raw_text_value
85 .as_deref()
86 .expect("validated raw_text_value");
87 if !text_values.insert(value.to_string()) {
88 return Err(DictSpecError::Validation(format!(
89 "duplicate rawTextValue: {value}"
90 )));
91 }
92 }
93 }
94 }
95
96 Ok(())
97 }
98
99 pub fn normalized_unknown_variant(&self) -> &str {
100 self.unknown_variant
101 .as_deref()
102 .filter(|value| !value.trim().is_empty())
103 .unwrap_or("Other")
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "camelCase")]
109pub struct DictionaryItemSpec {
110 pub code: String,
111 pub label: String,
112 pub description: Option<String>,
113 pub raw_int_value: Option<i64>,
114 pub raw_text_value: Option<String>,
115 #[serde(default)]
116 pub sort_index: i64,
117 #[serde(default = "default_true")]
118 pub enabled: bool,
119 pub meta: Option<Value>,
120}
121
122impl DictionaryItemSpec {
123 pub fn validate(&self, raw_value_kind: RawValueKind) -> Result<(), DictSpecError> {
124 ensure_non_empty("item.code", &self.code)?;
125 ensure_non_empty("item.label", &self.label)?;
126 match raw_value_kind {
127 RawValueKind::Int => {
128 if self.raw_int_value.is_none() || self.raw_text_value.is_some() {
129 return Err(DictSpecError::Validation(format!(
130 "item {} must define rawIntValue only",
131 self.code
132 )));
133 }
134 }
135 RawValueKind::String => {
136 if self.raw_int_value.is_some() || self.raw_text_value.is_none() {
137 return Err(DictSpecError::Validation(format!(
138 "item {} must define rawTextValue only",
139 self.code
140 )));
141 }
142 ensure_non_empty(
143 "item.rawTextValue",
144 self.raw_text_value.as_deref().unwrap_or_default(),
145 )?;
146 }
147 }
148 Ok(())
149 }
150
151 pub fn description_text(&self) -> &str {
152 self.description.as_deref().unwrap_or("")
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "lowercase")]
158pub enum RawValueKind {
159 Int,
160 String,
161}
162
163#[derive(Debug, thiserror::Error)]
164pub enum DictSpecError {
165 #[error("invalid dictionary spec: {0}")]
166 Validation(String),
167 #[error("invalid dictionary spec json: {0}")]
168 Json(#[from] serde_json::Error),
169}
170
171fn default_true() -> bool {
172 true
173}
174
175fn ensure_non_empty(field: &str, value: &str) -> Result<(), DictSpecError> {
176 if value.trim().is_empty() {
177 return Err(DictSpecError::Validation(format!(
178 "{field} cannot be empty"
179 )));
180 }
181 Ok(())
182}