1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-24 06:20:56 +01:00

Make chart function argument parsing modular/flexible with 900 pages of error messages

Summary:
Depends on D20444. Ref T13279. Instead of ad-hoc parsing and messages, formalize chart function arguments.

Also, add a whole lot of extra type checking.

Test Plan: Built and charted various functions with various valid and invalid argument lists, got sensible-seeming errors and results.

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20445
This commit is contained in:
epriestley 2019-04-17 10:45:27 -07:00
parent 7b8ac020b5
commit edaf17f3fe
10 changed files with 368 additions and 76 deletions

View file

@ -2652,6 +2652,8 @@ phutil_register_library_map(array(
'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php', 'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php',
'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php', 'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php',
'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php', 'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php',
'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php',
'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php',
'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php', 'PhabricatorChatLogApplication' => 'applications/chatlog/application/PhabricatorChatLogApplication.php',
'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php', 'PhabricatorChatLogChannel' => 'applications/chatlog/storage/PhabricatorChatLogChannel.php',
'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php',
@ -8629,6 +8631,8 @@ phutil_register_library_map(array(
'PhabricatorChartAxis' => 'Phobject', 'PhabricatorChartAxis' => 'Phobject',
'PhabricatorChartDataQuery' => 'Phobject', 'PhabricatorChartDataQuery' => 'Phobject',
'PhabricatorChartFunction' => 'Phobject', 'PhabricatorChartFunction' => 'Phobject',
'PhabricatorChartFunctionArgument' => 'Phobject',
'PhabricatorChartFunctionArgumentParser' => 'Phobject',
'PhabricatorChatLogApplication' => 'PhabricatorApplication', 'PhabricatorChatLogApplication' => 'PhabricatorApplication',
'PhabricatorChatLogChannel' => array( 'PhabricatorChatLogChannel' => array(
'PhabricatorChatLogDAO', 'PhabricatorChatLogDAO',

View file

@ -7,6 +7,8 @@ abstract class PhabricatorChartFunction
private $yAxis; private $yAxis;
private $limit; private $limit;
private $argumentParser;
final public function getFunctionKey() { final public function getFunctionKey() {
return $this->getPhobjectClassConstant('FUNCTIONKEY', 32); return $this->getPhobjectClassConstant('FUNCTIONKEY', 32);
} }
@ -19,11 +21,51 @@ abstract class PhabricatorChartFunction
} }
final public function setArguments(array $arguments) { final public function setArguments(array $arguments) {
$this->newArguments($arguments); $parser = $this->getArgumentParser();
$parser->setRawArguments($arguments);
$specs = $this->newArguments();
if (!is_array($specs)) {
throw new Exception(
pht(
'Expected "newArguments()" in class "%s" to return a list of '.
'argument specifications, got %s.',
get_class($this),
phutil_describe_type($specs)));
}
assert_instances_of($specs, 'PhabricatorChartFunctionArgument');
foreach ($specs as $spec) {
$parser->addArgument($spec);
}
$parser->setHaveAllArguments(true);
$parser->parseArguments();
return $this; return $this;
} }
abstract protected function newArguments(array $arguments); abstract protected function newArguments();
final protected function newArgument() {
return new PhabricatorChartFunctionArgument();
}
final protected function getArgument($key) {
return $this->getArgumentParser()->getArgumentValue($key);
}
final protected function getArgumentParser() {
if (!$this->argumentParser) {
$parser = id(new PhabricatorChartFunctionArgumentParser())
->setFunction($this);
$this->argumentParser = $parser;
}
return $this->argumentParser;
}
public function loadData() { public function loadData() {
return; return;

View file

@ -0,0 +1,129 @@
<?php
final class PhabricatorChartFunctionArgument
extends Phobject {
private $name;
private $type;
public function setName($name) {
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setType($type) {
$types = array(
'fact-key' => true,
'function' => true,
'number' => true,
);
if (!isset($types[$type])) {
throw new Exception(
pht(
'Chart function argument type "%s" is unknown. Valid types '.
'are: %s.',
$type,
implode(', ', array_keys($types))));
}
$this->type = $type;
return $this;
}
public function getType() {
return $this->type;
}
public function newValue($value) {
switch ($this->getType()) {
case 'fact-key':
if (!is_string($value)) {
throw new Exception(
pht(
'Value for "fact-key" argument must be a string, got %s.',
phutil_describe_type($value)));
}
$facts = PhabricatorFact::getAllFacts();
$fact = idx($facts, $value);
if (!$fact) {
throw new Exception(
pht(
'Fact key "%s" is not a known fact key.',
$value));
}
return $fact;
case 'function':
// If this is already a function object, just return it.
if ($value instanceof PhabricatorChartFunction) {
return $value;
}
if (!is_array($value)) {
throw new Exception(
pht(
'Value for "function" argument must be a function definition, '.
'formatted as a list, like: [fn, arg1, arg, ...]. Actual value '.
'is %s.',
phutil_describe_type($value)));
}
if (!phutil_is_natural_list($value)) {
throw new Exception(
pht(
'Value for "function" argument must be a natural list, not '.
'a dictionary. Actual value is "%s".',
phutil_describe_type($value)));
}
if (!$value) {
throw new Exception(
pht(
'Value for "function" argument must be a list with a function '.
'name; got an empty list.'));
}
$function_name = array_shift($value);
if (!is_string($function_name)) {
throw new Exception(
pht(
'Value for "function" argument must be a natural list '.
'beginning with a function name as a string. The first list '.
'item has the wrong type, %s.',
phutil_describe_type($function_name)));
}
$functions = PhabricatorChartFunction::getAllFunctions();
if (!isset($functions[$function_name])) {
throw new Exception(
pht(
'Function "%s" is unknown. Valid functions are: %s',
$function_name,
implode(', ', array_keys($functions))));
}
return id(clone $functions[$function_name])
->setArguments($value);
case 'number':
if (!is_float($value) && !is_int($value)) {
throw new Exception(
pht(
'Value for "number" argument must be an integer or double, '.
'got %s.',
phutil_describe_type($value)));
}
return $value;
}
throw new PhutilMethodNotImplementedException();
}
}

View file

@ -0,0 +1,154 @@
<?php
final class PhabricatorChartFunctionArgumentParser
extends Phobject {
private $function;
private $rawArguments;
private $unconsumedArguments;
private $haveAllArguments = false;
private $unparsedArguments;
private $argumentMap = array();
private $argumentPosition = 0;
private $argumentValues = array();
public function setFunction(PhabricatorChartFunction $function) {
$this->function = $function;
return $this;
}
public function getFunction() {
return $this->function;
}
public function setRawArguments(array $arguments) {
$this->rawArguments = $arguments;
$this->unconsumedArguments = $arguments;
}
public function addArgument(PhabricatorChartFunctionArgument $spec) {
$name = $spec->getName();
if (!strlen($name)) {
throw new Exception(
pht(
'Chart function "%s" emitted an argument specification with no '.
'argument name. Argument specifications must have unique names.',
$this->getFunctionArgumentSignature()));
}
$type = $spec->getType();
if (!strlen($type)) {
throw new Exception(
pht(
'Chart function "%s" emitted an argument specification ("%s") with '.
'no type. Each argument specification must have a valid type.',
$name));
}
if (isset($this->argumentMap[$name])) {
throw new Exception(
pht(
'Chart function "%s" emitted multiple argument specifications '.
'with the same name ("%s"). Each argument specification must have '.
'a unique name.',
$this->getFunctionArgumentSignature(),
$name));
}
$this->argumentMap[$name] = $spec;
$this->unparsedArguments[] = $spec;
return $this;
}
public function parseArgument(
PhabricatorChartFunctionArgument $spec) {
$this->addArgument($spec);
return $this->parseArguments();
}
public function setHaveAllArguments($have_all) {
$this->haveAllArguments = $have_all;
return $this;
}
public function parseArguments() {
$have_count = count($this->rawArguments);
$want_count = count($this->argumentMap);
if ($this->haveAllArguments) {
if ($want_count !== $have_count) {
throw new Exception(
pht(
'Function "%s" expects %s argument(s), but %s argument(s) were '.
'provided.',
$this->getFunctionArgumentSignature(),
$want_count,
$have_count));
}
}
while ($this->unparsedArguments) {
$argument = array_shift($this->unparsedArguments);
$name = $argument->getName();
if (!$this->unconsumedArguments) {
throw new Exception(
pht(
'Function "%s" expects at least %s argument(s), but only %s '.
'argument(s) were provided.',
$this->getFunctionArgumentSignature(),
$want_count,
$have_count));
}
$raw_argument = array_shift($this->unconsumedArguments);
$this->argumentPosition++;
try {
$value = $argument->newValue($raw_argument);
} catch (Exception $ex) {
throw new Exception(
pht(
'Argument "%s" (in position "%s") to function "%s" is '.
'invalid: %s',
$name,
$this->argumentPosition,
$this->getFunctionArgumentSignature(),
$ex->getMessage()));
}
$this->argumentValues[$name] = $value;
}
}
public function getArgumentValue($key) {
if (!array_key_exists($key, $this->argumentValues)) {
throw new Exception(
pht(
'Function "%s" is requesting an argument ("%s") that it did '.
'not define.',
$this->getFunctionArgumentSignature(),
$key));
}
return $this->argumentValues[$key];
}
private function getFunctionArgumentSignature() {
$argument_list = array();
foreach ($this->argumentMap as $key => $spec) {
$argument_list[] = $key;
}
if (!$this->haveAllArguments) {
$argument_list[] = '...';
}
return sprintf(
'%s(%s)',
$this->getFunction()->getFunctionKey(),
implode(', ', $argument_list));
}
}

View file

@ -7,36 +7,26 @@ final class PhabricatorConstantChartFunction
private $value; private $value;
protected function newArguments(array $arguments) { protected function newArguments() {
if (count($arguments) !== 1) { return array(
throw new Exception( $this->newArgument()
pht( ->setName('n')
'Chart function "constant(...)" expects one argument, got %s. '. ->setType('number'),
'Pass a constant.', );
count($arguments)));
}
if (!is_int($arguments[0])) {
throw new Exception(
pht(
'First argument for "fact(...)" is invalid: expected int, '.
'got %s.',
phutil_describe_type($arguments[0])));
}
$this->value = $arguments[0];
} }
public function getDatapoints(PhabricatorChartDataQuery $query) { public function getDatapoints(PhabricatorChartDataQuery $query) {
$x_min = $query->getMinimumValue(); $x_min = $query->getMinimumValue();
$x_max = $query->getMaximumValue(); $x_max = $query->getMaximumValue();
$value = $this->getArgument('n');
$points = array(); $points = array();
$steps = $this->newLinearSteps($x_min, $x_max, 2); $steps = $this->newLinearSteps($x_min, $x_max, 2);
foreach ($steps as $step) { foreach ($steps as $step) {
$points[] = array( $points[] = array(
'x' => $step, 'x' => $step,
'y' => $this->value, 'y' => $value,
); );
} }

View file

@ -5,40 +5,21 @@ final class PhabricatorFactChartFunction
const FUNCTIONKEY = 'fact'; const FUNCTIONKEY = 'fact';
private $factKey;
private $fact; private $fact;
private $datapoints; private $datapoints;
protected function newArguments(array $arguments) { protected function newArguments() {
if (count($arguments) !== 1) { $key_argument = $this->newArgument()
throw new Exception( ->setName('fact-key')
pht( ->setType('fact-key');
'Chart function "fact(...)" expects one argument, got %s. '.
'Pass the key for a fact.',
count($arguments)));
}
if (!is_string($arguments[0])) { $parser = $this->getArgumentParser();
throw new Exception( $parser->parseArgument($key_argument);
pht(
'First argument for "fact(...)" is invalid: expected string, '.
'got %s.',
phutil_describe_type($arguments[0])));
}
$facts = PhabricatorFact::getAllFacts(); $fact = $this->getArgument('fact-key');
$fact = idx($facts, $arguments[0]);
if (!$fact) {
throw new Exception(
pht(
'Argument to "fact(...)" is invalid: "%s" is not a known fact '.
'key.',
$arguments[0]));
}
$this->factKey = $arguments[0];
$this->fact = $fact; $this->fact = $fact;
return $fact->getFunctionArguments();
} }
public function loadData() { public function loadData() {

View file

@ -5,30 +5,20 @@ final class PhabricatorSinChartFunction
const FUNCTIONKEY = 'sin'; const FUNCTIONKEY = 'sin';
private $argument; protected function newArguments() {
return array(
protected function newArguments(array $arguments) { $this->newArgument()
if (count($arguments) !== 1) { ->setName('x')
throw new Exception( ->setType('function'),
pht( );
'Chart function "sin(..)" expects one argument, got %s.',
count($arguments)));
} }
$argument = $arguments[0]; protected function assignArguments(array $arguments) {
$this->argument = $arguments[0];
if (!($argument instanceof PhabricatorChartFunction)) {
throw new Exception(
pht(
'Argument to chart function should be a function, got %s.',
phutil_describe_type($argument)));
}
$this->argument = $argument;
} }
public function getDatapoints(PhabricatorChartDataQuery $query) { public function getDatapoints(PhabricatorChartDataQuery $query) {
$points = $this->argument->getDatapoints($query); $points = $this->getArgument('x')->getDatapoints($query);
foreach ($points as $key => $point) { foreach ($points as $key => $point) {
$points[$key]['y'] = sin(deg2rad($points[$key]['y'])); $points[$key]['y'] = sin(deg2rad($points[$key]['y']));

View file

@ -5,13 +5,8 @@ final class PhabricatorXChartFunction
const FUNCTIONKEY = 'x'; const FUNCTIONKEY = 'x';
protected function newArguments(array $arguments) { protected function newArguments() {
if (count($arguments) !== 0) { return array();
throw new Exception(
pht(
'Chart function "x()" expects zero arguments, got %s.',
count($arguments)));
}
} }
public function getDatapoints(PhabricatorChartDataQuery $query) { public function getDatapoints(PhabricatorChartDataQuery $query) {

View file

@ -23,6 +23,9 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
$x_function = id(new PhabricatorXChartFunction()) $x_function = id(new PhabricatorXChartFunction())
->setArguments(array()); ->setArguments(array());
$functions[] = id(new PhabricatorConstantChartFunction())
->setArguments(array(360));
$functions[] = id(new PhabricatorSinChartFunction()) $functions[] = id(new PhabricatorSinChartFunction())
->setArguments(array($x_function)); ->setArguments(array($x_function));

View file

@ -37,4 +37,8 @@ abstract class PhabricatorFact extends Phobject {
abstract protected function newTemplateDatapoint(); abstract protected function newTemplateDatapoint();
final public function getFunctionArguments() {
return array();
}
} }