From b36045ad394cdb6e1657279b8f7cc804014dc349 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:35:02 +0000 Subject: [PATCH 1/4] Initial plan From 8d2cbce23e694ca92f62ba9a23e2cb5f875c07db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:42:30 +0000 Subject: [PATCH 2/4] Fix Frends.JSON.Query compliance findings: net8, static class, CancellationToken, error handling, csproj properties --- Frends.JSON.Query/CHANGELOG.md | 8 ++++ .../Frends.JSON.Query.UnitTests.csproj | 4 +- .../Frends.JSON.Query.UnitTests/UnitTests.cs | 31 +++++++++++-- .../Frends.JSON.Query/Definitions/Error.cs | 19 ++++++++ .../Frends.JSON.Query/Definitions/Options.cs | 18 +++++++- .../Frends.JSON.Query/Definitions/Result.cs | 11 +++++ .../Frends.JSON.Query.csproj | 4 +- Frends.JSON.Query/Frends.JSON.Query/Query.cs | 46 +++++++++++++++---- 8 files changed, 125 insertions(+), 16 deletions(-) create mode 100644 Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs diff --git a/Frends.JSON.Query/CHANGELOG.md b/Frends.JSON.Query/CHANGELOG.md index 6a821f1..34dfdbb 100644 --- a/Frends.JSON.Query/CHANGELOG.md +++ b/Frends.JSON.Query/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.3.0] - 2026-07-31 +### Changed +- The task class is now properly declared as static, as required by Frends task standards. +- Added a `CancellationToken` parameter so that Frends can cancel the task when needed. +- Added `ThrowErrorOnFailure` and `ErrorMessageOnFailure` options. When `ThrowErrorOnFailure` is set to false, task failures are returned as a result object (with `Success = false` and an `Error` containing the message and exception) instead of raising an exception. The default behaviour (throwing on error) is unchanged. +- The result object now includes an `Error` property that is populated when `ThrowErrorOnFailure` is false. +- Updated target framework from .NET 6 to .NET 8. + ## [1.2.0] - 2026-07-08 ### Fixed - Fixed issue where Options.ErrorWhenNotMatched did not throw an exception when a JSONPath filter expression (e.g. `[?(...)]`) matched no results. diff --git a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/Frends.JSON.Query.UnitTests.csproj b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/Frends.JSON.Query.UnitTests.csproj index a3a1e56..83b776e 100644 --- a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/Frends.JSON.Query.UnitTests.csproj +++ b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/Frends.JSON.Query.UnitTests.csproj @@ -1,11 +1,13 @@ - net6.0 + net8.0 enable enable false + https://frends.com/ + Frends diff --git a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs index 113ad1a..7d7bfb5 100644 --- a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs +++ b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Threading; namespace Frends.JSON.Query.UnitTests; @@ -53,7 +54,7 @@ public void QueryShouldWorkWithStringInput() ErrorWhenNotMatched = true, }; - var result = JSON.Query(input, options); + var result = JSON.Query(input, options, CancellationToken.None); Assert.IsTrue(result.Success); Assert.AreEqual(2, result.Data.Count()); Assert.AreEqual("Anvil", result.Data.First().ToString()); @@ -74,7 +75,7 @@ public void QueryShouldWorkWithJTokenInput() ErrorWhenNotMatched = true, }; - var result = JSON.Query(input, options); + var result = JSON.Query(input, options, CancellationToken.None); Assert.IsTrue(result.Success); Assert.AreEqual(2, result.Data.Count()); Assert.AreEqual("Anvil", result.Data.First().ToString()); @@ -94,7 +95,7 @@ public void QueryShouldThrowIfOptionSetAndFilterMatchesNothing() ErrorWhenNotMatched = true, }; - Assert.ThrowsException(() => JSON.Query(input, options)); + Assert.ThrowsException(() => JSON.Query(input, options, CancellationToken.None)); } [TestMethod] @@ -111,9 +112,31 @@ public void QueryShouldNotThrowIfOptionNotSetAndNothingIsFound() ErrorWhenNotMatched = false, }; - var result = JSON.Query(input, options); + var result = JSON.Query(input, options, CancellationToken.None); Assert.IsTrue(result.Success); Assert.IsNotNull(result.Data); Assert.IsFalse(result.Data.Any()); } + + [TestMethod] + public void QueryShouldReturnErrorResultWhenThrowErrorOnFailureIsFalse() + { + var input = new Input() + { + Json = jsonString, + Query = "$..Products[?(@.Price >= 1000)].Name" + }; + + var options = new Options() + { + ErrorWhenNotMatched = true, + ThrowErrorOnFailure = false, + }; + + var result = JSON.Query(input, options, CancellationToken.None); + Assert.IsFalse(result.Success); + Assert.IsNotNull(result.Error); + Assert.IsFalse(string.IsNullOrEmpty(result.Error.Message)); + Assert.IsNotNull(result.Error.AdditionalInfo); + } } \ No newline at end of file diff --git a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs new file mode 100644 index 0000000..de1af5e --- /dev/null +++ b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs @@ -0,0 +1,19 @@ +using System; + +namespace Frends.JSON.Query.Definitions; + +/// +/// Error information returned when the task fails and ThrowErrorOnFailure is false. +/// +public class Error +{ + /// + /// Error message. + /// + public string Message { get; internal set; } + + /// + /// The exception that caused the error. + /// + public Exception AdditionalInfo { get; internal set; } +} diff --git a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs index a33ff74..2363816 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs @@ -1,4 +1,6 @@ -namespace Frends.JSON.Query.Definitions; +using System.ComponentModel; + +namespace Frends.JSON.Query.Definitions; /// /// Options parameters. @@ -10,4 +12,18 @@ public class Options /// /// true public bool ErrorWhenNotMatched { get; set; } + + /// + /// If true, exceptions are thrown on failure. If false, errors are returned as part of the result. + /// + /// true + [DefaultValue(true)] + public bool ThrowErrorOnFailure { get; set; } = true; + + /// + /// Custom error message to use when an error occurs. Leave empty to use the original exception message. + /// + /// + [DefaultValue("")] + public string ErrorMessageOnFailure { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs index 79da9fc..8277ae9 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs @@ -19,9 +19,20 @@ public class Result /// [ { Foo }, { Bar } ] public IEnumerable Data { get; private set; } + /// + /// Error information, populated when the task fails and ThrowErrorOnFailure is false. + /// + public Error Error { get; private set; } + internal Result(bool success, IEnumerable data) { Success = success; Data = data; } + + internal Result(Error error) + { + Success = false; + Error = error; + } } \ No newline at end of file diff --git a/Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj b/Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj index 2be73b3..c5c0074 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj +++ b/Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj @@ -1,8 +1,8 @@  - net6.0 - 1.2.0 + net8.0 + 1.3.0 Frends Frends Frends diff --git a/Frends.JSON.Query/Frends.JSON.Query/Query.cs b/Frends.JSON.Query/Frends.JSON.Query/Query.cs index 044ab4a..8b45bab 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Query.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Query.cs @@ -1,18 +1,20 @@ using Frends.JSON.Query.Definitions; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; +using System.Threading; namespace Frends.JSON.Query; /// /// JSON Task. /// -public class JSON +public static class JSON { /// Mem cleanup. static JSON() @@ -29,16 +31,44 @@ static JSON() /// /// Input parameters /// Optional parameter. - /// Object { bool Success, IEnumerable<object> Data } - public static Result Query([PropertyTab] Input input, [PropertyTab] Options options) + /// Token generated by frends to stop this Task. + /// Object { bool Success, IEnumerable<object> Data, Error Error } + public static Result Query([PropertyTab] Input input, [PropertyTab] Options options, CancellationToken cancellationToken) { - JToken jToken = GetJTokenFromInput(input.Json); - var tokens = jToken.SelectTokens(input.Query, options.ErrorWhenNotMatched).ToList(); + try + { + cancellationToken.ThrowIfCancellationRequested(); + JToken jToken = GetJTokenFromInput(input.Json); + var tokens = jToken.SelectTokens(input.Query, options.ErrorWhenNotMatched).ToList(); - if (tokens.Count == 0 && options.ErrorWhenNotMatched) - throw new JsonException($"No matches found for query '{input.Query}'."); + if (tokens.Count == 0 && options.ErrorWhenNotMatched) + throw new JsonException($"No matches found for query '{input.Query}'."); - return new Result(true, tokens); + return new Result(true, tokens); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + if (options.ThrowErrorOnFailure) + { + if (string.IsNullOrEmpty(options.ErrorMessageOnFailure)) + throw new Exception(ex.Message, ex); + throw new Exception(options.ErrorMessageOnFailure, ex); + } + + var errorMessage = string.IsNullOrEmpty(options.ErrorMessageOnFailure) + ? ex.Message + : $"{options.ErrorMessageOnFailure}: {ex.Message}"; + + return new Result(new Error + { + Message = errorMessage, + AdditionalInfo = ex, + }); + } } private static object GetJTokenFromInput(dynamic json) From 2b6c72006ed355496cfe53877c488fd9e7c83b64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 06:29:44 +0000 Subject: [PATCH 3/4] Refactor error handling: extract ErrorHandler helper, simplify Result class --- .../Frends.JSON.Query/Definitions/Result.cs | 18 ++----- .../Frends.JSON.Query/Helpers/ErrorHandler.cs | 54 +++++++++++++++++++ Frends.JSON.Query/Frends.JSON.Query/Query.cs | 24 ++------- 3 files changed, 60 insertions(+), 36 deletions(-) create mode 100644 Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs diff --git a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs index 8277ae9..a44b97b 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs @@ -11,28 +11,16 @@ public class Result /// Operation complete without errors. /// /// true - public bool Success { get; private set; } + public bool Success { get; internal set; } /// /// Result data. /// /// [ { Foo }, { Bar } ] - public IEnumerable Data { get; private set; } + public IEnumerable Data { get; internal set; } /// /// Error information, populated when the task fails and ThrowErrorOnFailure is false. /// - public Error Error { get; private set; } - - internal Result(bool success, IEnumerable data) - { - Success = success; - Data = data; - } - - internal Result(Error error) - { - Success = false; - Error = error; - } + public Error Error { get; internal set; } } \ No newline at end of file diff --git a/Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs b/Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs new file mode 100644 index 0000000..33bd800 --- /dev/null +++ b/Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs @@ -0,0 +1,54 @@ +using Frends.JSON.Query.Definitions; +using System; + +namespace Frends.JSON.Query.Helpers; + +internal static class ErrorHandler +{ + /// + /// Handle an exception according to the task options. + /// + /// The exception to handle. + /// Task options that control whether failures are returned as a Result object or thrown. + /// + /// When true, an OperationCanceledException is rethrown immediately. + /// When false, cancellation is handled like any other failure. + /// + internal static Result Handle(this Exception exception, Options options, bool throwCanceled = true) + { + ThrowIfCanceled(exception, throwCanceled); + if (options.ThrowErrorOnFailure) ThrowBaseException(exception, options.ErrorMessageOnFailure); + + return ReturnResult(exception, options.ErrorMessageOnFailure); + } + + private static void ThrowIfCanceled(Exception exception, bool throwCanceled = true) + { + if (throwCanceled && exception is OperationCanceledException) throw exception; + } + + private static void ThrowBaseException(Exception exception, string customMessage = null) + { + if (string.IsNullOrEmpty(customMessage)) + throw new Exception(exception.Message, exception); + + throw new Exception(customMessage, exception); + } + + private static Result ReturnResult(Exception exception, string customMessage = null) + { + var errorMessage = string.IsNullOrEmpty(customMessage) + ? exception.Message + : $"{customMessage}: {exception.Message}"; + + return new Result + { + Success = false, + Error = new Error + { + Message = errorMessage, + AdditionalInfo = exception, + }, + }; + } +} diff --git a/Frends.JSON.Query/Frends.JSON.Query/Query.cs b/Frends.JSON.Query/Frends.JSON.Query/Query.cs index 8b45bab..3ac75ef 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Query.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Query.cs @@ -1,4 +1,5 @@ using Frends.JSON.Query.Definitions; +using Frends.JSON.Query.Helpers; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -44,30 +45,11 @@ public static Result Query([PropertyTab] Input input, [PropertyTab] Options opti if (tokens.Count == 0 && options.ErrorWhenNotMatched) throw new JsonException($"No matches found for query '{input.Query}'."); - return new Result(true, tokens); - } - catch (OperationCanceledException) - { - throw; + return new Result { Success = true, Data = tokens }; } catch (Exception ex) { - if (options.ThrowErrorOnFailure) - { - if (string.IsNullOrEmpty(options.ErrorMessageOnFailure)) - throw new Exception(ex.Message, ex); - throw new Exception(options.ErrorMessageOnFailure, ex); - } - - var errorMessage = string.IsNullOrEmpty(options.ErrorMessageOnFailure) - ? ex.Message - : $"{options.ErrorMessageOnFailure}: {ex.Message}"; - - return new Result(new Error - { - Message = errorMessage, - AdditionalInfo = ex, - }); + return ex.Handle(options); } } From 4c0380d1f23eafa0987de640e4cfcc9f60b2a538 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 08:31:58 +0000 Subject: [PATCH 4/4] Add ErrorHandlerTests.cs; remove inline error-handler test from UnitTests.cs --- .../ErrorHandlerTests.cs | 49 +++++++++++++++++++ .../Frends.JSON.Query.UnitTests/UnitTests.cs | 22 --------- 2 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs diff --git a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs new file mode 100644 index 0000000..c080d95 --- /dev/null +++ b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs @@ -0,0 +1,49 @@ +using Frends.JSON.Query.Definitions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading; + +namespace Frends.JSON.Query.UnitTests; + +[TestClass] +public class ErrorHandlerTests +{ + private const string CustomErrorMessage = "CustomErrorMessage"; + + [TestMethod] + public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True() + { + var ex = Assert.ThrowsException(() => + JSON.Query(DefaultInput(), DefaultOptions(), CancellationToken.None)); + Assert.IsNotNull(ex); + } + + [TestMethod] + public void Should_Return_Failed_Result_When_ThrowErrorOnFailure_Is_False() + { + var options = DefaultOptions(); + options.ThrowErrorOnFailure = false; + var result = JSON.Query(DefaultInput(), options, CancellationToken.None); + Assert.IsFalse(result.Success); + } + + [TestMethod] + public void Should_Use_Custom_ErrorMessageOnFailure() + { + var options = DefaultOptions(); + options.ErrorMessageOnFailure = CustomErrorMessage; + var ex = Assert.ThrowsException(() => + JSON.Query(DefaultInput(), options, CancellationToken.None)); + Assert.IsNotNull(ex); + StringAssert.Contains(ex.Message, CustomErrorMessage); + } + + private static Input DefaultInput() => new() + { + Json = "{\"key\":\"value\"}", + Query = "$.nonexistent" + }; + + private static Options DefaultOptions() => + new() { ErrorWhenNotMatched = true, ThrowErrorOnFailure = true, ErrorMessageOnFailure = string.Empty }; +} diff --git a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs index 7d7bfb5..2c04011 100644 --- a/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs +++ b/Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs @@ -117,26 +117,4 @@ public void QueryShouldNotThrowIfOptionNotSetAndNothingIsFound() Assert.IsNotNull(result.Data); Assert.IsFalse(result.Data.Any()); } - - [TestMethod] - public void QueryShouldReturnErrorResultWhenThrowErrorOnFailureIsFalse() - { - var input = new Input() - { - Json = jsonString, - Query = "$..Products[?(@.Price >= 1000)].Name" - }; - - var options = new Options() - { - ErrorWhenNotMatched = true, - ThrowErrorOnFailure = false, - }; - - var result = JSON.Query(input, options, CancellationToken.None); - Assert.IsFalse(result.Success); - Assert.IsNotNull(result.Error); - Assert.IsFalse(string.IsNullOrEmpty(result.Error.Message)); - Assert.IsNotNull(result.Error.AdditionalInfo); - } } \ No newline at end of file