diff --git a/source/poodinis/config/dictionary.d b/source/poodinis/config/dictionary.d index 79de2ee..5b782d8 100644 --- a/source/poodinis/config/dictionary.d +++ b/source/poodinis/config/dictionary.d @@ -10,7 +10,7 @@ module poodinis.config.dictionary; import std.exception : enforce; -import std.string : split, startsWith, endsWith; +import std.string : split, startsWith, endsWith, join; import std.conv : to, ConvException; class ConfigReadException : Exception { @@ -27,6 +27,7 @@ class PathParseException : Exception { } interface ConfigNode { + string nodeType(); } class ValueNode : ConfigNode { @@ -38,6 +39,10 @@ class ValueNode : ConfigNode { this(string value) { this.value = value; } + + string nodeType() { + return "value"; + } } class ObjectNode : ConfigNode { @@ -55,6 +60,10 @@ class ObjectNode : ConfigNode { children[key] = new ValueNode(value); } } + + string nodeType() { + return "object"; + } } class ArrayNode : ConfigNode { @@ -72,9 +81,13 @@ class ArrayNode : ConfigNode { children ~= new ValueNode(value); } } + + string nodeType() { + return "array"; + } } -class PathSegment { +interface PathSegment { } class ArrayPathSegment : PathSegment { @@ -95,6 +108,7 @@ class PropertyPathSegment : PathSegment { class ConfigPath { private const string path; + private string[] previousSegments; private string[] segments; this(const string path) { @@ -108,7 +122,8 @@ class ConfigPath { } PathSegment ret(PathSegment segment) { - segments = segments.length > 1 ? segments[1 .. $] : []; + previousSegments ~= segments[0]; + segments = segments[1 .. $]; return segment; } @@ -130,6 +145,10 @@ class ConfigPath { return ret(new PropertyPathSegment(segment)); } + + string getCurrentPath() { + return previousSegments.join("."); + } } class ConfigDictionary { @@ -151,13 +170,29 @@ class ConfigDictionary { auto path = new ConfigPath(configPath); auto currentNode = rootNode; PathSegment currentPathSegment = path.getNextSegment(); + string createExceptionPath() { + return "'" ~ configPath ~ "' (at '" ~ path.getCurrentPath() ~ "')"; + } + while (currentPathSegment !is null) { + if (currentNode is null) { + throw new ConfigReadException( + "Path does not exist: " ~ createExceptionPath()); + } + + auto valueNode = cast(ValueNode) currentNode; + if (valueNode) { + throw new ConfigReadException( + "Path does not exist: " ~ createExceptionPath()); + } + auto arrayPath = cast(ArrayPathSegment) currentPathSegment; if (arrayPath) { auto arrayNode = cast(ArrayNode) currentNode; if (arrayNode) { if (arrayNode.children.length < arrayPath.index) { - throw new ConfigReadException("Array index out of bounds: " ~ configPath); + throw new ConfigReadException( + "Array index out of bounds: " ~ createExceptionPath()); } currentNode = arrayNode.children[arrayPath.index]; @@ -171,6 +206,9 @@ class ConfigDictionary { auto propertyNode = propertyPath.propertyName in objectNode.children; if (propertyNode) { currentNode = *propertyNode; + } else { + throw new ConfigReadException( + "Path does not exist: " ~ createExceptionPath()); } } } @@ -183,7 +221,7 @@ class ConfigDictionary { return value.value; } else { throw new ConfigReadException( - "The configuration at the given path is not a value: " ~ configPath); + "Value expected but " ~ currentNode.nodeType ~ " found at path: " ~ createExceptionPath()); } } } @@ -256,4 +294,67 @@ version (unittest) { assert(dictionary.get("noot") == "nut"); assert(dictionary.get("mies") == "mies"); } + + @("Get value from object in object") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ObjectNode([ + "server": new ObjectNode([ + "port": "8080" + ]) + ]); + + assert(dictionary.get("server.port") == "8080"); + } + + @("Get value from array in object") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ObjectNode([ + "hostname": new ArrayNode(["google.com", "dlang.org"]) + ]); + + assert(dictionary.get("hostname.[1]") == "dlang.org"); + } + + @("Exception is thrown when array out of bounds when fetching from root") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ArrayNode(["google.com", "dlang.org"]); + + assertThrown!ConfigReadException(dictionary.get("[5]")); + } + + @("Exception is thrown when array out of bounds when fetching from object") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ObjectNode([ + "hostname": new ArrayNode(["google.com", "dlang.org"]) + ]); + + assertThrown!ConfigReadException(dictionary.get("hostname.[5]")); + } + + @("Exception is thrown when path does not exist") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ObjectNode( + [ + "hostname": new ObjectNode(["cluster": new ValueNode("")]) + ]); + + assertThrown!ConfigReadException(dictionary.get("hostname.cluster.spacey")); + } + + @("Exception is thrown when given path terminates too early") + unittest { + auto dictionary = new ConfigDictionary(); + dictionary.rootNode = new ObjectNode( + [ + "hostname": new ObjectNode(["cluster": new ValueNode(null)]) + ]); + + assertThrown!ConfigReadException(dictionary.get("hostname")); + } + }