migrate to initial.d over dini

initial.d is my very own INI parser, see
  <https://git.sr.ht/~jeremy/initial>
This commit is contained in:
Jeremy Baxter 2024-02-13 09:31:56 +13:00
parent de9e18f06e
commit 2e93c891fd
10 changed files with 509 additions and 1642 deletions

454
initial.d Normal file
View file

@ -0,0 +1,454 @@
/*
* 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)
{
sections[defaultSection][k] = v;
}
/++
+ Sets the value of the `INISection` named *sect*.
+/
void
opIndexAssign(INISection v, string sect)
{
sections[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 *k* to *T*.
+ On conversion failure throws an `INITypeException`
+ with a message containing location information.
+/
T
keyAs(T)(string k, string defaultValue = null)
{
try
return key(k, defaultValue).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 if a hash character `#` is found at the beginning of a line,
+ the rest of the line is ignored)
+ $(LI if a left square bracket character `[` is found at
+ the beginning of a line, the line is considered a
+ section heading)
+ $(LI inside a section heading, 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 if 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 if whitespace is found in the first part of an assignment,
+ it is ignored and the next equals sign is looked for
+ immediately)
+ $(LI if 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;
nextline:
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) {
/* comment? */
case '#':
if (cn == 0)
continue nextline;
text ~= ch;
break;
/* 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));
}