fieldmasker 0.0.1

A utility for selecting and filtering response fields via field masks.
Documentation
use crate::spec::MaskSpec;
use crate::trie::MaskTrie;
use crate::validate::validate_one;
use crate::FieldMaskError;
use std::collections::BTreeSet;

/// A parsed, validated field mask.
///
/// A `FieldMask` represents a set of paths (e.g., `"a,b.c,d.*.e"`) that can be applied
/// during serialization to include only the selected fields.
///
/// Use [`FieldMask::parse`] to build from a string, or [`FieldMask::all`] to include everything.
///
/// Field masks are **schema-aware** at use sites: path validation is performed against
/// a type’s [`MaskSpec`] when you call methods like [`FieldMask::contains_exact`] or [`FieldMask::intersects`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FieldMask {
    /// `true` if the mask is a wildcard (`"*"`) and therefore includes all fields.
    all: bool,

    /// The original unmodified source string.
    raw: String,

    /// Normalized, deduplicated set of path segments.
    pub(crate) paths: BTreeSet<Vec<String>>,
}

impl FieldMask {
    /// Parses a field mask from a string.
    ///
    /// * Comma separates top-level paths: `"a,b.c"`.
    /// * Dot navigates into nested fields: `"a.b.c"`.
    /// * `*` matches any element in arrays or map values: `"items.*.id"`.
    ///
    /// Returns [`FieldMaskError::InvalidSyntax`] on malformed input.
    ///
    /// # Examples
    /// ```
    /// # use fieldmasker::{FieldMask, FieldMaskError};
    /// let m = FieldMask::parse("a, b.c, items.*.id")?;
    /// assert!(!m.is_all());
    /// # Ok::<_, FieldMaskError>(())
    /// ```
    pub fn parse(raw: &str) -> Result<Self, FieldMaskError> {
        let s = raw.trim();
        if s.is_empty() {
            return Ok(Self {
                all: false,
                raw: raw.to_string(),
                paths: BTreeSet::new(),
            });
        }
        if s == "*" {
            return Ok(Self {
                all: true,
                raw: s.to_string(),
                paths: BTreeSet::new(),
            });
        }
        let mut set = BTreeSet::new();
        for token in s.split(',') {
            let token = token.trim();
            if token.is_empty() {
                continue;
            }
            let segs: Vec<String> = token
                .split('.')
                .map(|t| t.trim())
                .filter(|t| !t.is_empty())
                .map(|t| t.to_string())
                .collect();
            if segs.is_empty() {
                return Err(FieldMaskError::InvalidSyntax);
            }
            set.insert(segs);
        }

        Ok(Self {
            all: false,
            raw: raw.to_string(),
            paths: set,
        })
    }

    /// Creates a wildcard mask that includes all fields (`"*"`)
    ///
    /// # Examples
    /// ```
    /// # use fieldmasker::FieldMask;
    /// let m = FieldMask::all();
    /// assert!(m.is_all());
    /// ```
    pub fn all() -> Self {
        Self {
            all: true,
            raw: "*".into(),
            paths: BTreeSet::new(),
        }
    }

    /// Returns `true` if the mask includes all fields.
    pub fn is_all(&self) -> bool {
        self.all
    }

    /// Returns `true` if the mask is neither `"*"` nor contains any paths.
    ///
    /// An empty mask serializes the input as-is (no filtering).
    pub fn is_empty(&self) -> bool {
        !self.all && self.paths.is_empty()
    }

    /// Returns the original source string used to build this mask.
    pub fn raw(&self) -> &str {
        &self.raw
    }

    /// Returns `true` if the mask selects the given `path`, considering schema `T`.
    ///
    /// This checks the exact path or any of its prefixes that terminate a mask path.
    /// Validation is performed against `T::mask_spec()`.
    ///
    /// # Errors
    /// Returns [`FieldMaskError::UnknownPath`] if `path` is not valid with respect to `T`.
    pub fn contains_exact<T: MaskSpec>(&self, path: &str) -> Result<bool, FieldMaskError> {
        if self.is_all() {
            return Ok(true);
        }
        let segs = parse_path(path)?;
        validate_one(&segs, T::mask_spec(), path.to_string())?;
        let trie = MaskTrie::new(self);
        Ok(trie.contains_path(&segs))
    }

    /// Returns `true` if the mask intersects (overlaps) with `path`, considering schema `T`.
    ///
    /// Intersection is `true` when:
    /// - the mask contains `path` (or a prefix of it), or
    /// - the mask contains any longer path that starts with `path`.
    ///
    /// # Errors
    /// Returns [`FieldMaskError::UnknownPath`] if `path` is not valid with respect to `T`.
    pub fn intersects<T: MaskSpec>(&self, path: &str) -> Result<bool, FieldMaskError> {
        if self.is_all() {
            return Ok(true);
        }
        let segs = parse_path(path)?;
        validate_one(&segs, T::mask_spec(), path.to_string())?;
        let trie = MaskTrie::new(self);
        if trie.contains_path(&segs) {
            return Ok(true);
        }
        for p in &self.paths {
            if p.starts_with(&segs) {
                return Ok(true);
            }
        }
        Ok(false)
    }
}

fn parse_path(path: &str) -> Result<Vec<String>, FieldMaskError> {
    let v: Vec<String> = path
        .split('.')
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .map(|s| s.to_string())
        .collect();
    if v.is_empty() {
        Err(FieldMaskError::InvalidSyntax)
    } else {
        Ok(v)
    }
}