Completed
Push — master ( 9a2e94...dbda34 )
by Vincent
05:01 queued 12s
created

AssertStructure::getAllResourceIdentifierObjects()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 26
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 8.013

Importance

Changes 0
Metric Value
cc 8
eloc 16
nc 7
nop 1
dl 0
loc 26
ccs 16
cts 17
cp 0.9412
crap 8.013
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace VGirol\JsonApiAssert\Asserts\Structure;
6
7
use PHPUnit\Framework\Assert as PHPUnit;
8
use VGirol\JsonApiAssert\Members;
9
use VGirol\JsonApiAssert\Messages;
10
11
/**
12
 * Assertions relating to the overall structure of the document
13
 */
14
trait AssertStructure
15
{
16
    /**
17
     * Asserts that a json document has valid structure.
18
     *
19
     * It will do the following checks :
20
     * 1) checks top-level members (@see assertHasValidTopLevelMembers)
21
     *
22
     * Optionaly, if presents, it will checks :
23
     * 2) primary data (@see assertIsValidPrimaryData)
24
     * 3) errors object (@see assertIsValidErrorsObject)
25
     * 4) meta object (@see assertIsValidMetaObject)
26
     * 5) jsonapi object (@see assertIsValidJsonapiObject)
27
     * 6) top-level links object (@see assertIsValidTopLevelLinksMember)
28
     * 7) included object (@see assertIsValidIncludedCollection)
29
     *
30
     * @param array   $json
31
     * @param boolean $strict If true, unsafe characters are not allowed when checking members name.
32
     *
33
     * @return void
34
     * @throws \PHPUnit\Framework\ExpectationFailedException
35
     */
36 27
    public static function assertHasValidStructure($json, bool $strict): void
37
    {
38 27
        static::assertHasValidTopLevelMembers($json);
39
40 24
        if (isset($json[Members::DATA])) {
41 18
            static::assertIsValidPrimaryData($json[Members::DATA], $strict);
42
43 15
            if (isset($json[Members::INCLUDED])) {
44 6
                static::assertIsValidIncludedCollection($json[Members::INCLUDED], $json[Members::DATA], $strict);
45
            }
46
        }
47
48 18
        if (isset($json[Members::ERRORS])) {
49 6
            static::assertIsValidErrorsObject($json[Members::ERRORS], $strict);
50
        }
51
52 15
        if (isset($json[Members::META])) {
53 6
            static::assertIsValidMetaObject($json[Members::META], $strict);
54
        }
55
56 12
        if (isset($json[Members::JSONAPI])) {
57 6
            static::assertIsValidJsonapiObject($json[Members::JSONAPI], $strict);
58
        }
59
60 9
        if (isset($json[Members::LINKS])) {
61 6
            static::assertIsValidTopLevelLinksMember($json[Members::LINKS], $strict);
62
        }
63 6
    }
64
65
    /**
66
     * Asserts that a json document has valid top-level structure.
67
     *
68
     * It will do the following checks :
69
     * 1) asserts that the json document contains at least one of the following top-level members :
70
     * "data", "meta" or "errors" (@see assertContainsAtLeastOneMember).
71
     * 2) asserts that the members "data" and "errors" does not coexist in the same document.
72
     * 3) asserts that the json document contains only the following members :
73
     * "data", "errors", "meta", "jsonapi", "links", "included" (@see assertContainsOnlyAllowedMembers).
74
     * 4) if the json document does not contain a top-level "data" member, the "included" member must not
75
     * be present either.
76
77
     * @param array $json
78
     *
79
     * @return void
80
     * @throws \PHPUnit\Framework\ExpectationFailedException
81
     */
82 42
    public static function assertHasValidTopLevelMembers($json): void
83
    {
84
        $expected = [
85 42
            Members::DATA,
86 42
            Members::ERRORS,
87 42
            Members::META
88
        ];
89 42
        static::assertContainsAtLeastOneMember(
90 42
            $expected,
91 14
            $json,
92 42
            \sprintf(Messages::TOP_LEVEL_MEMBERS, implode('", "', $expected))
93
        );
94
95 39
        PHPUnit::assertFalse(
96 39
            isset($json[Members::DATA]) && isset($json[Members::ERRORS]),
97 39
            Messages::TOP_LEVEL_DATA_AND_ERROR
98
        );
99
100
        $allowed = [
101 36
            Members::DATA,
102 36
            Members::ERRORS,
103 36
            Members::META,
104 36
            Members::JSONAPI,
105 36
            Members::LINKS,
106 36
            Members::INCLUDED
107
        ];
108 36
        static::assertContainsOnlyAllowedMembers(
109 36
            $allowed,
110 12
            $json
111
        );
112
113 30
        PHPUnit::assertFALSE(
114 30
            !isset($json[Members::DATA]) && isset($json[Members::INCLUDED]),
115 30
            Messages::TOP_LEVEL_DATA_AND_INCLUDED
116
        );
117 27
    }
118
119
    /**
120
     * Asserts that a json fragment is a valid top-level links member.
121
     *
122
     * It will do the following checks :
123
     * 1) asserts that the top-level "links" member contains only the following allowed members :
124
     * "self", "related", "first", "last", "next", "prev" (@see assertIsValidLinksObject).
125
     *
126
     * @param array   $json
127
     * @param boolean $strict If true, unsafe characters are not allowed when checking members name.
128
     *
129
     * @return void
130
     * @throws \PHPUnit\Framework\ExpectationFailedException
131
     */
132 12
    public static function assertIsValidTopLevelLinksMember($json, bool $strict): void
133
    {
134
        $allowed = [
135 12
            Members::LINK_SELF,
136 12
            Members::LINK_RELATED,
137 12
            Members::LINK_PAGINATION_FIRST,
138 12
            Members::LINK_PAGINATION_LAST,
139 12
            Members::LINK_PAGINATION_NEXT,
140 12
            Members::LINK_PAGINATION_PREV
141
        ];
142 12
        static::assertIsValidLinksObject($json, $allowed, $strict);
143 6
    }
144
145
    /**
146
     * Asserts a json fragment is a valid primary data object.
147
     *
148
     * It will do the following checks :
149
     * 1) asserts that the primary data is either an object, an array of objects or the `null` value.
150
     * 2) if the primary data is not null, checks if it is a valid single resource or a valid resource collection
151
     * (@see assertIsValidResourceObject or @see assertIsValidResourceIdentifierObject).
152
     *
153
     * @param array|null $json
154
     * @param boolean    $strict If true, unsafe characters are not allowed when checking members name.
155
     *
156
     * @return void
157
     * @throws \PHPUnit\Framework\ExpectationFailedException
158
     */
159 48
    public static function assertIsValidPrimaryData($json, bool $strict): void
160
    {
161 48
        if ($json === null) {
162 3
            $json = [];
163
        }
164
165 48
        PHPUnit::assertIsArray(
166 48
            $json,
167 48
            Messages::PRIMARY_DATA_NOT_ARRAY
168
        );
169
170 45
        if (empty($json)) {
171 6
            return;
172
        }
173
174 39
        if (static::isArrayOfObjects($json)) {
175
            // Resource collection (Resource Objects or Resource Identifier Objects)
176 15
            static::assertIsValidPrimaryCollection($json, true, $strict);
177
178 9
            return;
179
        }
180
181
        // Single Resource (Resource Object or Resource Identifier Object)
182 24
        static::assertIsValidPrimarySingle($json, $strict);
183 18
    }
184
185
    /**
186
     * Asserts that a collection of resource object is valid.
187
     *
188
     * @param array   $list
189
     * @param boolean $checkType If true, asserts that all resources of the collection are of same type
190
     * @param boolean $strict    If true, excludes not safe characters when checking members name
191
     *
192
     * @return void
193
     * @throws \PHPUnit\Framework\ExpectationFailedException
194
     */
195 15
    private static function assertIsValidPrimaryCollection($list, bool $checkType, bool $strict): void
196
    {
197 15
        $isResourceObject = null;
198 15
        foreach ($list as $index => $resource) {
199 15
            if ($checkType) {
200
                // Assert that all resources of the collection are of same type.
201 15
                if ($index == 0) {
202 15
                    $isResourceObject = static::dataIsResourceObject($resource);
203 15
                    continue;
204
                }
205
206 15
                PHPUnit::assertEquals(
207 15
                    $isResourceObject,
208 15
                    static::dataIsResourceObject($resource),
209 15
                    Messages::PRIMARY_DATA_SAME_TYPE
210
                );
211
            }
212
213
            // Check the resource
214 12
            static::assertIsValidPrimarySingle($resource, $strict);
215
        }
216 9
    }
217
218
    /**
219
     * Assert that a single resource object is valid.
220
     *
221
     * @param array   $resource
222
     * @param boolean $strict   If true, excludes not safe characters when checking members name
223
     *
224
     * @return void
225
     * @throws \PHPUnit\Framework\ExpectationFailedException
226
     */
227 36
    private static function assertIsValidPrimarySingle($resource, bool $strict): void
228
    {
229 36
        if (static::dataIsResourceObject($resource)) {
230 27
            static::assertIsValidResourceObject($resource, $strict);
231
232 21
            return;
233
        }
234
235 9
        static::assertIsValidResourceIdentifierObject($resource, $strict);
236 6
    }
237
238
    /**
239
     * Asserts that a collection of included resources is valid.
240
     *
241
     * It will do the following checks :
242
     * 1) asserts that it is an array of objects (@see assertIsArrayOfObjects).
243
     * 2) asserts that each resource of the collection is valid (@see assertIsValidResourceObject).
244
     * 3) asserts that each resource in the collection corresponds to an existing resource linkage
245
     * present in either primary data, primary data relationships or another included resource.
246
     * 4) asserts that each resource in the collection is unique (i.e. each couple id-type is unique).
247
     *
248
     * @param array   $included The included top-level member of the json document.
249
     * @param array   $data     The primary data of the json document.
250
     * @param boolean $strict   If true, unsafe characters are not allowed when checking members name.
251
     *
252
     * @return void
253
     * @throws \PHPUnit\Framework\ExpectationFailedException
254
     */
255 21
    public static function assertIsValidIncludedCollection($included, $data, bool $strict): void
256
    {
257 21
        static::assertIsValidResourceObjectCollection($included, $strict);
258
259 12
        $resIdentifiers = array_merge(
260 12
            static::getAllResourceIdentifierObjects($data),
261 12
            static::getAllResourceIdentifierObjects($included)
262
        );
263
264 12
        $present = [];
265 12
        foreach ($included as $inc) {
266 12
            PHPUnit::assertTrue(
267 12
                self::existsInArray($inc, $resIdentifiers),
268 12
                Messages::INCLUDED_RESOURCE_NOT_LINKED
269
            );
270
271 12
            if (!isset($present[$inc[Members::TYPE]])) {
272 12
                $present[$inc[Members::TYPE]] = [];
273
            }
274 12
            PHPUnit::assertNotContains(
275 12
                $inc[Members::ID],
276 12
                $present[$inc[Members::TYPE]],
277 12
                Messages::COMPOUND_DOCUMENT_ONLY_ONE_RESOURCE
278
            );
279 12
            array_push($present[$inc[Members::TYPE]], $inc[Members::ID]);
280
        }
281 6
    }
282
283
    /**
284
     * Checks if a given json fragment is a resource object.
285
     *
286
     * @param array $resource
287
     *
288
     * @return bool
289
     */
290 39
    private static function dataIsResourceObject($resource): bool
291
    {
292
        $expected = [
293 39
            Members::ATTRIBUTES,
294 39
            Members::RELATIONSHIPS,
295 39
            Members::LINKS
296
        ];
297
298 39
        return static::containsAtLeastOneMember($expected, $resource);
299
    }
300
301
    /**
302
     * Get all the resource identifier objects (resource linkage) presents in a collection of resource.
303
     *
304
     * @param array $data
305
     *
306
     * @return array
307
     */
308 12
    private static function getAllResourceIdentifierObjects($data): array
309
    {
310 12
        $arr = [];
311 12
        if (empty($data)) {
312
            return $arr;
313
        }
314 12
        if (!static::isArrayOfObjects($data)) {
315 6
            $data = [$data];
316
        }
317 12
        foreach ($data as $obj) {
318 12
            if (!isset($obj[Members::RELATIONSHIPS])) {
319 12
                continue;
320
            }
321 12
            foreach ($obj[Members::RELATIONSHIPS] as $relationship) {
322 12
                if (!isset($relationship[Members::DATA])) {
323 3
                    continue;
324
                }
325 12
                $arr = array_merge(
326 12
                    $arr,
327 12
                    static::isArrayOfObjects($relationship[Members::DATA]) ?
328 12
                        $relationship[Members::DATA] : [$relationship[Members::DATA]]
329
                );
330
            }
331
        }
332
333 12
        return $arr;
334
    }
335
336
    /**
337
     * Checks if a resource is present in a given array.
338
     *
339
     * @param array $needle
340
     * @param array $arr
341
     *
342
     * @return bool
343
     */
344 12
    private static function existsInArray($needle, $arr): bool
345
    {
346 12
        foreach ($arr as $resIdentifier) {
347 12
            $test = $resIdentifier[Members::TYPE] === $needle[Members::TYPE]
348 12
                && $resIdentifier[Members::ID] === $needle[Members::ID];
349 12
            if ($test) {
350 12
                return true;
351
            }
352
        }
353
354 3
        return false;
355
    }
356
}
357