Skip to content

Commit 11b720e

Browse files
author
Rogier Spieker
committed
feat: Completed Conditional Expressions
- completed $cond implementation - added $ifNull - added $switch
1 parent bb87cd1 commit 11b720e

File tree

8 files changed

+737
-27
lines changed

8 files changed

+737
-27
lines changed

docs/status/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ This document provides a comprehensive reference of all MongoDB query operators
5959
| Bitwise | 4 | 0 | 0% |
6060
| Boolean | 6 | 0 | 0% |
6161
| Comparison | 8 | 7 | 87% |
62-
| Conditional | 3 | 0 | 0% |
62+
| Conditional | 3 | 3 | 100% |
6363
| Custom Aggregation | 1 | 0 | 0% |
6464
| Data Size | 2 | 0 | 0% |
6565
| Date | 21 | 0 | 0% |
@@ -151,7 +151,7 @@ Expressions are MQL components that resolve to a value. Expressions are stateles
151151
| [`$cmp`](#cmp-expr) | Comparison | 1.0 ||
152152
| [`$concat`](#concat) | String | 1.0 | × |
153153
| [`$concatArrays`](#concatArrays) | Array | 3.2 | × |
154-
| [`$cond`](#cond) | Conditional | 1.0 | |
154+
| [`$cond`](#cond) | Conditional | 1.0 | |
155155
| [`$convert`](#convert) | Type | 4.0 ||
156156
| [`$cos`](#cos) | Trigonometry | 3.6 | × |
157157
| [`$cosh`](#cosh) | Trigonometry | 3.6 | × |
@@ -183,7 +183,7 @@ Expressions are MQL components that resolve to a value. Expressions are stateles
183183
| [`$gt`](#gt-expr) | Comparison | 1.0 ||
184184
| [`$gte`](#gte-expr) | Comparison | 1.0 ||
185185
| [`$hour`](#hour) | Date | 1.0 | × |
186-
| [`$ifNull`](#ifNull) | Conditional | 1.0 | × |
186+
| [`$ifNull`](#ifNull) | Conditional | 1.0 | |
187187
| [`$in`](#in-expr) | Comparison | 1.0 | × |
188188
| [`$indexOfArray`](#indexOfArray) | Array | 3.4 | × |
189189
| [`$indexOfBytes`](#indexOfBytes) | String | 3.4 | × |
@@ -256,7 +256,7 @@ Expressions are MQL components that resolve to a value. Expressions are stateles
256256
| [`$substrBytes`](#substrBytes) | String | 3.4 | × |
257257
| [`$substrCP`](#substrCP) | String | 3.4 | × |
258258
| [`$subtract`](#subtract-expr) | Arithmetic | 1.0 | × |
259-
| [`$switch`](#switch) | Conditional | 1.0 | × |
259+
| [`$switch`](#switch) | Conditional | 1.0 | |
260260
| [`$tan`](#tan) | Trigonometry | 3.6 | × |
261261
| [`$tanh`](#tanh) | Trigonometry | 3.6 | × |
262262
| [`$toBool`](#toBool) | Type | 3.6 ||

docs/status/findings/cond.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# $cond - Implementation Status
2+
3+
**Operator**: `$cond`
4+
**Type**: Conditional
5+
**MongoDB Version**: 1.0
6+
7+
## Summary
8+
9+
Status: **complete**
10+
11+
The `$cond` operator is a ternary operator that evaluates a boolean expression and returns one of two values depending on the result.
12+
13+
## Implementation Details
14+
15+
**Source File**: `source/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts` (lines 99-129)
16+
17+
**Function Signature**:
18+
```typescript
19+
export function $cond(
20+
query: Condition,
21+
compile: ExpressionCompiler,
22+
): Evaluator<any>
23+
```
24+
25+
**Logic**:
26+
- Supports two syntaxes:
27+
- Object syntax: `{ if: <condition>, then: <true-case>, else: <false-case> }`
28+
- Array syntax: `[ <condition>, <true-case>, <false-case> ]`
29+
- Normalizes both syntaxes to array format upfront
30+
- Uses `isTruthy` helper for MongoDB-style truthiness checking
31+
- Returns compiled evaluator that evaluates condition at runtime
32+
33+
**Validation**:
34+
- `isConditionArray`: Checks array has exactly 3 elements
35+
- `isConditionObject`: Uses `all(isKey("if"), isKey("then"), isKey("else"))` from @konfirm/guard
36+
- Clear error messages for missing properties or wrong formats
37+
38+
## Test Coverage
39+
40+
**Test File**: `test/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts`
41+
42+
**Test Results**: All tests passing
43+
44+
**Test Cases (12 total)**:
45+
-Object syntax with true/false conditions
46+
-Array syntax with true/false conditions
47+
-Truthy/falsy value handling (1, 0, '', 'hello', null, undefined)
48+
-Different return types (strings, numbers, objects, arrays)
49+
-Array syntax with various types
50+
-Unhappy paths: wrong array length, missing properties, invalid types
51+
52+
**Coverage Summary**:
53+
-Both syntax variations
54+
-All MongoDB truthy/falsy values
55+
-Various return value types
56+
-Validation error handling
57+
58+
## Exported From
59+
60+
- `source/Domain/Filter/Operator/Evaluation/Expression.ts`
61+
62+
## Dependencies
63+
64+
- **@konfirm/guard**: `isArray`, `isKey`, `isObject`, `all`
65+
- **Internal**: `isTruthy`, `isFalsy` helpers (also exported)
66+
67+
## Notes
68+
69+
- Uses normalization pattern - both syntaxes converted to array format
70+
- Validation uses declarative guard library patterns
71+
- Very commonly used in real-world aggregation pipelines

docs/status/findings/ifNull.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# $ifNull - Implementation Status
2+
3+
**Operator**: `$ifNull`
4+
**Type**: Conditional
5+
**MongoDB Version**: 1.0
6+
7+
## Summary
8+
9+
Status: **complete**
10+
11+
The `$ifNull` operator returns the first expression if it evaluates to a non-null value. Otherwise, it returns the second expression's value.
12+
13+
## Implementation Details
14+
15+
**Source File**: `source/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts` (lines 140-151)
16+
17+
**Function Signature**:
18+
```typescript
19+
export function $ifNull(
20+
query: [unknown, unknown],
21+
compile: ExpressionCompiler,
22+
): Evaluator<any>
23+
```
24+
25+
**Logic**:
26+
- Accepts array with exactly 2 elements: `[expression, replacement]`
27+
- Compiles both expressions using the expression compiler
28+
- Returns evaluator that returns first value unless it's null/undefined
29+
- Uses nullish coalescing operator (`??`) for clean implementation
30+
31+
**Validation**:
32+
- `isArrayOfSize(2, 2)`: Ensures exactly 2-element array
33+
- Clear error message: "$ifNull must be an array with exactly 2 elements"
34+
35+
**Implementation**:
36+
```typescript
37+
const [prefer, otherwise] = query.map(compile);
38+
return (input: any) => prefer(input) ?? otherwise(input);
39+
```
40+
41+
## Test Coverage
42+
43+
**Test File**: `test/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts`
44+
45+
**Test Results**: All tests passing
46+
47+
**Test Cases (8 total)**:
48+
-Valid value returns itself (strings, numbers, objects)
49+
-null returns replacement value
50+
-undefined returns replacement value
51+
-Falsy but non-null values return themselves (0, '', false)
52+
-Different replacement types (strings, objects)
53+
-Unhappy paths: wrong array length (1 element, 3 elements)
54+
-Unhappy paths: non-array types (string, object)
55+
56+
**Coverage Summary**:
57+
-Null and undefined handling
58+
-Distinguishes between null/undefined and other falsy values
59+
-Various value types
60+
-Validation error handling
61+
62+
## Exported From
63+
64+
- `source/Domain/Filter/Operator/Evaluation/Expression.ts`
65+
66+
## Dependencies
67+
68+
- **@konfirm/guard**: `isArrayOfSize`
69+
70+
## Notes
71+
72+
- Very commonly used for providing default values
73+
- Distinguishes null/undefined from other falsy values (0, '', false)
74+
- Clean implementation using nullish coalescing operator (`??`)
75+
- First conditional operator to use `isArrayOfSize` validation pattern

docs/status/findings/switch.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# $switch - Implementation Status
2+
3+
**Operator**: `$switch`
4+
**Type**: Conditional
5+
**MongoDB Version**: 1.0
6+
7+
## Summary
8+
9+
Status: **complete**
10+
11+
The `$switch` operator evaluates a series of case expressions and executes the first matching case, similar to a switch/case statement in programming languages.
12+
13+
## Implementation Details
14+
15+
**Source File**: `source/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts` (lines 194-215)
16+
17+
**Function Signature**:
18+
```typescript
19+
export function $switch(
20+
query: SwitchObject,
21+
compile: ExpressionCompiler,
22+
): Evaluator<any>
23+
```
24+
25+
**Logic**:
26+
- Accepts object with `branches` array and optional `default`
27+
- Each branch has `case` (condition) and `then` (value)
28+
- Compiles all branches and default into `CompiledBranch` objects
29+
- Uses clever pattern: adds default as final branch with `() => true` test
30+
- Returns evaluator that finds first matching branch using `.find()`
31+
32+
**Validation**:
33+
- `isDefined`: `all(not(isNULL), not(isUndefined))` - ensures values exist
34+
- `isSwitchBranch`: `isStructure({ case: isDefined, then: isDefined })`
35+
- `isSwitchObject`: `isStructure({ branches: isArrayOfType(isSwitchBranch), default: isDefined }, 'default')`
36+
- Declarative validation using @konfirm/guard's `isStructure` and `isArrayOfType`
37+
38+
**Helper Function**:
39+
```typescript
40+
type CompiledBranch = {
41+
test: Evaluator<boolean>;
42+
apply: Evaluator<unknown>;
43+
};
44+
45+
function compileBranch(branch: SwitchBranch, compile: ExpressionCompiler): CompiledBranch
46+
```
47+
48+
## Test Coverage
49+
50+
**Test File**: `test/Domain/Filter/Operator/Evaluation/Expression/Conditional.ts`
51+
52+
**Test Results**: All tests passing
53+
54+
**Test Cases (6 total)**:
55+
-Multiple branches with equality checks
56+
-Multiple branches with range checks ($gt)
57+
-Default case matching when no branch matches
58+
-No default case returns null
59+
-First matching branch wins (order matters)
60+
-Unhappy paths: invalid structure, wrong keys, missing branches
61+
62+
**Coverage Summary**:
63+
-Multiple branches
64+
-Default handling
65+
-No-default case (returns null)
66+
-Various condition types
67+
-Validation error handling
68+
69+
## Exported From
70+
71+
- `source/Domain/Filter/Operator/Evaluation/Expression.ts`
72+
73+
## Dependencies
74+
75+
- **@konfirm/guard**: `isStructure`, `isArrayOfType`, `all`, `not`, `isNULL`, `isUndefined`
76+
- **Internal**: `isTruthy` helper
77+
78+
## Notes
79+
80+
- Most elegant implementation of the three conditional operators
81+
- Uses clever pattern: default case added as final always-matching branch
82+
- Excellent example of @konfirm/guard's `isStructure` and `isArrayOfType` usage
83+
- Type-safe with proper TypeScript narrowing after validation
84+
- More powerful than `$cond` for multiple conditions

source/Domain/Filter/Operator/Evaluation/Expression.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as Arithmetic from './Expression/Arithmetic';
66
import * as Array from './Expression/Array';
77
// import * as Boolean from './Expression/Boolean';
88
import * as Comparison from './Expression/Comparison';
9-
// import * as Conditional from './Expression/Conditional';
9+
import * as Conditional from './Expression/Conditional';
1010
// import * as Custom from './Expression/Custom';
1111
// import * as DataSize from './Expression/DataSize';
1212
// import * as Date from './Expression/Date';
@@ -25,6 +25,7 @@ const expressions = {
2525
...Arithmetic,
2626
...Array,
2727
...Comparison,
28+
...Conditional,
2829
...Literal,
2930
...Misc,
3031
};
@@ -73,13 +74,13 @@ function compile(query: Partial<ExpressionQuery> | FieldReference | unknown): (i
7374
// TODO: allow for $comment
7475
const keys = Object.keys(query);
7576
const ops = keys.filter((key) => key in expressions).map((key) => {
76-
const op = expressions[key as keyof typeof expressions];
77+
const op = expressions[key as keyof typeof expressions] as (...args: Array<any>) => (v: any) => any;
7778
const { [key as keyof typeof query]: value } = query;
7879

79-
return (<(...args: Array<any>) => ReturnType<typeof op>>op)(value, compile);
80+
return op(value, compile);
8081
});
8182

82-
return (input: any) => ops.reduce((carry, op) => op(carry), input);
83+
return (input: any) => ops.reduce((carry, operation) => operation(carry), input);
8384
}
8485

8586
if (isFieldReference(query)) {

0 commit comments

Comments
 (0)