Skip to content

Commit

Permalink
Refactored exception handling classes
Browse files Browse the repository at this point in the history
  • Loading branch information
aotroshko committed Dec 21, 2023
1 parent e931320 commit c7ef26d
Show file tree
Hide file tree
Showing 32 changed files with 422 additions and 240 deletions.
11 changes: 6 additions & 5 deletions Fauna.Test/Client.Tests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Fauna.Constants;
using Fauna.Exceptions;
using Fauna.Serialization;
using NUnit.Framework;
using System.Buffers;
Expand Down Expand Up @@ -85,13 +86,13 @@ public async Task CreateClientError()

try
{
var r = await c.QueryAsync<string>(
var r = await c.QueryAsync<int>(
new QueryExpr(new QueryLiteral($"let x = {expected}; abort(x)")),
new QueryOptions { QueryTags = new Dictionary<string, string> { { "foo", "bar" }, { "baz", "luhrmann" } } });
}
catch (FaunaAbortException ex)
catch (AbortException ex)
{
var abortData = ex.GetData<int>();
var abortData = ex.GetData();
Assert.AreEqual("abort", ex.QueryFailure?.ErrorInfo.Code);
Assert.IsInstanceOf<int>(abortData);
Assert.AreEqual(expected, abortData);
Expand Down Expand Up @@ -132,9 +133,9 @@ public async Task AbortReturnsQueryFailureAndThrows()
var query = new QueryExpr(new QueryLiteral($"let x = {expected}; abort(x)"));
var r = await c.QueryAsync<string>(query);
}
catch (FaunaAbortException ex)
catch (AbortException ex)
{
var abortData = ex.GetData<int>();
var abortData = ex.GetData();
Assert.AreEqual("abort", ex.QueryFailure?.ErrorInfo.Code);
Assert.IsInstanceOf<int>(abortData);
Assert.AreEqual(expected, abortData);
Expand Down
143 changes: 143 additions & 0 deletions Fauna.Test/Exceptions/AbortException.Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using Fauna.Exceptions;
using NUnit.Framework;

namespace Fauna.Tests;

[TestFixture]
public class AbortExceptionTests
{
private class TestClass
{
public string Name { get; set; }

Check warning on line 11 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (8.0.x)

Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
public int Age { get; set; }

}

[Test]
public void Ctor_InitializesPropertiesCorrectly()
{
var expectedQueryFailure = CreateQueryFailure(@"{{\""@int\"":\""123\""}}");
var expectedMessage = "Test message";
var expectedInnerException = new Exception("Inner exception");

var actual1 = new AbortException(expectedQueryFailure, expectedMessage);
var actual2 = new AbortException(expectedQueryFailure, expectedMessage, expectedInnerException);

Assert.AreEqual(expectedQueryFailure.ErrorInfo.Abort, actual1.QueryFailure.ErrorInfo.Abort);
Assert.AreEqual(expectedMessage, actual1.Message);

Assert.AreEqual(expectedQueryFailure.ErrorInfo.Abort, actual2.QueryFailure.ErrorInfo.Abort);
Assert.AreEqual(expectedMessage, actual2.Message);
Assert.AreEqual(expectedInnerException, actual2.InnerException);
}

[Test]
public void GetData_WithIntData_ReturnsDeserializedObject()
{
var expected = 123;
var queryFailure = CreateQueryFailure(@$"{{\""@int\"":\""{expected}\""}}");
var exception = new AbortException(queryFailure, "Test message");

var actual = exception.GetData();

Assert.IsNotNull(actual);
Assert.IsInstanceOf<int>(actual);
Assert.AreEqual(expected, (int)actual!);
}


[Test]
public void GetDataT_WithIntData_ReturnsDeserializedTypedObject()
{
var expected = 123;
var queryFailure = CreateQueryFailure(@$"{{\""@int\"":\""{expected}\""}}");
var exception = new AbortException(queryFailure, "Test message");

var actual = exception.GetData<int>();

Assert.IsNotNull(actual);
Assert.AreEqual(expected, actual);
}

[Test]
public void GetData_WithPocoData_ReturnsDeserializedObject()
{
var expected = new TestClass { Name = "John", Age = 105 };
var queryFailure = CreateQueryFailure(@$"{{\""@object\"":{{\""Name\"":\""{expected.Name}\"",\""Age\"":{{\""@int\"":\""{expected.Age}\""}}}}}}");
var exception = new AbortException(queryFailure, "Test message");

var actual = exception.GetData() as IDictionary<string, object>;

Assert.IsNotNull(actual);
Assert.AreEqual(expected.Name, actual["Name"]);

Check warning on line 72 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (6.0.x)

Dereference of a possibly null reference.

Check warning on line 72 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (8.0.x)

Dereference of a possibly null reference.
Assert.AreEqual(expected.Age, actual["Age"]);
}

[Test]
public void GetDataT_WithPocoData_ReturnsDeserializedTypedObject()
{
var expected = new TestClass { Name = "John", Age = 105 };
var queryFailure = CreateQueryFailure(@$"{{\""@object\"":{{\""Name\"":\""{expected.Name}\"",\""Age\"":{{\""@int\"":\""{expected.Age}\""}}}}}}");
var exception = new AbortException(queryFailure, "Test message");

var actual = exception.GetData<TestClass>();

Assert.IsNotNull(actual);
Assert.AreEqual(expected.Name, actual.Name);

Check warning on line 86 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (6.0.x)

Dereference of a possibly null reference.

Check warning on line 86 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (8.0.x)

Dereference of a possibly null reference.
Assert.AreEqual(expected.Age, actual.Age);
}

[Test]
public void GetData_WithNullAbortData_ReturnsNull()
{
var queryFailure = CreateQueryFailure(null);

Check warning on line 93 in Fauna.Test/Exceptions/AbortException.Tests.cs

View workflow job for this annotation

GitHub Actions / dotnet-test (6.0.x)

Cannot convert null literal to non-nullable reference type.
var exception = new AbortException(queryFailure, "Test message");

var result = exception.GetData();

Assert.IsNull(result);
}

[Test]
public void GetData_CachesDataCorrectly()
{
var mockData = new TestClass { Name = "John", Age = 105 };
var queryFailure = CreateQueryFailure(@$"{{\""@object\"":{{\""Name\"":\""{mockData.Name}\"",\""Age\"":{{\""@int\"":\""{mockData.Age}\""}}}}}}");
var exception = new AbortException(queryFailure, "Test message");

var callResult1 = exception.GetData<TestClass>();
var typedCallResult1 = exception.GetData();
var callResult2 = exception.GetData<TestClass>();
var typedCallResult2 = exception.GetData();

Assert.AreSame(callResult1, callResult2);
Assert.AreSame(typedCallResult1, typedCallResult2);
Assert.AreNotSame(callResult1, typedCallResult1);
Assert.AreNotSame(callResult2, typedCallResult2);
}

private QueryFailure CreateQueryFailure(string abortData)
{
var rawResponseText = $@"{{
""error"": {{
""code"": ""abort"",
""message"": ""Query aborted."",
""abort"": ""{abortData}""
}},
""summary"": ""error: Query aborted."",
""txn_ts"": 1702346199930000,
""stats"": {{
""compute_ops"": 1,
""read_ops"": 0,
""write_ops"": 0,
""query_time_ms"": 105,
""contention_retries"": 0,
""storage_bytes_read"": 261,
""storage_bytes_write"": 0,
""rate_limits_hit"": []
}},
""schema_version"": 0
}}";
return new QueryFailure(rawResponseText);
}
}
37 changes: 22 additions & 15 deletions Fauna/Client/Client.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Fauna.Constants;
using Fauna.Exceptions;
using Fauna.Serialization;

namespace Fauna;
Expand Down Expand Up @@ -34,7 +35,7 @@ public async Task<QuerySuccess<T>> QueryAsync<T>(
{
if (query == null)
{
throw new FaunaClientException("Query cannot be null");
throw new ClientException("Query cannot be null");
}

var finalOptions = QueryOptions.GetFinalQueryOptions(_config.DefaultQueryOptions, queryOptions);
Expand All @@ -51,29 +52,35 @@ public async Task<QuerySuccess<T>> QueryAsync<T>(

throw failure.ErrorInfo.Code switch
{
// Authentication Errors
"unauthorized" => new FaunaServiceException(FormatMessage("Unauthorized"), failure),
// Auth Errors
"unauthorized" => new AuthenticationException(failure, FormatMessage("Unauthorized")),
"forbidden" => new AuthorizationException(failure, FormatMessage("Forbidden")),

// Query Errors
"invalid_query" => new FaunaQueryCheckException(FormatMessage("Invalid Query"), failure),
"invalid_argument" => new FaunaQueryRuntimeException(FormatMessage("Invalid Argument"), failure),
"abort" => new FaunaAbortException(FormatMessage("Abort"), failure),
"invalid_query" or
"invalid_function_definition" or
"invalid_identifier" or
"invalid_syntax" or
"invalid_type" => new QueryCheckException(failure, FormatMessage("Invalid Query")),
"invalid_argument" => new QueryRuntimeException(failure, FormatMessage("Invalid Argument")),
"abort" => new AbortException(failure, FormatMessage("Abort")),

// Request/Transaction Errors
"invalid_request" => new FaunaInvalidRequestException(FormatMessage("Invalid Request"), failure),
"contended_transaction" => new FaunaContendedTransactionException(FormatMessage("Contended Transaction"), failure),
"invalid_request" => new InvalidRequestException(failure, FormatMessage("Invalid Request")),
"contended_transaction" => new ContendedTransactionException(failure, FormatMessage("Contended Transaction")),

// Capacity Errors
"limit_exceeded" => new FaunaThrottlingException(FormatMessage("Limit Exceeded"), failure),
"time_limit_exceeded" => new FaunaQueryTimeoutException(FormatMessage("Time Limit Exceeded"), failure),
"limit_exceeded" => new ThrottlingException(failure, FormatMessage("Limit Exceeded")),
"time_limit_exceeded" => new QueryTimeoutException(failure, FormatMessage("Time Limit Exceeded")),

// Server/Network Errors
"internal_error" => new FaunaServiceException(FormatMessage("Internal Error"), failure),
"timeout" => new FaunaQueryTimeoutException(FormatMessage("Timeout"), failure),
"bad_gateway" => new FaunaNetworkException(FormatMessage("Bad Gateway")),
"gateway_timeout" => new FaunaNetworkException(FormatMessage("Gateway Timeout")),
"internal_error" => new ServiceException(failure, FormatMessage("Internal Error")),
"timeout" or
"time_out" => new QueryTimeoutException(failure, FormatMessage("Timeout")),
"bad_gateway" => new NetworkException(FormatMessage("Bad Gateway")),
"gateway_timeout" => new NetworkException(FormatMessage("Gateway Timeout")),

_ => new FaunaBaseException(FormatMessage("Unexpected Error")),
_ => new FaunaException(FormatMessage("Unexpected Error")),
};
}
else
Expand Down
27 changes: 12 additions & 15 deletions Fauna/Client/Connection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net.Http.Headers;
using System.Security.Authentication;
using Fauna.Exceptions;
using System.Net.Http.Headers;
using System.Text.Json;
using AuthenticationException = System.Security.Authentication.AuthenticationException;

namespace Fauna;

Expand Down Expand Up @@ -50,46 +51,42 @@ public async Task<QueryResponse> DoPostAsync<T>(
}
catch (HttpRequestException ex)
{
throw new FaunaNetworkException(FormatMessage("Network Error", ex.Message), ex);
}
catch (TimeoutException ex)
{
throw new FaunaClientException(FormatMessage("Operation Timed Out", ex.Message), ex);
throw new NetworkException(FormatMessage("Network Error", ex.Message), ex);
}
catch (TaskCanceledException ex)
{
if (!ex.CancellationToken.IsCancellationRequested)
{
throw new FaunaClientException(FormatMessage("Operation Canceled", ex.Message), ex);
throw new ClientException(FormatMessage("Operation Canceled", ex.Message), ex);
}
else
{
throw new FaunaClientException(FormatMessage("Operation Timed Out", ex.Message), ex);
throw new ClientException(FormatMessage("Operation Timed Out", ex.Message), ex);
}
}
catch (ArgumentNullException ex)
{
throw new FaunaClientException(FormatMessage("Null Argument", ex.Message), ex);
throw new ClientException(FormatMessage("Null Argument", ex.Message), ex);
}
catch (InvalidOperationException ex)
{
throw new FaunaClientException(FormatMessage("Invalid Operation", ex.Message), ex);
throw new ClientException(FormatMessage("Invalid Operation", ex.Message), ex);
}
catch (JsonException ex)
{
throw new FaunaProtocolException(FormatMessage("Response Parsing Failed", ex.Message), ex);
throw new ProtocolException(FormatMessage("Response Parsing Failed", ex.Message), ex);
}
catch (AuthenticationException ex)
{
throw new FaunaClientException(FormatMessage("Authentication Failed", ex.Message), ex);
throw new ClientException(FormatMessage("Authentication Failed", ex.Message), ex);
}
catch (NotSupportedException ex)
{
throw new FaunaClientException(FormatMessage("Not Supported Operation", ex.Message), ex);
throw new ClientException(FormatMessage("Not Supported Operation", ex.Message), ex);
}
catch (Exception ex)
{
throw new FaunaBaseException(FormatMessage("Unexpected Error", ex.Message), ex);
throw new FaunaException(FormatMessage("Unexpected Error", ex.Message), ex);
}
}
}
69 changes: 69 additions & 0 deletions Fauna/Exceptions/AbortException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Fauna.Serialization;

namespace Fauna.Exceptions;

/// <summary>
/// Represents an exception that occurs when the FQL `abort` function is called.
/// This exception captures the data provided during the abort operation.
/// </summary>
public class AbortException : QueryRuntimeException
{
private readonly Dictionary<Type, object?> cache = new();
private static readonly Type NonTypedKey = typeof(object);

/// <summary>
/// Initializes a new instance of the <see cref="AbortException"/> class with a specified error message and query failure details.
/// </summary>
/// <param name="queryFailure">The <see cref="QueryFailure"/> object containing details about the query failure.</param>
/// <param name="message">The error message that explains the reason for the exception.</param>
public AbortException(QueryFailure queryFailure, string message)
: base(queryFailure, message) { }

/// <summary>
/// Initializes a new instance of the <see cref="AbortException"/> class with a specified error message, a reference to the inner exception, and query failure details.
/// </summary>
/// <param name="queryFailure">The <see cref="QueryFailure"/> object containing details about the query failure.</param>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
public AbortException(QueryFailure queryFailure, string message, Exception innerException)
: base(queryFailure, message, innerException) { }

/// <summary>
/// Retrieves the deserialized data associated with the abort operation as an object.
/// </summary>
/// <returns>The deserialized data as an object, or null if no data is available.</returns>
public object? GetData()
{
if (!cache.TryGetValue(NonTypedKey, out var cachedData))
{
var abortDataString = QueryFailure.ErrorInfo.Abort.ToString();
if (!string.IsNullOrEmpty(abortDataString))
{
cachedData = Serializer.Deserialize(abortDataString);
cache[NonTypedKey] = cachedData;
}
}
return cachedData;
}

/// <summary>
/// Retrieves the deserialized data associated with the abort operation as a specific type.
/// </summary>
/// <typeparam name="T">The type to which the data should be deserialized.</typeparam>
/// <returns>The deserialized data as the specified type, or null if no data is available.</returns>
public T? GetData<T>()
{
var typeKey = typeof(T);
if (!cache.TryGetValue(typeKey, out var cachedData))
{
var abortDataString = QueryFailure.ErrorInfo.Abort.ToString();
if (!string.IsNullOrEmpty(abortDataString))
{
T? deserializedResult = Serializer.Deserialize<T>(abortDataString);
cache[typeKey] = deserializedResult;
return deserializedResult;
}
}
return (T?)cachedData;
}
}
10 changes: 10 additions & 0 deletions Fauna/Exceptions/AuthenticationException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Fauna.Exceptions;

public class AuthenticationException : ServiceException
{
public AuthenticationException(QueryFailure queryFailure, string message)
: base(queryFailure, message) { }

public AuthenticationException(QueryFailure queryFailure, string message, Exception innerException)
: base(queryFailure, message, innerException) { }
}
Loading

0 comments on commit c7ef26d

Please sign in to comment.