modkit_odata/
schema.rs

1//! `OData` schema types for type-safe query building.
2//!
3//! This module defines the core schema abstraction for `OData` queries:
4//! - `Schema` trait: Maps field enums to string names
5//! - `FieldRef`: Type-safe field references with compile-time type checking
6//! - Helper traits for field operations and value conversion
7//!
8//! These types are protocol-level abstractions independent of SDK implementation.
9
10use crate::ast::{CompareOperator, Expr, Value};
11use std::marker::PhantomData;
12
13/// Schema trait defining field enums and their string mappings.
14///
15/// Implement this trait for your entity schemas to enable type-safe query building.
16///
17/// # Example
18///
19/// ```rust,ignore
20/// #[derive(Copy, Clone, Eq, PartialEq)]
21/// enum UserField {
22///     Id,
23///     Name,
24/// }
25///
26/// struct UserSchema;
27///
28/// impl Schema for UserSchema {
29///     type Field = UserField;
30///
31///     fn field_name(field: Self::Field) -> &'static str {
32///         match field {
33///             UserField::Id => "id",
34///             UserField::Name => "name",
35///         }
36///     }
37/// }
38/// ```
39pub trait Schema {
40    /// The field enum type (must be Copy + Eq)
41    type Field: Copy + Eq;
42
43    /// Map a field enum to its string name
44    fn field_name(field: Self::Field) -> &'static str;
45}
46
47/// Type-safe field reference holding schema and Rust type information.
48///
49/// This struct binds a field to both its schema and expected Rust type,
50/// enabling compile-time type checking for filter operations.
51///
52/// **NOTE:** `FieldRef` equality and hashing are based solely on the underlying
53/// schema field. The generic type parameter `T` is a phantom type used only for
54/// compile-time validation of operations and is not part of the field identity.
55///
56/// # Type Parameters
57///
58/// * `S` - The schema type implementing `Schema`
59/// * `T` - The Rust type this field represents (e.g., `String`, `uuid::Uuid`)
60pub struct FieldRef<S: Schema, T> {
61    field: S::Field,
62    _phantom: PhantomData<(S, T)>,
63}
64
65impl<S: Schema, T> FieldRef<S, T> {
66    /// Create a new typed field reference.
67    ///
68    /// # Example
69    ///
70    /// ```rust,ignore
71    /// const NAME: FieldRef<UserSchema, String> = FieldRef::new(UserField::Name);
72    /// ```
73    #[must_use]
74    pub const fn new(field: S::Field) -> Self {
75        Self {
76            field,
77            _phantom: PhantomData,
78        }
79    }
80
81    /// Get the field name as a string.
82    #[must_use]
83    pub fn name(&self) -> &'static str {
84        S::field_name(self.field)
85    }
86
87    /// Create an identifier expression for this field.
88    #[must_use]
89    fn identifier(&self) -> Expr {
90        Expr::Identifier(self.name().to_owned())
91    }
92}
93
94impl<S: Schema, T> Clone for FieldRef<S, T> {
95    fn clone(&self) -> Self {
96        *self
97    }
98}
99
100impl<S: Schema, T> Copy for FieldRef<S, T> {}
101
102impl<S: Schema, T> std::fmt::Debug for FieldRef<S, T> {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("FieldRef")
105            .field("field", &self.name())
106            .finish()
107    }
108}
109
110impl<S: Schema, T> PartialEq for FieldRef<S, T> {
111    fn eq(&self, other: &Self) -> bool {
112        self.field == other.field
113    }
114}
115
116impl<S: Schema, T> Eq for FieldRef<S, T> {}
117
118impl<S: Schema, T> std::hash::Hash for FieldRef<S, T>
119where
120    S::Field: std::hash::Hash,
121{
122    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
123        self.field.hash(state);
124    }
125}
126
127/// Trait for extracting field names from field references.
128///
129/// This allows the `select` method to accept heterogeneous field arrays
130/// with different type parameters.
131#[doc(hidden)]
132pub trait AsFieldName {
133    /// Get the field name as a string.
134    fn as_field_name(&self) -> &'static str;
135}
136
137/// Trait for extracting schema field keys from field references.
138///
139/// `QueryBuilder::select()` stores schema keys (`S::Field`) instead of field names
140/// so we can avoid allocating `String`s during the builder phase and only allocate
141/// during `build()`.
142#[doc(hidden)]
143pub trait AsFieldKey<S: Schema> {
144    /// Get the schema field key.
145    fn as_field_key(&self) -> S::Field;
146}
147
148impl<S: Schema, T> AsFieldName for FieldRef<S, T> {
149    fn as_field_name(&self) -> &'static str {
150        self.name()
151    }
152}
153
154impl<S: Schema, T> AsFieldKey<S> for FieldRef<S, T> {
155    fn as_field_key(&self) -> S::Field {
156        self.field
157    }
158}
159
160impl<T: AsFieldName + ?Sized> AsFieldName for &T {
161    fn as_field_name(&self) -> &'static str {
162        (*self).as_field_name()
163    }
164}
165
166impl<S: Schema, T: AsFieldKey<S> + ?Sized> AsFieldKey<S> for &T {
167    fn as_field_key(&self) -> S::Field {
168        (*self).as_field_key()
169    }
170}
171
172/// Trait for types that can be converted to `OData` AST values.
173pub trait IntoODataValue {
174    /// Convert this value into an `OData` AST value.
175    fn into_odata_value(self) -> Value;
176}
177
178impl IntoODataValue for bool {
179    fn into_odata_value(self) -> Value {
180        Value::Bool(self)
181    }
182}
183
184impl IntoODataValue for uuid::Uuid {
185    fn into_odata_value(self) -> Value {
186        Value::Uuid(self)
187    }
188}
189
190impl IntoODataValue for String {
191    fn into_odata_value(self) -> Value {
192        Value::String(self)
193    }
194}
195
196impl IntoODataValue for &str {
197    fn into_odata_value(self) -> Value {
198        Value::String(self.to_owned())
199    }
200}
201
202impl IntoODataValue for i32 {
203    fn into_odata_value(self) -> Value {
204        Value::Number(self.into())
205    }
206}
207
208impl IntoODataValue for i64 {
209    fn into_odata_value(self) -> Value {
210        Value::Number(self.into())
211    }
212}
213
214impl IntoODataValue for u32 {
215    fn into_odata_value(self) -> Value {
216        Value::Number(self.into())
217    }
218}
219
220impl IntoODataValue for u64 {
221    fn into_odata_value(self) -> Value {
222        Value::Number(self.into())
223    }
224}
225
226impl IntoODataValue for chrono::DateTime<chrono::Utc> {
227    fn into_odata_value(self) -> Value {
228        Value::DateTime(self)
229    }
230}
231
232impl IntoODataValue for chrono::NaiveDate {
233    fn into_odata_value(self) -> Value {
234        Value::Date(self)
235    }
236}
237
238impl IntoODataValue for chrono::NaiveTime {
239    fn into_odata_value(self) -> Value {
240        Value::Time(self)
241    }
242}
243
244/// Comparison operations for any field type.
245impl<S: Schema, T> FieldRef<S, T> {
246    /// Create an equality comparison: `field eq value`
247    ///
248    /// # Example
249    ///
250    /// ```rust,ignore
251    /// let filter = ID.eq(user_id);
252    /// ```
253    #[must_use]
254    pub fn eq<V: IntoODataValue>(self, value: V) -> Expr {
255        Expr::Compare(
256            Box::new(self.identifier()),
257            CompareOperator::Eq,
258            Box::new(Expr::Value(value.into_odata_value())),
259        )
260    }
261
262    /// Create a not-equal comparison: `field ne value`
263    #[must_use]
264    pub fn ne<V: IntoODataValue>(self, value: V) -> Expr {
265        Expr::Compare(
266            Box::new(self.identifier()),
267            CompareOperator::Ne,
268            Box::new(Expr::Value(value.into_odata_value())),
269        )
270    }
271
272    /// Create a greater-than comparison: `field gt value`
273    #[must_use]
274    pub fn gt<V: IntoODataValue>(self, value: V) -> Expr {
275        Expr::Compare(
276            Box::new(self.identifier()),
277            CompareOperator::Gt,
278            Box::new(Expr::Value(value.into_odata_value())),
279        )
280    }
281
282    /// Create a greater-than-or-equal comparison: `field ge value`
283    #[must_use]
284    pub fn ge<V: IntoODataValue>(self, value: V) -> Expr {
285        Expr::Compare(
286            Box::new(self.identifier()),
287            CompareOperator::Ge,
288            Box::new(Expr::Value(value.into_odata_value())),
289        )
290    }
291
292    /// Create a less-than comparison: `field lt value`
293    #[must_use]
294    pub fn lt<V: IntoODataValue>(self, value: V) -> Expr {
295        Expr::Compare(
296            Box::new(self.identifier()),
297            CompareOperator::Lt,
298            Box::new(Expr::Value(value.into_odata_value())),
299        )
300    }
301
302    /// Create a less-than-or-equal comparison: `field le value`
303    #[must_use]
304    pub fn le<V: IntoODataValue>(self, value: V) -> Expr {
305        Expr::Compare(
306            Box::new(self.identifier()),
307            CompareOperator::Le,
308            Box::new(Expr::Value(value.into_odata_value())),
309        )
310    }
311
312    /// Create a null check: `field eq null`
313    ///
314    /// # Example
315    ///
316    /// ```rust,ignore
317    /// let filter = OPTIONAL_FIELD.is_null();
318    /// ```
319    #[must_use]
320    pub fn is_null(self) -> Expr {
321        Expr::Compare(
322            Box::new(self.identifier()),
323            CompareOperator::Eq,
324            Box::new(Expr::Value(Value::Null)),
325        )
326    }
327
328    /// Create a not-null check: `field ne null`
329    ///
330    /// # Example
331    ///
332    /// ```rust,ignore
333    /// let filter = OPTIONAL_FIELD.is_not_null();
334    /// ```
335    #[must_use]
336    pub fn is_not_null(self) -> Expr {
337        Expr::Compare(
338            Box::new(self.identifier()),
339            CompareOperator::Ne,
340            Box::new(Expr::Value(Value::Null)),
341        )
342    }
343}
344
345/// String-specific operations (only available for String fields).
346impl<S: Schema> FieldRef<S, String> {
347    /// Create a contains function call: `contains(field, 'value')`
348    ///
349    /// # Example
350    ///
351    /// ```rust,ignore
352    /// let filter = NAME.contains("john");
353    /// ```
354    #[must_use]
355    pub fn contains(self, substring: &str) -> Expr {
356        Expr::Function(
357            "contains".to_owned(),
358            vec![
359                self.identifier(),
360                Expr::Value(Value::String(substring.to_owned())),
361            ],
362        )
363    }
364
365    /// Create a startswith function call: `startswith(field, 'prefix')`
366    ///
367    /// # Example
368    ///
369    /// ```rust,ignore
370    /// let filter = NAME.startswith("Dr");
371    /// ```
372    #[must_use]
373    pub fn startswith(self, prefix: &str) -> Expr {
374        Expr::Function(
375            "startswith".to_owned(),
376            vec![
377                self.identifier(),
378                Expr::Value(Value::String(prefix.to_owned())),
379            ],
380        )
381    }
382
383    /// Create an endswith function call: `endswith(field, 'suffix')`
384    ///
385    /// # Example
386    ///
387    /// ```rust,ignore
388    /// let filter = EMAIL.endswith("@example.com");
389    /// ```
390    #[must_use]
391    pub fn endswith(self, suffix: &str) -> Expr {
392        Expr::Function(
393            "endswith".to_owned(),
394            vec![
395                self.identifier(),
396                Expr::Value(Value::String(suffix.to_owned())),
397            ],
398        )
399    }
400}