1 module config.config; 2 3 import std.typecons : BitFlags, Nullable; 4 import std.range : isOutputRange; 5 6 import config.setting; 7 8 9 /// Options used by Config when writing a file 10 enum Option { 11 AutoConvert = 0x01, 12 SemiColonSeparators = 0x02, 13 ColonAssignmentForGroups = 0x04, 14 ColonAssignmentForNonGroups = 0x08, 15 OpenBraceOnSeparateLine = 0x10, 16 } 17 18 /// Generic libconfig exception 19 class ConfigException : Exception 20 { 21 this(in string msg) 22 { 23 super(msg); 24 } 25 } 26 27 /// Exception thrown when invalid input is met during parsing 28 class InvalidConfigInput : ConfigException 29 { 30 this(in string input, in string errorMsg) 31 { 32 this.input = input; 33 this.errorMsg = errorMsg; 34 super("Invalid config input. Error:\n"~errorMsg~"\nInput:\n"~prependLineNumbers(input)); 35 } 36 37 /// actual parsing input 38 string input; 39 /// error message, which differ from msg. msg is composed of input and errorMsg 40 string errorMsg; 41 } 42 43 /// Exception thrown when attempting to build inconsistent configuration. 44 /// e.g: when building ArraySetting with different types of children 45 class InconsistentConfigState : ConfigException 46 { 47 this(in string msg) 48 { 49 super(msg); 50 } 51 } 52 53 /// Main Config class 54 class Config 55 { 56 import std.stdio : File; 57 import std.ascii : newline; 58 59 this() { 60 _root = new GroupSetting(this, null, ""); 61 62 _options = BitFlags!Option( 63 Option.SemiColonSeparators | 64 Option.ColonAssignmentForGroups | 65 Option.OpenBraceOnSeparateLine 66 ); 67 68 _tabWidth = 2; 69 _floatPrecision = 2; 70 } 71 72 @property inout(GroupSetting) root() inout { return _root; } 73 74 inout(Setting) lookUp(in string name) inout 75 { 76 return root.lookUp(name); 77 } 78 79 Nullable!T lookUpValue(T)(in string name) const 80 { 81 return root.lookUpValue!T(name); 82 } 83 84 bool remove(in string path) 85 { 86 return root.remove(path); 87 } 88 89 @property BitFlags!Option options() const { return _options; } 90 @property void options(BitFlags!Option options) { _options = options; } 91 92 @property IntegerFormat defaultIntegerFormat() const { return _defaultIntegerFormat; } 93 @property void defaultIntegerFormat(IntegerFormat format) { _defaultIntegerFormat = format; } 94 95 @property ushort tabWidth() const { return _tabWidth; } 96 @property void tabWidth(ushort val) { 97 import std.algorithm : max; 98 _tabWidth = max(ushort(15), val); 99 } 100 101 @property ushort floatPrecision() const { return _floatPrecision; } 102 @property void floatPrecision(ushort val) { _floatPrecision = val; } 103 104 105 /// Read a config from an opened file. 106 /// The file must be open in text mode (e.g. File(fname, "r")) 107 static Config read(File configFile, in string[] includeDirs=[]) 108 { 109 import std.array : join; 110 import std.stdio : KeepTerminator; 111 import std.exception : enforce; 112 import config.util : stripComments, handleIncludeDirs; 113 114 enforce(configFile.isOpen); 115 116 immutable config = configFile 117 .byLineCopy(KeepTerminator.no, "\n") 118 .stripComments() 119 .handleIncludeDirs(includeDirs) 120 .join(newline); 121 return Parser.readConfig(config); 122 } 123 124 /// Read from configuration string 125 static Config readString(in string configStr, string[] includeDirs=[]) { 126 import std.string : lineSplitter; 127 import std.array : join; 128 import config.util : stripComments, handleIncludeDirs; 129 130 immutable config = configStr 131 .lineSplitter() 132 .stripComments() 133 .handleIncludeDirs(includeDirs) 134 .join(newline); 135 136 return Parser.readConfig(config); 137 } 138 139 /// read from configuration residing in configFile 140 static Config readFile(in string configFile, string[] includeDirs=[]) { 141 return read(File(configFile, "r"), includeDirs); 142 } 143 144 /// writes the configuration to a string 145 override string toString() 146 { 147 import std.array : appender; 148 auto app = appender!string(); 149 Writer.writeSetting(app, root, 0); 150 return app.data; 151 } 152 153 /// writes the configuration to the given output range 154 void writeTo(O)(O writer) 155 if (isOutputRange!(O, char)) 156 { 157 Writer.writeSetting(writer, root, 0); 158 } 159 160 package { 161 Setting makeSetting(AggregateSetting parent, string name, Type type) { 162 switch(type) { 163 case Type.Bool: 164 case Type.Int: 165 case Type.Float: 166 case Type.String: 167 return new ScalarSetting(this, parent, name, type); 168 case Type.Array: 169 return new ArraySetting(this, parent, name); 170 case Type.List: 171 return new ListSetting(this, parent, name); 172 case Type.Group: 173 return new GroupSetting(this, parent, name); 174 default: 175 assert(false, "unsupported Setting type: "~type.stringof); 176 } 177 } 178 } 179 180 private { 181 GroupSetting _root; 182 BitFlags!Option _options; 183 IntegerFormat _defaultIntegerFormat; 184 ushort _tabWidth; 185 ushort _floatPrecision; 186 187 188 struct Parser 189 { 190 import cg = config.grammar; 191 192 static Config readConfig(in string config) 193 { 194 import std.exception : enforce; 195 196 auto conf = new Config; 197 auto mainTree = cg.Config(config); 198 enforce(mainTree.successful, new InvalidConfigInput(config, mainTree.failMsg)); 199 200 assert(mainTree.children.length == 1); 201 assert(mainTree.children[0].name == "Config.Document"); 202 auto docTree = mainTree.children[0]; 203 204 foreach (pt; docTree.children) { 205 assert(pt.name == "Config.Setting"); 206 conf.root.addChild(parseSetting(conf, conf.root, pt)); 207 } 208 209 return conf; 210 } 211 212 static Setting parseSetting(Config conf, AggregateSetting par, cg.ParseTree pt) 213 { 214 assert(pt.name == "Config.Setting"); 215 216 // assertions enforced by grammar 217 assert(pt.children.length == 2); 218 assert(pt.children[0].name == "Config.Name"); 219 assert(pt.children[1].name == "Config.Value"); 220 221 string name = pt.children[0].matches[0]; 222 return parseValue(conf, par, name, pt.children[1]); 223 } 224 225 226 static Setting parseValue(Config conf, AggregateSetting par, string name, cg.ParseTree valTree) 227 { 228 assert(valTree.name == "Config.Value"); 229 230 valTree = valTree.children[0]; 231 switch (valTree.name) { 232 case "Config.Scalar": 233 return parseScalar(conf, par, name, valTree); 234 case "Config.Array": { 235 auto s = new ArraySetting(conf, par, name); 236 auto ss = new Setting[valTree.children.length]; 237 foreach (i, pt; valTree.children) { 238 ss[i] = parseScalar(conf, s, "", pt); 239 } 240 s.setChildren(ss); 241 return s; 242 } 243 case "Config.List": { 244 auto s = new ListSetting(conf, par, name); 245 auto ss = new Setting[valTree.children.length]; 246 foreach (i, pt; valTree.children) { 247 ss[i] = parseValue(conf, s, "", pt); 248 } 249 s.setChildren(ss); 250 return s; 251 } 252 case "Config.Group": { 253 auto s = new GroupSetting(conf, par, name); 254 auto ss = new Setting[valTree.children.length]; 255 foreach (i, pt; valTree.children) { 256 ss[i] = parseSetting(conf, s, pt); 257 } 258 s.setChildren(ss); 259 return s; 260 } 261 default: assert(false); 262 } 263 } 264 265 static Setting parseScalar(Config conf, AggregateSetting par, string name, cg.ParseTree scalTree) 266 { 267 assert(scalTree.name == "Config.Scalar"); 268 269 switch (scalTree.children[0].name) { 270 case "Config.Bool": { 271 import std.uni : toLower; 272 ScalarSetting s = new ScalarSetting(conf, par, name, Type.Bool); 273 if (scalTree.matches[0][0].toLower == 't') { 274 s.value = true; 275 } 276 else { 277 s.value = false; 278 } 279 return s; 280 } 281 case "Config.Integer": { 282 import std.conv : to; 283 ScalarSetting s = new ScalarSetting(conf, par, name, Type.Int); 284 if (scalTree.children[0].children[0].name == "Config.Dec") { 285 s.value = scalTree.matches[0].to!long(10); 286 } 287 else { 288 assert(scalTree.children[0].children[0].name == "Config.Hex"); 289 s.value = scalTree.matches[0].to!long(16); 290 } 291 return s; 292 } 293 case "Config.Float": { 294 import std.conv : to; 295 ScalarSetting s = new ScalarSetting(conf, par, name, Type.Float); 296 s.value = scalTree.matches[0].to!double(); 297 return s; 298 } 299 case "Config.String": { 300 ScalarSetting s = new ScalarSetting(conf, par, name, Type.String); 301 s.value = scalTree.matches[0]; 302 return s; 303 } 304 default: assert(false); 305 } 306 } 307 308 static void validateArrayChildren(Setting[] children) { 309 import std.algorithm : each; 310 import std.exception : enforce; 311 enum seenBool = 1; 312 enum seenInt = 2; 313 enum seenFloat = 4; 314 enum seenString = 8; 315 static int seen (in Type t) { 316 switch(t) { 317 case Type.Bool: return seenBool; 318 case Type.Int: return seenInt; 319 case Type.Float: return seenFloat; 320 case Type.String: return seenString; 321 default: assert(false); 322 } 323 } 324 325 int seenFl; 326 children.each!(c => seenFl |= seen(c.type)); 327 328 enforce( 329 seenFl == seenBool || seenFl == seenString || seenFl & (seenInt | seenFloat), 330 new InconsistentConfigState("Array with different types") 331 ); 332 if ((seenFl & seenInt)==seenInt && (seenFl & seenFloat)==seenFloat) { 333 // mix of float and ints, convert all ints to float 334 foreach(ref c; children) { 335 if (c.type == Type.Int) { 336 auto sc = cast(ScalarSetting)c; 337 immutable val = sc.value!long; 338 sc.value = cast(double)val; 339 } 340 } 341 } 342 } 343 } 344 345 struct Writer 346 { 347 import std.range : isOutputRange, repeat, take; 348 import std.range.primitives : put; 349 import std.format : format; 350 351 static void writeIndent(O)(O output, in int depth, in int width) 352 in { 353 assert(depth > 1); 354 } 355 body { 356 put(output, repeat(' ').take((depth-1)*width)); 357 } 358 359 static void writeValue(O)(O output, in Setting setting, in int depth) 360 { 361 if (setting.isScalar) 362 { 363 writeScalarValue(output, setting.asScalar); 364 } 365 else if (setting.isArray) 366 { 367 writeArrayValue(output, setting.asArray, depth); 368 } 369 else if (setting.isList) 370 { 371 writeListValue(output, setting.asList, depth); 372 } 373 else if (setting.isGroup) 374 { 375 writeGroupValue(output, setting.asGroup, depth); 376 } 377 else assert(false); 378 } 379 380 static void writeScalarValue(O)(O output, in ScalarSetting setting) 381 { 382 switch(setting.type) 383 { 384 case Type.Bool: { 385 put(output, setting.value!bool ? "true" : "false"); 386 break; 387 } 388 case Type.Int: { 389 immutable val = setting.value!long; 390 string suffix = (val > int.max || val < int.min) ? "L" : ""; 391 string fmt = (setting.integerFormat == IntegerFormat.Hex) ? "0x%X%s" : "%d%s"; 392 put(output, format(fmt, val, suffix)); 393 break; 394 } 395 case Type.Float: { 396 import std.uni : toLower; 397 import std.algorithm : canFind; 398 string fval = format("%.*f", setting.config.floatPrecision, setting.value!double); 399 if (!fval.canFind('.') && !fval.toLower.canFind('e')) fval ~= ".0"; 400 put(output, fval); 401 break; 402 } 403 case Type.String: { 404 import std.uni : isControl; 405 put(output, '"'); 406 foreach(c; setting.value!string) 407 { 408 switch(c) 409 { 410 case '"': 411 put(output, `\"`); break; 412 case '\\': 413 put(output, `\\`); break; 414 case '\n': 415 put(output, "\\n"); break; 416 case '\r': 417 put(output, "\\r"); break; 418 case '\f': 419 put(output, "\\f"); break; 420 case '\t': 421 put(output, "\\t"); break; 422 default: { 423 if (isControl(c)) 424 { 425 put(output, format("\\x%02X", c)); 426 } 427 else 428 { 429 put(output, c); 430 } 431 break; 432 } 433 } 434 } 435 put(output, '"'); 436 break; 437 } 438 default: assert(false); 439 } 440 } 441 442 static void writeArrayValue(O)(O output, in ArraySetting setting, int depth) 443 { 444 put(output, "[ "); 445 writeListContent(output, setting, depth); 446 put(output, ']'); 447 } 448 449 static void writeListValue(O)(O output, in ListSetting setting, int depth) 450 { 451 put(output, "( "); 452 writeListContent(output, setting, depth); 453 put(output, ')'); 454 } 455 456 static void writeListContent(O)(O output, in AggregateSetting setting, in int depth) 457 { 458 auto children = setting.children; 459 foreach (i, s; children) 460 { 461 writeValue(output, s, depth+1); 462 if (i < children.length-1) put(output, ','); 463 put(output, ' '); 464 } 465 } 466 467 static void writeGroupValue(O)(O output, in GroupSetting setting, in int depth) 468 { 469 if (depth > 0) 470 { 471 if (setting.config.options & Option.OpenBraceOnSeparateLine) 472 { 473 put(output, newline); 474 if (depth > 1) writeIndent(output, depth, setting.config.tabWidth); 475 } 476 put(output, "{"~newline); 477 } 478 479 foreach (s; setting.children) 480 { 481 writeSetting(output, s, depth+1); 482 } 483 484 if (depth > 1) writeIndent(output, depth, setting.config.tabWidth); 485 if (depth > 0) put(output, '}'); 486 } 487 488 static void writeSetting(O)(O output, in Setting setting, in int depth) 489 { 490 auto config = setting.config; 491 auto groupAssignChar = (config.options & Option.ColonAssignmentForGroups) ? ':' : '='; 492 auto nonGroupAssignChar = (config.options & Option.ColonAssignmentForNonGroups) ? ':' : '='; 493 494 if (depth > 1) writeIndent(output, depth, config.tabWidth); 495 496 if (setting.name.length) 497 { 498 put(output, format("%s %s ", 499 setting.name, setting.isGroup ? groupAssignChar : nonGroupAssignChar 500 )); 501 } 502 503 writeValue(output, setting, depth); 504 505 if (depth > 0) 506 { 507 if (config.options & Option.SemiColonSeparators) 508 put(output, ';'); 509 put(output, newline); 510 } 511 } 512 } 513 } 514 } 515 516 517 518 package enum pathTok = ".:/"; 519 520 521 522 private string prependLineNumbers(string text) { 523 import std.string : splitLines, KeepTerminator; 524 import std.format : format; 525 import std.math : log10; 526 527 auto lines = text.splitLines(KeepTerminator.yes); 528 immutable width = 1 + cast(int)log10(lines.length); 529 string res; 530 foreach(i, l; lines) { 531 res ~= format("%*s. %s", width, i+1, l); 532 } 533 return res; 534 }