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 }