1 /** 2 * Utilities for loading JSON configurations. 3 * 4 * Authors: 5 * Mike Bierlee, m.bierlee@lostmoment.com 6 * Copyright: 2022 Mike Bierlee 7 * License: 8 * This software is licensed under the terms of the MIT license. 9 * The full terms of the license can be found in the LICENSE file. 10 */ 11 12 module mirage.json; 13 14 import std.json : JSONValue, JSONType, parseJSON; 15 import std.conv : to; 16 17 import mirage.config : ConfigFactory, ConfigDictionary, ConfigNode, ValueNode, ObjectNode, ArrayNode, ConfigCreationException; 18 19 /** 20 * Creates configuration dictionaries from JSONs. 21 */ 22 class JsonConfigFactory : ConfigFactory { 23 24 /** 25 * Parse configuration from the given JSON string. 26 * 27 * Params: 28 * contents = Text contents of the config to be parsed. 29 * Returns: The parsed configuration. 30 */ 31 override ConfigDictionary parseConfig(string contents) { 32 return parseJson(parseJSON(contents)); 33 } 34 35 /** 36 * Parse configuration from a JSONValue tree. 37 * 38 * Params: 39 * contents = JSONValue config to be parsed. 40 * Returns: The parsed configuration. 41 */ 42 ConfigDictionary parseJson(JSONValue json) { 43 return new ConfigDictionary(convertJValue(json)); 44 } 45 46 /** 47 * Alias for parseConfig 48 * 49 * Params: 50 * contents = Text contents of the config to be parsed. 51 * Returns: The parsed configuration. 52 * See_Also: parseConfig 53 */ 54 ConfigDictionary parseJson(string json) { 55 return parseConfig(json); 56 } 57 58 private ConfigNode convertJValue(JSONValue json) { 59 if (json.type() == JSONType.object) { 60 auto objectNode = new ObjectNode(); 61 auto objectJson = json.object(); 62 foreach (propertyName, jvalue; objectJson) { 63 objectNode.children[propertyName] = convertJValue(jvalue); 64 } 65 66 return objectNode; 67 } 68 69 if (json.type() == JSONType.array) { 70 auto arrayNode = new ArrayNode(); 71 auto arrayJson = json.array(); 72 foreach (jvalue; arrayJson) { 73 arrayNode.children ~= convertJValue(jvalue); 74 } 75 76 return arrayNode; 77 } 78 79 if (json.type() == JSONType.null_) { 80 return new ValueNode(null); 81 } 82 83 if (json.type() == JSONType..string) { 84 return new ValueNode(json.get!string); 85 } 86 87 if (json.type() == JSONType.integer) { 88 return new ValueNode(json.integer.to!string); 89 } 90 91 if (json.type() == JSONType.float_) { 92 return new ValueNode(json.floating.to!string); 93 } 94 95 throw new ConfigCreationException("JSONValue is not supported: " ~ json.toString()); 96 } 97 } 98 99 /** 100 * Parse JSON config from the given JSON string. 101 102 * Params: 103 * json = Text contents of the config to be parsed. 104 * Returns: The parsed configuration. 105 */ 106 ConfigDictionary parseJsonConfig(const string json) { 107 return new JsonConfigFactory().parseConfig(json); 108 } 109 110 /** 111 * Parse JSON config from the given JSONValue. 112 * 113 * Params: 114 * contents = JSONValue config to be parsed. 115 * Returns: The parsed configuration. 116 */ 117 ConfigDictionary parseJsonConfig(const JSONValue json) { 118 return new JsonConfigFactory().parseJson(json); 119 } 120 121 /** 122 * Load a JSON configuration file from disk. 123 * 124 * Params: 125 * filePath = Path to the JSON configuration file. 126 * Returns: The loaded configuration. 127 */ 128 ConfigDictionary loadJsonConfig(const string filePath) { 129 return new JsonConfigFactory().loadFile(filePath); 130 } 131 132 version (unittest) { 133 @("Parse JSON") 134 unittest { 135 JSONValue serverJson = ["hostname": "hosty.com", "port": "1234"]; 136 JSONValue nullJson = ["isNull": null]; 137 JSONValue socketsJson = [ 138 "/var/sock/one", "/var/sock/two", "/var/sock/three" 139 ]; 140 JSONValue numbersJson = [1, 2, 3, 4, -7]; 141 JSONValue decimalsJson = [1.2, 4.5, 6.7]; 142 JSONValue jsonConfig = [ 143 "server": serverJson, "sockets": socketsJson, "nully": nullJson, 144 "numberos": numbersJson, "decimalas": decimalsJson 145 ]; 146 147 auto config = parseJsonConfig(jsonConfig); 148 149 assert(config.get("server.hostname") == "hosty.com"); 150 assert(config.get("server.port") == "1234"); 151 assert(config.get("sockets[2]") == "/var/sock/three"); 152 assert(config.get("nully.isNull") == null); 153 assert(config.get("numberos[3]") == "4"); 154 assert(config.get("numberos[4]") == "-7"); 155 assert(config.get("decimalas[0]") == "1.2"); 156 assert(config.get("decimalas[2]") == "6.7"); 157 } 158 159 @("Parse JSON root values") 160 unittest { 161 assert(parseJsonConfig(JSONValue("hi")).get(".") == "hi"); 162 assert(parseJsonConfig(JSONValue(1)).get(".") == "1"); 163 assert(parseJsonConfig(JSONValue(null)).get(".") == null); 164 assert(parseJsonConfig(JSONValue(1.8)).get(".") == "1.8"); 165 assert(parseJsonConfig(JSONValue([1, 2, 3])).get("[2]") == "3"); 166 } 167 168 @("Parse JSON string") 169 unittest { 170 string json = " 171 { 172 \"name\": \"Groot\", 173 \"traits\": [\"groot\", \"tree\"], 174 \"age\": 8728, 175 \"taxNumber\": null 176 } 177 "; 178 179 auto config = parseJsonConfig(json); 180 181 assert(config.get("name") == "Groot"); 182 assert(config.get("traits[1]") == "tree"); 183 assert(config.get("age") == "8728"); 184 assert(config.get("taxNumber") == null); 185 } 186 187 @("Load JSON file") 188 unittest { 189 auto config = loadJsonConfig("testfiles/groot.json"); 190 191 assert(config.get("name") == "Groot"); 192 assert(config.get("traits[1]") == "tree"); 193 assert(config.get("age") == "8728"); 194 assert(config.get("taxNumber") == null); 195 } 196 }