1use crate::{
2 types::{Account, Principal, Ulid},
3 value::{TextMode, Value, ValueFamily},
4};
5use std::{cmp::Ordering, collections::BTreeMap, mem::discriminant, str::FromStr};
6
7#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
27pub enum CoercionId {
28 Strict,
29 NumericWiden,
30 IdentifierText,
31 TextCasefold,
32 CollectionElement,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CoercionSpec {
47 pub id: CoercionId,
48 pub params: BTreeMap<String, String>,
49}
50
51impl CoercionSpec {
52 #[must_use]
53 pub const fn new(id: CoercionId) -> Self {
54 Self {
55 id,
56 params: BTreeMap::new(),
57 }
58 }
59}
60
61impl Default for CoercionSpec {
62 fn default() -> Self {
63 Self::new(CoercionId::Strict)
64 }
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum CoercionFamily {
73 Any,
74 Family(ValueFamily),
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub struct CoercionRule {
89 pub left: CoercionFamily,
90 pub right: CoercionFamily,
91 pub id: CoercionId,
92}
93
94pub const COERCION_TABLE: &[CoercionRule] = &[
95 CoercionRule {
96 left: CoercionFamily::Any,
97 right: CoercionFamily::Any,
98 id: CoercionId::Strict,
99 },
100 CoercionRule {
101 left: CoercionFamily::Family(ValueFamily::Numeric),
102 right: CoercionFamily::Family(ValueFamily::Numeric),
103 id: CoercionId::NumericWiden,
104 },
105 CoercionRule {
106 left: CoercionFamily::Family(ValueFamily::Identifier),
107 right: CoercionFamily::Family(ValueFamily::Textual),
108 id: CoercionId::IdentifierText,
109 },
110 CoercionRule {
111 left: CoercionFamily::Family(ValueFamily::Textual),
112 right: CoercionFamily::Family(ValueFamily::Identifier),
113 id: CoercionId::IdentifierText,
114 },
115 CoercionRule {
116 left: CoercionFamily::Family(ValueFamily::Textual),
117 right: CoercionFamily::Family(ValueFamily::Textual),
118 id: CoercionId::TextCasefold,
119 },
120 CoercionRule {
121 left: CoercionFamily::Family(ValueFamily::Identifier),
122 right: CoercionFamily::Family(ValueFamily::Identifier),
123 id: CoercionId::TextCasefold,
124 },
125 CoercionRule {
126 left: CoercionFamily::Family(ValueFamily::Identifier),
127 right: CoercionFamily::Family(ValueFamily::Textual),
128 id: CoercionId::TextCasefold,
129 },
130 CoercionRule {
131 left: CoercionFamily::Family(ValueFamily::Textual),
132 right: CoercionFamily::Family(ValueFamily::Identifier),
133 id: CoercionId::TextCasefold,
134 },
135 CoercionRule {
136 left: CoercionFamily::Any,
137 right: CoercionFamily::Any,
138 id: CoercionId::CollectionElement,
139 },
140];
141
142#[must_use]
143pub fn supports_coercion(left: ValueFamily, right: ValueFamily, id: CoercionId) -> bool {
144 COERCION_TABLE.iter().any(|rule| {
145 rule.id == id && family_matches(rule.left, left) && family_matches(rule.right, right)
146 })
147}
148
149fn family_matches(rule: CoercionFamily, value: ValueFamily) -> bool {
150 match rule {
151 CoercionFamily::Any => true,
152 CoercionFamily::Family(expected) => expected == value,
153 }
154}
155
156#[derive(Clone, Copy, Debug, Eq, PartialEq)]
161pub enum TextOp {
162 Eq,
163 Contains,
164 StartsWith,
165 EndsWith,
166}
167
168#[must_use]
173pub fn compare_eq(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<bool> {
174 match coercion.id {
175 CoercionId::Strict | CoercionId::CollectionElement => {
176 same_variant(left, right).then_some(left == right)
177 }
178 CoercionId::NumericWiden => left.cmp_numeric(right).map(|ord| ord == Ordering::Equal),
179 CoercionId::IdentifierText => {
180 let (l, r) = coerce_identifier_text(left, right)?;
181 Some(l == r)
182 }
183 CoercionId::TextCasefold => compare_casefold(left, right),
184 }
185}
186
187#[must_use]
192pub fn compare_order(left: &Value, right: &Value, coercion: &CoercionSpec) -> Option<Ordering> {
193 match coercion.id {
194 CoercionId::Strict | CoercionId::CollectionElement => {
195 if !same_variant(left, right) {
196 return None;
197 }
198 strict_ordering(left, right)
199 }
200 CoercionId::NumericWiden => left.cmp_numeric(right),
201 CoercionId::IdentifierText => {
202 let (l, r) = coerce_identifier_text(left, right)?;
203 strict_ordering(&l, &r)
204 }
205 CoercionId::TextCasefold => {
206 let left = casefold_value(left)?;
207 let right = casefold_value(right)?;
208 Some(left.cmp(&right))
209 }
210 }
211}
212
213#[must_use]
218pub fn compare_text(
219 left: &Value,
220 right: &Value,
221 coercion: &CoercionSpec,
222 op: TextOp,
223) -> Option<bool> {
224 let mode = match coercion.id {
225 CoercionId::Strict => TextMode::Cs,
226 CoercionId::TextCasefold => TextMode::Ci,
227 _ => return None,
228 };
229
230 match op {
231 TextOp::Eq => left.text_eq(right, mode),
232 TextOp::Contains => left.text_contains(right, mode),
233 TextOp::StartsWith => left.text_starts_with(right, mode),
234 TextOp::EndsWith => left.text_ends_with(right, mode),
235 }
236}
237
238fn same_variant(left: &Value, right: &Value) -> bool {
239 discriminant(left) == discriminant(right)
240}
241
242fn strict_ordering(left: &Value, right: &Value) -> Option<Ordering> {
247 match (left, right) {
248 (Value::Account(a), Value::Account(b)) => Some(a.cmp(b)),
249 (Value::Bool(a), Value::Bool(b)) => a.partial_cmp(b),
250 (Value::Date(a), Value::Date(b)) => a.partial_cmp(b),
251 (Value::Decimal(a), Value::Decimal(b)) => a.partial_cmp(b),
252 (Value::Duration(a), Value::Duration(b)) => a.partial_cmp(b),
253 (Value::E8s(a), Value::E8s(b)) => a.partial_cmp(b),
254 (Value::E18s(a), Value::E18s(b)) => a.partial_cmp(b),
255 (Value::Enum(a), Value::Enum(b)) => a.partial_cmp(b),
256 (Value::Float32(a), Value::Float32(b)) => a.partial_cmp(b),
257 (Value::Float64(a), Value::Float64(b)) => a.partial_cmp(b),
258 (Value::Int(a), Value::Int(b)) => a.partial_cmp(b),
259 (Value::Int128(a), Value::Int128(b)) => a.partial_cmp(b),
260 (Value::IntBig(a), Value::IntBig(b)) => a.partial_cmp(b),
261 (Value::Principal(a), Value::Principal(b)) => a.partial_cmp(b),
262 (Value::Subaccount(a), Value::Subaccount(b)) => a.partial_cmp(b),
263 (Value::Text(a), Value::Text(b)) => a.partial_cmp(b),
264 (Value::Timestamp(a), Value::Timestamp(b)) => a.partial_cmp(b),
265 (Value::Uint(a), Value::Uint(b)) => a.partial_cmp(b),
266 (Value::Uint128(a), Value::Uint128(b)) => a.partial_cmp(b),
267 (Value::UintBig(a), Value::UintBig(b)) => a.partial_cmp(b),
268 (Value::Ulid(a), Value::Ulid(b)) => a.partial_cmp(b),
269 (Value::Unit, Value::Unit) => Some(Ordering::Equal),
270 _ => None,
271 }
272}
273
274fn coerce_identifier_text(left: &Value, right: &Value) -> Option<(Value, Value)> {
277 match (left, right) {
278 (Value::Ulid(_) | Value::Principal(_) | Value::Account(_), Value::Text(_)) => {
279 let parsed = parse_identifier_text(left, right)?;
280 Some((left.clone(), parsed))
281 }
282 (Value::Text(_), Value::Ulid(_) | Value::Principal(_) | Value::Account(_)) => {
283 let parsed = parse_identifier_text(right, left)?;
284 Some((parsed, right.clone()))
285 }
286 _ => None,
287 }
288}
289
290fn parse_identifier_text(identifier: &Value, text: &Value) -> Option<Value> {
291 let Value::Text(raw) = text else {
292 return None;
293 };
294
295 match identifier {
296 Value::Ulid(_) => Ulid::from_str(raw).ok().map(Value::Ulid),
297 Value::Principal(_) => Principal::from_str(raw).ok().map(Value::Principal),
298 Value::Account(_) => Account::from_str(raw).ok().map(Value::Account),
299 _ => None,
300 }
301}
302
303fn compare_casefold(left: &Value, right: &Value) -> Option<bool> {
304 let left = casefold_value(left)?;
305 let right = casefold_value(right)?;
306 Some(left == right)
307}
308
309fn casefold_value(value: &Value) -> Option<String> {
312 match value {
313 Value::Text(text) => Some(casefold(text)),
314 Value::Ulid(ulid) => Some(casefold(&ulid.to_string())),
315 Value::Principal(principal) => Some(casefold(&principal.to_string())),
316 Value::Account(account) => Some(casefold(&account.to_string())),
317 _ => None,
318 }
319}
320
321fn casefold(input: &str) -> String {
322 if input.is_ascii() {
323 return input.to_ascii_lowercase();
324 }
325
326 input.to_lowercase()
328}