diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2fbde97 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [v1.2.0] - In Development + +### Added +- Array indexing support in JSONPath (e.g., `array[0].property`) +- New comparison operators: `starts_with`, `ends_with`, and `contains` +- Comprehensive unit tests for all new features + +### Changed +- Replaced JSON serialization with direct object navigation for path evaluation (50%+ performance improvement) +- Optimized expression parsing without regex dependency +- Fixed namespace inconsistency across library components +- Implemented thread-safe operator dictionary to prevent race conditions + +### Performance +- Direct object navigation replacing JSON serialization for path evaluation (50%+ faster) +- Optimized expression parsing without regex +- Reduced memory allocations by 60%+ + +### Fixed +- Namespace inconsistency: Changed JsonPathPredicate to JSONPredicate in supporting files +- Thread safety in operator dictionary access + +## [v1.0.0] - Initial Release +- Initial release of JSONPredicate library +- Core functionality with basic operators +- JSONPath-style property navigation \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml index c967cdc..d9fd3d8 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,4 +1,4 @@ -next-version: 1.1.0 +next-version: 1.2.0 tag-prefix: '[vV]' mode: ContinuousDeployment branches: diff --git a/README.md b/README.md index f0b886d..8d3641f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ninja JSONPredicate v1.1.0 +# ninja JSONPredicate v1.2.0 [![NuGet version](https://badge.fury.io/nu/JSONPredicate.svg)](https://badge.fury.io/nu/JSONPredicate) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/JSONPredicate/blob/master/LICENSE.md) [![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/JSONPredicate?logo=github&sort=semver)](https://github.com/CodeShayk/JSONPredicate/releases/latest) [![master-build](https://github.com/CodeShayk/JSONPredicate/actions/workflows/Master-Build.yml/badge.svg)](https://github.com/CodeShayk/JSONPredicate/actions/workflows/Master-Build.yml) @@ -18,7 +18,7 @@ - **Multiple Operators**: `eq` (equal), `in` (contains), `not` (not equal), `gt` (greater than), `gte` (greater than or equal), `lt` (less than), `lte` (less than or equal) - **Logical Operators**: `and`, `or` with proper precedence handling - **Array Handling**: Evaluate conditions on `arrays` and `collections`. Support array `indexing` (Available from v1.1.0) -- **String Operations**: `starts_with`, `ends_with`, `contains` (Available from v1.1.0) +- **String Operations**: `starts_with`, `ends_with`, `contains` (Available from v1.2.0) - **Type Safety**: `Automatic` type conversion and validation - **Complex Expressions**: `Parentheses` grouping and `nested` operations - **Lightweight**: `Minimal` dependencies, `fast` evaluation @@ -38,7 +38,7 @@ The expression syntax is ([JSONPath] [Comparison Operator] [Value]) [Logical Ope #### ii. Supported Operators - Comparison Operators - `eq`, `in`, `gt`, `gte`, `lt`, `lte` & `Not` - Logical Operators - `and` & `or` -- String Operators - `starts_with`, `ends_with`, `contains` (Available from v1.1.0) +- String Operators - `starts_with`, `ends_with`, `contains` (Available from v1.2.0) ### Example ``` var customer = new { @@ -66,13 +66,13 @@ bool result2 = JSONPredicate.Evaluate("client.address.postcode eq `e113et` and c bool result3 = JSONPredicate.Evaluate("client.tags in [`vip`, `standard`]", customer); bool ``` -#### iv. String operators (Available from v1.1.0) +#### iv. String operators (Available from v1.2.0) ``` bool result4 = JSONPredicate.Evaluate("client.address.postcode starts_with `e11`", customer); bool result5 = JSONPredicate.Evaluate("client.address.postcode ends_with `3et`", customer); bool result6 = JSONPredicate.Evaluate("client.address.postcode contains `13`", customer); ``` -#### v. Deep Array Indexing (Available from v1.1.0) +#### v. Deep Array Indexing (Available from v1.2.0) ``` bool result7 = JSONPredicate.Evaluate("client.tags[1] eq `premium`", customer); ``` @@ -86,7 +86,7 @@ This section provides the summary of planned releases with key details about eac | Version | Release Date | Type | Key Improvements | Backward Compatible | |---------|--------------|------|------------------|-------------------| | v1.0.0 | August 2025 | Major | Initial release with core predicate evaluation | N/A | -| v1.1.0 | October 2025 | Minor | Namespace consistency, thread safety, 50%+ performance improvement, array indexing, new operators (`starts_with`, `ends_with`, `contains`) | Yes | +| v1.2.0 | October 2025 | Minor | Namespace consistency, thread safety, 50%+ performance improvement, array indexing, new operators (`starts_with`, `ends_with`, `contains`), comprehensive unit tests | Yes | | v2.0.0 | TBC | Major | Comprehensive validation, performance benchmarking, thread safety verification, complete documentation | Mostly* | *Note: v2.0.0 marked as "Mostly" backward compatible due to major internal changes that may affect some advanced usage patterns. diff --git a/Release_Roadmap.md b/Release_Roadmap.md index cdb6623..f03c3c5 100644 --- a/Release_Roadmap.md +++ b/Release_Roadmap.md @@ -26,7 +26,7 @@ Initial release of the JSONPredicate library. Provides core functionality for ev --- -### v1.1.0 - Combined Fix, Feature and Performance Release +### v1.2.0 - Combined Fix, Feature and Performance Release **Release Type**: Minor (Backward Compatible) **Release Date**: October 2025 **Focus**: Critical fixes, new functionality and performance improvements @@ -148,7 +148,7 @@ Major feature and performance release with comprehensive validation. This releas ## Evolution Summary -### v1.0.0 → v1.1.0: Foundation, Stability, Performance and Features +### v1.0.0 → v1.2.0: Foundation, Stability, Performance and Features - **Focus**: Internal consistency, thread safety, performance optimization and new functionality - **Improvement**: Fixed critical namespace inconsistency - **Improvement**: Made operator dictionary thread-safe @@ -156,16 +156,9 @@ Major feature and performance release with comprehensive validation. This releas - **Improvement**: Expression parsing optimization removing regex dependency - **Addition**: Array indexing support (e.g., `array[0].property`) - **Addition**: New operators (`starts_with`, `ends_with`, `contains`) +- **Addition**: Comprehensive unit tests for all new features - **Impact**: Better stability, reliability and performance with new features, all backward compatible -### v1.1.0 → v2.0.0: Validation and Production Readiness -- **Focus**: Comprehensive validation and release preparation -- **Achievement**: All features integrated and validated together -- **Achievement**: Performance improvements quantitatively verified -- **Achievement**: Thread safety comprehensively tested -- **Achievement**: Complete documentation and packaging -- **Impact**: Production-ready major release with all improvements validated - ## Technical Improvements Summary ### Performance Improvements @@ -188,21 +181,13 @@ Major feature and performance release with comprehensive validation. This releas ## Upgrade Path -### From v1.0.0 to v1.1.0 +### From v1.0.0 to v1.2.0 - Drop-in replacement - No code changes required for existing functionality - Benefits: Thread safety, performance improvements, and new features available -### From v1.0.x to v2.0.0 -- Drop-in replacement for basic usage -- Thorough testing recommended for advanced usage due to internal implementation changes -- Benefit: All improvements and comprehensive validation - ## Key Metrics | Version | Performance Improvement | Memory Improvement | New Features | Backward Compatible | |---------|------------------------|-------------------|----------------|-------------------| -| v1.1.0 | 50-70% | 60%+ | Namespace consistency, thread safety, array indexing, 3 new operators | Yes | -| v2.0.0 | 50-70% (maintained) | 60%+ (maintained) | Complete validation, documentation | Mostly | - -*Note: v2.0.0 marked as "Mostly" backward compatible due to major internal changes that may affect some advanced usage patterns.* +| v1.2.0 | 50-70% | 60%+ | Namespace consistency, thread safety, array indexing, 3 new operators, comprehensive unit tests | Yes | diff --git a/src/JSONPredicate/Expression.cs b/src/JSONPredicate/Expression.cs index 5b1537e..6785bdf 100644 --- a/src/JSONPredicate/Expression.cs +++ b/src/JSONPredicate/Expression.cs @@ -31,7 +31,7 @@ public static (string Path, string Operator, string Value) Parse(string expressi throw new ArgumentException($"Invalid expression format: {expression}"); // Define operators in order of length (longer first) to avoid partial matches - var operators = new[] { "gte", "lte", "not", "eq", "gt", "lt", "in" }; + var operators = new[] { "gte", "lte", "not", "eq", "gt", "lt", "in", "starts_with", "ends_with", "contains" }; for (int i = 0; i < expr.Length; i++) { diff --git a/src/JSONPredicate/JSONPredicate.csproj b/src/JSONPredicate/JSONPredicate.csproj index 944e42c..a6f84e4 100644 --- a/src/JSONPredicate/JSONPredicate.csproj +++ b/src/JSONPredicate/JSONPredicate.csproj @@ -1,4 +1,4 @@ - + net462;netstandard2.0;netstandard2.1;net9.0 @@ -21,7 +21,7 @@ https://github.com/CodeShayk/JSONPredicate/wiki https://github.com/CodeShayk/JSONPredicate - v1.1.0 - Enhanced JSONPredicate library with significant improvements. + v1.2.0 - Enhanced JSONPredicate library with additional improvements. - Array indexing support in JSONPath (e.g., `array[0].property`) - New comparison operators: `starts_with`, `ends_with`, and `contains` - Direct object navigation with 50%+ performance improvement @@ -29,9 +29,9 @@ - Optimized expression parsing without regex dependency. For more details, visit the release page: - https://github.com/CodeShayk/JSONPredicate/releases/tag/v1.1.0 + https://github.com/CodeShayk/JSONPredicate/releases/tag/v1.2.0 - 1.1.0 + 1.2.0 True JSONPredicate diff --git a/tests/JSONPredicate.Tests/PredicateTests.cs b/tests/JSONPredicate.Tests/PredicateTests.cs index f837733..522ac90 100644 --- a/tests/JSONPredicate.Tests/PredicateTests.cs +++ b/tests/JSONPredicate.Tests/PredicateTests.cs @@ -566,5 +566,336 @@ public void Evaluate_InOperatorWithParenthesesAndNestedLogic_ShouldReturnTrue() var result = JSONPredicate.Evaluate("(client.role in (`admin`, `manager`) or client.tags in (`vip`)) and client.address.active eq true", _testObject); Assert.That(result, Is.True); } + + // === v1.1.0 NEW FEATURES TESTS === + + // Array Indexing Tests - NEW in v1.1.0 + [Test] + public void Evaluate_ArrayIndexing_AccessFirstElement_ShouldReturnTrue() + { + var obj = new { items = new[] { "first", "second", "third" } }; + var result = JSONPredicate.Evaluate("items[0] eq `first`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_AccessMiddleElement_ShouldReturnTrue() + { + var obj = new { items = new[] { "first", "second", "third" } }; + var result = JSONPredicate.Evaluate("items[1] eq `second`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_AccessLastElement_ShouldReturnTrue() + { + var obj = new { items = new[] { "first", "second", "third" } }; + var result = JSONPredicate.Evaluate("items[2] eq `third`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_InvalidIndex_ShouldReturnFalse() + { + var obj = new { items = new[] { "first", "second" } }; + var result = JSONPredicate.Evaluate("items[10] eq `nonexistent`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_ArrayIndexing_NegativeIndex_ShouldReturnFalse() + { + var obj = new { items = new[] { "first", "second" } }; + var result = JSONPredicate.Evaluate("items[-1] eq `first`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_ArrayIndexing_NonArrayProperty_ShouldReturnFalse() + { + var obj = new { items = "not-an-array" }; + var result = JSONPredicate.Evaluate("items[0] eq `first`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_ArrayIndexing_NestedArrayElement_ShouldReturnTrue() + { + var obj = new + { + users = new[] + { + new { name = "John", role = "admin" }, + new { name = "Jane", role = "user" } + } + }; + var result = JSONPredicate.Evaluate("users[0].name eq `John`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_NestedArrayElementWithComparison_ShouldReturnTrue() + { + var obj = new + { + users = new[] + { + new { name = "John", role = "admin" }, + new { name = "Jane", role = "user" } + } + }; + var result = JSONPredicate.Evaluate("users[1].role eq `user`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_WithLogicalOperators_ShouldReturnTrue() + { + var obj = new + { + items = new[] { "active", "pending" }, + status = "ok" + }; + var result = JSONPredicate.Evaluate("items[0] eq `active` and status eq `ok`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ArrayIndexing_WithInOperator_ShouldReturnTrue() + { + var obj = new + { + tags = new[] { "premium", "verified" } + }; + var result = JSONPredicate.Evaluate("tags[0] in (`premium`, `basic`)", obj); + Assert.That(result, Is.True); + } + + // StartsWith Operator Tests - NEW in v1.1.0 + [Test] + public void Evaluate_StartsWithOperator_MatchBeginning_ShouldReturnTrue() + { + var obj = new { name = "John Doe" }; + var result = JSONPredicate.Evaluate("name starts_with `John`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_StartsWithOperator_NoMatch_ShouldReturnFalse() + { + var obj = new { name = "John Doe" }; + var result = JSONPredicate.Evaluate("name starts_with `Jane`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_StartsWithOperator_CaseInsensitive_ShouldReturnTrue() + { + var obj = new { name = "John Doe" }; + var result = JSONPredicate.Evaluate("name starts_with `john`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_StartsWithOperator_EmptyString_ShouldReturnTrue() + { + var obj = new { name = "John" }; + var result = JSONPredicate.Evaluate("name starts_with ``", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_StartsWithOperator_EntireString_ShouldReturnTrue() + { + var obj = new { name = "John" }; + var result = JSONPredicate.Evaluate("name starts_with `John`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_StartsWithOperator_WithArrayElement_ShouldReturnTrue() + { + var obj = new { tags = new[] { "premium-user", "verified" } }; + var result = JSONPredicate.Evaluate("tags[0] starts_with `premium`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_StartsWithOperator_WithLogicalOperators_ShouldReturnTrue() + { + var obj = new { name = "John", status = "active" }; + var result = JSONPredicate.Evaluate("name starts_with `Jo` and status eq `active`", obj); + Assert.That(result, Is.True); + } + + // EndsWith Operator Tests - NEW in v1.1.0 + [Test] + public void Evaluate_EndsWithOperator_MatchEnd_ShouldReturnTrue() + { + var obj = new { email = "john@example.com" }; + var result = JSONPredicate.Evaluate("email ends_with `.com`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_EndsWithOperator_NoMatch_ShouldReturnFalse() + { + var obj = new { email = "john@example.com" }; + var result = JSONPredicate.Evaluate("email ends_with `.org`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_EndsWithOperator_CaseInsensitive_ShouldReturnTrue() + { + var obj = new { email = "john@EXAMPLE.COM" }; + var result = JSONPredicate.Evaluate("email ends_with `.com`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_EndsWithOperator_EmptyString_ShouldReturnTrue() + { + var obj = new { name = "John" }; + var result = JSONPredicate.Evaluate("name ends_with ``", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_EndsWithOperator_EntireString_ShouldReturnTrue() + { + var obj = new { name = "John" }; + var result = JSONPredicate.Evaluate("name ends_with `John`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_EndsWithOperator_WithArrayElement_ShouldReturnTrue() + { + var obj = new { tags = new[] { "user-premium", "verified" } }; + var result = JSONPredicate.Evaluate("tags[0] ends_with `premium`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_EndsWithOperator_WithLogicalOperators_ShouldReturnTrue() + { + var obj = new { name = "Johnson", status = "active" }; + var result = JSONPredicate.Evaluate("name ends_with `son` and status eq `active`", obj); + Assert.That(result, Is.True); + } + + // Contains Operator Tests - NEW in v1.1.0 + [Test] + public void Evaluate_ContainsOperator_MatchSubstring_ShouldReturnTrue() + { + var obj = new { description = "This is a sample text" }; + var result = JSONPredicate.Evaluate("description contains `sample`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ContainsOperator_NoMatch_ShouldReturnFalse() + { + var obj = new { description = "This is a sample text" }; + var result = JSONPredicate.Evaluate("description contains `missing`", obj); + Assert.That(result, Is.False); + } + + [Test] + public void Evaluate_ContainsOperator_CaseInsensitive_ShouldReturnTrue() + { + var obj = new { description = "This Is A Sample Text" }; + var result = JSONPredicate.Evaluate("description contains `sample`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ContainsOperator_MatchEntireString_ShouldReturnTrue() + { + var obj = new { text = "hello" }; + var result = JSONPredicate.Evaluate("text contains `hello`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ContainsOperator_EmptySubstring_ShouldReturnTrue() + { + var obj = new { text = "hello" }; + var result = JSONPredicate.Evaluate("text contains ``", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ContainsOperator_WithArrayElement_ShouldReturnTrue() + { + var obj = new { tags = new[] { "super-premium-package", "verified" } }; + var result = JSONPredicate.Evaluate("tags[0] contains `premium`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_ContainsOperator_WithLogicalOperators_ShouldReturnTrue() + { + var obj = new { name = "John Smith", department = "Engineering" }; + var result = JSONPredicate.Evaluate("name contains `Smith` and department contains `Eng`", obj); + Assert.That(result, Is.True); + } + + // Combined NEW Features Tests + [Test] + public void Evaluate_CombinedNewFeatures_ArrayIndexingWithStartsWith_ShouldReturnTrue() + { + var obj = new + { + tags = new[] { "premium-user", "basic-user", "admin" } + }; + var result = JSONPredicate.Evaluate("tags[0] starts_with `premium`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_CombinedNewFeatures_ArrayIndexingWithEndsWith_ShouldReturnTrue() + { + var obj = new + { + filenames = new[] { "document.pdf", "image.jpg", "spreadsheet.xlsx" } + }; + var result = JSONPredicate.Evaluate("filenames[1] ends_with `.jpg`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_CombinedNewFeatures_ArrayIndexingWithContains_ShouldReturnTrue() + { + var obj = new + { + descriptions = new[] { "This is a premium service", "Basic option", "Standard plan" } + }; + var result = JSONPredicate.Evaluate("descriptions[0] contains `premium`", obj); + Assert.That(result, Is.True); + } + + [Test] + public void Evaluate_CombinedNewFeatures_ComplexExpressionWithAllNewFeatures_ShouldReturnTrue() + { + var obj = new + { + users = new[] + { + new { name = "John Smith", email = "john.smith@example.com", roles = new[] { "user", "editor" } }, + new { name = "Jane Doe", email = "jane.doe@work.org", roles = new[] { "admin", "manager" } } + } + }; + + // Complex expression using array indexing, and new string operators + var result = JSONPredicate.Evaluate( + "users[0].name starts_with `John` and " + + "users[1].email ends_with `.org` and " + + "users[0].email contains `smith` and " + + "users[1].roles[0] eq `admin`", obj); + + Assert.That(result, Is.True); + } } } \ No newline at end of file