EmanueleMinotto /
json-schema-faker
| 1 | <?php |
||
| 2 | |||
| 3 | namespace EmanueleMinotto\JsonSchemaFaker; |
||
| 4 | |||
| 5 | use Faker\Provider\Base; |
||
| 6 | use Faker\Provider\Lorem; |
||
| 7 | |||
| 8 | class JsonSchemaProvider |
||
| 9 | { |
||
| 10 | 111 | public static function jsonSchemaContent(string $content) |
|
| 11 | { |
||
| 12 | 111 | $schema = json_decode($content, true); |
|
| 13 | |||
| 14 | 111 | return static::jsonSchema($schema); |
|
| 15 | } |
||
| 16 | |||
| 17 | 111 | public static function jsonSchema(array $schema) |
|
| 18 | { |
||
| 19 | 111 | if (!isset($schema['type']) && !empty($schema['enum'])) { |
|
| 20 | 3 | return array_rand(array_flip($schema['enum'])); |
|
| 21 | } |
||
| 22 | |||
| 23 | 111 | switch ($schema['type']) { |
|
| 24 | 111 | case 'string': |
|
| 25 | 41 | return static::fromString($schema); |
|
| 26 | 90 | case 'number': |
|
| 27 | 33 | return static::fromNumber($schema); |
|
| 28 | 69 | case 'integer': |
|
| 29 | 31 | return static::fromInteger($schema); |
|
| 30 | 66 | case 'boolean': |
|
| 31 | 9 | return static::fromBoolean($schema); |
|
| 32 | 63 | case 'array': |
|
| 33 | 27 | return static::fromArray($schema); |
|
| 34 | 39 | case 'object': |
|
| 35 | 36 | return static::fromObject($schema); |
|
| 36 | 4 | case 'null': |
|
| 37 | default: |
||
| 38 | 4 | return null; |
|
| 39 | } |
||
| 40 | } |
||
| 41 | |||
| 42 | 41 | private static function fromString(array $schema): string |
|
| 43 | { |
||
| 44 | 41 | $output = ''; |
|
| 45 | 41 | if (isset($schema['pattern'])) { |
|
| 46 | 6 | $output = Base::regexify($schema['pattern']); |
|
| 47 | } |
||
| 48 | |||
| 49 | 41 | $minLength = max($schema['minLength'] ?? 1, 1); |
|
| 50 | 41 | $maxLength = min($schema['maxLength'] ?? 100, 100); |
|
| 51 | |||
| 52 | 41 | $max = max($minLength, $maxLength); |
|
| 53 | 41 | $output = $output ?: Lorem::text(max($max, 5)); |
|
| 54 | 41 | $output = substr($output, 0, $max); |
|
| 55 | |||
| 56 | 41 | $contentMediaType = $schema['contentMediaType'] ?? 'text/plain'; |
|
| 57 | 41 | $contentMediaType = in_array($contentMediaType, ['text/plain', 'application/json']) |
|
| 58 | 41 | ? $contentMediaType |
|
| 59 | 41 | : 'text/plain'; |
|
| 60 | |||
| 61 | 41 | if ('application/json' === $contentMediaType) { |
|
| 62 | 6 | $output = json_encode($output); |
|
| 63 | } |
||
| 64 | |||
| 65 | 41 | $contentEncoding = $schema['contentEncoding'] ?? 'binary'; |
|
| 66 | 41 | $contentEncoding = in_array($contentEncoding, ['binary', 'base64']) |
|
| 67 | 41 | ? $contentEncoding |
|
| 68 | 41 | : 'binary'; |
|
| 69 | |||
| 70 | 41 | if ('base64' === $contentEncoding) { |
|
| 71 | 6 | $output = base64_encode($output); |
|
| 72 | } |
||
| 73 | |||
| 74 | 41 | return $output; |
|
| 75 | } |
||
| 76 | |||
| 77 | 59 | private static function fromNumber(array $schema): float |
|
| 78 | { |
||
| 79 | 59 | $minimum = $schema['minimum'] ?? 0; |
|
| 80 | 59 | if (isset($schema['exclusiveMinimum'])) { |
|
| 81 | 6 | if (true === $schema['exclusiveMinimum']) { |
|
| 82 | 3 | $minimum += 0.001; |
|
| 83 | } else { |
||
| 84 | 3 | $minimum = $schema['exclusiveMinimum']; |
|
| 85 | } |
||
| 86 | } |
||
| 87 | |||
| 88 | 59 | $maximum = $schema['maximum'] ?? PHP_INT_MAX; |
|
| 89 | 59 | if (isset($schema['exclusiveMaximum'])) { |
|
| 90 | 3 | if (true === $schema['exclusiveMaximum']) { |
|
| 91 | $maximum -= 0.001; |
||
| 92 | } else { |
||
| 93 | 3 | $maximum = $schema['exclusiveMaximum']; |
|
| 94 | } |
||
| 95 | } |
||
| 96 | |||
| 97 | 59 | $multipleOf = $schema['multipleOf'] ?? 1; |
|
| 98 | |||
| 99 | 59 | $output = Base::randomFloat(null, $minimum, $maximum / max($multipleOf, 1)); |
|
| 100 | 59 | if (1 !== $multipleOf) { |
|
| 101 | 6 | $output = $multipleOf * round($output); |
|
| 102 | } |
||
| 103 | |||
| 104 | 59 | return min(max($output, $minimum), $maximum); |
|
| 105 | } |
||
| 106 | |||
| 107 | 31 | private static function fromInteger(array $schema): int |
|
| 108 | { |
||
| 109 | 31 | return round(static::fromNumber($schema)); |
|
| 110 | } |
||
| 111 | |||
| 112 | 9 | private static function fromBoolean(array $schema): bool |
|
| 113 | { |
||
| 114 | 9 | return 1 === mt_rand(0, 1); |
|
| 115 | } |
||
| 116 | |||
| 117 | 27 | private static function fromArray(array $schema): array |
|
| 118 | { |
||
| 119 | 27 | $minItems = max($schema['minItems'] ?? 0, 0); |
|
| 120 | 27 | $maxItems = min($schema['maxItems'] ?? 50, 50); |
|
| 121 | 27 | $uniqueItems = $schema['uniqueItems'] ?? false; |
|
| 122 | |||
| 123 | 27 | $items = $schema['items'] ?? []; |
|
| 124 | 27 | if (!isset($items[0]) && !empty($items)) { |
|
| 125 | 6 | $items = array_fill(0, max($minItems, 1), $items); |
|
| 126 | |||
| 127 | 6 | unset($schema['additionalItems'], $schema['contains']); |
|
| 128 | } |
||
| 129 | |||
| 130 | 27 | if (isset($schema['contains'])) { |
|
| 131 | 3 | $items = array_fill(0, max($minItems, 1), $schema['contains']); |
|
| 132 | } |
||
| 133 | |||
| 134 | 27 | if (isset($schema['additionalItems'])) { |
|
| 135 | 3 | $items += array_fill(count($items), max(0, $maxItems - count($items)), $schema['additionalItems']); |
|
| 136 | } |
||
| 137 | |||
| 138 | 27 | if (empty($items)) { |
|
| 139 | 12 | $type = array_rand(array_flip(['string', 'number', 'integer'])); |
|
| 140 | 12 | $items = array_fill(0, mt_rand($minItems, $maxItems), [ |
|
| 141 | 12 | 'type' => $type, |
|
| 142 | ]); |
||
| 143 | } |
||
| 144 | |||
| 145 | 27 | $data = []; |
|
| 146 | 27 | for ($i = 0; $i < count($items); ++$i) { |
|
|
0 ignored issues
–
show
|
|||
| 147 | 26 | $value = static::jsonSchema($items[$i]); |
|
| 148 | |||
| 149 | 26 | if ($uniqueItems && in_array($value, $data, true)) { |
|
| 150 | --$i; |
||
| 151 | continue; |
||
| 152 | } |
||
| 153 | |||
| 154 | 26 | $data[] = $value; |
|
| 155 | } |
||
| 156 | |||
| 157 | 27 | return $data; |
|
| 158 | } |
||
| 159 | |||
| 160 | 36 | private static function fromObject(array $schema): \stdClass |
|
| 161 | { |
||
| 162 | 36 | $object = new \stdClass(); |
|
| 163 | |||
| 164 | 36 | if (!empty($schema['properties'])) { |
|
| 165 | 12 | foreach ($schema['properties'] as $property => $subschema) { |
|
| 166 | 12 | if (0 === mt_rand(0, 1) && !in_array($property, $schema['required'] ?? [])) { |
|
| 167 | 9 | continue; |
|
| 168 | } |
||
| 169 | |||
| 170 | 9 | $object->{$property} = !is_bool($subschema) |
|
| 171 | 5 | ? static::jsonSchema($subschema) |
|
| 172 | 6 | : static::getFromPatternProperties($property, $schema); |
|
| 173 | } |
||
| 174 | } |
||
| 175 | |||
| 176 | 36 | $minProperties = max($schema['minProperties'] ?? 0, 0); |
|
| 177 | |||
| 178 | 36 | if (0 !== $minProperties && count(get_object_vars($object)) < $minProperties) { |
|
| 179 | do { |
||
| 180 | 3 | $propertyName = isset($schema['propertyNames']) |
|
| 181 | ? static::jsonSchema($schema['propertyNames']) |
||
| 182 | 3 | : Lorem::word(); |
|
| 183 | |||
| 184 | 3 | $object->{$propertyName} = static::getFromPatternProperties($propertyName, $schema); |
|
| 185 | 3 | } while (count(get_object_vars($object)) < $minProperties); |
|
| 186 | } |
||
| 187 | |||
| 188 | 36 | if (isset($schema['maxProperties']) && count(get_object_vars($object)) < $schema['maxProperties']) { |
|
| 189 | do { |
||
| 190 | 3 | $propertyName = isset($schema['propertyNames']) |
|
| 191 | ? static::jsonSchema($schema['propertyNames']) |
||
| 192 | 3 | : Lorem::word(); |
|
| 193 | |||
| 194 | 3 | $object->{$propertyName} = static::getFromPatternProperties($propertyName, $schema); |
|
| 195 | 3 | } while (count(get_object_vars($object)) < mt_rand($minProperties, $schema['maxProperties'])); |
|
| 196 | } |
||
| 197 | |||
| 198 | 36 | foreach ($schema['required'] ?? [] as $property) { |
|
| 199 | 6 | if (!isset($object->{$property})) { |
|
| 200 | 4 | $object->{$property} = static::getFromPatternProperties($property, $schema); |
|
| 201 | } |
||
| 202 | } |
||
| 203 | |||
| 204 | 36 | return $object; |
|
| 205 | } |
||
| 206 | |||
| 207 | 13 | private static function getFromPatternProperties(string $property, array $schema) |
|
| 208 | { |
||
| 209 | 13 | if (isset($schema['patternProperties'])) { |
|
| 210 | 2 | foreach ($schema['patternProperties'] as $key => $value) { |
|
| 211 | 2 | if (preg_match('/'.$key.'/', $property)) { |
|
| 212 | return static::jsonSchema($schema); |
||
| 213 | } |
||
| 214 | } |
||
| 215 | } |
||
| 216 | |||
| 217 | 13 | if (isset($schema['additionalProperties']) && !is_bool($schema['additionalProperties'])) { |
|
| 218 | 2 | return static::jsonSchema($schema['additionalProperties']); |
|
| 219 | } |
||
| 220 | |||
| 221 | 11 | return static::fromRandom(); |
|
| 222 | } |
||
| 223 | |||
| 224 | 11 | private static function fromRandom() |
|
| 225 | { |
||
| 226 | 11 | $type = array_rand(array_flip(['string', 'number', 'integer'])); |
|
| 227 | |||
| 228 | 11 | return static::jsonSchema(['type' => $type]); |
|
| 229 | } |
||
| 230 | } |
||
| 231 |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: