Skip to content

Commit

Permalink
Merge pull request #3025 from mrixner/complex-settings
Browse files Browse the repository at this point in the history
Complex Settings
  • Loading branch information
marticliment authored Dec 1, 2024
2 parents 16c43bf + 1ad66de commit 5ee697f
Show file tree
Hide file tree
Showing 12 changed files with 537 additions and 92 deletions.
150 changes: 147 additions & 3 deletions src/UniGetUI.Core.Settings.Tests/SettingsTest.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
using System.Text.Json;
using UniGetUI.Core.Data;
using UniGetUI.Core.SettingsEngine;

namespace UniGetUI.Core.SettingsEgine.Tests
namespace UniGetUI.Core.SettingsEngine.Tests
{

public sealed class SerializableTestSub
{
public SerializableTestSub(string s, int c) { sub = s; count = c; }
public string sub { get; set; }
public int count { get; set; }
}
public sealed class SerializableTest
{
public SerializableTest(string t, int c, SerializableTestSub s) { title = t; count = c; sub = s; }
public string title { get; set; }
public int count { get; set; }
public SerializableTestSub sub { get; set; }
}

public class SettingsTest
{
[Theory]
Expand Down Expand Up @@ -64,5 +79,134 @@ public void TestValueSettings(string SettingName, string st1, string st2, string
Assert.Equal("", Settings.GetValue(SettingName));
Assert.False(File.Exists(Path.Join(CoreData.UniGetUIDataDirectory, SettingName)));
}

[Theory]
[InlineData("lsTestSetting1", new string[] { "UpdatedFirstValue", "RandomString1", "RandomTestValue", "AnotherRandomValue" }, new int[] { 9, 15, 21, 1001, 4567 }, new string[] { "itemA", "itemB", "itemC" })]
[InlineData("lsktjgshdfsd", new string[] { "newValue1", "updatedString", "emptyString", "randomSymbols@123" }, new int[] { 42, 23, 17, 98765, 3482 }, new string[] { "itemX", "itemY", "itemZ" })]
[InlineData("lsª", new string[] { "UniqueVal1", "NewVal2", "AnotherVal3", "TestVal4" }, new int[] { 123, 456, 789, 321, 654 }, new string[] { "item1", "item2", "item3" })]
[InlineData("lsTestSettingEntry With A Space", new string[] { "ChangedFirstValue", "AlteredSecondVal", "TestedValue", "FinalVal" }, new int[] { 23, 98, 456, 753, 951 }, new string[] { "testA", "testB", "testC" })]
[InlineData("lsVeryVeryLongTestSettingEntrySoTheClassCanReallyBeStressedOut", new string[] { "newCharacterSet\x99\x01\x02", "UpdatedRandomValue", "TestEmptyString", "FinalTestValue" }, new int[] { 0b11001100, 1234, 5678, 1000000 }, new string[] { "finalTest1", "finalTest2", "finalTest3" })]
public void TestListSettings(string SettingName, string[] ls1Array, int[] ls2Array, string[] ls3Array)
{
// Convert arrays to Lists manually
List<string> ls1 = ls1Array.ToList();
List<int> ls2 = ls2Array.ToList();
List<SerializableTest> ls3 = [];
foreach (var item in ls3Array.ToList())
{
ls3.Add(new SerializableTest(item, new Random().Next(), new SerializableTestSub(item + new Random().Next(), new Random().Next())));
}

Settings.ClearList(SettingName);
Assert.Empty(Settings.GetList<object>(SettingName) ?? ["this shouldn't be null; something's wrong"]);
Settings.SetList(SettingName, ls1);
Assert.NotEmpty(Settings.GetList<string>(SettingName) ?? []);
Assert.Equal(ls1[0], Settings.GetListItem<string>(SettingName, 0));
Assert.Equal(ls1[2], Settings.GetListItem<string>(SettingName, 2));
Assert.True(Settings.ListContains(SettingName, ls1[0]));
Assert.False(Settings.ListContains(SettingName, "this is not a test case"));
Assert.True(Settings.RemoveFromList(SettingName, ls1[0]));
Assert.False(Settings.ListContains(SettingName, ls1[0]));
Assert.False(Settings.RemoveFromList(SettingName, ls1[0]));
Assert.False(Settings.ListContains(SettingName, ls1[0]));
Assert.Equal(ls1[2], Settings.GetListItem<string>(SettingName, 1));
Settings.AddToList(SettingName, "this is now a test case");
Assert.Equal("this is now a test case", Settings.GetListItem<string>(SettingName, 3));
Assert.Null(Settings.GetListItem<string>(SettingName, 4));

Assert.Equal(Settings.GetListItem<string>(SettingName, 0), JsonSerializer.Deserialize<List<string>>(File.ReadAllText(Path.Join(CoreData.UniGetUIDataDirectory, $"{SettingName}.json")))[0]);

Settings.ClearList(SettingName);
Assert.Empty(Settings.GetList<object>(SettingName) ?? ["this shouldn't be null; something's wrong"]);

Settings.SetList(SettingName, ls2);
Assert.NotEmpty(Settings.GetList<int>(SettingName) ?? []);
Assert.Equal(ls2[0], Settings.GetListItem<int>(SettingName, 0));
Assert.False(Settings.ListContains(SettingName, -12000));
Assert.True(Settings.ListContains(SettingName, ls2[3]));
Assert.True(Settings.RemoveFromList(SettingName, ls2[0]));
Assert.False(Settings.ListContains(SettingName, ls2[0]));
Assert.False(Settings.RemoveFromList(SettingName, ls2[0]));
Assert.False(Settings.ListContains(SettingName, ls2[0]));

Settings.SetList(SettingName, ls3);
Assert.Equal(ls3.Count, Settings.GetList<SerializableTest>(SettingName)?.Count);
Assert.Equal(ls3[1].sub.sub, Settings.GetListItem<SerializableTest>(SettingName, 1)?.sub.sub);
Assert.True(Settings.RemoveFromList(SettingName, ls3[0]));
Assert.False(Settings.RemoveFromList(SettingName, ls3[0]));
Assert.Equal(ls3[1].sub.sub, Settings.GetListItem<SerializableTest>(SettingName, 0)?.sub.sub);
Settings.ClearList(SettingName); // Cleanup
Assert.Empty(Settings.GetList<object>(SettingName) ?? ["this shouldn't be null; something's wrong"]);
Assert.False(File.Exists(Path.Join(CoreData.UniGetUIDataDirectory, $"{SettingName}.json")));
}

[Theory]
[InlineData("dTestSetting1", new string[] { "UpdatedFirstValue", "RandomString1", "RandomTestValue", "AnotherRandomValue" }, new int[] { 9, 15, 21, 1001, 4567 }, new string[] { "itemA", "itemB", "itemC" })]
[InlineData("dktjgshdfsd", new string[] { "newValue1", "updatedString", "emptyString", "randomSymbols@123" }, new int[] { 42, 23, 17, 98765, 3482 }, new string[] { "itemX", "itemY", "itemZ" })]
[InlineData("dª", new string[] { "UniqueVal1", "NewVal2", "AnotherVal3", "TestVal4" }, new int[] { 123, 456, 789, 321, 654 }, new string[] { "item1", "item2", "item3" })]
[InlineData("dTestSettingEntry With A Space", new string[] { "ChangedFirstValue", "AlteredSecondVal", "TestedValue", "FinalVal" }, new int[] { 23, 98, 456, 753, 951 }, new string[] { "testA", "testB", "testC" })]
[InlineData("dVeryVeryLongTestSettingEntrySoTheClassCanReallyBeStressedOut", new string[] { "newCharacterSet\x99\x01\x02", "UpdatedRandomValue", "TestEmptyString", "FinalTestValue" }, new int[] { 0b11001100, 1234, 5678, 1000000 }, new string[] { "finalTest1", "finalTest2", "finalTest3" })]
public void TestDictionarySettings(string SettingName, string[] keyArray, int[] intArray, string[] strArray)
{
Dictionary<string, SerializableTest?> test = [];
Dictionary<string, string> emptyDictionary = [];
Dictionary<string, SerializableTest?> nonEmptyDictionary = [];
nonEmptyDictionary["this should not be null; something's wrong"] = null;

for (int idx = 0; idx < keyArray.Length; idx++)
{
test[keyArray[idx]] = new SerializableTest(
strArray[idx % strArray.Length],
intArray[idx % intArray.Length],
new SerializableTestSub(
strArray[(idx + 1) % strArray.Length],
intArray[(idx + 1) % intArray.Length]
)
);
}

string randStr = new Random().Next().ToString();
Settings.SetDictionaryItem(randStr, "key", 12);
Assert.Equal(12, Settings.GetDictionaryItem<string, int>(randStr, "key"));
Settings.SetDictionary(SettingName, test);
Assert.Equal(JsonSerializer.Serialize(test), File.ReadAllLines(Path.Join(CoreData.UniGetUIDataDirectory, $"{SettingName}.json"))[0]);
Assert.Equal(test[keyArray[0]]?.sub.count, Settings.GetDictionary<string, SerializableTest?>(SettingName)?[keyArray[0]]?.sub.count);
Assert.Equal(test[keyArray[1]]?.sub.count, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[1])?.sub.count);
Settings.SetDictionaryItem(SettingName, keyArray[0], test[keyArray[1]]);
Assert.Equal(test[keyArray[1]]?.sub.count, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[0])?.sub.count);
Assert.NotEqual(test[keyArray[0]]?.sub.count, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[0])?.sub.count);
Assert.Equal(test[keyArray[1]]?.sub.count, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[1])?.sub.count);
Assert.Equal(test[keyArray[1]]?.count, Settings.SetDictionaryItem(
SettingName,
keyArray[0],
new SerializableTest(
"this is not test data",
-12000,
new SerializableTestSub("this sub is not test data", -13000)
)
)?.count);
Assert.Equal(-12000, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[0])?.count);
Assert.Equal("this is not test data", Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[0])?.title);
Assert.Equal(-13000, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, keyArray[0])?.sub.count);
Settings.SetDictionaryItem(SettingName, "this is not a test data key", test[keyArray[0]]);
Assert.Equal(test[keyArray[0]]?.title, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, "this is not a test data key")?.title);
Assert.Equal(test[keyArray[0]]?.sub.count, Settings.GetDictionaryItem<string, SerializableTest?>(SettingName, "this is not a test data key")?.sub.count);
Assert.True(Settings.DictionaryContainsKey<string, SerializableTest?>(SettingName, "this is not a test data key"));
Assert.True(Settings.DictionaryContainsValue<string, SerializableTest?>(SettingName, test[keyArray[0]]));
Assert.NotNull(Settings.RemoveDictionaryKey<string, SerializableTest?>(SettingName, "this is not a test data key"));
Assert.Null(Settings.RemoveDictionaryKey<string, SerializableTest?>(SettingName, "this is not a test data key"));
Assert.False(Settings.DictionaryContainsKey<string, SerializableTest?>(SettingName, "this is not a test data key"));
Assert.False(Settings.DictionaryContainsValue<string, SerializableTest?>(SettingName, test[keyArray[0]]));
Assert.True(Settings.DictionaryContainsValue<string, SerializableTest?>(SettingName, test[keyArray[2]]));

Assert.Equal(
JsonSerializer.Serialize(Settings.GetDictionary<string, SerializableTest>(SettingName)),
File.ReadAllLines(Path.Join(CoreData.UniGetUIDataDirectory, $"{SettingName}.json"))[0]
);

Settings.ClearDictionary(SettingName); // Cleanup
Assert.Empty(Settings.GetDictionary<string, SerializableTest?>(SettingName) ?? nonEmptyDictionary);
Assert.False(File.Exists(Path.Join(CoreData.UniGetUIDataDirectory, $"{SettingName}.json")));
}
}
}
}
2 changes: 1 addition & 1 deletion src/UniGetUI.Core.Settings/SettingsEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace UniGetUI.Core.SettingsEngine
{
public static class Settings
public static partial class Settings
{
private static ConcurrentDictionary<string, bool> booleanSettings = new();
private static ConcurrentDictionary<string, string> valueSettings = new();
Expand Down
168 changes: 168 additions & 0 deletions src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using System.Collections.Concurrent;
using System.Text.Json;
using UniGetUI.Core.Data;
using UniGetUI.Core.Logging;

namespace UniGetUI.Core.SettingsEngine
{
public static partial class Settings
{
private static ConcurrentDictionary<string, Dictionary<object, object?>> dictionarySettings = new();

// Returns an empty dictionary if the setting doesn't exist and null if the types are invalid
private static Dictionary<K, V>? _getDictionary<K, V>(string setting)
where K : notnull
{
try
{
if (dictionarySettings.TryGetValue(setting, out Dictionary<object, object>? result))

Check warning on line 18 in src/UniGetUI.Core.Settings/SettingsEngine_Dictionaries.cs

View workflow job for this annotation

GitHub Actions / test-codebase

Argument of type 'Dictionary<object, object>' cannot be used as an output of type 'Dictionary<object, object?>' for parameter 'value' in 'bool ConcurrentDictionary<string, Dictionary<object, object?>>.TryGetValue(string key, out Dictionary<object, object?> value)' due to differences in the nullability of reference types.
{
// If the setting was cached
return result.ToDictionary(
kvp => (K)kvp.Key,
kvp => (V)kvp.Value
);
}
}
catch (InvalidCastException)
{
Logger.Error($"Tried to get a dictionary setting with a key of type {typeof(K)} and a value of type {typeof(V)}, which is not the type of the dictionary");
return null;
}

// Otherwhise, load the setting from disk and cache that setting
Dictionary<K, V> value = new();
if (File.Exists(Path.Join(CoreData.UniGetUIDataDirectory, $"{setting}.json")))
{
string result = File.ReadAllText(Path.Join(CoreData.UniGetUIDataDirectory, $"{setting}.json"));
try
{
if (result != "")
{
Dictionary<K, V>? item = JsonSerializer.Deserialize<Dictionary<K, V>>(result);
if (item is not null)
{
value = item;
}
}
}
catch (InvalidCastException)
{
Logger.Error($"Tried to get a dictionary setting with a key of type {typeof(K)} and a value of type {typeof(V)}, but the setting on disk ({result}) cannot be deserialized to that");
}
}

dictionarySettings[setting] = value.ToDictionary(
kvp => (object)kvp.Key,
kvp => (object?)kvp.Value
);
return value;
}

// Returns an empty dictionary if the setting doesn't exist and null if the types are invalid
public static IReadOnlyDictionary<K, V>? GetDictionary<K, V>(string setting)
where K : notnull
{
return _getDictionary<K, V>(setting);
}

public static void SetDictionary<K, V>(string setting, Dictionary<K, V> value)
where K : notnull
{
dictionarySettings[setting] = value.ToDictionary(
kvp => (object)kvp.Key,
kvp => (object?)kvp.Value
);

var file = Path.Join(CoreData.UniGetUIDataDirectory, $"{setting}.json");
try
{
if (value.Any()) File.WriteAllText(file, JsonSerializer.Serialize(value));
else if (File.Exists(file)) File.Delete(file);
}
catch (Exception e)
{
Logger.Error($"CANNOT SET SETTING DICTIONARY FOR setting={setting} [{string.Join(", ", value)}]");
Logger.Error(e);
}
}

public static V? GetDictionaryItem<K, V>(string setting, K key)
where K : notnull
{
Dictionary<K, V>? dictionary = _getDictionary<K, V>(setting);
if (dictionary == null || !dictionary.TryGetValue(key, out V? value)) return default;

return value;
}

// Also works as `Add`
public static V? SetDictionaryItem<K, V>(string setting, K key, V value)
where K : notnull
{
Dictionary<K, V>? dictionary = _getDictionary<K, V>(setting);
if (dictionary == null)
{
dictionary = new()
{
{ key, value }
};
SetDictionary(setting, dictionary);
return default;
}

if (dictionary.TryGetValue(key, out V? oldValue))
{
dictionary[key] = value;
SetDictionary(setting, dictionary);
return oldValue;
}
else
{
dictionary.Add(key, value);
SetDictionary(setting, dictionary);
return default;
}
}

public static V? RemoveDictionaryKey<K, V>(string setting, K key)
where K : notnull
{
Dictionary<K, V>? dictionary = _getDictionary<K, V>(setting);
if (dictionary == null) return default;

bool success = false;
if (dictionary.TryGetValue(key, out V? value))
{
success = dictionary.Remove(key);
SetDictionary(setting, dictionary);
}

if (!success) return default;
return value;
}

public static bool DictionaryContainsKey<K, V>(string setting, K key)
where K : notnull
{
Dictionary<K, V>? dictionary = _getDictionary<K, V>(setting);
if (dictionary == null) return false;

return dictionary.ContainsKey(key);
}

public static bool DictionaryContainsValue<K, V>(string setting, V value)
where K : notnull
{
Dictionary<K, V>? dictionary = _getDictionary<K, V>(setting);
if (dictionary == null) return false;

return dictionary.ContainsValue(value);
}

public static void ClearDictionary(string setting)
{
SetDictionary(setting, new Dictionary<object, object>());
}
}
}
Loading

0 comments on commit 5ee697f

Please sign in to comment.