Skip to main content

oxihuman_core/
struct_map.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! A map from string field names to typed field values, for lightweight struct-like storage.
6
7use std::collections::HashMap;
8
9/// A typed field value in a [`StructMap`].
10#[allow(dead_code)]
11#[derive(Debug, Clone, PartialEq)]
12pub enum FieldVal {
13    Bool(bool),
14    Int(i64),
15    Float(f32),
16    Str(String),
17    Bytes(Vec<u8>),
18}
19
20/// A map of named fields with typed values.
21#[allow(dead_code)]
22pub struct StructMap {
23    fields: HashMap<String, FieldVal>,
24    schema: Vec<String>,
25}
26
27#[allow(dead_code)]
28impl StructMap {
29    pub fn new() -> Self {
30        Self {
31            fields: HashMap::new(),
32            schema: Vec::new(),
33        }
34    }
35
36    /// Declare a field name (optional schema enforcement).
37    pub fn declare(&mut self, name: &str) {
38        if !self.schema.contains(&name.to_string()) {
39            self.schema.push(name.to_string());
40        }
41    }
42
43    pub fn set(&mut self, name: &str, val: FieldVal) {
44        self.fields.insert(name.to_string(), val);
45    }
46
47    pub fn get(&self, name: &str) -> Option<&FieldVal> {
48        self.fields.get(name)
49    }
50
51    pub fn remove(&mut self, name: &str) -> bool {
52        self.fields.remove(name).is_some()
53    }
54
55    pub fn contains(&self, name: &str) -> bool {
56        self.fields.contains_key(name)
57    }
58
59    pub fn field_count(&self) -> usize {
60        self.fields.len()
61    }
62
63    pub fn is_empty(&self) -> bool {
64        self.fields.is_empty()
65    }
66
67    pub fn field_names(&self) -> Vec<String> {
68        let mut names: Vec<String> = self.fields.keys().cloned().collect();
69        names.sort();
70        names
71    }
72
73    pub fn get_bool(&self, name: &str) -> Option<bool> {
74        match self.fields.get(name)? {
75            FieldVal::Bool(b) => Some(*b),
76            _ => None,
77        }
78    }
79
80    pub fn get_int(&self, name: &str) -> Option<i64> {
81        match self.fields.get(name)? {
82            FieldVal::Int(i) => Some(*i),
83            _ => None,
84        }
85    }
86
87    pub fn get_float(&self, name: &str) -> Option<f32> {
88        match self.fields.get(name)? {
89            FieldVal::Float(f) => Some(*f),
90            _ => None,
91        }
92    }
93
94    pub fn get_str(&self, name: &str) -> Option<&str> {
95        match self.fields.get(name)? {
96            FieldVal::Str(s) => Some(s.as_str()),
97            _ => None,
98        }
99    }
100
101    pub fn missing_declared(&self) -> Vec<String> {
102        self.schema
103            .iter()
104            .filter(|n| !self.fields.contains_key(*n))
105            .cloned()
106            .collect()
107    }
108
109    pub fn clear(&mut self) {
110        self.fields.clear();
111    }
112}
113
114impl Default for StructMap {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120pub fn new_struct_map() -> StructMap {
121    StructMap::new()
122}
123
124pub fn stm_set(m: &mut StructMap, name: &str, val: FieldVal) {
125    m.set(name, val);
126}
127
128pub fn stm_get(m: &StructMap, name: &str) -> Option<FieldVal> {
129    m.get(name).cloned()
130}
131
132pub fn stm_contains(m: &StructMap, name: &str) -> bool {
133    m.contains(name)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn empty_on_creation() {
142        let m = new_struct_map();
143        assert!(m.is_empty());
144    }
145
146    #[test]
147    fn set_and_get_bool() {
148        let mut m = new_struct_map();
149        stm_set(&mut m, "active", FieldVal::Bool(true));
150        assert_eq!(m.get_bool("active"), Some(true));
151    }
152
153    #[test]
154    fn set_and_get_int() {
155        let mut m = new_struct_map();
156        stm_set(&mut m, "age", FieldVal::Int(30));
157        assert_eq!(m.get_int("age"), Some(30));
158    }
159
160    #[test]
161    fn set_and_get_float() {
162        let mut m = new_struct_map();
163        stm_set(&mut m, "x", FieldVal::Float(std::f32::consts::PI));
164        assert!((m.get_float("x").expect("should succeed") - std::f32::consts::PI).abs() < 1e-5);
165    }
166
167    #[test]
168    fn set_and_get_str() {
169        let mut m = new_struct_map();
170        stm_set(&mut m, "name", FieldVal::Str("Alice".to_string()));
171        assert_eq!(m.get_str("name"), Some("Alice"));
172    }
173
174    #[test]
175    fn remove_field() {
176        let mut m = new_struct_map();
177        stm_set(&mut m, "tmp", FieldVal::Bool(false));
178        assert!(m.remove("tmp"));
179        assert!(!stm_contains(&m, "tmp"));
180    }
181
182    #[test]
183    fn field_names_sorted() {
184        let mut m = new_struct_map();
185        stm_set(&mut m, "z", FieldVal::Int(1));
186        stm_set(&mut m, "a", FieldVal::Int(2));
187        assert_eq!(m.field_names(), vec!["a".to_string(), "z".to_string()]);
188    }
189
190    #[test]
191    fn missing_declared_fields() {
192        let mut m = new_struct_map();
193        m.declare("x");
194        m.declare("y");
195        stm_set(&mut m, "x", FieldVal::Int(0));
196        assert_eq!(m.missing_declared(), vec!["y".to_string()]);
197    }
198
199    #[test]
200    fn clear_resets() {
201        let mut m = new_struct_map();
202        stm_set(&mut m, "k", FieldVal::Bool(true));
203        m.clear();
204        assert!(m.is_empty());
205    }
206
207    #[test]
208    fn wrong_type_returns_none() {
209        let mut m = new_struct_map();
210        stm_set(&mut m, "x", FieldVal::Int(5));
211        assert_eq!(m.get_bool("x"), None);
212    }
213}