diff --git a/Source/Parsing/ParserHelper.cs b/Source/Parsing/ParserHelper.cs index 69032ae..b6dbf10 100644 --- a/Source/Parsing/ParserHelper.cs +++ b/Source/Parsing/ParserHelper.cs @@ -176,75 +176,6 @@ namespace Nuclex.Support.Parsing { return false; } - /// Tried to parse a boolean literal - /// Value that will be parsed as a boolean literal - /// - /// True or false if the value was a boolean literal, null if it wasn't - /// - public static bool? ParseBooleanLiteral(string value) { - if(value == null) { - return null; - } - - var stringSegment = new StringSegment(value, 0, value.Length); - return ParseBooleanLiteral(ref stringSegment); - } - - /// Tried to parse a boolean literal - /// Value that will be parsed as a boolean literal - /// - /// True or false if the value was a boolean literal, null if it wasn't - /// - public static bool? ParseBooleanLiteral(ref StringSegment value) { - switch(value.Count) { - - // If the string spells 'no', it is considered a boolean - case 2: { - bool isSpellingNo = - ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) && - ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O')); - return isSpellingNo ? new Nullable(false) : null; - } - - // If the string spells 'yes', it is considered a boolean - case 3: { - bool isSpellingYes = - ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) && - ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) && - ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S')); - return isSpellingYes ? new Nullable(true) : null; - } - - // If the string spells 'true', it is considered a boolean - case 4: { - bool isSpellingTrue = - ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) && - ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) && - ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) && - ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E')); - return isSpellingTrue ? new Nullable(true) : null; - } - - // If the string spells 'false', it is considered a boolean - case 5: { - bool isSpellingFalse = - ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) && - ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) && - ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) && - ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) && - ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E')); - return isSpellingFalse ? new Nullable(false) : null; - } - - // Anything else is not considered a boolean - default: { - return null; - } - - } - } - - } } // namespace Nuclex.Support.Parsing diff --git a/Source/Settings/ConfigurationFileStore.Parsing.cs b/Source/Settings/ConfigurationFileStore.Parsing.cs index d5a2e0b..ed8b6cc 100644 --- a/Source/Settings/ConfigurationFileStore.Parsing.cs +++ b/Source/Settings/ConfigurationFileStore.Parsing.cs @@ -253,13 +253,67 @@ namespace Nuclex.Support.Settings { } // If it parses as a boolean literal, then it must be a boolean - if(ParserHelper.ParseBooleanLiteral(ref value) != null) { + if(parseBooleanLiteral(ref value) != null) { return typeof(bool); } return typeof(string); } + /// Tried to parse a boolean literal + /// Value that will be parsed as a boolean literal + /// + /// True or false if the value was a boolean literal, null if it wasn't + /// + private static bool? parseBooleanLiteral(ref StringSegment value) { + switch(value.Count) { + + // If the string spells 'no', it is considered a boolean + case 2: { + bool isSpellingNo = + ((value.Text[value.Offset + 0] == 'n') || (value.Text[value.Offset + 0] == 'N')) && + ((value.Text[value.Offset + 1] == 'o') || (value.Text[value.Offset + 1] == 'O')); + return isSpellingNo ? new Nullable(false) : null; + } + + // If the string spells 'yes', it is considered a boolean + case 3: { + bool isSpellingYes = + ((value.Text[value.Offset + 0] == 'y') || (value.Text[value.Offset + 0] == 'Y')) && + ((value.Text[value.Offset + 1] == 'e') || (value.Text[value.Offset + 1] == 'E')) && + ((value.Text[value.Offset + 2] == 's') || (value.Text[value.Offset + 2] == 'S')); + return isSpellingYes ? new Nullable(true) : null; + } + + // If the string spells 'true', it is considered a boolean + case 4: { + bool isSpellingTrue = + ((value.Text[value.Offset + 0] == 't') || (value.Text[value.Offset + 0] == 'T')) && + ((value.Text[value.Offset + 1] == 'r') || (value.Text[value.Offset + 1] == 'R')) && + ((value.Text[value.Offset + 2] == 'u') || (value.Text[value.Offset + 2] == 'U')) && + ((value.Text[value.Offset + 3] == 'e') || (value.Text[value.Offset + 3] == 'E')); + return isSpellingTrue ? new Nullable(true) : null; + } + + // If the string spells 'false', it is considered a boolean + case 5: { + bool isSpellingFalse = + ((value.Text[value.Offset + 0] == 'f') || (value.Text[value.Offset + 0] == 'F')) && + ((value.Text[value.Offset + 1] == 'a') || (value.Text[value.Offset + 1] == 'A')) && + ((value.Text[value.Offset + 2] == 'l') || (value.Text[value.Offset + 2] == 'L')) && + ((value.Text[value.Offset + 3] == 's') || (value.Text[value.Offset + 3] == 'S')) && + ((value.Text[value.Offset + 4] == 'e') || (value.Text[value.Offset + 4] == 'E')); + return isSpellingFalse ? new Nullable(false) : null; + } + + // Anything else is not considered a boolean + default: { + return null; + } + + } + } + } } // namespace Nuclex.Support.Configuration diff --git a/Source/Settings/ConfigurationFileStore.cs b/Source/Settings/ConfigurationFileStore.cs index 5481d1d..a6136b5 100644 --- a/Source/Settings/ConfigurationFileStore.cs +++ b/Source/Settings/ConfigurationFileStore.cs @@ -156,7 +156,7 @@ namespace Nuclex.Support.Settings { Option option; if(containingCategory.OptionLookup.TryGetValue(optionName, out option)) { if(typeof(TValue) == typeof(bool)) { - bool? boolean = ParserHelper.ParseBooleanLiteral(ref option.OptionValue); + bool? boolean = parseBooleanLiteral(ref option.OptionValue); if(boolean.HasValue) { value = (TValue)(object)boolean.Value; return true; diff --git a/Source/Settings/ISettingsStore.cs b/Source/Settings/ISettingsStore.cs index 7bcfe28..f04b2c2 100644 --- a/Source/Settings/ISettingsStore.cs +++ b/Source/Settings/ISettingsStore.cs @@ -24,6 +24,27 @@ using System.Collections.Generic; namespace Nuclex.Support.Settings { /// Interface by which settings and configuration data can be accessed + /// + /// + /// The intended usage pattern for options is for your application to simply read and + /// write whatever options it needs using the type it expects them to be. + /// + /// + /// When you enumerate the options appearing under a category, the settings store will + /// try to guess the likely type of an option, but this is not always accurate. For + /// example, assigning the text 'true' to an option in a .cfg or .ini file could mean + /// that the option is a boolean or it could simply be a coincidence. When you read + /// this value as a boolean, the settings store will correctly convert it to a boolean, + /// when you read it as a string, you will get back "true" in plain text. + /// + /// + /// Which types of values a settings store can accept can also change between different + /// settings store implementations. The windows registry supports string and byte + /// arrays whereas configuration files have no standardized way of holding these. + /// Any property store must support a minimal subset including booleans, integers, + /// floating point values and strings. + /// + /// public interface ISettingsStore { /// Enumerates the categories defined in the configuration diff --git a/Source/Settings/WindowsRegistryStore.Test.cs b/Source/Settings/WindowsRegistryStore.Test.cs index 79107d4..0e4e116 100644 --- a/Source/Settings/WindowsRegistryStore.Test.cs +++ b/Source/Settings/WindowsRegistryStore.Test.cs @@ -23,6 +23,10 @@ License along with this library using System; using NUnit.Framework; +using System.IO; +using Microsoft.Win32; +using System.Globalization; +using System.Collections.Generic; namespace Nuclex.Support.Settings { @@ -30,7 +34,153 @@ namespace Nuclex.Support.Settings { [TestFixture] internal class WindowsRegistryStoreTest { - + #region class TestContext + + /// Sets up a temporary registry key for the unit test + private class TestContext : IDisposable { + + /// Initializes a new test context + public TestContext() { + this.keyName = Guid.NewGuid().ToString(); + this.registryKey = Registry.CurrentUser.CreateSubKey(this.keyName); + this.store = new WindowsRegistryStore(this.registryKey, writable: true); + } + + /// Immediately frees all resources owned by the test context + public void Dispose() { + if(this.store != null) { + this.store.Dispose(); + this.store = null; + this.registryKey = null; + } else if(this.registryKey != null) { + this.registryKey.Dispose(); + this.registryKey = null; + } + if(this.keyName != null) { + Registry.CurrentUser.DeleteSubKeyTree(this.keyName); + this.keyName = null; + } + } + + /// Store created on a temporary registry key + public WindowsRegistryStore Store { + get { return this.store; } + } + + /// Name of the temporary registry key + private string keyName; + /// Registry key (ownership transfered to the store) + private RegistryKey registryKey; + /// Store that is accessing the registry key + private WindowsRegistryStore store; + + } + + #endregion // class TestContext + + /// Verifies that new instances of the registry store can be created + [Test] + public void CanBeCreated() { + Assert.That( + () => { using(var context = new TestContext()) { } }, Throws.Nothing + ); + } + + /// Verifies that booleans can be stored in the registry + [Test] + public void BooleansCanBeStored() { + using(var context = new TestContext()) { + context.Store.Set(null, "test", true); + Assert.That(context.Store.Get(null, "test"), Is.True); + + context.Store.Set(null, "test", false); + Assert.That(context.Store.Get(null, "test"), Is.False); + } + } + + /// Verifies that integers can be stored in the registry + [Test] + public void IntegersCanBeStored() { + using(var context = new TestContext()) { + context.Store.Set(null, "test", 123); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo(123)); + + context.Store.Set(null, "test", 456); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo(456)); + } + } + + /// Verifies that floats can be stored in the registry + [Test] + public void FloatsCanBeStored() { + float testValue = float.Parse("123.456", CultureInfo.InvariantCulture); + + using(var context = new TestContext()) { + context.Store.Set(null, "test", testValue); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo(testValue)); + + testValue = float.Parse("654.321", CultureInfo.InvariantCulture); + + context.Store.Set(null, "test", testValue); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo(testValue)); + } + } + + /// Verifies that strings can be stored in the registry + [Test] + public void StringsCanBeStored() { + using(var context = new TestContext()) { + context.Store.Set(null, "test", "hello world"); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo("hello world")); + + context.Store.Set(null, "test", "world hello"); + Assert.That(context.Store.Get(null, "test"), Is.EqualTo("world hello")); + } + } + + /// Verifies that the subkeys of a registry key can be enumerated + [Test] + public void CategoriesCanBeEnumerated() { + string[] names = new string[] { "one", "two", "three" }; + + using(var context = new TestContext()) { + context.Store.Set(names[0], "sol", 21); + context.Store.Set(names[1], "sol", 42); + context.Store.Set(names[2], "sol", 84); + + Assert.That(context.Store.EnumerateCategories(), Is.EquivalentTo(names)); + } + } + + /// Verifies that the values under a registry subkey can be enumerated + [Test] + public void OptionsInCategoryCanBeEnumerated() { + string[] names = new string[] { "one", "two", "three" }; + + using(var context = new TestContext()) { + context.Store.Set("test", names[0], 1); + context.Store.Set("test", names[1], 2); + context.Store.Set("test", names[2], 3); + + var optionInfos = new List(context.Store.EnumerateOptions("test")); + Assert.That(optionInfos.Count, Is.EqualTo(3)); + } + } + + /// Verifies that the values under a registry key can be enumerated + [Test] + public void RootOptionsCanBeEnumerated() { + string[] names = new string[] { "one", "two", "three" }; + + using(var context = new TestContext()) { + context.Store.Set(null, names[0], 1); + context.Store.Set(null, names[1], 2); + context.Store.Set(null, names[2], 3); + + var optionInfos = new List(context.Store.EnumerateOptions(null)); + Assert.That(optionInfos.Count, Is.EqualTo(3)); + } + } } diff --git a/Source/Settings/WindowsRegistryStore.cs b/Source/Settings/WindowsRegistryStore.cs index 560fd3d..9781674 100644 --- a/Source/Settings/WindowsRegistryStore.cs +++ b/Source/Settings/WindowsRegistryStore.cs @@ -131,8 +131,13 @@ namespace Nuclex.Support.Settings { if(string.IsNullOrEmpty(category)) { return tryGetValueFromKey(this.rootKey, optionName, out value); } else { - using(RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable)) { - return tryGetValueFromKey(this.rootKey, optionName, out value); + RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable); + if(categoryKey == null) { + value = default(TValue); + return false; + } + using(categoryKey) { + return tryGetValueFromKey(categoryKey, optionName, out value); } } } @@ -143,7 +148,43 @@ namespace Nuclex.Support.Settings { /// Name of the option that will be saved /// The value under which the option will be saved public void Set(string category, string optionName, TValue value) { - throw new NotImplementedException(); + if(string.IsNullOrEmpty(category)) { + setValue(this.rootKey, optionName, value); + } else { + RegistryKey categoryKey = this.rootKey.OpenSubKey(category, this.writable); + if(categoryKey == null) { + categoryKey = this.rootKey.CreateSubKey(category); + } + using(categoryKey) { + setValue(categoryKey, optionName, value); + } + } + } + + /// Writes a setting to the registry + /// + /// + /// + /// + private void setValue(RegistryKey registryKey, string optionName, TValue value) { + if(typeof(TValue) == typeof(int)) { + registryKey.SetValue(optionName, value, RegistryValueKind.DWord); + } else if(typeof(TValue) == typeof(long)) { + registryKey.SetValue(optionName, value, RegistryValueKind.QWord); + } else if(typeof(TValue) == typeof(bool)) { + registryKey.SetValue(optionName, value, RegistryValueKind.DWord); + } else if(typeof(TValue) == typeof(string)) { + registryKey.SetValue(optionName, value, RegistryValueKind.String); + } else if(typeof(TValue) == typeof(string[])) { + registryKey.SetValue(optionName, value, RegistryValueKind.MultiString); + } else if(typeof(TValue) == typeof(byte[])) { + registryKey.SetValue(optionName, value, RegistryValueKind.Binary); + } else { + string valueAsString = (string)Convert.ChangeType( + value, typeof(string), CultureInfo.InvariantCulture + ); + registryKey.SetValue(optionName, valueAsString, RegistryValueKind.String); + } } /// Removes the option with the specified name @@ -167,22 +208,6 @@ namespace Nuclex.Support.Settings { if(valueAsObject == null) { value = default(TValue); return false; - } - - if(typeof(TValue) == typeof(bool)) { - string valueAsString = (string)Convert.ChangeType( - valueAsObject, typeof(string), CultureInfo.InvariantCulture - ); - - bool? boolean = ParserHelper.ParseBooleanLiteral(valueAsString); - if(boolean.HasValue) { - value = (TValue)(object)boolean.Value; - return true; - } else { - throw new FormatException( - "The value '" + valueAsString + "' can not be intepreted as a boolean" - ); - } } else { value = (TValue)Convert.ChangeType( valueAsObject, typeof(TValue), CultureInfo.InvariantCulture @@ -227,11 +252,6 @@ namespace Nuclex.Support.Settings { } } - // If it parses as a boolean literal, then it must be a boolean - if(ParserHelper.ParseBooleanLiteral(value) != null) { - return typeof(bool); - } - return typeof(string); }