Skip to main content

nightjar_lang/context/
entity.rs

1// Copyright 2026 Wayne Hong (h-alice) <contact@halice.art>
2// Nightjar Language Project
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Entity module.
17//!
18//! Entity is the core tagged data value (Int, Float, String, Bool, List, Map, Null)
19//! with its TypeTag enum and conversions used throughout the runtime.
20
21use std::collections::HashMap;
22use std::fmt;
23
24/// Type tags
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum TypeTag {
27    /// 64-bit signed integer.
28    Int,
29    /// IEEE-754 double-precision float.
30    Float,
31    /// UTF-8 string.
32    String,
33    /// Boolean.
34    Bool,
35    /// Ordered, heterogeneous list.
36    List,
37    /// String-keyed, heterogeneous map.
38    Map,
39    /// Absence of value.
40    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/// The fundamental data-holding unit.
59#[derive(Debug, Clone, PartialEq)]
60pub enum Entity {
61    /// 64-bit signed integer.
62    Int(i64),
63    /// IEEE-754 double-precision float.
64    Float(f64),
65    /// UTF-8 string.
66    String(String),
67    /// Boolean.
68    Bool(bool),
69    /// Ordered, heterogeneous list of entities.
70    List(Vec<Entity>),
71    /// String-keyed map of entities.
72    Map(HashMap<String, Entity>),
73    /// Absence of value.
74    Null,
75}
76
77impl Entity {
78    /// Return the [`TypeTag`] corresponding to this entity's variant.
79    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    /// NonEmpty check
92    ///
93    /// - `String`/`List`/`Map` are empty when their container is empty.
94    /// - `Null` is always empty.
95    /// - Scalars (`Int`, `Float`, `Bool`) are always non-empty.
96    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
107// ─────────────────── promote to entity type ───────────────────
108
109impl 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                    // Extremely large u64 values that don't fit in i64 or f64:
152                    // fall back to 0 via f64
153                    //
154                    //practically unreachable, who would use such large values?
155                    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}