Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Frends.JSON.Query/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [1.3.0] - 2026-07-31
### Changed
- 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.
Expand Down
49 changes: 49 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query.UnitTests/ErrorHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Frends.JSON.Query.Definitions;
using NUnit.Framework;
using System;
using System.Threading;

namespace Frends.JSON.Query.UnitTests;

[TestFixture]
public class ErrorHandlerTests
{
private const string CustomErrorMessage = "CustomErrorMessage";

[Test]
public void Should_Throw_Error_When_ThrowErrorOnFailure_Is_True()
{
var ex = Assert.Throws<Exception>(() =>
JSON.Query(DefaultInput(), DefaultOptions(), CancellationToken.None));
Assert.That(ex, Is.Not.Null);
}

[Test]
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.That(result.Success, Is.False);
}

[Test]
public void Should_Use_Custom_ErrorMessageOnFailure()
{
var options = DefaultOptions();
options.ErrorMessageOnFailure = CustomErrorMessage;
var ex = Assert.Throws<Exception>(() =>
JSON.Query(DefaultInput(), options, CancellationToken.None));
Assert.That(ex, Is.Not.Null);
Assert.That(ex!.Message, Does.Contain(CustomErrorMessage));
}

private static Input DefaultInput() => new()
{
Json = "{\"key\":\"value\"}",
Query = "$.nonexistent"
};

private static Options DefaultOptions() =>
new() { ErrorWhenNotMatched = true, ThrowErrorOnFailure = true, ErrorMessageOnFailure = string.Empty };
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<IsPackable>false</IsPackable>
<PackageProjectUrl>https://frends.com/</PackageProjectUrl>
<Product>Frends</Product>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
<PackageReference Include="coverlet.collector" Version="3.1.2" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
</ItemGroup>

<ItemGroup>
Expand Down
9 changes: 5 additions & 4 deletions Frends.JSON.Query/Frends.JSON.Query.UnitTests/UnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Threading;

namespace Frends.JSON.Query.UnitTests;

Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -94,7 +95,7 @@ public void QueryShouldThrowIfOptionSetAndFilterMatchesNothing()
ErrorWhenNotMatched = true,
};

Assert.ThrowsException<JsonException>(() => JSON.Query(input, options));
Assert.ThrowsException<Exception>(() => JSON.Query(input, options, CancellationToken.None));
}

[TestMethod]
Expand All @@ -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());
Expand Down
19 changes: 19 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query/Definitions/Error.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace Frends.JSON.Query.Definitions;

/// <summary>
/// Error information returned when the task fails and ThrowErrorOnFailure is false.
/// </summary>
public class Error
{
/// <summary>
/// Error message.
/// </summary>
public string Message { get; internal set; }

/// <summary>
/// The exception that caused the error.
/// </summary>
public Exception AdditionalInfo { get; internal set; }
}
18 changes: 17 additions & 1 deletion Frends.JSON.Query/Frends.JSON.Query/Definitions/Options.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace Frends.JSON.Query.Definitions;
using System.ComponentModel;

namespace Frends.JSON.Query.Definitions;

/// <summary>
/// Options parameters.
Expand All @@ -10,4 +12,18 @@ public class Options
/// </summary>
/// <example>true</example>
public bool ErrorWhenNotMatched { get; set; }

/// <summary>
/// If true, exceptions are thrown on failure. If false, errors are returned as part of the result.
/// </summary>
/// <example>true</example>
[DefaultValue(true)]
public bool ThrowErrorOnFailure { get; set; } = true;

/// <summary>
/// Custom error message to use when an error occurs. Leave empty to use the original exception message.
/// </summary>
/// <example></example>
[DefaultValue("")]
public string ErrorMessageOnFailure { get; set; } = string.Empty;
}
13 changes: 6 additions & 7 deletions Frends.JSON.Query/Frends.JSON.Query/Definitions/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,16 @@ public class Result
/// Operation complete without errors.
/// </summary>
/// <example>true</example>
public bool Success { get; private set; }
public bool Success { get; internal set; }

/// <summary>
/// Result data.
/// </summary>
/// <example>[ { Foo }, { Bar } ]</example>
public IEnumerable<object> Data { get; private set; }
public IEnumerable<object> Data { get; internal set; }

internal Result(bool success, IEnumerable<object> data)
{
Success = success;
Data = data;
}
/// <summary>
/// Error information, populated when the task fails and ThrowErrorOnFailure is false.
/// </summary>
public Error Error { get; internal set; }
}
4 changes: 2 additions & 2 deletions Frends.JSON.Query/Frends.JSON.Query/Frends.JSON.Query.csproj
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0</TargetFrameworks>
<Version>1.2.0</Version>
<TargetFrameworks>net8.0</TargetFrameworks>
<Version>1.3.0</Version>
<Authors>Frends</Authors>
<Copyright>Frends</Copyright>
<Company>Frends</Company>
Expand Down
54 changes: 54 additions & 0 deletions Frends.JSON.Query/Frends.JSON.Query/Helpers/ErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Frends.JSON.Query.Definitions;
using System;

namespace Frends.JSON.Query.Helpers;

internal static class ErrorHandler
{
/// <summary>
/// Handle an exception according to the task options.
/// </summary>
/// <param name="exception">The exception to handle.</param>
/// <param name="options">Task options that control whether failures are returned as a Result object or thrown.</param>
/// <param name="throwCanceled">
/// When true, an OperationCanceledException is rethrown immediately.
/// When false, cancellation is handled like any other failure.
/// </param>
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,
},
};
}
}
28 changes: 20 additions & 8 deletions Frends.JSON.Query/Frends.JSON.Query/Query.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// JSON Task.
/// </summary>
public class JSON
public static class JSON
{
/// Mem cleanup.
static JSON()
Expand All @@ -29,16 +32,25 @@ static JSON()
/// </summary>
/// <param name="input">Input parameters</param>
/// <param name="options">Optional parameter.</param>
/// <returns>Object { bool Success, IEnumerable&lt;object&gt; Data }</returns>
public static Result Query([PropertyTab] Input input, [PropertyTab] Options options)
/// <param name="cancellationToken">Token generated by frends to stop this Task.</param>
/// <returns>Object { bool Success, IEnumerable&lt;object&gt; Data, Error Error }</returns>
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)
Comment thread
MatteoDelOmbra marked this conversation as resolved.
{
return ex.Handle(options);
}
}

private static object GetJTokenFromInput(dynamic json)
Expand Down
Loading