Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.41% covered (warning)
57.41%
182 / 317
35.00% covered (danger)
35.00%
7 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZErrorFactory
57.41% covered (warning)
57.41%
182 / 317
35.00% covered (danger)
35.00%
7 / 20
1174.29
0.00% covered (danger)
0.00%
0 / 1
 getErrorDescriptors
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
5.04
 buildStructureValidationZError
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
 buildStructureValidationZErrorItem
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
 identifyNestedErrors
88.24% covered (warning)
88.24%
15 / 17
0.00% covered (danger)
0.00%
0 / 1
9.13
 identifyError
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
5.02
 flattenErrors
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
5
 groupErrors
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 errorMatchesDescriptor
80.00% covered (warning)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
10.80
 getDataType
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
7.04
 errorMatchesType
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 createZErrorList
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createValidationZError
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createAuthorizationZError
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 createKeyValueZError
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createArrayElementZError
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 createLabelClashZErrors
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 createZErrorInstance
32.71% covered (danger)
32.71%
35 / 107
0.00% covered (danger)
0.00%
0 / 1
661.98
 createTypedError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createUnknownValidationError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 wrapMessageInZError
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * WikiLambda interface for validation error formatting
4 *
5 * @file
6 * @copyright 2020– Abstract Wikipedia team; see AUTHORS.txt
7 * @license MIT
8 */
9
10namespace MediaWiki\Extension\WikiLambda;
11
12use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
13use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
14use MediaWiki\Extension\WikiLambda\Validation\SchemataUtils;
15use MediaWiki\Extension\WikiLambda\ZObjects\ZError;
16use MediaWiki\Extension\WikiLambda\ZObjects\ZKeyReference;
17use MediaWiki\Extension\WikiLambda\ZObjects\ZObject;
18use MediaWiki\Extension\WikiLambda\ZObjects\ZQuote;
19use MediaWiki\Extension\WikiLambda\ZObjects\ZReference;
20use MediaWiki\Extension\WikiLambda\ZObjects\ZString;
21use MediaWiki\Extension\WikiLambda\ZObjects\ZTypedError;
22use Opis\JsonSchema\ValidationError;
23
24class ZErrorFactory {
25
26    /**
27     * @var array
28     */
29    private static $errorDescriptors;
30
31    /**
32     * Read and parse the yaml file with the error type descriptors
33     *
34     * @return array
35     */
36    private static function getErrorDescriptors(): array {
37        if ( !self::$errorDescriptors ) {
38            try {
39                $descriptor = json_decode(
40                    SchemataUtils::readYamlAsSecretJson(
41                        SchemataUtils::joinPath(
42                            SchemataUtils::dataDirectory(),
43                            "errors",
44                            "error_types.yaml"
45                        )
46                    ), true
47                );
48                if (
49                    array_key_exists( 'patterns', $descriptor ) &&
50                    array_key_exists( 'keywords', $descriptor['patterns'] )
51                ) {
52                    self::$errorDescriptors = $descriptor['patterns']['keywords'];
53                } else {
54                    self::$errorDescriptors = [];
55                }
56            } catch ( \Exception $e ) {
57                self::$errorDescriptors = [];
58            }
59        }
60        return self::$errorDescriptors;
61    }
62
63    /**
64     * Root function to transform an array of Opis\JsonSchema\ValidationError objects
65     * to a root Z5/ZError object with all the nested errors identified by the
66     * built-in Z50/ZErrorTypes
67     *
68     * @param ValidationError[] $errors
69     * @return ZError|false
70     */
71    public static function buildStructureValidationZError( $errors ) {
72        $zerrors = [];
73
74        foreach ( $errors as $error ) {
75            $zerror = self::buildStructureValidationZErrorItem( $error );
76            if ( $zerror !== false ) {
77                $zerrors[] = $zerror;
78            }
79        }
80
81        if ( count( $zerrors ) === 0 ) {
82            return false;
83        } elseif ( count( $zerrors ) === 1 ) {
84            return self::createValidationZError( $zerrors[0] );
85        } else {
86            return self::createValidationZError( self::createZErrorList( $zerrors ) );
87        }
88    }
89
90    /**
91     * From the error structure built from opis/json-schema, identify the error types
92     * using the error descriptors and build a nested Z5/ZError with all that information
93     *
94     * @param ValidationError $err
95     * @return ZError|bool
96     */
97    private static function buildStructureValidationZErrorItem( $err ) {
98        $nestedErrors = [];
99        $flatErrors = array_unique( self::flattenErrors( $err ), SORT_REGULAR );
100
101        foreach ( $flatErrors as $flat ) {
102            self::groupErrors(
103                $nestedErrors,
104                $flat['dataPointer'],
105                [
106                    'data' => $flat['data'],
107                    'dataPointer' => $flat['dataPointer'],
108                    'keyword' => $flat['keyword'],
109                    'keywordArgs' => $flat['keywordArgs']
110                ]
111            );
112        }
113
114        // Return the Z5/ZError structure with identified Z50/ZErrorTypes
115        return self::identifyNestedErrors( $nestedErrors );
116    }
117
118    /**
119     * Recursive function that walks the nested structure of errors and:
120     * 1. when finding a terminal error, identifies it and builds a ZError by
121     * matching the error descriptors, and
122     * 2. when finding a non-terminal error, identifies it as a "key not wellformed"
123     * builtin error and continues walking the branch.
124     * It returns false if no errors could be identified or a root Z5/ZError if
125     * with the identified errors.
126     *
127     * @param array $nestedErrors
128     * @return ZError|bool Z5/ZError or false for no errors found
129     */
130    public static function identifyNestedErrors( $nestedErrors ) {
131        $zerrors = [];
132
133        foreach ( $nestedErrors as $index => $errors ) {
134            if ( $index === 'errors' ) {
135                // Leaf with n errors detected by the parser: we identify and keep
136                // those that are matched to our error descriptors
137                foreach ( $errors as $error ) {
138                    $errorType = self::identifyError( $error );
139                    if ( $errorType ) {
140                        $zerrors[] = self::createZErrorInstance( $errorType, $error );
141                    }
142                }
143            } else {
144                // Branch with errors in their leafs, form errors recursively
145                $zerror = self::identifyNestedErrors( $errors );
146                if ( $zerror !== false ) {
147                    if ( is_int( $index ) ) {
148                        $zerrors[] = self::createArrayElementZError( (string)$index, $zerror );
149                    } else {
150                        $zerrors[] = self::createKeyValueZError( $index, $zerror );
151                    }
152                }
153            }
154        }
155
156        // If we found a list of errors, return a single error Z509/Multiple errors
157        if ( count( $zerrors ) === 0 ) {
158            return false;
159        } elseif ( count( $zerrors ) === 1 ) {
160            return $zerrors[0];
161        }
162        return self::createZErrorList( $zerrors );
163    }
164
165    /**
166     * Given the information provided by the JsonSchema parser about the error
167     * (keyword, keywordArgs, data and dataPointer), it tries to match an error
168     * type from the error descriptors
169     *
170     * @param array $error
171     * @return string|bool
172     */
173    public static function identifyError( $error ) {
174        $errorDescriptors = self::getErrorDescriptors();
175        $keyword = $error['keyword'];
176
177        // If no descriptors for this keyword, ignore
178        if ( !array_key_exists( $keyword, $errorDescriptors ) ) {
179            return false;
180        }
181
182        foreach ( $errorDescriptors[ $keyword ] as $errorDescriptor ) {
183            // If no descriptor matched for this error, ignore
184            if ( !self::errorMatchesDescriptor( $error, $errorDescriptor ) ) {
185                continue;
186            }
187
188            // If a descriptor is found but the type doesn't match, ignore
189            if ( !self::errorMatchesType( $error ) ) {
190                continue;
191            }
192
193            // Error descriptor matched: return error type
194            return $errorDescriptor['errorType'];
195        }
196
197        // No descriptor was found
198        return false;
199    }
200
201    /**
202     * Given a nested structure of parsing errors found by json-schema,
203     * it returns a flat list of errors, with enough information to match
204     * them to error type descriptors
205     *
206     * @param ValidationError $err
207     * @return array
208     */
209    public static function flattenErrors( $err ) {
210        $leaves = [];
211
212        // If keyword is additionalProps, it can contain as subErrors the
213        // dataPoint of the failed additionalProp, but the information about
214        // the error is not in the child but in the parent.
215        // Before recursively flattening sub-errors, we need to account for
216        // this edge case.
217        if ( $err->keyword() === 'additionalProperties' ) {
218            foreach ( $err->subErrors() as $subErr ) {
219                $leaves[] = [
220                    'data' => $err->data(),
221                    'dataPointer' => $subErr->dataPointer(),
222                    'keyword' => $err->keyword(),
223                    'keywordArgs' => $subErr->keywordArgs()
224                ];
225            }
226            return $leaves;
227        }
228
229        // If keyword is terminal, return array with 1 element
230        $countSubErr = count( $err->subErrors() );
231        if ( $countSubErr === 0 ) {
232            $leaves[] = json_decode( json_encode(
233                [
234                    'data' => $err->data(),
235                    'dataPointer' => $err->dataPointer(),
236                    'keyword' => $err->keyword(),
237                    'keywordArgs' => $err->keywordArgs()
238                ]
239            ), true );
240            return $leaves;
241        }
242
243        // If keyword is not terminal, and subErrors contains an array of
244        // errors, return the array of leaves for each one of the suberrors.
245        // Non-terminal keywords are:
246        // anyOf, allOf, oneOf, contains, propertyNames
247        // patternProperties, and additionalProperties
248        foreach ( $err->subErrors() as $index => $serr ) {
249            $leaves = array_merge( $leaves, self::flattenErrors( $serr ) );
250        }
251        return $leaves;
252    }
253
254    /**
255     * @param array &$errors
256     * @param array $keys
257     * @param array $value
258     */
259    private static function groupErrors( &$errors, $keys, $value ) {
260        if ( count( $keys ) === 0 ) {
261            // If this is the last key, we assign $value by pushing it to an existing
262            // errors array, or initializing one with $value as its only item.
263            if ( array_key_exists( 'errors', $errors ) ) {
264                $errors['errors'][] = $value;
265            } else {
266                $errors['errors'] = [ $value ];
267            }
268        } else {
269            // If this is a middle key, we check if it's already set.
270            // If it exists, we recursively assign the value
271            if ( array_key_exists( $keys[0], $errors ) ) {
272                self::groupErrors( $errors[$keys[0]], array_slice( $keys, 1 ), $value );
273            } else {
274                $a = [ 'errors' => [ $value ] ];
275                foreach ( array_reverse( $keys ) as $key ) {
276                    $a = [ $key => $a ];
277                }
278                $errors[ $keys[0] ] = $a[ $keys[0] ];
279            }
280        }
281    }
282
283    /**
284     * @param array $error
285     * @param array $descriptor
286     * @return bool
287     */
288    public static function errorMatchesDescriptor( $error, $descriptor ): bool {
289        // 1. The descriptor keyword must be the same as the error keyword
290        if ( $error['keyword'] !== $descriptor['keyword'] ) {
291            return false;
292        }
293
294        // 2. Every item from the descriptor keywordArgs must be present in the error
295        foreach ( $descriptor['keywordArgs'] as $wantedArg => $wantedValue ) {
296            if ( !array_key_exists( $wantedArg, $error['keywordArgs'] ) ) {
297                return false;
298            }
299
300            if ( is_array( $wantedValue ) && !in_array( $error['keywordArgs'][ $wantedArg ], $wantedValue ) ) {
301                return false;
302            }
303
304            if ( is_string( $wantedValue ) && ( $error['keywordArgs'][ $wantedArg ] !== $wantedValue ) ) {
305                return false;
306            }
307        }
308
309        // 3. The descriptor dataPointer must match the tail of the error dataPointer
310        // 3.1. If the descriptor dataPointer is longer than the error dataPointer, not a match
311        if ( count( $descriptor['dataPointer'] ) > count( $error['dataPointer'] ) ) {
312            return false;
313        }
314
315        // 3.2 Get the tail of the error descriptor and check (with ===)
316        // that both arrays contain the same elements in the same order
317        if ( count( $descriptor['dataPointer'] ) > 0 ) {
318            $tail = array_slice( $error['dataPointer'], -( count( $descriptor['dataPointer'] ) ) );
319            return $descriptor['dataPointer'] === $tail;
320        }
321        // If the descriptor dataPointer is empty, return true
322        return true;
323    }
324
325    /**
326     * Given an error data field, which is the partial ZObject in which
327     * the error has been found, returns its data type if available
328     *
329     * @param array $data
330     * @return string|bool
331     */
332    public static function getDataType( $data ) {
333        // $data = json_decode( json_encode( $errorData ), true);
334        if ( !is_array( $data ) ) {
335            return false;
336        }
337
338        if ( !array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $data ) ) {
339            return false;
340        }
341
342        if ( is_string( $data[ ZTypeRegistry::Z_OBJECT_TYPE ] ) ) {
343            return $data[ ZTypeRegistry::Z_OBJECT_TYPE ];
344        }
345
346        if (
347            array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $data[ZTypeRegistry::Z_OBJECT_TYPE ] ) &&
348            ( $data[ ZTypeRegistry::Z_OBJECT_TYPE ][ ZTypeRegistry::Z_OBJECT_TYPE ] === ZTypeRegistry::Z_REFERENCE ) &&
349            array_key_exists( ZTypeRegistry::Z_REFERENCE_VALUE, $data[ ZTypeRegistry::Z_OBJECT_TYPE  ] )
350        ) {
351            return $data[ ZTypeRegistry::Z_OBJECT_TYPE ][ ZTypeRegistry::Z_REFERENCE_VALUE ];
352        }
353
354        return false;
355    }
356
357    /**
358     * Infers the type of the error data field and returns true if the
359     * missing key matches that type.
360     *
361     * @param array|string $error
362     * @return bool
363     */
364    public static function errorMatchesType( $error ): bool {
365        // Infer data type if we can
366        $dataType = self::getDataType( $error['data'] );
367        if ( !$dataType ) {
368            return true;
369        }
370
371      // If there's a missing key and an inferred type, match them
372        if ( !array_key_exists( 'missing', $error['keywordArgs'] ) ) {
373            return true;
374        }
375
376        $keyPrefix = $dataType . 'K';
377        $missingKey = $error['keywordArgs']['missing'];
378        return ( strpos( $missingKey, $keyPrefix ) === 0 );
379    }
380
381    /**
382     * Create a Z5/ZError of the type Z509/Multiple errors
383     *
384     * @param ZError[] $errorList
385     * @return ZError
386     */
387    public static function createZErrorList( $errorList ): ZError {
388        $errorsList = array_merge( [ new ZReference( ZTypeRegistry::Z_ERROR ) ], $errorList );
389        return self::createZErrorInstance(
390            ZErrorTypeRegistry::Z_ERROR_LIST,
391            [
392                // We don't need to catch the error thrown by ZObjectFactory::createChild because
393                // we know that every item of the list is already an instance of ZObject
394                'errorList' => ZObjectFactory::createChild( $errorsList )
395            ]
396        );
397    }
398
399    /**
400     * Create a Z5/ZError of the type Z502/Not wellformed
401     *
402     * @param ZError $childError
403     * @return ZError
404     */
405    public static function createValidationZError( ZError $childError ): ZError {
406        return self::createZErrorInstance(
407            ZErrorTypeRegistry::Z_ERROR_NOT_WELLFORMED,
408            [
409                'subtype' => $childError->getZErrorType(),
410                'childError' => $childError
411            ]
412        );
413    }
414
415    /**
416     * Create a Z5/ZError of the type Z502/Not wellformed
417     *
418     * @param string $right
419     * @param ZObjectContent $content
420     * @param int $flags whether is edit EDIT_UPDATE or create EDIT_NEW
421     * @return ZError
422     */
423    public static function createAuthorizationZError( $right, $content, $flags ): ZError {
424        $error = null;
425
426        switch ( $right ) {
427            case 'edit':
428            case 'wikilambda-create':
429            case 'wikilambda-edit':
430                $message = ( $flags === EDIT_NEW ) ? 'nocreatetext' : 'badaccess-group0';
431                $error = self::createZErrorInstance(
432                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT,
433                    [ 'message' => wfMessage( $message )->text() ]
434                );
435                break;
436
437            default:
438                // Return descriptive error message using the right action message:
439                $message = wfMessage( 'apierror-permissiondenied', wfMessage( "action-$right" ) )->text();
440                $error = self::createZErrorInstance(
441                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT,
442                    [ 'message' => $message ]
443                );
444        }
445
446        return $error;
447    }
448
449    /**
450     * Create a Z5/ZError of the type Z526/Key not wellformed
451     *
452     * @param string $key
453     * @param ZError $childError
454     * @return ZError
455     */
456    public static function createKeyValueZError( $key, $childError ): ZError {
457        return self::createZErrorInstance(
458            ZErrorTypeRegistry::Z_ERROR_KEY_VALUE_NOT_WELLFORMED,
459            [
460                'key' => $key,
461                'childError' => $childError
462            ]
463        );
464    }
465
466    /**
467     * Create a Z5/ZError of the type Z522/Array element not wellformed
468     *
469     * @param string $index
470     * @param ZError $childError
471     * @return ZError
472     */
473    public static function createArrayElementZError( $index, $childError ): ZError {
474        return self::createZErrorInstance(
475            ZErrorTypeRegistry::Z_ERROR_ARRAY_ELEMENT_NOT_WELLFORMED,
476            [
477                'index' => $index,
478                'childError' => $childError
479            ]
480        );
481    }
482
483    /**
484     * Create a ZError wrapping one or more label clashing errors
485     *
486     * @param array $clashes
487     * @return ZError
488     */
489    public static function createLabelClashZErrors( $clashes ): ZError {
490        $clashErrors = [];
491        foreach ( $clashes as $language => $clashZid ) {
492            $clashErrors[] = self::createZErrorInstance(
493                ZErrorTypeRegistry::Z_ERROR_LABEL_CLASH,
494                [
495                    'zid' => $clashZid,
496                    'language' => $language
497                ]
498            );
499        }
500
501        if ( count( $clashErrors ) > 1 ) {
502            return self::createZErrorList( $clashErrors );
503        }
504        return $clashErrors[0];
505    }
506
507    /**
508     * Create a Z5/ZError of a given Z50/ZErrorType
509     *
510     * @param string $zErrorType
511     * @param array $payload
512     * @return ZError
513     */
514    public static function createZErrorInstance( $zErrorType, $payload ): ZError {
515        $zErrorValue = [];
516
517        switch ( $zErrorType ) {
518            case ZErrorTypeRegistry::Z_ERROR_UNKNOWN:
519                $zErrorValue[] = new ZString( $payload['message'] );
520                break;
521
522            case ZErrorTypeRegistry::Z_ERROR_NOT_WELLFORMED:
523                $zErrorValue[] = new ZString( $payload['subtype'] );
524                $zErrorValue[] = $payload['childError'];
525                break;
526
527            case ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND:
528                $zErrorValue[] = new ZString( $payload['data'] );
529                break;
530
531            case ZErrorTypeRegistry::Z_ERROR_EVALUATION:
532                $zErrorValue[] = new ZQuote( $payload['functionCall'] );
533                $zErrorValue[] = $payload['error'];
534                break;
535
536            case ZErrorTypeRegistry::Z_ERROR_LIST:
537                $zErrorValue[] = $payload['errorList'];
538                break;
539
540            case ZErrorTypeRegistry::Z_ERROR_MISSING_KEY:
541                $ref = array_key_exists( 'missing', $payload['keywordArgs'] ) ? $payload['keywordArgs']['missing'] : '';
542                $zErrorValue[] = new ZKeyReference( $ref );
543                $zErrorValue[] = new ZQuote( $payload['data'] );
544                break;
545
546            case ZErrorTypeRegistry::Z_ERROR_MISSING_PERSISTENT_VALUE:
547                $zErrorValue[] = new ZQuote( $payload['data'] );
548                break;
549
550            case ZErrorTypeRegistry::Z_ERROR_UNDEFINED_LIST_TYPE:
551                $zErrorValue[] = new ZQuote( $payload['data'] );
552                break;
553
554            case ZErrorTypeRegistry::Z_ERROR_WRONG_LIST_TYPE:
555                $zErrorValue[] = new ZQuote( $payload['data'] );
556                break;
557
558            case ZErrorTypeRegistry::Z_ERROR_NOT_NUMBER_BOOLEAN_NULL:
559                $zErrorValue[] = new ZQuote( $payload['data'] );
560                break;
561
562            case ZErrorTypeRegistry::Z_ERROR_ARRAY_ELEMENT_NOT_WELLFORMED:
563                $zErrorValue[] = new ZString( $payload['index'] );
564                $zErrorValue[] = $payload['childError'];
565                break;
566
567            case ZErrorTypeRegistry::Z_ERROR_MISSING_TYPE:
568                $zErrorValue[] = new ZQuote( $payload['data'] );
569                break;
570
571            case ZErrorTypeRegistry::Z_ERROR_TYPE_NOT_STRING_ARRAY:
572                $zErrorValue[] = $payload['data'];
573                break;
574
575            case ZErrorTypeRegistry::Z_ERROR_INVALID_KEY:
576                $zErrorValue[] = new ZString( end( $payload['dataPointer'] ) );
577                break;
578
579            case ZErrorTypeRegistry::Z_ERROR_KEY_VALUE_NOT_WELLFORMED:
580                $zErrorValue[] = new ZKeyReference( $payload['key'] );
581                $zErrorValue[] = $payload['childError'];
582                break;
583
584            case ZErrorTypeRegistry::Z_ERROR_STRING_VALUE_MISSING:
585                $zErrorValue[] = new ZQuote( $payload['data'] );
586                break;
587
588            case ZErrorTypeRegistry::Z_ERROR_STRING_VALUE_WRONG_TYPE:
589                $zErrorValue[] = new ZQuote( $payload['data'] );
590                break;
591
592            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_MISSING:
593                $zErrorValue[] = new ZQuote( $payload['data'] );
594                break;
595
596            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_WRONG_TYPE:
597                $zErrorValue[] = new ZQuote( $payload['data'] );
598                break;
599
600            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_INVALID:
601                $zErrorValue[] = new ZString( $payload['data'] );
602                break;
603
604            case ZErrorTypeRegistry::Z_ERROR_WRONG_NAMESPACE:
605                $zErrorValue[] = new ZString( $payload['title'] );
606                break;
607
608            case ZErrorTypeRegistry::Z_ERROR_WRONG_CONTENT_TYPE:
609                $zErrorValue[] = new ZString( $payload['title'] );
610                break;
611
612            case ZErrorTypeRegistry::Z_ERROR_INVALID_LANG_CODE:
613                $zErrorValue[] = new ZString( $payload['lang'] );
614                break;
615
616            case ZErrorTypeRegistry::Z_ERROR_LANG_NOT_FOUND:
617                $zErrorValue[] = new ZString( $payload['lang'] );
618                break;
619
620            case ZErrorTypeRegistry::Z_ERROR_UNEXPECTED_ZTYPE:
621                $zErrorValue[] = new ZReference( $payload['expected'] );
622                $zErrorValue[] = new ZReference( $payload['used'] );
623                break;
624
625            case ZErrorTypeRegistry::Z_ERROR_ZTYPE_NOT_FOUND:
626                $zErrorValue[] = new ZString( $payload['type'] );
627                break;
628
629            case ZErrorTypeRegistry::Z_ERROR_CONFLICTING_TYPE_NAMES:
630                $zErrorValue[] = new ZString( $payload['zid'] );
631                $zErrorValue[] = new ZString( $payload['name'] );
632                $zErrorValue[] = new ZString( $payload['existing'] );
633                break;
634
635            case ZErrorTypeRegistry::Z_ERROR_CONFLICTING_TYPE_ZIDS:
636                $zErrorValue[] = new ZString( $payload['zid'] );
637                $zErrorValue[] = new ZString( $payload['name'] );
638                $zErrorValue[] = new ZString( $payload['existing'] );
639                break;
640
641            case ZErrorTypeRegistry::Z_ERROR_BUILTIN_TYPE_NOT_FOUND:
642                $zErrorValue[] = new ZString( $payload['zid'] );
643                $zErrorValue[] = new ZString( $payload['name'] );
644                break;
645
646            case ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT:
647                $zErrorValue[] = new ZQuote( $payload['data'] );
648                break;
649
650            case ZErrorTypeRegistry::Z_ERROR_INVALID_JSON:
651                $zErrorValue[] = new ZString( $payload['message'] );
652                $zErrorValue[] = new ZQuote( $payload['data'] );
653                break;
654
655            case ZErrorTypeRegistry::Z_ERROR_INVALID_REFERENCE:
656                $zErrorValue[] = new ZString( $payload['data'] );
657                break;
658
659            case ZErrorTypeRegistry::Z_ERROR_UNKNOWN_REFERENCE:
660                $zErrorValue[] = new ZString( $payload['data'] );
661                break;
662
663            case ZErrorTypeRegistry::Z_ERROR_SCHEMA_TYPE_MISMATCH:
664                $keyRef = end( $payload['dataPointer'] );
665                $zErrorValue[] = new ZKeyReference( $keyRef ?: '' );
666                $zErrorValue[] = new ZString( $payload['keywordArgs']['expected'] );
667                $zErrorValue[] = new ZString( $payload['keywordArgs']['used'] );
668                break;
669
670            case ZErrorTypeRegistry::Z_ERROR_ARRAY_TYPE_MISMATCH:
671                $zErrorValue[] = new ZKeyReference( $payload['key'] );
672                $zErrorValue[] = new ZString( $payload['expected'] );
673                $zErrorValue[] = new ZQuote( $payload['data'] );
674                break;
675
676            case ZErrorTypeRegistry::Z_ERROR_DISALLOWED_ROOT_ZOBJECT:
677                $zErrorValue[] = new ZQuote( $payload['data'] );
678                break;
679
680            case ZErrorTypeRegistry::Z_ERROR_LABEL_CLASH:
681                $zErrorValue[] = new ZString( $payload['zid'] );
682                $zErrorValue[] = new ZString( $payload['language'] );
683                break;
684
685            case ZErrorTypeRegistry::Z_ERROR_UNMATCHING_ZID:
686                $zErrorValue[] = new ZString( $payload['zid'] );
687                $zErrorValue[] = new ZString( $payload['title'] );
688                break;
689
690            case ZErrorTypeRegistry::Z_ERROR_INVALID_TITLE:
691                $zErrorValue[] = new ZString( $payload['title'] );
692                break;
693
694            case ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT:
695                $zErrorValue[] = new ZString( $payload['message'] );
696                break;
697
698            case ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN:
699                // No context.
700                break;
701
702            default:
703                break;
704        }
705
706        return new ZError(
707            new ZReference( $zErrorType ),
708            self::createTypedError( $zErrorType, $zErrorValue )
709        );
710    }
711
712    /**
713     * Create a ZTypedError instance given a errorType Zid and a set of values.
714     *
715     * @param string $errorType
716     * @param ZObject[] $errorValues
717     * @return ZTypedError
718     */
719    private static function createTypedError( $errorType, $errorValues ) {
720        return new ZTypedError( ZTypedError::buildType( $errorType ), $errorValues );
721    }
722
723    /**
724     * Create an unknown validation error
725     *
726     * @param string $message
727     * @return ZError
728     */
729    public static function createUnknownValidationError( $message ): ZError {
730        $zError = self::createZErrorInstance(
731            ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
732            [ 'message' => $message ]
733        );
734        return self::createValidationZError( $zError );
735    }
736
737    /**
738     * Convenience method to wrap a non-error in a Z507/Evaluation ZError
739     *
740     * @param string|ZObject $message The non-error to wrap.
741     * @param string $call The functional call context.
742     * @return ZError
743     */
744    public static function wrapMessageInZError( $message, $call ): ZError {
745        $wrappedError = self::createZErrorInstance(
746            ZErrorTypeRegistry::Z_ERROR_UNKNOWN, [ 'message' => $message ]
747        );
748        $zerror = self::createZErrorInstance(
749            ZErrorTypeRegistry::Z_ERROR_EVALUATION,
750            [
751                'functionCall' => $call,
752                'error' => $wrappedError
753            ]
754        );
755        return $zerror;
756    }
757}