Skip to main content
Glama
query.rs17.8 kB
use std::str::FromStr; use ulid::Ulid; use crate::search::{ Error, Result, parser, }; /// A search query #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchQuery { MatchValue(SearchTerm), MatchAttr { name: String, terms: Vec<SearchTerm>, }, And(Vec<SearchQuery>), /// One of the sub-queries must match Or(Vec<SearchQuery>), /// The sub-query must not match Not(Box<SearchQuery>), /// Used for the empty query. Matches everything. All, } /// A value to match #[derive(Debug, Clone, PartialEq, Eq)] pub enum SearchTerm { /// Match the string, allowing * and such: Instance, AWS::*::*Group Match(String), /// Exact literal match: "AWS::EC2::Instance" Exact(String), /// Match starting with the given string: "AWS::EC2::Inst StartsWith(String), } impl FromStr for SearchQuery { type Err = Error; fn from_str(s: &str) -> Result<Self> { parser::parse(s) } } impl SearchTerm { /// Get the inner string, regardless of the type of match pub fn as_str(&self) -> &str { match self { SearchTerm::Exact(s) | SearchTerm::StartsWith(s) | SearchTerm::Match(s) => s, } } /// Match a query term against a JSON value (with case-insensitive string matching) pub fn match_value(&self, value: &serde_json::Value) -> bool { match value { serde_json::Value::String(value) => self.match_str(value), serde_json::Value::Bool(value) => self.match_bool(*value), serde_json::Value::Number(value) => self.match_number(value), serde_json::Value::Null => self.match_null(), serde_json::Value::Object(_) | serde_json::Value::Array(_) => false, } } /// Match a query term like Instance or "Instance" against a string, case insensitively pub fn match_str(&self, value: &str) -> bool { // TODO handle international case comparison as well. Library? icu? case_sensitive_string? match self { SearchTerm::Match(term) => { // If we have a*b*c, find a, then b, then c ... let mut value = value; for part in term.split('*') { let Some((_, remaining)) = value.split_once_ignore_ascii_case(part) else { return false; }; value = remaining; } true } SearchTerm::Exact(term) => value.eq_ignore_ascii_case(term), SearchTerm::StartsWith(term) => value.starts_with_ignore_ascii_case(term), } } /// Match a query term like "1" or "3.14" against a number (e.g. "1.0" matches 1) pub fn match_number(&self, value: &serde_json::Number) -> bool { // We don't support partial matches for numbers, so we treat quotes and non-quotes the same. let term = self.as_str(); // Try to parse the term as a number // TODO perf: don't re-parse the term on every attribute we check against. Float parsing // is actually really expensive. // TODO(jkeiser) serde_json doesn't support comparing term.parse().is_ok_and(|term| value == &term) } /// Match a query term like "true" or "false" against a bool pub fn match_bool(&self, value: bool) -> bool { // For bools, we only want to check true or false and don't care whether it's quoted or not. let term = self.as_str(); // TODO t, tr, tru, f, fa, fal should also match given the incremental typing rule if term.eq_ignore_ascii_case("true") { value } else if term.eq_ignore_ascii_case("false") { !value } else { false } } /// Match a query term like "null" against a bool pub fn match_null(&self) -> bool { // For null, we only want to check null and don't care whether it's quoted or not. let term = self.as_str(); // TODO n, nu, nul should also match given the incremental typing rule term.eq_ignore_ascii_case("null") } /// Match a query term like "01F8MECHZX3TBDSZ7XRADM79XE" against a ULID pub fn match_ulid(&self, value: impl Into<Ulid>) -> bool { let value = value.into(); match self { SearchTerm::Exact(term) | SearchTerm::Match(term) | SearchTerm::StartsWith(term) => { term.parse().is_ok_and(|u| value == u) } } } } /// String methods that ignore ascii case (like eq_ignore_ascii_case for other pattern matches) trait StrIgnoreAsciiCaseHelpers { /// Like find(), but ignores ascii case fn find_ignore_ascii_case(&self, pattern: &str) -> Option<usize>; /// Like split_once(), but ignores ascii case fn split_once_ignore_ascii_case<'a>(&'a self, pattern: &str) -> Option<(&'a str, &'a str)>; /// Like starts_with(), but ignores ascii case fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool; } impl StrIgnoreAsciiCaseHelpers for str { fn find_ignore_ascii_case(&self, pattern: &str) -> Option<usize> { let pattern = pattern.as_bytes(); let bytes = self.as_bytes(); if pattern.len() > self.len() { return None; } (0..=(bytes.len() - pattern.len())) .find(|&i| bytes[i..i + pattern.len()].eq_ignore_ascii_case(pattern)) } fn split_once_ignore_ascii_case<'a>(&'a self, pattern: &str) -> Option<(&'a str, &'a str)> { self.find_ignore_ascii_case(pattern) .map(|i| (&self[..i], &self[i + pattern.len()..])) } fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool { self.get(..prefix.len()) .is_some_and(|slice| slice.eq_ignore_ascii_case(prefix)) } } #[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] use serde_json::Number; use super::*; #[test] fn match_str() { let term = SearchTerm::Match("Instance".to_string()); assert!(term.match_str("Instance")); // exact assert!(term.match_str("instance")); // case assert!(term.match_str("Instances")); // suffix assert!(term.match_str("AWS::EC2::Instance")); // prefix assert!(term.match_str("AWS::EC2::Instances")); // prefix+suffix assert!(!term.match_str("Inst")); // partial (start) assert!(!term.match_str("stan")); // partial (middle) assert!(!term.match_str("ance")); // partial (end) assert!(!term.match_str("")); // empty assert!(!term.match_str("foo")); // something else entirely assert!(!term.match_str("foobario")); // something else entirely of the same length let value = "Instance"; assert!(SearchTerm::Match("*Stan*".to_string()).match_str(value)); assert!(SearchTerm::Match("*St*an*".to_string()).match_str(value)); assert!(SearchTerm::Match("*S*n*".to_string()).match_str(value)); assert!(SearchTerm::Match("n*t*n*e".to_string()).match_str(value)); assert!(SearchTerm::Match("**".to_string()).match_str(value)); assert!(SearchTerm::Match("n*".to_string()).match_str(value)); assert!(SearchTerm::Match("**ta**c**e**".to_string()).match_str(value)); assert!(!SearchTerm::Match("n*t*x".to_string()).match_str(value)); assert!(!SearchTerm::Match("*an*St*".to_string()).match_str(value)); assert!(!SearchTerm::Match("*an*St*".to_string()).match_str(value)); assert!(!SearchTerm::Match("*a*cee*".to_string()).match_str(value)); } #[test] fn match_str_exact() { let term = SearchTerm::Exact("Instance".to_string()); assert!(term.match_str("Instance")); // exact assert!(term.match_str("instance")); // case assert!(!term.match_str("Instances")); // suffix assert!(!term.match_str("AWS::EC2::Instance")); // prefix assert!(!term.match_str("AWS::EC2::Instances")); // prefix+suffix assert!(!term.match_str("Inst")); // partial (start) assert!(!term.match_str("stan")); // partial (middle) assert!(!term.match_str("ance")); // partial (end) assert!(!term.match_str("")); // empty assert!(!term.match_str("foo")); // something else entirely assert!(!term.match_str("foobario")); // something else entirely of the same length } #[test] fn match_str_starts_with() { let term = SearchTerm::StartsWith("Instance".to_string()); assert!(term.match_str("Instance")); // exact assert!(term.match_str("instance")); // case assert!(term.match_str("Instances")); // suffix assert!(!term.match_str("AWS::EC2::Instance")); // prefix assert!(!term.match_str("AWS::EC2::Instances")); // prefix+suffix assert!(!term.match_str("Inst")); // partial (start) assert!(!term.match_str("stan")); // partial (middle) assert!(!term.match_str("ance")); // partial (end) assert!(!term.match_str("")); // empty assert!(!term.match_str("foo")); // something else entirely assert!(!term.match_str("foobario")); // something else entirely of the same length } #[test] fn match_null() { assert!(SearchTerm::Match("null".to_string()).match_null()); assert!(SearchTerm::Match("NULL".to_string()).match_null()); // TODO These partial ones should eventually match! assert!(!SearchTerm::Match("nul".to_string()).match_null()); assert!(!SearchTerm::Match("nu".to_string()).match_null()); assert!(!SearchTerm::Match("n".to_string()).match_null()); assert!(!SearchTerm::Match("nulll".to_string()).match_null()); assert!(!SearchTerm::Match("nnull".to_string()).match_null()); assert!(!SearchTerm::Match("true".to_string()).match_null()); assert!(!SearchTerm::Match("false".to_string()).match_null()); assert!(!SearchTerm::Match("0".to_string()).match_null()); assert!(!SearchTerm::Match("1".to_string()).match_null()); } #[test] fn match_bool() { assert!(SearchTerm::Match("true".to_string()).match_bool(true)); assert!(SearchTerm::Match("TRUE".to_string()).match_bool(true)); // TODO These partial ones should eventually match! assert!(!SearchTerm::Match("tru".to_string()).match_bool(true)); assert!(!SearchTerm::Match("tr".to_string()).match_bool(true)); assert!(!SearchTerm::Match("t".to_string()).match_bool(true)); assert!(!SearchTerm::Match("truee".to_string()).match_bool(true)); assert!(!SearchTerm::Match("ttrue".to_string()).match_bool(true)); assert!(!SearchTerm::Match("true".to_string()).match_bool(false)); assert!(SearchTerm::Match("false".to_string()).match_bool(false)); assert!(SearchTerm::Match("FALSE".to_string()).match_bool(false)); // TODO These partial ones should eventually match! assert!(!SearchTerm::Match("fals".to_string()).match_bool(false)); assert!(!SearchTerm::Match("fal".to_string()).match_bool(false)); assert!(!SearchTerm::Match("fa".to_string()).match_bool(false)); assert!(!SearchTerm::Match("f".to_string()).match_bool(false)); assert!(!SearchTerm::Match("falsee".to_string()).match_bool(false)); assert!(!SearchTerm::Match("ffalse".to_string()).match_bool(false)); assert!(!SearchTerm::Match("false".to_string()).match_bool(true)); assert!(!SearchTerm::Match("null".to_string()).match_bool(true)); assert!(!SearchTerm::Match("null".to_string()).match_bool(false)); assert!(!SearchTerm::Match("0".to_string()).match_bool(true)); assert!(!SearchTerm::Match("0".to_string()).match_bool(false)); assert!(!SearchTerm::Match("1".to_string()).match_bool(true)); assert!(!SearchTerm::Match("1".to_string()).match_bool(false)); } #[test] fn match_number() { assert!(SearchTerm::Match("0".to_string()).match_number(&0.into())); // assert!(SearchTerm::Match("-0".to_string()).match_number(&0.into())); assert!(SearchTerm::Match("1".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("1.0".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("1e0".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("10e-1".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("0.1e1".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("01".to_string()).match_number(&1.into())); // assert!(SearchTerm::Match("01.0e0".to_string()).match_number(&1.into())); assert!( !SearchTerm::Match("1".to_string()).match_number(&Number::from_f64(1.0001).unwrap()) ); assert!(!SearchTerm::Match("1".to_string()).match_number(&0.into())); assert!(!SearchTerm::Match("1".to_string()).match_number(&(-1).into())); assert!(!SearchTerm::Match("1".to_string()).match_number(&10.into())); // assert!(SearchTerm::Match("1".to_string()).match_number(&Number::from_f64(1.0).unwrap())); assert!(SearchTerm::Match("1.0".to_string()).match_number(&Number::from_f64(1.0).unwrap())); assert!(SearchTerm::Match("1e0".to_string()).match_number(&Number::from_f64(1.0).unwrap())); assert!( SearchTerm::Match("10e-1".to_string()).match_number(&Number::from_f64(1.0).unwrap()) ); assert!( SearchTerm::Match("0.1e1".to_string()).match_number(&Number::from_f64(1.0).unwrap()) ); // assert!(SearchTerm::Match("01".to_string()).match_number(&Number::from_f64(1.0).unwrap())); // assert!(SearchTerm::Match("01.0e0".to_string()).match_number(&1.into())); assert!( !SearchTerm::Match("1".to_string()).match_number(&Number::from_f64(1.0001).unwrap()) ); assert!(!SearchTerm::Match("1".to_string()).match_number(&0.into())); assert!(!SearchTerm::Match("1".to_string()).match_number(&(-1).into())); assert!(!SearchTerm::Match("1".to_string()).match_number(&10.into())); assert!(SearchTerm::Match("-1".to_string()).match_number(&(-1).into())); // assert!(SearchTerm::Match("-1.0".to_string()).match_number(&(-1).into())); assert!(!SearchTerm::Match("-1".to_string()).match_number(&0.into())); assert!(!SearchTerm::Match("-1".to_string()).match_number(&1.into())); assert!(SearchTerm::Match("123".to_string()).match_number(&123.into())); assert!(!SearchTerm::Match("123".to_string()).match_number(&1.into())); assert!(!SearchTerm::Match("123".to_string()).match_number(&124.into())); assert!(SearchTerm::Match("1.2".to_string()).match_number(&Number::from_f64(1.2).unwrap())); assert!( SearchTerm::Match("1.20".to_string()).match_number(&Number::from_f64(1.2).unwrap()) ); assert!( SearchTerm::Match("12e-1".to_string()).match_number(&Number::from_f64(1.2).unwrap()) ); assert!(!SearchTerm::Match("1.2".to_string()).match_number(&1.into())); assert!( !SearchTerm::Match("1.2".to_string()).match_number(&Number::from_f64(-1.2).unwrap()) ); assert!( !SearchTerm::Match("1.2".to_string()).match_number(&Number::from_f64(1.2001).unwrap()) ); assert!(!SearchTerm::Match("1.2".to_string()).match_number(&1.into())); } #[test] fn find_ignore_ascii_case() { assert_eq!("Hello, world".find_ignore_ascii_case("hello"), Some(0)); assert_eq!("Hello, world".find_ignore_ascii_case("WORLD"), Some(7)); assert_eq!("Hello, world".find_ignore_ascii_case("o, W"), Some(4)); assert_eq!("Hello, world".find_ignore_ascii_case("foo"), None); assert_eq!("Hello, world".find_ignore_ascii_case(""), Some(0)); assert_eq!( "Hello, world".find_ignore_ascii_case("Hello, world"), Some(0) ); assert_eq!("Hello, world".find_ignore_ascii_case("Hello, worldd"), None); assert_eq!("".find_ignore_ascii_case("foo"), None); assert_eq!("".find_ignore_ascii_case(""), Some(0)); } #[test] fn split_once_ignore_ascii_case() { assert_eq!( "abc DEF def".split_once_ignore_ascii_case("def"), Some(("abc ", " def")) ); assert_eq!( "abc DEF def".split_once_ignore_ascii_case("def"), Some(("abc ", " def")) ); assert_eq!( "abc DEF def".split_once_ignore_ascii_case("abc DEF def"), Some(("", "")) ); assert_eq!( "abc DEF def".split_once_ignore_ascii_case("abc DEF deff"), None ); assert_eq!("abc DEF def".split_once_ignore_ascii_case("ghi"), None); assert_eq!( "abc DEF def".split_once_ignore_ascii_case(""), Some(("", "abc DEF def")) ); assert_eq!( "abc DEF def".split_once_ignore_ascii_case("c d"), Some(("ab", "EF def")) ); assert_eq!("".split_once_ignore_ascii_case("foo"), None); assert_eq!("".split_once_ignore_ascii_case(""), Some(("", ""))); } #[test] fn starts_with_ignore_ascii_case() { assert!("Hello, world".starts_with_ignore_ascii_case("hello")); assert!("Hello, world".starts_with_ignore_ascii_case("HELLO")); assert!(!"Hello, world".starts_with_ignore_ascii_case("world")); assert!(!"Hello, world".starts_with_ignore_ascii_case("foo")); assert!("Hello, world".starts_with_ignore_ascii_case("")); assert!(!"".starts_with_ignore_ascii_case("foo")); assert!("".starts_with_ignore_ascii_case("")); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server