1use serde::{Deserialize, Serialize};
2use utils::{ReasoningEffort, is_false};
3
4type Meta = serde_json::Map<String, serde_json::Value>;
5
6#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
8pub struct ConfigOptionMeta {
9 #[serde(default, skip_serializing_if = "is_false")]
10 pub multi_select: bool,
11}
12
13#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
15pub struct SelectOptionMeta {
16 #[serde(default, skip_serializing_if = "Vec::is_empty")]
17 pub reasoning_levels: Vec<ReasoningEffort>,
18 #[serde(default, skip_serializing_if = "is_false")]
19 pub supports_image: bool,
20 #[serde(default, skip_serializing_if = "is_false")]
21 pub supports_audio: bool,
22}
23
24impl SelectOptionMeta {
25 pub fn supports_reasoning(&self) -> bool {
26 !self.reasoning_levels.is_empty()
27 }
28}
29
30impl ConfigOptionMeta {
31 pub fn into_meta(self) -> Option<Meta> {
32 if self == Self::default() {
33 return None;
34 }
35 match serde_json::to_value(self).expect("ConfigOptionMeta should serialize") {
36 serde_json::Value::Object(map) => Some(map),
37 _ => unreachable!(),
38 }
39 }
40
41 pub fn from_meta(meta: Option<&Meta>) -> Self {
42 meta.and_then(|m| serde_json::from_value(serde_json::Value::Object(m.clone())).ok()).unwrap_or_default()
43 }
44}
45
46impl SelectOptionMeta {
47 pub fn into_meta(self) -> Option<Meta> {
48 if self == Self::default() {
49 return None;
50 }
51 match serde_json::to_value(self).expect("SelectOptionMeta should serialize") {
52 serde_json::Value::Object(map) => Some(map),
53 _ => unreachable!(),
54 }
55 }
56
57 pub fn from_meta(meta: Option<&Meta>) -> Self {
58 meta.and_then(|m| serde_json::from_value(serde_json::Value::Object(m.clone())).ok()).unwrap_or_default()
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn config_option_meta_roundtrip() {
68 let original = ConfigOptionMeta { multi_select: true };
69 let meta = original.clone().into_meta();
70 assert!(meta.is_some());
71 let restored = ConfigOptionMeta::from_meta(meta.as_ref());
72 assert_eq!(restored, original);
73 }
74
75 #[test]
76 fn select_option_meta_roundtrip() {
77 let original = SelectOptionMeta {
78 reasoning_levels: vec![ReasoningEffort::Low, ReasoningEffort::Medium, ReasoningEffort::High],
79 supports_image: true,
80 supports_audio: false,
81 };
82 let meta = original.clone().into_meta();
83 assert!(meta.is_some());
84 let restored = SelectOptionMeta::from_meta(meta.as_ref());
85 assert_eq!(restored, original);
86 }
87
88 #[test]
89 fn default_produces_none() {
90 assert!(ConfigOptionMeta::default().into_meta().is_none());
91 assert!(SelectOptionMeta::default().into_meta().is_none());
92 }
93
94 #[test]
95 fn from_meta_none_returns_default() {
96 assert_eq!(ConfigOptionMeta::from_meta(None), ConfigOptionMeta::default());
97 assert_eq!(SelectOptionMeta::from_meta(None), SelectOptionMeta::default());
98 }
99
100 #[test]
101 fn unknown_keys_are_ignored() {
102 let mut map = serde_json::Map::new();
103 map.insert("multi_select".to_string(), serde_json::Value::Bool(true));
104 map.insert("unknown_field".to_string(), serde_json::Value::String("hello".to_string()));
105 let parsed = ConfigOptionMeta::from_meta(Some(&map));
106 assert_eq!(parsed, ConfigOptionMeta { multi_select: true });
107 }
108
109 #[test]
110 fn false_fields_omitted_from_serialized_output() {
111 let meta = ConfigOptionMeta { multi_select: false };
112 let value = serde_json::to_value(&meta).unwrap();
113 let obj = value.as_object().unwrap();
114 assert!(!obj.contains_key("multi_select"));
115
116 let meta = SelectOptionMeta::default();
117 let value = serde_json::to_value(&meta).unwrap();
118 let obj = value.as_object().unwrap();
119 assert!(!obj.contains_key("reasoning_levels"));
120 assert!(!obj.contains_key("supports_image"));
121 assert!(!obj.contains_key("supports_audio"));
122 }
123
124 #[test]
125 fn supports_reasoning_convenience() {
126 assert!(!SelectOptionMeta::default().supports_reasoning());
127 let meta = SelectOptionMeta {
128 reasoning_levels: vec![ReasoningEffort::Low, ReasoningEffort::High],
129 supports_image: false,
130 supports_audio: false,
131 };
132 assert!(meta.supports_reasoning());
133 }
134}