From 2e3b8f536a4f8c304566a46b8a1e5a6fc56f5318 Mon Sep 17 00:00:00 2001 From: Jeremy Baxter Date: Wed, 24 Jan 2024 20:15:16 +1300 Subject: [PATCH] add initial.d --- initial.d | 444 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 initial.d diff --git a/initial.d b/initial.d new file mode 100644 index 0000000..7bfc03e --- /dev/null +++ b/initial.d @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2024 Jeremy Baxter. All rights reserved. + * + * Boost Software License - Version 1.0 - August 17th, 2003 + * + * Permission is hereby granted, free of charge, to any person or organization + * obtaining a copy of the software and accompanying documentation covered by + * this license (the "Software") to use, reproduce, display, distribute, + * execute, and transmit the Software, and to prepare derivative works of the + * Software, and to permit third-parties to whom the Software is furnished to + * do so, all subject to the following: + * + * The copyright notices in the Software and this entire statement, including + * the above license grant, this restriction and the following disclaimer, + * must be included in all copies of the Software, in whole or in part, and + * all derivative works of the Software, unless such copies or derivative + * works are solely in the form of machine-executable object code generated by + * a source language processor. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT + * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +/++ + + INI is a simple, concise configuration or data interchange + + format. It consists of keys and values listed inside a section. + + An INI document might look something like this: + + --- + + [user] + + name = Kevin + + age = 26 + + emailAddress = kevin@example.net + + + + [config] + + useBundledMalloc = false + + forceDefaultInit = true + + --- + + + + $(B initial) is a library for parsing INI documents into an `INIUnit`, + + a data structure representing an INI document. The function `readINI` + + contains the parser; it parses the INI given to it and deserialises + + it into the given `INIUnit`. The `readINIFile` function can be used + + to read the INI from a file rather than a string. + + + + $(B initial) is fully @safe, and can be used in @safe code. + + + + License: [Boost License 1.0](https://www.boost.org/LICENSE_1_0.txt). + + Authors: Jeremy Baxter + +/ +module initial; + +import std.conv : to, ConvException; +import std.exception : basicExceptionCtors, enforce; +import std.format : format; +import std.traits : isSomeChar; + +private alias parserEnforce = enforce!INIParseException; + +@safe: + +/++ + + Structure that represents an INI file. + + + + Contains `INISection`s in an associative array that can be + + directly accessed if needs be, or the index operator can be + + used to get an `INISection` and automatically initialise it + + if needed. + + + + The `serialise` method can be used to serialise the entire + + structure into an INI document that can be then read again + + by the parser and modified by humans. + + + + Example: + + --- + + INIUnit ini; + + + + /* write INI data */ + + ini["section"]["key"] = "value"; + + ini["section"]["num"] = "4.8"; + + + + /* read INI data */ + + readINI(ini, ` + + [section] + + num = 5.3 + + `); + + + + /* read INI from file */ + + readINIFile(ini, "config.ini"); + + + + /* write INI to file */ + + import std.file : write; + + write("config.ini", ini.serialise()); + + --- + +/ +struct INIUnit +{ + INISection[string] sections; /++ Hashmap of INISections in this unit. +/ + string defaultSection = "main"; /++ Name of the default section. +/ + + nothrow: + + /++ + + Returns the value of *k* in the default section + + or *defaultValue* if *k* is not present. + +/ + string + key(string k, string defaultValue = null) + { + return this[defaultSection].key(k, defaultValue); + } + + /++ + + Serialises this `INIUnit` into an INI document. + + + + If *defaultSectionHeading* is true, includes the + + section heading of the default section. + + Example: + + --- + + INIUnit ini; + + + + ini["sections"] = "ages suburbs"; + + ini["ages"]["John"] = "37"; + + ini["ages"]["Mary"] = "29"; + + ini["suburbs"]["John"] = "Gordonton"; + + ini["suburbs"]["Mary"] = "Ashmore"; + + + + writeln(ini.serialise()); + + /* + + * sections = ages suburbs + + * [ages] + + * John = 37 + + * Mary = 29 + + * [suburbs] + + * John = Gordonton + + * Mary = Ashmore + + */ + + --- + +/ + char[] + serialise(bool defaultSectionHeading = false) + { + char[] buf; + + buf = this[defaultSection].serialise(defaultSectionHeading); + foreach (string sect; sections.byKey()) { + if (sect == defaultSection) + continue; + + buf ~= this[sect].serialise(); + } + + return buf; + } + + /++ + + Returns the `INISection` associated with the name *sect*. + + Initialises it if it doesn't exist. + +/ + ref INISection + opIndex(string sect) + { + if (!(sect in sections)) + sections[sect] = INISection(sect); + + return sections[sect]; + } + + /++ + + Sets the value of the key *k* in the default section to *v*. + + Example: + + --- + + ini["key"] = "value"; + + --- + +/ + void + opIndexAssign(string v, string k) + { + this[defaultSection][k] = v; + } + + /++ + + Sets the value of the `INISection` named *sect*. + +/ + void + opIndexAssign(INISection v, string sect) + { + this[sect] = v; + } +} + +/++ + + Represents an INI section. + + + + Holds keys in the form of an associative array. + + Index the `keys` property to get this data raw, or to avoid + + range errors, use the `key` or `keyAs` functions. + + + + `alias this` is applied to the `keys` associative array, + + meaning you can index the structure to get or set keys' values. + +/ +struct INISection +{ + string[string] keys; /++ Hashmap of keys belonging to this section. +/ + string name; /++ Name of this section. +/ + + /++ + + Construct a new `INISection` with the given name. + + Called by `readINI` and `INIUnit.opIndex`. + +/ + this(string name) nothrow + { + this.name = name; + keys = new string[string]; + } + + /+ + + Returns the value of `k` in this section or + + `defaultValue` if the key is not present. + +/ + string + key(string k, string defaultValue = null) nothrow + { + return k in keys ? keys[k] : defaultValue; + } + + /++ + + Using `std.conv`, converts the value of the given key + + to the given type. On conversion failure throws an + + `INITypeException` with a message containing location + + information. + +/ + T + keyAs(T)(string k) + { + try + return key(k).to!T(); + catch (ConvException) + throw new INITypeException( + format!"unable to convert [%s].%s to %s"(name, k, T.stringof)); + } + + /++ + + Serialise this section into an INI document. + + + + If `sectionHeading` is true, includes the section heading at the top. + +/ + char[] + serialise(bool sectionHeading = true) nothrow + { + char[] buf; + + if (sectionHeading) + buf ~= "[" ~ name.to!(char[]) ~ "]\n"; + + foreach (string key; keys.byKey()) { + buf ~= key ~ " = " ~ keys[key] ~ "\n"; + } + + return buf; + } + + alias keys this; +} + +/++ + + Exception thrown when parsing failure occurs. + +/ +class INIParseException : Exception +{ + mixin basicExceptionCtors; +} + +/++ + + Exception thrown when conversion failure occurs. + +/ +class INITypeException : Exception +{ + mixin basicExceptionCtors; +} + +private string +locate(size_t l, size_t c) +{ + return format!"%d:%d"(l, c); +} + +/++ + + Parse the given INI data into an `INIUnit`. + + + + To parse a file's contents, use readINIFile instead. + + + + Whitespace is first stripped from the beginning and end of + + the line before parsing it. + + + + The following rules apply when parsing: + + $(UL + + $(LI once a left square bracket character `[` is found at + + the beginning of a line, the line is considered a + + section heading) + + $(LI after the left square bracket character, any character + + can be used inside the section heading except the right + + square bracket character) + + $(LI once the right square bracket character `]` is found, + + the section heading is considered finished. Any trailing + + characters are garbage and will trigger a parse error.) + + $(LI once a character except the left square bracket is found + + at the beginning of a line, the line is considered an + + assignment in the current section) + + $(LI once an equals character `=` is found after the first + + part of an assignment, the rest of the line is considered + + the "value" while the first part is the "key") + + $(LI when whitespace is found in the first part of an assignment, + + it is ignored and the next equals sign is looked for + + immediately) + + $(LI when whitespace is found after an equals character `=` + + in an assignment, it is ignored until the first + + non-whitespace character is found) + + $(LI once a non-whitespace character is found after an equals + + character `=` in an assignment, it is counted part of the + + "value") + +/ +void +readINI(ref INIUnit ini, string data) +{ + import std.string : splitLines, strip; + + string section; /* name of current section */ + string text; /* current key, value, section, whatever */ + string key; /* name of key in section */ + string value; /* value */ + bool inAssign; /* in key assignment? */ + bool inHeading; /* in section heading? */ + bool needEqls; /* require equals sign immediately? */ + bool pastEqls; /* past equals sign in assignment? */ + bool trailing; /* trailing garbage after heading? */ + + section = ini.defaultSection; + + foreach (size_t ln, string line; data.splitLines()) { + line = line.strip(); + if (line.length == 0) + continue; + + inAssign = inHeading = needEqls = pastEqls = trailing = false; + text = key = value = ""; + foreach (size_t cn, char ch; line) { + switch (ch) { + /* beginning of a section heading? */ + case '[': + if (cn == 0) /* beginning of line? */ + inHeading = true; + else + text ~= ch; + break; + + /* end of a section heading? */ + case ']': + parserEnforce(inHeading || inAssign, + locate(ln, cn) ~ ": unexpected character ']'"); + + if (inHeading) { + section = text; + text = ""; + inHeading = false; + trailing = true; + } else if (inAssign) { + text ~= ch; + } + break; + + /* middle of an assignment? */ + case '=': + if (inAssign) { + if (pastEqls) { + /* count it as part of the value */ + text ~= ch; + } else { + key = text; + text = ""; + pastEqls = true; + needEqls = false; + } + } else { + goto default; + } + break; + + /* whitespace */ + case ' ': + case '\t': + case '\v': + case '\r': + case '\n': + case '\f': + if (inAssign) { + if (!pastEqls) + needEqls = true; + } + break; + + default: + if (cn == 0) /* beginning of line? */ + inAssign = true; + + parserEnforce((inAssign || inHeading) && !trailing, + locate(ln, cn) ~ ": unexpected character '" ~ ch ~ "'"); + + if (inAssign) + parserEnforce(!needEqls, + locate(ln, cn) ~ ": expected '=', not '" ~ ch ~ "'"); + + text ~= ch; + break; + } + } + /* once the line has finished being looped over... */ + if (inAssign) { + parserEnforce(key.length != 0, + locate(ln, line.length - 1) ~ ": key cannot be empty"); + value = text; + text = ""; + + if (!(section in ini.sections)) + ini[section] = INISection(section); + ini[section][key] = value; + } + } +} + +/++ + + Parse INI from a file located at `fileName`. + +/ +void +readINIFile(ref INIUnit ini, string fileName) +{ + import std.file : readText; + + readINI(ini, readText(fileName)); +}