Skip to content

Commit

Permalink
Merge pull request #85 from maxisoft/options_saver
Browse files Browse the repository at this point in the history
Improve ASF-FreeGames: JSON serialization, error handling, and HttpClient optimizations
  • Loading branch information
maxisoft authored Aug 8, 2024
2 parents 85b6dd0 + 2b7d29b commit 79fb49d
Show file tree
Hide file tree
Showing 11 changed files with 676 additions and 36 deletions.
58 changes: 58 additions & 0 deletions ASFFreeGames.Tests/Configurations/ASFFreeGamesOptionsSaverTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using ASFFreeGames.Configurations;
using Xunit;

namespace Maxisoft.ASF.Tests.Configurations;

public class ASFFreeGamesOptionsSaverTests {
[Fact]
#pragma warning disable CA1707
public async void SaveOptions_WritesValidJson_And_ParsesCorrectly() {
#pragma warning restore CA1707

// Arrange
ASFFreeGamesOptions options = new() {
RecheckInterval = TimeSpan.FromHours(1),
RandomizeRecheckInterval = true,
SkipFreeToPlay = false,
SkipDLC = true,
Blacklist = new HashSet<string> {
"game1",
"game2",
"a gamewith2xquote(\")'",
"game with strange char €$çêà /\\\n\r\t"
},
VerboseLog = null,
Proxy = "http://localhost:1080",
RedditProxy = "socks5://192.168.1.1:1081"
};

using MemoryStream memoryStream = new();

// Act
_ = await ASFFreeGamesOptionsSaver.SaveOptions(memoryStream, options).ConfigureAwait(false);

// Assert - Validate UTF-8 encoding
memoryStream.Position = 0;
Assert.NotEmpty(Encoding.UTF8.GetString(memoryStream.ToArray()));

// Assert - Parse JSON and access properties
memoryStream.Position = 0;
string json = Encoding.UTF8.GetString(memoryStream.ToArray());
ASFFreeGamesOptions? deserializedOptions = JsonSerializer.Deserialize<ASFFreeGamesOptions>(json);

Assert.NotNull(deserializedOptions);
Assert.Equal(options.RecheckInterval, deserializedOptions.RecheckInterval);
Assert.Equal(options.RandomizeRecheckInterval, deserializedOptions.RandomizeRecheckInterval);
Assert.Equal(options.SkipFreeToPlay, deserializedOptions.SkipFreeToPlay);
Assert.Equal(options.SkipDLC, deserializedOptions.SkipDLC);
Assert.Equal(options.Blacklist, deserializedOptions.Blacklist);
Assert.Equal(options.VerboseLog, deserializedOptions.VerboseLog);
Assert.Equal(options.Proxy, deserializedOptions.Proxy);
Assert.Equal(options.RedditProxy, deserializedOptions.RedditProxy);
}
}
10 changes: 9 additions & 1 deletion ASFFreeGames/Commands/CommandDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace ASFFreeGames.Commands {
// Implement the IBotCommand interface
internal sealed class CommandDispatcher(ASFFreeGamesOptions options) : IBotCommand {
internal sealed class CommandDispatcher(ASFFreeGamesOptions options) : IBotCommand, IDisposable {
// Declare a private field for the plugin options instance
private readonly ASFFreeGamesOptions Options = options ?? throw new ArgumentNullException(nameof(options));

Expand Down Expand Up @@ -51,5 +51,13 @@ internal sealed class CommandDispatcher(ASFFreeGamesOptions options) : IBotComma

return null; // Return null if an exception occurs or if no command is found
}

public void Dispose() {
foreach ((_, IBotCommand? value) in Commands) {
if (value is IDisposable disposable) {
disposable.Dispose();
}
}
}
}
}
20 changes: 15 additions & 5 deletions ASFFreeGames/Commands/FreeGamesCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,32 @@
using ASFFreeGames.Configurations;
using Maxisoft.ASF;
using Maxisoft.ASF.Configurations;
using Maxisoft.ASF.HttpClientSimple;
using Maxisoft.ASF.Reddit;
using SteamKit2;

namespace ASFFreeGames.Commands {
// Implement the IBotCommand interface
internal sealed class FreeGamesCommand : IBotCommand, IDisposable {
public void Dispose() => SemaphoreSlim?.Dispose();
internal sealed class FreeGamesCommand(ASFFreeGamesOptions options) : IBotCommand, IDisposable {
public void Dispose() {
if (HttpFactory.IsValueCreated) {
HttpFactory.Value.Dispose();
}

SemaphoreSlim?.Dispose();
}

internal const string SaveOptionsInternalCommandString = "_SAVEOPTIONS";
internal const string CollectInternalCommandString = "_COLLECT";

private static PluginContext Context => ASFFreeGamesPlugin.Context;

// Declare a private field for the plugin options instance
private ASFFreeGamesOptions Options;
private ASFFreeGamesOptions Options = options ?? throw new ArgumentNullException(nameof(options));

private readonly Lazy<SimpleHttpClientFactory> HttpFactory = new(() => new SimpleHttpClientFactory(options));

// Define a constructor that takes an plugin options instance as a parameter
public FreeGamesCommand(ASFFreeGamesOptions options) => Options = options ?? throw new ArgumentNullException(nameof(options));

/// <inheritdoc />
/// <summary>
Expand Down Expand Up @@ -207,7 +215,9 @@ private async Task<int> CollectGames(IEnumerable<Bot> bots, ECollectGameRequestS
ICollection<RedditGameEntry> games;

try {
games = await RedditHelper.GetGames(cancellationToken).ConfigureAwait(false);
#pragma warning disable CA2000
games = await RedditHelper.GetGames(HttpFactory.Value.CreateForReddit(), cancellationToken).ConfigureAwait(false);
#pragma warning restore CA2000
}
catch (Exception e) when (e is InvalidOperationException or JsonException or IOException or RedditServerException) {
if (Options.VerboseLog ?? false) {
Expand Down
2 changes: 2 additions & 0 deletions ASFFreeGames/Commands/GetIp/GetIPCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ internal sealed class GetIPCommand : IBotCommand {
}
}
catch (Exception e) when (e is JsonException or IOException) {
#pragma warning disable CA1863
return IBotCommand.FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, Strings.ErrorIsInvalid, e.Message));
#pragma warning restore CA1863
}

return null;
Expand Down
8 changes: 7 additions & 1 deletion ASFFreeGames/Configurations/ASFFreeGamesOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ public bool IsBlacklisted(in GameIdentifier gid) {

public bool IsBlacklisted(in Bot? bot) => bot is null || ((Blacklist.Count > 0) && Blacklist.Contains($"bot/{bot.BotName}"));
#endregion
}

#region proxy
[JsonPropertyName("proxy")]
public string? Proxy { get; set; }

[JsonPropertyName("redditProxy")]
public string? RedditProxy { get; set; }
#endregion
}
25 changes: 21 additions & 4 deletions ASFFreeGames/Configurations/ASFFreeGamesOptionsLoader.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
Expand Down Expand Up @@ -28,6 +29,8 @@ public static void Bind(ref ASFFreeGamesOptions options) {
options.SkipFreeToPlay = configurationRoot.GetValue("SkipFreeToPlay", options.SkipFreeToPlay);
options.SkipDLC = configurationRoot.GetValue("SkipDLC", options.SkipDLC);
options.RandomizeRecheckInterval = configurationRoot.GetValue("RandomizeRecheckInterval", options.RandomizeRecheckInterval);
options.Proxy = configurationRoot.GetValue("Proxy", options.Proxy);
options.RedditProxy = configurationRoot.GetValue("RedditProxy", options.RedditProxy);
}
finally {
Semaphore.Release();
Expand All @@ -38,6 +41,7 @@ private static IConfigurationRoot CreateConfigurationRoot() {
IConfigurationRoot configurationRoot = new ConfigurationBuilder()
.SetBasePath(Path.GetFullPath(BasePath))
.AddJsonFile(DefaultJsonFile, true, false)
.AddEnvironmentVariables("FREEGAMES_")
.Build();

return configurationRoot;
Expand All @@ -55,13 +59,26 @@ public static async Task Save(ASFFreeGamesOptions options, CancellationToken can
#pragma warning disable CA2007

// Use FileOptions.Asynchronous when creating a file stream for async operations
await using FileStream fs = new(path, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous);
await using FileStream fs = new(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 4096, FileOptions.Asynchronous);
#pragma warning restore CA2007
#pragma warning restore CAC001
using IMemoryOwner<byte> buffer = MemoryPool<byte>.Shared.Rent(checked(fs.Length > 0 ? (int) fs.Length + 1 : 1 << 15));
int read = await fs.ReadAsync(buffer.Memory, cancellationToken).ConfigureAwait(false);

// Use JsonSerializerOptions.PropertyNamingPolicy to specify the JSON property naming convention
await JsonSerializer.SerializeAsync(fs, options, cancellationToken: cancellationToken).ConfigureAwait(false);
fs.SetLength(fs.Position);
try {
fs.Position = 0;
fs.SetLength(0);
int written = await ASFFreeGamesOptionsSaver.SaveOptions(fs, options, true, cancellationToken).ConfigureAwait(false);
fs.SetLength(written);
}

catch (Exception) {
fs.Position = 0;
await fs.WriteAsync(buffer.Memory[..read], cancellationToken).ConfigureAwait(false);
fs.SetLength(read);

throw;
}
}
finally {
Semaphore.Release();
Expand Down
Loading

0 comments on commit 79fb49d

Please sign in to comment.