Merge branch 'master' into feature/tree

This commit is contained in:
Tristan B. Velloza Kildaire 2024-04-27 15:17:13 +02:00
commit eeb1fd4a46
5 changed files with 1088 additions and 8 deletions

View File

@ -40,6 +40,10 @@ is expected to grow over time.
* `niknaks.containers`
* Some useful container types
* Things such as `CacheMap`
* `niknaks.mechanisms`
* User-defined input prompter, retry mechanisms
* `niknaks.config`
* Configuration entries and management
## License

858
source/niknaks/config.d Normal file
View File

@ -0,0 +1,858 @@
/**
* Configuration management
*
* Configuration entries and
* a registry in which to
* manage a set of them
*
* Authors: Tristan Brice Velloza Kildaire (deavmi)
*/
module niknaks.config;
import std.string : format;
version(unittest)
{
import std.stdio : writeln;
}
/**
* A union which expands to
* the byte-width of its
* biggest member, allowing
* us to have space enough
* for any one exclusive member
* at a time
*
* See_Also: ConfigEntry
*/
private union ConfigValue
{
string text;
int integer;
bool flag;
string[] textArray;
}
/**
* The type of the entry
*/
public enum ConfigType
{
/**
* A string
*/
TEXT,
/**
* An integer
*/
NUMERIC,
/**
* A boolean
*/
FLAG,
/**
* A string array
*/
ARRAY
}
/**
* An exception thrown when you misuse
* a configuration entry
*/
public final class ConfigException : Exception
{
private this(string msg)
{
super(msg);
}
}
/**
* A configuration entry which
* acts as a typed union and
* supports certain fixed types
*/
public struct ConfigEntry
{
private ConfigValue value;
private ConfigType type;
/**
* A flag which is used to
* know if a value has been
* set at all. This helps
* with the fact that
* an entry can be constructed
* without having a value set
*/
private bool isSet = false;
/**
* Ensures a value is set
*/
private void ensureSet()
{
if(!this.isSet)
{
throw new ConfigException("This config entry has not yet been set");
}
}
/**
* Marks this entry as having
* a value set
*/
private void set()
{
this.isSet = true;
}
// TODO: Must have an unset flag
// @disable
// private this();
/**
* Constructs a new `ConfigEntry`
* with the given value and type
*
* Params:
* value = the value itself
* type = the value's type
*/
private this(ConfigValue value, ConfigType type)
{
this.value = value;
this.type = type;
set();
}
/**
* Creates a new configuration entry
* containing text
*
* Params:
* text = the text
* Returns: a `ConfigEntry`
*/
public static ConfigEntry ofText(string text)
{
ConfigValue tmp;
tmp.text = text;
return ConfigEntry(tmp, type.TEXT);
}
/**
* Creates a new configuration entry
* containing an integer
*
* Params:
* i = the integer
* Returns: a `ConfigEntry`
*/
public static ConfigEntry ofNumeric(int i)
{
ConfigValue tmp;
tmp.integer = i;
return ConfigEntry(tmp, type.NUMERIC);
}
/**
* Creates a new configuration entry
* containing a flag
*
* Params:
* flag = the flag
* Returns: a `ConfigEntry`
*/
public static ConfigEntry ofFlag(bool flag)
{
ConfigValue tmp;
tmp.flag = flag;
return ConfigEntry(tmp, type.FLAG);
}
/**
* Creates a new configuration entry
* containing a textual array
*
* Params:
* array = the textual array
* Returns: a `ConfigEntry`
*/
public static ConfigEntry ofArray(string[] array)
{
ConfigValue tmp;
tmp.textArray = array;
return ConfigEntry(tmp, type.ARRAY);
}
/**
* Returns the type of the
* entry's value
*
* Returns: a `ConfigType`
*/
public ConfigType getType()
{
return this.type;
}
/**
* Ensures the requested type
* matches the current type
* set
*
* Params:
* requested = the requested
* type
* Returns: `true` if the types
* are the same, `false` otherwise
*/
private bool ensureTypeMatch0(ConfigType requested)
{
return getType() == requested;
}
/**
* A version of the type
* matcher but which throws
* an exception on type mismatch
*
* See_Also: `ensureTypeMatch0(ConfigType)`
*/
private void ensureTypeMatch(ConfigType requested)
{
if(!ensureTypeMatch0(requested))
{
throw new ConfigException(format("The entry is not of type '%s'", requested));
}
}
/**
* Obtains the numeric value
* of this entry
*
* Returns: an integer
* Throws: ConfigException if
* the type of the value in this
* entry is not numeric
*/
public int numeric()
{
ensureSet;
ensureTypeMatch(ConfigType.NUMERIC);
return this.value.integer;
}
/**
* Obtains the textual array
* value of this entry
*
* Returns: a `string[]`
* Throws: ConfigException if
* the type of the value in this
* entry is not a textual array
*/
public string[] array()
{
ensureSet;
ensureTypeMatch(ConfigType. ARRAY);
return this.value.textArray;
}
/**
* See_Also: `array()`
*/
public string[] opSlice()
{
return array();
}
/**
* Obtains the flag value
* of this entry
*
* Returns: a `string[]`
* Throws: ConfigException if
* the type of the value in this
* entry is not a flag
*/
public bool flag()
{
ensureSet;
ensureTypeMatch(ConfigType.FLAG);
return this.value.flag;
}
/**
* See_Also: `flag()`
*/
public bool isTrue()
{
return flag() == true;
}
/**
* See_Also: `flag()`
*/
public bool isFalse()
{
return flag() == false;
}
/**
* Obtains the text value
* of this entry
*
* Returns: a string
* Throws: ConfigException if
* the type of the value in this
* entry is not a string
*/
public string text()
{
ensureSet;
ensureTypeMatch(ConfigType.TEXT);
return this.value.text;
}
/**
* Obtains the value of this
* configuration entry dependant
* on the requested casting type
* and matching that to the supported
* types of the configuration entry
*
* Returns: a value of type `T`
*/
public T opCast(T)()
{
static if(__traits(isSame, T, bool))
{
return flag();
}
else static if(__traits(isSame, T, string))
{
return text();
}
else static if(__traits(isSame, T, int))
{
return numeric();
}
else static if(__traits(isSame, T, string[]))
{
return array();
}
else
{
pragma(msg, "ConfigEntry opCast(): Cannot cast to a type '", T, "'");
static assert(false);
}
}
}
/**
* Tests out using the configuration
* entry and its various operator
* overloads
*/
unittest
{
ConfigEntry entry = ConfigEntry.ofArray(["hello", "world"]);
assert(entry[] == ["hello", "world"]);
entry = ConfigEntry.ofNumeric(1);
assert(entry.numeric() == 1);
entry = ConfigEntry.ofText("hello");
assert(cast(string)entry == "hello");
entry = ConfigEntry.ofFlag(true);
assert(entry);
}
/**
* Tests out the erroneous usage of a
* configuration entry
*/
unittest
{
ConfigEntry entry = ConfigEntry.ofText("hello");
try
{
entry[];
assert(false);
}
catch(ConfigException e)
{
}
}
/**
* Tests out the erroneous usage of a
* configuration entry
*/
unittest
{
ConfigEntry entry;
try
{
entry[];
assert(false);
}
catch(ConfigException e)
{
}
}
/**
* An entry derived from
* the `Registry` containing
* the name and the configuration
* entry itself
*/
public struct RegistryEntry
{
private string name;
private ConfigEntry val;
/**
* Constructs a new `RegistryEntry`
* with the given name and configuration
* entry
*
* Params:
* name = the name
* entry = the entry itself
*/
this(string name, ConfigEntry entry)
{
this.name = name;
this.val = entry;
}
/**
* Obtains the entry's name
*
* Returns: the name
*/
public string getName()
{
return this.name;
}
/**
* Obtains the entry itself
*
* Returns: a `ConfigEntry`
*/
public ConfigEntry getEntry()
{
return this.val;
}
}
/**
* An exception thrown when something
* goes wrong with your usage of the
* `Registry`
*/
public final class RegistryException : Exception
{
private this(string msg)
{
super(msg);
}
}
/**
* A registry for managing
* multiple mappings of
* string-based names to
* configuration entries
*/
public struct Registry
{
private ConfigEntry[string] entries;
private bool allowOverwriteEntry;
/**
* Creates a new `Registry`
* and sets the overwriting policy
*
* Params:
* allowOverwritingOfEntries = `true`
* if you want to allow overwriting of
* previously added entries, otherwise
* `false`
*/
this(bool allowOverwritingOfEntries)
{
setAllowOverwrite(allowOverwritingOfEntries);
}
/**
* Checks if an entry is present
*
* Params:
* name = the name
* Returns: `true` if present,
* otherwise `false`
*/
public bool hasEntry(string name)
{
return getEntry0(name) !is null;
}
/**
* Ontains a pointer to the configuration
* entry at the given key.
*
* Params:
* name = the key
* Returns: a `ConfigEntry*` if found,
* otherwise `null`
*/
private ConfigEntry* getEntry0(string name)
{
ConfigEntry* potEntry = name in this.entries;
return potEntry;
}
/**
* Obtains a pointer to the configuration
* entry at the given key. Allowing you
* to swap out its contents directly if
* you want to.
*
* Params:
* name = the key
* Returns: a `ConfigEntry*` if found,
* otherwise `null`
*/
public ConfigEntry* opBinaryRight(string op)(string name)
if(op == "in")
{
return getEntry0(name);
}
/**
* Obtain a configuration entry
* at the given key
*
* Params:
* name = the key
* entry = the found entry
* (if any)
* Returns: `true` if found,
* otherwise `false`
*/
public bool getEntry_nothrow(string name, ref ConfigEntry entry)
{
ConfigEntry* potEntry = getEntry0(name);
if(potEntry is null)
{
return false;
}
entry = *potEntry;
return true;
}
/**
* Obtain a configuration entry
* at the given key
*
* Params:
* name = the key
* Returns: a configuration entry
* Throws: RegistryException if
* there is no entry at that key
*/
public ConfigEntry opIndex(string name)
{
ConfigEntry entry;
if(!getEntry_nothrow(name, entry))
{
throw new RegistryException(format("Cannot find an entry by the name of '%s'", name));
}
return entry;
}
/**
* Set whether or not the overwriting
* of an entry should be allowed
*
* Params:
* flag = `true` if to allow, `false`
* if to deny
*/
public void setAllowOverwrite(bool flag)
{
this.allowOverwriteEntry = flag;
}
/**
* Adds a new configuration entry at the
* given key and allows you to choose
* certain behaviors based on the
* existence or non-existence of
* an entry at the same key.
*
* Params:
* name = the name of the entry
* entry = the entry itself
* allowOverWriteNow = if `true`
* then if an entry exists already
* at that key it will be overwritten,
* otherwise an exception will be thrown
* allowSetOnCreation = if there is
* no entry at the given key then,
* if `true`, an entry will be created,
* otherwise an exception will be thrown
*/
private void newEntry(string name, ConfigEntry entry, bool allowOverWriteNow, bool allowSetOnCreation)
{
// Obtain the address of the value that occupies the value
// the key in the map
ConfigEntry* entryExist = getEntry0(name);
// If something is present but overwiritng is disabled
if((entryExist !is null) && !allowOverWriteNow)
{
throw new RegistryException(format("An entry already exists at '%s' and overwriting is not allowed", name));
}
// If something is present and overwiring is enabled
else if(entryExist !is null)
{
// Now simply update the data in-place
*entryExist = entry;
}
// If nothing is present but setting-on-creation is enabled
else if(allowSetOnCreation)
{
// Then create the entry
this.entries[name] = entry;
}
// If nothing is present BUT setting-on-creation was NOT allowed
else
{
throw new RegistryException(format("Cannot set-on-creation for entry '%s' as it is not allowed", name));
}
}
/**
* Creates a new entry and adds it
*
* An exception is thrown if an entry
* at that key exists and the policy
* for overwriting is to deny
*
* Params:
* name = the key
* entry = the configuration entry
*/
public void newEntry(string name, ConfigEntry entry)
{
newEntry(name, entry, this.allowOverwriteEntry, true);
}
/**
* See_Also: `newEntry(name, ConfigEntry)`
*/
public void newEntry(string name, int numeric)
{
newEntry(name, ConfigEntry.ofNumeric(numeric));
}
/**
* See_Also: `newEntry(name, ConfigEntry)`
*/
public void newEntry(string name, string text)
{
newEntry(name, ConfigEntry.ofText(text));
}
/**
* See_Also: `newEntry(name, ConfigEntry)`
*/
public void newEntry(string name, bool flag)
{
newEntry(name, ConfigEntry.ofFlag(flag));
}
/**
* See_Also: `newEntry(name, ConfigEntry)`
*/
public void newEntry(string name, string[] array)
{
newEntry(name, ConfigEntry.ofArray(array));
}
/**
* Sets the entry at the given name
* to the provided entry
*
* This will throw an exception if
* the entry trying to be set does
* not yet exist.
*
* Overwriting will only be allowed
* if the policy allows it.
*
* Params:
* name = the key
* entry = the configuration
* entry
*/
public void setEntry(string name, ConfigEntry entry)
{
newEntry(name, entry, this.allowOverwriteEntry, false);
}
/**
* Assigns the provided configuration
* entry to the provided name
*
* Take note that using this method
* will create the entry if it does
* not yet exist.
*
* It will also ALWAYS allow overwriting.
*
* Params:
* entry = the entry to add
* name = the name at which to
* add the entry
*/
public void opIndexAssign(ConfigEntry entry, string name)
{
newEntry(name, entry, true, true);
}
/**
* See_Also: `opIndexAssign(ConfigEntry, string)`
*/
public void opIndexAssign(int numeric, string name)
{
opIndexAssign(ConfigEntry.ofNumeric(numeric), name);
}
/**
* See_Also: `opIndexAssign(ConfigEntry, string)`
*/
public void opIndexAssign(string entry, string name)
{
opIndexAssign(ConfigEntry.ofText(entry), name);
}
/**
* See_Also: `opIndexAssign(ConfigEntry, string)`
*/
public void opIndexAssign(bool flag, string name)
{
opIndexAssign(ConfigEntry.ofFlag(flag), name);
}
/**
* See_Also: `opIndexAssign(ConfigEntry, string)`
*/
public void opIndexAssign(string[] array, string name)
{
opIndexAssign(ConfigEntry.ofArray(array), name);
}
/**
* Returns all the entries in the
* registry as a mapping of their
* name to their configuration entry
*
* See_Also: RegistryEntry
* Returns: an array of registry
* entries
*/
public RegistryEntry[] opSlice()
{
RegistryEntry[] entrieS;
foreach(string entryName; this.entries.keys())
{
entrieS ~= RegistryEntry(entryName, this.entries[entryName]);
}
return entrieS;
}
}
/**
* Tests out the working with the
* registry in order to manage
* a set of named configuration
* entries
*/
unittest
{
Registry reg = Registry(false);
// Add an entry
reg.newEntry("name", ConfigEntry.ofText("Tristan"));
// Check it exists
assert(reg.hasEntry("name"));
// Adding it again should fail
try
{
reg.newEntry("name", ConfigEntry.ofText("Tristan2"));
assert(false);
}
catch(RegistryException e)
{
}
// Check that the entry still has the right value
assert(cast(string)reg["name"] == "Tristan");
// Add a new entry and test its prescence
reg["age"] = 24;
assert(cast(int)reg["age"] == 24);
// Update it
reg["age"] = 25;
assert(cast(int)reg["age"] == 25);
// Obtain a handle on the configuration
// entry, then update it and read it back
// to confirm
ConfigEntry* ageEntry = "age" in reg;
*ageEntry = ConfigEntry.ofNumeric(69_420);
assert(cast(int)reg["age"] == 69_420);
// Should not be able to set entry it not yet existent
try
{
reg.setEntry("male", ConfigEntry.ofFlag(true));
assert(false);
}
catch(RegistryException e)
{
}
// All entries
RegistryEntry[] all = reg[];
assert(all.length == 2);
writeln(all);
}

View File

@ -357,6 +357,14 @@ public template CacheMap(K, V)
return keyValue;
}
/**
* See_Also: get
*/
public V opIndex(K key)
{
return get(key);
}
/**
* Removes the given key
* returning whether or
@ -502,18 +510,18 @@ unittest
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(10));
// Get the value
int tValue = map.get("Tristan");
int tValue = map["Tristan"];
assert(tValue == 1);
// Get the value (should still be cached)
tValue = map.get("Tristan");
tValue = map["Tristan"];
assert(tValue == 1);
// Wait for expiry (by sweeping thread)
Thread.sleep(dur!("seconds")(11));
// Should call replacement function
tValue = map.get("Tristan");
tValue = map["Tristan"];
assert(tValue == 2);
// Wait for expiry (by sweeping thread)
@ -545,14 +553,14 @@ unittest
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(5), dur!("seconds")(10));
// Get the value
int tValue = map.get("Tristan");
int tValue = map["Tristan"];
assert(tValue == 1);
// Wait for 5 seconds (the entry should then be expired by then for on-access check)
Thread.sleep(dur!("seconds")(5));
// Get the value (should have replacement function run)
tValue = map.get("Tristan");
tValue = map["Tristan"];
assert(tValue == 2);
// Destroy the map (such that it ends the sweeper
@ -577,14 +585,14 @@ unittest
CacheMap!(string, int) map = new CacheMap!(string, int)(&getVal, dur!("seconds")(10), dur!("seconds")(10));
// Get the value
int tValue = map.get("Tristan");
int tValue = map["Tristan"];
assert(tValue == 1);
// Remove the key
assert(map.removeKey("Tristan"));
// Get the value
tValue = map.get("Tristan");
tValue = map["Tristan"];
assert(tValue == 2);
// Destroy the map (such that it ends the sweeper

View File

@ -9,6 +9,8 @@ import std.functional : toDelegate;
import std.datetime : Duration;
import std.datetime.stopwatch : StopWatch, AutoStart;
import core.thread : Thread;
import std.stdio : File, write;
import std.string : strip;
/**
* A verdict-providing function
@ -291,4 +293,203 @@ unittest
}
}
/**
* A user-defined prompt
*/
public struct Prompt
{
private string query;
private string value;
/**
* Constructs a new prompt
* with the given query
*
* Params:
* query = the prompt
* query itself
*/
this(string query)
{
this.query = query;
}
/**
* Gets the prompt query
*
* Returns: the query
*/
public string getQuery()
{
return this.query;
}
/**
* Retrieves this prompt's
* answer
*
* Returns: the answer
*/
public string getValue()
{
return this.value;
}
/**
* Fill this prompt's
* query with a corresponding
* answer
*
* Params:
* value = the answer
*/
public void fill(string value)
{
this.value = value;
}
}
/**
* A prompting mechanism
* which can be filled up
* with questions and a
* file-based source to
* read answers in from
* and associate with
* their original respective
* questions
*/
public class Prompter
{
/**
* Source file
*/
private File source;
/**
* Whether or not to close
* the source file on destruction
*/
private bool closeOnDestruct;
/**
* Prompts to query by
*/
private Prompt[] prompts;
/**
* Constructs a new prompter
* with the given file source
* from where the input is to
* be read from.
*
* Params:
* source = the `File` to
* read from
* closeOnDestruct = if
* set to `true` then on
* destruction we will close
* the source, if `false` it
* is left untouched
*
* Throws:
* Exception if the provided
* `File` is not open
*/
this(File source, bool closeOnDestruct = false)
{
if(!source.isOpen())
{
throw new Exception("Source not open");
}
this.closeOnDestruct = closeOnDestruct;
this.source = source;
}
/**
* Appends the given prompt
*
* Params:
* prompt = the prompt
*/
public void addPrompt(Prompt prompt)
{
this.prompts ~= prompt;
}
/**
* Performs the prompting
* by querying each attached
* prompt for an answer
* which is then associated
* with the given prompt
*
* Returns: the answered
* prompts
*/
public Prompt[] prompt()
{
char[] buff;
foreach(ref Prompt prompt; this.prompts)
{
scope(exit)
{
buff.length = 0;
}
// Perform the query
write(prompt.getQuery());
this.source.readln(buff);
// Fill answer into prompt
prompt.fill(strip(cast(string)buff));
}
return this.prompts;
}
/**
* Destructor
*/
~this()
{
if(this.closeOnDestruct)
{
this.source.close();
}
}
}
version(unittest)
{
import std.process : pipe, Pipe;
import std.conv : to;
import std.stdio : writeln;
}
unittest
{
Pipe pipe = pipe();
// Create a prompter with some prompts
Prompter p = new Prompter(pipe.readEnd());
p.addPrompt(Prompt("What is your name?"));
p.addPrompt(Prompt("How old are you"));
// Fill up pipe with data for read end
File writeEnd = pipe.writeEnd();
writeEnd.writeln("Tristan Brice Velloza Kildaire");
writeEnd.writeln(1);
writeEnd.flush();
// Perform the prompt and get the
// answers back out
Prompt[] ans = p.prompt();
writeln(ans);
assert(ans[0].getValue() == "Tristan Brice Velloza Kildaire");
assert(to!(int)(ans[1].getValue()) == 1); // TODO: Allow union conversion later
}

View File

@ -41,4 +41,13 @@ public import niknaks.debugging;
* Useful data structures
* that are templatised
*/
public import niknaks.containers;
public import niknaks.containers;
/**
* Configuration management
*
* Configuration entries and
* a registry in which to
* manage a set of them
*/
public import niknaks.config;