diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -612,6 +612,7 @@ 'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php', 'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php', 'PhutilArray' => 'utils/PhutilArray.php', + 'PhutilArrayCheck' => 'utils/PhutilArrayCheck.php', 'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php', 'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php', 'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php', @@ -1619,6 +1620,7 @@ 'ArrayAccess', 'Iterator', ), + 'PhutilArrayCheck' => 'Phobject', 'PhutilArrayTestCase' => 'PhutilTestCase', 'PhutilArrayWithDefaultValue' => 'PhutilArray', 'PhutilAsanaFuture' => 'FutureProxy', diff --git a/src/utils/PhutilArrayCheck.php b/src/utils/PhutilArrayCheck.php new file mode 100644 --- /dev/null +++ b/src/utils/PhutilArrayCheck.php @@ -0,0 +1,251 @@ +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)); + } + +}