Skip to content

Commit ccb539a

Browse files
committed
Initial version
1 parent bdf4f38 commit ccb539a

File tree

16 files changed

+1797
-0
lines changed

16 files changed

+1797
-0
lines changed

.github/workflows/main.yml

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
name: Java CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
workflow_dispatch:
7+
inputs:
8+
deploy:
9+
description: 'Deploy to Maven Central'
10+
type: boolean
11+
default: false
12+
luceeVersions:
13+
description: 'JSON array of Lucee versions to test'
14+
required: false
15+
default: '["6.2/stable/light", "7.0/stable/light"]'
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
outputs:
21+
version: ${{ steps.extract-version.outputs.VERSION }}
22+
steps:
23+
- uses: actions/checkout@v4
24+
25+
- name: Set up JDK 11
26+
uses: actions/setup-java@v4
27+
with:
28+
java-version: '11'
29+
distribution: 'temurin'
30+
31+
- name: Cache Maven packages
32+
uses: actions/cache@v4
33+
with:
34+
path: ~/.m2
35+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
36+
restore-keys: |
37+
${{ runner.os }}-maven-
38+
39+
- name: Extract version number
40+
id: extract-version
41+
run: |
42+
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout -f source/java/pom.xml)
43+
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
44+
45+
- name: Build extension
46+
run: ant -buildfile build.xml
47+
48+
- name: Upload Artifact
49+
uses: actions/upload-artifact@v4
50+
with:
51+
name: jsonata-lex
52+
path: target/*.lex
53+
54+
- name: Cache Lucee files
55+
uses: actions/cache@v4
56+
with:
57+
path: ~/work/_actions/lucee/script-runner/main/lucee-download-cache
58+
key: lucee-downloads
59+
60+
test:
61+
runs-on: ubuntu-latest
62+
needs: build
63+
strategy:
64+
fail-fast: false
65+
matrix:
66+
lucee: ${{ fromJSON(inputs.luceeVersions || vars.LUCEE_TEST_VERSIONS || '["6.2/stable/light", "7.0/snapshot/light"]') }}
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- name: Download extension artifact
71+
uses: actions/download-artifact@v4
72+
with:
73+
name: jsonata-lex
74+
path: target
75+
76+
- name: Checkout Lucee
77+
uses: actions/checkout@v4
78+
with:
79+
repository: lucee/lucee
80+
path: lucee
81+
82+
- name: Run Lucee Test Suite
83+
uses: lucee/script-runner@main
84+
with:
85+
webroot: ${{ github.workspace }}/lucee/test
86+
execute: /bootstrap-tests.cfm
87+
luceeVersionQuery: ${{ matrix.lucee }}
88+
extensionDir: ${{ github.workspace }}/target
89+
env:
90+
testLabels: jsonata
91+
testAdditional: ${{ github.workspace }}/tests
92+
LUCEE_ADMIN_PASSWORD: admin
93+
94+
deploy:
95+
runs-on: ubuntu-latest
96+
needs: [build, test]
97+
if: github.event_name == 'workflow_dispatch' && inputs.deploy && needs.test.result == 'success'
98+
steps:
99+
- uses: actions/checkout@v4
100+
101+
- name: Set up JDK 11
102+
uses: actions/setup-java@v4
103+
with:
104+
distribution: 'temurin'
105+
java-version: '11'
106+
107+
- name: Cache Maven packages
108+
uses: actions/cache@v4
109+
with:
110+
path: ~/.m2
111+
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
112+
restore-keys: |
113+
${{ runner.os }}-maven-
114+
115+
- name: Import GPG key
116+
run: |
117+
echo "$GPG_PRIVATE_KEY" | base64 --decode | gpg --batch --import
118+
env:
119+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
120+
121+
- name: Build and Deploy with Maven
122+
env:
123+
MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
124+
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
125+
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
126+
run: |
127+
if [[ "${{ needs.build.outputs.version }}" == *-SNAPSHOT ]]; then
128+
echo "------- Maven Deploy snapshot -------";
129+
mvn -B -e -f source/java/pom.xml clean deploy -Dgoal=deploy --settings maven-settings.xml
130+
elif [[ "${{ needs.build.outputs.version }}" == *-ALPHA ]]; then
131+
echo "------- Maven Install alpha -------";
132+
mvn -B -e -f source/java/pom.xml clean install -Dgoal=install --settings maven-settings.xml
133+
else
134+
echo "------- Maven Deploy release -------";
135+
mvn -B -e -f source/java/pom.xml clean deploy -Dgoal=deploy -DperformRelease=true --settings maven-settings.xml
136+
fi

.gitignore

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Build outputs
2+
*.class
3+
target/
4+
build/
5+
temp/
6+
7+
# IDE
8+
*.classpath
9+
*.project
10+
*.settings/
11+
.idea/
12+
*.iml
13+
14+
# OS
15+
*.DS_Store
16+
Thumbs.db
17+
18+
# Maven
19+
.flattened-pom.xml
20+
dependency-reduced-pom.xml
21+
22+
# Test output
23+
test-output/
24+
25+
# Local dev
26+
.claude/
27+
.vscode/
28+
PLAN.md
29+
test.bat

README.md

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)