diff --git a/.github/workflows/pypi.yaml b/.github/workflows/pypi.yaml index 9fe50bd..774bb32 100644 --- a/.github/workflows/pypi.yaml +++ b/.github/workflows/pypi.yaml @@ -24,12 +24,12 @@ jobs: submodules: recursive - name: Build wheels - uses: pypa/cibuildwheel@v2.21.3 + uses: pypa/cibuildwheel@f03ac7617d6cff873ccf24cc0d567ef5ba5a9e6d # v4.0.0 env: CIBW_SKIP: cp36-* *-win32 CIBW_BEFORE_BUILD_WINDOWS: "choco install mingw -y" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: wheels-${{ matrix.os }} path: ./wheelhouse/*.whl @@ -47,7 +47,7 @@ jobs: python -m pip install --upgrade build setuptools python -m build --sdist . - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sdist path: dist/*.tar.gz @@ -67,7 +67,7 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: - python-version: '3.12' + python-version: '3.14' - name: Install MinGW (Windows) if: runner.os == 'Windows' @@ -75,7 +75,7 @@ jobs: choco install mingw -y echo "C:\ProgramData\chocolatey\bin" >> $env:GITHUB_PATH - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: sdist path: dist @@ -94,7 +94,7 @@ jobs: permissions: contents: read steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: artifacts merge-multiple: true diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 657fbbe..0afb467 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,8 +23,15 @@ jobs: - name: install dependencies run: | sudo apt-get update --fix-missing - sudo apt-get install -y -f -o Acquire::Retries=3 libjson-c-dev pkg-config - - name: Build and test json-c + sudo apt-get install -y -f -o Acquire::Retries=3 libjson-c-dev cmake pkg-config + - name: Build and install yyjson + run: | + git clone --depth 1 https://github.com/ibireme/yyjson.git /tmp/yyjson + cmake -S /tmp/yyjson -B /tmp/yyjson/build -DCMAKE_BUILD_TYPE=Release + cmake --build /tmp/yyjson/build --parallel + sudo cmake --install /tmp/yyjson/build + sudo ldconfig + - name: Build and test json-c/yyjson run: | make make tests-c diff --git a/Makefile b/Makefile index 9dc4b21..2304db3 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ JSON_C_CFLAGS=$(shell pkg-config --cflags json-c) JSON_C_LDFLAGS=$(shell pkg-config --libs json-c) +YYJSON_CFLAGS=$(shell pkg-config --cflags yyjson) +YYJSON_LDFLAGS=$(shell pkg-config --libs yyjson) include config.mk -all: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index +all: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union + +YYJSON_TESTS=test-yyjson bench: make -C bench @@ -36,7 +40,13 @@ test-json-filter-and-missing-key: tests/json-c/filter-and-missing-key.c csonpath test-json-get-array-big-index: tests/json-c/get-array-big-index.c csonpath_json-c.h csonpath.h csonpath_do.h $(CC) tests/json-c/get-array-big-index.c $(EXTRA_FILES) $(JSON_C_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-json-get-array-big-index $(JSON_C_LDFLAGS) $(LDFLAGS) -tests-c: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index +test-json-union: tests/json-c/union.c csonpath_json-c.h csonpath.h csonpath_do.h + $(CC) tests/json-c/union.c $(EXTRA_FILES) $(JSON_C_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-json-union $(JSON_C_LDFLAGS) $(LDFLAGS) + +test-yyjson: tests/yyjson/test-yyjson.c csonpath_yyjson.h csonpath.h csonpath_do.h + $(CC) tests/yyjson/test-yyjson.c $(EXTRA_FILES) $(YYJSON_CFLAGS) $(CFLAGS) -Wno-format -I./ -o test-yyjson $(YYJSON_LDFLAGS) $(LDFLAGS) + + tests-c: test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-yyjson ./test-json-c-get-a ./test-json-update ./test-json-filter @@ -44,6 +54,8 @@ tests-c: test-json-c-get-a test-json-update test-json-filter test-json-subpath t ./test-json-c-array-root ./test-json-filter-and-missing-key ./test-json-get-array-big-index + ./test-json-union + ./test-yyjson pip-dev: pip install -e .[dev] --force-reinstall @@ -54,5 +66,5 @@ tests-py: pip-dev tests: tests-py tests-c clean: - rm -rvf test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index + rm -rvf test-json-c-get-a test-json-update test-json-filter test-json-subpath test-json-c-array-root test-json-filter-and-missing-key test-json-get-array-big-index test-json-union test-yyjson diff --git a/README.md b/README.md index 9788ce3..b3a316e 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,289 @@ # csonpath -> that's not my path, that's not your path, but csonpath +> That's not my path, that's not your path, but **csonpath**. [![Project Sandbox](https://docs.outscale.com/fr/userguide/_images/Project-Sandbox-yellow.svg)](https://docs.outscale.com/en/userguide/Open-Source-Projects.html) **csonpath** is a partial [JSONPath](https://goessner.net/articles/JsonPath/) implementation in C, with Python bindings. It allows you to query, update, and remove data from JSON objects using path expressions. +Unlike many JSONPath libraries, csonpath is **backend-agnostic**: it can work with any C library or environment that manipulates array, object, and scalar types—not just JSON. Out of the box, backends for [json-c](https://github.com/json-c/json-c) and Python are provided. + +--- + +## 🚀 Features + +### JSONPath Syntax + +| Feature | Example | Description | +|---|---|---| +| Dot notation | `$.a.b` | Access nested object fields | +| Bracket notation | `$['a']['b']` | Alternative object/array access | +| Array index | `$.array[0]` | Access by zero-based index | +| Wildcard `[*]` | `$.array[*].field` | Iterate all array elements | +| Recursive descent `..` | `$..name` | Search recursively for a key | +| Union `\|` / `,` | `$.a \| $.b` or `$['a','b']` | Match multiple paths at once | +| Filters | `$.items[?price > 10]` | Filter array elements | +| Regex filters | `$.items[?name =~ "foo"]` | POSIX regex-based filtering | +| Multiple filters (`&`) | `$.items[?a=1 & b=2]` | Combine conditions | +| Subpath expressions | `$.obj[$.key]` | Use JSON values as dynamic path keys | +| `@` current object | `$.items[?@.price > 10]` | Reference the current element in filters | + +### Operations +- **Find First** — retrieve the first match. +- **Find All** — retrieve all matches (returns an array). +- **Update or Create** — modify existing values or create missing ones. +- **Remove** — delete matching elements. +- **Callback** — execute a custom callback on each match. +- **Update or Create Callback** — traverse the path, creating missing intermediate objects and arrays, then invoke the callback on each leaf node. + +--- + ## 🌐 Links - Project website: https://github.com/outscale/csonpath - Join our community on [Discord](https://discord.gg/HUVtY5gT6s) +--- ## 📄 Table of Contents -- [Overview](#-overview) -- [Requirements](#-requirements) +- [Features](#-features) - [Installation](#-installation) -- [Configuration](#-configuration) - [Usage](#-usage) -- [Examples](#-examples) -- [License](#-license) +- [C API Reference](#-c-api-reference) +- [Python API Reference](#-python-api-reference) +- [Custom Backends](#-custom-backends) +- [Running Tests](#-running-tests) +- [Directory Structure](#-directory-structure) - [Contributing](#-contributing) +- [License](#-license) +--- -## 🚀 Features - -### jsonpath -- support simple path like ex: `"$.a[3]["oui"]` -- support `..`: `"$..non` -- support `[*]`: `"$.array[*].obj[*].field"` -- support filters: - - with `>` or `<`: `"$.obj[?field > 123]"` - - with `=` or '!=': `"$.obj[?field != "123"]"` - - with regex, using POSIX regcom: `"$.obj[?.field ~= "123"]"` - - with `&`: `"$.obj[?field != "123" & second = 1]"` -- support `|`: `"$.obj | $.otherobj"` -- support subpath: `$.obj[$.key]`, even in filter `$.obj[?field > $.value]` -### Functions -- **Find First**: Retrieve the first value matching a path. -- **Find All**: Retrieve all values matching a path. -- **Update or Create**: Update values or create new ones according to the path. -- **Remove**: Remove values matching a path. -- **Update or Create Callback**: Update values or create new ones according to the path, using a user-define callback. -- **Callback**: Call a callback according to the path. -- **Supports both C (using json-c) and Python objects.** - -## ✅ Getting Started +## 📦 Installation ### Prerequisites - -- C compiler (gcc/clang) -- [json-c](https://github.com/json-c/json-c) library for C usage +- C compiler (`gcc` or `clang`) +- [json-c](https://github.com/json-c/json-c) library (for C usage) - Python 3.x (for Python bindings) -### Build +### C (json-c) +Just include the header in your project. There is no separate install step required: +```c +#include "csonpath_json-c.h" +``` +Make sure to link against `json-c` when compiling: +```sh +gcc myapp.c -o myapp $(pkg-config --cflags --libs json-c) +``` -To build the C tests and examples: +### Python +Install from PyPI: +```sh +pip install csonpath +``` +To install from source (development): ```sh -make +pip install . +# or +make pip-dev ``` -### Usage (json-c) +--- -See examples in [`tests/json-c/get-a.c`](tests/json-c/get-a.c): +## 🛠️ Usage + +### C (json-c) ```c #include "csonpath_json-c.h" -const char *json_str = "{ \"a\": \"value\", \"array\": [1,2,3] }"; -struct csonpath p; -struct json_object *jobj = json_tokener_parse(json_str); -struct json_object *ret; +static void my_cb(json_object *parent, struct csonpath_child_info *info, + json_object *current, void *ud) +{ + json_object_set_string(current, "modified"); +} -csonpath_init(&p, "$.a"); -ret = csonpath_find_first(&p, jobj); -// ret now points to "value" -``` +int main(void) +{ + struct json_object *jobj = json_tokener_parse(json_str); + struct csonpath *p = csonpath_new("$.a"); -#### Avaible functions: -```C -int csonpath_init(struct csonpath cjp[static 1], const char path[static 1]) + /* Find First: return the first match, or NULL */ + struct json_object *ret = csonpath_find_first(p, jobj); + + /* Find All: return a NEW json_object array. Caller must free it. */ + ret = csonpath_find_all(p, jobj); + json_object_put(ret); + + /* Remove: delete matching keys (or set array slots to null). Returns count. */ + int removed = csonpath_remove(p, jobj); + + /* Update or Create: replace matches, or create the full path if missing. */ + csonpath_update_or_create(p, jobj, json_object_new_string("new_value")); + + /* Callback: call a user function for every match. */ + csonpath_callback(p, jobj, my_cb, NULL); + + /* Update or Create Callback: like callback, but creates missing parents first, + then invokes the callback on every leaf (existing or newly created). */ + csonpath_update_or_create_callback(p, jobj, my_cb, NULL); + + csonpath_destroy(p); + json_object_put(jobj); + return 0; +} ``` -init a csonpath struct -```C -int csonpath_set_path(struct csonpath cjp[static 1], const char path[static 1]) +### Python + +```python +import csonpath + +data = {"a": "value", "array": [1, 2, 3]} +p = csonpath.CsonPath("$.a") + +# Find First / Find All +p.find_first(data) # -> "value" +p.find_all(data) # -> ["value"] + +# Remove: returns number of removed items +p.set_path("$.array[*]") +p.remove(data) + +# Update or Create: builds missing objects/arrays automatically +p.set_path("$.x.y.z") +p.update_or_create(data, []) +# data is now {"a": "value", "array": [1, 2, 3], "x": {"y": {"z": []}}} + +# Callback +p.set_path("$.a") +p.callback(data, lambda parent, idx, cur, _: parent.__setitem__(idx, cur.upper())) + +# Update or Create Callback: creates parents, then calls cb on each leaf +p.set_path("$[*].a") +p.update_or_create_callback(dst, my_sync_fn, userdata) ``` -change the path of a struct -```C -int csonpath_compile(struct csonpath cjp[static 1]) +--- + +## 📘 C API Reference + +```c +struct csonpath *csonpath_new(const char *path); ``` -compile the path, doesn't need to be called before using it, but can be useful to catch error earlyer. -```C -void csonpath_print_instruction(struct csonpath cjp[static 1]) +Create and initialize a new csonpath object. + +```c +int csonpath_set_path(struct csonpath *p, const char *path); ``` -print all instructions once compiled -```C -struct json_object *csonpath_find_first(struct csonpath cjp[static 1], struct json_object *json); +Change the path of an existing object. + +```c +int csonpath_compile(struct csonpath *p); ``` -Retrieve the first value matching a path -```C -struct json_object *csonpath_find_all(struct csonpath cjp[static 1], struct json_object *json); +Compile the path expression. This is optional—paths are compiled automatically on first use—but explicit compilation can help catch syntax errors earlier. + +```c +void csonpath_print_instruction(struct csonpath *p); ``` -Retrieve all values matching a path, return need to be free using `json_object_put` -```C -struct json_object *csonpath_remove(struct csonpath cjp[static 1], struct json_object *json); +Print the compiled bytecode instructions (useful for debugging). + +```c +struct json_object *csonpath_find_first(struct csonpath *p, struct json_object *json); ``` -Remove all matching elements from json -```C -struct json_object *csonpath_update_or_create(struct csonpath cjp[static 1], struct json_object *json, struct json_object *new_val) +Return the first matching value, or `NULL` if none is found. + +```c +struct json_object *csonpath_find_all(struct csonpath *p, struct json_object *json); ``` -update and push new_val, in json. -```C -struct json_object *csonpath_callback(struct csonpath cjp[static 1], struct json_object *json, json_c_callback callback, void *userdata) +Return a new `json_object` array containing all matches. **Must be freed with `json_object_put()`.** + +```c +int csonpath_remove(struct csonpath *p, struct json_object *json); ``` -call `callback` on each value matching path -```C -struct json_object *csonpath_update_or_create_callback(struct csonpath cjp[static 1], struct json_object *json, json_c_callback callback, void *userdata) +Remove all matching elements. Returns the number of elements removed. + +```c +int csonpath_update_or_create(struct csonpath *p, struct json_object *json, struct json_object *new_val); ``` -like callback, but update json base on the path, along the way. +Replace matching values with `new_val`, or create the path if it does not exist. -### Usage (Python) +```c +int csonpath_callback(struct csonpath *p, struct json_object *json, + json_c_callback callback, void *userdata); +``` +Invoke `callback` for every match. -#### Example -```python -import csonpath +```c +int csonpath_update_or_create_callback(struct csonpath *p, struct json_object *json, + json_c_callback callback, void *userdata); +``` +Like `callback`, but traverses the path while updating/creating missing intermediate objects. -d = { "a": "value", "array": [1,2,3] } -o = csonpath.CsonPath("$.a") -print(o.find_first(d)) # Output: "value" +```c +void csonpath_destroy(struct csonpath *p); ``` +Free the csonpath object. + +--- + +## 📗 Python API Reference + +- **`CsonPath(path, return_empty_array=False, jq_like=False)`** — Create a new csonpath object. Optional flags: `return_empty_array` returns `[]` instead of `None` when `find_all()` finds nothing; `jq_like` allows jq-style paths without a leading `$`. +- **`set_path(path)`** — Change the path expression. +- **`find_first(json)`** — Return the first match, or `None`. +- **`find_all(json)`** — Return a list of all matches, or `None` (or `[]` if configured). +- **`remove(json)`** — Remove all matches. Returns the number of removed items. +- **`update_or_create(json, value)`** — Replace matches with `value`, or create the path. +- **`callback(json, callback, callback_data=None)`** — Call `callback(parent, idx, current, callback_data)` for every match. +- **`update_or_create_callback(json, callback, callback_data=None)`** — Same as `callback`, but creates missing parent objects along the path. + +--- + +## 🔌 Custom Backends + +csonpath is designed to be backend-agnostic. It can work with any data structure that supports array, object, and scalar semantics. + +To create a custom backend, define the required macros and types in a header file (similar to `csonpath_json-c.h` or `csonpath_python.c`), then include your backend header before `csonpath.h`. This allows you to adapt csonpath for manipulating data in any format that supports array/object semantics, giving you full flexibility beyond just JSON. + +For more details, see the existing backend implementations: +- `csonpath_json-c.h` — json-c backend +- `csonpath_python.c` — Python backend -#### Avaible methodes: -- `CsonPath(path)`: create a new csonpath object. -- `CsonPath(path, return_empty_array)`: Create a new csonpath object and return an empty list instead of None (like jsonpath-ng). -- `OBJ.find_first(self, json)`: take a json, return first found occurrence. -- `OBJ.find_all(self, json)`: take a json, return all found occurrence in a list. -- `OBJ.remove(self, json)`: take a json, remove all found occurrence from it. -- `OBJ.update_or_create(self, json, value)`: take a json, replace all found occurrence from it, by the value, if doesn't found something, create it. -- `OBJ.update_or_create_callback(self, json, callback, callback_data)`: take a json, if it doesn't find a parent object, create it, then it calls a callback. -- `OBJ.callback(self, json, callback, callback_data)`: take a json, call a callback, an all occurrence. +--- -`callback` is a function that take 4 arguments: `parent`, `idx`, `cur_value` and `callback_data` +## 🧪 Running Tests -Check [`tests`](tests/python/) for more examples +### C Tests +```sh +make tests-c +``` -### Backends and Direct Usage +### Python Tests +```sh +make tests-py +``` -csonpath is inspired by JSONPath, but is designed to be backend-agnostic: it can work with any C library or environment that manipulates array, object, and scalar types—not just JSON. Out of the box, backends for Python and json-c are provided, but you can create your own backend for other formats such as YAML, or any custom data structure. +### All Tests +```sh +make tests +``` + +--- -To use csonpath with a different data type or library, simply define the required macros and types in a header file (similar to `csonpath_json-c.h` or `csonpath_python.c`), then include your backend header before `csonpath.h`. This approach allows you to adapt csonpath for manipulating data in any format that supports array/object semantics, giving you full flexibility beyond just JSON. +## 📁 Directory Structure -## Directory Structure +| File / Directory | Description | +|---|---| +| `csonpath.h`, `csonpath_do.h` | Core implementation (header-only style) | +| `csonpath_json-c.h` | json-c backend | +| `csonpath_python.c` | Python C extension backend | +| `tests/` | C and Python test suites | +| `bench/` | Performance benchmarks | -- `csonpath.h`, `csonpath_do.h` - Main implementation files -- `csonpath_json-c.h` - Json-C implementation, use it by including "csonpath_json-c.h" directly in your source -- `csonpath_python.c` - python implementation, use it like a python lib (so `pip install .`) -- `tests/` - Contains tests -- `bench/` - Some benchmarks +--- ## 🤝 Contributing @@ -170,5 +293,8 @@ Please read our [Contributing Guidelines](CONTRIBUTING.md) and [Code of Conduct] Feel free to open issues or pull requests! +--- + ## 📜 License + BSD 3-Clause. See [LICENSE](LICENSE). diff --git a/bench/bench-json-c.c b/bench/bench-json-c.c index b5f22a2..1325407 100644 --- a/bench/bench-json-c.c +++ b/bench/bench-json-c.c @@ -70,6 +70,7 @@ int main(int ac, char **av) { const char *queries[] = { "$.store.book[?(@.price) > 20].title", "$.store.book[*].title", + "$.store.book[*]['title','category']", "$.store.book[?title =~ \"Book\"].title", "$..title", }; diff --git a/bench/bench-jsonpath-ng.py b/bench/bench-jsonpath-ng.py index 02e2c7b..527f71f 100644 --- a/bench/bench-jsonpath-ng.py +++ b/bench/bench-jsonpath-ng.py @@ -19,19 +19,21 @@ queries = [ "$.store.book[?(@.price) > 20].title", "$.store.book[*].title", + "$.store.book[*]['title','category']", '$.store.book[?(@.title) =~ "Book"].title', "$..title", ] # Benchmark for query in queries: - #start = time.perf_counter() - #expr = parse(query) - #for x in range(1000): - # result = expr.find(data) - #elapsed = time.perf_counter() - start - #print(f"jsonpath-ng Query: {query}") - #print(f"jsonpath-ng Results: {len(result)}, Time: {elapsed:.6f} seconds\n") + start = time.perf_counter() + expr = parse(query) + for x in range(100): + result = expr.find(data) + # this one is so slow, that I do 100 loop instead of 1k, and multip,y result per 10 + elapsed = (time.perf_counter() - start) * 10 + print(f"jsonpath-ng Query: {query}") + print(f"jsonpath-ng Results: {len(result)}, Time: {elapsed:.6f} seconds\n") start = time.perf_counter() cp = csonpath.CsonPath(query) @@ -68,6 +70,12 @@ def rec_get_title(d, out): for l in d: rec_get_title(l, out) rec_get_title(data, result) + elif query == "$.store.book[*]['title','category']": + result = [] + books = data["store"]["book"] + for b in books: + result.append(b["title"]) + result.append(b["category"]) elif query == "$.store.book[*].title": result = [] books = data["store"]["book"] diff --git a/bench/bench-yyjson.c b/bench/bench-yyjson.c index 848fc10..3685f89 100644 --- a/bench/bench-yyjson.c +++ b/bench/bench-yyjson.c @@ -46,6 +46,7 @@ int main() { const char *queries[] = { "$.store.book[?(@.price) > 20].title", "$.store.book[*].title", + "$.store.book[*]['title','category']", "$.store.book[?title =~ /Book/].title", "$..title", }; diff --git a/csonpath.h b/csonpath.h index a40b21d..f7695f6 100644 --- a/csonpath.h +++ b/csonpath.h @@ -83,6 +83,9 @@ enum csonpath_instuction_raw { CSONPATH_INST_FILTER_KEY_REG_EQ, CSONPATH_INST_FILTER_AND, CSONPATH_INST_GET_ALL, + CSONPATH_INST_GET_UNION, + CSONPATH_INST_UNION_JMP, + CSONPATH_INST_UNION_END, CSONPATH_INST_FIND_ALL, CSONPATH_INST_RANGE, CSONPATH_INST_OR, @@ -108,6 +111,9 @@ static int csonpath_instuction_len[] = { 2, /* B CSONPATH_INST_FILTER_KEY_REG_EQ */ 1, /* D CSONPATH_INST_FILTER_AND */ 1, /* E CSONPATH_INST_GET_ALL */ + 1, /* E.5 CSONPATH_INST_GET_UNION */ + 1, /* E.5000 CSONPATH_INST_UNION_JMP */ + 1, /* E.6 CSONPATH_INST_UNION_END */ -1, /* F CSONPATH_INST_FIND_ALL */ 1, /* 10 CSONPATH_INST_RANGE */ 1, /* 11 CSONPATH_INST_OR */ @@ -135,6 +141,9 @@ CSONPATH_UNUSED static const char *csonpath_instuction_str[] = { "FILTER_KEY_REG_EQ", "FILTER_AND", "GET_ALL", + "UNION", + "UNION_JMP", + "UNION_END", "FIND_ALL", "RANGE", "OR", @@ -306,7 +315,11 @@ static inline struct csonpath *csonpath_new_ex(const char path[static 1], int fl * max inst is use so we know, we will never overflow. */ int max_inst = strlen(path) / 2 + 1; - struct csonpath *ret = malloc(sizeof *ret + CSONPATH_BYTE_PER_INST * max_inst + strlen(path) + 1); + int extra = 0; + for (const char *tmp = path; *tmp; ++tmp) + if (*tmp == ',') + ++extra; + struct csonpath *ret = malloc(sizeof *ret + CSONPATH_BYTE_PER_INST * max_inst + strlen(path) + 1 + extra); if (!ret) { return NULL; } @@ -357,8 +370,16 @@ static void csonpath_print_instruction(const struct csonpath cjp[const static 1] return; } int cnt = 0; - for (;*walker != CSONPATH_INST_END; walker = csonpath_walker_next_inst(walker)) { + int cnt_sub = 0; + for (;cnt_sub > 0 || *walker != CSONPATH_INST_END; walker = csonpath_walker_next_inst(walker)) { + for (int i = 0; i < cnt_sub; ++i) { + printf(" "); + } printf("%d: %s\n", (int)(intptr_t)(walker - origin), csonpath_instuction_str[(int)*walker]); + if (*walker == CSONPATH_INST_END) + --cnt_sub; + else if (*walker == CSONPATH_INST_GET_SUBPATH) + ++cnt_sub; if (cnt++ == 1000) { printf("%d: TOO MUCH INSTRUCTION, BAIL OUT\n", (int)(intptr_t)(walker - origin)); return; @@ -445,6 +466,7 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], const char *next; char to_check; int inst; + int in_union = 0; root_again: if (!(flag & CSONPATH_AUTO_ROOT)) { @@ -454,21 +476,55 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], to_check = *walker; again: + if (in_union) { + csonpath_push_char(cjp, CSONPATH_INST_UNION_END, inst_idx); + in_union = 0; + } + switch (to_check) { case '[': { int end; inst = 0; + + do_array: + if (!in_union) { + int cnt_brackets = 0; + for (const char *tmp_walk = walker + 1; + *tmp_walk && *tmp_walk != ']' && !cnt_brackets; + ++tmp_walk) { + if (!cnt_brackets && *tmp_walk == ',') { + csonpath_push_char(cjp, CSONPATH_INST_GET_UNION, inst_idx); + in_union = 1; + break; + } + if (*tmp_walk == '[') + ++cnt_brackets; + if (cnt_brackets && *tmp_walk == ']') + --cnt_brackets; + if (*tmp_walk == '"') + for (++tmp_walk; *tmp_walk != '"' && *tmp_walk; ++tmp_walk); + if (*tmp_walk == '\'') + for (++tmp_walk; *tmp_walk != '\'' && *tmp_walk; ++tmp_walk); + } + } ++walker; + for (; isblank(*walker); ++walker); + if (*walker == '*') { if (inst == CSONPATH_INST_FIND_ALL) { CSONPATH_COMPILE_ERR(tmp, walker - orig, "'%c' is invalide here\n", *walker); - goto error; + goto error; } csonpath_push_char(cjp, CSONPATH_INST_GET_ALL, inst_idx); + if (in_union && walker[1] == ',') { + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + walker += 1; + goto do_array; + } if (walker[1] != ']') { CSONPATH_COMPILE_ERR(tmp, walker - orig, "%s", "unclose bracket\n"); } @@ -615,12 +671,18 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } else { csonpath_push_char(cjp, CSONPATH_INST_FILTER_KEY_INFERIOR, inst_idx); } - } else if (to_check == ']') { + } else if (to_check == ']' || (in_union && to_check == ',')) { + int union_jmp = in_union && to_check == ','; if (have_parentesis > 0) CSONPATH_COMPILE_ERR(tmp, next - orig, "too many open parentesis"); csonpath_push_char(cjp, CSONPATH_INST_FILTER_KEY_EXIST, inst_idx); for (;isblank(*next); ++next); push_filter_getter(cjp, inst_idx, nb_getter_inst, filter_getter); + if (union_jmp) { + walker = next - 1; /* will be re-skipp hopefully :) */ + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + goto do_array; + } walker = next; to_check = *walker; goto again; @@ -691,7 +753,8 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } csonpath_push_char(cjp, CSONPATH_INST_GET_SUBPATH, inst_idx); int ret = csonpath_compile_do(cjp, orig, walker, tmp, - inst_idx, flag, ']', &end_sentinel); + inst_idx, flag, in_union ? 'u' : ']', + &end_sentinel); if (ret < 0) return -1; walker = end_sentinel; @@ -757,6 +820,11 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], --have_parentesis; } + if (in_union && to_check == ',') { + walker = next; + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + goto do_array; + } if (to_check != ']') { CSONPATH_REQUIRE_ERR(']', next); } @@ -768,10 +836,14 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], const char *end_sentinel; csonpath_push_char(cjp, CSONPATH_INST_GET_SUBPATH, inst_idx); int ret = csonpath_compile_do(cjp, orig, walker, tmp, - inst_idx, flag, ']', &end_sentinel); + inst_idx, flag,in_union ? 'u' : ']', &end_sentinel); if (ret < 0) return -1; walker = end_sentinel; + if (in_union && *end_sentinel == ',') { + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + goto do_array; + } ++walker; to_check = *walker; goto again; @@ -793,11 +865,14 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], ++next; continue; } + for (; isblank(*next); ++next); if (*next < '0' || *next > '9') { CSONPATH_COMPILE_ERR(tmp, walker - orig, "unexpected '%c', sting, filter or number require\n", *next); } next++; + if (in_union && *next == ',') + break; } while (*next && *next != ']'); if (!*next) { CSONPATH_COMPILE_ERR(tmp, walker - orig, @@ -805,6 +880,11 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } num = atoi(walker); csonpath_fill_walker_with_int(cjp, inst_idx, num); + if (in_union && *next == ',') { + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + walker = next; + goto do_array; + } walker = next + 1; to_check = *walker; goto again; @@ -825,6 +905,11 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], } ++next; // skipp end csonpath_push_char(cjp, 0, inst_idx); + if (in_union && *next == ',') { + walker = next; + csonpath_push_char(cjp, CSONPATH_INST_UNION_JMP, inst_idx); + goto do_array; + } if (*next != ']') CSONPATH_COMPILE_ERR(tmp, walker - orig, "']' require instead of '%c'\n", *next); @@ -878,7 +963,7 @@ static int csonpath_compile_do(struct csonpath *cjp, const char orig[static 1], ++walker; goto again; } - else if (*walker == end_path) { + else if ((end_path == 'u' && (*walker == ']' || *walker == ',')) || *walker == end_path) { csonpath_push_char(cjp, CSONPATH_INST_END, inst_idx); free(cjp->compile_error); cjp->compile_error = NULL; @@ -972,29 +1057,24 @@ static _Bool csonpath_make_match(const struct csonpath cjp[const static 1], const char **owalker, int operation); -static _Bool csonpath_is_endish_inst(int instruction) +static inline _Bool csonpath_is_endish_inst(int instruction) { return instruction == CSONPATH_INST_END || instruction == CSONPATH_INST_OR; } -/* helper use multiple times */ -#define CSONPATH_DO_GET_NOTFOUND_UPDATER(this_idx) \ - do { \ - int append_ret = 0; \ - if (to_check == CSONPATH_INST_GET_OBJ) { \ - tmp = CSONPATH_NEW_OBJECT(); \ - append_ret = CSONPATH_APPEND_AT(ctx, this_idx, tmp); \ - CSONPATH_DECREF(tmp); \ - } else { \ - tmp = CSONPATH_NEW_ARRAY(); \ - append_ret = CSONPATH_APPEND_AT(ctx, this_idx, tmp); \ - CSONPATH_DECREF(tmp); \ - } \ - if (append_ret < 0) return append_ret; \ - goto next_inst; \ - } while (0) +static const char *csonpath_skipp_union_jmp(const char *walker) +{ + int union_cnt = 0; + while (union_cnt || *walker != CSONPATH_INST_UNION_END) { + walker = csonpath_walker_next_inst(walker); + if (*walker == CSONPATH_INST_GET_UNION) + ++union_cnt; + } + return walker; +} + #define CSONPATH_GOTO_ON_RELOOP(where) \ nb_res += tret; if (need_reloop_in) goto where; @@ -1115,6 +1195,11 @@ need_reloop_in = 0; if (need_reloop_in){ goto range_again; }; \ }) +#define CSONPATH_DO_GET_UNION_POST ({ \ + if (tret < 0) return -1; \ + nb_res += tret; \ + }) + #define CSONPATH_DO_EXTRA_DECLATION , struct csonpath_child_info child_info, int *need_reloop #define CSONPATH_DO_EXTRA_ARGS_IN , (struct csonpath_child_info) {.type = CSONPATH_NONE}, NULL @@ -1146,12 +1231,25 @@ need_reloop_in = 0; /* update_or_create */ +#define CSONPATH_PRE_GET_ROOT \ + int to_check = walker[1]; \ + if (csonpath_is_endish_inst(to_check)) { \ + if (CSONPATH_IS_OBJ(origin) && CSONPATH_IS_OBJ(to_update)) { \ + return csonpath_sync_root_obj(origin, to_update); \ + } else if (CSONPATH_IS_ARRAY(origin) && CSONPATH_IS_ARRAY(to_update)) { \ + return csonpath_sync_root_array(origin, to_update); \ + } else { \ + CSONPATH_EXCEPTION("can't update root ($)\n"); \ + } \ + } + #define CSONPATH_DO_DECLARATION \ int nb_res = 0; #define CSONPATH_DO_RET_TYPE int #define CSONPATH_DO_FUNC_NAME update_or_create +#define CSONPATH_DO_GET_NOTFOUND(osef) goto next_inst; /* useless but make gcc shup up on warning */ /* * assuming tmp == value can only be true, @@ -1159,29 +1257,56 @@ need_reloop_in = 0; * otherwise CSONPATH_PRE_GET, is the part doing the buisness */ #define CSONPATH_DO_RETURN \ - if (tmp == value) { \ - *need_reloop = 1; \ - if (child_info->type == CSONPATH_INTEGER) \ - return CSONPATH_APPEND_AT(ctx, child_info->idx, to_update); \ - else \ - return CSONPATH_APPEND_AT(ctx, child_info->key, to_update); \ - return 1; \ - } \ - return 0; + if (child_info->type != CSONPATH_NONE) { \ + if (need_reloop) *need_reloop = 1; \ + if (child_info->type == CSONPATH_INTEGER) \ + return CSONPATH_APPEND_AT(ctx, child_info->idx, to_update); \ + else \ + return CSONPATH_APPEND_AT(ctx, child_info->key, to_update); \ + return 1; \ + } \ + return 0; -#define CSONPATH_DO_EXTRA_ARGS_FIND_ALL , to_update, NULL, need_reloop +#define CSONPATH_DO_EXTRA_ARGS_FIND_ALL , to_update, child_info, need_reloop #define CSONPATH_DO_EXTRA_ARGS_NEESTED , to_update, \ csonpath_child_info_set(&(struct csonpath_child_info ){}, tmp, (intptr_t)key_idx), &need_reloop_in #define CSONPATH_DO_EXTRA_ARGS , CSONPATH_JSON to_update -#define CSONPATH_DO_EXTRA_ARGS_IN , to_update, NULL, NULL +#define CSONPATH_DO_EXTRA_ARGS_IN , to_update, &(struct csonpath_child_info ){}, NULL #define CSONPATH_DO_EXTRA_DECLATION CSONPATH_DO_EXTRA_ARGS, struct csonpath_child_info *child_info, int *need_reloop #define CSONPATH_DO_FIND_ALL nb_res += tret; #define CSONPATH_DO_FILTER_FIND CSONPATH_GOTO_ON_RELOOP(filter_again) -#define CSONPATH_DO_FIND_ALL_PRE_LOOP int need_reloop_in = 0; +#define CSONPATH_NEW_GUESS_CNT() \ + ({ \ + const char *tmp_wal = csonpath_walker_next_inst(walker); \ + int is_array = 1; \ + for (; *tmp_wal && !csonpath_is_endish_inst(*tmp_wal); \ + tmp_wal = csonpath_walker_next_inst(walker)) { \ + if (*tmp_wal == CSONPATH_INST_GET_UNION) \ + tmp_wal = csonpath_skipp_union_jmp(tmp_wal); \ + if (*tmp_wal == CSONPATH_INST_GET_OBJ || \ + (*tmp_wal >= CSONPATH_INST_FILTER_KEY_EXIST && *tmp_wal <= CSONPATH_INST_FILTER_KEY_REG_EQ)) { \ + is_array = 0; \ + break; \ + } else if (*tmp_wal == CSONPATH_INST_GET_ARRAY_SMALL || *tmp_wal == CSONPATH_INST_GET_ARRAY_BIG) { \ + break; \ + } \ + } \ + CSONPATH_JSON r; \ + if (is_array) \ + r = CSONPATH_NEW_ARRAY(); \ + else \ + r = CSONPATH_NEW_OBJECT(); \ + r; \ + }) -#define CSONPATH_DO_FILTER_PRE_LOOP CSONPATH_PREPARE_RELOOP(filter_again) +#define CSONPATH_DO_FIND_ALL_PRE_LOOP int need_reloop_in = 0; \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_GUESS_CNT); + +#define CSONPATH_DO_FILTER_PRE_LOOP \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_OBJECT); \ + CSONPATH_PREPARE_RELOOP(filter_again) #define CSONPATH_DO_FIND_ALL_OUT return nb_res; @@ -1189,8 +1314,8 @@ static int csonpath_sync_root_array(CSONPATH_JSON parent, CSONPATH_JSON to_updat { CSONPATH_JSON child; size_t idx; - (void) idx; + (void) idx; CSONPATH_ARRAY_CLEAR(parent); CSONPATH_FOREACH_ARRAY(to_update, child, idx) { if (CSONPATH_APPEND_AT(parent, idx, child) < 0) @@ -1204,6 +1329,7 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) CSONPATH_JSON child; const char *key; + (void )key; CSONPATH_OBJ_CLEAR(parent); CSONPATH_FOREACH_OBJ(to_update, child, key) { if (CSONPATH_APPEND_AT(parent, key, child) < 0) @@ -1212,33 +1338,37 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) return 1; } -#define CSONPATH_PRE_GET_ROOT \ - int to_check = walker[1]; \ - if (to_check == CSONPATH_INST_END || to_check == CSONPATH_INST_OR) { \ - if (CSONPATH_IS_OBJ(origin) && CSONPATH_IS_OBJ(to_update)) { \ - return csonpath_sync_root_obj(origin, to_update); \ - } else if (CSONPATH_IS_ARRAY(origin) && CSONPATH_IS_ARRAY(to_update)) { \ - return csonpath_sync_root_array(origin, to_update); \ - } else { \ - CSONPATH_EXCEPTION("can't update root ($)\n"); \ - } \ - } -#define CSONPATH_PRE_GET(this_idx) \ - const char *check_at = walker; \ - int to_check; \ - do { \ - check_at = csonpath_walker_next_inst(check_at); \ - to_check = *check_at; \ - } while (to_check == CSONPATH_INST_GET_ALL || to_check == CSONPATH_INST_FIND_ALL); \ - if (to_check == CSONPATH_INST_END || to_check == CSONPATH_INST_OR) { \ - if (CSONPATH_APPEND_AT(ctx, this_idx, to_update) < 0) return -1; \ - return 1; \ +#define CSONPATH_UPDATE_CHECK_EXIST(mk_cnt) \ + if (tmp == CSONPATH_NULL) { \ + int append_ret = 0; \ + tmp = mk_cnt(); \ + if (child_info->type == CSONPATH_INTEGER) \ + append_ret = CSONPATH_APPEND_AT(ctx, child_info->idx, tmp); \ + else if (child_info->type == CSONPATH_STR) \ + append_ret = CSONPATH_APPEND_AT(ctx, child_info->key, tmp); \ + else \ + CSONPATH_GETTER_ERR("CSONPATH_NONE is nope\n"); \ + CSONPATH_DECREF(tmp); \ + if (append_ret < 0) return append_ret; \ + ctx = tmp; \ } +#define CSONPATH_PRE_GET_OBJ(this_idx) \ + if (tmp != CSONPATH_NULL && !CSONPATH_IS_OBJ(tmp)) { \ + CSONPATH_GETTER_ERR("Unable to follow path(%s): Dict expected", this_idx); \ + } \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_OBJECT); \ + csonpath_child_info_set(child_info, tmp, (intptr_t)this_idx); + + +#define CSONPATH_PRE_GET(this_idx) \ + if (tmp != CSONPATH_NULL && !CSONPATH_IS_ARRAY(tmp)) { \ + CSONPATH_GETTER_ERR("Unable to follow path(%d): List expected", this_idx); \ + } \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_ARRAY); \ + csonpath_child_info_set(child_info, tmp, this_idx); -#define CSONPATH_DO_GET_NOTFOUND(this_idx) \ - CSONPATH_DO_GET_NOTFOUND_UPDATER(this_idx) #include "csonpath_do.h" @@ -1305,20 +1435,27 @@ static int csonpath_sync_root_obj(CSONPATH_JSON parent, CSONPATH_JSON to_update) #define CSONPATH_PRE_GET_ROOT \ int to_check = *walker; \ - if (to_check == CSONPATH_INST_END || to_check == CSONPATH_INST_OR) { \ + if (csonpath_is_endish_inst(to_check)) { \ CSONPATH_GETTER_ERR("can't update root ($)\n"); \ return CSONPATH_NONE_FOUND_RET; \ } -#define CSONPATH_PRE_GET(this_idx) \ - int to_check = *walker; \ - csonpath_child_info_set(child_info, ctx, (intptr_t)this_idx) - +#define CSONPATH_PRE_GET_OBJ(this_idx) \ + if (tmp != CSONPATH_NULL && !CSONPATH_IS_OBJ(tmp)) { \ + CSONPATH_GETTER_ERR("Unable to follow path(%s): Dict expected", this_idx); \ + } \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_OBJECT); \ + csonpath_child_info_set(child_info, tmp, (intptr_t)this_idx); -#define CSONPATH_DO_GET_NOTFOUND(this_idx) \ - CSONPATH_DO_GET_NOTFOUND_UPDATER(this_idx) +#define CSONPATH_PRE_GET(this_idx) \ + if (tmp != CSONPATH_NULL && !CSONPATH_IS_ARRAY(tmp)) { \ + CSONPATH_GETTER_ERR("Unable to follow path(%d): List expected", this_idx); \ + } \ + CSONPATH_UPDATE_CHECK_EXIST(CSONPATH_NEW_ARRAY); \ + csonpath_child_info_set(child_info, tmp, this_idx); +#define CSONPATH_DO_GET_NOTFOUND(osef) goto next_inst; #include "csonpath_do.h" @@ -1344,38 +1481,50 @@ static _Bool csonpath_make_match(const struct csonpath cjp[const static 1], return 0; switch (operation) { case CSONPATH_INST_FILTER_KEY_NOT_EQ: - if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) + if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) { return CSONPATH_GET_NUM(el2) != CSONPATH_GET_NUM(jret); - if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) + } + if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) { return (!!strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret))); + } return 0; case CSONPATH_INST_FILTER_KEY_EQ: - if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) + if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) { return CSONPATH_GET_NUM(el2) == CSONPATH_GET_NUM(jret); - if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) + } + if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) { return (!strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret))); + } return 0; case CSONPATH_INST_FILTER_KEY_SUPERIOR_EQ: case CSONPATH_INST_FILTER_KEY_SUPERIOR: - if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) - if (operation == CSONPATH_INST_FILTER_KEY_SUPERIOR_EQ) + if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) { + if (operation == CSONPATH_INST_FILTER_KEY_SUPERIOR_EQ) { return CSONPATH_GET_NUM(el2) >= CSONPATH_GET_NUM(jret); + } return CSONPATH_GET_NUM(el2) > CSONPATH_GET_NUM(jret); - if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) - if (operation == CSONPATH_INST_FILTER_KEY_SUPERIOR_EQ) + } + if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) { + if (operation == CSONPATH_INST_FILTER_KEY_SUPERIOR_EQ) { return strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret)) >= 0; + } return strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret)) > 0; + } return 0; case CSONPATH_INST_FILTER_KEY_INFERIOR: case CSONPATH_INST_FILTER_KEY_INFERIOR_EQ: - if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) - if (operation == CSONPATH_INST_FILTER_KEY_INFERIOR_EQ) + if (CSONPATH_IS_NUM(el2) && CSONPATH_IS_NUM(jret)) { + if (operation == CSONPATH_INST_FILTER_KEY_INFERIOR_EQ) { return CSONPATH_GET_NUM(el2) <= CSONPATH_GET_NUM(jret); + } return CSONPATH_GET_NUM(el2) < CSONPATH_GET_NUM(jret); - if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) - if (operation == CSONPATH_INST_FILTER_KEY_INFERIOR_EQ) + } + if (CSONPATH_IS_STR(el2) && CSONPATH_IS_STR(jret)) { + if (operation == CSONPATH_INST_FILTER_KEY_INFERIOR_EQ) { return strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret)) <= 0; + } return strcmp(CSONPATH_GET_STR(el2), CSONPATH_GET_STR(jret)) < 0; + } return 0; default: return 0; diff --git a/csonpath_do.h b/csonpath_do.h index 51bacc9..0e904f2 100644 --- a/csonpath_do.h +++ b/csonpath_do.h @@ -100,10 +100,11 @@ #define CSONPATH_DO_RANGE CSONPATH_DO_FIND_ALL #endif -#ifndef CSONPATH_DO_RANGE -#define CSONPATH_DO_RANGE CSONPATH_DO_FIND_ALL +#ifndef CSONPATH_DO_GET_UNION_POST +#define CSONPATH_DO_GET_UNION_POST CSONPATH_DO_FIND_ALL #endif + #ifndef CSONPATH_DO_RANGE_PRE_LOOP #define CSONPATH_DO_RANGE_PRE_LOOP CSONPATH_DO_FIND_ALL_PRE_LOOP #endif @@ -139,6 +140,7 @@ static CSONPATH_DO_RET_TYPE csonpath_do_dotdot(const struct csonpath cjp[const s intptr_t key_idx; CSONPATH_DO_RET_TYPE tret = CSONPATH_NONE_FOUND_RET; const char *next_inst = NULL; + (void)ctx; CSONPATH_DO_FIND_ALL_PRE_LOOP; if (is_obj) { @@ -186,15 +188,22 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const (void)ctx; /* maybe unused */ - while (*walker != CSONPATH_INST_END && - *walker != CSONPATH_INST_OR) { + while (!csonpath_is_endish_inst(*walker)) { switch (*walker) { + case CSONPATH_INST_UNION_JMP: + { + walker = csonpath_skipp_union_jmp(walker); + } + /* fallthough */ + case CSONPATH_INST_UNION_END: + break; case CSONPATH_INST_ROOT: { CSONPATH_PRE_GET_ROOT value = origin; tmp = value; ctx = CSONPATH_NULL; + } break; case CSONPATH_INST_FILTER_KEY_EXIST: @@ -305,6 +314,37 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const CSONPATH_DO_GET_ALL_OUT; break; } + case CSONPATH_INST_GET_UNION: + { + intptr_t key_idx = 0; + int need_reloop_in = 0; + + (void)need_reloop_in; + (void)key_idx; + do { + ++walker; + int union_cnt = 0; + CSONPATH_DO_RET_TYPE tret = csonpath_do_internal(cjp, origin, tmp, CSONPATH_NULL, + walker + CSONPATH_DO_EXTRA_ARGS_NEESTED); + CSONPATH_DO_GET_UNION_POST; + while (1) { + walker = csonpath_walker_next_inst(walker); + if (*walker == CSONPATH_INST_GET_UNION) + ++union_cnt; + if (*walker == CSONPATH_INST_UNION_JMP && !union_cnt) + break; + if (*walker == CSONPATH_INST_UNION_END) { + if (union_cnt) + --union_cnt; + else + break; + } + } + } while (*walker != CSONPATH_INST_UNION_END); + CSONPATH_DO_GET_ALL_OUT; + break; + } case CSONPATH_INST_GET_ALL: { CSONPATH_JSON el; @@ -326,16 +366,20 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const case CSONPATH_INST_GET_SUBPATH: { const char *end_sentinel; - walker = csonpath_walker_next_inst(walker); + const char *owalker = walker; + owalker = csonpath_walker_next_inst(owalker); CSONPATH_JSON jret = csonpath_find_first_internal( - cjp, origin, origin, CSONPATH_NULL, walker, &end_sentinel); + cjp, origin, origin, CSONPATH_NULL, owalker, &end_sentinel); /* here it should point to inst_end, but walker is incr after */ - walker = end_sentinel; + owalker = end_sentinel; + if (tmp != CSONPATH_NULL) + ctx = tmp; if (CSONPATH_IS_NUM(jret)) { int this_idx = CSONPATH_GET_NUM(jret); CSONPATH_PRE_GET(this_idx); tmp = CSONPATH_AT(tmp, this_idx); if (tmp == CSONPATH_NULL) { + walker = owalker; CSONPATH_DO_GET_NOTFOUND(this_idx); } CSONPATH_DO_POST_FIND_ARRAY; @@ -344,19 +388,22 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const CSONPATH_PRE_GET_OBJ(this_idx); tmp = CSONPATH_GET(tmp, this_idx); if (tmp == CSONPATH_NULL) { + walker = owalker; CSONPATH_DO_GET_NOTFOUND(this_idx); } CSONPATH_DO_POST_FIND_OBJ; } else { CSONPATH_GETTER_ERR("GET_SUBPATH return need to be either number or str"); } + walker = owalker; break; } case CSONPATH_INST_GET_OBJ: { CSONPATH_UNUSED const char *this_idx = &walker[1]; - ctx = tmp; + if (tmp != CSONPATH_NULL) + ctx = tmp; CSONPATH_PRE_GET_OBJ(this_idx); ++walker; tmp = CSONPATH_GET(tmp, walker); @@ -371,7 +418,8 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const { int this_idx; - ctx = tmp; + if (tmp != CSONPATH_NULL) + ctx = tmp; this_idx = (int)walker[1]; CSONPATH_PRE_GET(this_idx); tmp = CSONPATH_AT(tmp, this_idx); @@ -383,7 +431,8 @@ static CSONPATH_DO_RET_TYPE csonpath_do_internal(const struct csonpath cjp[const break; } case CSONPATH_INST_GET_ARRAY_BIG: - ctx = tmp; + if (tmp != CSONPATH_NULL) + ctx = tmp; { CSONPATH_UNUSED int this_idx; union {int n; char c[4];} to_num = @@ -468,5 +517,6 @@ static CSONPATH_DO_RET_TYPE csonpath_do_(struct csonpath cjp[static 1], #undef CSONPATH_DO_RANGE #undef CSONPATH_DO_RANGE_PRE_LOOP #undef CSONPATH_PRE_GET_OBJ +#undef CSONPATH_DO_GET_UNION_POST #endif diff --git a/csonpath_python.c b/csonpath_python.c index dc7df4c..3306003 100644 --- a/csonpath_python.c +++ b/csonpath_python.c @@ -98,7 +98,13 @@ static int pydict_try_setitemstring(PyObject *obj, const char * const at, PyObject *el) { if (!PyDict_Check(obj)) { - PyErr_Format(PyExc_ValueError, "Unable to follow path (%s): Dict expected", at); + PyObject* repr = PyObject_Repr(obj); + PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~"); + const char *bytes = PyBytes_AS_STRING(str); + + PyErr_Format(PyExc_ValueError, "Unable to follow path (%s) in %s: Dict expected", at, bytes); + Py_XDECREF(repr); + Py_XDECREF(str); return -1; } PyDict_SetItemString(obj, at, el); @@ -217,8 +223,6 @@ typedef struct { struct csonpath *cp; } PyCsonPathObject; -#define CAPSULE_NAME "csonpath_capsule" - #define BAD_ARG() ({fprintf(stderr, "bad argument\n"); PyErr_BadArgument(); return NULL;}) diff --git a/csonpath_yyjson.h b/csonpath_yyjson.h index cb0d5de..4e37643 100644 --- a/csonpath_yyjson.h +++ b/csonpath_yyjson.h @@ -70,31 +70,34 @@ typedef void (*yyjson_val_callback)(yyjson_val *, struct csonpath_child_info *, CSONPATH_FOREACH_EXT(obj, el, ({code}), key_idx) #define CSONPATH_FOREACH_ARRAY(obj, el, key_idx_) \ - size_t max_; \ + intptr_t max_; \ yyjson_arr_foreach(obj, key_idx_, max_, el) #define CSONPATH_FOREACH_OBJ(obj, val, key) \ - size_t idx_, max_; \ - yyjson_val *key_; \ - for ((idx_) = 0, \ - (max_) = yyjson_obj_size(obj), \ - (key_) = (obj) ? unsafe_yyjson_get_first(obj) : NULL, \ - (val) = (key_) + 1; \ - ({int r = (idx_) < (max_); if (r) key = yyjson_get_str(key_); r; }); \ - (idx_)++, \ - (key_) = unsafe_yyjson_get_next(val), \ - (val) = (key_) + 1) + intptr_t idx_, max_; \ + yyjson_val *key_; \ + for ((idx_) = 0, \ + (max_) = yyjson_obj_size(obj), \ + (key_) = (obj) ? unsafe_yyjson_get_first(obj) : NULL, \ + (val) = (key_) + 1; \ + ({int r = (idx_) < (max_); if (r) key = yyjson_get_str(key_); r; }); \ + (idx_)++, \ + (key_) = unsafe_yyjson_get_next(val), \ + (val) = (key_) + 1) #define CSONPATH_FOREACH_EXT(obj, el, code, key_idx_) \ - if (yyjson_get_type(obj) == YYJSON_TYPE_ARR) { \ - size_t key_idx_, max_; \ + if (yyjson_get_type(obj) == YYJSON_TYPE_ARR) { \ + intptr_t key_idx_, max_; \ yyjson_val *val; \ + (void)val; \ yyjson_arr_foreach(obj, key_idx_, max_, el) { \ (void) key_idx_; code; \ } \ } else if (yyjson_get_type(obj) == YYJSON_TYPE_OBJ) { \ - size_t idx_, max_; \ + intptr_t idx_, max_; \ yyjson_val *key, *val; \ + (void)val; \ + (void)key; \ yyjson_obj_foreach(obj, idx_, max_, key, el) { \ const char *key_idx_ = yyjson_get_str(key); \ (void) key_idx_; code; \ diff --git a/pyproject.toml b/pyproject.toml index 8ac0197..c005f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "csonpath" -version = "0.14.0" +version = "0.15.0" readme = "README.md" [tool.setuptools] @@ -27,7 +27,7 @@ test-command = "pytest {project}/tests/python" before-build = "choco install mingw -y" [tool.bumpversion] -current_version = "0.14.0" +current_version = "0.15.0" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] search = "{current_version}" diff --git a/setup.py b/setup.py index 320a3f6..c4c60e2 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,6 @@ import os +import sys +import sysconfig import platform from setuptools import setup, Extension from setuptools.command.build_ext import build_ext @@ -12,6 +14,33 @@ def finalize_options(self): self.compiler = "mingw32" super().finalize_options() + def get_libraries(self, ext): + libraries = build_ext.get_libraries(self, ext) + # distutils/setuptools hardcodes the Python library name as pythonXY + # for mingw32, but free-threading builds (3.13+) on Windows ship + # pythonXYt.lib instead of pythonXY.lib. + # We only "fix" this for 3.14+ because 3.13t is considered + # experimental and we prefer not to link against it. + if ( + getattr(self.compiler, "compiler_type", None) == "mingw32" + and platform.system() == "Windows" + and (sys.version_info.major, sys.version_info.minor) >= (3, 14) + ): + version_nodot = f"{sys.version_info.major}{sys.version_info.minor}" + expected = f"python{version_nodot}" + if expected in libraries: + for prefix in (sys.base_prefix, sysconfig.get_config_var("prefix")): + if not prefix: + continue + libs_dir = os.path.join(prefix, "libs") + if os.path.exists(os.path.join(libs_dir, f"{expected}t.lib")): + libraries = [ + f"{expected}t" if lib == expected else lib + for lib in libraries + ] + break + return libraries + # Base sources sources = ["csonpath_python.c"] diff --git a/tests/json-c/union.c b/tests/json-c/union.c new file mode 100644 index 0000000..6461182 --- /dev/null +++ b/tests/json-c/union.c @@ -0,0 +1,149 @@ +#include +#include +#include +#include "csonpath_json-c.h" + +const char *json_str = "{" + "\"a\": \"val_a\"," + " \"b\": \"val_b\"," + " \"c\": { \"d\": \"val_cd\" }," + " \"d\": { \"d\": \"val_de\" }," + " \"array\": [0, \"ah\", \"oh\", {\"x\": 1, \"y\": 2}]," + " \"ar2\": [0, \"bh\", \"pp\", {\"x\": 1, \"y\": 2}]," + " \"items\": [{\"n\": 1}, {\"n\": 2}, {\"n\": 3}]" + "}"; + +void update_callback(json_object *parent, struct csonpath_child_info *c_nfo, + json_object *current, void *data) +{ + (void)parent; (void)c_nfo; + json_object_set_string(current, data); +} + +int main(void) +{ + struct csonpath *p; + struct json_object *jobj = json_tokener_parse(json_str); + struct json_object *ret; + int iret; + + /* 1. union de deux clés quoted : $['a','b'] -> find_first retourne 'a' */ + assert((p = csonpath_new("$['a','b']"))); + ret = csonpath_find_first(p, jobj); + assert(ret); + assert(!strcmp(json_object_get_string(ret), "val_a")); + + /* 2. union d'indices numériques : $[array][0,1] -> find_first retourne 0 */ + assert((p = csonpath_set_path(p, "$.array[0,1]"))); + ret = csonpath_find_first(p, jobj); + assert(ret); + assert(json_object_get_int(ret) == 0); + + /* 3. union d'indices avec find_all -> 2 éléments */ + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_is_type(ret, json_type_array)); + assert(json_object_array_length(ret) == 2); + assert(json_object_get_int(json_object_array_get_idx(ret, 0)) == 0); + assert(!strcmp(json_object_get_string(json_object_array_get_idx(ret, 1)), "ah")); + json_object_put(ret); + + /* 4. union de clés + continuation .d : $['a','c'].d -> c.d == "val_cd" */ + assert((p = csonpath_set_path(p, "$['c','a'].d"))); + ret = csonpath_find_first(p, jobj); + assert(ret); + assert(!strcmp(json_object_get_string(ret), "val_cd")); + + /* 5. union avec wildcard : $[*, 'a'] -> find_first sur root obj */ + assert((p = csonpath_set_path(p, "$['a','b']"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_array_length(ret) == 2); + json_object_put(ret); + + /* 6. union de 3 éléments quoted : $['a','b','c'] -> 3 résultats en find_all */ + assert((p = csonpath_set_path(p, "$['a','b','c']"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_array_length(ret) == 3); + json_object_put(ret); + + /* 7. union barewords : $[a,b] -> find_first == "val_a" */ + assert((p = csonpath_set_path(p, "$['c','d'].d"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(!strcmp(json_object_get_string(json_object_array_get_idx(ret, 0)), "val_cd")); + assert(!strcmp(json_object_get_string(json_object_array_get_idx(ret, 1)), "val_de")); + json_object_put(ret); + + /* 8. union qui inclut un élément inexistant : $['a','z'] -> existe quand même */ + ret = csonpath_find_all(p, jobj); + assert(ret); + /* la longueur dépend de si l'implémente filtre les null ; on check juste que ça crash pas */ + json_object_put(ret); + + /* 9. update_or_create sur union -> doit updater au moins un élément */ + assert((p = csonpath_set_path(p, "$['a','z']"))); + iret = csonpath_update_or_create(p, jobj, json_object_new_string("modified")); + assert(iret >= 1); + + /* 10. union sur sous-champs d'array : $.array[*].['x','y'] -> find_all */ + assert((p = csonpath_set_path(p, "$.array[*].['x','y']"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_is_type(ret, json_type_array)); + /* 2 clés sur 1 objet matchant = 2 résultats */ + assert(json_object_array_length(ret) == 2); + json_object_put(ret); + + /* 11. union avec callback via update_or_create_callback */ + assert((p = csonpath_set_path(p, "$['a','b']"))); + iret = csonpath_update_or_create_callback(p, jobj, update_callback, "cb_val"); + assert(iret >= 1); + assert((p = csonpath_set_path(p, "$.a"))); + ret = csonpath_find_first(p, jobj); + assert(ret); + assert(!strcmp(json_object_get_string(ret), "cb_val")); + + /* 12. remove sur union -> supprime plusieurs clés */ + assert((p = csonpath_set_path(p, "$['a','b']"))); + iret = csonpath_remove(p, jobj); + assert(iret == 2); + assert(!json_object_object_get(jobj, "a")); + assert(!json_object_object_get(jobj, "b")); + + /* 13. union sur array indices avec continuation : $.items[0,1].n -> [1,2] */ + assert((p = csonpath_set_path(p, "$.items[0,1].n"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_array_length(ret) == 2); + assert(json_object_get_int(json_object_array_get_idx(ret, 0)) == 1); + assert(json_object_get_int(json_object_array_get_idx(ret, 1)) == 2); + json_object_put(ret); + + /* 14. union de filter expressions : $.items[?n==1, ?n==2] -> 2 éléments */ + assert((p = csonpath_set_path(p, "$.items[?n==1, ?n==2]"))); + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_is_type(ret, json_type_array)); + assert(json_object_array_length(ret) == 2); + json_object_put(ret); + + /* 15. union sur éléments d'un tableau avec find_first : $[array][2,1] -> "ah" (élément 1) */ + assert((p = csonpath_set_path(p, "$.array[2,1]"))); + ret = csonpath_find_first(p, jobj); + assert(ret); + assert(!strcmp(json_object_get_string(ret), "oh")); + + ret = csonpath_find_all(p, jobj); + assert(ret); + assert(json_object_is_type(ret, json_type_array)); + assert(!strcmp(json_object_get_string(json_object_array_get_idx(ret, 0)), "oh")); + assert(!strcmp(json_object_get_string(json_object_array_get_idx(ret, 1)), "ah")); + json_object_put(ret); + + json_object_put(jobj); + csonpath_destroy(p); + + return 0; +} diff --git a/tests/python/test_refcount.py b/tests/python/test_refcount.py index 5d5f4e0..a2ded07 100644 --- a/tests/python/test_refcount.py +++ b/tests/python/test_refcount.py @@ -43,7 +43,7 @@ def test_update_or_create_refcount_insert_twice(): def test_update_or_create_refcount_replaces_and_releases_old(): - old_value = "old-------" + old_value = ["old-------"] new_value = ["new"] d = {"a": old_value} cp = csonpath.CsonPath("$.a") diff --git a/tests/python/test_subpath.py b/tests/python/test_subpath.py new file mode 100644 index 0000000..7f0245e --- /dev/null +++ b/tests/python/test_subpath.py @@ -0,0 +1,28 @@ +import pytest +import csonpath +import sys + +def test_update_or_create_subpath_index_empty_array(): + """Subpath index on empty array should create the element, not overwrite the index key.""" + d = {"metrics": [], "_idx": 0} + p = csonpath.CsonPath("$.metrics[$._idx].name") + p.update_or_create(d, "hello") + assert d == {"metrics": [{"name": "hello"}], "_idx": 0} + + +def test_update_or_create_subpath_index_nonempty_array(): + """Subpath index on non-empty array works correctly.""" + d = {"metrics": [{}], "_idx": 0} + p = csonpath.CsonPath("$.metrics[$._idx].name") + p.update_or_create(d, "hello") + assert d == {"metrics": [{"name": "hello"}], "_idx": 0} + +def test_update_or_create_callback_subpath_index_empty_array(): + """update_or_create_callback with subpath index on empty array should create parents.""" + d = {"metrics": [], "_idx": 0} + p = csonpath.CsonPath("$.metrics[$._idx].name") + + def cb(parent, idx, cur, ud): + parent[idx] = ud + p.update_or_create_callback(d, cb, "hello") + assert d == {"metrics": [{"name": "hello"}], "_idx": 0} diff --git a/tests/yyjson/test-yyjson.c b/tests/yyjson/test-yyjson.c new file mode 100644 index 0000000..bcc88d2 --- /dev/null +++ b/tests/yyjson/test-yyjson.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include "csonpath_yyjson.h" + +const char *json_str = "{\"a\":\"x\",\"b\":{\"B\":\"y\"},\"array\":[0,\"ah\",\"oh\"],\"items\":[{\"name\":\"A\",\"price\":10},{\"name\":\"B\",\"price\":50}],\"key\":\"a\",\"threshold\":20}"; + +int main(void) +{ + yyjson_doc *doc = yyjson_read(json_str, strlen(json_str), 0); + yyjson_val *root = yyjson_doc_get_root(doc); + yyjson_val *v; + struct find_all_ret *r; + struct csonpath *p = csonpath_new("$.a"); + + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "x")); + + p = csonpath_set_path(p, "$.b.B"); + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "y")); + + p = csonpath_set_path(p, "$['a']"); + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "x")); + + p = csonpath_set_path(p, "$.array[1]"); + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "ah")); + + p = csonpath_set_path(p, "$.array[*]"); + r = csonpath_find_all(p, root); + assert(r->i == 3); + free_find_all(r); + + p = csonpath_set_path(p, "$..name"); + r = csonpath_find_all(p, root); + assert(r->i == 2); + free_find_all(r); + + p = csonpath_set_path(p, "$.z|$.a"); + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "x")); + + p = csonpath_set_path(p, "$['a','b']"); + r = csonpath_find_all(p, root); + assert(r->i == 2); + free_find_all(r); + + p = csonpath_set_path(p, "$.items[?price>20]"); + r = csonpath_find_all(p, root); + assert(r->i == 1); + free_find_all(r); + + p = csonpath_set_path(p, "$.items[?name=\"A\"]"); + v = csonpath_find_first(p, root); + assert(v && yyjson_get_type(v) == YYJSON_TYPE_OBJ); + + p = csonpath_set_path(p, "$.items[?name=~\"A.*\"]"); + r = csonpath_find_all(p, root); + assert(r->i == 1); + free_find_all(r); + + p = csonpath_set_path(p, "$.items[?price>$.threshold]"); + r = csonpath_find_all(p, root); + assert(r->i == 1); + free_find_all(r); + + p = csonpath_set_path(p, "$[$.key]"); + v = csonpath_find_first(p, root); + assert(v && !strcmp(yyjson_get_str(v), "x")); + + p = csonpath_set_path(p, "$.items[?(@.name)]"); + r = csonpath_find_all(p, root); + assert(r->i == 2); + free_find_all(r); + + p = csonpath_set_path(p, "$.array[0,1]"); + r = csonpath_find_all(p, root); + assert(r->i == 2); + free_find_all(r); + + csonpath_destroy(p); + yyjson_doc_free(doc); + return 0; +}