Created
March 2, 2022 23:41
-
-
Save olleharstedt/e18004ad82e57e18047690596781a05a to your computer and use it in GitHub Desktop.
One universal dry-run mock-spy AST evaluator to rule them all
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Tmp6; | |
use InvalidArgumentException; | |
interface Node | |
{ | |
} | |
class If_ implements Node | |
{ | |
public $if; | |
public $then; | |
public function __construct(Node|Callable $if) | |
{ | |
$this->if = $if; | |
} | |
public function setThen(Node $if) | |
{ | |
$this->then = $if; | |
} | |
} | |
class FileExists implements Node | |
{ | |
public $file; | |
public function __construct($file) | |
{ | |
$this->file = $file; | |
} | |
} | |
function fileExists(string $file) | |
{ | |
return new FileExists($file); | |
} | |
class FileGetContents implements Node | |
{ | |
public $file; | |
public function __construct($file) | |
{ | |
$this->file = $file; | |
} | |
} | |
function fileGetContents(string $file) | |
{ | |
return new FileGetContents($file); | |
} | |
class Set implements Node | |
{ | |
public $var; | |
public $val; | |
public function __construct(mixed &$var, mixed $val) | |
{ | |
$this->var = &$var; | |
$this->val = $val; | |
} | |
} | |
function set(&$var, $val) | |
{ | |
return new Set($var, $val); | |
} | |
interface EvaluatorInterface | |
{ | |
public function evalNode(Node $node); | |
} | |
class NodeEvaluator implements EvaluatorInterface | |
{ | |
/** | |
* @return mixed | |
*/ | |
public function evalNode(Node $node) | |
{ | |
$className = get_class_name($node::class); | |
switch ($className) { | |
case "If_": | |
if ($this->evalNode($node->if)) { | |
$this->evalNode($node->then); | |
} elseif (!empty($node->else)) { | |
$this->evalNode($node->else); | |
} | |
break; | |
case "FileExists": | |
return file_exists($node->file); | |
case "FileGetContents": | |
return file_get_contents($node->file); | |
case "Set": | |
if ($node->val instanceof Node) { | |
$node->var = $this->evalNode($node->val); | |
} elseif (gettype($node->val) === 'string') { | |
$node->var = $node->val; | |
} else { | |
throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val)); | |
} | |
break; | |
default: | |
throw new InvalidArgumentException('Unsupported node type: ' . $className); | |
} | |
} | |
} | |
class DryRunEvaluator implements EvaluatorInterface | |
{ | |
public $log = []; | |
public $returnValues = []; | |
public function __construct(array $returnValues) | |
{ | |
$this->returnValues = $returnValues; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function evalNode($node) | |
{ | |
$className = get_class_name($node::class); | |
switch ($className) { | |
case "If_": | |
$this->log[] = "Evaluating if"; | |
if ($this->evalNode($node->if)) { | |
$this->log[] = "Evaluating then"; | |
$this->evalNode($node->then); | |
} elseif (!empty($node->else)) { | |
$this->log[] = "Evaluating else"; | |
$this->evalNode($node->else); | |
} | |
break; | |
case "FileExists": | |
$this->log[] = "File exists: arg1 = " . $node->file; | |
return array_pop($this->returnValues); | |
case "FileGetContents": | |
$this->log[] = "File get contents: arg1 = " . $node->file; | |
return array_pop($this->returnValues); | |
case "Set": | |
if ($node->val instanceof Node) { | |
$val = $this->evalNode($node->val); | |
$node->var = $val; | |
$this->log[] = "Set var to: " . $val; | |
} elseif (gettype($node->val) === 'string') { | |
$node->var = $node->val; | |
$this->log[] = "Set var to: " . $node->val; | |
} else { | |
throw new InvalidArgumentException('Unknown type of val in set: ' . gettype($node->val)); | |
} | |
break; | |
default: | |
throw new InvalidArgumentException('Unsupported node type: ' . $className); | |
} | |
} | |
} | |
class St | |
{ | |
public $queue = []; | |
public $ev; | |
public function __construct(EvaluatorInterface $ev) | |
{ | |
$this->ev = $ev; | |
} | |
public function if($if) | |
{ | |
$this->queue[] = new If_($if); | |
return $this; | |
} | |
public function then($if) | |
{ | |
$i = \count($this->queue) - 1; | |
if ($this->queue[$i] instanceof If_) { | |
$this->queue[$i]->setThen($if); | |
} else { | |
throw new InvalidArgumentException('then must come after if'); | |
} | |
return $this; | |
} | |
public function run($ev) | |
{ | |
foreach ($this->queue as $node) { | |
$ev->evalNode($node); | |
} | |
} | |
public function set($var, $value) | |
{ | |
return $this; | |
} | |
public function __invoke() | |
{ | |
foreach ($this->queue as $node) { | |
$this->ev->evalNode($node); | |
} | |
} | |
} | |
function get_class_name($classname) | |
{ | |
if ($pos = strrpos($classname, '\\')) | |
return substr($classname, $pos + 1); | |
return $pos; | |
} | |
/* | |
Use-case from: https://blog.ploeh.dk/2016/09/26/decoupling-decisions-from-effects/ | |
public static string GetUpperText(string path) | |
{ | |
if (!File.Exists(path)) return "DEFAULT"; | |
var text = File.ReadAllText(path); | |
return text.ToUpperInvariant(); | |
} | |
*/ | |
// Using an expression builder | |
function getUpperText(string $file, St $st) | |
{ | |
$result = 'DEFAULT'; | |
$st | |
->if(fileExists($file)) | |
->then(set($result, fileGetContents($file))) | |
(); | |
return strtoupper($result); | |
} | |
// Using a mock | |
function getUpperTextMock(string $file, IO $io) | |
{ | |
$result = 'DEFAULT'; | |
if ($io->fileExists($file)) { | |
$result = $io->fileGetContents($file); | |
} | |
return strtoupper($result); | |
} | |
// Instead of mocking return types, set the return values | |
$returnValues = array_reverse( | |
[ | |
true, | |
'Some example file content, bla bla bla' | |
] | |
); | |
$ev = new DryRunEvaluator($returnValues); | |
$st = new St($ev); | |
$text = getUpperText('moo.txt', $st); | |
// Output: string(38) "SOME EXAMPLE FILE CONTENT, BLA BLA BLA" | |
var_dump($text); | |
// Instead of a spy, you can inspect the dry-run log | |
var_dump($ev->log); | |
/* Output: | |
array(5) { | |
[0] => | |
string(13) "Evaluating if" | |
[1] => | |
string(27) "File exists: arg1 = moo.txt" | |
[2] => | |
string(15) "Evaluating then" | |
[3] => | |
string(33) "File get contents: arg1 = moo.txt" | |
[4] => | |
string(50) "Set var to: Some example file content, bla bla bla" | |
} | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment