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/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/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..2c04011 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,7 +112,7 @@ 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()); 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..a44b97b 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs @@ -11,17 +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; } - internal Result(bool success, IEnumerable data) - { - Success = success; - Data = data; - } + /// + /// Error information, populated when the task fails and ThrowErrorOnFailure is false. + /// + public Error Error { get; internal set; } } \ 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/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 044ab4a..3ac75ef 100644 --- a/Frends.JSON.Query/Frends.JSON.Query/Query.cs +++ b/Frends.JSON.Query/Frends.JSON.Query/Query.cs @@ -1,18 +1,21 @@ using Frends.JSON.Query.Definitions; +using Frends.JSON.Query.Helpers; 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 +32,25 @@ 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 { Success = true, Data = tokens }; + } + catch (Exception ex) + { + return ex.Handle(options); + } } private static object GetJTokenFromInput(dynamic json)