wme-models 0.1.3

Type definitions for the Wikimedia Enterprise API
Documentation
//! Request parameter types for API queries.
//!
//! This module provides types for building API requests with filters,
//! field selection, and limits. These parameters allow you to customize
//! API responses to only include the data you need.
//!
//! # Building Requests
//!
//! Use [`RequestParams`] with its builder methods to construct requests:
//!
//! ```
//! use wme_models::RequestParams;
//!
//! let params = RequestParams::new()
//!     .field("name")
//!     .field("url")
//!     .filter("in_language.identifier", "en")
//!     .filter("is_part_of.identifier", "enwiki")
//!     .limit(5);
//! ```
//!
//! # Filters
//!
//! Filters narrow results to specific subsets. Field names use dot notation
//! (e.g., `is_part_of.identifier`). Only single-value fields can be filtered.
//!
//! # Field Selection
//!
//! The `fields` parameter specifies which fields to include. When fields are
//! specified, only those fields are returned (sparse fieldset). Omitting the
//! fields parameter returns all available fields.
//!
//! # Limits
//!
//! Limits restrict the number of results. Default is 3, maximum is 10.
//! Limits only work with On-demand endpoints.

use serde::{Deserialize, Serialize};

/// Filter for API requests to narrow down results.
///
/// Filters specify a field and value to match. Multiple filters can be
/// combined to narrow results further. Filters use AND logic - all
/// filters must match.
///
/// # Field Names
///
/// Use dot notation for nested fields:
/// - `is_part_of.identifier`
/// - `in_language.identifier`
/// - `namespace.identifier`
/// - `version.is_minor_edit`
///
/// # Example
///
/// ```
/// use wme_models::{Filter, FilterValue};
///
/// let filter = Filter {
///     field: "in_language.identifier".to_string(),
///     value: FilterValue::String("en".to_string()),
/// };
/// ```
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
pub struct Filter {
    /// Field name to filter on (using dot notation, e.g., "is_part_of.identifier")
    pub field: String,
    /// Value to match for the specified field
    pub value: FilterValue,
}

/// Value types that can be used in filters.
///
/// Supports strings, integers, floats, and booleans. Arrays and objects
/// cannot be used in filters.
///
/// # Type Coercion
///
/// The `From` trait implementations allow easy conversion from standard types:
///
/// ```
/// use wme_models::FilterValue;
///
/// let string_val: FilterValue = "en".into();
/// let int_val: FilterValue = 42i64.into();
/// let bool_val: FilterValue = true.into();
/// ```
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
#[serde(untagged)]
pub enum FilterValue {
    /// String value
    String(String),
    /// Integer value
    Integer(i64),
    /// Float value
    Float(f64),
    /// Boolean value
    Boolean(bool),
}

/// Request parameters for API endpoints that support filtering, field selection, and limits.
///
/// This struct provides a builder pattern for constructing API requests.
/// All fields are optional and will only be serialized if set.
///
/// # Builder Pattern
///
/// ```
/// use wme_models::RequestParams;
///
/// let params = RequestParams::new()
///     .field("name")
///     .field("url")
///     .filter("in_language.identifier", "en")
///     .limit(5);
/// ```
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
pub struct RequestParams {
    /// Fields to include in the response (dot notation)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub fields: Option<Vec<String>>,
    /// Filters to apply to narrow down results
    #[serde(skip_serializing_if = "Option::is_none")]
    pub filters: Option<Vec<Filter>>,
    /// Maximum number of results to return (default: 3, max: 10, On-demand API only)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub limit: Option<u32>,
}

impl RequestParams {
    /// Create a new empty request parameters builder.
    ///
    /// # Example
    ///
    /// ```
    /// use wme_models::RequestParams;
    ///
    /// let params = RequestParams::new();
    /// ```
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a field to include in the response.
    ///
    /// # Example
    ///
    /// ```
    /// use wme_models::RequestParams;
    ///
    /// let params = RequestParams::new()
    ///     .field("name")
    ///     .field("url");
    /// ```
    pub fn field(mut self, field: impl Into<String>) -> Self {
        self.fields.get_or_insert_with(Vec::new).push(field.into());
        self
    }

    /// Add multiple fields to include in the response.
    ///
    /// # Example
    ///
    /// ```
    /// use wme_models::RequestParams;
    ///
    /// let params = RequestParams::new()
    ///     .fields(vec!["name", "url", "identifier"]);
    /// ```
    pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
        self.fields
            .get_or_insert_with(Vec::new)
            .extend(fields.into_iter().map(Into::into));
        self
    }

    /// Add a filter to narrow down results.
    ///
    /// # Example
    ///
    /// ```
    /// use wme_models::RequestParams;
    ///
    /// let params = RequestParams::new()
    ///     .filter("in_language.identifier", "en")
    ///     .filter("is_part_of.identifier", "enwiki");
    /// ```
    pub fn filter(mut self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
        self.filters.get_or_insert_with(Vec::new).push(Filter {
            field: field.into(),
            value: value.into(),
        });
        self
    }

    /// Set the limit for number of results (On-demand API only, max 10).
    ///
    /// Values above 10 will be clamped to 10.
    ///
    /// # Example
    ///
    /// ```
    /// use wme_models::RequestParams;
    ///
    /// let params = RequestParams::new().limit(5);
    /// ```
    pub fn limit(mut self, limit: u32) -> Self {
        self.limit = Some(limit.min(10));
        self
    }
}

impl From<String> for FilterValue {
    fn from(s: String) -> Self {
        FilterValue::String(s)
    }
}

impl From<&str> for FilterValue {
    fn from(s: &str) -> Self {
        FilterValue::String(s.to_string())
    }
}

impl From<i64> for FilterValue {
    fn from(i: i64) -> Self {
        FilterValue::Integer(i)
    }
}

impl From<i32> for FilterValue {
    fn from(i: i32) -> Self {
        FilterValue::Integer(i as i64)
    }
}

impl From<u64> for FilterValue {
    fn from(u: u64) -> Self {
        FilterValue::Integer(u as i64)
    }
}

impl From<u32> for FilterValue {
    fn from(u: u32) -> Self {
        FilterValue::Integer(u as i64)
    }
}

impl From<f64> for FilterValue {
    fn from(f: f64) -> Self {
        FilterValue::Float(f)
    }
}

impl From<bool> for FilterValue {
    fn from(b: bool) -> Self {
        FilterValue::Boolean(b)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_request_params_builder() {
        let params = RequestParams::new()
            .field("name")
            .field("url")
            .filter("in_language.identifier", "en")
            .filter("is_part_of.identifier", "enwiki")
            .limit(5);

        assert_eq!(params.fields.as_ref().unwrap().len(), 2);
        assert_eq!(params.filters.as_ref().unwrap().len(), 2);
        assert_eq!(params.limit, Some(5));
    }

    #[test]
    fn test_filter_value_conversions() {
        let string_val: FilterValue = "test".into();
        let int_val: FilterValue = 42i64.into();
        let bool_val: FilterValue = true.into();

        assert!(matches!(string_val, FilterValue::String(_)));
        assert!(matches!(int_val, FilterValue::Integer(42)));
        assert!(matches!(bool_val, FilterValue::Boolean(true)));
    }

    #[test]
    fn test_filter_serialization() {
        let filter = Filter {
            field: "in_language.identifier".to_string(),
            value: FilterValue::String("en".to_string()),
        };

        let json = serde_json::to_string(&filter).unwrap();
        assert!(json.contains("in_language.identifier"));
        assert!(json.contains("en"));
    }

    #[test]
    fn test_request_params_serialization() {
        let params = RequestParams::new()
            .field("name")
            .field("url")
            .filter("in_language.identifier", "en")
            .limit(5);

        let json = serde_json::to_string(&params).unwrap();
        assert!(json.contains("fields"));
        assert!(json.contains("name"));
        assert!(json.contains("url"));
        assert!(json.contains("filters"));
        assert!(json.contains("limit"));
    }

    #[test]
    fn test_limit_clamping() {
        let params = RequestParams::new().limit(15);
        assert_eq!(params.limit, Some(10)); // Clamped to max
    }
}