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