From 584c964b6e847979f8e565bea3b562307082e377 Mon Sep 17 00:00:00 2001 From: f3ath Date: Thu, 30 May 2024 21:07:30 -0700 Subject: [PATCH] New functions --- CHANGELOG.md | 5 +++ README.md | 20 +++++++++- lib/fun_extra.dart | 2 + lib/src/expression/nodes.dart | 2 + lib/src/fun/extra/index.dart | 15 ++++++++ lib/src/fun/extra/is_array.dart | 2 +- lib/src/fun/extra/is_boolean.dart | 2 +- lib/src/fun/extra/is_number.dart | 2 +- lib/src/fun/extra/is_object.dart | 2 +- lib/src/fun/extra/is_string.dart | 2 +- lib/src/fun/extra/key.dart | 15 ++++++++ lib/src/fun/fun_factory.dart | 26 +++++++++---- pubspec.yaml | 2 +- test/cases/extra/cases.json | 64 ------------------------------- test/cases/extra/count.json | 10 +++++ test/cases/extra/index.json | 21 ++++++++++ test/cases/extra/is_array.json | 11 ++++++ test/cases/extra/is_boolean.json | 10 +++++ test/cases/extra/is_number.json | 10 +++++ test/cases/extra/is_object.json | 10 +++++ test/cases/extra/is_string.json | 10 +++++ test/cases/extra/key.json | 27 +++++++++++++ test/cases/extra/reverse.json | 16 ++++++++ test/cases/extra/xor.json | 16 ++++++++ test/cases_test.dart | 2 + 25 files changed, 225 insertions(+), 79 deletions(-) create mode 100644 lib/src/fun/extra/index.dart create mode 100644 lib/src/fun/extra/key.dart delete mode 100644 test/cases/extra/cases.json create mode 100644 test/cases/extra/count.json create mode 100644 test/cases/extra/index.json create mode 100644 test/cases/extra/is_array.json create mode 100644 test/cases/extra/is_boolean.json create mode 100644 test/cases/extra/is_number.json create mode 100644 test/cases/extra/is_object.json create mode 100644 test/cases/extra/is_string.json create mode 100644 test/cases/extra/key.json create mode 100644 test/cases/extra/reverse.json create mode 100644 test/cases/extra/xor.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 39bad1a..b822c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.2] - 2024-05-30 +### Added +- New functions: `key()` and `index()` + ## [0.7.1] - 2024-03-02 ### Changed - Bumped the CTS to the latest @@ -185,6 +189,7 @@ Previously, no modification would be made and no errors/exceptions thrown. ### Added - Basic design draft +[0.7.2]: https://github.com/f3ath/jessie/compare/0.7.1...0.7.2 [0.7.1]: https://github.com/f3ath/jessie/compare/0.7.0...0.7.1 [0.7.0]: https://github.com/f3ath/jessie/compare/0.6.6...0.7.0 [0.6.6]: https://github.com/f3ath/jessie/compare/0.6.5...0.6.6 diff --git a/README.md b/README.md index 949306a..1995003 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,26 @@ To use it: For more details see the included example. This package comes with some non-standard functions which you might find useful. -To use them, import `package:json_path/fun_extra.dart`. +- `count()` - returns the number of nodes selected by the argument +- `index()` - returns the index under which the array element is referenced by the parent array +- `key()` - returns the key under which the object element is referenced by the parent object +- `is_array()` - returns true if the value is an array +- `is_boolean()` - returns true if the value is a boolean +- `is_number()` - returns true if the value is a number +- `is_object()` - returns true if the value is an object +- `is_string()` - returns true if the value is a string +- `reverse()` - reverses the string +- `siblings()` - returns the siblings for the nodes +- `xor(, )` - returns the XOR of two booleans arguments +To use them, import `package:json_path/fun_extra.dart` and supply them to the `JsonPath()` constructor: + +```dart +final jsonPath = JsonPathParser(functions: [ + const Key(), + const Reverse(), +]).parse(r'$[?key(@) == reverse(key(@))]'); +``` ## References - [Standard development](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base) - [Feature comparison matrix](https://cburgmer.github.io/json-path-comparison/) diff --git a/lib/fun_extra.dart b/lib/fun_extra.dart index 11a85a3..1eb3133 100644 --- a/lib/fun_extra.dart +++ b/lib/fun_extra.dart @@ -1,11 +1,13 @@ /// A collection of semi-useful non-standard functions for JSONPath. library fun_extra; +export 'package:json_path/src/fun/extra/index.dart'; export 'package:json_path/src/fun/extra/is_array.dart'; export 'package:json_path/src/fun/extra/is_boolean.dart'; export 'package:json_path/src/fun/extra/is_number.dart'; export 'package:json_path/src/fun/extra/is_object.dart'; export 'package:json_path/src/fun/extra/is_string.dart'; +export 'package:json_path/src/fun/extra/key.dart'; export 'package:json_path/src/fun/extra/reverse.dart'; export 'package:json_path/src/fun/extra/siblings.dart'; export 'package:json_path/src/fun/extra/xor.dart'; diff --git a/lib/src/expression/nodes.dart b/lib/src/expression/nodes.dart index 45c3059..f6b8722 100644 --- a/lib/src/expression/nodes.dart +++ b/lib/src/expression/nodes.dart @@ -11,6 +11,8 @@ class SingularNodeList with NodeList { final NodeList _nodes; + Node? get node => length == 1 ? first : null; + @override Iterator> get iterator => _nodes.iterator; } diff --git a/lib/src/fun/extra/index.dart b/lib/src/fun/extra/index.dart new file mode 100644 index 0000000..f549e8b --- /dev/null +++ b/lib/src/fun/extra/index.dart @@ -0,0 +1,15 @@ +import 'package:json_path/fun_sdk.dart'; + +/// Returns the index under which the node referenced by the argument +/// is found in the parent array. +/// If the parent is not an array, returns [Nothing]. +/// If the argument does not reference a single node, returns [Nothing]. +class Index implements Fun1 { + const Index(); + + @override + final name = 'index'; + + @override + Maybe call(SingularNodeList nodes) => Just(nodes.node?.index).type(); +} diff --git a/lib/src/fun/extra/is_array.dart b/lib/src/fun/extra/is_array.dart index 77800cf..4055627 100644 --- a/lib/src/fun/extra/is_array.dart +++ b/lib/src/fun/extra/is_array.dart @@ -1,6 +1,6 @@ import 'package:json_path/fun_sdk.dart'; -/// Checks if the value is a JSON array. +/// Returns true if the value is a JSON array. class IsArray implements Fun1 { const IsArray(); diff --git a/lib/src/fun/extra/is_boolean.dart b/lib/src/fun/extra/is_boolean.dart index dc88400..e37c088 100644 --- a/lib/src/fun/extra/is_boolean.dart +++ b/lib/src/fun/extra/is_boolean.dart @@ -1,6 +1,6 @@ import 'package:json_path/fun_sdk.dart'; -/// Checks if the value is a JSON boolean. +/// Returns true if the value is a JSON boolean. class IsBoolean implements Fun1 { const IsBoolean(); diff --git a/lib/src/fun/extra/is_number.dart b/lib/src/fun/extra/is_number.dart index 4725d79..4d45927 100644 --- a/lib/src/fun/extra/is_number.dart +++ b/lib/src/fun/extra/is_number.dart @@ -1,6 +1,6 @@ import 'package:json_path/fun_sdk.dart'; -/// Checks if the value is a JSON number. +/// Returns true if the value is a JSON number. class IsNumber implements Fun1 { const IsNumber(); diff --git a/lib/src/fun/extra/is_object.dart b/lib/src/fun/extra/is_object.dart index c758653..9ea83e7 100644 --- a/lib/src/fun/extra/is_object.dart +++ b/lib/src/fun/extra/is_object.dart @@ -1,6 +1,6 @@ import 'package:json_path/fun_sdk.dart'; -/// Checks if the value is a JSON object. +/// Returns true if the value is a JSON object. class IsObject implements Fun1 { const IsObject(); diff --git a/lib/src/fun/extra/is_string.dart b/lib/src/fun/extra/is_string.dart index 0713a00..6a13189 100644 --- a/lib/src/fun/extra/is_string.dart +++ b/lib/src/fun/extra/is_string.dart @@ -1,6 +1,6 @@ import 'package:json_path/fun_sdk.dart'; -/// Checks if the value is a JSON string. +/// Returns true if the value is a JSON string. class IsString implements Fun1 { const IsString(); diff --git a/lib/src/fun/extra/key.dart b/lib/src/fun/extra/key.dart new file mode 100644 index 0000000..9d2fb2f --- /dev/null +++ b/lib/src/fun/extra/key.dart @@ -0,0 +1,15 @@ +import 'package:json_path/fun_sdk.dart'; + +/// Returns the key under which the node referenced by the argument +/// is found in the parent object. +/// If the parent is not an object, returns [Nothing]. +/// If the argument does not reference a single node, returns [Nothing]. +class Key implements Fun1 { + const Key(); + + @override + final name = 'key'; + + @override + Maybe call(SingularNodeList nodes) => Just(nodes.node?.key).type(); +} diff --git a/lib/src/fun/fun_factory.dart b/lib/src/fun/fun_factory.dart index dbbf380..5f78fea 100644 --- a/lib/src/fun/fun_factory.dart +++ b/lib/src/fun/fun_factory.dart @@ -46,10 +46,13 @@ class FunFactory { Expression _any1(String name, Expression a0) { final f = _getFun1(name); - final cast0 = cast(a0, - value: f is Fun1, - logical: f is Fun1, - nodes: f is Fun1); + final cast0 = cast( + a0, + value: f is Fun1, + logical: f is Fun1, + node: f is Fun1, + nodes: f is Fun1, + ); return cast0.map(f.call); } @@ -60,12 +63,14 @@ class FunFactory { a0, value: f is Fun2, logical: f is Fun2, + node: f is Fun2, nodes: f is Fun2, ); final cast1 = cast( a1, value: f is Fun2, logical: f is Fun2, + node: f is Fun2, nodes: f is Fun2, ); return cast0.merge(cast1, f.call); @@ -84,16 +89,21 @@ class FunFactory { } static Expression cast(Expression arg, - {required bool value, required bool logical, required bool nodes}) { + {required bool value, + required bool logical, + required bool node, + required bool nodes}) { if (value) { if (arg is Expression) return arg; if (arg is Expression) return arg.map((v) => v.asValue); - } - if (logical) { + } else if (logical) { if (arg is Expression) return arg; if (arg is Expression) return arg.map((v) => v.asLogical); + } else if (node) { + if (arg is Expression) return arg; + } else if (nodes) { + if (arg is Expression) return arg; } - if (nodes && arg is Expression) return arg; throw Exception('Arg type mismatch'); } } diff --git a/pubspec.yaml b/pubspec.yaml index f7d379b..d8e4200 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: json_path -version: 0.7.1 +version: 0.7.2 description: "Implementation of RFC 9535 - JSONPath: Query Expressions for JSON. Reads and writes values in parsed JSON objects using queries like `$.store.book[2].price`." homepage: "https://github.com/f3ath/jessie" diff --git a/test/cases/extra/cases.json b/test/cases/extra/cases.json deleted file mode 100644 index 108d3ee..0000000 --- a/test/cases/extra/cases.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "tests": [ - { - "name": "reverse(@)", - "selector" : "$[?reverse(@)=='cba']", - "document" : ["abc", "cba"], - "result": ["abc"] - }, - { - "name": "reverse(reverse(@))", - "selector" : "$[?reverse(reverse(@))=='cba']", - "document" : ["abc", "cba"], - "result": ["cba"] - }, - { - "name": "count(siblings(@))", - "selector" : "$..[?count(siblings(@)) == 1]", - "document" : {"a": {"b": "x", "d": "x"}}, - "result": ["x", "x"] - }, - { - "name": "is_object(@)", - "selector" : "$[?is_object(@)]", - "document" : [1, true, {}, [42], "foo", {"a": "b"}], - "result": [{}, {"a": "b"}] - }, - { - "name": "is_array(@)", - "selector" : "$[?is_array(@)]", - "document" : [1, true, {}, [42], "foo", {"a": "b"}], - "result": [[42]] - }, - { - "name": "is_string", - "selector" : "$[?is_string(@)]", - "document" : [1, true, {}, [42], "foo", {"a": "b"}], - "result": ["foo"] - }, - { - "name": "is_number", - "selector" : "$[?is_number(@)]", - "document" : [1, true, {}, [42], 3.14, "foo", {"a": "b"}], - "result": [1, 3.14] - }, - { - "name": "is_boolean", - "selector" : "$[?is_boolean(@)]", - "document" : [1, true, {}, [42], "foo", {"a": "b"}, false], - "result": [true, false] - }, - { - "name": "parens in functional args", - "selector" : "$[?xor((@.b), (@.a))]", - "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], - "result": [{"a": 0}, {"b": 0}] - }, - { - "name": "nodes to logical conversion in function arg", - "selector" : "$[?xor(@.*, @.*)]", - "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], - "result": [] - } - ] -} \ No newline at end of file diff --git a/test/cases/extra/count.json b/test/cases/extra/count.json new file mode 100644 index 0000000..9643262 --- /dev/null +++ b/test/cases/extra/count.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "count(siblings(@))", + "selector" : "$..[?count(siblings(@)) == 1]", + "document" : {"a": {"b": "x", "d": "x"}}, + "result": ["x", "x"] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/index.json b/test/cases/extra/index.json new file mode 100644 index 0000000..96fc71e --- /dev/null +++ b/test/cases/extra/index.json @@ -0,0 +1,21 @@ +{ + "tests": [ + { + "name": "index()", + "selector" : "$[?index(@) == 1]", + "document" : [ "A", "B"], + "result": ["B"] + }, + { + "name": "index(), does not work on objects", + "selector" : "$[?index(@) == 0]", + "document" : {"0": "A", "1": "B"}, + "result": [] + }, + { + "name": "index(), non singular", + "selector" : "$[?index(@.*) == 0]", + "invalid_selector": true + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/is_array.json b/test/cases/extra/is_array.json new file mode 100644 index 0000000..52a916c --- /dev/null +++ b/test/cases/extra/is_array.json @@ -0,0 +1,11 @@ + +{ + "tests": [ + { + "name": "is_array(@)", + "selector" : "$[?is_array(@)]", + "document" : [1, true, {}, [42], "foo", {"a": "b"}], + "result": [[42]] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/is_boolean.json b/test/cases/extra/is_boolean.json new file mode 100644 index 0000000..9f5d696 --- /dev/null +++ b/test/cases/extra/is_boolean.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "is_boolean", + "selector" : "$[?is_boolean(@)]", + "document" : [1, true, {}, [42], "foo", {"a": "b"}, false], + "result": [true, false] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/is_number.json b/test/cases/extra/is_number.json new file mode 100644 index 0000000..a452350 --- /dev/null +++ b/test/cases/extra/is_number.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "is_number", + "selector" : "$[?is_number(@)]", + "document" : [1, true, {}, [42], 3.14, "foo", {"a": "b"}], + "result": [1, 3.14] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/is_object.json b/test/cases/extra/is_object.json new file mode 100644 index 0000000..107103b --- /dev/null +++ b/test/cases/extra/is_object.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "is_object(@)", + "selector" : "$[?is_object(@)]", + "document" : [1, true, {}, [42], "foo", {"a": "b"}], + "result": [{}, {"a": "b"}] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/is_string.json b/test/cases/extra/is_string.json new file mode 100644 index 0000000..2ee4859 --- /dev/null +++ b/test/cases/extra/is_string.json @@ -0,0 +1,10 @@ +{ + "tests": [ + { + "name": "is_string", + "selector" : "$[?is_string(@)]", + "document" : [1, true, {}, [42], "foo", {"a": "b"}], + "result": ["foo"] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/key.json b/test/cases/extra/key.json new file mode 100644 index 0000000..b1f4a61 --- /dev/null +++ b/test/cases/extra/key.json @@ -0,0 +1,27 @@ +{ + "tests": [ + { + "name": "key()", + "selector" : "$[?key(@) == 'a']", + "document" : {"a": "A", "b": "B"}, + "result": ["A"] + }, + { + "name": "key(), palindromic keys", + "selector" : "$[?key(@) == reverse(key(@))]", + "document" : {"foo": "FOO", "bar": "BAR", "bab": "BAB", "": "", "a": "A"}, + "result": ["BAB","","A"] + }, + { + "name": "key(), does not work on arrays", + "selector" : "$[?key(@) == 0]", + "document" : ["A", "B"], + "result": [] + }, + { + "name": "key(), non singular", + "selector" : "$[?key(@.*) == 'a']", + "invalid_selector": true + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/reverse.json b/test/cases/extra/reverse.json new file mode 100644 index 0000000..c3adef5 --- /dev/null +++ b/test/cases/extra/reverse.json @@ -0,0 +1,16 @@ +{ + "tests": [ + { + "name": "reverse(@)", + "selector" : "$[?reverse(@)=='cba']", + "document" : ["abc", "cba"], + "result": ["abc"] + }, + { + "name": "reverse(reverse(@))", + "selector" : "$[?reverse(reverse(@))=='cba']", + "document" : ["abc", "cba"], + "result": ["cba"] + } + ] +} \ No newline at end of file diff --git a/test/cases/extra/xor.json b/test/cases/extra/xor.json new file mode 100644 index 0000000..2ee34c5 --- /dev/null +++ b/test/cases/extra/xor.json @@ -0,0 +1,16 @@ +{ + "tests": [ + { + "name": "parens in functional args", + "selector" : "$[?xor((@.b), (@.a))]", + "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], + "result": [{"a": 0}, {"b": 0}] + }, + { + "name": "nodes to logical conversion in function arg", + "selector" : "$[?xor(@.*, @.*)]", + "document" : [{"a": 0}, {"a": 0, "b": 0}, {"b": 0}, {}], + "result": [] + } + ] +} \ No newline at end of file diff --git a/test/cases_test.dart b/test/cases_test.dart index 5579934..9d65337 100644 --- a/test/cases_test.dart +++ b/test/cases_test.dart @@ -8,11 +8,13 @@ void main() { runTestsInDirectory('test/cases/standard'); runTestsInDirectory('test/cases/extra', parser: JsonPathParser(functions: [ + const Index(), const IsArray(), const IsBoolean(), const IsNumber(), const IsObject(), const IsString(), + const Key(), const Reverse(), const Siblings(), const Xor(),