nightjar_lang/context/
entity.rs1use std::collections::HashMap;
22use std::fmt;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TypeTag {
27 Int,
29 Float,
31 String,
33 Bool,
35 List,
37 Map,
39 Null,
41}
42
43impl fmt::Display for TypeTag {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 let s = match self {
46 TypeTag::Int => "Int",
47 TypeTag::Float => "Float",
48 TypeTag::String => "String",
49 TypeTag::Bool => "Bool",
50 TypeTag::List => "List",
51 TypeTag::Map => "Map",
52 TypeTag::Null => "Null",
53 };
54 f.write_str(s)
55 }
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub enum Entity {
61 Int(i64),
63 Float(f64),
65 String(String),
67 Bool(bool),
69 List(Vec<Entity>),
71 Map(HashMap<String, Entity>),
73 Null,
75}
76
77impl Entity {
78 pub fn type_tag(&self) -> TypeTag {
80 match self {
81 Entity::Int(_) => TypeTag::Int,
82 Entity::Float(_) => TypeTag::Float,
83 Entity::String(_) => TypeTag::String,
84 Entity::Bool(_) => TypeTag::Bool,
85 Entity::List(_) => TypeTag::List,
86 Entity::Map(_) => TypeTag::Map,
87 Entity::Null => TypeTag::Null,
88 }
89 }
90
91 pub fn is_non_empty(&self) -> bool {
97 match self {
98 Entity::String(s) => !s.is_empty(),
99 Entity::List(v) => !v.is_empty(),
100 Entity::Map(m) => !m.is_empty(),
101 Entity::Null => false,
102 Entity::Int(_) | Entity::Float(_) | Entity::Bool(_) => true,
103 }
104 }
105}
106
107impl From<i64> for Entity {
110 fn from(v: i64) -> Self {
111 Entity::Int(v)
112 }
113}
114
115impl From<f64> for Entity {
116 fn from(v: f64) -> Self {
117 Entity::Float(v)
118 }
119}
120
121impl From<String> for Entity {
122 fn from(v: String) -> Self {
123 Entity::String(v)
124 }
125}
126
127impl From<&str> for Entity {
128 fn from(v: &str) -> Self {
129 Entity::String(v.to_string())
130 }
131}
132
133impl From<bool> for Entity {
134 fn from(v: bool) -> Self {
135 Entity::Bool(v)
136 }
137}
138
139#[cfg(feature = "json")]
140impl From<serde_json::Value> for Entity {
141 fn from(val: serde_json::Value) -> Self {
142 match val {
143 serde_json::Value::Null => Entity::Null,
144 serde_json::Value::Bool(b) => Entity::Bool(b),
145 serde_json::Value::Number(n) => {
146 if let Some(i) = n.as_i64() {
147 Entity::Int(i)
148 } else if let Some(f) = n.as_f64() {
149 Entity::Float(f)
150 } else {
151 Entity::Float(0.0)
156 }
157 }
158 serde_json::Value::String(s) => Entity::String(s),
159 serde_json::Value::Array(arr) => {
160 Entity::List(arr.into_iter().map(Entity::from).collect())
161 }
162 serde_json::Value::Object(map) => {
163 Entity::Map(map.into_iter().map(|(k, v)| (k, Entity::from(v))).collect())
164 }
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn type_tag_for_every_variant() {
175 assert_eq!(Entity::Int(0).type_tag(), TypeTag::Int);
176 assert_eq!(Entity::Float(0.0).type_tag(), TypeTag::Float);
177 assert_eq!(Entity::String("".into()).type_tag(), TypeTag::String);
178 assert_eq!(Entity::Bool(false).type_tag(), TypeTag::Bool);
179 assert_eq!(Entity::List(vec![]).type_tag(), TypeTag::List);
180 assert_eq!(Entity::Map(HashMap::new()).type_tag(), TypeTag::Map);
181 assert_eq!(Entity::Null.type_tag(), TypeTag::Null);
182 }
183
184 #[test]
185 fn display_impl_for_typetag() {
186 assert_eq!(format!("{}", TypeTag::Int), "Int");
187 assert_eq!(format!("{}", TypeTag::Null), "Null");
188 assert_eq!(format!("{}", TypeTag::Map), "Map");
189 }
190
191 #[test]
192 fn is_non_empty_for_scalars_always_true() {
193 assert!(Entity::Int(0).is_non_empty());
194 assert!(Entity::Float(0.0).is_non_empty());
195 assert!(Entity::Bool(false).is_non_empty());
196 }
197
198 #[test]
199 fn is_non_empty_for_null_false() {
200 assert!(!Entity::Null.is_non_empty());
201 }
202
203 #[test]
204 fn is_non_empty_for_containers() {
205 assert!(!Entity::String("".into()).is_non_empty());
206 assert!(Entity::String("a".into()).is_non_empty());
207 assert!(!Entity::List(vec![]).is_non_empty());
208 assert!(Entity::List(vec![Entity::Int(1)]).is_non_empty());
209 assert!(!Entity::Map(HashMap::new()).is_non_empty());
210 let mut m = HashMap::new();
211 m.insert("k".to_string(), Entity::Int(1));
212 assert!(Entity::Map(m).is_non_empty());
213 }
214
215 #[test]
216 fn from_primitives() {
217 assert_eq!(Entity::from(5_i64), Entity::Int(5));
218 assert_eq!(Entity::from(2.5_f64), Entity::Float(2.5));
219 assert_eq!(Entity::from(true), Entity::Bool(true));
220 assert_eq!(Entity::from("hello"), Entity::String("hello".to_string()));
221 assert_eq!(
222 Entity::from(String::from("world")),
223 Entity::String("world".to_string())
224 );
225 }
226
227 #[cfg(feature = "json")]
228 #[test]
229 fn from_json_scalars() {
230 use serde_json::json;
231 assert_eq!(Entity::from(json!(null)), Entity::Null);
232 assert_eq!(Entity::from(json!(true)), Entity::Bool(true));
233 assert_eq!(Entity::from(json!(42)), Entity::Int(42));
234 assert_eq!(Entity::from(json!(-7)), Entity::Int(-7));
235 assert_eq!(Entity::from(json!(1.618)), Entity::Float(1.618));
236 assert_eq!(
237 Entity::from(json!("abc")),
238 Entity::String("abc".to_string())
239 );
240 }
241
242 #[cfg(feature = "json")]
243 #[test]
244 fn from_json_nested_object_and_array() {
245 use serde_json::json;
246 let j = json!({
247 "data": {
248 "department_1": { "revenue": 100 },
249 "department_2": { "revenue": 200 }
250 },
251 "id_list": [10, 20, 30]
252 });
253 let ent = Entity::from(j);
254 match ent {
255 Entity::Map(map) => {
256 assert_eq!(map.len(), 2);
257 assert!(matches!(map.get("data"), Some(Entity::Map(_))));
258 if let Some(Entity::List(ids)) = map.get("id_list") {
259 assert_eq!(ids.len(), 3);
260 assert_eq!(ids[0], Entity::Int(10));
261 } else {
262 panic!("id_list should be a list");
263 }
264 }
265 other => panic!("expected Map, got {:?}", other),
266 }
267 }
268
269 #[cfg(feature = "json")]
270 #[test]
271 fn from_json_preserves_unicode_keys() {
272 use serde_json::json;
273 let j = json!({ "營收": 500 });
274 let ent = Entity::from(j);
275 if let Entity::Map(map) = ent {
276 assert_eq!(map.get("營收"), Some(&Entity::Int(500)));
277 } else {
278 panic!("expected Map");
279 }
280 }
281}