Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.06% covered (success)
96.06%
268 / 279
88.89% covered (warning)
88.89%
16 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ZErrorFactory
96.06% covered (success)
96.06%
268 / 279
88.89% covered (warning)
88.89%
16 / 18
101
0.00% covered (danger)
0.00%
0 / 1
 getLogger
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getErrorDescriptors
62.50% covered (warning)
62.50%
15 / 24
0.00% covered (danger)
0.00%
0 / 1
6.32
 joinPath
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 readYamlAsSecretJson
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 errorMatchesDescriptor
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
10
 getDataType
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 errorMatchesType
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 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
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 createKeyValueZError
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createArrayElementZError
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 createLabelClashZErrors
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 createZErrorInstance
100.00% covered (success)
100.00%
132 / 132
100.00% covered (success)
100.00%
1 / 1
54
 createTypedError
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 createUnknownValidationError
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 createEvaluationError
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 createApiFailureError
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 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 Exception;
13use MediaWiki\Extension\WikiLambda\Registry\ZErrorTypeRegistry;
14use MediaWiki\Extension\WikiLambda\Registry\ZTypeRegistry;
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 MediaWiki\Logger\LoggerFactory;
23use Psr\Log\LoggerInterface;
24use Symfony\Component\Yaml\Yaml;
25
26class ZErrorFactory {
27
28    /**
29     * @var array
30     */
31    private static $errorDescriptors;
32
33    private static ?LoggerInterface $logger = null;
34
35    private static function getLogger(): LoggerInterface {
36        self::$logger ??= LoggerFactory::getInstance( 'WikiLambda' );
37        return self::$logger;
38    }
39
40    /**
41     * Read and parse the yaml file with the error type descriptors
42     *
43     * @return array
44     */
45    private static function getErrorDescriptors(): array {
46        if ( !self::$errorDescriptors ) {
47            try {
48                $descriptor = json_decode(
49                    self::readYamlAsSecretJson(
50                        self::joinPath(
51                            self::joinPath( __DIR__, "..", "function-schemata", "data" ),
52                            "errors",
53                            "error_types.yaml"
54                        )
55                    ), true
56                );
57                if (
58                    array_key_exists( 'patterns', $descriptor ) &&
59                    array_key_exists( 'keywords', $descriptor['patterns'] )
60                ) {
61                    self::$errorDescriptors = $descriptor['patterns']['keywords'];
62                } else {
63                    self::getLogger()->warning(
64                        __METHOD__ . ': error_types.yaml loaded but missing expected structure'
65                    );
66                    self::$errorDescriptors = [];
67                }
68            } catch ( Exception $e ) {
69                self::getLogger()->warning(
70                    __METHOD__ . ': Failed to load error_types.yaml: {message}',
71                    [ 'message' => $e->getMessage() ]
72                );
73                self::$errorDescriptors = [];
74            }
75        }
76        return self::$errorDescriptors;
77    }
78
79    /**
80     * Joins an arbitrary number of path components via DIRECTORY_SEPARATOR.
81     * @param string ...$pathComponents
82     * @return string
83     */
84    private static function joinPath( string ...$pathComponents ): string {
85        $components = [];
86        foreach ( $pathComponents as $component ) {
87            $component = rtrim( $component, DIRECTORY_SEPARATOR );
88            array_push( $components, $component );
89        }
90        return implode( DIRECTORY_SEPARATOR, $components );
91    }
92
93    /**
94     * @param string $yamlFile File to read
95     * @return string
96     */
97    public static function readYamlAsSecretJson( string $yamlFile ) {
98        return json_encode( Yaml::parseFile( $yamlFile ) );
99    }
100
101    /**
102     * @param array $error
103     * @param array $descriptor
104     * @return bool
105     */
106    public static function errorMatchesDescriptor( $error, $descriptor ): bool {
107        // 1. The descriptor keyword must be the same as the error keyword
108        if ( $error['keyword'] !== $descriptor['keyword'] ) {
109            return false;
110        }
111
112        // 2. Every item from the descriptor keywordArgs must be present in the error
113        foreach ( $descriptor['keywordArgs'] as $wantedArg => $wantedValue ) {
114            if ( !array_key_exists( $wantedArg, $error['keywordArgs'] ) ) {
115                return false;
116            }
117
118            if ( is_array( $wantedValue ) && !in_array( $error['keywordArgs'][ $wantedArg ], $wantedValue ) ) {
119                return false;
120            }
121
122            if ( is_string( $wantedValue ) && ( $error['keywordArgs'][ $wantedArg ] !== $wantedValue ) ) {
123                return false;
124            }
125        }
126
127        // 3. The descriptor dataPointer must match the tail of the error dataPointer
128        // 3.1. If the descriptor dataPointer is longer than the error dataPointer, not a match
129        if ( count( $descriptor['dataPointer'] ) > count( $error['dataPointer'] ) ) {
130            return false;
131        }
132
133        // 3.2 Get the tail of the error descriptor and check (with ===)
134        // that both arrays contain the same elements in the same order
135        if ( count( $descriptor['dataPointer'] ) > 0 ) {
136            $tail = array_slice( $error['dataPointer'], -( count( $descriptor['dataPointer'] ) ) );
137            return $descriptor['dataPointer'] === $tail;
138        }
139        // If the descriptor dataPointer is empty, return true
140        return true;
141    }
142
143    /**
144     * Given an error data field, which is the partial ZObject in which
145     * the error has been found, returns its data type if available
146     *
147     * @param array $data
148     * @return string|bool
149     */
150    public static function getDataType( $data ) {
151        // $data = json_decode( json_encode( $errorData ), true);
152        if ( !is_array( $data ) ) {
153            return false;
154        }
155
156        if ( !array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $data ) ) {
157            return false;
158        }
159
160        if ( is_string( $data[ ZTypeRegistry::Z_OBJECT_TYPE ] ) ) {
161            return $data[ ZTypeRegistry::Z_OBJECT_TYPE ];
162        }
163
164        if (
165            array_key_exists( ZTypeRegistry::Z_OBJECT_TYPE, $data[ZTypeRegistry::Z_OBJECT_TYPE ] ) &&
166            ( $data[ ZTypeRegistry::Z_OBJECT_TYPE ][ ZTypeRegistry::Z_OBJECT_TYPE ] === ZTypeRegistry::Z_REFERENCE ) &&
167            array_key_exists( ZTypeRegistry::Z_REFERENCE_VALUE, $data[ ZTypeRegistry::Z_OBJECT_TYPE  ] )
168        ) {
169            return $data[ ZTypeRegistry::Z_OBJECT_TYPE ][ ZTypeRegistry::Z_REFERENCE_VALUE ];
170        }
171
172        return false;
173    }
174
175    /**
176     * Infers the type of the error data field and returns true if the
177     * missing key matches that type.
178     *
179     * @param array|string $error
180     * @return bool
181     */
182    public static function errorMatchesType( $error ): bool {
183        // Infer data type if we can
184        $dataType = self::getDataType( $error['data'] );
185        if ( !$dataType ) {
186            return true;
187        }
188
189      // If there's a missing key and an inferred type, match them
190        if ( !array_key_exists( 'missing', $error['keywordArgs'] ) ) {
191            return true;
192        }
193
194        $keyPrefix = $dataType . 'K';
195        $missingKey = $error['keywordArgs']['missing'];
196        return ( strpos( $missingKey, $keyPrefix ) === 0 );
197    }
198
199    /**
200     * Create a Z5/ZError of the type Z509/Multiple errors
201     *
202     * @param ZError[] $errorList
203     * @return ZError
204     */
205    public static function createZErrorList( $errorList ): ZError {
206        $errorsList = array_merge( [ new ZReference( ZTypeRegistry::Z_ERROR ) ], $errorList );
207        return self::createZErrorInstance(
208            ZErrorTypeRegistry::Z_ERROR_LIST,
209            [
210                // We don't need to catch the error thrown by ZObjectFactory::create because
211                // we know that every item of the list is already an instance of ZObject
212                'errorList' => ZObjectFactory::create( $errorsList )
213            ]
214        );
215    }
216
217    /**
218     * Create a Z5/ZError of the type Z502/Not wellformed
219     *
220     * @param ZError $childError
221     * @return ZError
222     */
223    public static function createValidationZError( ZError $childError ): ZError {
224        return self::createZErrorInstance(
225            ZErrorTypeRegistry::Z_ERROR_NOT_WELLFORMED,
226            [
227                'subtype' => $childError->getZErrorType(),
228                'childError' => $childError
229            ]
230        );
231    }
232
233    /**
234     * Create a Z5/ZError of the type Z502/Not wellformed
235     *
236     * @param string $right
237     * @param int $flags whether is edit EDIT_UPDATE or create EDIT_NEW
238     * @return ZError
239     */
240    public static function createAuthorizationZError( $right, $flags ): ZError {
241        $error = null;
242
243        switch ( $right ) {
244            case 'edit':
245            case 'wikilambda-create':
246            case 'wikilambda-edit':
247                $message = ( $flags === EDIT_NEW ) ? 'nocreatetext' : 'badaccess-group0';
248                $error = self::createZErrorInstance(
249                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT,
250                    [ 'message' => wfMessage( $message )->text() ]
251                );
252                break;
253
254            default:
255                // Return descriptive error message using the right action message:
256                $message = wfMessage( 'apierror-permissiondenied', wfMessage( "action-$right" ) )->text();
257                $error = self::createZErrorInstance(
258                    ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT,
259                    [ 'message' => $message ]
260                );
261        }
262
263        return $error;
264    }
265
266    /**
267     * Create a Z5/ZError of the type Z526/Key not wellformed
268     *
269     * @param string $key
270     * @param ZError $childError
271     * @return ZError
272     */
273    public static function createKeyValueZError( $key, $childError ): ZError {
274        return self::createZErrorInstance(
275            ZErrorTypeRegistry::Z_ERROR_KEY_VALUE_NOT_WELLFORMED,
276            [
277                'key' => $key,
278                'childError' => $childError
279            ]
280        );
281    }
282
283    /**
284     * Create a Z5/ZError of the type Z522/Array element not wellformed
285     *
286     * @param string $index
287     * @param ZError $childError
288     * @return ZError
289     */
290    public static function createArrayElementZError( $index, $childError ): ZError {
291        return self::createZErrorInstance(
292            ZErrorTypeRegistry::Z_ERROR_ARRAY_ELEMENT_NOT_WELLFORMED,
293            [
294                'index' => $index,
295                'childError' => $childError
296            ]
297        );
298    }
299
300    /**
301     * Create a ZError wrapping one or more label clashing errors
302     *
303     * @param array $clashes
304     * @return ZError
305     */
306    public static function createLabelClashZErrors( $clashes ): ZError {
307        $clashErrors = [];
308        foreach ( $clashes as $language => $clashZid ) {
309            $clashErrors[] = self::createZErrorInstance(
310                ZErrorTypeRegistry::Z_ERROR_LABEL_CLASH,
311                [
312                    'zid' => $clashZid,
313                    'language' => $language
314                ]
315            );
316        }
317
318        if ( count( $clashErrors ) > 1 ) {
319            return self::createZErrorList( $clashErrors );
320        }
321        return $clashErrors[0];
322    }
323
324    /**
325     * Create a Z5/ZError of a given Z50/ZErrorType
326     *
327     * @param string $zErrorType
328     * @param array $payload
329     * @return ZError
330     */
331    public static function createZErrorInstance( $zErrorType, $payload ): ZError {
332        $zErrorValue = [];
333
334        switch ( $zErrorType ) {
335            case ZErrorTypeRegistry::Z_ERROR_UNKNOWN:
336                $zErrorValue[] = new ZString( $payload['message'] );
337                break;
338
339            case ZErrorTypeRegistry::Z_ERROR_INVALID_SYNTAX:
340                $zErrorValue[] = new ZString( $payload['message'] );
341                $zErrorValue[] = new ZString( $payload['input'] );
342                break;
343
344            case ZErrorTypeRegistry::Z_ERROR_NOT_WELLFORMED:
345                $zErrorValue[] = new ZString( $payload['subtype'] );
346                $zErrorValue[] = $payload['childError'];
347                break;
348
349            case ZErrorTypeRegistry::Z_ERROR_ZID_NOT_FOUND:
350                $zErrorValue[] = new ZString( $payload['data'] );
351                break;
352
353            case ZErrorTypeRegistry::Z_ERROR_EVALUATION:
354                $zErrorValue[] = new ZQuote( $payload['functionCall'] );
355                $zErrorValue[] = $payload['error'];
356                break;
357
358            case ZErrorTypeRegistry::Z_ERROR_LIST:
359                $zErrorValue[] = $payload['errorList'];
360                break;
361
362            case ZErrorTypeRegistry::Z_ERROR_MISSING_KEY:
363                $ref = array_key_exists( 'missing', $payload['keywordArgs'] ) ? $payload['keywordArgs']['missing'] : '';
364                $zErrorValue[] = new ZKeyReference( $ref );
365                $zErrorValue[] = new ZQuote( $payload['data'] );
366                break;
367
368            case ZErrorTypeRegistry::Z_ERROR_MISSING_PERSISTENT_VALUE:
369                $zErrorValue[] = new ZQuote( $payload['data'] );
370                break;
371
372            case ZErrorTypeRegistry::Z_ERROR_UNDEFINED_LIST_TYPE:
373                $zErrorValue[] = new ZQuote( $payload['data'] );
374                break;
375
376            case ZErrorTypeRegistry::Z_ERROR_WRONG_LIST_TYPE:
377                $zErrorValue[] = new ZQuote( $payload['data'] );
378                break;
379
380            case ZErrorTypeRegistry::Z_ERROR_NOT_NUMBER_BOOLEAN_NULL:
381                $zErrorValue[] = new ZQuote( $payload['data'] );
382                break;
383
384            case ZErrorTypeRegistry::Z_ERROR_ARRAY_ELEMENT_NOT_WELLFORMED:
385                $zErrorValue[] = new ZString( $payload['index'] );
386                $zErrorValue[] = $payload['childError'];
387                break;
388
389            case ZErrorTypeRegistry::Z_ERROR_MISSING_TYPE:
390                $zErrorValue[] = new ZQuote( $payload['data'] );
391                break;
392
393            case ZErrorTypeRegistry::Z_ERROR_TYPE_NOT_STRING_ARRAY:
394                $zErrorValue[] = $payload['data'];
395                break;
396
397            case ZErrorTypeRegistry::Z_ERROR_INVALID_KEY:
398                $zErrorValue[] = new ZString( end( $payload['dataPointer'] ) );
399                break;
400
401            case ZErrorTypeRegistry::Z_ERROR_KEY_VALUE_NOT_WELLFORMED:
402                $zErrorValue[] = new ZKeyReference( $payload['key'] );
403                $zErrorValue[] = $payload['childError'];
404                break;
405
406            case ZErrorTypeRegistry::Z_ERROR_STRING_VALUE_MISSING:
407                $zErrorValue[] = new ZQuote( $payload['data'] );
408                break;
409
410            case ZErrorTypeRegistry::Z_ERROR_STRING_VALUE_WRONG_TYPE:
411                $zErrorValue[] = new ZQuote( $payload['data'] );
412                break;
413
414            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_MISSING:
415                $zErrorValue[] = new ZQuote( $payload['data'] );
416                break;
417
418            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_WRONG_TYPE:
419                $zErrorValue[] = new ZQuote( $payload['data'] );
420                break;
421
422            case ZErrorTypeRegistry::Z_ERROR_REFERENCE_VALUE_INVALID:
423                $zErrorValue[] = new ZString( $payload['data'] );
424                break;
425
426            case ZErrorTypeRegistry::Z_ERROR_WRONG_NAMESPACE:
427                $zErrorValue[] = new ZString( $payload['title'] );
428                break;
429
430            case ZErrorTypeRegistry::Z_ERROR_WRONG_CONTENT_TYPE:
431                $zErrorValue[] = new ZString( $payload['title'] );
432                break;
433
434            case ZErrorTypeRegistry::Z_ERROR_INVALID_LANG_CODE:
435                $zErrorValue[] = new ZString( $payload['lang'] );
436                break;
437
438            case ZErrorTypeRegistry::Z_ERROR_LANG_NOT_FOUND:
439                $zErrorValue[] = new ZString( $payload['lang'] );
440                break;
441
442            case ZErrorTypeRegistry::Z_ERROR_UNEXPECTED_ZTYPE:
443                $zErrorValue[] = new ZReference( $payload['expected'] );
444                $zErrorValue[] = new ZReference( $payload['used'] );
445                break;
446
447            case ZErrorTypeRegistry::Z_ERROR_ZTYPE_NOT_FOUND:
448                $zErrorValue[] = new ZString( $payload['type'] );
449                break;
450
451            case ZErrorTypeRegistry::Z_ERROR_CONFLICTING_TYPE_NAMES:
452                $zErrorValue[] = new ZString( $payload['zid'] );
453                $zErrorValue[] = new ZString( $payload['name'] );
454                $zErrorValue[] = new ZString( $payload['existing'] );
455                break;
456
457            case ZErrorTypeRegistry::Z_ERROR_CONFLICTING_TYPE_ZIDS:
458                $zErrorValue[] = new ZString( $payload['zid'] );
459                $zErrorValue[] = new ZString( $payload['name'] );
460                $zErrorValue[] = new ZString( $payload['existing'] );
461                break;
462
463            case ZErrorTypeRegistry::Z_ERROR_BUILTIN_TYPE_NOT_FOUND:
464                $zErrorValue[] = new ZString( $payload['zid'] );
465                $zErrorValue[] = new ZString( $payload['name'] );
466                break;
467
468            case ZErrorTypeRegistry::Z_ERROR_INVALID_FORMAT:
469                $zErrorValue[] = new ZQuote( $payload['data'] );
470                break;
471
472            case ZErrorTypeRegistry::Z_ERROR_INVALID_JSON:
473                $zErrorValue[] = new ZString( $payload['message'] );
474                $zErrorValue[] = new ZQuote( $payload['data'] );
475                break;
476
477            case ZErrorTypeRegistry::Z_ERROR_INVALID_REFERENCE:
478                $zErrorValue[] = new ZString( $payload['data'] );
479                break;
480
481            case ZErrorTypeRegistry::Z_ERROR_UNKNOWN_REFERENCE:
482                $zErrorValue[] = new ZString( $payload['data'] );
483                break;
484
485            case ZErrorTypeRegistry::Z_ERROR_SCHEMA_TYPE_MISMATCH:
486                $keyRef = end( $payload['dataPointer'] );
487                $zErrorValue[] = new ZKeyReference( $keyRef ?: '' );
488                $zErrorValue[] = new ZString( $payload['keywordArgs']['expected'] );
489                $zErrorValue[] = new ZString( $payload['keywordArgs']['used'] );
490                break;
491
492            case ZErrorTypeRegistry::Z_ERROR_ARRAY_TYPE_MISMATCH:
493                $zErrorValue[] = new ZKeyReference( $payload['key'] );
494                $zErrorValue[] = new ZString( $payload['expected'] );
495                $zErrorValue[] = new ZQuote( $payload['data'] );
496                break;
497
498            case ZErrorTypeRegistry::Z_ERROR_DISALLOWED_ROOT_ZOBJECT:
499                $zErrorValue[] = new ZQuote( $payload['data'] );
500                break;
501
502            case ZErrorTypeRegistry::Z_ERROR_LABEL_CLASH:
503                $zErrorValue[] = new ZString( $payload['zid'] );
504                $zErrorValue[] = new ZString( $payload['language'] );
505                break;
506
507            case ZErrorTypeRegistry::Z_ERROR_UNMATCHING_ZID:
508                $zErrorValue[] = new ZString( $payload['zid'] );
509                $zErrorValue[] = new ZString( $payload['title'] );
510                break;
511
512            case ZErrorTypeRegistry::Z_ERROR_INVALID_TITLE:
513                $zErrorValue[] = new ZString( $payload['title'] );
514                break;
515
516            case ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_EDIT:
517                $zErrorValue[] = new ZString( $payload['message'] );
518                break;
519
520            case ZErrorTypeRegistry::Z_ERROR_USER_CANNOT_RUN:
521                // No context.
522                break;
523
524            case ZErrorTypeRegistry::Z_ERROR_ARGUMENT_TYPE_MISMATCH:
525                $zErrorValue[] = new ZReference( $payload['expected'] );
526                $zErrorValue[] = new ZReference( $payload['actual'] );
527                $zErrorValue[] = new ZQuote( $payload['argument'] );
528                break;
529
530            case ZErrorTypeRegistry::Z_ERROR_ARGUMENT_COUNT_MISMATCH:
531                $zErrorValue[] = new ZString( $payload['expected'] );
532                $zErrorValue[] = new ZString( $payload['actual'] );
533                $zErrorValue[] = ZObjectFactory::create( [ 'Z1', ...$payload['arguments'] ] );
534                break;
535
536            case ZErrorTypeRegistry::Z_ERROR_NOT_IMPLEMENTED_YET:
537                $zErrorValue[] = new ZString( $payload['data'] );
538                break;
539
540            case ZErrorTypeRegistry::Z_ERROR_INVALID_EVALUATION_RESULT:
541                $zErrorValue[] = new ZQuote( $payload['result'] );
542                break;
543
544            case ZErrorTypeRegistry::Z_ERROR_API_FAILURE:
545                $zErrorValue[] = new ZQuote( $payload['request'] );
546                $zErrorValue[] = $payload['error'];
547                break;
548
549            case ZErrorTypeRegistry::Z_ERROR_OBJECT_TYPE_MISMATCH:
550                $zErrorValue[] = new ZReference( $payload['expected'] );
551                $zErrorValue[] = new ZQuote( $payload['actual'] );
552                break;
553
554            case ZErrorTypeRegistry::Z_ERROR_CONNECTION_FAILURE:
555                $zErrorValue[] = new ZString( $payload['host'] );
556                break;
557
558            case ZErrorTypeRegistry::Z_ERROR_DUPLICATE_LANGUAGES:
559                $zErrorValue[] = new ZString( $payload['language'] );
560                break;
561
562            default:
563                break;
564        }
565
566        return new ZError(
567            new ZReference( $zErrorType ),
568            self::createTypedError( $zErrorType, $zErrorValue )
569        );
570    }
571
572    /**
573     * Create a ZTypedError instance given a errorType Zid and a set of values.
574     *
575     * @param string $errorType
576     * @param ZObject[] $errorValues
577     * @return ZTypedError
578     */
579    private static function createTypedError( $errorType, $errorValues ) {
580        return new ZTypedError( ZTypedError::buildType( $errorType ), $errorValues );
581    }
582
583    /**
584     * Create an unknown validation error
585     *
586     * @param string $message
587     * @return ZError
588     */
589    public static function createUnknownValidationError( $message ): ZError {
590        $zError = self::createZErrorInstance(
591            ZErrorTypeRegistry::Z_ERROR_UNKNOWN,
592            [ 'message' => $message ]
593        );
594        return self::createValidationZError( $zError );
595    }
596
597    /**
598     * Create Z507/Evaluation Error wrapping a Z500/Generic Error that
599     * wraps a string error message.
600     *
601     * @param string|ZObject $message The non-error to wrap.
602     * @param string $call The functional call context.
603     * @return ZError
604     */
605    public static function createEvaluationError( $message, $call ): ZError {
606        $wrappedError = self::createZErrorInstance(
607            ZErrorTypeRegistry::Z_ERROR_UNKNOWN, [ 'message' => $message ]
608        );
609        $zerror = self::createZErrorInstance(
610            ZErrorTypeRegistry::Z_ERROR_EVALUATION,
611            [
612                'functionCall' => $call,
613                'error' => $wrappedError
614            ]
615        );
616        return $zerror;
617    }
618
619    /**
620     * Convenience method to create a ApiFailureError that propagates a given
621     * ZError or wraps a Z500 with a given message.
622     *
623     * @param string|ZError $error The error or message to wrap
624     * @param \stdClass|string $request The function call that generated an API error
625     * @return ZError
626     */
627    public static function createApiFailureError( $error, $request ): ZError {
628        $wrappedError = ( $error instanceof ZError ) ? $error :
629            self::createZErrorInstance( ZErrorTypeRegistry::Z_ERROR_UNKNOWN, [ 'message' => $error ] );
630        return self::createZErrorInstance(
631            ZErrorTypeRegistry::Z_ERROR_API_FAILURE, [
632                'request' => $request,
633                'error' => $wrappedError
634            ]
635        );
636    }
637}