|
| 1 | +# JSONata Extension for Lucee |
| 2 | + |
| 3 | +Query and transform JSON data using [JSONata](https://jsonata.org) expressions. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +Download the `.lex` file from releases and install via Lucee Admin, or drop it into your server's `deploy` directory. |
| 8 | + |
| 9 | +## Usage |
| 10 | + |
| 11 | +```cfml |
| 12 | +result = JSONata( expression, data [, bindings [, options]] ) |
| 13 | +``` |
| 14 | + |
| 15 | +Arguments: |
| 16 | + |
| 17 | +- **expression** - JSONata query/transform expression (required) |
| 18 | +- **data** - CFML struct/array or JSON string (required) |
| 19 | +- **bindings** - Optional struct of variables accessible via `$varName` in the expression |
| 20 | +- **options** - Optional struct with runtime options (timeout, maxDepth, functions) |
| 21 | + |
| 22 | +## Examples |
| 23 | + |
| 24 | +### Basic Queries |
| 25 | + |
| 26 | +```cfml |
| 27 | +// Simple path |
| 28 | +data = { name: "Zac", age: 42 }; |
| 29 | +JSONata( "name", data ) // "Zac" |
| 30 | +
|
| 31 | +// Works with JSON strings too |
| 32 | +JSONata( "name", '{"name":"Zac","age":42}' ) // "Zac" |
| 33 | +
|
| 34 | +// Nested paths |
| 35 | +data = { user: { profile: { city: "Sydney" } } }; |
| 36 | +JSONata( "user.profile.city", data ) // "Sydney" |
| 37 | +``` |
| 38 | + |
| 39 | +### Aggregations |
| 40 | + |
| 41 | +```cfml |
| 42 | +data = { values: [ 1, 2, 3, 4, 5 ] }; |
| 43 | +
|
| 44 | +JSONata( "$sum(values)", data ) // 15 |
| 45 | +JSONata( "$average(values)", data ) // 3 |
| 46 | +JSONata( "$count(values)", data ) // 5 |
| 47 | +JSONata( "$min(values)", data ) // 1 |
| 48 | +JSONata( "$max(values)", data ) // 5 |
| 49 | +``` |
| 50 | + |
| 51 | +### Filtering |
| 52 | + |
| 53 | +```cfml |
| 54 | +data = { |
| 55 | + products: [ |
| 56 | + { name: "apple", price: 1.50 }, |
| 57 | + { name: "banana", price: 0.75 }, |
| 58 | + { name: "cherry", price: 3.00 } |
| 59 | + ] |
| 60 | +}; |
| 61 | +
|
| 62 | +// Filter by condition |
| 63 | +JSONata( "products[price > 1].name", data ) // ["apple", "cherry"] |
| 64 | +
|
| 65 | +// First match |
| 66 | +JSONata( "products[price < 1]", data ) // { name: "banana", price: 0.75 } |
| 67 | +``` |
| 68 | + |
| 69 | +### Transformations |
| 70 | + |
| 71 | +```cfml |
| 72 | +data = { firstName: "Zac", lastName: "Spitzer" }; |
| 73 | +
|
| 74 | +// String concatenation |
| 75 | +JSONata( 'firstName & " " & lastName', data ) // "Zac Spitzer" |
| 76 | +
|
| 77 | +// Build new structure |
| 78 | +JSONata( '{ "fullName": firstName & " " & lastName }', data ) |
| 79 | +// { fullName: "Zac Spitzer" } |
| 80 | +
|
| 81 | +// String functions |
| 82 | +JSONata( "$uppercase(firstName)", data ) // "ZAC" |
| 83 | +JSONata( "$lowercase(lastName)", data ) // "spitzer" |
| 84 | +``` |
| 85 | + |
| 86 | +### Complex Queries |
| 87 | + |
| 88 | +```cfml |
| 89 | +data = { |
| 90 | + orders: [ |
| 91 | + { product: "A", quantity: 2, price: 10 }, |
| 92 | + { product: "B", quantity: 1, price: 25 }, |
| 93 | + { product: "A", quantity: 3, price: 10 } |
| 94 | + ] |
| 95 | +}; |
| 96 | +
|
| 97 | +// Calculate total |
| 98 | +JSONata( "$sum(orders.(quantity * price))", data ) // 75 |
| 99 | +``` |
| 100 | + |
| 101 | +### Variable Bindings |
| 102 | + |
| 103 | +```cfml |
| 104 | +// Simple variable |
| 105 | +data = { name: "World" }; |
| 106 | +JSONata( "$greeting & ' ' & name", data, { greeting: "Hello" } ) // "Hello World" |
| 107 | +
|
| 108 | +// Multiple variables |
| 109 | +data = { value: 10 }; |
| 110 | +JSONata( "value * $multiplier + $offset", data, { multiplier: 5, offset: 3 } ) // 53 |
| 111 | +
|
| 112 | +// Array as variable |
| 113 | +data = { multiplier: 2 }; |
| 114 | +JSONata( "$sum($values) * multiplier", data, { values: [ 1, 2, 3 ] } ) // 12 |
| 115 | +
|
| 116 | +// Struct as variable |
| 117 | +data = { items: [ 1, 2, 3 ] }; |
| 118 | +JSONata( "$config.prefix & $string($sum(items))", data, { config: { prefix: "Total: " } } ) // "Total: 6" |
| 119 | +``` |
| 120 | + |
| 121 | +### Timeout and Safety Options |
| 122 | + |
| 123 | +Protect against runaway expressions with timeout and recursion depth limits: |
| 124 | + |
| 125 | +```cfml |
| 126 | +// With timeout (milliseconds) and max recursion depth |
| 127 | +JSONata( expression, data, {}, { timeout: 5000, maxDepth: 50 } ) |
| 128 | +
|
| 129 | +// Throws error if expression exceeds limits |
| 130 | +// - timeout: "Expression evaluation timeout: Check for infinite loop" |
| 131 | +// - maxDepth: "Stack overflow error: Check for non-terminating recursive function..." |
| 132 | +``` |
| 133 | + |
| 134 | +Default values: |
| 135 | + |
| 136 | +- **timeout**: 5000ms (5 seconds) |
| 137 | +- **maxDepth**: 100 |
| 138 | + |
| 139 | +### Custom Functions |
| 140 | + |
| 141 | +Register CFML closures as custom JSONata functions: |
| 142 | + |
| 143 | +```cfml |
| 144 | +// Single-arg function - use lowercase in expression |
| 145 | +data = { price: 42.5 }; |
| 146 | +JSONata( |
| 147 | + "$formatprice(price)", |
| 148 | + data, |
| 149 | + {}, |
| 150 | + { |
| 151 | + functions: { |
| 152 | + formatPrice: function( val ) { |
| 153 | + return "$" & numberFormat( val, ",.00" ); |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | +) // "$42.50" |
| 158 | +
|
| 159 | +// Multi-arg function |
| 160 | +data = { name: "Zac", age: 42 }; |
| 161 | +JSONata( |
| 162 | + "$makeuser(name, age)", |
| 163 | + data, |
| 164 | + {}, |
| 165 | + { |
| 166 | + functions: { |
| 167 | + makeUser: function( n, a ) { |
| 168 | + return { username: n, years: a }; |
| 169 | + } |
| 170 | + } |
| 171 | + } |
| 172 | +) // { username: "Zac", years: 42 } |
| 173 | +
|
| 174 | +// Combine with bindings and timeout |
| 175 | +JSONata( |
| 176 | + "$calc(value) + $offset", |
| 177 | + { value: 100 }, |
| 178 | + { offset: 5 }, |
| 179 | + { |
| 180 | + timeout: 5000, |
| 181 | + functions: { |
| 182 | + calc: function( v ) { return v * 2; } |
| 183 | + } |
| 184 | + } |
| 185 | +) // 205 |
| 186 | +``` |
| 187 | + |
| 188 | +**Note:** Use lowercase function names in expressions (e.g., `$formatprice`) since CFML uppercases struct keys. |
| 189 | + |
| 190 | +## Return Values |
| 191 | + |
| 192 | +- Scalar results return as CFML strings, numbers, or booleans |
| 193 | +- Object results return as CFML Structs |
| 194 | +- Array results return as CFML Arrays |
| 195 | +- Missing paths return empty string `""` |
| 196 | + |
| 197 | +## JSONata Reference |
| 198 | + |
| 199 | +Full expression syntax: https://docs.jsonata.org |
| 200 | + |
| 201 | +## Building |
| 202 | + |
| 203 | +```bash |
| 204 | +ant -buildfile build.xml |
| 205 | +``` |
| 206 | + |
| 207 | +Output: `target/jsonata-extension-{version}.lex` |
| 208 | + |
| 209 | +## Dependencies |
| 210 | + |
| 211 | +Uses [dashjoin/jsonata-java](https://github.com/dashjoin/jsonata-java) (embedded, zero external dependencies). |
| 212 | + |
| 213 | +## License |
| 214 | + |
| 215 | +LGPL 2.1 |
0 commit comments