mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-22 14:52:40 +01:00
Add a support class to simplify typechecking list-of-objects return values
Summary: With some frequency, code wants to assert that some "$o->m()" returns a list of objects of type X, possibly with unique values for some "getKey()"-style method result. Existing checks via `PhutilTypeMap` and `assert_instances_of()` aren't quite powerful enough to do this while producing an easily understandable error state. We want to know that the error arose from a call to "$o->m()" in particular. Test Plan: Currently used elsewhere, in Piledriver code. Differential Revision: https://secure.phabricator.com/D21293
This commit is contained in:
parent
c76cfc8c82
commit
a0c346bf63
2 changed files with 253 additions and 0 deletions
|
@ -612,6 +612,7 @@ phutil_register_library_map(array(
|
||||||
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
|
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
|
||||||
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
|
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
|
||||||
'PhutilArray' => 'utils/PhutilArray.php',
|
'PhutilArray' => 'utils/PhutilArray.php',
|
||||||
|
'PhutilArrayCheck' => 'utils/PhutilArrayCheck.php',
|
||||||
'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
|
'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
|
||||||
'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
|
'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
|
||||||
'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
|
'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
|
||||||
|
@ -1619,6 +1620,7 @@ phutil_register_library_map(array(
|
||||||
'ArrayAccess',
|
'ArrayAccess',
|
||||||
'Iterator',
|
'Iterator',
|
||||||
),
|
),
|
||||||
|
'PhutilArrayCheck' => 'Phobject',
|
||||||
'PhutilArrayTestCase' => 'PhutilTestCase',
|
'PhutilArrayTestCase' => 'PhutilTestCase',
|
||||||
'PhutilArrayWithDefaultValue' => 'PhutilArray',
|
'PhutilArrayWithDefaultValue' => 'PhutilArray',
|
||||||
'PhutilAsanaFuture' => 'FutureProxy',
|
'PhutilAsanaFuture' => 'FutureProxy',
|
||||||
|
|
251
src/utils/PhutilArrayCheck.php
Normal file
251
src/utils/PhutilArrayCheck.php
Normal file
|
@ -0,0 +1,251 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhutilArrayCheck
|
||||||
|
extends Phobject {
|
||||||
|
|
||||||
|
private $instancesOf;
|
||||||
|
private $uniqueMethod;
|
||||||
|
private $context;
|
||||||
|
|
||||||
|
private $object;
|
||||||
|
private $method;
|
||||||
|
|
||||||
|
public function setInstancesOf($instances_of) {
|
||||||
|
$this->instancesOf = $instances_of;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInstancesOf() {
|
||||||
|
return $this->instancesOf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUniqueMethod($unique_method) {
|
||||||
|
$this->uniqueMethod = $unique_method;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUniqueMethod() {
|
||||||
|
return $this->uniqueMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContext($object, $method) {
|
||||||
|
if (!is_object($object) && !is_string($object)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Expected an object or string for "object" context, got "%s".',
|
||||||
|
phutil_describe_type($object)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($method)) {
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Expected a string for "method" context, got "%s".',
|
||||||
|
phutil_describe_type($method)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$argv = func_get_args();
|
||||||
|
$argv = array_slice($argv, 2);
|
||||||
|
|
||||||
|
$this->context = array(
|
||||||
|
'object' => $object,
|
||||||
|
'method' => $method,
|
||||||
|
'argv' => $argv,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkValues($maps) {
|
||||||
|
foreach ($maps as $idx => $map) {
|
||||||
|
$maps[$idx] = $this->checkValue($map);
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = $this->getUniqueMethod();
|
||||||
|
if ($unique === null) {
|
||||||
|
$result = array();
|
||||||
|
|
||||||
|
foreach ($maps as $map) {
|
||||||
|
foreach ($map as $value) {
|
||||||
|
$result[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$items = array();
|
||||||
|
foreach ($maps as $idx => $map) {
|
||||||
|
foreach ($map as $key => $value) {
|
||||||
|
$items[$key][$idx] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($items as $key => $values) {
|
||||||
|
if (count($values) === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Unexpected return value from calls to "%s(...)". More than one '.
|
||||||
|
'object returned a value with unique key "%s". This key was '.
|
||||||
|
'returned by objects with indexes: %s.',
|
||||||
|
$unique,
|
||||||
|
$key,
|
||||||
|
implode(', ', array_keys($values))));
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($items as $key => $values) {
|
||||||
|
$result[$key] = head($values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function checkValue($items) {
|
||||||
|
if (!$this->context) {
|
||||||
|
throw new PhutilInvalidStateException('setContext');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_array($items)) {
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Expected value to be a list, got "%s".',
|
||||||
|
phutil_describe_type($items)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$instances_of = $this->getInstancesOf();
|
||||||
|
if ($instances_of !== null) {
|
||||||
|
foreach ($items as $idx => $item) {
|
||||||
|
if (!($item instanceof $instances_of)) {
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Expected value to be a list of objects which are instances of '.
|
||||||
|
'"%s", but item with index "%s" is "%s".',
|
||||||
|
$instances_of,
|
||||||
|
$idx,
|
||||||
|
phutil_describe_type($item)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = $this->getUniqueMethod();
|
||||||
|
if ($unique !== null) {
|
||||||
|
if ($instances_of === null) {
|
||||||
|
foreach ($items as $idx => $item) {
|
||||||
|
if (!is_object($item)) {
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Expected value to be a list of objects to support calling '.
|
||||||
|
'"%s" to generate unique keys, but item with index "%s" is '.
|
||||||
|
'"%s".',
|
||||||
|
$unique,
|
||||||
|
$idx,
|
||||||
|
phutil_describe_type($item)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = array();
|
||||||
|
|
||||||
|
foreach ($items as $idx => $item) {
|
||||||
|
$key = call_user_func(array($item, $unique));
|
||||||
|
|
||||||
|
if (!is_string($key) && !is_int($key)) {
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Expected method "%s->%s()" to return a string or integer for '.
|
||||||
|
'use as a unique key, got "%s" from object at index "%s".',
|
||||||
|
get_class($item),
|
||||||
|
$unique,
|
||||||
|
phutil_describe_type($key),
|
||||||
|
$idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
$key = phutil_string_cast($key);
|
||||||
|
|
||||||
|
$map[$key][$idx] = $item;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
foreach ($map as $key => $values) {
|
||||||
|
if (count($values) === 1) {
|
||||||
|
$result[$key] = head($values);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$classes = array();
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$classes[] = get_class($value);
|
||||||
|
}
|
||||||
|
$classes = array_fuse($classes);
|
||||||
|
|
||||||
|
if (count($classes) === 1) {
|
||||||
|
$class_display = head($classes);
|
||||||
|
} else {
|
||||||
|
$class_display = sprintf(
|
||||||
|
'[%s]',
|
||||||
|
implode(', ', $classes));
|
||||||
|
}
|
||||||
|
|
||||||
|
$index_display = array();
|
||||||
|
foreach ($values as $idx => $value) {
|
||||||
|
$index_display[] = pht(
|
||||||
|
'"%s" (%s)',
|
||||||
|
$idx,
|
||||||
|
get_class($value));
|
||||||
|
}
|
||||||
|
$index_display = implode(', ', $index_display);
|
||||||
|
|
||||||
|
$this->raiseValueException(
|
||||||
|
pht(
|
||||||
|
'Expected method "%s->%s()" to return a unique key, got "%s" '.
|
||||||
|
'from %s object(s) at indexes: %s.',
|
||||||
|
$class_display,
|
||||||
|
$unique,
|
||||||
|
$key,
|
||||||
|
phutil_count($values),
|
||||||
|
$index_display));
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function raiseValueException($message) {
|
||||||
|
$context = $this->context;
|
||||||
|
|
||||||
|
$object = $context['object'];
|
||||||
|
$method = $context['method'];
|
||||||
|
$argv = $context['argv'];
|
||||||
|
|
||||||
|
if (is_object($object)) {
|
||||||
|
$object_display = sprintf(
|
||||||
|
'%s->%s',
|
||||||
|
get_class($object),
|
||||||
|
$method);
|
||||||
|
} else {
|
||||||
|
$object_display = sprintf(
|
||||||
|
'%s::%s',
|
||||||
|
$object,
|
||||||
|
$method);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($argv)) {
|
||||||
|
$call_display = sprintf(
|
||||||
|
'%s(...)',
|
||||||
|
$object_display);
|
||||||
|
} else {
|
||||||
|
$call_display = sprintf(
|
||||||
|
'%s()',
|
||||||
|
$object_display);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unexpected return value from call to "%s": %s.',
|
||||||
|
$call_display,
|
||||||
|
$message));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue