From 62dc3c4b5787dd57c9f48f1b7464846d71110af8 Mon Sep 17 00:00:00 2001 From: "George L. Albany" Date: Sun, 24 Apr 2022 21:09:21 -0400 Subject: [PATCH 1/3] Corrected lack of XML.ElementInfo text and element child mixing ElementInfo: Added XmlNodeType Type Added IsText for validating ElementInfo as text element Added default ctor that sets Type to Element Added argument ctor to streamline construction Added TryContentAs methods for bool validation of content Added GetContentAs methods for default failed conversions of content Added TryAttributeAs methods Added GetAttributeAs methods Added AttributeAs methods for throwing exceptions Added TryAddChild for bool validation of add Added TrySetParent for bool validation of parent setting Added TrySetAttribute for bool validation attribute setting Added TryGetAttribute for bool validation and out of attribute string Added SetParent, and SetAttribute for method chaining Added GetAttribute for attribute string retrieval with default WriteToXML will write text elements with WriteString(Content) Added FromText for generating a text element from a string input Created ElementInfoStringExtensions for string handling Created ElementInfoListExtensions to take the place of ListExtensions ListExtensions is obsolete Created ElementInfoDictionaryExtensions for conversion for the attribute dictionary EventExecutor implements element and text mixing --- PathfinderAPI/Util/XML/ElementInfo.cs | 242 ++++++++++++++++++++---- PathfinderAPI/Util/XML/EventExecutor.cs | 10 +- 2 files changed, 211 insertions(+), 41 deletions(-) diff --git a/PathfinderAPI/Util/XML/ElementInfo.cs b/PathfinderAPI/Util/XML/ElementInfo.cs index 27505546..41ac1657 100644 --- a/PathfinderAPI/Util/XML/ElementInfo.cs +++ b/PathfinderAPI/Util/XML/ElementInfo.cs @@ -1,4 +1,7 @@ -using System.Text; +using System.Linq; +using System; +using System.Collections.Generic; +using System.Text; using System.Xml; namespace Pathfinder.Util.XML; @@ -16,6 +19,20 @@ public string Content { public Dictionary Attributes { get; set; } = new Dictionary(); public List Children { get; set; } = new List(); public ulong NodeID { get; } = freeId++; + public XmlNodeType Type { get; set; } + public bool IsText => Type == XmlNodeType.Text; + + public ElementInfo() { Type = XmlNodeType.Element; } + + public ElementInfo(string name, string content = null, Dictionary attributes = null, List children = null, ElementInfo parent = null) + : this() + { + Name = name; + if(content != null) Content = content; + Attributes = attributes ?? new Dictionary(); + Children = children ?? new List(); + Parent = parent; + } public override string ToString() { @@ -28,60 +45,219 @@ public override string ToString() { WriteToXML(writer); } - return builder.Replace("\t", " ").ToString(); } + public bool TryContentAsBoolean(out bool result) + => Content.TryAsBoolean(out result); + + public bool TryContentAsInt(out int result) + => Content.TryAsInt(out result); + + public bool TryContentAsFloat(out float result) + => Content.TryAsFloat(out result); + + public bool GetContentAsBoolean(bool defaultVal = default) + => TryContentAsBoolean(out var result) ? result : defaultVal; + + public int GetContentAsInt(int defaultVal = default) + => TryContentAsInt(out var result) ? result : defaultVal; + + public float GetContentAsFloat(float defaultVal = default) + => TryContentAsFloat(out var result) ? result : defaultVal; + public bool ContentAsBoolean() - => bool.TryParse(Content, out var value) - ? value - : throw new FormatException($"Value of '{Name}' is not true or false"); + => Content.AsBoolean(nameof(Content)); public int ContentAsInt() - => int.TryParse(Content, out var value) - ? value - : throw new FormatException($"Value of '{Name}' is not an integer, e.g.: 0, 1, 2"); - + => Content.AsInt(nameof(Content)); + public float ContentAsFloat() - => float.TryParse(Content, out var value) - ? value - : throw new FormatException($"Value of '{Name}' is not a float, e.g.: 1.0"); + => Content.AsFloat(nameof(Content)); + + public bool TryAttributeAsBoolean(string attribName, out bool result) + => Attributes.TryAsBoolean(attribName, out result); + + public bool TryAttributeAsInt(string attribName, out int result) + => Attributes.TryAsInt(attribName, out result); + + public bool TryAttributeAsFloat(string attribName, out float result) + => Attributes.TryAsFloat(attribName, out result); + + public bool GetAttributeAsBoolean(string attribName, bool defaultVal = default) + => TryAttributeAsBoolean(attribName, out var result) ? result : defaultVal; + + public int GetAttributeAsInt(string attribName, int defaultVal = default) + => TryAttributeAsInt(attribName, out var result) ? result : defaultVal; + + public float GetAttributeAsFloat(string attribName, float defaultVal = default) + => TryAttributeAsFloat(attribName, out var result) ? result : defaultVal; + + public bool AttributeAsBoolean(string attribName) + => Attributes.AsBoolean(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + + public int AttributeAsInt(string attribName) + => Attributes.AsInt(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + + public float AttributeAsFloat(string attribName) + => Attributes.AsFloat(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + + public bool TryAddChild(ElementInfo info) + { + if(Children == null) return false; + if(info.Parent != null) + info.Parent.Children.Remove(info); + Children.Add(info); + info.Parent = this; + return Children.Last() == info; + } + + public bool TrySetParent(ElementInfo info) + { + return info.TryAddChild(this); + } + + public bool TrySetAttribute(string key, string value) + { + if(Attributes == null) return false; + Attributes[key] = value; + return Attributes[key] == value; + } + + public bool TryGetAttribute(string key, ref string value) + { + if(!Attributes?.TryGetValue(key, out value) ?? true) return false; + return true; + } + + public ElementInfo AddChild(ElementInfo info) + { + TryAddChild(info); + return this; + } + + public ElementInfo SetParent(ElementInfo info) + { + TrySetParent(info); + return this; + } + + public ElementInfo SetAttribute(string key, string value) + { + TrySetAttribute(key, value); + return this; + } + + public string GetAttribute(string key, string defaultValue = null) + { + TryGetAttribute(key, ref defaultValue); + return defaultValue; + } public void WriteToXML(XmlWriter writer) { + if(IsText) + { + writer.WriteString(Content); + return; + } writer.WriteStartElement(Name, ""); foreach (var attr in Attributes) writer.WriteAttributeString(attr.Key, attr.Value); - if (Content == null) - { - foreach (var child in Children) - child.WriteToXML(writer); - } - else - { - writer.WriteValue(Content); - } + foreach (var child in Children) + child.WriteToXML(writer); writer.WriteEndElement(); } + + public static ElementInfo FromText(string input) + => new ElementInfo + { + Content = input, + Type = XmlNodeType.Text, + Attributes = null, + Children = null + }; } -public static class ListExtensions +public static class ElementInfoStringExtensions { - public static ElementInfo GetElement(this List list, string elementName) - { - foreach (var possibleInfo in list) - { - if (possibleInfo.Name == elementName) - { - return possibleInfo; - } - } + public static bool TryAsBoolean(this string content, out bool result) + => bool.TryParse(content, out result); + public static bool TryAsInt(this string content, out int result) + => int.TryParse(content, out result); + public static bool TryAsFloat(this string content, out float result) + => float.TryParse(content, out result); - return null; + public static bool AsBooleanSafe(this string content, bool defaultVal = default) + => content.TryAsBoolean(out var value) + ? value + : defaultVal; + public static int AsIntSafe(this string content, int defaultVal = default) + => content.TryAsInt(out var value) + ? value + : defaultVal; + public static float AsFloatSafe(this string content, float defaultVal = default) + => content.TryAsFloat(out var value) + ? value + : defaultVal; + + public static bool AsBoolean(this string content, string valName = "content") + => content.TryAsBoolean(out var value) + ? value + : throw new FormatException($"Value of '{valName}' is not true or false"); + public static int AsInt(this string content, string valName = "content") + => content.TryAsInt(out var value) + ? value + : throw new FormatException($"Value of '{valName}' is not an integer, e.g.: 0, 1, 2"); + public static float AsFloat(this string content, string valName = "content") + => content.TryAsFloat(out var value) + ? value + : throw new FormatException($"Value of '{valName}' is not a float, e.g.: 1.0"); +} + +public static class ElementInfoListExtensions +{ + public static ElementInfo GetElement(IEnumerable list, string elementName) + { + return list.FirstOrDefault(e => e.Name == elementName); } - public static bool TryGetElement(this List list, string elementName, out ElementInfo info) + public static bool TryGetElement(IEnumerable list, string elementName, out ElementInfo info) { info = GetElement(list, elementName); return info != null; } +} + +[Obsolete("Use ElementInfoListExtensions")] +public static class ListExtensions +{ + public static ElementInfo GetElement(this IEnumerable list, string elementName) + => ElementInfoListExtensions.GetElement(list, elementName); + public static bool TryGetElement(this IEnumerable list, string elementName, out ElementInfo info) + => ElementInfoListExtensions.TryGetElement(list, elementName, out info); +} + +public static class ElementInfoDictionaryExtensions +{ + public static bool TryAsBoolean(this IDictionary attribute, string key, out bool result) + { + result = default; + return attribute.TryGetValue(key, out var str) ? str.TryAsBoolean(out result) : false; + } + public static bool TryAsInt(this IDictionary attribute, string key, out int result) + { + result = default; + return attribute.TryGetValue(key, out var str) ? str.TryAsInt(out result) : false; + } + public static bool TryAsFloat(this IDictionary attribute, string key, out float result) + { + result = default; + return attribute.TryGetValue(key, out var str) ? str.TryAsFloat(out result) : false; + } + public static bool AsBoolean(this IDictionary attribute, string key, string valName = "attribute[key]") + => attribute[key].AsBoolean(valName); + public static int AsInt(this IDictionary attribute, string key, string valName = "attribute[key]") + => attribute[key].AsInt(valName); + public static float AsFloat(this IDictionary attribute, string key, string valName = "attribute[key]") + => attribute[key].AsFloat(valName); } \ No newline at end of file diff --git a/PathfinderAPI/Util/XML/EventExecutor.cs b/PathfinderAPI/Util/XML/EventExecutor.cs index 4f7a4808..cdda5980 100644 --- a/PathfinderAPI/Util/XML/EventExecutor.cs +++ b/PathfinderAPI/Util/XML/EventExecutor.cs @@ -193,14 +193,7 @@ protected override void ReadElement(Dictionary attributes) else { var topElement = currentElementStack.Peek(); - var element = new ElementInfo() - { - Name = Reader.Name, - Attributes = attributes, - Parent = topElement, - Content = null - }; - topElement.Children.Add(element); + var element = new ElementInfo(Reader.Name, attributes: attributes).SetParent(topElement); currentElementStack.Push(element); } } @@ -242,6 +235,7 @@ protected override void ReadText() topElement.Content = Reader.Value; else topElement.Content += "\n" + Reader.Value; + topElement.AddChild(ElementInfo.FromText(Reader.Value)); } } From c8112b89c4e530779fe912b02db36a92ee94c9e1 Mon Sep 17 00:00:00 2001 From: "George L. Albany" Date: Sun, 24 Apr 2022 21:27:05 -0400 Subject: [PATCH 2/3] Prevent null attribute dictionary reading from throwing an NRE --- PathfinderAPI/Util/XML/ElementInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PathfinderAPI/Util/XML/ElementInfo.cs b/PathfinderAPI/Util/XML/ElementInfo.cs index 41ac1657..b75080e7 100644 --- a/PathfinderAPI/Util/XML/ElementInfo.cs +++ b/PathfinderAPI/Util/XML/ElementInfo.cs @@ -242,16 +242,19 @@ public static class ElementInfoDictionaryExtensions public static bool TryAsBoolean(this IDictionary attribute, string key, out bool result) { result = default; + if(attribute == null) return false; return attribute.TryGetValue(key, out var str) ? str.TryAsBoolean(out result) : false; } public static bool TryAsInt(this IDictionary attribute, string key, out int result) { result = default; + if(attribute == null) return false; return attribute.TryGetValue(key, out var str) ? str.TryAsInt(out result) : false; } public static bool TryAsFloat(this IDictionary attribute, string key, out float result) { result = default; + if(attribute == null) return false; return attribute.TryGetValue(key, out var str) ? str.TryAsFloat(out result) : false; } public static bool AsBoolean(this IDictionary attribute, string key, string valName = "attribute[key]") From 14f35ddcb3f87d2791721037a87562e50495b70e Mon Sep 17 00:00:00 2001 From: "George L. Albany" Date: Thu, 28 Apr 2022 18:51:50 -0400 Subject: [PATCH 3/3] Make type parse exceptions more useful AsBoolean, AsFloat, and AsInt FormatExceptions describe both the content value and the key's value for dictionaries --- PathfinderAPI/Util/XML/ElementInfo.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/PathfinderAPI/Util/XML/ElementInfo.cs b/PathfinderAPI/Util/XML/ElementInfo.cs index b75080e7..8bafbde2 100644 --- a/PathfinderAPI/Util/XML/ElementInfo.cs +++ b/PathfinderAPI/Util/XML/ElementInfo.cs @@ -94,13 +94,13 @@ public float GetAttributeAsFloat(string attribName, float defaultVal = default) => TryAttributeAsFloat(attribName, out var result) ? result : defaultVal; public bool AttributeAsBoolean(string attribName) - => Attributes.AsBoolean(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + => Attributes.AsBoolean(attribName, $"{nameof(Attributes)}"); public int AttributeAsInt(string attribName) - => Attributes.AsInt(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + => Attributes.AsInt(attribName, $"{nameof(Attributes)}"); public float AttributeAsFloat(string attribName) - => Attributes.AsFloat(attribName, $"{nameof(Attributes)}[{nameof(attribName)}]"); + => Attributes.AsFloat(attribName, $"{nameof(Attributes)}"); public bool TryAddChild(ElementInfo info) { @@ -204,15 +204,15 @@ public static float AsFloatSafe(this string content, float defaultVal = default) public static bool AsBoolean(this string content, string valName = "content") => content.TryAsBoolean(out var value) ? value - : throw new FormatException($"Value of '{valName}' is not true or false"); + : throw new FormatException($"Value of '{valName}' is '{content}' which is not true or false"); public static int AsInt(this string content, string valName = "content") => content.TryAsInt(out var value) ? value - : throw new FormatException($"Value of '{valName}' is not an integer, e.g.: 0, 1, 2"); + : throw new FormatException($"Value of '{valName}' is '{content}' which is not an integer, e.g.: 0, 1, 2"); public static float AsFloat(this string content, string valName = "content") => content.TryAsFloat(out var value) ? value - : throw new FormatException($"Value of '{valName}' is not a float, e.g.: 1.0"); + : throw new FormatException($"Value of '{valName}' is '{content}' which is not a float, e.g.: 1.0"); } public static class ElementInfoListExtensions @@ -257,10 +257,10 @@ public static bool TryAsFloat(this IDictionary attribute, string if(attribute == null) return false; return attribute.TryGetValue(key, out var str) ? str.TryAsFloat(out result) : false; } - public static bool AsBoolean(this IDictionary attribute, string key, string valName = "attribute[key]") - => attribute[key].AsBoolean(valName); - public static int AsInt(this IDictionary attribute, string key, string valName = "attribute[key]") - => attribute[key].AsInt(valName); - public static float AsFloat(this IDictionary attribute, string key, string valName = "attribute[key]") - => attribute[key].AsFloat(valName); + public static bool AsBoolean(this IDictionary attribute, string key, string dictName = "attribute") + => attribute[key].AsBoolean($"{dictName}[{key}]"); + public static int AsInt(this IDictionary attribute, string key, string dictName = "attribute") + => attribute[key].AsInt($"{dictName}[{key}]"); + public static float AsFloat(this IDictionary attribute, string key, string dictName = "attribute") + => attribute[key].AsFloat($"{dictName}[{key}]"); } \ No newline at end of file