Skip to main content

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}