niknaks/source/niknaks/mechanisms.d

495 lines
9.4 KiB
D

/**
* An assortment of mechanisms
*
* Authors: Tristan Brice Velloza Kildaire (deavmi)
*/
module niknaks.mechanisms;
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
*/
public alias VerdictProviderFunction = bool function();
/**
* A verdict-providing delegate
*/
public alias VerdictProviderDelegate = bool delegate();
/**
* An exception thrown when a `Delay`
* mechanism times out
*/
public final class DelayTimeoutException : Exception
{
/**
* The offending delay mechanism
*/
private Delay delay;
/**
* Constructs a new exception with
* the offending `Delay`
*
* Params:
* delay = the offending `Delay`
*/
this(Delay delay)
{
super("Timed out whilst attempting delay mechanism");
this.delay = delay;
}
/**
* Returns the offending delay
* mechanism
*
* Returns: the `Delay`
*/
public Delay getDelay()
{
return this.delay;
}
}
/**
* A mechanism that consumes a function
* and calls it at a regular interval,
* exiting if it returns a `true` verdict
* within a certain time limit but
* throwing an exception if it never
* returned a `true` verdict in said
* time window and the time was exceeded
*/
public class Delay
{
/**
* The interval to retry
* and the total timeout
*/
private Duration interval, timeout;
/**
* The delegate to call
* to obtain a verdict
*/
private VerdictProviderDelegate verdictProvider;
/**
* Internal timer
*/
private StopWatch timer = StopWatch(AutoStart.no);
/**
* Constructs a new delay mechanism
* with the given delegate to call
* in order to determine the verdict,
* an interval to call it at and the
* total timeout
*
* Params:
* verdictProvider = the provider of the verdicts
* interval = thje interval to retry at
* timeout = the timeout
*/
this(VerdictProviderDelegate verdictProvider, Duration interval, Duration timeout)
{
this.verdictProvider = verdictProvider;
this.interval = interval;
this.timeout = timeout;
}
/**
* Constructs a new delay mechanism
* with the given function to call
* in order to determine the verdict,
* an interval to call it at and the
* total timeout
*
* Params:
* verdictProvider = the provider of the verdicts
* interval = thje interval to retry at
* timeout = the timeout
*/
this(VerdictProviderFunction verdictProvider, Duration interval, Duration timeout)
{
this(toDelegate(verdictProvider), interval, timeout);
}
/**
* Performs the delay mechanism
*
* Throws:
* DelayTimeoutException if
* we time out
*/
public void go()
{
// On leave stop-and-reset (this is for re-use)
scope(exit)
{
this.timer.stop();
this.timer.reset();
}
// Start timer
this.timer.start();
// Try get verdict initially
bool result = verdictProvider();
// If verdict is a pass, return now
if(result)
{
return;
}
// Whilst still in time window
while(this.timer.peek() < this.timeout)
{
// Wait a little bit
Thread.sleep(this.interval);
// Try get verdict
result = verdictProvider();
// If verdict is a pasds, return now
if(result)
{
return;
}
}
// If we get here it is because we timed out
throw new DelayTimeoutException(this);
}
}
version(unittest)
{
import std.datetime : dur;
}
/**
* Tests out the delay mechanism
* with a verdict provider (as a
* delegate) which is always false
*/
unittest
{
bool alwaysFalse()
{
return false;
}
Delay delay = new Delay(&alwaysFalse, dur!("seconds")(1), dur!("seconds")(1));
try
{
delay.go();
assert(false);
}
catch(DelayTimeoutException e)
{
assert(true);
}
}
version(unittest)
{
bool alwaysFalseFunc()
{
return false;
}
}
/**
* Tests out the delay mechanism
* with a verdict provider (as a
* function) which is always false
*/
unittest
{
Delay delay = new Delay(&alwaysFalseFunc, dur!("seconds")(1), dur!("seconds")(1));
try
{
delay.go();
assert(false);
}
catch(DelayTimeoutException e)
{
assert(true);
}
}
/**
* Tests out the delay mechanism
* with a verdict provider (as a
* function) which is always true
*/
unittest
{
bool alwaysTrue()
{
return true;
}
Delay delay = new Delay(&alwaysTrue, dur!("seconds")(1), dur!("seconds")(1));
try
{
delay.go();
assert(true);
}
catch(DelayTimeoutException e)
{
assert(false);
}
}
/**
* Tests out the delay mechanism
* with a verdict provider (as a
* delegate) which is only true
* on the second call
*/
unittest
{
int cnt = 0;
bool happensLater()
{
cnt++;
if(cnt == 2)
{
return true;
}
else
{
return false;
}
}
Delay delay = new Delay(&happensLater, dur!("seconds")(1), dur!("seconds")(1));
try
{
delay.go();
assert(true);
}
catch(DelayTimeoutException e)
{
assert(false);
}
}
/**
* 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
}