Skip to content

Commit

Permalink
Feat/enable nullable reference types core (#52)
Browse files Browse the repository at this point in the history
* feat: enable nullable in Sinch.csproj

* refactor: use nullable in SinchClient.cs

* refactor: update example

* refactor: refs?(will refer with this to nullable ref types), use IHttp in loggers

* chore: disable unused variables warning in examples

* chore: add dictionary shared

* refactor(auth): refs?

* fix: don't throw nullref when T is null in JsonInterfaceConverter.cs

* refactor: use nullable logger factory

* chore: add comment for OAuth why suppress nulref

* fix: interface converter use object if value is null

* feat(tests): add tests for JsonInterfaceConverter
  • Loading branch information
Dovchik authored Apr 19, 2024
1 parent 4f30a74 commit 8bd6c13
Show file tree
Hide file tree
Showing 20 changed files with 209 additions and 116 deletions.
2 changes: 2 additions & 0 deletions Sinch.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=PSTN/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
3 changes: 3 additions & 0 deletions examples/Console/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[*]
resharper_unused_variable_highlighting=none
resharper_unused_variable_compiler_highlighting=none
1 change: 0 additions & 1 deletion examples/Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using DotNetEnv;
using Sinch;
using Sinch.Auth;

// Assume .env file is present in your output directory
Env.Load();
Expand Down
4 changes: 2 additions & 2 deletions examples/Console/UseServicePlanIdForSms.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public void Example()
var sinchClient = new SinchClient(default, default, default,
options =>
{
options.UseServicePlanIdWithSms(Environment.GetEnvironmentVariable("SINCH_SERVICE_PLAN_ID"),
Environment.GetEnvironmentVariable("SINCH_API_TOKEN"), SmsServicePlanIdHostingRegion.Ca);
options.UseServicePlanIdWithSms(Environment.GetEnvironmentVariable("SINCH_SERVICE_PLAN_ID")!,
Environment.GetEnvironmentVariable("SINCH_API_TOKEN")!, SmsServicePlanIdHostingRegion.Ca);
});
sinchClient.Sms.Batches.Send(new SendTextBatchRequest()
{
Expand Down
12 changes: 6 additions & 6 deletions src/Sinch/ApiError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@ namespace Sinch
{
internal sealed class ApiErrorResponse
{
public ApiError Error { get; set; }
public ApiError? Error { get; set; }

public string Code { get; set; }
public string? Code { get; set; }

public string Text { get; set; }
public string? Text { get; set; }
}

internal sealed class ApiError
{
public int Code { get; set; }

public string Message { get; set; }
public string? Message { get; set; }

public string Status { get; set; }
public string? Status { get; set; }

public List<JsonNode> Details { get; set; }
public List<JsonNode>? Details { get; set; }
}
}
14 changes: 7 additions & 7 deletions src/Sinch/Auth/ApplicationSignedAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ internal class ApplicationSignedAuth : ISinchAuth
{
private readonly string _appSecret;
private readonly string _appKey;
private byte[] _jsonBodyInBytes;
private string _httpVerb;
private string _requestContentType;
private string _requestPath;
private string _timestamp;
private byte[]? _jsonBodyInBytes;
private string? _httpVerb;
private string? _requestContentType;
private string? _requestPath;
private string? _timestamp;

public string Scheme { get; } = AuthSchemes.Application;

Expand All @@ -24,7 +24,7 @@ public ApplicationSignedAuth(string appKey, string appSecret)
}

public string GetSignedAuth(byte[] jsonBodyBytes, string httpVerb,
string requestPath, string timestamp, string contentType)
string requestPath, string timestamp, string? contentType)
{
_jsonBodyInBytes = jsonBodyBytes;
_httpVerb = httpVerb;
Expand All @@ -44,7 +44,7 @@ public Task<string> GetAuthToken(bool force = false)
encodedBody = Convert.ToBase64String(md5Bytes);
}

var toSign = new StringBuilder().AppendJoin('\n', _httpVerb.ToUpperInvariant(), encodedBody,
var toSign = new StringBuilder().AppendJoin('\n', _httpVerb?.ToUpperInvariant(), encodedBody,
_requestContentType,
_timestamp, _requestPath).ToString();

Expand Down
8 changes: 4 additions & 4 deletions src/Sinch/Auth/AuthApiError.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ namespace Sinch.Auth
{
internal class AuthApiError
{
public string Error { get; set; }
public string? Error { get; set; }

[JsonPropertyName("error_verbose")]
public string ErrorVerbose { get; set; }
public string? ErrorVerbose { get; set; }

[JsonPropertyName("error_description")]
public string ErrorDescription { get; set; }
public string? ErrorDescription { get; set; }

[JsonPropertyName("error_hint")]
public string ErrorHint { get; set; }
public string? ErrorHint { get; set; }
}
}
14 changes: 10 additions & 4 deletions src/Sinch/Auth/OAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ internal class OAuth : ISinchAuth
private readonly HttpClient _httpClient;
private readonly string _keyId;
private readonly string _keySecret;
private readonly ILoggerAdapter<OAuth> _logger;
private volatile string _token;
private readonly ILoggerAdapter<OAuth>? _logger;
private volatile string? _token;
private readonly Uri _baseAddress;

public string Scheme { get; } = AuthSchemes.Bearer;

public OAuth(string keyId, string keySecret, HttpClient httpClient, ILoggerAdapter<OAuth> logger, Uri baseAddress)
public OAuth(string keyId, string keySecret, HttpClient httpClient, ILoggerAdapter<OAuth>? logger,
Uri baseAddress)
{
_keyId = keyId;
_keySecret = keySecret;
Expand Down Expand Up @@ -80,7 +81,12 @@ public async Task<string> GetAuthToken(bool force = false)
private class AuthResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; }
#if NET7_0_OR_GREATER
public required string AccessToken { get; set; }
#else
public string AccessToken { get; set; } = null!;
#endif


/// <summary>
/// In seconds
Expand Down
12 changes: 6 additions & 6 deletions src/Sinch/Auth/SinchAuthException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ namespace Sinch.Auth
{
public sealed class SinchAuthException : HttpRequestException
{
private SinchAuthException(HttpStatusCode statusCode, string message, Exception inner) : base(message, inner,
private SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner) : base(message, inner,
statusCode)
{
}

internal SinchAuthException(HttpStatusCode statusCode, string message, Exception inner, AuthApiError authApiError)
internal SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner, AuthApiError? authApiError)
: this(statusCode, message, inner)
{
Error = authApiError?.Error;
Expand All @@ -20,12 +20,12 @@ internal SinchAuthException(HttpStatusCode statusCode, string message, Exception
ErrorVerbose = authApiError?.ErrorVerbose;
}

public string Error { get; }
public string? Error { get; }

public string ErrorVerbose { get; }
public string? ErrorVerbose { get; }

public string ErrorDescription { get; }
public string? ErrorDescription { get; }

public string ErrorHint { get; }
public string? ErrorHint { get; }
}
}
6 changes: 4 additions & 2 deletions src/Sinch/Core/EnumRecord.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class EnumRecordJsonConverter<T> : JsonConverter<T> where T : EnumRecord
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return Activator.CreateInstance(typeToConvert, reader.GetString()) as T;
return Activator.CreateInstance(typeToConvert, reader.GetString()) as T ??
throw new InvalidOperationException("Created instance is null");
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
Expand All @@ -28,7 +29,8 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions
public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
return Activator.CreateInstance(typeToConvert, reader.GetString()) as T;
return Activator.CreateInstance(typeToConvert, reader.GetString()) as T ??
throw new InvalidOperationException("Created instance is null");
}

public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
Expand Down
2 changes: 1 addition & 1 deletion src/Sinch/Core/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public static async Task EnsureSuccessApiStatusCode(this HttpResponseMessage htt
throw new SinchApiException(httpResponseMessage.StatusCode, httpResponseMessage.ReasonPhrase, null, apiError);
}

public static async Task<T> TryGetJson<T>(this HttpResponseMessage httpResponseMessage)
public static async Task<T?> TryGetJson<T>(this HttpResponseMessage httpResponseMessage)
{
var authResponse = default(T);
if (httpResponseMessage.IsJson()) authResponse = await httpResponseMessage.Content.ReadFromJsonAsync<T>();
Expand Down
32 changes: 20 additions & 12 deletions src/Sinch/Core/Http.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal interface IHttp
/// <param name="cancellationToken"></param>
/// <typeparam name="TResponse">The type of the response object.</typeparam>
/// <returns></returns>
Task<TResponse> Send<TResponse>(Uri uri, HttpMethod httpMethod,
Task<TResponse?> Send<TResponse>(Uri uri, HttpMethod httpMethod,
CancellationToken cancellationToken = default);

/// <summary>
Expand All @@ -43,7 +43,7 @@ Task<TResponse> Send<TResponse>(Uri uri, HttpMethod httpMethod,
/// <typeparam name="TRequest">The type of the request object.</typeparam>
/// <typeparam name="TResponse">The type of the response object.</typeparam>
/// <returns></returns>
Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpMethod, TRequest httpContent,
Task<TResponse?> Send<TRequest, TResponse>(Uri uri, HttpMethod httpMethod, TRequest httpContent,
CancellationToken cancellationToken = default);
}

Expand All @@ -52,12 +52,12 @@ internal class Http : IHttp
{
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly ILoggerAdapter<Http> _logger;
private readonly ILoggerAdapter<IHttp>? _logger;
private readonly ISinchAuth _auth;
private readonly string _userAgentHeaderValue;


public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter<Http> logger,
public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter<IHttp>? logger,
JsonNamingPolicy jsonNamingPolicy)
{
_logger = logger;
Expand All @@ -68,25 +68,25 @@ public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter<Http> logger,
PropertyNamingPolicy = jsonNamingPolicy,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var sdkVersion = new AssemblyName(typeof(Http).GetTypeInfo()!.Assembly!.FullName!).Version!.ToString();
var sdkVersion = new AssemblyName(typeof(Http).GetTypeInfo().Assembly.FullName!).Version!.ToString();
_userAgentHeaderValue =
$"sinch-sdk/{sdkVersion} (csharp/{RuntimeInformation.FrameworkDescription};;)";
}

public Task<TResponse> Send<TResponse>(Uri uri, HttpMethod httpMethod,
public Task<TResponse?> Send<TResponse>(Uri uri, HttpMethod httpMethod,
CancellationToken cancellationToken = default)
{
return Send<object, TResponse>(uri, httpMethod, null, cancellationToken);
}

public async Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpMethod, TRequest request,
public async Task<TResponse?> Send<TRequest, TResponse>(Uri uri, HttpMethod httpMethod, TRequest? request,
CancellationToken cancellationToken = default)
{
var retry = true;
while (true)
{
_logger?.LogDebug("Sending request to {uri}", uri);
HttpContent httpContent =
HttpContent? httpContent =
request == null ? null : JsonContent.Create(request, options: _jsonSerializerOptions);

#if DEBUG
Expand All @@ -108,10 +108,17 @@ public async Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpM
var now = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture);
const string headerName = "x-timestamp";
msg.Headers.Add(headerName, now);

var bytes = Array.Empty<byte>();
if (msg.Content is not null)
{
bytes = await msg.Content.ReadAsByteArrayAsync(cancellationToken);
}

token = appSignAuth.GetSignedAuth(
msg.Content?.ReadAsByteArrayAsync(cancellationToken).GetAwaiter().GetResult(),
bytes,
msg.Method.ToString().ToUpperInvariant(), msg.RequestUri.PathAndQuery,
$"{headerName}:{now}", msg.Content?.Headers?.ContentType?.ToString());
$"{headerName}:{now}", msg.Content?.Headers.ContentType?.ToString());
retry = false;
}
else
Expand All @@ -130,8 +137,8 @@ public async Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpM
{
// will not retry when no "expired" header for a token.
const string wwwAuthenticateHeader = "www-authenticate";
if (_auth.Scheme == AuthSchemes.Bearer && true == result.Headers?.Contains(wwwAuthenticateHeader) &&
false == result.Headers?.GetValues(wwwAuthenticateHeader)?.Contains("expired"))
if (_auth.Scheme == AuthSchemes.Bearer && result.Headers.Contains(wwwAuthenticateHeader) &&
!result.Headers.GetValues(wwwAuthenticateHeader).Contains("expired"))
{
_logger?.LogDebug("OAuth Unauthorized");
}
Expand All @@ -152,6 +159,7 @@ public async Task<TResponse> Send<TRequest, TResponse>(Uri uri, HttpMethod httpM

_logger?.LogWarning("Response is not json, but {content}",
await result.Content.ReadAsStringAsync(cancellationToken));

return default;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sinch/Core/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Sinch.Core
{
public static class JsonExtensions
{
public static object ToObject(this JsonElement element, Type type, JsonSerializerOptions options = null)
public static object? ToObject(this JsonElement element, Type type, JsonSerializerOptions? options = null)
{
var bufferWriter = new ArrayBufferWriter<byte>();
using (var writer = new Utf8JsonWriter(bufferWriter))
Expand Down
27 changes: 17 additions & 10 deletions src/Sinch/Core/JsonInterfaceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,38 @@ public JsonInterfaceConverterAttribute(Type convertedType) : base(convertedType)
}
}

public class InterfaceConverter<T> : JsonConverter<T> where T : class
public class InterfaceConverter<T> : JsonConverter<T?> where T : class
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var type = typeof(T);
var types = AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p));

var elem = JsonElement.ParseValue(ref reader);
dynamic obj = null;
dynamic? obj = null;
foreach (var @class in types)
{
if (elem.IsTypeOf(@class, options))
{
obj = elem.ToObject(@class, options);
break;
}
if (!elem.IsTypeOf(@class, options)) continue;

obj = elem.ToObject(@class, options);
break;
}

return (T)obj;
return (T?)obj;
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
{
// just serialize as usual null object
if (value is null)
{
JsonSerializer.Serialize(writer, value, typeof(object), options);
return;
}

// need getType to get an actual instance and not typeof<T> which is always the interface itself
var type = value.GetType();
JsonSerializer.Serialize(writer, value, type, options);
}
Expand Down
Loading

0 comments on commit 8bd6c13

Please sign in to comment.