diff --git a/Source/Settings/ConfigurationFileStore.Parsing.cs b/Source/Settings/ConfigurationFileStore.Parsing.cs index 5444622..4d537a2 100644 --- a/Source/Settings/ConfigurationFileStore.Parsing.cs +++ b/Source/Settings/ConfigurationFileStore.Parsing.cs @@ -122,10 +122,13 @@ namespace Nuclex.Support.Settings { return; // No closing brace in line } + // Skip any whitespaces between the last character and the closing brace do { --nameEndIndex; } while(char.IsWhiteSpace(line, nameEndIndex)); + // Now we know that the line holds a category definition and where exactly in + // the line the category name is located. Create the category. state.Category = new Category() { LineIndex = state.Store.lines.Count - 1, CategoryName = new StringSegment( @@ -134,6 +137,7 @@ namespace Nuclex.Support.Settings { OptionLookup = new Dictionary() }; state.Store.categories.Add(state.Category); + state.Store.categoryLookup.Add(state.Category.CategoryName.ToString(), state.Category); } /// Parses an option definition encountered on a line @@ -143,10 +147,43 @@ namespace Nuclex.Support.Settings { private static void parseOption( ParserState state, string line, int firstCharacterIndex ) { + int assignmentIndex = line.IndexOf('=', firstCharacterIndex + 1); + if(assignmentIndex == -1) { + return; // No assignment took place + } + + // Cut off any whitespaces between the option name and the assignment + int nameEndIndex = assignmentIndex; + do { + --nameEndIndex; + } while(char.IsWhiteSpace(line, nameEndIndex)); + + // We have enough information to know that this is an assignment of some kind Option option = new Option() { - LineIndex = state.Store.lines.Count - 1 + LineIndex = state.Store.lines.Count - 1, + OptionName = new StringSegment( + line, firstCharacterIndex, nameEndIndex - firstCharacterIndex + 1 + ), }; - throw new NotImplementedException(); + + // If there is a value in this assignment, parse it too + int valueStartIndex = assignmentIndex + 1; + ParserHelper.SkipSpaces(line, ref valueStartIndex); + if(valueStartIndex < line.Length) { + parseOptionValue(option, line, valueStartIndex); + } + + // We've got the option assignment, either with an empty or proper value + state.Store.options.Add(option); + state.Category.OptionLookup.Add(option.OptionName.ToString(), option); + } + + /// Parses the value assigned to an option + /// Option to which a value is being assigned + /// Line containing the option assignment + /// Index of the value's first character + private static void parseOptionValue(Option option, string line, int valueStartIndex) { + } /// Determines the best matching type for an option value diff --git a/Source/Settings/ConfigurationFileStore.Test.cs b/Source/Settings/ConfigurationFileStore.Test.cs index bd61cb5..db96786 100644 --- a/Source/Settings/ConfigurationFileStore.Test.cs +++ b/Source/Settings/ConfigurationFileStore.Test.cs @@ -21,10 +21,10 @@ License along with this library #if UNITTEST using System; +using System.IO; using NUnit.Framework; -using System.IO; -using System.Linq; +using System.Collections.Generic; namespace Nuclex.Support.Settings { @@ -60,6 +60,79 @@ namespace Nuclex.Support.Settings { Assert.That(configurationFile.EnumerateCategories(), Is.EquivalentTo(categoryNames)); } + /// + /// Verifies that malformed categories can be handled by the parser + /// + [Test] + public void MalformedCategoriesAreIgnored() { + string fileContents = + "[ Not a category\r\n" + + " ["; + ConfigurationFileStore configurationFile = load(fileContents); + + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that empty lines in the configuration file have no meaning + /// + [Test] + public void EmptyLinesAreSkipped() { + string fileContents = + "\r\n" + + " "; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that category definitions after a comment sign are ignored + /// + [Test] + public void CommentedOutCategoriesAreIgnored() { + string fileContents = + "#[NotACategory]\r\n" + + "; [ Also Not A Category ]\r\n"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + } + + /// + /// Verifies that assignments without an option name are ignored by the parser + /// + [Test] + public void NamelessAssignmentsAreIgnored() { + string fileContents = + "=\r\n" + + " = \r\n" + + " = hello"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + Assert.That(configurationFile.EnumerateOptions(), Is.Empty); + } + + /// + /// Verifies that assignments without an option name are ignored by the parser + /// + [Test] + public void OptionsCanHaveEmptyValues() { + string fileContents = + "a =\r\n" + + "b = \r\n" + + "c = ; hello"; + ConfigurationFileStore configurationFile = load(fileContents); + Assert.That(configurationFile.EnumerateCategories(), Is.Empty); + + var options = new List(configurationFile.EnumerateOptions()); + Assert.That(options.Count, Is.EqualTo(3)); + + for(int index = 0; index < options.Count; ++index) { + Assert.That( + configurationFile.Get(null, options[index].Name), Is.Null + ); + } + } + } } // namespace Nuclex.Support.Settings diff --git a/Source/Settings/ConfigurationFileStore.cs b/Source/Settings/ConfigurationFileStore.cs index b4ec07c..3e0bb0f 100644 --- a/Source/Settings/ConfigurationFileStore.cs +++ b/Source/Settings/ConfigurationFileStore.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using System.IO; using Nuclex.Support.Parsing; +using System.Text; namespace Nuclex.Support.Settings { @@ -101,15 +102,7 @@ namespace Nuclex.Support.Settings { /// Category whose options will be enumerated /// An enumerable list of all options in the category public IEnumerable EnumerateOptions(string category = null) { - Category enumeratedCategory; - - if(string.IsNullOrEmpty(category)) { - enumeratedCategory = this.RootCategory; - } else if(!this.categoryLookup.TryGetValue(category, out enumeratedCategory)) { - throw new KeyNotFoundException( - "There is no category named '" + category + "' in the configuration file" - ); - } + Category enumeratedCategory = getCategoryByName(category); foreach(Option option in this.RootCategory.OptionLookup.Values) { OptionInfo optionInfo = new OptionInfo() { @@ -153,7 +146,16 @@ namespace Nuclex.Support.Settings { /// parameter, false otherwise /// public bool TryGet(string category, string optionName, out TValue value) { - throw new NotImplementedException(); + Category containingCategory = getCategoryByName(category); + + Option option; + if(containingCategory.OptionLookup.TryGetValue(optionName, out option)) { + value = (TValue)Convert.ChangeType(option.OptionValue.ToString(), typeof(TValue)); + return true; + } else { + value = default(TValue); + return false; + } } /// Saves an option in the settings store @@ -162,7 +164,30 @@ 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(); + Category targetCategory; + bool optionMightExist; + + if(string.IsNullOrEmpty(category)) { + targetCategory = this.RootCategory; + optionMightExist = true; + } else if(this.categoryLookup.TryGetValue(category, out targetCategory)) { + optionMightExist = true; + } else { + targetCategory = createCategory(category); + optionMightExist = false; + } + + + Option targetOption; + if(optionMightExist) { + if(targetCategory.OptionLookup.TryGetValue(optionName, out targetOption)) { + return; + } + } + + // Append at bottom of category + + } /// Removes the option with the specified name @@ -173,6 +198,129 @@ namespace Nuclex.Support.Settings { throw new NotImplementedException(); } + /// Looks a category up by its name + /// + /// Name of the category. Can be null for the root category + /// + /// The category with the specified name + private Category getCategoryByName(string categoryName) { + Category category; + + if(string.IsNullOrEmpty(categoryName)) { + category = this.RootCategory; + } else if(!this.categoryLookup.TryGetValue(categoryName, out category)) { + throw new KeyNotFoundException( + "There is no category named '" + categoryName + "' in the configuration file" + ); + } + + return category; + } + + /// Creates a new option + /// Category the option will be added to + /// Name of the option + /// Value that will be assigned to the option + private void createOption(Category category, string name, string value) { + int valueLength; + if(value == null) { + valueLength = 0; + } else { + valueLength = value.Length; + } + + // Build the complete line containing the option assignment + string line; + { + StringBuilder builder = new StringBuilder(name.Length + 3 + valueLength); + + builder.Append(name); + builder.Append(" = "); + if(valueLength > 0) { + builder.Append(value); + } + + line = builder.ToString(); + } + + Option newOption = new Option() { + LineIndex = this.lines.Count, + OptionName = new StringSegment(line, 0, name.Length), + OptionValue = new StringSegment(line, name.Length + 3, valueLength) + }; + + // TODO: Find end line of category and add line + } + + /// Changes the value of an option + /// Option whose value will be changed + /// New value that will be assigned to the option + private void changeOption(Option option, string newValue) { + int newValueLength; + if(newValue == null) { + newValueLength = 0; + } else { + newValueLength = newValue.Length; + } + + // Form the new line + string line = option.OptionValue.Text; + { + StringBuilder builder = new StringBuilder( + line.Length - option.OptionValue.Count + newValue.Length + ); + + // Stuff before the value + if(option.OptionValue.Offset > 0) { + builder.Append(line, 0, option.OptionValue.Offset); + } + + // The value itself + if(newValueLength > 0) { + builder.Append(newValue); + } + + // Stuff after the value + int endIndex = option.OptionValue.Offset + option.OptionValue.Count; + if(endIndex < line.Length) { + builder.Append(line, endIndex, line.Length - endIndex); + } + + line = builder.ToString(); + } + + this.lines[option.LineIndex] = line; + option.OptionValue = new StringSegment(line, option.OptionValue.Offset, newValueLength); + } + + /// Creates a new category in the configuration file + /// Name of the new category + /// The category that was created + private Category createCategory(string category) { + string categoryDefinition; + { + StringBuilder builder = new StringBuilder(category.Length + 2); + builder.Append('['); + builder.Append(category); + builder.Append(']'); + categoryDefinition = builder.ToString(); + } + + // An empty line before the category definition for better readability + this.lines.Add(string.Empty); + + Category newCategory = new Category() { + LineIndex = this.lines.Count, + CategoryName = new StringSegment(categoryDefinition, 1, category.Length), + OptionLookup = new Dictionary() + }; + this.lines.Add(categoryDefinition); + + this.categoryLookup.Add(category, newCategory); + + return newCategory; + } + /// Lines contained in the configuration file private IList lines;