finance_query/constants/enums/value_format.rs
1use serde::{Deserialize, Serialize};
2
3/// Value format for API responses
4///
5/// Controls how `FormattedValue<T>` fields are serialized in responses.
6/// This allows API consumers to choose between raw numeric values,
7/// human-readable formatted strings, or both.
8/// The `alias`es mirror the shorthands [`FromStr`](std::str::FromStr) accepts, so
9/// deserializing (axum query extraction, JSON) takes the same spellings parsing does.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum ValueFormat {
13 /// Return only raw numeric values (e.g., `123.45`) - default
14 /// Best for programmatic use, calculations, charts
15 #[default]
16 Raw,
17 /// Return only formatted strings (e.g., `"$123.45"`, `"1.2B"`)
18 /// Best for display purposes
19 #[serde(alias = "fmt")]
20 Pretty,
21 /// Return both raw and formatted values
22 /// Returns the full `{raw, fmt, longFmt}` object
23 #[serde(alias = "full")]
24 Both,
25}
26
27impl std::str::FromStr for ValueFormat {
28 type Err = ();
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 match s.to_lowercase().as_str() {
32 "raw" => Ok(ValueFormat::Raw),
33 "pretty" | "fmt" => Ok(ValueFormat::Pretty),
34 "both" | "full" => Ok(ValueFormat::Both),
35 _ => Err(()),
36 }
37 }
38}
39
40impl ValueFormat {
41 /// Parse from string (case-insensitive), returns None on invalid input
42 pub fn parse(s: &str) -> Option<Self> {
43 s.parse().ok()
44 }
45
46 /// Convert to string representation
47 pub fn as_str(&self) -> &'static str {
48 match self {
49 ValueFormat::Raw => "raw",
50 ValueFormat::Pretty => "pretty",
51 ValueFormat::Both => "both",
52 }
53 }
54
55 /// Transform a JSON value based on this format
56 ///
57 /// Recursively processes the JSON, detecting FormattedValue objects
58 /// (objects with `raw` key and optionally `fmt`/`longFmt`) and
59 /// transforming them according to the format setting.
60 ///
61 /// # Example
62 ///
63 /// ```
64 /// use finance_query::ValueFormat;
65 /// use serde_json::json;
66 ///
67 /// let data = json!({"price": {"raw": 123.45, "fmt": "$123.45"}});
68 ///
69 /// // Raw format extracts just the raw value (default)
70 /// let raw = ValueFormat::default().transform(data.clone());
71 /// assert_eq!(raw, json!({"price": 123.45}));
72 ///
73 /// // Pretty extracts just the formatted string
74 /// let pretty = ValueFormat::Pretty.transform(data.clone());
75 /// assert_eq!(pretty, json!({"price": "$123.45"}));
76 ///
77 /// // Both keeps the full object
78 /// let both = ValueFormat::Both.transform(data);
79 /// assert_eq!(both, json!({"price": {"raw": 123.45, "fmt": "$123.45"}}));
80 /// ```
81 pub fn transform(&self, value: serde_json::Value) -> serde_json::Value {
82 match self {
83 ValueFormat::Both => value, // No transformation needed
84 _ => self.transform_recursive(value),
85 }
86 }
87
88 fn transform_recursive(&self, value: serde_json::Value) -> serde_json::Value {
89 use serde_json::Value;
90
91 match value {
92 Value::Object(map) => {
93 // Check if this looks like a FormattedValue (has 'raw' key)
94 if self.is_formatted_value(&map) {
95 return self.extract_value(&map);
96 }
97
98 // Otherwise, recursively transform all values
99 let transformed: serde_json::Map<String, Value> = map
100 .into_iter()
101 .map(|(k, v)| (k, self.transform_recursive(v)))
102 .collect();
103 Value::Object(transformed)
104 }
105 Value::Array(arr) => Value::Array(
106 arr.into_iter()
107 .map(|v| self.transform_recursive(v))
108 .collect(),
109 ),
110 // Primitives pass through unchanged
111 other => other,
112 }
113 }
114
115 /// Check if an object looks like a FormattedValue
116 fn is_formatted_value(&self, map: &serde_json::Map<String, serde_json::Value>) -> bool {
117 // Must have 'raw' key (can be null)
118 // May have 'fmt' and/or 'longFmt'
119 // Should not have many other keys (FormattedValue only has these 3)
120 if !map.contains_key("raw") {
121 return false;
122 }
123
124 let known_keys = ["raw", "fmt", "longFmt"];
125 let unknown_keys = map
126 .keys()
127 .filter(|k| !known_keys.contains(&k.as_str()))
128 .count();
129
130 // If there are unknown keys, it's probably not a FormattedValue
131 unknown_keys == 0
132 }
133
134 /// Extract the appropriate value based on format
135 fn extract_value(&self, map: &serde_json::Map<String, serde_json::Value>) -> serde_json::Value {
136 match self {
137 ValueFormat::Raw => {
138 // Return raw value directly (or null if not present)
139 map.get("raw").cloned().unwrap_or(serde_json::Value::Null)
140 }
141 ValueFormat::Pretty => {
142 // Prefer fmt, fall back to longFmt, then null
143 map.get("fmt")
144 .or_else(|| map.get("longFmt"))
145 .cloned()
146 .unwrap_or(serde_json::Value::Null)
147 }
148 ValueFormat::Both => {
149 // Keep as-is (shouldn't reach here, but handle anyway)
150 serde_json::Value::Object(map.clone())
151 }
152 }
153 }
154}