Skip to content

Commit

Permalink
[#169] Support Filters Being Bound to Different Stages of Processing
Browse files Browse the repository at this point in the history
Makes it possible for a filter definition to include a new `stage` key
that can be set to `before_validation`, `after_validation`,
`request_wire`, or `response_wire`; which binds the filter to fire off
either before validation (the default), after validation, before being
written to the wire in a request, or after being read from the wire from
a response, respectively.

Still needs tests.
  • Loading branch information
Guy Elsmore-Paddock committed Aug 21, 2019
1 parent d14ba44 commit 8576ba1
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 65 deletions.
34 changes: 24 additions & 10 deletions src/Handler/ValidatedDescriptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use GuzzleHttp\Command\CommandInterface;
use GuzzleHttp\Command\Exception\CommandException;
use GuzzleHttp\Command\Guzzle\DescriptionInterface;
use GuzzleHttp\Command\Guzzle\Parameter;
use GuzzleHttp\Command\Guzzle\SchemaValidator;

/**
Expand Down Expand Up @@ -43,17 +44,30 @@ public function __invoke(callable $handler)
foreach ($operation->getParams() as $name => $schema) {
$value = $command[$name];

if ($value) {
$value = $schema->filter($value);
}
$preValidationValue = $schema->filter(
$value,
Parameter::FILTER_STAGE_BEFORE_VALIDATION
);

if (!$this->validator->validate($schema, $preValidationValue)) {
$errors =
array_merge($errors, $this->validator->getErrors());
} else {
$postValidationValue = $schema->filter(
$preValidationValue,
Parameter::FILTER_STAGE_AFTER_VALIDATION
);

if (! $this->validator->validate($schema, $value)) {
$errors = array_merge($errors, $this->validator->getErrors());
} elseif ($value !== $command[$name]) {
// Update the config value if it changed and no validation errors were encountered.
// This happen when the user extending an operation
// See https://github.com/guzzle/guzzle-services/issues/145
$command[$name] = $value;
if ($postValidationValue !== $command[$name]) {
// Update the parameter value if it has changed and no
// validation errors were encountered. This ensures the
// parameter has a value even when the user is extending
// an operation.
//
// See:
// https://github.com/guzzle/guzzle-services/issues/145
$command[$name] = $postValidationValue;
}
}
}

Expand Down
237 changes: 208 additions & 29 deletions src/Parameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@
*/
class Parameter implements ToArrayInterface
{
/**
* The name of the filter stage that happens before a parameter is
* validated, for filtering raw data (e.g. clean-up before validation).
*/
const FILTER_STAGE_BEFORE_VALIDATION = 'before_validation';

/**
* The name of the filter stage that happens immediately after a parameter
* has been validated but before it is evaluated by location handlers to be
* written out on the wire.
*/
const FILTER_STAGE_AFTER_VALIDATION = 'after_validation';

/**
* The name of the filter stage that happens right before a validated value
* is being written out "on the wire" (e.g. for adjusting the structure or
* format of the data before sending it to the server).
*/
const FILTER_STAGE_REQUEST_WIRE = 'request_wire';

/**
* The name of the filter stage that happens right after a value has been
* read out of a response "on the wire" (e.g. for adjusting the structure or
* format of the data after receiving it back from the server).
*/
const FILTER_STAGE_RESPONSE_WIRE = 'response_wire';

/**
* A list of all allowed filter stages.
*/
const FILTER_STAGES = [
self::FILTER_STAGE_BEFORE_VALIDATION,
self::FILTER_STAGE_AFTER_VALIDATION,
self::FILTER_STAGE_REQUEST_WIRE,
self::FILTER_STAGE_RESPONSE_WIRE
];

private $originalData;

/** @var string $name */
Expand Down Expand Up @@ -117,14 +154,23 @@ class Parameter implements ToArrayInterface
* full class path to a static method or an array of complex filter
* information. You can specify static methods of classes using the full
* namespace class name followed by '::' (e.g. Foo\Bar::baz). Some
* filters require arguments in order to properly filter a value. For
* complex filters, use a hash containing a 'method' key pointing to a
* static method, and an 'args' key containing an array of positional
* arguments to pass to the method. Arguments can contain keywords that
* are replaced when filtering a value: '@value' is replaced with the
* value being validated, '@api' is replaced with the Parameter object.
* filters require arguments in order to properly filter a value.
*
* - properties: When the type is an object, you can specify nested parameters
* For complex filters, use a hash containing a 'method' key pointing to a
* static method, an 'args' key containing an array of positional
* arguments to pass to the method, and an optional 'stage' key. Arguments
* can contain keywords that are replaced when filtering a value: '@value'
* is replaced with the value being validated, '@api' is replaced with the
* Parameter object, and '@stage' is replaced with the current filter
* stage (if any was provided).
*
* The optional 'stage' key can be provided to control when the filter is
* invoked. The key can indicate that a filter should only be invoked
* 'before_validation', 'after_validation', when being written out to the
* 'request_wire' or being read from the 'response_wire'.
*
* - properties: When the type is an object, you can specify nested
* parameters
*
* - additionalProperties: (array) This attribute defines a schema for all
* properties that are not explicitly defined in an object type
Expand Down Expand Up @@ -250,14 +296,30 @@ public function getValue($value)
* parameter.
*
* @param mixed $value Value to filter
* @param string $stage An optional specifier of what filter stage to
* invoke. If null, then all filters are invoked no matter what stage
* they apply to. Otherwise, only filters for the specified stage are
* invoked.
*
* @return mixed Returns the filtered value
* @throws \RuntimeException when trying to format when no service
* description is available.
*/
public function filter($value)
{
// Formats are applied exclusively and supersed filters
* @throws \InvalidArgumentException if an invalid validation stage is
* provided.
*/
public function filter($value, $stage = null)
{
if (($stage !== null) && !in_array($stage, self::FILTER_STAGES)) {
throw new \InvalidArgumentException(
sprintf(
'$stage must be one of [%s], but was given "%s"',
implode(', ', self::FILTER_STAGES),
$stage
)
);
}

// Formats are applied exclusively and supercede filters
if ($this->format) {
if (!$this->serviceDescription) {
throw new \RuntimeException('No service description was set so '
Expand All @@ -273,24 +335,7 @@ public function filter($value)

// Apply filters to the value
if ($this->filters) {
foreach ($this->filters as $filter) {
if (is_array($filter)) {
// Convert complex filters that hold value place holders
foreach ($filter['args'] as &$data) {
if ($data == '@value') {
$data = $value;
} elseif ($data == '@api') {
$data = $this;
}
}
$value = call_user_func_array(
$filter['method'],
$filter['args']
);
} else {
$value = call_user_func($filter, $value);
}
}
$value = $this->invokeCustomFilters($value, $stage);
}

return $value;
Expand Down Expand Up @@ -628,6 +673,17 @@ private function addFilter($filter)
'A [method] value must be specified for each complex filter'
);
}

if (isset($filter['stage'])
&& !in_array($filter['stage'], self::FILTER_STAGES)) {
throw new \InvalidArgumentException(
sprintf(
'[stage] value must be one of [%s], but was given "%s"',
implode(', ', self::FILTER_STAGES),
$filter['stage']
)
);
}
}

if (!$this->filters) {
Expand All @@ -652,4 +708,127 @@ public function has($var)
}
return isset($this->{$var}) && !empty($this->{$var});
}

/**
* Filters the given data using filter methods specified in the config.
*
* If $stage is provided, only filters that apply to the provided filter
* stage will be invoked. To preserve legacy behavior, filters that do not
* specify a stage are implicitly invoked only in the pre-validation stage.
*
* @param mixed $value The value to filter.
* @param string $stage An optional specifier of what filter stage to
* invoke. If null, then all filters are invoked no matter what stage
* they apply to. Otherwise, only filters for the specified stage are
* invoked.
*
* @return mixed The filtered value.
*/
private function invokeCustomFilters($value, $stage) {
$filteredValue = $value;

foreach ($this->filters as $filter) {
if (is_array($filter)) {
$filteredValue =
$this->invokeComplexFilter($filter, $value, $stage);
} else {
$filteredValue =
$this->invokeSimpleFilter($filter, $value, $stage);
}
}

return $filteredValue;
}

/**
* Invokes a filter that uses value substitution and/or should only be
* invoked for a particular filter stage.
*
* If $stage is provided, and the filter specifies a stage, it is not
* invoked unless $stage matches the stage the filter indicates it applies
* to. If the filter is not invoked, $value is returned exactly as it was
* provided to this method.
*
* To preserve legacy behavior, if the filter does not specify a stage, it
* is implicitly invoked only in the pre-validation stage.
*
* @param array $filter Information about the filter to invoke.
* @param mixed $value The value to filter.
* @param string $stage An optional specifier of what filter stage to
* invoke. If null, then the filter is invoked no matter what stage it
* indicates it applies to. Otherwise, the filter is only invoked if it
* matches the specified stage.
*
* @return mixed The filtered value.
*/
private function invokeComplexFilter(array $filter, $value, $stage) {
if (isset($filter['stage'])) {
$filterStage = $filter['stage'];
} else {
$filterStage = self::FILTER_STAGE_AFTER_VALIDATION;
}

if (($stage === null) || ($filterStage == $stage)) {
// Convert complex filters that hold value place holders
$filterArgs =
$this->expandFilterArgs($filter['args'], $value, $stage);

$filteredValue =
call_user_func_array($filter['method'], $filterArgs);
} else {
$filteredValue = $value;
}

return $filteredValue;
}

/**
* Replaces any placeholders in filter arguments with values from the
* current context.
*
* @param array $filterArgs The array of arguments to pass to the filter
* function. Some of the elements of this array are expected to be
* placeholders that will be replaced by this function.
*
* @return array The array of arguments, with all placeholders replaced.
*/
function expandFilterArgs(array $filterArgs, $value, $stage) {
$replacements = [
'@value' => $value,
'@api' => $this,
'@stage' => $stage,
];

foreach ($filterArgs as &$argValue) {
if (isset($replacements[$argValue])) {
$argValue = $replacements[$argValue];
}
}

return $filterArgs;
}

/**
* Invokes a filter only provides a function or method name to invoke,
* without additional parameters.
*
* If $stage is provided, the filter is not invoked unless we are in the
* pre-validation stage, to preserve legacy behavior.
*
* @param array $filter Information about the filter to invoke.
* @param mixed $value The value to filter.
* @param string $stage An optional specifier of what filter stage to
* invoke. If null, then the filter is invoked no matter what.
* Otherwise, the filter is only invoked if the value is
* FILTER_STAGE_AFTER_VALIDATION.
*
* @return mixed The filtered value.
*/
private function invokeSimpleFilter($filter, $value, $stage) {
if ($stage === self::FILTER_STAGE_AFTER_VALIDATION) {
return $value;
} else {
return call_user_func($filter, $value);
}
}
}
4 changes: 2 additions & 2 deletions src/RequestLocation/AbstractLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ protected function prepareValue($value, Parameter $param)
{
return is_array($value)
? $this->resolveRecursively($value, $param)
: $param->filter($value);
: $param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
}

/**
Expand Down Expand Up @@ -96,6 +96,6 @@ protected function resolveRecursively(array $value, Parameter $param)
}
}

return $param->filter($value);
return $param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);
}
}
15 changes: 10 additions & 5 deletions src/RequestLocation/BodyLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,20 @@ public function visit(
RequestInterface $request,
Parameter $param
) {
$oldValue = $request->getBody()->getContents();
$existingResponse = $request->getBody()->getContents();

$value = $command[$param->getName()];
$value = $param->getName() . '=' . $param->filter($value);
$filteredValue =
$param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);

if ($oldValue !== '') {
$value = $oldValue . '&' . $value;
$valueForResponse = sprintf('%s=%s', $param->getName(), $filteredValue);

if ($existingResponse == '') {
$response = $valueForResponse;
} else {
$response = $existingResponse . '&' . $valueForResponse;
}

return $request->withBody(Psr7\stream_for($value));
return $request->withBody(Psr7\stream_for($response));
}
}
11 changes: 9 additions & 2 deletions src/RequestLocation/HeaderLocation.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public function visit(
Parameter $param
) {
$value = $command[$param->getName()];
$filteredValue =
$param->filter($value, Parameter::FILTER_STAGE_REQUEST_WIRE);

return $request->withHeader($param->getWireName(), $param->filter($value));
return $request->withHeader($param->getWireName(), $filteredValue);
}

/**
Expand All @@ -57,7 +59,12 @@ public function after(
if ($additional && ($additional->getLocation() === $this->locationName)) {
foreach ($command->toArray() as $key => $value) {
if (!$operation->hasParam($key)) {
$request = $request->withHeader($key, $additional->filter($value));
$filteredValue = $additional->filter(
$value,
Parameter::FILTER_STAGE_REQUEST_WIRE
);

$request = $request->withHeader($key, $filteredValue);
}
}
}
Expand Down
Loading

0 comments on commit 8576ba1

Please sign in to comment.