1use crate::error::{ModelError, Result};
4use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "lowercase")]
10#[non_exhaustive]
11pub enum CharFormat {
12 Bool,
14 Uint8,
16 Uint16,
18 Uint32,
20 Uint64,
22 Int,
24 Float,
26 String,
28 Tlv8,
30 Data,
32}
33
34impl CharFormat {
35 pub fn as_str(self) -> &'static str {
37 match self {
38 CharFormat::Bool => "bool",
39 CharFormat::Uint8 => "uint8",
40 CharFormat::Uint16 => "uint16",
41 CharFormat::Uint32 => "uint32",
42 CharFormat::Uint64 => "uint64",
43 CharFormat::Int => "int",
44 CharFormat::Float => "float",
45 CharFormat::String => "string",
46 CharFormat::Tlv8 => "tlv8",
47 CharFormat::Data => "data",
48 }
49 }
50
51 fn uint_max(self) -> Option<u64> {
52 match self {
53 CharFormat::Uint8 => Some(u64::from(u8::MAX)),
54 CharFormat::Uint16 => Some(u64::from(u16::MAX)),
55 CharFormat::Uint32 => Some(u64::from(u32::MAX)),
56 CharFormat::Uint64 => Some(u64::MAX),
57 _ => None,
58 }
59 }
60
61 pub fn value_from_json(self, v: &serde_json::Value) -> Result<CharValue> {
68 use serde_json::Value as J;
69 match self {
70 CharFormat::Bool => match v {
71 J::Bool(b) => Ok(CharValue::Bool(*b)),
72 J::Number(n) if n.as_u64() == Some(0) => Ok(CharValue::Bool(false)),
73 J::Number(n) if n.as_u64() == Some(1) => Ok(CharValue::Bool(true)),
74 other => Err(self.type_err(other)),
75 },
76 CharFormat::Uint8 | CharFormat::Uint16 | CharFormat::Uint32 | CharFormat::Uint64 => {
77 let n = v.as_u64().ok_or_else(|| self.type_err(v))?;
78 if let Some(max) = self.uint_max() {
79 if n > max {
80 return Err(ModelError::ValueRange {
81 format: self.as_str(),
82 value: n.to_string(),
83 });
84 }
85 }
86 Ok(CharValue::Uint(n))
87 }
88 CharFormat::Int => {
89 let n = v.as_i64().ok_or_else(|| self.type_err(v))?;
90 Ok(CharValue::Int(n))
91 }
92 CharFormat::Float => {
93 let f = v.as_f64().ok_or_else(|| self.type_err(v))?;
94 Ok(CharValue::Float(f))
95 }
96 CharFormat::String => match v {
97 J::String(s) => Ok(CharValue::Str(s.clone())),
98 other => Err(self.type_err(other)),
99 },
100 CharFormat::Tlv8 | CharFormat::Data => match v {
101 J::String(s) => {
102 let bytes = B64
103 .decode(s.as_bytes())
104 .map_err(|e| ModelError::Base64(e.to_string()))?;
105 Ok(CharValue::Bytes(bytes))
106 }
107 other => Err(self.type_err(other)),
108 },
109 }
110 }
111
112 fn type_err(self, v: &serde_json::Value) -> ModelError {
113 ModelError::ValueType {
114 format: self.as_str(),
115 detail: format!("got JSON {v}"),
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq)]
122#[non_exhaustive]
123pub enum CharValue {
124 Bool(bool),
126 Int(i64),
128 Uint(u64),
130 Float(f64),
132 Str(String),
134 Bytes(Vec<u8>),
136}
137
138impl CharValue {
139 pub fn to_json(&self) -> serde_json::Value {
142 use serde_json::Value as J;
143 match self {
144 CharValue::Bool(b) => J::Bool(*b),
145 CharValue::Int(n) => J::Number((*n).into()),
146 CharValue::Uint(n) => J::Number((*n).into()),
147 CharValue::Float(f) => serde_json::Number::from_f64(*f).map_or(J::Null, J::Number),
148 CharValue::Str(s) => J::String(s.clone()),
149 CharValue::Bytes(b) => J::String(B64.encode(b)),
150 }
151 }
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used)]
157mod tests {
158 use super::*;
159 use serde_json::json;
160
161 #[test]
162 fn bool_accepts_json_bool_and_0_1() {
163 assert_eq!(
164 CharFormat::Bool.value_from_json(&json!(true)).unwrap(),
165 CharValue::Bool(true)
166 );
167 assert_eq!(
168 CharFormat::Bool.value_from_json(&json!(0)).unwrap(),
169 CharValue::Bool(false)
170 );
171 assert_eq!(
172 CharFormat::Bool.value_from_json(&json!(1)).unwrap(),
173 CharValue::Bool(true)
174 );
175 assert!(CharFormat::Bool.value_from_json(&json!("x")).is_err());
176 }
177
178 #[test]
179 fn uint8_rejects_over_255() {
180 assert_eq!(
181 CharFormat::Uint8.value_from_json(&json!(255)).unwrap(),
182 CharValue::Uint(255)
183 );
184 assert!(matches!(
185 CharFormat::Uint8.value_from_json(&json!(256)),
186 Err(ModelError::ValueRange { .. })
187 ));
188 }
189
190 #[test]
191 fn float_accepts_integer_json() {
192 assert_eq!(
193 CharFormat::Float.value_from_json(&json!(3)).unwrap(),
194 CharValue::Float(3.0)
195 );
196 assert_eq!(
197 CharFormat::Float.value_from_json(&json!(0.5)).unwrap(),
198 CharValue::Float(0.5)
199 );
200 }
201
202 #[test]
203 fn tlv8_base64_round_trip() {
204 let v = CharFormat::Tlv8.value_from_json(&json!("AQID")).unwrap();
206 assert_eq!(v, CharValue::Bytes(vec![1, 2, 3]));
207 assert_eq!(v.to_json(), json!("AQID"));
208 assert!(matches!(
209 CharFormat::Data.value_from_json(&json!("!!!")),
210 Err(ModelError::Base64(_))
211 ));
212 }
213
214 #[test]
215 fn format_serde_round_trip() {
216 let s = serde_json::to_string(&CharFormat::Uint16).unwrap();
217 assert_eq!(s, "\"uint16\"");
218 let f: CharFormat = serde_json::from_str("\"tlv8\"").unwrap();
219 assert_eq!(f, CharFormat::Tlv8);
220 }
221}