diff --git a/Deployment/Configuration.cs b/Deployment/Configuration.cs deleted file mode 100644 index a2a2a14..0000000 --- a/Deployment/Configuration.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace Deployment; - -public static class ConfigurationBootstrapper -{ - private const string confpath = "appsettings.json"; - - public static T Load() where T : new() - { - if(File.Exists("appsettings.json")) - { - return JsonConvert.DeserializeObject(File.ReadAllText(confpath)) ?? new T(); - } - else - { - var toReturn = new T(); - File.WriteAllText(confpath, JsonConvert.SerializeObject(toReturn)); - return toReturn; - } - } -} diff --git a/Deployment/ConfigurationBootstrapper.cs b/Deployment/ConfigurationBootstrapper.cs new file mode 100644 index 0000000..a7ec107 --- /dev/null +++ b/Deployment/ConfigurationBootstrapper.cs @@ -0,0 +1,134 @@ +using System; +using System.Dynamic; +using System.Reflection; +using System.Runtime.CompilerServices; +using Newtonsoft.Json; + +[assembly: InternalsVisibleTo("deployment.tests")] +namespace greyn.Deployment +{ + public static class ConfigurationBootstrapper + { + private const string confpath = "appsettings.json"; + + public static T Load() where T : new() + { + if (File.Exists("appsettings.json")) + { + /* "aaaahhh this is the most abstract and reflective thing in the goddamn world aaaahhhhh next you'll make an ECS on a dynamic aaaahhh" + * Ok, yes. I share these fears. but. + * this way we can read a *very* arbitrary configuration file. + * some random external file's configuration values is a good reason to aim for very high flexibility, imo + * if the configuration expects new values we write them in, + * if you left other junk for whatever reason, we don't delete them. + */ + var actualConfig = JsonConvert.DeserializeObject(File.ReadAllText(confpath)) ?? new ExpandoObject(); + var toReturn = new T(); + populateExpando(toReturn, ref actualConfig); + File.WriteAllText(confpath, JsonConvert.SerializeObject(actualConfig, Formatting.Indented)); + + readFromExpando(ref toReturn, actualConfig); + + return toReturn; + } + else + { + var toReturn = new T(); + File.WriteAllText(confpath, JsonConvert.SerializeObject(toReturn)); + return toReturn; + } + } + + //TODO: make private but get tests to cooperate + public static void populateExpando(T config, ref ExpandoObject fromFile) + { + if (config == null) return; + + if (fromFile == null) + { + var expandoPopulated = new ExpandoObject(); + var dictionaryFromExpandoPopulated = (IDictionary)expandoPopulated; + foreach (var property in config.GetType().GetProperties()) + dictionaryFromExpandoPopulated.Add(property.Name, property.GetValue(config)); + expandoPopulated = (ExpandoObject)dictionaryFromExpandoPopulated; + fromFile = expandoPopulated; + return; + } + var dictionaryFromExpandoFromFile = (IDictionary)fromFile; + foreach (var property in config.GetType().GetProperties()) + { + if (dictionaryFromExpandoFromFile.TryGetValue(property.Name, out object? value)) + { + if (value != null) + { + if (property.PropertyType.GetMembers() == null) + { + var childProperty = (ExpandoObject)property.GetValue(fromFile); + populateExpando(property.GetValue(config), ref childProperty); + property.SetValue(fromFile, childProperty); + } + } + } + else + { + fromFile.TryAdd(property.Name, property.GetValue(config)); + } + } + } + public static void readFromExpando(ref T config, ExpandoObject readFromFile) + { + //TODO: read from Expando + } + + // private static T sync(ref T config, ref ExpandoObject readFromFile) + // { + // var fieldsFoundThisLevel = new List(); + + // if(config == null) + // { + // //TODO: it's entirely possible this is null. + // throw new ArgumentNullException(); + // } + // if(readFromFile == null) + // { + // //TODO: it's entirely possible this is null. + // throw new ArgumentNullException(); + // } + // var readUnExpandoed = (IDictionary)readFromFile; + // //if it's present on what was read + // //set it on Config + // //if it's not found on what was read, or type is different + + // foreach (var prop in config.GetType().GetProperties()) + // { + // fieldsFoundThisLevel.Add(prop.Name); + // if(readUnExpandoed.TryGetValue(prop.Name, out object? valueFromFile)) //so a configuration file can *set* a value *to null* + // { + // if(valueFromFile != null) + // { + // var defaultConfigValue = prop.GetValue(config); + // if (defaultConfigValue != null) + // { + // //expandofy, since we objectified it. + // var expandoFromFile = new ExpandoObject(); + // var dictionaryFromExpandoFromFile = (IDictionary)expandoFromFile; + // foreach (var property in valueFromFile.GetType().GetProperties()) + // dictionaryFromExpandoFromFile.Add(property.Name, property.GetValue(valueFromFile)); + // expandoFromFile = (ExpandoObject)dictionaryFromExpandoFromFile; + // //dive in, repeat + // sync(ref defaultConfigValue, ref expandoFromFile); + // //we've now Sync'd expandoFromFile and defaultConfigValue + // prop.SetValue(readFromFile, expandoFromFile); + // prop.SetValue(config, expandoFromFile); + // } + + // } + // } + // else + // { + // readFromFile.TryAdd(prop.Name, prop.GetValue(config)); + // } + // } + // } + } +} \ No newline at end of file diff --git a/Deployment/Deployment.csproj b/Deployment/Deployment.csproj index a9f8edf..21191a6 100644 --- a/Deployment/Deployment.csproj +++ b/Deployment/Deployment.csproj @@ -6,8 +6,12 @@ enable - - + + + + + + diff --git a/deploy-test/Config.cs b/deploy-test/Config.cs deleted file mode 100644 index 932f501..0000000 --- a/deploy-test/Config.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace deploy_test; -using Deployment; - -public class Config -{ - public string imavalue { get; set; } = "ask me anything"; - -} diff --git a/deploy-test/Program.cs b/deploy-test/Program.cs deleted file mode 100644 index 684320e..0000000 --- a/deploy-test/Program.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using Deployment; - -namespace deploy_test; - -class Program -{ - static void Main(string[] args) - { - var c = ConfigurationBootstrapper.Load(); - Console.WriteLine(c.imavalue); - - } -} diff --git a/deploy-test/deploy-test.csproj b/deploy-test/deploy-test.csproj deleted file mode 100644 index e000832..0000000 --- a/deploy-test/deploy-test.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - Exe - net8.0 - deploy_test - enable - enable - - - diff --git a/deployment.tests/ConfigTests.cs b/deployment.tests/ConfigTests.cs new file mode 100644 index 0000000..6231004 --- /dev/null +++ b/deployment.tests/ConfigTests.cs @@ -0,0 +1,476 @@ +using greyn.Deployment; +using System; +using System.Dynamic; +using System.Collections.Generic; +using System.Reflection; +using Newtonsoft.Json; +using System.Globalization; + +namespace deployment.tests; + +public class ConfigTests +{ + public class AConfigurationSubType + { + public double aValueType {get;set;} = 892.49; + public int aValueTypeButNotAField = 1429; + public Dictionary anEnumerableType {get; set;} = new Dictionary {{"test1", 420.69}, {"test2", 420.70}}; + } + public class AConfigurationType + { + public float aValueTypeButNotAField = 156.697f; + public string aField {get;set;} = "hi there, hello"; + public AConfigurationSubType subtyped = new AConfigurationSubType(); + public AConfigurationSubType subtyped2 = new AConfigurationSubType(); + } + + #region Config + private const string PerfectConfiguration = @"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"; + private const string EmptyConfiguration = "{}"; + private const string MalformedConfiguration = "{{}"; + private const string InvalidValueConfiguration_00001 = @"{ + aValueTypeButNotAField = ""796.651"", + }"; + #endregion + + private ExpandoObject parse(string confstr) + { + return JsonConvert.DeserializeObject(confstr) ?? new ExpandoObject(); + } + + [Test] + public void parseworks() + { + Assert.IsNotNull(parse(PerfectConfiguration)); + } + + [Test] + public void parseworksasexpected() + { + var expandoed = parse(PerfectConfiguration); + Assert.IsNotNull(expandoed); + var casted = (IDictionary)expandoed; + Assert.IsNotNull(casted); + //Console.WriteLine(JsonConvert.SerializeObject(casted, Formatting.Indented)); + } + + #region jokes + [Test] + public void fuckit_werealreadytestingthetest_letstestthattest() + { + parseworks(); + Assert.Pass(); + } + [Test] + public void more_tests_more_good() + { + fuckit_werealreadytestingthetest_letstestthattest(); + Assert.Pass(); + } + [Test] + public void what_do_you_mean_a_bug_escaped_tests() + { + more_tests_more_good(); + Assert.Pass(); + } + + [Test] + public void vibecheck() + { + Assert.NotZero(new Random().Next(0, 2)); + } + + [Test] + public void populateExpando_doesnt_explode() + { + var toReturn = new AConfigurationType(); + var actualConfig = parse(PerfectConfiguration); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(toReturn, ref actualConfig); + Assert.Pass(); + } + #endregion + + [Test] + public void populateExpando_getsmissing_property() + { + var actualConfig = parse(@"{ + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + Console.WriteLine(casted["aValueTypeButNotAField"]); + Assert.Pass(); + } + + [Test] + public void populateExpando_getsmissing_field() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + Console.WriteLine(casted["aField"]); + Assert.Pass(); + } + [Test] + public void populateExpando_getsmissing_subobject() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + Console.WriteLine(casted["subtyped"]); + Assert.Pass(); + } + [Test] + public void populateExpando_getsmissing_subobject_field() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: + { + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + var subtypeCasted = (IDictionary)casted["subtyped"]; + Console.WriteLine(subtypeCasted["aValueType"]); + Assert.Pass(); + } + [Test] + public void populateExpando_getsmissing_subobject_property() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + var subtypeCasted = (IDictionary)casted["subtyped"]; + Console.WriteLine(subtypeCasted["aValueTypeButNotAField"]); + Assert.Pass(); + } + [Test] + public void populateExpando_getsmissing_subobject_enumerable() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + var subtypeCasted = (IDictionary)casted["subtyped"]; + Console.WriteLine(subtypeCasted["anEnumerableType"]); + Assert.Pass(); + } + [Test] + public void populateExpando_doesntoverridenullednullable() + { + var actualConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: null, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + Assert.IsNull(casted["subtyped"]); + } + [Test] + public void populateExpando_doesntremoveextras() + { + var actualConfig = parse(@"{ + hiImHereToo: ""sup"", + aValueTypeButNotAField: 796.651, + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + Assert.IsNotNull(casted["hiImHereToo"]); + } + [Test] + public void populateExpando_populatesblank() + { + var actualConfig = parse("{}"); + greyn.Deployment.ConfigurationBootstrapper.populateExpando(new AConfigurationType(), ref actualConfig); + var casted = (IDictionary)actualConfig; + var subtypeCasted = (IDictionary)casted["subtyped"]; + Console.WriteLine(subtypeCasted["aValueTypeButNotAField"]); + Assert.Pass(); + } + [Test] + public void readfromExpando_doesnt_explode() + { + var toReturn = new AConfigurationType(); + var actualConfig = parse(PerfectConfiguration); + greyn.Deployment.ConfigurationBootstrapper.readFromExpando(ref toReturn, actualConfig); + Assert.Pass(); + } + [Test] + public void readfromExpando_does() + { + var readConfig = parse(PerfectConfiguration); + var conf = new AConfigurationType(); + greyn.Deployment.ConfigurationBootstrapper.readFromExpando(ref conf, readConfig); + Assert.AreEqual(conf.aValueTypeButNotAField, 796.651); + Assert.AreEqual(conf.aField, "I've decided"); + Assert.AreEqual(conf.subtyped.aValueType, 94.298); + Assert.AreEqual(conf.subtyped.aValueTypeButNotAField, 9241); + Assert.IsNotNull(conf.subtyped.anEnumerableType); + Assert.AreEqual(conf.subtyped.anEnumerableType["test3"], 420.71); + Assert.AreEqual(conf.subtyped.anEnumerableType["test4"], 420.72); + Assert.IsNotNull(conf.subtyped2); + } + [Test] + public void readfromExpando_acceptsnull() + { + var readConfig = parse(PerfectConfiguration); + var conf = new AConfigurationType(); + greyn.Deployment.ConfigurationBootstrapper.readFromExpando(ref conf, readConfig); + + Assert.AreEqual(conf.aValueTypeButNotAField, 796.651); + Assert.AreEqual(conf.aField, "I've decided"); + Assert.AreEqual(conf.subtyped.aValueType, 94.298); + Assert.AreEqual(conf.subtyped.aValueTypeButNotAField, 9241); + Assert.IsNotNull(conf.subtyped); + Assert.IsNull(conf.subtyped.anEnumerableType); + Assert.IsNotNull(conf.subtyped2); + Assert.AreEqual(conf.subtyped2.aValueType, 95.298); + Assert.AreEqual(conf.subtyped2.aValueTypeButNotAField, 9242); + Assert.IsNotNull(conf.subtyped2.anEnumerableType); + Assert.AreEqual(conf.subtyped2.anEnumerableType["test5"], 420.73); + Assert.AreEqual(conf.subtyped2.anEnumerableType["test6"], 420.74); + } + [Test] + public void readfromExpando_usesdefaultfornullablemissing() + { + var readConfig = parse(@"{ + aValueTypeButNotAField: 796.651, + + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + var conf = new AConfigurationType(); + greyn.Deployment.ConfigurationBootstrapper.readFromExpando(ref conf, readConfig); + Assert.AreEqual(conf.aValueTypeButNotAField, 796.651); + Assert.AreEqual(conf.aField, "hi there, hello"); + Assert.AreEqual(conf.subtyped.aValueType, 94.298); + Assert.AreEqual(conf.subtyped.aValueTypeButNotAField, 9241); + Assert.IsNotNull(conf.subtyped.anEnumerableType); + Assert.AreEqual(conf.subtyped.anEnumerableType["test3"], 420.71); + Assert.AreEqual(conf.subtyped.anEnumerableType["test4"], 420.72); + Assert.IsNotNull(conf.subtyped2); + } + [Test] + public void readfromExpando_usesdefaultfornonnullablemissing() + { + var readConfig = parse(@"{ + + aField: ""I've decided"", + subtyped: + { + aValueType: 94.298, + aValueTypeButNotAField: 9241, + anEnumerableType: + [ + {""test3"": 420.71}, + {""test4"": 420.72} + ] + }, + subtyped2: + { + aValueType: 95.298, + aValueTypeButNotAField: 9242, + anEnumerableType: + [ + {""test5"": 420.73}, + {""test6"": 420.74} + ] + } + }"); + var conf = new AConfigurationType(); + greyn.Deployment.ConfigurationBootstrapper.readFromExpando(ref conf, readConfig); + Assert.AreEqual(conf.aValueTypeButNotAField, 156.697f); + Assert.AreEqual(conf.aField, "I've decided"); + Assert.AreEqual(conf.subtyped.aValueType, 94.298); + Assert.AreEqual(conf.subtyped.aValueTypeButNotAField, 9241); + Assert.IsNotNull(conf.subtyped.anEnumerableType); + Assert.AreEqual(conf.subtyped.anEnumerableType["test3"], 420.71); + Assert.AreEqual(conf.subtyped.anEnumerableType["test4"], 420.72); + Assert.IsNotNull(conf.subtyped2); + } +} diff --git a/deployment.tests/deployment.tests.csproj b/deployment.tests/deployment.tests.csproj new file mode 100644 index 0000000..2168dd4 --- /dev/null +++ b/deployment.tests/deployment.tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + +