1 /**
2  * An assortment of mechanisms
3  *
4  * Authors: Tristan Brice Velloza Kildaire (deavmi)
5  */
6 module niknaks.mechanisms;
7 
8 import std.functional : toDelegate;
9 import std.datetime : Duration;
10 import std.datetime.stopwatch : StopWatch, AutoStart;
11 import core.thread : Thread;
12 import std.stdio : File, write;
13 import std.string : strip, empty;
14 
15 version(unittest)
16 {
17     import std.process : pipe, Pipe;
18     import std.conv : to;
19     import std.stdio : writeln;
20 }
21 
22 /** 
23  * A verdict-providing function
24  */
25 public alias VerdictProviderFunction = bool function();
26 
27 /** 
28  * A verdict-providing delegate
29  */
30 public alias VerdictProviderDelegate = bool delegate();
31 
32 /** 
33  * An exception thrown when a `Delay`
34  * mechanism times out
35  */
36 public final class DelayTimeoutException : Exception
37 {
38     /** 
39      * The offending delay mechanism
40      */
41     private Delay delay;
42 
43     /** 
44      * Constructs a new exception with
45      * the offending `Delay`
46      *
47      * Params:
48      *   delay = the offending `Delay`
49      */
50     this(Delay delay)
51     {
52         super("Timed out whilst attempting delay mechanism");
53 
54         this.delay = delay;
55     }
56 
57     /** 
58      * Returns the offending delay
59      * mechanism
60      *
61      * Returns: the `Delay`
62      */
63     public Delay getDelay()
64     {
65         return this.delay;
66     }
67 }
68 
69 /** 
70  * A mechanism that consumes a function
71  * and calls it at a regular interval,
72  * exiting if it returns a `true` verdict
73  * within a certain time limit but
74  * throwing an exception if it never
75  * returned a `true` verdict in said
76  * time window and the time was exceeded
77  */
78 public class Delay
79 {
80     /** 
81      * The interval to retry
82      * and the total timeout
83      */
84     private Duration interval, timeout;
85 
86     /** 
87      * The delegate to call
88      * to obtain a verdict
89      */
90     private VerdictProviderDelegate verdictProvider;
91 
92     /** 
93      * Internal timer
94      */
95     private StopWatch timer = StopWatch(AutoStart.no);
96 
97     /** 
98      * Constructs a new delay mechanism
99      * with the given delegate to call
100      * in order to determine the verdict,
101      * an interval to call it at and the
102      * total timeout
103      *
104      * Params:
105      *   verdictProvider = the provider of the verdicts
106      *   interval = thje interval to retry at
107      *   timeout = the timeout
108      */
109     this(VerdictProviderDelegate verdictProvider, Duration interval, Duration timeout)
110     {
111         this.verdictProvider = verdictProvider;
112         this.interval = interval;
113         this.timeout = timeout;
114     }
115 
116     /** 
117      * Constructs a new delay mechanism
118      * with the given function to call
119      * in order to determine the verdict,
120      * an interval to call it at and the
121      * total timeout
122      *
123      * Params:
124      *   verdictProvider = the provider of the verdicts
125      *   interval = thje interval to retry at
126      *   timeout = the timeout
127      */
128     this(VerdictProviderFunction verdictProvider, Duration interval, Duration timeout)
129     {
130         this(toDelegate(verdictProvider), interval, timeout);
131     }
132 
133     /** 
134      * Performs the delay mechanism
135      *
136      * Throws:
137      *    DelayTimeoutException if
138      * we time out
139      */
140     public void go()
141     {
142         // On leave stop-and-reset (this is for re-use)
143         scope(exit)
144         {
145             this.timer.stop();
146             this.timer.reset();
147         }
148 
149         // Start timer
150         this.timer.start();
151 
152         // Try get verdict initially
153         bool result = verdictProvider();
154 
155         // If verdict is a pass, return now
156         if(result)
157         {
158             return;
159         }
160 
161         // Whilst still in time window
162         while(this.timer.peek() < this.timeout)
163         {
164             // Wait a little bit
165             Thread.sleep(this.interval);
166 
167             // Try get verdict
168             result = verdictProvider();
169 
170             // If verdict is a pasds, return now
171             if(result)
172             {
173                 return;
174             }
175         }
176         
177         // If we get here it is because we timed out
178         throw new DelayTimeoutException(this);
179     }
180 
181 }
182 
183 version(unittest)
184 {
185     import std.datetime : dur;
186 }
187 
188 /**
189  * Tests out the delay mechanism
190  * with a verdict provider (as a
191  * delegate) which is always false
192  */
193 unittest
194 {
195     bool alwaysFalse()
196     {
197         return false;
198     }
199 
200     Delay delay = new Delay(&alwaysFalse, dur!("seconds")(1), dur!("seconds")(1));
201 
202     try
203     {
204         delay.go();
205         assert(false);
206     }
207     catch(DelayTimeoutException e)
208     {
209         assert(true);
210     }
211     
212 }
213 
214 version(unittest)
215 {
216     bool alwaysFalseFunc()
217     {
218         return false;
219     }
220 }
221 
222 /**
223  * Tests out the delay mechanism
224  * with a verdict provider (as a
225  * function) which is always false
226  */
227 unittest
228 {
229     Delay delay = new Delay(&alwaysFalseFunc, dur!("seconds")(1), dur!("seconds")(1));
230 
231     try
232     {
233         delay.go();
234         assert(false);
235     }
236     catch(DelayTimeoutException e)
237     {
238         assert(true);
239     }
240     
241 }
242 
243 /**
244  * Tests out the delay mechanism
245  * with a verdict provider (as a
246  * function) which is always true
247  */
248 unittest
249 {
250     bool alwaysTrue()
251     {
252         return true;
253     }
254 
255     Delay delay = new Delay(&alwaysTrue, dur!("seconds")(1), dur!("seconds")(1));
256 
257     try
258     {
259         delay.go();
260         assert(true);
261     }
262     catch(DelayTimeoutException e)
263     {
264         assert(false);
265     }
266 }
267 
268 /**
269  * Tests out the delay mechanism
270  * with a verdict provider (as a
271  * delegate) which is only true
272  * on the second call
273  */
274 unittest
275 {
276     int cnt = 0;
277     bool happensLater()
278     {
279         cnt++;
280         if(cnt == 2)
281         {
282             return true;
283         }
284         else
285         {
286             return false;
287         }
288     }
289 
290     Delay delay = new Delay(&happensLater, dur!("seconds")(1), dur!("seconds")(1));
291 
292     try
293     {
294         delay.go();
295         assert(true);
296     }
297     catch(DelayTimeoutException e)
298     {
299         assert(false);
300     }
301 }
302 
303 /** 
304  * A user-defined prompt
305  */
306 public struct Prompt
307 {
308     private bool isMultiValue;
309     private bool allowEmpty;
310     private string query;
311     private string[] value;
312 
313     /** 
314      * Constructs a new prompt
315      * with the given query
316      *
317      * Params:
318      *   query = the prompt
319      * query itself
320      *   isMultiValue = if the
321      * query allows for multiple
322      * inputs (default is `false`)
323      *   allowEmpty = if the
324      * answer may be empty (default
325      * is `true`)
326      */
327     this(string query, bool isMultiValue = false, bool allowEmpty = false)
328     {
329         this.query = query;
330         this.isMultiValue = isMultiValue;
331         this.allowEmpty = allowEmpty;
332     }
333 
334     /** 
335      * Gets the prompt query
336      *
337      * Returns: the query
338      */
339     public string getQuery()
340     {
341         return this.query;
342     }
343 
344     /** 
345      * Retrieves this prompt's
346      * answer
347      *
348      * Params:
349      *   answer = the first
350      * answer is placed here
351      * (if any)
352      * Returns: `true` if there
353      * is at least one answer,
354      * `false` otherwise
355      */
356     public bool getValue(ref string answer)
357     {
358         if(this.value.length)
359         {
360             answer = this.value[0];
361             return true;
362         }
363 
364         return false;
365     }
366 
367     /** 
368      * Retrieves this prompt's
369      * multiple anwers
370      *
371      * Params:
372      *  answers = the answers
373      * (if any)
374      * Returns: `true` if there
375      * are answers, `false` otherwise
376      */
377     public bool getValues(ref string[] answers)
378     {
379         if(this.value.length)
380         {
381             answers = this.value;
382             return true;
383         }
384         
385         return false;
386     }
387 
388     /** 
389      * Fill this prompt's
390      * query with a corresponding
391      * answer
392      *
393      * Params:
394      *   value = the answer
395      */
396     public void fill(string value)
397     {
398         this.value ~= value;
399     }
400 }
401 
402 /** 
403  * A prompting mechanism
404  * which can be filled up
405  * with questions and a
406  * file-based source to
407  * read answers in from
408  * and associate with
409  * their original respective
410  * questions
411  */
412 public class Prompter
413 {
414     /** 
415      * Source file
416      */
417     private File source;
418 
419     /** 
420      * Whether or not to close
421      * the source file on destruction
422      */
423     private bool closeOnDestruct;
424 
425     /** 
426      * Prompts to query by
427      */
428     private Prompt[] prompts;
429 
430     /** 
431      * Constructs a new prompter
432      * with the given file source
433      * from where the input is to
434      * be read from.
435      *
436      * Params:
437      *   source = the `File` to
438      * read from
439      *   closeOnDestruct = if
440      * set to `true` then on
441      * destruction we will close
442      * the source, if `false` it
443      * is left untouched
444      *
445      * Throws:
446      *   Exception if the provided
447      * `File` is not open
448      */
449     this(File source, bool closeOnDestruct = false)
450     {
451         if(!source.isOpen())
452         {
453             throw new Exception("Source not open");
454         }
455 
456         this.closeOnDestruct = closeOnDestruct;
457         this.source = source;
458     }
459 
460     /** 
461      * Appends the given prompt
462      *
463      * Params:
464      *   prompt = the prompt
465      */
466     public void addPrompt(Prompt prompt)
467     {
468         this.prompts ~= prompt;
469     }
470 
471     /** 
472      * Performs the prompting
473      * by querying each attached
474      * prompt for an answer
475      * which is then associated
476      * with the given prompt
477      *
478      * Returns: the answered
479      * prompts
480      */
481     public Prompt[] prompt()
482     {
483         char[] buff;
484 
485         prompt_loop: foreach(ref Prompt prompt; this.prompts)
486         {
487             scope(exit)
488             {
489                 buff.length = 0;
490             }
491 
492             // Prompt until empty
493             if(prompt.isMultiValue)
494             {
495                 string ans;
496 
497                 do
498                 {
499                     // If EOF signalled to us then
500                     // exit
501                     if(this.source.eof())
502                     {
503                         break prompt_loop;
504                     }
505 
506                     scope(exit)
507                     {
508                         buff.length = 0;
509                     }
510 
511                     // Perform the query
512                     write(prompt.getQuery());
513                     this.source.readln(buff);
514                     ans = strip(cast(string)buff);
515 
516                     // If not empty, then add
517                     if(!ans.empty())
518                     {
519                         prompt.fill(ans);
520                     }
521                 }
522                 while(!ans.empty());
523             }
524             // Prompt once (or more depending on policy)
525             else
526             {
527                 string ans;
528                 do
529                 {
530                     // If EOF signalled to us then
531                     // exit
532                     if(this.source.eof())
533                     {
534                         break prompt_loop;
535                     }
536 
537                     // Perform the query
538                     write(prompt.getQuery());
539                     this.source.readln(buff);
540                     ans = strip(cast(string)buff);
541                 }
542                 while(ans.empty() && !prompt.allowEmpty);
543 
544                 // Fill answer into prompt
545                 prompt.fill(ans);
546             }
547         }
548 
549         return this.prompts;
550     }
551 
552     /** 
553      * Destructor
554      */
555     ~this()
556     {
557         if(this.closeOnDestruct)
558         {
559             this.source.close();
560         }
561     }
562 }
563 
564 /**
565  * Creating two single-valued prompts
566  * and then extracting the answers to
567  * them out
568  */
569 unittest
570 {
571     Pipe pipe = pipe();
572 
573     // Create a prompter with some prompts
574     Prompter p = new Prompter(pipe.readEnd());
575     p.addPrompt(Prompt("What is your name?"));
576     p.addPrompt(Prompt("How old are you"));
577 
578     // Fill up pipe with data for read end
579     File writeEnd = pipe.writeEnd();
580     writeEnd.writeln("Tristan Brice Velloza Kildaire");
581     writeEnd.writeln(1);
582     writeEnd.flush();
583 
584     // Perform the prompt and get the
585     // answers back out
586     Prompt[] ans = p.prompt();
587 
588     writeln(ans);
589 
590     string nameVal;
591     assert(ans[0].getValue(nameVal));
592     assert(nameVal == "Tristan Brice Velloza Kildaire");
593 
594     string ageVal;
595     assert(ans[1].getValue(ageVal));
596     assert(to!(int)(ageVal) == 1); // TODO: Allow union conversion later
597 }
598 
599 /**
600  * Creating a single-value prompt
601  * which CANNOT be empty and then
602  * also a multi-valued prompt
603  */
604 unittest
605 {
606     Pipe pipe = pipe();
607 
608     // Create a prompter with some prompts
609     Prompter p = new Prompter(pipe.readEnd());
610     p.addPrompt(Prompt("What is your name?", false, false));
611     p.addPrompt(Prompt("Enter the names of your friends", true));
612 
613     // Fill up pipe with data for read end
614     File writeEnd = pipe.writeEnd();
615     writeEnd.writeln(""); // Purposefully do empty (for name)
616     writeEnd.writeln("Tristan Brice Velloza Kildaire"); // Now actually fill it in (for name)
617     writeEnd.writeln("Thomas");
618     writeEnd.writeln("Risima");
619     writeEnd.writeln("");
620     writeEnd.flush();
621 
622     // Perform the prompt and get the
623     // answers back out
624     Prompt[] ans = p.prompt();
625 
626     writeln(ans);
627 
628     string nameVal;
629     assert(ans[0].getValue(nameVal));
630     assert(nameVal == "Tristan Brice Velloza Kildaire");
631 
632     string[] friends;
633     assert(ans[1].getValues(friends));
634     assert(friends == ["Thomas", "Risima"]);
635 }