Skip to content

Commit

Permalink
Merge pull request #19 from jg-rp/match-and-first
Browse files Browse the repository at this point in the history
Add `match`, `first` and `match?`
  • Loading branch information
jg-rp authored Feb 8, 2025
2 parents beb3954 + a3222a2 commit bcf545a
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [0.4.0] - (unreleased)

- Added `JSONP3.find_enum`, `JSONP3::JSONPathEnvironment.find_enum` and `JSONP3::JSONPath.find_enum`. `find_enum` is like `find`, but returns an Enumerable (usually an Enumerator) of `JSONPathNode` instances instead of a `JSONPathNodeList`. `find_enum` can be more efficient for some combinations of query and data, especially for large data and recursive queries.
- Added `JSONP3.match`, `JSONP3.match?`, `JSONP3.first` and equivalent methods for `JSONPathEnvironment` and `JSONPath`.

## [0.3.2] - 2025-01-29

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,21 @@ end
# {"name"=>"John", "score"=>86, "admin"=>true} at $['users'][2]
```

### match / first

`match(query, value) -> JSONPathNode | nil`

`match` (alias `first`) returns a node for the first available match when applying _query_ to _value_, or `nil` if there were no matches.

### match?

`match?(query, value) -> bool`

`match?` returns `true` if there was at least one match from applying _query_ to _value_, or `false` otherwise.

### JSONPathEnvironment

The `find`, `find_enum` and `compile` methods described above are convenience methods equivalent to
The `find`, `find_enum` and `compile` methods described above are convenience methods equivalent to:

```
JSONP3::DEFAULT_ENVIRONMENT.find(query, data)
Expand Down
12 changes: 12 additions & 0 deletions lib/json_p3.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ def self.compile(path)
DefaultEnvironment.compile(path)
end

def self.match(path, data)
DefaultEnvironment.match(path, data)
end

def self.match?(path, data)
DefaultEnvironment.match?(path, data)
end

def self.first(path, data)
DefaultEnvironment.first(path, data)
end

def self.resolve(pointer, value, default: JSONPointer::UNDEFINED)
JSONPointer.new(pointer).resolve(value, default: default)
end
Expand Down
27 changes: 27 additions & 0 deletions lib/json_p3/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ def find_enum(query, value)
compile(query).find_enum(value)
end

# Apply JSONPath expression _query_ to _value_ an return the first
# available node.
# @param query [String] the JSONPath expression
# @param value [JSON-like data] the target JSON "document"
# @return [JSONPathNode | nil]
def match(path, value)
find_enum(path, value).first
end

# Apply JSONPath expression _query_ to _value_ an return `true` if there's at
# least one node, or nil if there were no matches.
# @param query [String] the JSONPath expression
# @param value [JSON-like data] the target JSON "document"
# @return [bool]
def match?(path, value)
!find_enum(path, value).first.nil?
end

# Apply JSONPath expression _query_ to _value_ an return the first
# available node, or nil if there were no matches.
# @param query [String] the JSONPath expression
# @param value [JSON-like data] the target JSON "document"
# @return [JSONPathNode | nil]
def first(path, value)
find_enum(path, value).first
end

# Override this function to configure JSONPath function extensions.
# By default, only the standard functions described in RFC 9535 are enabled.
def setup_function_extensions
Expand Down
21 changes: 21 additions & 0 deletions lib/json_p3/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,27 @@ def find_enum(root)
nodes
end

# Return the first node from applying this JSONPath expression to JSON-like value _root_.
# @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
# @return [JSONPathNode | nil] the first available node or nil if there were no matches.
def match(root)
find_enum(root).first
end

# Return `true` if this query results in at least one node, or `false` otherwise.
# @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
# @return [bool] `true` if this query results in at least one node, or `false` otherwise.
def match?(root)
!find_enum(root).first.nil?
end

# Return the first node from applying this JSONPath expression to JSON-like value _root_.
# @param root [Array, Hash, String, Integer, nil] the root JSON-like value to apply this query to.
# @return [JSONPathNode | nil] the first available node or nil if there were no matches.
def first(root)
find_enum(root).first
end

# Return _true_ if this JSONPath expression is a singular query.
def singular?
@segments.each do |segment|
Expand Down
22 changes: 21 additions & 1 deletion sig/json_p3.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ module JSONP3
DefaultEnvironment: JSONPathEnvironment

def self.find: (String path, untyped data) -> JSONPathNodeList

def self.find_enum: (String path, untyped data) -> Enumerable[JSONPathNode]

def self.compile: (String path) -> JSONPath

def self.match: (String path, untyped data) -> (JSONPathNode | nil)

def self.first: (String path, untyped data) -> (JSONPathNode | nil)

def self.match?: (String path, untyped data) -> bool

def self.resolve: (String pointer, untyped value, ?default: untyped) -> untyped

def self.apply: (Array[Op | Hash[String, untyped]] ops, untyped value) -> untyped
Expand Down Expand Up @@ -67,6 +75,12 @@ module JSONP3
def find: (String query, untyped value) -> JSONPathNodeList

def find_enum: (String query, untyped value) -> Enumerable[JSONPathNode]

def match: (String query, untyped value) -> (JSONPathNode | nil)

def match?: (String query, untyped value) -> bool

def first: (String query, untyped value) -> (JSONPathNode | nil)

def setup_function_extensions: () -> void
end
Expand Down Expand Up @@ -639,9 +653,15 @@ module JSONP3
# @param root [Array, Hash, String, Integer] the root JSON-like value to apply this query to.
# @return [Array<JSONPathNode>] the sequence of nodes found while applying this query to _root_.
def find: (untyped root) -> JSONPathNodeList

def find_enum: (untyped root) -> Enumerable[JSONPathNode]

def match: (untyped root) -> (JSONPathNode | nil)

def match?: (untyped root) -> bool

def first: (untyped root) -> (JSONPathNode | nil)

alias apply find

# Return true if this JSONPath expression is a singular query.
Expand Down
64 changes: 64 additions & 0 deletions test/test_convenience.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# frozen_string_literal: true

require "test_helper"

class TestConvenienceMethods < Minitest::Spec
def test_compile
path = JSONP3.compile("$.a.b")
data = { "a" => { "b" => 42 } }
nodes = path.find(data)
_(nodes.map(&:value)).must_equal([42])
end

def test_find
data = { "a" => { "b" => 42 } }
nodes = JSONP3.find("$.a.b", data)
_(nodes.map(&:value)).must_equal([42])
end

def test_find_enum
data = { "a" => { "b" => 42 } }
nodes = JSONP3.find_enum("$.a.b", data).to_a
_(nodes.map(&:value)).must_equal([42])
end

def test_match
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.match("$.a.*", data)
_(node.value).must_equal(1)
end

def test_no_match
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.match("$.b.*", data)

assert_nil(node)
end

def test_first
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.first("$.a.*", data)
_(node.value).must_equal(1)
end

def test_no_first
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.first("$.b.*", data)

assert_nil(node)
end

def test_match?
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.match?("$.a.*", data)

_(node).must_equal(true)
end

def test_no_match?
data = { "a" => [1, 2, 3, 4] }
node = JSONP3.match?("$.b.*", data)

_(node).must_equal(false)
end
end

0 comments on commit bcf545a

Please sign in to comment.