Skip to main content

mpl_lang/
tags.rs

1//! Series tags and tag values
2use std::{
3    fmt,
4    hash::{DefaultHasher, Hash, Hasher},
5};
6
7use ordered_float::OrderedFloat;
8use strumbra::SharedString;
9
10use crate::{query::TagType, types::StrumbraError};
11
12/// Value for a tag k/v pair
13#[derive(Clone, PartialEq, serde::Deserialize, serde::Serialize, Default)]
14#[cfg_attr(feature = "bincode", derive(bincode::Encode, bincode::Decode))]
15#[serde(untagged)]
16#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
17#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
18pub enum TagValue {
19    #[default]
20    /// No value
21    None,
22    /// Boolean value
23    Bool(bool),
24    /// Integer value
25    Int(i64),
26    /// Float value
27    Float(f64),
28    /// String value
29    String(
30        #[cfg_attr(feature = "wasm", tsify(type = "String"))]
31        #[cfg_attr(feature = "bincode", bincode(with_serde))]
32        SharedString,
33    ),
34}
35impl TagValue {
36    /// Returns the type of the tag value.
37    #[must_use]
38    pub fn tpe(&self) -> TagType {
39        match self {
40            Self::None => TagType::None,
41            Self::Bool(_) => TagType::Bool,
42            Self::Int(_) => TagType::Int,
43            Self::Float(_) => TagType::Float,
44            Self::String(_) => TagType::String,
45        }
46    }
47}
48
49impl fmt::Debug for TagValue {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        match self {
52            Self::None => write!(f, "None"),
53            Self::Bool(arg0) => f.debug_tuple("Bool").field(arg0).finish(),
54            Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(),
55            Self::Float(arg0) => f.debug_tuple("Float").field(arg0).finish(),
56            Self::String(arg0) => {
57                // Since arguments could include PII we do replace them with a hash
58                let mut hasher = DefaultHasher::new();
59                arg0.hash(&mut hasher);
60                f.debug_tuple("PiiSafeString")
61                    .field(&hasher.finish())
62                    .finish()
63            }
64        }
65    }
66}
67
68impl Ord for TagValue {
69    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
70        match (self, other) {
71            // First the easy cases, if we have two values of the same type,
72            // compare them directly
73            (TagValue::None, TagValue::None) => std::cmp::Ordering::Equal,
74            (TagValue::Int(a), TagValue::Int(b)) => a.cmp(b),
75            (TagValue::Float(a), TagValue::Float(b)) => OrderedFloat(*a).cmp(&OrderedFloat(*b)),
76            (TagValue::String(a), TagValue::String(b)) => a.cmp(b),
77            (TagValue::Bool(a), TagValue::Bool(b)) => a.cmp(b),
78
79            // If we have two numeric values of different types,
80            // cast them to f64 for and compare
81            (TagValue::Int(i), TagValue::Float(f)) =>
82            {
83                #[allow(clippy::cast_precision_loss)]
84                OrderedFloat(*i as f64).cmp(&OrderedFloat(*f))
85            }
86            (TagValue::Float(f), TagValue::Int(i)) =>
87            {
88                #[allow(clippy::cast_precision_loss)]
89                OrderedFloat(*f).cmp(&OrderedFloat(*i as f64))
90            }
91
92            // This are now in reverse order of precedence
93            // the rule we use is 'the more complex the type is the
94            // greater the ordering'
95
96            // Everything greater than None
97            (TagValue::None, _) => std::cmp::Ordering::Less,
98            (_, TagValue::None) => std::cmp::Ordering::Greater,
99
100            // The rest if larger than bool
101            (TagValue::Bool(_), _) => std::cmp::Ordering::Less,
102            (_, TagValue::Bool(_)) => std::cmp::Ordering::Greater,
103
104            // now everything else is larger than int
105            (TagValue::Int(_), _) => std::cmp::Ordering::Less,
106            (_, TagValue::Int(_)) => std::cmp::Ordering::Greater,
107
108            // now everything else is larger than float
109            (TagValue::Float(_), _) => std::cmp::Ordering::Less,
110            (_, TagValue::Float(_)) => std::cmp::Ordering::Greater,
111            // string is the largest type - this is a unreachable case
112            // as the prior matches already handle this.
113            // (TagValue::String(_), _) => std::cmp::Ordering::Less,
114            // (_, TagValue::String(_)) => std::cmp::Ordering::Greater,
115        }
116    }
117}
118impl PartialOrd for TagValue {
119    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
120        Some(self.cmp(other))
121    }
122}
123impl TagValue {
124    /// Tries to access the tag value as a string
125    #[must_use]
126    pub fn as_str(&self) -> Option<&str> {
127        if let TagValue::String(s) = self {
128            Some(s.as_str())
129        } else {
130            None
131        }
132    }
133
134    /// Returns the length of the tag value
135    #[must_use]
136    pub fn len(&self) -> usize {
137        match self {
138            TagValue::None => 0,
139            TagValue::String(s) => s.len(),
140            TagValue::Int(_) | TagValue::Float(_) => 8, // size of i64 or f64
141            TagValue::Bool(_) => 1,                     // size of bool
142        }
143    }
144    /// Returns true if the tag value is empty
145    #[must_use]
146    pub fn is_empty(&self) -> bool {
147        match self {
148            TagValue::None => true,
149            TagValue::String(s) => s.is_empty(),
150            TagValue::Bool(_) | TagValue::Int(_) | TagValue::Float(_) => false, // bool, i64 and f64 are never empty
151        }
152    }
153}
154
155impl Hash for TagValue {
156    fn hash<H: Hasher>(&self, state: &mut H) {
157        core::mem::discriminant(self).hash(state);
158        match self {
159            TagValue::None => (),
160            TagValue::String(s) => s.hash(state),
161            TagValue::Int(i) => i.hash(state),
162            TagValue::Float(fl) => OrderedFloat(*fl).hash(state),
163            TagValue::Bool(b) => b.hash(state),
164        }
165    }
166}
167
168// FIXME! This is not good since we have floats
169impl Eq for TagValue {}
170
171impl std::fmt::Display for TagValue {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        match self {
174            TagValue::None => write!(f, "None"),
175            TagValue::String(s) => {
176                let mut hasher = DefaultHasher::new();
177                s.hash(&mut hasher);
178
179                write!(f, "\"<PII Safe String: {}>\"", &hasher.finish())
180            }
181            TagValue::Int(i) => write!(f, "{i}"),
182            TagValue::Float(fl) => write!(f, "{fl}"),
183            TagValue::Bool(b) => write!(f, "{b}"),
184        }
185    }
186}
187
188impl From<i64> for TagValue {
189    fn from(i: i64) -> Self {
190        TagValue::Int(i)
191    }
192}
193
194impl From<f64> for TagValue {
195    fn from(f: f64) -> Self {
196        TagValue::Float(f)
197    }
198}
199
200impl From<bool> for TagValue {
201    fn from(b: bool) -> Self {
202        TagValue::Bool(b)
203    }
204}
205impl TryFrom<String> for TagValue {
206    type Error = StrumbraError;
207    fn try_from(s: String) -> Result<Self, Self::Error> {
208        Ok(TagValue::String(SharedString::try_from(s)?))
209    }
210}
211impl TryFrom<&str> for TagValue {
212    type Error = StrumbraError;
213    fn try_from(s: &str) -> Result<Self, Self::Error> {
214        Ok(TagValue::String(SharedString::try_from(s)?))
215    }
216}