InterfaceValidator::collectReference()   A
last analyzed

Complexity

Conditions 5
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlgoWeb\ODataMetadata\Edm\Validation\Internal;
6
7
use AlgoWeb\ODataMetadata\Edm\Validation\EdmError;
8
use AlgoWeb\ODataMetadata\Edm\Validation\EdmErrorCode;
9
use AlgoWeb\ODataMetadata\Edm\Validation\Internal\InterfaceValidator\VisitorBase;
10
use AlgoWeb\ODataMetadata\Edm\Validation\Internal\InterfaceValidator\VisitorOfT;
11
use AlgoWeb\ODataMetadata\Edm\Validation\ObjectLocation;
12
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationContext;
13
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationRule;
14
use AlgoWeb\ODataMetadata\Edm\Validation\ValidationRuleSet;
15
use AlgoWeb\ODataMetadata\EdmUtil;
16
use AlgoWeb\ODataMetadata\Exception\InvalidOperationException;
17
use AlgoWeb\ODataMetadata\Interfaces\Annotations\IDirectValueAnnotation;
18
use AlgoWeb\ODataMetadata\Interfaces\ICheckable;
19
use AlgoWeb\ODataMetadata\Interfaces\IEdmElement;
20
use AlgoWeb\ODataMetadata\Interfaces\IEdmValidCoreModelElement;
21
use AlgoWeb\ODataMetadata\Interfaces\ILocatable;
22
use AlgoWeb\ODataMetadata\Interfaces\ILocation;
23
use AlgoWeb\ODataMetadata\Interfaces\IModel;
24
use AlgoWeb\ODataMetadata\Interfaces\IPrimitiveType;
25
use AlgoWeb\ODataMetadata\Interfaces\IPrimitiveTypeReference;
26
use AlgoWeb\ODataMetadata\Interfaces\ITypeReference;
27
use AlgoWeb\ODataMetadata\StringConst;
28
use AlgoWeb\ODataMetadata\Structure\HashSetInternal;
29
30
class InterfaceValidator
31
{
32
    private static $interfaceVisitors = null;
33
34
    private static $concreteTypeInterfaceVisitors = [];
35
36
    private static function getInterfaceVisitors(): iterable
37
    {
38
        return self::$interfaceVisitors ?? self::$interfaceVisitors = self::createInterfaceVisitorsMap();
39
    }
40
41
    /**
42
     * @var HashSetInternal
43
     */
44
    private $visited;
45
    /**
46
     * @var HashSetInternal
47
     */
48
    private $visitedBad;
49
    /**
50
     * @var HashSetInternal
51
     */
52
    private $danglingReferences;
53
    /**
54
     * @var HashSetInternal|null
55
     */
56
    private $skipVisitation;
57
    /**
58
     * @var bool
59
     */
60
    private $validateDirectValueAnnotations;
61
    /**
62
     * @var IModel|null
63
     */
64
    private $model;
65
66
    private function __construct(?iterable $skipVisitation, ?IModel $model, bool $validateDirectValueAnnotations)
67
    {
68
        $this->skipVisitation                 = null === $skipVisitation ? null : new HashSetInternal(iterable_to_array($skipVisitation));
69
        $this->model                          = $model;
70
        $this->validateDirectValueAnnotations = $validateDirectValueAnnotations;
71
        $this->visited                        = new HashSetInternal();
72
        $this->visitedBad                     = new HashSetInternal();
73
        $this->danglingReferences             = new HashSetInternal();
74
    }
75
76
    /**
77
     * @param  IModel               $model
78
     * @param  ValidationRuleSet    $semanticRuleSet
79
     * @throws \ReflectionException
80
     * @return iterable|EdmError[]
81
     */
82
    public static function validateModelStructureAndSemantics(IModel $model, ValidationRuleSet $semanticRuleSet): iterable
83
    {
84
        $modelValidator = new InterfaceValidator(null, $model, true);
85
86
        // Perform structural validation of the root object.
87
        $errors = iterable_to_array($modelValidator->validateStructure($model));
88
89
        // Then check references for structural integrity using separate validator (in order to avoid adding referenced objects to the this.visited).
90
        $referencesValidator              = new InterfaceValidator($modelValidator->visited, $model, false);
91
        $referencesToStructurallyValidate = $modelValidator->danglingReferences;
92
        while (count($referencesToStructurallyValidate) !== 0) {
93
            foreach ($referencesToStructurallyValidate as $reference) {
94
                $errors = array_merge(iterable_to_array($errors), $referencesValidator->validateStructure($reference));
95
            }
96
97
            $referencesToStructurallyValidate = $referencesValidator->danglingReferences;
98
        }
99
        $critical = array_filter($errors, [ValidationHelper::class, 'isInterfaceCritical']);
100
        // If there are any critical structural errors detected, then it is not safe to traverse the root object, so return the errors without further processing.
101
        if (count($critical) > 0) {
102
            return $errors;
103
        }
104
105
        // If the root object is structurally sound, apply validation rules to the visited objects that are not known to be bad.
106
        $semanticValidationContext = new ValidationContext(
107
            $model,
108
            function (IEdmElement $item) use ($modelValidator, $referencesValidator): bool {
109
                return $modelValidator->visitedBad->contains($item) || $referencesValidator->visitedBad->contains($item);
110
            }
111
        );
112
        $concreteTypeSemanticInterfaceVisitors = [];
113
        foreach ($modelValidator->visited as $item) {
114
            if (!$modelValidator->visitedBad->contains($item)) {
115
                /** * @var ValidationRule $rule */
116
                foreach (self::getSemanticInterfaceVisitorsForObject(
117
                    get_class($item),
118
                    $semanticRuleSet,
119
                    $concreteTypeSemanticInterfaceVisitors
120
                ) as $rule) {
121
                    $rule($semanticValidationContext, $item);
122
                }
123
            }
124
        }
125
126
        $errors = array_merge($errors, $semanticValidationContext->getErrors());
127
        return $errors;
128
    }
129
130
    /**
131
     * @param  IEdmElement         $item
132
     * @return iterable|EdmError[]
133
     */
134
    public static function getStructuralErrors(IEdmElement $item): iterable
135
    {
136
        $model               = $item instanceof IModel ? $item : null;
137
        $structuralValidator = new InterfaceValidator(null, $model, $model !== null);
138
        return $structuralValidator->validateStructure($item);
139
    }
140
141
    /**
142
     * @return iterable|array<string, VisitorBase>
143
     */
144
    private static function createInterfaceVisitorsMap(): iterable
145
    {
146
        $map    = [];
147
        $handle = opendir('.');
148
        if (false !== $handle) {
149
            while (false !== ($entry = readdir($handle))) {
150
                /** @var string $name */
151
                $name = substr($entry, 0, -4);
152
                $ext  = substr($entry, -4);
153
                if ($entry === '.' || $entry === '..' || is_dir($entry) || $ext !== '.php' || empty($ext)) {
154
                    continue;
155
                }
156
                if ($name === 'VisitorBase' || $name === 'VisitorOfT') {
157
                    continue;
158
                }
159
                $class = __CLASS__ . '\\' . $name;
160
                /** @var VisitorOfT $instance */
161
                $instance                  = new $class();
162
                $map[$instance->forType()] = $instance;
163
            }
164
        }
165
        return $map;
166
    }
167
168
    /**
169
     * @param  string                 $objectType
170
     * @return iterable|VisitorBase[]
171
     */
172
    private static function computeInterfaceVisitorsForObject(string $objectType): iterable
173
    {
174
        $visitors = [];
175
        foreach (class_implements($objectType) as $type) {
176
            $visitor = null;
177
            if (isset(self::getInterfaceVisitors()[$type])) {
178
                $visitor    = self::getInterfaceVisitors()[$type];
179
                $visitors[] = $visitor;
180
            }
181
        }
182
183
        return $visitors;
184
    }
185
186
    private function getInterfaceVisitorsForObject(string $objectType): iterable
187
    {
188
        $visitors = [];
189
        if (!isset(self::$concreteTypeInterfaceVisitors[$objectType])) {
190
            $visitors                                         = self::computeInterfaceVisitorsForObject($objectType);
191
            self::$concreteTypeInterfaceVisitors[$objectType] = $visitors;
192
        }
193
194
        return $visitors;
195
    }
196
197
    public static function createPropertyMustNotBeNullError($item, string $propertyName): EdmError
198
    {
199
        return new EdmError(
200
            self::getLocation($item),
201
            EdmErrorCode::InterfaceCriticalPropertyValueMustNotBeNull(),
202
            StringConst::EdmModel_Validator_Syntactic_PropertyMustNotBeNull(get_class($item), $propertyName)
203
        );
204
    }
205
206
    public static function createEnumPropertyOutOfRangeError($item, $enumValue, string $propertyName): EdmError
207
    {
208
        return new EdmError(
209
            self::getLocation($item),
210
            EdmErrorCode::InterfaceCriticalEnumPropertyValueOutOfRange(),
211
            StringConst::EdmModel_Validator_Syntactic_EnumPropertyValueOutOfRange(get_class($item), $propertyName, get_class($enumValue), $enumValue)
212
        );
213
    }
214
215
    public static function checkForInterfaceKindValueMismatchError($item, $kind, string $propertyName, string $interface): ?EdmError
216
    {
217
        // If object implements an expected interface, return no error.
218
        if (in_array($interface, class_implements($item))) {
219
            return null;
220
        }
221
222
        return new EdmError(
223
            self::getLocation($item),
224
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
225
            StringConst::EdmModel_Validator_Syntactic_InterfaceKindValueMismatch($kind, get_class($item), $propertyName, $interface)
226
        );
227
    }
228
229
    public static function createInterfaceKindValueUnexpectedError($item, $kind, string $propertyName): EdmError
230
    {
231
        return new EdmError(
232
            self::getLocation($item),
233
            EdmErrorCode::InterfaceCriticalKindValueUnexpected(),
234
            StringConst::EdmModel_Validator_Syntactic_InterfaceKindValueUnexpected($kind, get_class($item), $propertyName)
235
        );
236
    }
237
238
    public static function createTypeRefInterfaceTypeKindValueMismatchError(ITypeReference $item): EdmError
239
    {
240
        EdmUtil::checkArgumentNull($item->getDefinition(), 'item.Definition');
241
        return new EdmError(
242
            self::getLocation($item),
243
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
244
            StringConst::EdmModel_Validator_Syntactic_TypeRefInterfaceTypeKindValueMismatch(get_class($item), $item->getDefinition()->getTypeKind()->getKey())
245
        );
246
    }
247
248
    public static function createPrimitiveTypeRefInterfaceTypeKindValueMismatchError(IPrimitiveTypeReference $item): EdmError
249
    {
250
        $definition = $item->getDefinition();
251
        if (!$definition instanceof IPrimitiveType) {
252
            throw new InvalidOperationException('item.Definition is IEdmPrimitiveType');
253
        }
254
        return new EdmError(
255
            self::getLocation($item),
256
            EdmErrorCode::InterfaceCriticalKindValueMismatch(),
257
            StringConst::EdmModel_Validator_Syntactic_TypeRefInterfaceTypeKindValueMismatch(get_class($item), $definition->getPrimitiveKind()->getKey())
258
        );
259
    }
260
261
    public static function processEnumerable($item, ?iterable $enumerable, string $propertyName, array &$targetList, array &$errors): void
262
    {
263
        if (null === $enumerable) {
264
            self::collectErrors(self::createPropertyMustNotBeNullError($item, $propertyName), $errors);
265
        } else {
266
            foreach ($enumerable as $enumMember) {
267
                if (null !== $enumMember) {
268
                    $targetList[] = $enumMember;
269
                } else {
270
                    self::collectErrors(
271
                        new EdmError(
272
                            self::getLocation($item),
273
                            EdmErrorCode::InterfaceCriticalEnumerableMustNotHaveNullElements(),
274
                            StringConst::EdmModel_Validator_Syntactic_EnumerableMustNotHaveNullElements(get_class($item), $propertyName)
275
                        ),
276
                        $errors
277
                    );
278
                    break;
279
                }
280
            }
281
        }
282
    }
283
284
    public static function collectErrors(?EdmError $newError, ?array &$errors): void
285
    {
286
        if ($newError != null) {
287
            if ($errors == null) {
288
                $errors = [];
289
            }
290
291
            $errors[] = $newError;
292
        }
293
    }
294
295
    public static function isCheckableBad($element): bool
296
    {
297
        return $element instanceof ICheckable && null !== $element->getErrors() && count($element->getErrors()) > 0;
298
    }
299
300
    public static function getLocation($item): ILocation
301
    {
302
        return $item instanceof ILocatable && null !== $item->getLocation() ? $item->getLocation() : new ObjectLocation($item);
303
    }
304
305
    /**
306
     * @param  string                    $objectType
307
     * @param  ValidationRuleSet         $ruleSet
308
     * @param  array                     $concreteTypeSemanticInterfaceVisitors
309
     * @return iterable|ValidationRule[]
310
     */
311
    private static function getSemanticInterfaceVisitorsForObject(string $objectType, ValidationRuleSet $ruleSet, array &$concreteTypeSemanticInterfaceVisitors): iterable
312
    {
313
        $visitors = null;
314
        if (!isset($concreteTypeSemanticInterfaceVisitors[$objectType])) {
315
            $visitors = [];
316
            foreach (class_implements($objectType) as $type) {
317
                $visitors = array_merge($visitors, $ruleSet->getRules($type));
318
            }
319
320
            $concreteTypeSemanticInterfaceVisitors[$objectType] = $visitors;
321
        }
322
323
        return $visitors;
324
    }
325
326
    /**
327
     * @param  mixed               $item
328
     * @return iterable|EdmError[]
329
     */
330
    private function validateStructure($item): iterable
331
    {
332
        if ($item instanceof IEdmValidCoreModelElement || $this->visited->contains($item) ||
333
            ($this->skipVisitation != null && $this->skipVisitation->contains($item))) {
334
            // If we already visited this object, then errors (if any) have already been reported.
335
            return [];
336
        }
337
338
        $this->visited->add($item);
339
        if ($this->danglingReferences->contains($item)) {
340
            // If this edm element is visited, then it is no longer a dangling reference.
341
            $this->danglingReferences->remove($item);
342
        }
343
344
        //// First pass: collect immediate errors for each interface and collect followup objects for the second pass.
345
346
        $immediateErrors = null;
347
        $followup        = [];
348
        $references      = [];
349
        $visitors        = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $visitors is dead and can be removed.
Loading history...
350
        $visitors        = $this->getInterfaceVisitorsForObject(get_class($item));
351
        /** @var VisitorBase $visitor */
352
        foreach ($visitors as $visitor) {
353
            $errors = $visitor->visit($item, $followup, $references);
354
355
            // For performance reasons some visitors may return null errors enumerator.
356
            if ($errors != null) {
357
                /** @var EdmError $error */
358
                foreach ($errors as $error) {
359
                    if ($immediateErrors == null) {
360
                        $immediateErrors = [];
361
                    }
362
363
                    $immediateErrors[] = $error;
364
                }
365
            }
366
        }
367
368
        // End of the first pass: if there are immediate errors, return them without doing the second pass.
369
        if ($immediateErrors !== null) {
370
            $this->visitedBad->add($item);
371
            return $immediateErrors;
372
        }
373
374
        //// Second pass: collect errors from followup objects.
375
376
        $followupErrors = [];
377
378
        // An element's direct value annotations are available only through a model,
379
        // and so are not found in a normal traversal.
380
        if ($this->validateDirectValueAnnotations) {
381
            if ($item instanceof IEdmElement) {
382
                $element = $item;
383
                EdmUtil::checkArgumentNull($this->model, 'this->model');
384
                foreach ($this->model->getDirectValueAnnotationsManager()->getDirectValueAnnotations($element) as $annotation) {
385
                    assert($annotation instanceof IDirectValueAnnotation);
386
                    $followupErrors = array_merge(
387
                        iterable_to_array($followupErrors),
388
                        iterable_to_array($this->validateStructure($annotation))
389
                    );
390
                }
391
            }
392
        }
393
394
        foreach ($followup as $followupItem) {
395
            $followupErrors = array_merge($followupErrors, $this->validateStructure($followupItem));
396
        }
397
398
        foreach ($references as $referencedItem) {
399
            $this->collectReference($referencedItem);
400
        }
401
402
        return $followupErrors;
403
    }
404
405
    private function collectReference($reference): void
406
    {
407
        if (!($reference instanceof IEdmValidCoreModelElement) &&
408
            !$this->visited->contains($reference) &&
409
            ($this->skipVisitation == null || !$this->skipVisitation->contains($reference))) {
410
            $this->danglingReferences->add($reference);
411
        }
412
    }
413
}
414