Skip to content

Commit e7b0be0

Browse files
committed
Init project
1 parent 090ca6d commit e7b0be0

90 files changed

Lines changed: 21705 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main, master]
6+
pull_request:
7+
branches: [main, master]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
fail-fast: false
14+
matrix:
15+
node-version: [20, 22]
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: pnpm/action-setup@v4
20+
with:
21+
version: 9
22+
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: ${{ matrix.node-version }}
26+
cache: pnpm
27+
28+
- name: Install dependencies
29+
run: pnpm install --frozen-lockfile
30+
31+
- name: Typecheck
32+
run: pnpm typecheck
33+
34+
- name: Lint
35+
run: pnpm lint
36+
37+
- name: Format check
38+
run: pnpm format:check
39+
40+
- name: Unit tests
41+
run: pnpm test:unit
42+
43+
- name: Integration tests
44+
run: pnpm test:integration
45+
46+
- name: Compliance tests
47+
run: pnpm test:compliance
48+
49+
- name: Build
50+
run: pnpm build

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Dependencies
2+
node_modules/
3+
4+
# Build outputs
5+
dist/
6+
*.tsbuildinfo
7+
8+
# Test outputs
9+
coverage/
10+
11+
# Editor
12+
.vscode/
13+
.idea/
14+
*.swp
15+
*.swo
16+
17+
# OS
18+
.DS_Store
19+
Thumbs.db
20+
21+
# Logs
22+
*.log
23+
npm-debug.log*
24+
yarn-debug.log*
25+
pnpm-debug.log*
26+
27+
# Local env
28+
.env
29+
.env.local
30+
31+
# Claude Code local settings
32+
.claude/

.oxfmtrc.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"$schema": "node_modules/oxfmt/configuration_schema.json",
3+
"ignorePatterns": ["pnpm-lock.yaml", "tests/compliance/fixtures/**"],
4+
"printWidth": 120,
5+
"semi": false,
6+
"singleQuote": true,
7+
"trailingComma": "es5",
8+
"experimentalSortPackageJson": {
9+
"sortScripts": true
10+
},
11+
"experimentalSortImports": {
12+
"groups": [
13+
["side_effect"],
14+
["side_effect_style"],
15+
["builtin"],
16+
["external"],
17+
["internal"],
18+
["subpath"], // e.g. ##/..., @/...
19+
["parent"],
20+
["sibling"],
21+
["index"]
22+
]
23+
}
24+
}

.oxlintrc.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "node_modules/oxlint/configuration_schema.json",
3+
"options": {
4+
"typeAware": true,
5+
"reportUnusedDisableDirectives": "error",
6+
"denyWarnings": true
7+
}
8+
}

README.md

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# jsonpath-engine
2+
3+
TypeScript-first, RFC 9535-compliant JSONPath query engine.
4+
5+
JSONPath is a small query language for JSON, in the same spirit as XPath for XML or CSS selectors for the DOM. `jsonpath-engine` implements [RFC 9535](https://www.rfc-editor.org/rfc/rfc9535), the IETF standard that defines JSONPath's syntax, semantics, and well-typedness rules.
6+
7+
## Installation
8+
9+
```sh
10+
npm install jsonpath-engine
11+
# or
12+
pnpm add jsonpath-engine
13+
# or
14+
yarn add jsonpath-engine
15+
```
16+
17+
## Basic usage
18+
19+
```ts
20+
import { query, paths, nodes, first, exists } from 'jsonpath-engine'
21+
22+
const document = {
23+
store: {
24+
book: [
25+
{ category: 'reference', author: 'Nigel Rees', title: 'Sayings of the Century', price: 8.95 },
26+
{ category: 'fiction', author: 'Evelyn Waugh', title: 'Sword of Honour', price: 12.99 },
27+
{ category: 'fiction', author: 'Herman Melville', title: 'Moby Dick', price: 8.99 },
28+
],
29+
bicycle: { color: 'red', price: 19.95 },
30+
},
31+
}
32+
33+
query(document, '$.store.book[*].title')
34+
// ["Sayings of the Century", "Sword of Honour", "Moby Dick"]
35+
36+
query(document, '$.store.book[?@.price < 10].title')
37+
// ["Sayings of the Century", "Moby Dick"]
38+
39+
paths(document, '$..price')
40+
// ["$['store']['book'][0]['price']", "$['store']['book'][1]['price']", ...]
41+
42+
nodes(document, '$.store.book[0]')
43+
// [{ value: { category: "reference", ... }, path: "$['store']['book'][0]" }]
44+
45+
first(document, '$.store.book[0].title')
46+
// "Sayings of the Century"
47+
48+
exists(document, "$.store.book[?@.author == 'Nigel Rees']")
49+
// true
50+
```
51+
52+
## Compile once, run many
53+
54+
`compile` parses a query and runs the well-typedness check up front, so syntax and type errors raise at compile time rather than every evaluation. The result exposes the same five operations as the one-shot helpers and can be reused across documents.
55+
56+
```ts
57+
import { compile } from 'jsonpath-engine'
58+
59+
const cheapBookTitles = compile('$.store.book[?@.price < 10].title')
60+
61+
for (const document of largeStream) {
62+
console.log(cheapBookTitles.query(document))
63+
}
64+
```
65+
66+
`cheapBookTitles.source` returns the original query string for logging.
67+
68+
## Custom functions
69+
70+
The standard functions are pre-registered. Define your own with `defineFunction`, and the parameter and return types are inferred from the declared `argTypes` and `resultType`.
71+
72+
```ts
73+
import { defineFunction, registerFunction, LogicalTrue, LogicalFalse, query } from 'jsonpath-engine'
74+
75+
const startsWith = defineFunction({
76+
argTypes: ['ValueType', 'ValueType'] as const,
77+
resultType: 'LogicalType',
78+
evaluate(subject, prefix) {
79+
// subject and prefix are typed as ValueType, no manual narrowing needed
80+
if (typeof subject !== 'string' || typeof prefix !== 'string') {
81+
return LogicalFalse
82+
}
83+
return subject.startsWith(prefix) ? LogicalTrue : LogicalFalse
84+
},
85+
})
86+
87+
registerFunction('starts_with', startsWith)
88+
89+
query(users, "$[?starts_with(@.name, 'A')]")
90+
```
91+
92+
Function names follow the RFC 9535 ABNF: a lowercase letter followed by lowercase letters, digits, and underscores. Standard names (`length`, `count`, `value`, `match`, `search`) are reserved.
93+
94+
## JSONPath syntax
95+
96+
| Construct | Syntax | Example |
97+
| --------------------- | ------------------------ | ------------------------------------ |
98+
| Root identifier | `$` | `$` returns the whole document |
99+
| Member-name shorthand | `.name` | `$.store.book` |
100+
| Bracketed member name | `['name']` or `["name"]` | `$['store']['book']` |
101+
| Wildcard | `*` or `.*` | `$.store.book[*]` |
102+
| Index selector | `[N]` | `$.store.book[0]` |
103+
| Negative index | `[-N]` | `$.store.book[-1]` (last) |
104+
| Slice selector | `[start:end:step]` | `$.store.book[0:2]`, `$..book[::-1]` |
105+
| Filter selector | `[?expr]` | `$.store.book[?@.price < 10]` |
106+
| Descendant segment | `..` | `$..price`, `$..book[?@.isbn]` |
107+
| Multiple selectors | `[a, b, c]` | `$['store']['book'][0, 2]` |
108+
| Current node | `@` | `[?@.price < 10]` inside a filter |
109+
110+
Filter expression operators by precedence, highest first: comparison (`< <= > >= == !=`), logical not (`!`), logical and (`&&`), logical or (`||`). Parentheses force grouping.
111+
112+
The five standard functions, per RFC 9535 sections 2.4.4 through 2.4.8:
113+
114+
| Function | Signature | Notes |
115+
| ------------------------ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
116+
| `length(value)` | `ValueType -> ValueType` | Unicode code-point count for strings, length for arrays, key count for objects. `Nothing` for other inputs. |
117+
| `count(nodes)` | `NodesType -> ValueType` | Size of a nodelist. |
118+
| `value(nodes)` | `NodesType -> ValueType` | The value of a singleton nodelist, or `Nothing`. |
119+
| `match(value, pattern)` | `ValueType, ValueType -> LogicalType` | Full-string i-regexp match per RFC 9485. |
120+
| `search(value, pattern)` | `ValueType, ValueType -> LogicalType` | Substring i-regexp match per RFC 9485. |
121+
122+
## Errors
123+
124+
Three error classes, each extending the native parent so existing `instanceof SyntaxError` or `instanceof TypeError` checks keep working.
125+
126+
```ts
127+
class JsonPathSyntaxError extends SyntaxError {
128+
readonly query: string
129+
readonly start: number
130+
readonly end: number
131+
readonly code: JsonPathSyntaxErrorCode
132+
}
133+
134+
class JsonPathTypeError extends TypeError {
135+
readonly query: string
136+
readonly start: number
137+
readonly end: number
138+
readonly code: JsonPathTypeErrorCode
139+
}
140+
141+
class JsonPathFunctionError extends Error {
142+
readonly functionName: string
143+
readonly code: JsonPathFunctionErrorCode
144+
}
145+
```
146+
147+
Each error has a stable string `code` for programmatic dispatch. `formatJsonPathError` renders a multi-line caret view of the offending span.
148+
149+
```ts
150+
try {
151+
compile('$.foo[?(')
152+
} catch (caught) {
153+
if (caught instanceof JsonPathSyntaxError) {
154+
console.error(formatJsonPathError(caught))
155+
// JsonPathSyntaxError [empty-filter]: ...
156+
// 1 | $.foo[?(
157+
// | ^
158+
}
159+
}
160+
```
161+
162+
## API
163+
164+
### Query helpers
165+
166+
```ts
167+
function query(json: JsonValue, path: string): JsonValue[]
168+
function paths(json: JsonValue, path: string): NormalizedPath[]
169+
function nodes(json: JsonValue, path: string): JsonPathNode[]
170+
function first(json: JsonValue, path: string): JsonValue | undefined
171+
function exists(json: JsonValue, path: string): boolean
172+
173+
function compile(path: string): CompiledJsonPath
174+
```
175+
176+
```ts
177+
type CompiledJsonPath = {
178+
readonly source: string
179+
query(json: JsonValue): JsonValue[]
180+
paths(json: JsonValue): NormalizedPath[]
181+
nodes(json: JsonValue): JsonPathNode[]
182+
first(json: JsonValue): JsonValue | undefined
183+
exists(json: JsonValue): boolean
184+
}
185+
186+
type JsonPathNode = {
187+
readonly value: JsonValue
188+
readonly path: NormalizedPath
189+
}
190+
191+
type Nodelist = readonly JsonPathNode[]
192+
type NormalizedPath = string
193+
```
194+
195+
### Function registry
196+
197+
```ts
198+
function defineFunction<Args, Result>(extension: {
199+
argTypes: Args
200+
resultType: Result
201+
evaluate(...args): RuntimeOf<Result>
202+
}): JsonPathFunctionExtension
203+
204+
function registerFunction(name: string, extension: JsonPathFunctionExtension): void
205+
function unregisterFunction(name: string): boolean
206+
function listFunctions(): readonly string[]
207+
```
208+
209+
### Runtime type system
210+
211+
The runtime types from RFC 9535 section 2.4.1, plus type guards and the `nodelistToValue` conversion from section 2.4.3.
212+
213+
```ts
214+
const Nothing: unique symbol
215+
const LogicalTrue: unique symbol
216+
const LogicalFalse: unique symbol
217+
218+
function isNothing(value: JsonPathFunctionArg): boolean
219+
function isLogicalTrue(value: JsonPathFunctionArg): boolean
220+
function isLogicalFalse(value: JsonPathFunctionArg): boolean
221+
function isNodesType(value: JsonPathFunctionArg): boolean
222+
function nodelistToValue(nodes: NodesType): ValueType
223+
```
224+
225+
## License
226+
227+
[MIT License](./LICENSE) © 2026-present [Cong Nguyen](https://github.com/chicong065)

lefthook.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pre-commit:
2+
parallel: true
3+
commands:
4+
typecheck:
5+
run: pnpm typecheck
6+
lint:
7+
glob: '{src,tests,scripts}/**/*.ts'
8+
run: pnpm exec oxlint -c oxlint.json {staged_files}
9+
format:
10+
glob: '{src,tests,scripts}/**/*.ts'
11+
run: pnpm exec oxfmt -c oxfmt.json --check {staged_files}
12+
test-unit:
13+
run: pnpm test:unit

0 commit comments

Comments
 (0)