summaryrefslogtreecommitdiff
path: root/server/vendor/php-opencloud/common/src/Common/Api/Parameter.php
blob: 97330a4dc5731eab7f1edd043d86ae010a08ef23 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<?php

namespace OpenCloud\Common\Api;

use OpenCloud\Common\HydratorStrategyTrait;

/**
 * Represents an individual request parameter in a RESTful operation. A parameter can take on many forms:
 * in a URL path, in a URL query, in a JSON body, and in a HTTP header. It is worth documenting brifly each
 * variety of parameter:
 *
 * * Header parameters are those which populate a HTTP header in a request. Header parameters can have
 *   aliases; for example, a user-facing name of "Foo" can be sent over the wire as "X-Foo_Bar", as defined
 *   by ``sentAs``. Prefixes can also be used.
 *
 * * Query parameters are those which populate a URL query parameter. The value is therefore usually
 *   confined to a string.
 *
 * * JSON parameters are those which populate a JSON request body. These are the most complex variety
 *   of Parameter, since there are so many different ways a JSON document can be constructed. The SDK
 *   supports deep-nesting according to a XPath syntax; for more information, see {@see \OpenCloud\Common\JsonPath}.
 *   Nested object and array properties are also supported since JSON is a recursive data type. What
 *   this means is that a Parameter can have an assortment of child Parameters, one for each object
 *   property or array element.
 *
 * * Raw parameters are those which populate a non-JSON request body. This is typically used for
 *   uploading payloads (such as Swift object data) to a remote API.
 *
 * * Path parameters are those which populate a URL path. They are serialized according to URL
 *   placeholders.
 *
 * @package OpenCloud\Common\Api
 */
class Parameter
{
    use HydratorStrategyTrait;

    const DEFAULT_LOCATION = 'json';

    /**
     * The human-friendly name of the parameter. This is what the user will input.
     *
     * @var string
     */
    private $name;

    /**
     * The alias for this parameter. Although the user will always interact with the human-friendly $name property,
     * the $sentAs is what's used over the wire.
     *
     * @var string
     */
    private $sentAs;

    /**
     * For array parameters (for example, an array of security group names when creating a server), each array element
     * will need to adhere to a common schema. For the aforementioned example, each element will need to be a string.
     * For more complicated parameters, you might be validated an array of complicated objects.
     *
     * @var Parameter
     */
    private $itemSchema;

    /**
     * For object parameters, each property will need to adhere to a specific schema. For every property in the
     * object, it has its own schema - meaning that this property is a hash of name/schema pairs.
     *
     * The *only* exception to this rule is for metadata parameters, which are arbitrary key/value pairs. Since it does
     * not make sense to have a schema for each metadata key, a common schema is use for every one. So instead of this
     * property being a hash of schemas, it is a single Parameter object instead. This single Parameter schema will
     * then be applied to each metadata key provided.
     *
     * @var []Parameter|Parameter
     */
    private $properties;

    /**
     * The value's PHP type which this parameter represents; either "string", "bool", "object", "array", "NULL".
     *
     * @var string
     */
    private $type;

    /**
     * Indicates whether this parameter requires a value from the user.
     *
     * @var bool
     */
    private $required;

    /**
     * The location in the HTTP request where this parameter will populate; either "header", "url", "query", "raw" or
     * "json".
     *
     * @var string
     */
    private $location;

    /**
     * Relevant to "json" location parameters only. This property allows for deep nesting through the use of
     * {@see OpenCloud\Common\JsonPath}.
     *
     * @var string
     */
    private $path;

    /**
     * Allows for the prefixing of parameter names.
     *
     * @var string
     */
    private $prefix;

    /**
     * The enum values for which this param is restricted.
     *
     * @var array
     */
    private $enum;

    /**
     * @param array $data
     */
    public function __construct(array $data)
    {
        $this->hydrate($data);

        $this->required = (bool)$this->required;

        $this->stockLocation($data);
        $this->stockItemSchema($data);
        $this->stockProperties($data);
    }

    private function stockLocation(array $data)
    {
        $this->location = isset($data['location']) ? $data['location'] : self::DEFAULT_LOCATION;

        if (!AbstractParams::isSupportedLocation($this->location)) {
            throw new \RuntimeException(sprintf("%s is not a permitted location", $this->location));
        }
    }

    private function stockItemSchema(array $data)
    {
        if (isset($data['items'])) {
            $this->itemSchema = new Parameter($data['items']);
        }
    }

    private function stockProperties(array $data)
    {
        if (isset($data['properties'])) {
            if (stripos($this->name, 'metadata') !== false) {
                $this->properties = new Parameter($data['properties']);
            } else {
                foreach ($data['properties'] as $name => $property) {
                    $this->properties[$name] = new Parameter($property + ['name' => $name]);
                }
            }
        }
    }

    /**
     * Retrieve the name that will be used over the wire.
     *
     * @return string
     */
    public function getName()
    {
        return $this->sentAs ?: $this->name;
    }

    /**
     * Indicates whether the user must provide a value for this parameter.
     *
     * @return bool
     */
    public function isRequired()
    {
        return $this->required === true;
    }

    /**
     * Validates a given user value and checks whether it passes basic sanity checking, such as types.
     *
     * @param $userValues The value provided by the user
     *
     * @return bool       TRUE if the validation passes
     * @throws \Exception If validation fails
     */
    public function validate($userValues)
    {
        $this->validateEnums($userValues);
        $this->validateType($userValues);

        if ($this->isArray()) {
            $this->validateArray($userValues);
        } elseif ($this->isObject()) {
            $this->validateObject($userValues);
        }

        return true;
    }

    private function validateEnums($userValues)
    {
        if (!empty($this->enum) && $this->type == 'string' && !in_array($userValues, $this->enum)) {
            throw new \Exception(sprintf(
                'The only permitted values are %s. You provided %s', implode(', ', $this->enum), print_r($userValues, true)
            ));
        }
    }

    private function validateType($userValues)
    {
        if (!$this->hasCorrectType($userValues)) {
            throw new \Exception(sprintf(
                'The key provided "%s" has the wrong value type. You provided %s (%s) but was expecting %s',
                $this->name, print_r($userValues, true), gettype($userValues), $this->type
            ));
        }
    }

    private function validateArray($userValues)
    {
        foreach ($userValues as $userValue) {
            $this->itemSchema->validate($userValue);
        }
    }

    private function validateObject($userValues)
    {
        foreach ($userValues as $key => $userValue) {
            $property = $this->getNestedProperty($key);
            $property->validate($userValue);
        }
    }

    /**
     * Internal method which retrieves a nested property for object parameters.
     *
     * @param $key The name of the child parameter
     *
     * @returns Parameter
     * @throws \Exception
     */
    private function getNestedProperty($key)
    {
        if (stripos($this->name, 'metadata') !== false && $this->properties instanceof Parameter) {
            return $this->properties;
        } elseif (isset($this->properties[$key])) {
            return $this->properties[$key];
        } else {
            throw new \Exception(sprintf('The key provided "%s" is not defined', $key));
        }
    }

    /**
     * Internal method which indicates whether the user value is of the same type as the one expected
     * by this parameter.
     *
     * @param $userValue The value being checked
     *
     * @return bool
     */
    private function hasCorrectType($userValue)
    {
        // Helper fn to see whether an array is associative (i.e. a JSON object)
        $isAssociative = function ($value) {
            return is_array($value) && array_keys($value) !== range(0, count($value) - 1);
        };

        // For params defined as objects, we'll let the user get away with
        // passing in an associative array - since it's effectively a hash
        if ($this->type == 'object' && $isAssociative($userValue)) {
            return true;
        }

        if (class_exists($this->type) || interface_exists($this->type)) {
            return is_a($userValue, $this->type);
        }

        if (!$this->type) {
            return true;
        }

        return gettype($userValue) == $this->type;
    }

    /**
     * Indicates whether this parameter represents an array type
     *
     * @return bool
     */
    public function isArray()
    {
        return $this->type == 'array' && $this->itemSchema instanceof Parameter;
    }

    /**
     * Indicates whether this parameter represents an object type
     *
     * @return bool
     */
    public function isObject()
    {
        return $this->type == 'object' && !empty($this->properties);
    }

    public function getLocation()
    {
        return $this->location;
    }

    /**
     * Verifies whether the given location matches the parameter's location.
     *
     * @param $value
     *
     * @return bool
     */
    public function hasLocation($value)
    {
        return $this->location == $value;
    }

    /**
     * Retrieves the parameter's path.
     *
     * @return string|null
     */
    public function getPath()
    {
        return $this->path;
    }

    /**
     * Retrieves the common schema that an array parameter applies to all its child elements.
     *
     * @return Parameter
     */
    public function getItemSchema()
    {
        return $this->itemSchema;
    }

    /**
     * Sets the name of the parameter to a new value
     *
     * @param string $name
     */
    public function setName($name)
    {
        $this->name = $name;
    }

    /**
     * Retrieves the child parameter for an object parameter.
     *
     * @param string $name The name of the child property
     *
     * @return null|Parameter
     */
    public function getProperty($name)
    {
        if ($this->properties instanceof Parameter) {
            $this->properties->setName($name);
            return $this->properties;
        }

        return isset($this->properties[$name]) ? $this->properties[$name] : null;
    }

    /**
     * Retrieves the prefix for a parameter, if any.
     *
     * @return string|null
     */
    public function getPrefix()
    {
        return $this->prefix;
    }

    public function getPrefixedName()
    {
        return $this->prefix . $this->getName();
    }
}