Skip to main content

icydb_core/value/ops/
collection.rs

1//! Module: value::ops::collection
2//!
3//! Responsibility: collection membership and emptiness operations for `Value`.
4//! Does not own: text casefolding internals or map canonicalization.
5//! Boundary: representation-local list/scalar membership helpers.
6
7use crate::value::{Value, ops::text};
8
9fn normalize_list_ref(value: &Value) -> Vec<&Value> {
10    match value {
11        Value::List(values) => values.iter().collect(),
12        value => vec![value],
13    }
14}
15
16fn contains_by<F>(value: &Value, needle: &Value, eq: F) -> Option<bool>
17where
18    F: Fn(&Value, &Value) -> bool,
19{
20    value
21        .as_list()
22        .map(|items| items.iter().any(|item| eq(item, needle)))
23}
24
25#[expect(clippy::unnecessary_wraps)]
26fn contains_any_by<F>(value: &Value, needles: &Value, eq: F) -> Option<bool>
27where
28    F: Fn(&Value, &Value) -> bool,
29{
30    let needles = normalize_list_ref(needles);
31    match value {
32        Value::List(items) => Some(
33            needles
34                .iter()
35                .any(|needle| items.iter().any(|item| eq(item, needle))),
36        ),
37        scalar => Some(needles.iter().any(|needle| eq(scalar, needle))),
38    }
39}
40
41#[expect(clippy::unnecessary_wraps)]
42fn contains_all_by<F>(value: &Value, needles: &Value, eq: F) -> Option<bool>
43where
44    F: Fn(&Value, &Value) -> bool,
45{
46    let needles = normalize_list_ref(needles);
47    match value {
48        Value::List(items) => Some(
49            needles
50                .iter()
51                .all(|needle| items.iter().any(|item| eq(item, needle))),
52        ),
53        scalar => Some(needles.len() == 1 && eq(scalar, needles[0])),
54    }
55}
56
57fn in_list_by<F>(value: &Value, haystack: &Value, eq: F) -> Option<bool>
58where
59    F: Fn(&Value, &Value) -> bool,
60{
61    if let Value::List(items) = haystack {
62        Some(items.iter().any(|item| eq(item, value)))
63    } else {
64        None
65    }
66}
67
68/// Return whether a value is empty when emptiness is defined for its variant.
69#[must_use]
70pub const fn is_empty(value: &Value) -> Option<bool> {
71    match value {
72        Value::List(values) => Some(values.is_empty()),
73        Value::Map(entries) => Some(entries.is_empty()),
74        Value::Text(text) => Some(text.is_empty()),
75        Value::Blob(blob) => Some(blob.is_empty()),
76
77        // Fields represented as Value::Null behave as empty values.
78        Value::Null => Some(true),
79
80        _ => None,
81    }
82}
83
84/// Logical negation of [`is_empty`].
85#[must_use]
86pub fn is_not_empty(value: &Value) -> Option<bool> {
87    is_empty(value).map(|empty| !empty)
88}
89
90/// Returns true if `value` contains `needle`.
91#[must_use]
92pub fn contains(value: &Value, needle: &Value) -> Option<bool> {
93    contains_by(value, needle, |left, right| left == right)
94}
95
96/// Returns true if any item in `needles` matches a member of `value`.
97#[must_use]
98pub fn contains_any(value: &Value, needles: &Value) -> Option<bool> {
99    contains_any_by(value, needles, |left, right| left == right)
100}
101
102/// Returns true if every item in `needles` matches a member of `value`.
103#[must_use]
104pub fn contains_all(value: &Value, needles: &Value) -> Option<bool> {
105    contains_all_by(value, needles, |left, right| left == right)
106}
107
108/// Returns true if `value` exists inside the provided list.
109#[must_use]
110pub fn in_list(value: &Value, haystack: &Value) -> Option<bool> {
111    in_list_by(value, haystack, |left, right| left == right)
112}
113
114/// Case-insensitive `contains` supporting text and identifier variants.
115#[must_use]
116pub fn contains_ci(value: &Value, needle: &Value) -> Option<bool> {
117    match value {
118        Value::List(_) => contains_by(value, needle, text::eq_ci),
119        _ => Some(text::eq_ci(value, needle)),
120    }
121}
122
123/// Case-insensitive variant of [`contains_any`].
124#[must_use]
125pub fn contains_any_ci(value: &Value, needles: &Value) -> Option<bool> {
126    contains_any_by(value, needles, text::eq_ci)
127}
128
129/// Case-insensitive variant of [`contains_all`].
130#[must_use]
131pub fn contains_all_ci(value: &Value, needles: &Value) -> Option<bool> {
132    contains_all_by(value, needles, text::eq_ci)
133}
134
135/// Case-insensitive variant of [`in_list`].
136#[must_use]
137pub fn in_list_ci(value: &Value, haystack: &Value) -> Option<bool> {
138    in_list_by(value, haystack, text::eq_ci)
139}
140
141impl Value {
142    #[must_use]
143    pub const fn is_empty(&self) -> Option<bool> {
144        is_empty(self)
145    }
146
147    /// Logical negation of [`is_empty`](Self::is_empty).
148    #[must_use]
149    pub fn is_not_empty(&self) -> Option<bool> {
150        is_not_empty(self)
151    }
152
153    /// Returns true if `self` contains `needle` (or equals it for scalars).
154    #[must_use]
155    pub fn contains(&self, needle: &Self) -> Option<bool> {
156        contains(self, needle)
157    }
158
159    /// Returns true if any item in `needles` matches a member of `self`.
160    #[must_use]
161    pub fn contains_any(&self, needles: &Self) -> Option<bool> {
162        contains_any(self, needles)
163    }
164
165    /// Returns true if every item in `needles` matches a member of `self`.
166    #[must_use]
167    pub fn contains_all(&self, needles: &Self) -> Option<bool> {
168        contains_all(self, needles)
169    }
170
171    /// Returns true if `self` exists inside the provided list.
172    #[must_use]
173    pub fn in_list(&self, haystack: &Self) -> Option<bool> {
174        in_list(self, haystack)
175    }
176
177    /// Case-insensitive `contains` supporting text and identifier variants.
178    #[must_use]
179    pub fn contains_ci(&self, needle: &Self) -> Option<bool> {
180        contains_ci(self, needle)
181    }
182
183    /// Case-insensitive variant of [`contains_any`](Self::contains_any).
184    #[must_use]
185    pub fn contains_any_ci(&self, needles: &Self) -> Option<bool> {
186        contains_any_ci(self, needles)
187    }
188
189    /// Case-insensitive variant of [`contains_all`](Self::contains_all).
190    #[must_use]
191    pub fn contains_all_ci(&self, needles: &Self) -> Option<bool> {
192        contains_all_ci(self, needles)
193    }
194
195    /// Case-insensitive variant of [`in_list`](Self::in_list).
196    #[must_use]
197    pub fn in_list_ci(&self, haystack: &Self) -> Option<bool> {
198        in_list_ci(self, haystack)
199    }
200}