source: trunk/common/array_schema.php @ 852

Revision 852, 14.7 KB checked in by cdleonard@…, 4 years ago (diff)

svn:eol-style native in trunk. Also svn:executable on all scripts

  • Property svn:eol-style set to native
Line 
1<?php
2
3require_once(IA_ROOT_DIR.'common/common.php');
4
5// Get an array element.
6// Path can be a string, int or array of string or ints.
7//
8// The $data array is navigated step by step; if it at any step on the way
9// there is no non-null key then the default value is returned.
10//
11// Null values in $data are treated as non-existant, so
12// array_get(array('ala' => null), 'ala', 'bala') will return 'bala'.
13// This function uses isset rather than array_key_exists.
14function array_get($data, $path, $default = null)
15{
16    if (!is_array($path)) {
17        if (is_string($path) || is_int($path)) {
18            // Optimize for single step.
19            if (!is_array($data)) {
20                return $default;
21            }
22            if (isset($data[$path])) {
23                return $data[$path];
24            } else {
25                return $default;
26            }
27        } else {
28            log_error("Invalid $path argument");
29        }
30    }
31    while (count($path) > 0) {
32        if (!is_set($path[0])) {
33            log_error("Path must an array with integer keys");
34        }
35        if (!is_string($path[0]) && !is_int($path[0])) {
36            log_error("Invalid array path step");
37        }
38        if (!is_array($data)) {
39            return $default;
40        }
41        if (isset($data[$path[0]])) {
42            $data = $data[array_shift($path)];
43        } else {
44            return $default;
45        }
46    }
47    if (isset($data)) {
48        return $data;
49    } else {
50        return $default;
51    }
52}
53
54// Get the schema for an array schema. Sadly this is implemented using
55// 'type' => 'any' and a callback. How ironic.
56function array_schema_get_schema()
57{
58    return array(
59        'type' => 'any',
60        'callback' => '_array_schema_schema_validate_callback',
61    );
62}
63
64// Callback for array schema schema validation.
65// That is, this function validates an array schema.
66// FIXME: Make a node type for typed_struct?
67function _array_schema_schema_validate_callback($data, $schema)
68{
69    static $fields = null;
70    if ($fields == null) {
71       $number_range = array(
72            'type' => 'struct',
73            'null' => 'true',
74            'fields' => array(
75                'min' => array('type' => 'number', 'null' => true),
76                'max' => array('type' => 'number', 'null' => true),
77                'min-ex' => array('type' => 'number', 'null' => true),
78                'max-ex' => array('type' => 'number', 'null' => true),
79            )
80        );
81        $fields = array(
82            'struct' => array('fields' => array_schema_get_schema()),
83            'sequence' => array('values' => array_schema_get_schema()),
84            'mapping' => array('values' => array_schema_get_schema()),
85            'string' => array(
86                'length' => array(
87                    // String length range min/max values have to be ints.
88                    'type' => 'struct',
89                    'null' => 'true',
90                    'fields' => array(
91                        'min' => array('type' => 'int', 'null' => true),
92                        'max' => array('type' => 'int', 'null' => true),
93                        'min-ex' => array('type' => 'int', 'null' => true),
94                        'max-ex' => array('type' => 'int', 'null' => true),
95                    )
96                ),
97                'pattern' => array(
98                    'type' => 'string',
99                    'null' => true,
100                ),
101                'enum' => array(
102                    'type' => 'sequence',
103                    'null' => 'true',
104                    'values' => array('type' => 'string'),
105                ),
106            ),
107            'int' => array('range' => $number_range),
108            'float' => array('range' => $number_range),
109            'number' => array('range' => $number_range),
110            'date' => array(
111                'range' => array(
112                    'type' => 'struct',
113                    'fields' => array(
114                        'min' => array('type' => 'date', 'null' => true),
115                        'max' => array('type' => 'date', 'null' => true),
116                        'min-ex' => array('type' => 'date', 'null' => true),
117                        'max-ex' => array('type' => 'date', 'null' => true),
118                    ),
119                ),
120            ),
121            'bool' => array(''),
122            'any' => array('')
123        );
124    }
125
126    if (!is_array($data)) {
127        return (array)_local_error("Schema must be an array.");
128    }
129    $type = array_get($data, 'type', 'string');
130    if (!is_string($type)) {
131        return (array)_local_error("Invalid schema node type $type.");
132    }
133    $type_schema_fields = array_get($fields, $type);
134    if (is_null($type_schema_fields)) {
135        return (array)_local_error("Unknown schema node type '$type'.");
136    }
137    return array_validate($data, array(
138        'type' => 'struct',
139        'fields' => $type_schema_fields,
140    ));
141}
142
143// Validate a data hash against a schema.
144// The schema is roughly inspire from kwalify, but there are significant
145// differences. Please see the tests for samples.
146// FIXME: This is incomplete
147function array_validate($data, $schema)
148{
149    // Default nullable is false, values are required by default.
150    // This is unlike SQL.
151    $null = array_get($schema, 'null', false);
152    if (is_null($data)) {
153        if (!$null) {
154            return array(_local_error("Required value missing."));
155        } else {
156            return array();
157        }
158    }
159
160    // Create a static hash mapping types to validation functions.
161    // Blazing fast.
162    static $validation_funcs = array(
163        'struct' => '_array_validate_struct',
164        'sequence' => '_array_validate_sequence',
165        'mapping' => '_array_validate_mapping',
166        'string' => '_array_validate_string',
167        'int' => '_array_validate_number',
168        'float' => '_array_validate_number',
169        'number' => '_array_validate_number',
170        'date' => '_array_validate_date',
171        'any' => '_array_validate_any',
172    );
173
174    // Default type is string.
175    $func = array_get($validation_funcs, $type = array_get($schema, 'type', 'string'));
176    if ($func !== null) {
177        $errors = $func($data, $schema);
178    } else {
179        // Invalid schema, crash.
180        log_error("Unknown type '$type'");
181    }
182
183    // Call the validation callback, but only if all other tests passed.
184    // This makes it a lot easier to write a validation callback function.
185    // Errors from the validaton function are merged with the declarative
186    // validation errors.
187    if ($errors == array()) {
188        $validation_function = array_get($schema, "callback", null);
189        if (is_callable($validation_function)) {
190            $errors = array_merge($errors, $validation_function($data, $schema));
191        }
192    }
193
194    return $errors;
195}
196
197// Structs are php arrays with different constraints for each field. They
198// are used among other things for database rows.
199function _array_validate_struct($data, $schema)
200{
201    if (!is_array($data)) {
202        return array(_local_error("Not a struct (is_array false)"));
203    }
204
205    $errors = array();
206    $struct_fields = $schema['fields'];
207
208    // Check defined fields first.
209    foreach ($struct_fields as $field_name => $field_schema) {
210        // Don't differentiate between null values and missing keys.
211        // Such a distinction doesn't exist in some languages and it can be
212        // very confusing.
213        $field_value = array_get($data, $field_name, null);
214
215        // Validate value and copy errors
216        // If $field_schema specifies null then it doesn't matter if
217        // $field_value doesn't exist, it will pass.
218        $field_errors = array_validate($field_value, $field_schema);
219        foreach ($field_errors as $field_error) {
220            array_unshift($field_error['path'], $field_name);
221            $errors[] = $field_error;
222        }
223    }
224
225    // By default allow unknown values, but if the struct is marked as
226    // sealed then report an error for extra fields.
227    //
228    // This will be rather tricky to extend for inherited constraints.
229    if (array_get($schema, 'sealed', false)) {
230        foreach ($data as $k => $v) {
231            // Throw up on undefined keys, if marked as sealed.
232            if (!array_get($struct_fields, $k)) {
233                $errors[] = array(
234                    'path' => array($k),
235                    'message' => 'Field undefined',
236                );
237            }
238        }
239    }
240
241    return $errors;
242}
243
244// Validate sequence types, returns $errors list.
245// Sequences are simple C-style arrays, indexed with continuous integer values
246// starting from 0.
247function _array_validate_sequence($data, $schema)
248{
249    // Check if it's at least an array.
250    if (!is_array($data)) {
251        return array(_local_error("Not a sequence (is_array false)."));
252    }
253
254    $value_schema = $schema['values'];
255    $errors = array();
256
257    // Check every value. There is no easy way to tell sequences from
258    // maps in php, so we check for consecutive integer indexes by hand.
259    $index = 0;
260    foreach ($data as $k => $v) {
261        if ($k != $index) {
262            $errors[] = _local_error("Not a sequence, array keys are not all consecutive integers.");
263        }
264        ++$index;
265        $value_errors = array_validate($v, $value_schema);
266        foreach ($value_errors as $value_error) {
267            array_unshift($value_error['path'], $k);
268            $errors[] = $value_error;
269        }
270    }
271
272    return $errors;
273}
274
275// Validate mapping types.
276// These are php arrays with constraints for values.
277// FIXME: It's not possible to place constraints on keys.
278// FIXME: How to properly report errors from key constraints?
279function _array_validate_mapping($data, $schema)
280{
281    // Check if it's at least an array.
282    if (!is_array($data)) {
283        return array(_local_error("Not a mapping (is_array false)."));
284    }
285
286    $value_schema = $schema['values'];
287
288    $errors = array();
289    // Check every key/value pair.
290    foreach ($data as $k => $v) {
291        $value_errors = array_validate($v, $value_schema);
292        foreach ($value_errors as $value_error) {
293            array_unshift($value_error['path'], $k);
294            $errors[] = $value_error;
295        }
296    }
297
298    return $errors;
299}
300
301// Check string nodes.
302// They can have length contraints similar to numeric ranges.
303// They can be enums and thus restricted to a fixed set of values.
304// They can also be forced to match a certain regex pattern.
305function _array_validate_string($data, $schema)
306{
307    if (!is_string($data)) {
308        return array(_local_error("Not a string."));
309    }
310
311    $errors = array();
312
313    // Check length
314    if (array_get($schema, 'length') !== null) {
315        $range = $schema['length'];
316        $len = strlen($data);
317
318        // Max value, inclusive
319        if (!is_null($max = array_get($range, 'max')) && $len > $max) {
320            $errors[] = _local_error("Length out of range, $len > $max.");
321        }
322        // Min value, inclusive
323        if (!is_null($min = array_get($range, 'min')) && $len < $min) {
324            $errors[] = _local_error("Length out of range, $len < $min.");
325        }
326        // Max value, exclusive
327        if (!is_null($maxex = array_get($range, 'max-ex')) && $len >= $maxex) {
328            $errors[] = _local_error("Length out of range, $len >= $maxex.");
329        }
330        // Min value, exclusive
331        if (!is_null($minex = array_get($range, 'min-ex')) && $len <= $minex) {
332            $errors[] = _local_error("Length out of range, $len <= $minex.");
333        }
334    }
335
336    // Check enum values.
337    // FIXME: flipping the array if too slow, better ideas?
338    if (array_get($schema, 'enum') !== null) {
339        if (!array_key_exists($data, array_flip($schema['enum']))) {
340            $errors[] = _local_error("Invalid enum value '$data'");
341        }
342    }
343
344    // Check string regular expression patterns.
345    if (array_get($schema, 'pattern') !== null) {
346        if (!preg_match($schema['pattern'], $data)) {
347            $errors[] = _local_error("Doesn't match pattern {$schema['pattern']}.");
348        }
349    }
350
351    return $errors;
352}
353
354// Validate numbers (ints or floats).
355// There are 3 types actually: ints, floats or "numbers", which means either.
356// $schema can have a 'range' field.
357function _array_validate_number($data, $schema)
358{
359    $type = array_get($schema, 'type', 'string');
360
361    // Check actual type.
362    if ($type == 'int') {
363        if (!is_int($data)) {
364            return array(_local_error("Not an integer."));
365        }
366    } elseif ($type == 'float') {
367        if (!is_float($data)) {
368            return array(_local_error("Not a float."));
369        }
370    } elseif ($type == 'number') {
371        if (!is_float($data) && !is_int($data)) {
372            return array(_local_error("Not a number, neither int not float."));
373        }
374    } else {
375        log_error("Invalid type '$type' for _array_validate_number.");
376    }
377
378    $errors = array();
379
380    // Check Ranges
381    if (!is_null($range = array_get($schema, 'range'))) {
382        // Max value, inclusive
383        if (!is_null($max = array_get($range, 'max')) && $data > $max) {
384            $errors[] = _local_error("Value out of range, $data > $max.");
385        }
386        // Min value, inclusive
387        if (!is_null($min = array_get($range, 'min')) && $data < $min) {
388            $errors[] = _local_error("Value out of range, $data < $min.");
389        }
390        // Max value, exclusive
391        if (!is_null($maxex = array_get($range, 'max-ex')) && $data >= $maxex) {
392            $errors[] = _local_error("Value out of range, $data >= $maxex.");
393        }
394        // Min value, exclusive
395        if (!is_null($minex = array_get($range, 'min-ex')) && $data <= $minex) {
396            $errors[] = _local_error("Value out of range, $data <= $minex.");
397        }
398    }
399
400    return $errors;
401}
402
403// Validate dates. Dates are represented as strings in mysql's format,
404// which is very similar with RFC 3339, without the T. In short, it's
405// YYYY-MM-DD HH:MM:SS. This is simple, readable and can even be ordered
406// using strcmp.
407//
408// FIXME: Support ranges. This is important.
409function _array_validate_date($data, $schema)
410{
411    if (!is_db_date($data)) {
412        return array(_local_error("Not a datetime value. Valid values are YYYY-MM-DD HH:MM:SS with the time part optional"));
413    }
414
415    // FIXME: Check date/time ranges.
416    if (array_get($schema, 'range') != null) {
417        log_error("Ranges not supported for dates yet");
418    }
419
420    return array();
421}
422
423// Validate booleans. There are no extra options for these.
424function _array_validate_bool($data, $schema)
425{
426    if (!is_bool($data)) {
427        return array(_local_error("Not a boolean."));
428    }
429
430    return array();
431}
432
433// Mock validation function for 'any' type.
434// This can be used to only rely only the validation callback.
435function _array_validate_any($data, $schema)
436{
437    return array();
438}
439
440// Make a tiny local validation error.
441function _local_error($message)
442{
443    return array('path' => array(), 'message' => $message);
444}
445
446?>
Note: See TracBrowser for help on using the repository browser.