Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "conflictsWith" functionality to Filterer library #90

Merged
merged 7 commits into from Nov 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/FilterOptions.php
@@ -0,0 +1,26 @@
<?php

namespace TraderInteractive;

final class FilterOptions
{
/**
* @var string
*/
const DEFAULT_VALUE = 'default';

/**
* @var string
*/
const CUSTOM_ERROR = 'error';

/**
* @var string
*/
const IS_REQUIRED = 'required';

/**
* @var string
*/
const CONFLICTS_WITH = 'conflictsWith';
}
93 changes: 67 additions & 26 deletions src/Filterer.php
Expand Up @@ -46,9 +46,9 @@ final class Filterer implements FiltererInterface
* @var array
*/
const DEFAULT_OPTIONS = [
'allowUnknowns' => false,
'defaultRequired' => false,
'responseType' => self::RESPONSE_TYPE_ARRAY,
FiltererOptions::ALLOW_UNKNOWNS => false,
FiltererOptions::DEFAULT_REQUIRED => false,
FiltererOptions::RESPONSE_TYPE => self::RESPONSE_TYPE_ARRAY,
];

/**
Expand Down Expand Up @@ -123,12 +123,15 @@ public function execute(array $input) : FilterResponse
$leftOverInput = array_diff_key($input, $this->specification);

$errors = [];
$conflicts = [];
foreach ($inputToFilter as $field => $input) {
$filters = $this->specification[$field];
self::assertFiltersIsAnArray($filters, $field);
$customError = self::validateCustomError($filters, $field);
unset($filters['required']);//doesn't matter if required since we have this one
unset($filters['default']);//doesn't matter if there is a default since we have a value
unset($filters[FilterOptions::IS_REQUIRED]);//doesn't matter if required since we have this one
unset($filters[FilterOptions::DEFAULT_VALUE]);//doesn't matter if there is a default since we have a value
$conflicts = self::extractConflicts($filters, $field, $conflicts);

foreach ($filters as $filter) {
self::assertFilterIsNotArray($filter, $field);

Expand Down Expand Up @@ -156,15 +159,16 @@ public function execute(array $input) : FilterResponse
foreach ($leftOverSpec as $field => $filters) {
self::assertFiltersIsAnArray($filters, $field);
$required = self::getRequired($filters, $this->defaultRequired, $field);
if (array_key_exists('default', $filters)) {
$inputToFilter[$field] = $filters['default'];
if (array_key_exists(FilterOptions::DEFAULT_VALUE, $filters)) {
$inputToFilter[$field] = $filters[FilterOptions::DEFAULT_VALUE];
continue;
}

$errors = self::handleRequiredFields($required, $field, $errors);
}

$errors = self::handleAllowUnknowns($this->allowUnknowns, $leftOverInput, $errors);
$errors = self::handleConflicts($inputToFilter, $conflicts, $errors);

return new FilterResponse($inputToFilter, $errors, $leftOverInput);
}
Expand All @@ -179,6 +183,40 @@ public function getAliases() : array
return $this->filterAliases ?? self::$registeredFilterAliases;
}

private static function extractConflicts(array &$filters, string $field, array $conflicts) : array
{
$conflictsWith = $filters[FilterOptions::CONFLICTS_WITH] ?? null;
unset($filters[FilterOptions::CONFLICTS_WITH]);
if ($conflictsWith === null) {
return $conflicts;
}

if (!is_array($conflictsWith)) {
$conflictsWith = [$conflictsWith];
}

$conflicts[$field] = $conflictsWith;

return $conflicts;
}

private static function handleConflicts(array $inputToFilter, array $conflicts, array $errors)
{
foreach (array_keys($inputToFilter) as $field) {
if (!array_key_exists($field, $conflicts)) {
continue;
}

foreach ($conflicts[$field] as $conflictsWith) {
if (array_key_exists($conflictsWith, $inputToFilter)) {
$errors[] = "Field '{$field}' cannot be given if field '{$conflictsWith}' is present.";
}
}
}

return $errors;
}

/**
* @return array
*
Expand Down Expand Up @@ -219,8 +257,8 @@ public function withSpecification(array $specification) : FiltererInterface
private function getOptions() : array
{
return [
'defaultRequired' => $this->defaultRequired,
'allowUnknowns' => $this->allowUnknowns,
FiltererOptions::DEFAULT_REQUIRED => $this->defaultRequired,
FiltererOptions::ALLOW_UNKNOWNS => $this->allowUnknowns,
];
}

Expand Down Expand Up @@ -296,7 +334,7 @@ private function getOptions() : array
public static function filter(array $specification, array $input, array $options = [])
{
$options += self::DEFAULT_OPTIONS;
$responseType = $options['responseType'];
$responseType = $options[FiltererOptions::RESPONSE_TYPE];

$filterer = new Filterer($specification, $options);
$filterResponse = $filterer->execute($input);
Expand Down Expand Up @@ -484,9 +522,11 @@ private static function handleRequiredFields(bool $required, string $field, arra

private static function getRequired($filters, $defaultRequired, $field) : bool
{
$required = isset($filters['required']) ? $filters['required'] : $defaultRequired;
$required = $filters[FilterOptions::IS_REQUIRED] ?? $defaultRequired;
if ($required !== false && $required !== true) {
throw new InvalidArgumentException("'required' for field '{$field}' was not a bool");
throw new InvalidArgumentException(
sprintf("'%s' for field '%s' was not a bool", FilterOptions::IS_REQUIRED, $field)
);
}

return $required;
Expand All @@ -508,11 +548,8 @@ private static function handleCustomError(
) : array {
$error = $customError;
if ($error === null) {
$error = sprintf(
"Field '%s' with value '{value}' failed filtering, message '%s'",
$field,
$e->getMessage()
);
$errorFormat = "Field '%s' with value '{value}' failed filtering, message '%s'";
$error = sprintf($errorFormat, $field, $e->getMessage());
}

$errors[$field] = str_replace('{value}', trim(var_export($value, true), "'"), $error);
Expand Down Expand Up @@ -547,33 +584,37 @@ private static function assertFilterIsNotArray($filter, string $field)
private static function validateCustomError(array &$filters, string $field)
{
$customError = null;
if (array_key_exists('error', $filters)) {
$customError = $filters['error'];
if (array_key_exists(FilterOptions::CUSTOM_ERROR, $filters)) {
$customError = $filters[FilterOptions::CUSTOM_ERROR];
if (!is_string($customError) || trim($customError) === '') {
throw new InvalidArgumentException("error for field '{$field}' was not a non-empty string");
throw new InvalidArgumentException(
sprintf("%s for field '%s' was not a non-empty string", FilterOptions::CUSTOM_ERROR, $field)
);
}

unset($filters['error']);//unset so its not used as a filter
unset($filters[FilterOptions::CUSTOM_ERROR]);//unset so its not used as a filter
}

return $customError;
}

private static function getAllowUnknowns(array $options) : bool
{
$allowUnknowns = $options['allowUnknowns'];
$allowUnknowns = $options[FiltererOptions::ALLOW_UNKNOWNS];
if ($allowUnknowns !== false && $allowUnknowns !== true) {
throw new InvalidArgumentException("'allowUnknowns' option was not a bool");
throw new InvalidArgumentException(sprintf("'%s' option was not a bool", FiltererOptions::ALLOW_UNKNOWNS));
}

return $allowUnknowns;
}

private static function getDefaultRequired(array $options) : bool
{
$defaultRequired = $options['defaultRequired'];
$defaultRequired = $options[FiltererOptions::DEFAULT_REQUIRED];
if ($defaultRequired !== false && $defaultRequired !== true) {
throw new InvalidArgumentException("'defaultRequired' option was not a bool");
throw new InvalidArgumentException(
sprintf("'%s' option was not a bool", FiltererOptions::DEFAULT_REQUIRED)
);
}

return $defaultRequired;
Expand Down Expand Up @@ -602,6 +643,6 @@ private static function generateFilterResponse(string $responseType, FilterRespo
];
}

throw new InvalidArgumentException("'responseType' was not a recognized value");
throw new InvalidArgumentException(sprintf("'%s' was not a recognized value", FiltererOptions::RESPONSE_TYPE));
}
}
21 changes: 21 additions & 0 deletions src/FiltererOptions.php
@@ -0,0 +1,21 @@
<?php

namespace TraderInteractive;

final class FiltererOptions
{
/**
* @var string
*/
const ALLOW_UNKNOWNS = 'allowUnknowns';

/**
* @var string
*/
const DEFAULT_REQUIRED = 'defaultRequired';

/**
* @var string
*/
const RESPONSE_TYPE = 'responseType';
}
58 changes: 58 additions & 0 deletions tests/FiltererTest.php
Expand Up @@ -238,6 +238,64 @@ public function provideValidFilterData() : array
'options' => [],
'result' => [true, ['field' => 'a string with newlines and extra spaces'], null, []],
],
'conflicts with single' => [
'spec' => [
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
'fieldTwo' => [['string']],
'fieldThree' => [FilterOptions::CONFLICTS_WITH => 'fieldOne', ['string']],
],
'input' => [
'fieldOne' => 'abc',
'fieldTwo' => '123',
'fieldThree' => 'xyz',
],
'options' => [],
'result' => [
false,
null,
"Field 'fieldOne' cannot be given if field 'fieldThree' is present.\n"
. "Field 'fieldThree' cannot be given if field 'fieldOne' is present.",
[],
],
],
'conflicts with multiple' => [
'spec' => [
'fieldOne' => [FilterOptions::CONFLICTS_WITH => ['fieldTwo', 'fieldThree'], ['string']],
'fieldTwo' => [['string']],
'fieldThree' => [['string']],
],
'input' => [
'fieldOne' => 'abc',
'fieldTwo' => '123',
],
'options' => [],
'result' => [
false,
null,
"Field 'fieldOne' cannot be given if field 'fieldTwo' is present.",
[],
],
],
'conflicts with not present' => [
'spec' => [
'fieldOne' => [FilterOptions::CONFLICTS_WITH => 'fieldThree', ['string']],
'fieldTwo' => [['string']],
],
'input' => [
'fieldOne' => 'abc',
'fieldTwo' => '123',
],
'options' => [],
'result' => [
true,
[
'fieldOne' => 'abc',
'fieldTwo' => '123',
],
null,
[],
],
],
];
}

Expand Down