Skip to main content

az_dict_spec/
lib.rs

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}