Files
storybook/src/query.rs
Sienna Meridian Satterwhite 8e4bdd3942 refactor(ast): rename value types to Number/Decimal/Text/Boolean
Renamed AST value types for v0.3 naming convention:
- Value::Int -> Value::Number
- Value::Float -> Value::Decimal
- Value::String -> Value::Text
- Value::Bool -> Value::Boolean
- Expr::IntLit -> Expr::NumberLit
- Expr::FloatLit -> Expr::DecimalLit
- Expr::StringLit -> Expr::TextLit
- Expr::BoolLit -> Expr::BooleanLit

Updated all references across parser, resolver, validator, LSP,
query engine, tests, and editor.
2026-02-14 14:03:21 +00:00

292 lines
8.1 KiB
Rust

//! Query interface for filtering and searching entities
//!
//! This module provides convenient methods for querying entities in a storybook
//! project. You can filter by various criteria like traits, age ranges, field
//! values, etc.
use crate::{
syntax::ast::Value,
types::*,
};
/// Extension methods for querying characters
pub trait CharacterQuery<'a> {
/// Filter characters by age range
fn with_age_range(
self,
min: i64,
max: i64,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a>;
/// Filter characters by trait value
fn with_trait(
self,
trait_name: &'a str,
min: f64,
max: f64,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a>;
/// Filter characters that have a specific field
fn with_field(
self,
field_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a>;
/// Filter characters by field value
fn with_field_value(
self,
field_name: &'a str,
value: Value,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a>;
}
impl<'a, I> CharacterQuery<'a> for I
where
I: Iterator<Item = &'a ResolvedCharacter> + 'a,
{
fn with_age_range(
self,
min: i64,
max: i64,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a> {
Box::new(self.filter(move |c| {
if let Some(Value::Number(age)) = c.fields.get("age") {
*age >= min && *age <= max
} else {
false
}
}))
}
fn with_trait(
self,
trait_name: &'a str,
min: f64,
max: f64,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a> {
Box::new(self.filter(move |c| {
if let Some(Value::Decimal(value)) = c.fields.get(trait_name) {
*value >= min && *value <= max
} else {
false
}
}))
}
fn with_field(
self,
field_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a> {
Box::new(self.filter(move |c| c.fields.contains_key(field_name)))
}
fn with_field_value(
self,
field_name: &'a str,
value: Value,
) -> Box<dyn Iterator<Item = &'a ResolvedCharacter> + 'a> {
Box::new(self.filter(move |c| c.fields.get(field_name) == Some(&value)))
}
}
/// Extension methods for querying relationships
pub trait RelationshipQuery<'a> {
/// Filter relationships by bond strength
fn with_bond_range(
self,
min: f64,
max: f64,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a>;
/// Filter relationships that include a specific participant
fn with_participant(
self,
participant_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a>;
/// Filter relationships that have a specific field
fn with_field(
self,
field_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a>;
}
impl<'a, I> RelationshipQuery<'a> for I
where
I: Iterator<Item = &'a ResolvedRelationship> + 'a,
{
fn with_bond_range(
self,
min: f64,
max: f64,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a> {
Box::new(self.filter(move |r| {
if let Some(Value::Decimal(bond)) = r.fields.get("bond") {
*bond >= min && *bond <= max
} else {
false
}
}))
}
fn with_participant(
self,
participant_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a> {
Box::new(self.filter(move |r| {
r.participants
.iter()
.any(|p| p.name.last().is_some_and(|name| name == participant_name))
}))
}
fn with_field(
self,
field_name: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedRelationship> + 'a> {
Box::new(self.filter(move |r| r.fields.contains_key(field_name)))
}
}
/// Extension methods for querying schedules
pub trait ScheduleQuery<'a> {
/// Filter schedules that have an activity
fn with_activity(
self,
activity: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedSchedule> + 'a>;
}
impl<'a, I> ScheduleQuery<'a> for I
where
I: Iterator<Item = &'a ResolvedSchedule> + 'a,
{
fn with_activity(
self,
activity: &'a str,
) -> Box<dyn Iterator<Item = &'a ResolvedSchedule> + 'a> {
Box::new(self.filter(move |s| s.blocks.iter().any(|block| block.activity == activity)))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::syntax::ast::Span;
fn make_character(name: &str, age: i64, trust: f64) -> ResolvedCharacter {
let mut fields = HashMap::new();
fields.insert("age".to_string(), Value::Number(age));
fields.insert("trust".to_string(), Value::Decimal(trust));
ResolvedCharacter {
name: name.to_string(),
species: None,
fields,
prose_blocks: HashMap::new(),
uses_behaviors: None,
uses_schedule: None,
span: Span::new(0, 10),
}
}
#[test]
fn test_filter_by_age_range() {
let characters = [
make_character("Alice", 25, 0.8),
make_character("Bob", 35, 0.6),
make_character("Charlie", 45, 0.9),
];
let filtered: Vec<_> = characters.iter().with_age_range(30, 50).collect();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "Bob");
assert_eq!(filtered[1].name, "Charlie");
}
#[test]
fn test_filter_by_trait() {
let characters = [
make_character("Alice", 25, 0.8),
make_character("Bob", 35, 0.6),
make_character("Charlie", 45, 0.9),
];
let filtered: Vec<_> = characters.iter().with_trait("trust", 0.75, 1.0).collect();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "Alice");
assert_eq!(filtered[1].name, "Charlie");
}
#[test]
fn test_chain_filters() {
let characters = [
make_character("Alice", 25, 0.8),
make_character("Bob", 35, 0.6),
make_character("Charlie", 45, 0.9),
make_character("David", 40, 0.85),
];
// Find characters aged 30-50 with trust > 0.8
let filtered: Vec<_> = characters
.iter()
.with_age_range(30, 50)
.with_trait("trust", 0.8, 1.0)
.collect();
assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0].name, "Charlie");
assert_eq!(filtered[1].name, "David");
}
#[test]
fn test_filter_with_field() {
let mut char1 = make_character("Alice", 25, 0.8);
char1
.fields
.insert("job".to_string(), Value::Text("baker".to_string()));
let char2 = make_character("Bob", 35, 0.6);
let characters = [char1, char2];
let filtered: Vec<_> = characters.iter().with_field("job").collect();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "Alice");
}
#[test]
fn test_relationship_with_bond_range() {
let mut fields1 = HashMap::new();
fields1.insert("bond".to_string(), Value::Decimal(0.9));
let mut fields2 = HashMap::new();
fields2.insert("bond".to_string(), Value::Decimal(0.5));
let relationships = [
ResolvedRelationship {
name: "Strong".to_string(),
participants: vec![],
fields: fields1,
span: Span::new(0, 10),
},
ResolvedRelationship {
name: "Weak".to_string(),
participants: vec![],
fields: fields2,
span: Span::new(0, 10),
},
];
let filtered: Vec<_> = relationships.iter().with_bond_range(0.8, 1.0).collect();
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].name, "Strong");
}
}