| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- <?php
- /** Project-local checks for the OpenAPI schema subset used by the mini API fixtures. */
- final class MiniApiContract
- {
- public readonly array $document;
- public function __construct()
- {
- $this->document = json_decode(file_get_contents(dirname(__DIR__, 2) . '/docs/openapi/water-mini-v1.json'), true, 512, JSON_THROW_ON_ERROR);
- }
- public function data(string $name, mixed $data): bool
- {
- // Match wire JSON object/array distinctions rather than PHP associative arrays.
- $this->validate($this->document['components']['schemas'][$name], json_decode(json_encode($data, JSON_THROW_ON_ERROR)), $name);
- return true;
- }
- public function resolve(string $reference): array
- {
- if (!str_starts_with($reference, '#/')) {
- throw new RuntimeException('Only local schema references are expected: ' . $reference);
- }
- $node = $this->document;
- foreach (explode('/', substr($reference, 2)) as $part) {
- $node = $node[str_replace(['~1', '~0'], ['/', '~'], $part)] ?? null;
- if (!is_array($node)) {
- throw new RuntimeException('Unresolved reference: ' . $reference);
- }
- }
- return $node;
- }
- public function validate(array $schema, mixed $value, string $path = 'data'): void
- {
- if (isset($schema['$ref'])) {
- $this->validate($this->resolve($schema['$ref']), $value, $path);
- return;
- }
- if ($value === null && ($schema['nullable'] ?? false) && isset($schema['type'])) {
- return;
- }
- foreach ($schema['allOf'] ?? [] as $part) {
- $this->validate($part, $value, $path);
- }
- foreach (['oneOf', 'anyOf'] as $union) {
- if (!isset($schema[$union])) {
- continue;
- }
- $matches = 0;
- foreach ($schema[$union] as $part) {
- try {
- $this->validate($part, $value, $path);
- $matches++;
- } catch (RuntimeException) {
- }
- }
- if ($matches === 0 || ($union === 'oneOf' && $matches !== 1)) {
- throw new RuntimeException($path . ': invalid ' . $union);
- }
- }
- $type = $schema['type'] ?? null;
- $valid = match ($type) {
- 'object' => $value instanceof stdClass,
- 'array' => is_array($value),
- 'integer' => is_int($value),
- 'number' => is_int($value) || is_float($value),
- 'string' => is_string($value),
- 'boolean' => is_bool($value),
- null => true,
- default => false,
- };
- if (!$valid || (isset($schema['enum']) && !in_array($value, $schema['enum'], true))) {
- throw new RuntimeException($path . ': expected ' . $type);
- }
- if ($type === 'object') {
- foreach ($schema['required'] ?? [] as $key) {
- if (!property_exists($value, $key)) {
- throw new RuntimeException($path . ': missing ' . $key);
- }
- }
- foreach (get_object_vars($value) as $key => $item) {
- $child = $schema['properties'][$key] ?? $schema['additionalProperties'] ?? true;
- if ($child === false) {
- throw new RuntimeException($path . ': unexpected ' . $key);
- }
- if (is_array($child)) {
- $this->validate($child, $item, $path . '.' . $key);
- }
- }
- }
- if ($type === 'array') {
- if (count($value) < ($schema['minItems'] ?? 0) || count($value) > ($schema['maxItems'] ?? PHP_INT_MAX)) {
- throw new RuntimeException($path . ': array length');
- }
- if (($schema['uniqueItems'] ?? false) && count(array_unique(array_map('serialize', $value))) !== count($value)) {
- throw new RuntimeException($path . ': duplicate item');
- }
- foreach ($value as $index => $item) {
- $this->validate($schema['items'], $item, $path . '[' . $index . ']');
- }
- }
- if ($type === 'string') {
- if (mb_strlen($value) < ($schema['minLength'] ?? 0) || mb_strlen($value) > ($schema['maxLength'] ?? PHP_INT_MAX)
- || (isset($schema['pattern']) && !preg_match('~' . str_replace('~', '\\~', $schema['pattern']) . '~uD', $value))) {
- throw new RuntimeException($path . ': string constraint');
- }
- }
- if (($type === 'integer' || $type === 'number') && ($value < ($schema['minimum'] ?? -INF) || $value > ($schema['maximum'] ?? INF))) {
- throw new RuntimeException($path . ': numeric range');
- }
- }
- }
|