feat: add $consider (UP/DOWN) toggle to query builder and evaluators
This commit is contained in:
parent
5328471229
commit
27c9044a8b
|
|
@ -50,6 +50,17 @@ pub struct Response {
|
||||||
pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
pub fn evaluate(query: &Value, response: &Response) -> Result<bool> {
|
||||||
match query {
|
match query {
|
||||||
Value::Object(map) => {
|
Value::Object(map) => {
|
||||||
|
// $consider — "up" (default) or "down": flips result if conditions match
|
||||||
|
if let Some(consider) = map.get("$consider") {
|
||||||
|
let is_down = consider.as_str() == Some("down");
|
||||||
|
let rest: serde_json::Map<String, Value> = map.iter()
|
||||||
|
.filter(|(k, _)| k.as_str() != "$consider")
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect();
|
||||||
|
let matches = evaluate(&Value::Object(rest), response).unwrap_or(false);
|
||||||
|
return Ok(if is_down { !matches } else { matches });
|
||||||
|
}
|
||||||
|
|
||||||
// $and / $or / $not
|
// $and / $or / $not
|
||||||
if let Some(and) = map.get("$and") {
|
if let Some(and) = map.get("$and") {
|
||||||
let Value::Array(clauses) = and else { bail!("$and expects array") };
|
let Value::Array(clauses) = and else { bail!("$and expects array") };
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class QueryBuilder {
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
this.logic = '$and';
|
this.logic = '$and';
|
||||||
|
this.consider = 'up'; // 'up' | 'down'
|
||||||
this.rules = [this._emptyRule()];
|
this.rules = [this._emptyRule()];
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
@ -34,8 +35,9 @@ class QueryBuilder {
|
||||||
.map(r => this._ruleToQuery(r))
|
.map(r => this._ruleToQuery(r))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
if (conditions.length === 0) return null;
|
if (conditions.length === 0) return null;
|
||||||
if (conditions.length === 1) return conditions[0];
|
const base = conditions.length === 1 ? conditions[0] : { [this.logic]: conditions };
|
||||||
return { [this.logic]: conditions };
|
if (this.consider === 'down') return { $consider: 'down', ...base };
|
||||||
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ruleToQuery(rule) {
|
_ruleToQuery(rule) {
|
||||||
|
|
@ -86,18 +88,23 @@ class QueryBuilder {
|
||||||
if (!query || typeof query !== 'object') {
|
if (!query || typeof query !== 'object') {
|
||||||
this.rules = [this._emptyRule()];
|
this.rules = [this._emptyRule()];
|
||||||
this.logic = '$and';
|
this.logic = '$and';
|
||||||
|
this.consider = 'up';
|
||||||
this.render();
|
this.render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('$and' in query || '$or' in query) {
|
// Strip $consider before parsing rules
|
||||||
this.logic = '$and' in query ? '$and' : '$or';
|
this.consider = query.$consider === 'down' ? 'down' : 'up';
|
||||||
const clauses = query[this.logic];
|
const q = Object.fromEntries(Object.entries(query).filter(([k]) => k !== '$consider'));
|
||||||
|
|
||||||
|
if ('$and' in q || '$or' in q) {
|
||||||
|
this.logic = '$and' in q ? '$and' : '$or';
|
||||||
|
const clauses = q[this.logic];
|
||||||
if (Array.isArray(clauses)) {
|
if (Array.isArray(clauses)) {
|
||||||
this.rules = clauses.map(c => this._queryToRule(c)).filter(Boolean);
|
this.rules = clauses.map(c => this._queryToRule(c)).filter(Boolean);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.rules = [this._queryToRule(query)].filter(Boolean);
|
this.rules = [this._queryToRule(q)].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.rules.length === 0) this.rules = [this._emptyRule()];
|
if (this.rules.length === 0) this.rules = [this._emptyRule()];
|
||||||
|
|
@ -164,13 +171,18 @@ class QueryBuilder {
|
||||||
|
|
||||||
this.container.innerHTML = `
|
this.container.innerHTML = `
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4 flex-wrap">
|
||||||
<span class="text-sm text-gray-400">Match</span>
|
<span class="text-sm text-gray-400">Consider</span>
|
||||||
|
<select id="qb-consider" class="bg-gray-800 border border-gray-700 text-sm rounded px-2 py-1 focus:border-blue-500 focus:outline-none font-medium ${this.consider === 'down' ? 'text-red-400' : 'text-green-400'}">
|
||||||
|
<option value="up" ${this.consider === 'up' ? 'selected' : ''}>UP</option>
|
||||||
|
<option value="down" ${this.consider === 'down' ? 'selected' : ''}>DOWN</option>
|
||||||
|
</select>
|
||||||
|
<span class="text-sm text-gray-400">when</span>
|
||||||
<select id="qb-logic" class="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:border-blue-500 focus:outline-none">
|
<select id="qb-logic" class="bg-gray-800 border border-gray-700 text-gray-200 text-sm rounded px-2 py-1 focus:border-blue-500 focus:outline-none">
|
||||||
<option value="$and" ${this.logic === '$and' ? 'selected' : ''}>ALL</option>
|
<option value="$and" ${this.logic === '$and' ? 'selected' : ''}>ALL</option>
|
||||||
<option value="$or" ${this.logic === '$or' ? 'selected' : ''}>ANY</option>
|
<option value="$or" ${this.logic === '$or' ? 'selected' : ''}>ANY</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="text-sm text-gray-400">of the following conditions</span>
|
<span class="text-sm text-gray-400">of the following match</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="qb-rules" class="space-y-2">
|
<div id="qb-rules" class="space-y-2">
|
||||||
|
|
@ -190,6 +202,12 @@ class QueryBuilder {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Bind events
|
// Bind events
|
||||||
|
this.container.querySelector('#qb-consider').addEventListener('change', (e) => {
|
||||||
|
this.consider = e.target.value;
|
||||||
|
this.render();
|
||||||
|
this.onChange?.(this.getQuery());
|
||||||
|
});
|
||||||
|
|
||||||
this.container.querySelector('#qb-logic').addEventListener('change', (e) => {
|
this.container.querySelector('#qb-logic').addEventListener('change', (e) => {
|
||||||
this.logic = e.target.value;
|
this.logic = e.target.value;
|
||||||
this.render();
|
this.render();
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,14 @@ export function evaluate(query: unknown, ctx: EvalContext): boolean {
|
||||||
|
|
||||||
const q = query as Record<string, unknown>;
|
const q = query as Record<string, unknown>;
|
||||||
|
|
||||||
|
// $consider — "up" (default) or "down": flips result if conditions match
|
||||||
|
if ("$consider" in q) {
|
||||||
|
const consider = q.$consider as string;
|
||||||
|
const rest = Object.fromEntries(Object.entries(q).filter(([k]) => k !== "$consider"));
|
||||||
|
const matches = evaluate(rest, ctx);
|
||||||
|
return consider === "down" ? !matches : matches;
|
||||||
|
}
|
||||||
|
|
||||||
// $and
|
// $and
|
||||||
if ("$and" in q) {
|
if ("$and" in q) {
|
||||||
const clauses = q.$and;
|
const clauses = q.$and;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue