diff --git a/docs/cli.md b/docs/cli.md index 16488dc..5c6e6d6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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. @@ -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: \ No newline at end of file +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 +``` \ No newline at end of file diff --git a/jsonpath/cli.py b/jsonpath/cli.py index 6b2a620..26cf6a9 100644 --- a/jsonpath/cli.py +++ b/jsonpath/cli.py @@ -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, diff --git a/jsonpath/pointer.py b/jsonpath/pointer.py index 4defde0..6557519 100644 --- a/jsonpath/pointer.py +++ b/jsonpath/pointer.py @@ -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:] diff --git a/tests/test_cli.py b/tests/test_cli.py index 1d29eca..4c433ca 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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: @@ -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) @@ -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,