JSONPath RFC 9535 Specification Parser¶
JSONPath expression parser for the Specification Pattern using the jsonpath-rfc9535 library.
Description¶
This implementation uses the jsonpath-rfc9535 library to parse JSONPath expressions and converts them into Specification Pattern AST nodes. Fully compliant with the RFC 9535 standard and supports C-style string formatting parameterization.
Key Advantages¶
Full RFC 9535 compliance - uses the official JSONPath standard
Parameterization - placeholder support (%s, %d, %f, %(name)s)
Standard operators -
==,!=,>,<,>=,<=RFC 9535 logical operators -
&&(AND),||(OR),!(NOT)Parentheses - logical expression grouping (
$[?(@.age >= 18 && @.age <= 65) && @.active == true])Wildcard collections - filtering collection elements
Nested wildcards - filtering by nested collections (
$.categories[*][?@.items[*][?@.price > 100]])Nested paths - access to nested fields (
$[?@.profile.age > 25],$[?@.company.department.manager.level > 5])Reusability - one specification with different parameters
Strict standard compliance - guaranteed compatibility
Usage¶
from ascetic_ddd.specification.domain.jsonpath.jsonpath_rfc9535_parser import parse
# Create specification
spec = parse("$[?@.age > %d]")
# Create context
class DictContext:
def __init__(self, data):
self._data = data
def get(self, key):
return self._data[key]
user = DictContext({"age": 30})
# Check match
result = spec.match(user, (25,)) # True
RFC 9535 Compliance¶
This implementation is fully compliant with the RFC 9535 standard:
Comparison Operators (RFC 9535)¶
==- Equal (double sign, as per the standard)!=- Not equal>- Greater than<- Less than>=- Greater than or equal<=- Less than or equal
Logical Operators (RFC 9535)¶
&&- Logical AND (as per the standard)||- Logical OR (as per the standard)!- Logical NOT (as per the standard)
References¶
@- Current node$- Root node
Supported Features¶
Comparison Operators¶
# RFC 9535 uses == for equality (double sign)
parse("$[?@.age == %d]") # Equal
parse("$[?@.age != %d]") # Not equal
parse("$[?@.age > %d]") # Greater than
parse("$[?@.age < %d]") # Less than
parse("$[?@.age >= %d]") # Greater than or equal
parse("$[?@.age <= %d]") # Less than or equal
Logical Operators¶
# RFC 9535 uses && for AND
parse("$[?@.age > %d && @.active == %s]")
# RFC 9535 uses || for OR
parse("$[?@.age < %d || @.age > %d]")
# RFC 9535 uses ! for NOT
parse("$[?!(@.active == %s)]")
# Complex expressions
parse("$[?(@.age >= %d && @.age <= %d) && @.status == %s]")
Parameterization¶
# Positional parameters
parse("$[?@.age > %d]") # Integer
parse("$[?@.name == %s]") # String
parse("$[?@.price > %f]") # Floating point number
# Named parameters
parse("$[?@.age > %(min_age)d]")
parse("$[?@.name == %(name)s]")
parse("$[?@.price > %(min_price)f]")
# Multiple parameters
parse("$[?@.age >= %(min_age)d && @.age <= %(max_age)d]")
Wildcard Collections¶
spec = parse("$.items[*][?@.price > %f]")
from ascetic_ddd.specification.domain.evaluate_visitor import CollectionContext
item1 = DictContext({"name": "Laptop", "price": 999.99})
item2 = DictContext({"name": "Mouse", "price": 29.99})
collection = CollectionContext([item1, item2])
store = DictContext({"items": collection})
# Check if there is at least one item with price > 500
spec.match(store, (500.0,)) # True
Nested Wildcards¶
# Nested collections: categories -> items
spec = parse("$.categories[*][?@.items[*][?@.price > %f]]")
# Create data structure
item1 = DictContext({"name": "Laptop", "price": 999.0})
items = CollectionContext([item1])
category = DictContext({"name": "Electronics", "items": items})
categories = CollectionContext([category])
store = DictContext({"categories": categories})
# Is there a category with an item costing more than 500?
spec.match(store, (500.0,)) # True
Examples¶
Basic Usage¶
from ascetic_ddd.specification.domain.jsonpath.jsonpath_rfc9535_parser import parse
# Simple comparison (RFC 9535: ==)
spec = parse("$[?@.age > %d]")
user = DictContext({"age": 30})
spec.match(user, (25,)) # True
# String comparison (RFC 9535: ==)
spec = parse("$[?@.status == %s]")
task = DictContext({"status": "done"})
spec.match(task, ("done",)) # True
# Named parameters
spec = parse("$[?@.score >= %(min_score)d]")
student = DictContext({"score": 85})
spec.match(student, {"min_score": 80}) # True
Logical Operators (RFC 9535)¶
# AND operator (RFC 9535: &&)
spec = parse("$[?@.age > %d && @.active == %s]")
user = DictContext({"age": 30, "active": True})
spec.match(user, (25, True)) # True
# OR operator (RFC 9535: ||)
spec = parse("$[?@.age < %d || @.age > %d]")
user_young = DictContext({"age": 15})
spec.match(user_young, (18, 65)) # True
# NOT operator (RFC 9535: !)
spec = parse("$[?!(@.active == %s)]")
user_inactive = DictContext({"active": False})
spec.match(user_inactive, (True,)) # True
Working with Collections¶
from ascetic_ddd.specification.domain.evaluate_visitor import CollectionContext
spec = parse("$.users[*][?@.age >= %d]")
user1 = DictContext({"name": "Alice", "age": 30})
user2 = DictContext({"name": "Bob", "age": 25})
users = CollectionContext([user1, user2])
root = DictContext({"users": users})
# Is there at least one user with age >= 28?
spec.match(root, (28,)) # True (Alice)
Nested Wildcards¶
from ascetic_ddd.specification.domain.evaluate_visitor import CollectionContext
# Nested wildcards: filtering by nested collections
spec = parse("$.categories[*][?@.items[*][?@.price > %f]]")
# Create structure: categories -> items
item1 = DictContext({"name": "Laptop", "price": 999.0})
item2 = DictContext({"name": "Mouse", "price": 29.0})
items1 = CollectionContext([item1, item2])
category1 = DictContext({"name": "Electronics", "items": items1})
item3 = DictContext({"name": "Shirt", "price": 49.0})
items2 = CollectionContext([item3])
category2 = DictContext({"name": "Clothing", "items": items2})
categories = CollectionContext([category1, category2])
store = DictContext({"categories": categories})
# Is there a category with an item costing more than 500?
spec.match(store, (500.0,)) # True (category1 has Laptop)
Nested wildcards with logic:
# Nested wildcard with AND operator
spec = parse("$.categories[*][?@.items[*][?@.price > %f && @.price < %f]]")
# Is there a category with an item in the 500-1000 range?
spec.match(store, (500.0, 1000.0)) # True (Laptop: 999)
# Is there a category with an item in the 1000-2000 range?
spec.match(store, (1000.0, 2000.0)) # False
Multiple matches:
# Check for multiple categories with expensive items
spec = parse("$.categories[*][?@.items[*][?@.price > %f]]")
# Add an expensive item to the second category
item4 = DictContext({"name": "Designer Jeans", "price": 299.0})
items2 = CollectionContext([item3, item4])
category2 = DictContext({"name": "Clothing", "items": items2})
categories = CollectionContext([category1, category2])
store = DictContext({"categories": categories})
# Now both categories have items costing more than 200
spec.match(store, (200.0,)) # True
Nested Paths¶
# Create a special context for nested structures
class NestedDictContext:
def __init__(self, data):
self._data = data
def get(self, key):
value = self._data[key]
# Automatically wrap nested dicts
if isinstance(value, dict):
return NestedDictContext(value)
return value
# Simple nested path: $[?@.profile.age > 25]
spec = parse("$[?@.profile.age > %d]")
data = NestedDictContext({
"profile": {
"age": 30,
"name": "Alice"
}
})
spec.match(data, (25,)) # True
Deeply nested paths:
# Deep nesting: $[?@.company.department.manager.level > 5]
spec = parse("$[?@.company.department.manager.level > %d]")
data = NestedDictContext({
"company": {
"department": {
"manager": {
"level": 7,
"name": "Alice"
}
}
}
})
spec.match(data, (5,)) # True
Nested paths with logical operators:
# $[?@.profile.age > 25 && @.profile.active == true]
spec = parse("$[?@.profile.age > %d && @.profile.active == %s]")
data = NestedDictContext({
"profile": {
"age": 30,
"active": True
}
})
spec.match(data, (25, True)) # True
Nested paths with parentheses:
# Parentheses for operator precedence
spec = parse("$[?(@.profile.age >= %d && @.profile.age <= %d) && @.profile.active == %s]")
data = NestedDictContext({
"profile": {
"age": 30,
"active": True
}
})
spec.match(data, (25, 35, True)) # True
Complex Expressions¶
# Combining operators
spec = parse("$[?(@.age >= %d && @.age <= %d) && @.status == %s]")
user = DictContext({"age": 30, "status": "active"})
spec.match(user, (25, 35, "active")) # True
# Multiple named parameters
spec = parse("$[?@.age >= %(min_age)d && @.age <= %(max_age)d]")
user = DictContext({"age": 30})
spec.match(user, {"min_age": 25, "max_age": 35}) # True
Testing¶
# Run RFC 9535 parser tests
python -m unittest ascetic_ddd.specification.domain.jsonpath.test_jsonpath_parser_rfc9535 -v
# Run examples
python ascetic_ddd/specification/domain/jsonpath/example_usage_rfc9535.py
# All tests
python -m unittest discover -s ascetic_ddd/specification -p "test_*.py" -v
Advantages of RFC 9535¶
Standard compliance - full compatibility with RFC 9535
Official specification - based on the official IETF standard
Portability - easily integrates with other RFC 9535 systems
Stability - the standard ensures long-term stability
Clear syntax -
==for equality,&&/||for logicActive support - the jsonpath-rfc9535 library is actively maintained
Dependencies¶
jsonpath-rfc9535- JSONPath expression parsing (RFC 9535 compliant)Modules from
ascetic_ddd.specification.domain:nodes- Specification AST nodesevaluate_visitor- Specification evaluation
Installation¶
pip install jsonpath-rfc9535