1<?php
2namespace Aws\Api;
3
4use Aws;
5
6/**
7 * Validates a schema against a hash of input.
8 */
9class Validator
10{
11    private $path = [];
12    private $errors = [];
13    private $constraints = [];
14
15    private static $defaultConstraints = [
16        'required' => true,
17        'min'      => true,
18        'max'      => false,
19        'pattern'  => false
20    ];
21
22    /**
23     * @param array $constraints Associative array of constraints to enforce.
24     *                           Accepts the following keys: "required", "min",
25     *                           "max", and "pattern". If a key is not
26     *                           provided, the constraint will assume false.
27     */
28    public function __construct(array $constraints = null)
29    {
30        static $assumedFalseValues = [
31            'required' => false,
32            'min'      => false,
33            'max'      => false,
34            'pattern'  => false
35        ];
36        $this->constraints = empty($constraints)
37            ? self::$defaultConstraints
38            : $constraints + $assumedFalseValues;
39    }
40
41    /**
42     * Validates the given input against the schema.
43     *
44     * @param string $name  Operation name
45     * @param Shape  $shape Shape to validate
46     * @param array  $input Input to validate
47     *
48     * @throws \InvalidArgumentException if the input is invalid.
49     */
50    public function validate($name, Shape $shape, array $input)
51    {
52        $this->dispatch($shape, $input);
53
54        if ($this->errors) {
55            $message = sprintf(
56                "Found %d error%s while validating the input provided for the "
57                    . "%s operation:\n%s",
58                count($this->errors),
59                count($this->errors) > 1 ? 's' : '',
60                $name,
61                implode("\n", $this->errors)
62            );
63            $this->errors = [];
64
65            throw new \InvalidArgumentException($message);
66        }
67    }
68
69    private function dispatch(Shape $shape, $value)
70    {
71        static $methods = [
72            'structure' => 'check_structure',
73            'list'      => 'check_list',
74            'map'       => 'check_map',
75            'blob'      => 'check_blob',
76            'boolean'   => 'check_boolean',
77            'integer'   => 'check_numeric',
78            'float'     => 'check_numeric',
79            'long'      => 'check_numeric',
80            'string'    => 'check_string',
81            'byte'      => 'check_string',
82            'char'      => 'check_string'
83        ];
84
85        $type = $shape->getType();
86        if (isset($methods[$type])) {
87            $this->{$methods[$type]}($shape, $value);
88        }
89    }
90
91    private function check_structure(StructureShape $shape, $value)
92    {
93        if (!$this->checkAssociativeArray($value)) {
94            return;
95        }
96
97        if ($this->constraints['required'] && $shape['required']) {
98            foreach ($shape['required'] as $req) {
99                if (!isset($value[$req])) {
100                    $this->path[] = $req;
101                    $this->addError('is missing and is a required parameter');
102                    array_pop($this->path);
103                }
104            }
105        }
106
107        foreach ($value as $name => $v) {
108            if ($shape->hasMember($name)) {
109                $this->path[] = $name;
110                $this->dispatch(
111                    $shape->getMember($name),
112                    isset($value[$name]) ? $value[$name] : null
113                );
114                array_pop($this->path);
115            }
116        }
117    }
118
119    private function check_list(ListShape $shape, $value)
120    {
121        if (!is_array($value)) {
122            $this->addError('must be an array. Found '
123                . Aws\describe_type($value));
124            return;
125        }
126
127        $this->validateRange($shape, count($value), "list element count");
128
129        $items = $shape->getMember();
130        foreach ($value as $index => $v) {
131            $this->path[] = $index;
132            $this->dispatch($items, $v);
133            array_pop($this->path);
134        }
135    }
136
137    private function check_map(MapShape $shape, $value)
138    {
139        if (!$this->checkAssociativeArray($value)) {
140            return;
141        }
142
143        $values = $shape->getValue();
144        foreach ($value as $key => $v) {
145            $this->path[] = $key;
146            $this->dispatch($values, $v);
147            array_pop($this->path);
148        }
149    }
150
151    private function check_blob(Shape $shape, $value)
152    {
153        static $valid = [
154            'string' => true,
155            'integer' => true,
156            'double' => true,
157            'resource' => true
158        ];
159
160        $type = gettype($value);
161        if (!isset($valid[$type])) {
162            if ($type != 'object' || !method_exists($value, '__toString')) {
163                $this->addError('must be an fopen resource, a '
164                    . 'GuzzleHttp\Stream\StreamInterface object, or something '
165                    . 'that can be cast to a string. Found '
166                    . Aws\describe_type($value));
167            }
168        }
169    }
170
171    private function check_numeric(Shape $shape, $value)
172    {
173        if (!is_numeric($value)) {
174            $this->addError('must be numeric. Found '
175                . Aws\describe_type($value));
176            return;
177        }
178
179        $this->validateRange($shape, $value, "numeric value");
180    }
181
182    private function check_boolean(Shape $shape, $value)
183    {
184        if (!is_bool($value)) {
185            $this->addError('must be a boolean. Found '
186                . Aws\describe_type($value));
187        }
188    }
189
190    private function check_string(Shape $shape, $value)
191    {
192        if ($shape['jsonvalue']) {
193            if (!self::canJsonEncode($value)) {
194                $this->addError('must be a value encodable with \'json_encode\'.'
195                    . ' Found ' . Aws\describe_type($value));
196            }
197            return;
198        }
199
200        if (!$this->checkCanString($value)) {
201            $this->addError('must be a string or an object that implements '
202                . '__toString(). Found ' . Aws\describe_type($value));
203            return;
204        }
205
206        $this->validateRange($shape, strlen($value), "string length");
207
208        if ($this->constraints['pattern']) {
209            $pattern = $shape['pattern'];
210            if ($pattern && !preg_match("/$pattern/", $value)) {
211                $this->addError("Pattern /$pattern/ failed to match '$value'");
212            }
213        }
214    }
215
216    private function validateRange(Shape $shape, $length, $descriptor)
217    {
218        if ($this->constraints['min']) {
219            $min = $shape['min'];
220            if ($min && $length < $min) {
221                $this->addError("expected $descriptor to be >= $min, but "
222                    . "found $descriptor of $length");
223            }
224        }
225
226        if ($this->constraints['max']) {
227            $max = $shape['max'];
228            if ($max && $length > $max) {
229                $this->addError("expected $descriptor to be <= $max, but "
230                    . "found $descriptor of $length");
231            }
232        }
233    }
234
235    private function checkCanString($value)
236    {
237        static $valid = [
238            'string'  => true,
239            'integer' => true,
240            'double'  => true,
241            'NULL'    => true,
242        ];
243
244        $type = gettype($value);
245
246        return isset($valid[$type]) ||
247            ($type == 'object' && method_exists($value, '__toString'));
248    }
249
250    private function checkAssociativeArray($value)
251    {
252        $isAssociative = false;
253
254        if (is_array($value)) {
255            $expectedIndex = 0;
256            $key = key($value);
257
258            do {
259                $isAssociative = $key !== $expectedIndex++;
260                next($value);
261                $key = key($value);
262            } while (!$isAssociative && null !== $key);
263        }
264
265        if (!$isAssociative) {
266            $this->addError('must be an associative array. Found '
267                . Aws\describe_type($value));
268            return false;
269        }
270
271        return true;
272    }
273
274    private function addError($message)
275    {
276        $this->errors[] =
277            implode('', array_map(function ($s) { return "[{$s}]"; }, $this->path))
278            . ' '
279            . $message;
280    }
281
282    private function canJsonEncode($data)
283    {
284        return !is_resource($data);
285    }
286}
287