Skip to content

Commit

Permalink
CLI docs for pointer and patch.
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Jul 14, 2023
1 parent 0f12e60 commit 2b88477
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 43 deletions.
124 changes: 120 additions & 4 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ One of the subcommands `path`, `pointer` or `patch` must be specified, depending

Find objects in a JSON document given a JSONPath. One of `-q`/`--query` or `-r`/`--path-file` must be given. `-q` being a JSONPath given on the command line as a string, `-r` being the path to a file containing a JSONPath.

```
json path [-h] (-q QUERY | -r PATH_FILE) [-f FILE] [-o OUTPUT]
```

#### `-q` / `--query`

The JSONPath as a string.
Expand Down Expand Up @@ -172,17 +176,129 @@ $ json path -q "$.price_cap" --file /tmp/source.json
The path to a file to write resulting objects to, as a JSON array. If omitted or a hyphen (`-`) is given, results will be written to the standard output stream.

```console
$ json path -q "$.price_cap" -f /tmp/source.json -o /result.json
$ json path -q "$.price_cap" -f /tmp/source.json -o result.json
```

```console
$ json path -q "$.price_cap" -f /tmp/source.json --output /result.json
$ json path -q "$.price_cap" -f /tmp/source.json --output result.json
```

### `pointer`

TODO:
Resolve a JSON Pointer against a JSON document. One of `-p`/`--pointer` or `-r`/`--pointer-file` must be given. `-p` being a JSON Pointer given on the command line as a string, `-r` being the path to a file containing a JSON Pointer.

```
json pointer [-h] (-p POINTER | -r POINTER_FILE) [-f FILE] [-o OUTPUT] [-u]
```

#### `-p` / `--pointer`

An RFC 6901 formatted JSON Pointer string.

```console
$ json pointer -p "/categories/0/name" -f /tmp/source.json
```

```console
$ json pointer --pointer "/categories/0/name" -f /tmp/source.json
```

#### `-r` / `--pointer-file`

The path to a file containing a JSON Pointer.

```console
$ json pointer -r /tmp/pointer.txt -f /tmp/source.json
```

```console
$ json pointer --pointer-file /tmp/pointer.txt -f /tmp/source.json
```

#### `-f` / `--file`

The path to a file containing the target JSON document. If omitted or a hyphen (`-`), the target JSON document will be read from the standard input stream.

```console
$ json pointer -p "/categories/0/name" -f /tmp/source.json
```

```console
$ json pointer -p "/categories/0/name" --file /tmp/source.json
```

#### `-o` / `--output`

The path to a file to write the resulting object to. If omitted or a hyphen (`-`) is given, results will be written to the standard output stream.

```console
$ json pointer -p "/categories/0/name" -f /tmp/source.json -o result.json
```
```console
$ json pointer -p "/categories/0/name" -f /tmp/source.json --output result.json
```

#### `-u` / `--uri-decode`

Enable URI decoding of the JSON Pointer. In this example, we would look for a property called "hello world" in the root of the target document.

```console
$ json pointer -p "/hello%20world" -f /tmp/source.json -u
```

```console
$ json pointer -p "/hello%20world" -f /tmp/source.json --uri-decode
```

### `patch`

TODO:
Apply a JSON Patch to a JSON document. Unlike `path` and `pointer` commands, a patch can't be given as a string argument. `PATCH` is a positional argument that should be a file path to a JSON Patch document or a hyphen (`-`), which means the patch document will be read from the standard input stream.

```
json patch [-h] [-f FILE] [-o OUTPUT] [-u] PATCH
```

These examples read the patch from `patch.json` and the document to modify from `target.json`

```console
$ json patch /tmp/patch.json -f /tmp/target.json
```

```console
$ cat /tmp/patch.json | json patch - -f /tmp/target.json
```

#### `-f` / `--file`

The path to a file containing the target JSON document. If omitted or a hyphen (`-`), the target JSON document will be read from the standard input stream.

```console
$ json patch /tmp/patch.json -f /tmp/target.json
```

```console
$ json patch /tmp/patch.json --file /tmp/target.json
```

#### `-o` / `--output`

The path to a file to write the resulting object to. If omitted or a hyphen (`-`) is given, results will be written to the standard output stream.

```console
$ json patch /tmp/patch.json -f /tmp/target.json -o result.json
```
```console
$ json patch /tmp/patch.json -f /tmp/target.json --output result.json
```

#### `-u` / `--uri-decode`

Enable URI decoding of JSON Pointers in the patch document.

```console
$ json patch /tmp/patch.json -f /tmp/target.json -u
```

```console
$ json patch /tmp/patch.json -f /tmp/target.json --uri-decode
```
8 changes: 6 additions & 2 deletions jsonpath/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,13 @@ def handle_path_command(args: argparse.Namespace) -> None: # noqa: PLR0912

def handle_pointer_command(args: argparse.Namespace) -> None:
"""Handle the `pointer` sub command."""
pointer = args.pointer or args.pointer_file.read()
# Empty string is OK.
if args.pointer is not None:
pointer = args.pointer
else:
# TODO: is a property with a trailing newline OK?
pointer = args.pointer_file.read().strip()

# TODO: default value or exist with non-zero
try:
match = jsonpath.pointer.resolve(
pointer,
Expand Down
2 changes: 2 additions & 0 deletions jsonpath/pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ def _parse(
.decode("utf-16")
)

# TODO: lstrip pointer
# TODO: handle pointer without leading slash and not empty string
return tuple(
self._index(p.replace("~1", "/").replace("~0", "~")) for p in s.split("/")
)[1:]
Expand Down
109 changes: 72 additions & 37 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,42 @@
from jsonpath.exceptions import JSONPointerResolutionError
from jsonpath.patch import JSONPatch

SAMPLE_DATA = {
"categories": [
{
"name": "footwear",
"products": [
{
"title": "Trainers",
"description": "Fashionable trainers.",
"price": 89.99,
},
{
"title": "Barefoot Trainers",
"description": "Running trainers.",
"price": 130.00,
},
],
},
{
"name": "headwear",
"products": [
{
"title": "Cap",
"description": "Baseball cap",
"price": 15.00,
},
{
"title": "Beanie",
"description": "Winter running hat.",
"price": 9.00,
},
],
},
],
"price_cap": 10,
}


@pytest.fixture()
def parser() -> argparse.ArgumentParser:
Expand All @@ -39,45 +75,9 @@ def outfile(tmp_path: pathlib.Path) -> str:

@pytest.fixture()
def sample_target(tmp_path: pathlib.Path) -> str:
sample_data = {
"categories": [
{
"name": "footwear",
"products": [
{
"title": "Trainers",
"description": "Fashionable trainers.",
"price": 89.99,
},
{
"title": "Barefoot Trainers",
"description": "Running trainers.",
"price": 130.00,
},
],
},
{
"name": "headwear",
"products": [
{
"title": "Cap",
"description": "Baseball cap",
"price": 15.00,
},
{
"title": "Beanie",
"description": "Winter running hat.",
"price": 9.00,
},
],
},
],
"price_cap": 10,
}

target_path = tmp_path / "source.json"
with open(target_path, "w") as fd:
json.dump(sample_data, fd)
json.dump(SAMPLE_DATA, fd)
return str(target_path)


Expand Down Expand Up @@ -320,6 +320,41 @@ def test_json_pointer(
assert json.load(fd) == "footwear"


def test_json_pointer_empty_string(
parser: argparse.ArgumentParser, sample_target: str, outfile: str
) -> None:
"""Test an empty JSON Pointer is valid."""
args = parser.parse_args(["pointer", "-p", "", "-f", sample_target, "-o", outfile])

handle_pointer_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert json.load(fd) == SAMPLE_DATA


def test_read_pointer_from_file(
parser: argparse.ArgumentParser,
sample_target: str,
outfile: str,
tmp_path: pathlib.Path,
) -> None:
"""Test an empty JSON Pointer is valid."""
pointer_file_path = tmp_path / "pointer.txt"
with pointer_file_path.open("w") as fd:
fd.write("/price_cap")

args = parser.parse_args(
["pointer", "-r", str(pointer_file_path), "-f", sample_target, "-o", outfile]
)

handle_pointer_command(args)
args.output.flush()

with open(outfile, "r") as fd:
assert json.load(fd) == SAMPLE_DATA["price_cap"]


def test_patch_command_invalid_patch(
parser: argparse.ArgumentParser,
sample_target: str,
Expand Down

0 comments on commit 2b88477

Please sign in to comment.