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
32 changes: 18 additions & 14 deletions Nickvision.Desktop.Tests/JsonFileServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Nickvision.Desktop.Filesystem;
using Nickvision.Desktop.Tests.Mocks;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;

namespace Nickvision.Desktop.Tests;
Expand All @@ -18,6 +19,10 @@ public Config()
}
}

[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)]
[JsonSerializable(typeof(Config))]
internal partial class TestJsonContext : JsonSerializerContext { }

[TestClass]
public sealed class JsonFileServiceTests
{
Expand All @@ -29,13 +34,12 @@ public static void ClassInitialize(TestContext context)
var configPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json");
var configAsyncPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json");
Directory.CreateDirectory(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests"));
if (File.Exists(configPath))
foreach (var path in new[] { configPath, configAsyncPath })
{
File.Delete(configPath);
}
if (File.Exists(configAsyncPath))
{
File.Delete(configAsyncPath);
if (File.Exists(path))
{
File.Delete(path);
}
}
}

Expand All @@ -51,7 +55,7 @@ public void Case001_Initialize()
public void Case002_Load()
{
Assert.IsNotNull(_jsonFileService);
var config = _jsonFileService.Load<Config>();
var config = _jsonFileService.Load(TestJsonContext.Default.Config);
Assert.IsNotNull(config);
Assert.IsFalse(config.DarkModeEnabled);
Assert.AreEqual(900, config.WindowGeometry.Width);
Expand All @@ -63,7 +67,7 @@ public void Case002_Load()
public async Task Case003_LoadAsync()
{
Assert.IsNotNull(_jsonFileService);
var configAsync = await _jsonFileService.LoadAsync<Config>("config-async");
var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async");
Assert.IsNotNull(configAsync);
Assert.IsFalse(configAsync.DarkModeEnabled);
Assert.AreEqual(900, configAsync.WindowGeometry.Width);
Expand All @@ -75,29 +79,29 @@ public async Task Case003_LoadAsync()
public void Case004_Change()
{
Assert.IsNotNull(_jsonFileService);
var config = _jsonFileService.Load<Config>();
var config = _jsonFileService.Load(TestJsonContext.Default.Config);
config.DarkModeEnabled = true;
Assert.IsTrue(config.DarkModeEnabled);
Assert.IsTrue(_jsonFileService.Save(config));
Assert.IsTrue(_jsonFileService.Save(config, TestJsonContext.Default.Config));
Assert.IsTrue(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json")));
}

[TestMethod]
public async Task Case005_ChangeAsync()
{
Assert.IsNotNull(_jsonFileService);
var configAsync = await _jsonFileService.LoadAsync<Config>("config-async");
var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async");
configAsync.DarkModeEnabled = true;
Assert.IsTrue(configAsync.DarkModeEnabled);
Assert.IsTrue(await _jsonFileService.SaveAsync(configAsync, "config-async"));
Assert.IsTrue(await _jsonFileService.SaveAsync(configAsync, TestJsonContext.Default.Config, "config-async"));
Assert.IsTrue(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json")));
}

[TestMethod]
public void Case006_Verify()
{
Assert.IsNotNull(_jsonFileService);
var config = _jsonFileService.Load<Config>();
var config = _jsonFileService.Load(TestJsonContext.Default.Config);
Assert.IsNotNull(config);
Assert.IsTrue(config.DarkModeEnabled);
}
Expand All @@ -106,7 +110,7 @@ public void Case006_Verify()
public async Task Case007_VerifyAsync()
{
Assert.IsNotNull(_jsonFileService);
var configAsync = await _jsonFileService.LoadAsync<Config>("config-async");
var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async");
Assert.IsNotNull(configAsync);
Assert.IsTrue(configAsync.DarkModeEnabled);
}
Expand Down
70 changes: 21 additions & 49 deletions Nickvision.Desktop/Application/UpdaterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ namespace Nickvision.Desktop.Application;
/// </summary>
public class UpdaterService : IDisposable, IUpdaterService
{
private static readonly JsonSerializerOptions JsonOptions;

private readonly ILogger<UpdaterService> _logger;
private readonly SHA256 _hasher;
private readonly GitHubClient _githubClient;
Expand All @@ -34,17 +32,6 @@ public class UpdaterService : IDisposable, IUpdaterService
private readonly string _name;
private readonly string _cacheReleasesPath;

/// <summary>
/// Statically constructs an UpdaterService.
/// </summary>
static UpdaterService()
{
JsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
};
}

/// <summary>
/// Constructs an UpdaterService.
/// </summary>
Expand Down Expand Up @@ -168,8 +155,8 @@ public async Task<bool> DownloadReleaseAssetAsync(AppVersion version, string pat
{
fileStream.Seek(0, SeekOrigin.Begin);
progress?.Report(new DownloadProgress(totalBytesToRead, totalBytesRead, true));
var assetWithDigest = await _httpClient.GetFromJsonAsync<GitHubReleaseAsset>(asset.Url, JsonOptions);
if(assetWithDigest is null)
var assetWithDigest = await _httpClient.GetFromJsonAsync(asset.Url, GitHubJsonContext.Default.GitHubReleaseAsset);
if (assetWithDigest is null)
{
_logger.LogError($"Failed to get asset information for {asset.Name} from GitHub API.");
return false;
Expand Down Expand Up @@ -300,7 +287,7 @@ public async Task<bool> WindowsApplicationUpdateAsync(AppVersion version, IProgr

private void Dispose(bool disposing)
{
if(!disposing)
if (!disposing)
{
return;
}
Expand All @@ -322,16 +309,30 @@ private async Task<IReadOnlyList<GitHubRelease>> GetReleasesAsync()
if (File.Exists(_cacheReleasesPath))
{
_logger.LogInformation($"Cache file found, loading releases from cache...");
releases = JsonSerializer.Deserialize<IReadOnlyList<GitHubRelease>>(await File.ReadAllTextAsync(_cacheReleasesPath)) ?? [];
releases = JsonSerializer.Deserialize(await File.ReadAllTextAsync(_cacheReleasesPath), GitHubJsonContext.Default.ListGitHubRelease) ?? [];
_logger.LogInformation($"Loaded {releases.Count} releases from cache.");
}
if (releases.Count == 0)
{
_logger.LogInformation($"No releases found in cache, fetching from GitHub API...");
var json = JsonSerializer.Serialize(await _githubClient.Repository.Release.GetAll(_owner, _name));
var octokitReleases = await _githubClient.Repository.Release.GetAll(_owner, _name);
var mappedReleases = octokitReleases.Select(r => new GitHubRelease
{
TagName = r.TagName,
Prerelease = r.Prerelease,
Draft = r.Draft,
Assets = r.Assets?.Select(a => new GitHubReleaseAsset
{
Url = a.Url,
Name = a.Name,
Size = a.Size,
BrowserDownloadUrl = a.BrowserDownloadUrl
}).ToList() ?? new List<GitHubReleaseAsset>()
}).ToList();
var json = JsonSerializer.Serialize(mappedReleases, GitHubJsonContext.Default.ListGitHubRelease);
await File.WriteAllTextAsync(_cacheReleasesPath, json);
File.SetLastWriteTimeUtc(_cacheReleasesPath, DateTime.UtcNow);
releases = JsonSerializer.Deserialize<IReadOnlyList<GitHubRelease>>(json) ?? [];
releases = mappedReleases;
_logger.LogInformation($"Fetched {releases.Count} releases from GitHub API and saved to cache.");
}
return releases;
Expand All @@ -344,36 +345,7 @@ private async Task<IReadOnlyList<GitHubRelease>> GetReleasesAsync()
}
}

internal class GitHubRelease
{
public string TagName { get; set; }
public bool Prerelease { get; set; }
public bool Draft { get; set; }
public List<GitHubReleaseAsset> Assets { get; set; }

public GitHubRelease()
{
TagName = string.Empty;
Prerelease = false;
Draft = false;
Assets = new List<GitHubReleaseAsset>();
}
}

internal class GitHubReleaseAsset
{
public string Url { get; set; }
public string Name { get; set; }
public long Size { get; set; }
public string Digest { get; set; }
public string BrowserDownloadUrl { get; set; }

public GitHubReleaseAsset()
{
Url = string.Empty;
Name = string.Empty;
Size = 0;
Digest = string.Empty;
BrowserDownloadUrl = string.Empty;
}
}

9 changes: 5 additions & 4 deletions Nickvision.Desktop/Converters/NullToDefaultObjectConverter.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Nickvision.Desktop.Converters;

public class NullToDefaultObjectConverter<T> : JsonConverter<T>
public class NullToDefaultObjectConverter<T> : JsonConverter<T> where T : new()
{
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return Activator.CreateInstance<T>();
return new T();
}
return JsonSerializer.Deserialize<T>(ref reader, options) ?? Activator.CreateInstance<T>();
return JsonSerializer.Deserialize(ref reader, (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T))) ?? new T();
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T)));
}
5 changes: 3 additions & 2 deletions Nickvision.Desktop/Converters/NullToDefaultValueConverter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;

namespace Nickvision.Desktop.Converters;

Expand All @@ -12,8 +13,8 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial
{
return default;
}
return JsonSerializer.Deserialize<T>(ref reader, options);
return JsonSerializer.Deserialize(ref reader, (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T)));
}

public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, (JsonTypeInfo<T>)options.GetTypeInfo(typeof(T)));
}
4 changes: 2 additions & 2 deletions Nickvision.Desktop/Converters/NullToEmptyStringConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
{
return string.Empty;
}
return JsonSerializer.Deserialize<string>(ref reader, options) ?? string.Empty;
return reader.GetString() ?? string.Empty;
}

public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
}
4 changes: 2 additions & 2 deletions Nickvision.Desktop/Converters/NullToFalseBoolConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer
{
return false;
}
return JsonSerializer.Deserialize<bool>(ref reader, options);
return reader.GetBoolean();
}

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) => writer.WriteBooleanValue(value);
}
4 changes: 2 additions & 2 deletions Nickvision.Desktop/Converters/NullToTrueBoolConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer
{
return true;
}
return JsonSerializer.Deserialize<bool>(ref reader, options);
return reader.GetBoolean();
}

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) => writer.WriteBooleanValue(value);
}
4 changes: 2 additions & 2 deletions Nickvision.Desktop/Converters/NullToZeroIntConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSeri
{
return 0;
}
return JsonSerializer.Deserialize<int>(ref reader, options);
return reader.GetInt32();
}

public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, options);
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => writer.WriteNumberValue(value);
}
16 changes: 9 additions & 7 deletions Nickvision.Desktop/Filesystem/IJsonFileService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Text.Json.Serialization.Metadata;
using System.Threading.Tasks;

namespace Nickvision.Desktop.Filesystem;
Expand All @@ -16,34 +17,35 @@ public interface IJsonFileService
/// <summary>
/// Loads a json file and deserializes it into an object.
/// </summary>
/// <param name="jsonTypeInfo">The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext</param>
/// <param name="name">The name of the json file (without the .json extension)</param>
/// <typeparam name="T">The type of the object to deserialize to</typeparam>
/// <returns>A deserialized object from the json file if successful, else a default constructed object</returns>
T Load<T>(string? name = null);

T Load<T>(JsonTypeInfo<T> jsonTypeInfo, string? name = null) where T : new();
/// <summary>
/// Loads a json file and deserializes it into an object asynchronously.
/// </summary>
/// <param name="jsonTypeInfo">The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext</param>
/// <param name="name">The name of the json file (without the .json extension)</param>
/// <typeparam name="T">The type of the object to deserialize to</typeparam>
/// <returns>A deserialized object from the json file if successful, else a default constructed object</returns>
Task<T> LoadAsync<T>(string? name = null);

Task<T> LoadAsync<T>(JsonTypeInfo<T> jsonTypeInfo, string? name = null) where T : new();
/// <summary>
/// Saves an object by serializing it into a json file.
/// </summary>
/// <param name="obj">The object to serialize</param>
/// <param name="jsonTypeInfo">The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext</param>
/// <param name="name">The name of the json file (without the .json extension)</param>
/// <typeparam name="T">The type of the object to serialize</typeparam>
/// <returns>True if the file was saved successfully, else false</returns>
bool Save<T>(T obj, string? name = null);

bool Save<T>(T obj, JsonTypeInfo<T> jsonTypeInfo, string? name = null);
/// <summary>
/// Saves an object by serializing it into a json file asynchronously.
/// </summary>
/// <param name="obj">The object to serialize</param>
/// <param name="jsonTypeInfo">The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext</param>
/// <param name="name">The name of the json file (without the .json extension)</param>
/// <typeparam name="T">The type of the object to serialize</typeparam>
/// <returns>True if the file was saved successfully, else false</returns>
Task<bool> SaveAsync<T>(T obj, string? name = null);
Task<bool> SaveAsync<T>(T obj, JsonTypeInfo<T> jsonTypeInfo, string? name = null);
}
Loading
Loading