diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5a829e1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,94 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on Keep a Changelog and this project adheres to Semantic Versioning. + +## [Unreleased] + +### Breaking +- SchemaInterface now requires `toArray(): array`. + - Any custom implementations of `Blugen\\Service\\Lexicon\\SchemaInterface` must add `toArray()`. +- Namespace and layout refactor for type-specific schemas. + - Old: `Blugen\\Service\\Lexicon\\V1\\TypeSpecificSchema\\...` + - New roots under `Blugen\\Service\\Lexicon\\V1\\Schema\\...` with sub-namespaces: + - `Concrete\\*` (e.g., `StringSchema`, `IntegerSchema`, `BlobSchema`, ...) + - `Container\\*` (e.g., `ArraySchema`, `ObjectSchema`, `ParamsSchema`) + - `Meta\\*` (e.g., `RefSchema`, `UnionSchema`, `UnknownSchema`, `TokenSchema`) + - `Support\\*` (e.g., `InputSchema`, `OutputSchema`, `MessageSchema`, `ErrorsSchema`, `ErrorSchema`) + - `Primary\\*` (e.g., `QuerySchema`, `ProcedureSchema`, `RecordSchema`, `SubscriptionSchema`, `HTTPAbstract`) + - Code importing old `TypeSpecificSchema` classes will break and must be updated to the new `Schema` sub-namespaces. +- Support schema `type()`/`description()` behavior changed via `SupportSchemaTrait`. + - `type()` is no longer supported on support schemas and now throws `BadMethodCallException`. + - `description()` on some support schemas may also be unsupported (unless class overrides it). +- `InputSchema::schema()` / `OutputSchema::schema()` return raw arrays. + - In 1.x these methods returned wrapped schema objects (e.g., `ObjectSchema|UnionSchema|RefSchema|null`). + - Now they return `?array`/`array` of raw schema content. Callers must adapt accordingly. + - `InputSchema::schema()` may now be `null` when absent (previously defaulted to an empty object schema). +- `ErrorsSchema` element type and access semantics changed. + - Iteration now yields `ErrorSchema` objects (previously arrays). + - Implements read-only `ArrayAccess`; attempting to modify throws `BadMethodCallException`. Out-of-bounds reads throw `OutOfBoundsException`. +- `ObjectSchema::properties()` contract changed. + - Returns an array of schema instances built via `SchemaFactory` (previously returned `Property` objects with name/nullable/required metadata). + - Throws `V1\\Exceptions\\MissingRequiredFieldException` when `properties` is missing or not an array (was previously tolerant). + - Callers needing metadata must use `required()`/`nullable()` arrays alongside property keys. +- Legacy classes removed: + - `V1/TypeSpecificSchema/Support/{ErrorsSchema,InputSchema,MessageSchema,OutputSchema}` + - `V1/TypeSpecificSchema/Field/ObjectSchema` + +### Added +- Primary schemas: `Schema\\Primary\\{QuerySchema, ProcedureSchema, RecordSchema, SubscriptionSchema}` and base `HTTPAbstract`. +- Traits to unify behavior and DRY: + - `V1\\Traits\\{ArrayableTrait, SchemaTrait, RawSchemaAccessorTrait, SupportSchemaTrait, DefinitionTrait}` +- `V1\\Schema\\Support\\ErrorSchema` and revamped `ErrorsSchema` (iterable, read-only `ArrayAccess`). +- `V1\\Exceptions\\MissingRequiredFieldException` for strict schema validation (e.g., object properties presence/type). + +### Changed +- Moved all type-specific schemas to `V1\\Schema\\{Concrete, Container, Meta, Support, Primary}` per their roles. +- `SchemaFactory` now creates from new `V1\\Schema\\{Concrete,Container,Meta}` classes; dependent generators updated accordingly. +- IO support schemas share a common `IOAbstract` (unified `description()`, `encoding()`, `schema()`, `parameters()`, `toArray()`). +- `OutputSchema`: `schema` field is optional and may be `null` when not present. + +### Fixed +- Treat `schema` as optional for Input/Output; tests and implementations updated accordingly. +- Resolved issues in `ObjectSchema` behavior and offset/exception handling in support schemas. + +### Removed +- Old `TypeSpecificSchema` support classes and the field `ObjectSchema` (replaced by `Schema\\Container\\ObjectSchema`). + +### Migration Guide +- Implementations of `SchemaInterface`: add `public function toArray(): array`. +- Update imports from `...\\V1\\TypeSpecificSchema\\Field\\*` to the new locations. Examples: + - `...\\TypeSpecificSchema\\Field\\StringSchema` → `...\\Schema\\Concrete\\StringSchema` + - `...\\TypeSpecificSchema\\Field\\ObjectSchema` → `...\\Schema\\Container\\ObjectSchema` + - `...\\TypeSpecificSchema\\Field\\RefSchema` → `...\\Schema\\Meta\\RefSchema` + - `...\\TypeSpecificSchema\\Support\\InputSchema` → `...\\Schema\\Support\\InputSchema` +- Support schema `type()` calls: remove/avoid calling `type()` on support schemas or guard with `method_exists`/try-catch. +- Input/Output/Message `schema()` usage: + - Previously: `$schemaObj = $output->schema(); $type = $schemaObj?->type();` + - Now: `$raw = $output->schema(); $type = $raw['type'] ?? null;` or wrap via `new V1\\Schema($raw)` and/or `SchemaFactory::create($raw['type'], $raw)`. +- `ErrorsSchema` iteration and access: + - Previously arrays: `$name = $errors[0]['name'];` + - Now objects: `$name = $errors[0]->name(); $desc = $errors[0]->description();` + - Collection is read-only; do not modify via array offsets. +- `ObjectSchema::properties()`: + - Previously returned `Property` objects with `name/nullable/required`. + - Now returns schema instances only. Use `required()`/`nullable()` and your properties keys to compute metadata. + - Handle `MissingRequiredFieldException` whenever reading properties of an object schema. + +## [1.0.0-alpha.1] - 2025-07-26 + +### Added +- First 1.x pre-release tag for the library. +- CLI `blugen` with `generate` and `clear` commands. +- Lexicon V1 foundation: reader, generator, definition and schema interfaces. +- Type-specific schemas under `V1\TypeSpecificSchema\{Field,Support}` and supporting `SchemaFactory`. +- Basic syntax validators for string, integer, boolean, array, and null types. +- XRPC client pieces: `Client`, `SessionManager`, encoders and helpers. + +### Changed +- Release automation and workflow adjustments for publishing the 1.x pre-release. + +--- +[Unreleased]: https://github.com/corebranch/blugen/compare/1.0.0-alpha.1...1.x +[1.0.0-alpha.1]: https://github.com/corebranch/blugen/releases/tag/1.0.0-alpha.1 diff --git a/src/Service/Lexicon/SchemaInterface.php b/src/Service/Lexicon/SchemaInterface.php index a8f7f02..3155606 100644 --- a/src/Service/Lexicon/SchemaInterface.php +++ b/src/Service/Lexicon/SchemaInterface.php @@ -7,4 +7,5 @@ interface SchemaInterface public function type(): string; public function description(): ?string; public function __get(string $name): mixed; + public function toArray(): array; } diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/ArrayComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/ArrayComponentGenerator.php index d9b00ee..c9d7e36 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/ArrayComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/ArrayComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ArraySchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ArraySchema; use Nette\PhpGenerator\ClassType; class ArrayComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/BlobComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/BlobComponentGenerator.php index aa2e4be..0fee018 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/BlobComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/BlobComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BlobSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BlobSchema; use Nette\PhpGenerator\ClassType; class BlobComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/BooleanComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/BooleanComponentGenerator.php index 43b3e3d..6a7f99c 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/BooleanComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/BooleanComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BooleanSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BooleanSchema; use Nette\PhpGenerator\ClassType; class BooleanComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/BytesComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/BytesComponentGenerator.php index c953f2d..e464181 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/BytesComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/BytesComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BytesSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BytesSchema; use Nette\PhpGenerator\ClassType; class BytesComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/CidLinkComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/CidLinkComponentGenerator.php index 0caf124..d33d16e 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/CidLinkComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/CidLinkComponentGenerator.php @@ -7,7 +7,6 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\CidLinkSchema; use Nette\PhpGenerator\ClassType; class CidLinkComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/IntegerComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/IntegerComponentGenerator.php index 6c0362d..5998220 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/IntegerComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/IntegerComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\IntegerSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\IntegerSchema; use Nette\PhpGenerator\ClassType; class IntegerComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/NullComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/NullComponentGenerator.php index 9e5c79b..a712446 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/NullComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/NullComponentGenerator.php @@ -4,7 +4,6 @@ use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\NullSchema; use Nette\PhpGenerator\ClassType; class NullComponentGenerator implements GeneratorInterface diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/ObjectComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/ObjectComponentGenerator.php index db29c75..d4aad1f 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/ObjectComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/ObjectComponentGenerator.php @@ -3,9 +3,11 @@ namespace Blugen\Service\Lexicon\V1\ComponentGenerator\Field; use Blugen\Service\Lexicon\GeneratorInterface; +use Blugen\Service\Lexicon\V1\Exceptions\MissingRequiredFieldException; use Blugen\Service\Lexicon\V1\Factory\ComponentGeneratorFactory; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ObjectSchema; +use Blugen\Service\Lexicon\V1\Schema; +use Blugen\Service\Lexicon\V1\Schema\Container\ObjectSchema; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Literal; @@ -22,7 +24,23 @@ public function generate(): void $anonClass = new ClassType(null); $objectSchema = new ObjectSchema($this->property->schema()); - foreach ($objectSchema->properties() as $childProperty) { + try { + $properties = $objectSchema->properties(); + } catch (MissingRequiredFieldException) { + $properties = []; + } + + $nullable = $objectSchema->nullable() ?? []; + $required = $objectSchema->required() ?? []; + + foreach ($properties as $childPropertyName => $childProperty) { + $childProperty = new Property( + $childPropertyName, + new Schema($childProperty), + in_array($childPropertyName, $nullable, true), + in_array($childPropertyName, $required, true), + ); + ComponentGeneratorFactory::create($anonClass, $childProperty)->generate(); } diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/ParamsComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/ParamsComponentGenerator.php index e248d5a..921bb63 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/ParamsComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/ParamsComponentGenerator.php @@ -2,12 +2,10 @@ namespace Blugen\Service\Lexicon\V1\ComponentGenerator\Field; -use Blugen\Service\Lexicon\ArraySerialization\ArrayField; -use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Factory\ComponentGeneratorFactory; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ParamsSchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ParamsSchema; use Nette\PhpGenerator\ClassType; use Nette\PhpGenerator\Literal; diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/RefComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/RefComponentGenerator.php index 1907434..3dd8c5e 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/RefComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/RefComponentGenerator.php @@ -10,7 +10,7 @@ use Blugen\Service\Lexicon\V1\Property; use Blugen\Service\Lexicon\V1\Resolver\NamespaceResolver; use Blugen\Service\Lexicon\V1\Resolver\NsidResolver; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\RefSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\RefSchema; use Nette\PhpGenerator\ClassType; class RefComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/StringComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/StringComponentGenerator.php index 439ab60..597a2ad 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/StringComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/StringComponentGenerator.php @@ -7,7 +7,7 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\StringSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\StringSchema; use Nette\PhpGenerator\ClassType; class StringComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/TokenComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/TokenComponentGenerator.php index 971d634..e0416eb 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/TokenComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/TokenComponentGenerator.php @@ -4,7 +4,6 @@ use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\TokenSchema; use Nette\PhpGenerator\ClassType; class TokenComponentGenerator implements GeneratorInterface diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/UnionComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/UnionComponentGenerator.php index b19be28..33d1ff8 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/UnionComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/UnionComponentGenerator.php @@ -9,9 +9,8 @@ use Blugen\Service\Lexicon\V1\Property; use Blugen\Service\Lexicon\V1\Resolver\NamespaceResolver; use Blugen\Service\Lexicon\V1\Resolver\NsidResolver; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnionSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnionSchema; use Nette\PhpGenerator\ClassType; -use Nette\PhpGenerator\Literal; class UnionComponentGenerator implements GeneratorInterface, ArraySerializationContributor { diff --git a/src/Service/Lexicon/V1/ComponentGenerator/Field/UnknownComponentGenerator.php b/src/Service/Lexicon/V1/ComponentGenerator/Field/UnknownComponentGenerator.php index d991d1f..023cf39 100644 --- a/src/Service/Lexicon/V1/ComponentGenerator/Field/UnknownComponentGenerator.php +++ b/src/Service/Lexicon/V1/ComponentGenerator/Field/UnknownComponentGenerator.php @@ -7,7 +7,6 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContributor; use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\V1\Property; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnknownSchema; use Nette\PhpGenerator\ClassType; class UnknownComponentGenerator implements GeneratorInterface, ArraySerializationContributor diff --git a/src/Service/Lexicon/V1/DefGenerator/Primary/ProcedureGenerator.php b/src/Service/Lexicon/V1/DefGenerator/Primary/ProcedureGenerator.php index cecf79c..277f7cc 100644 --- a/src/Service/Lexicon/V1/DefGenerator/Primary/ProcedureGenerator.php +++ b/src/Service/Lexicon/V1/DefGenerator/Primary/ProcedureGenerator.php @@ -6,14 +6,17 @@ use Blugen\Service\Lexicon\GeneratorInterface; use Blugen\Service\Lexicon\InputInterface; use Blugen\Service\Lexicon\ProcedureInterface; -use Blugen\Service\Lexicon\V1\ComponentGenerator\Field\RefComponentGenerator; +use Blugen\Service\Lexicon\V1\Exceptions\MissingRequiredFieldException; use Blugen\Service\Lexicon\V1\Factory\ComponentGeneratorFactory; +use Blugen\Service\Lexicon\V1\Property; use Blugen\Service\Lexicon\V1\Resolver\NamespaceResolver; use Blugen\Service\Lexicon\V1\Resolver\NsidResolver; +use Blugen\Service\Lexicon\V1\Schema; +use Blugen\Service\Lexicon\V1\Schema\Container\ObjectSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\RefSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnionSchema; use Blugen\Service\Lexicon\V1\TypeSpecificDefinition\Primary\ProcedureTypeDefinition; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ObjectSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\RefSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnionSchema; +use Blugen\Service\Syntax\Factory\SchemaFactory; use Blugen\Service\Xrpc\CallableInterface; use Blugen\Service\Xrpc\Encoder\Encoder; use Nette\PhpGenerator\ClassType; @@ -47,7 +50,12 @@ public function generate(): array $this->class->addImplement(ProcedureInterface::class); - $schema = $this->definition->input()?->schema(); + $schemaArr = $this->definition->input()?->schema(); + $schema = null; + + if (! empty($schemaArr)) { + $schema = container()->get(SchemaFactory::class)::create($schemaArr['type'], $schemaArr); + } if ($schema instanceof UnionSchema) { $refs = array_map(function (string $ref) { @@ -75,7 +83,23 @@ public function generate(): array $schemaClass = $schemaPhpNamespace->addClass($schemaClassName); $schemaClass->addImplement(InputInterface::class); - foreach($schema->properties() as $property) { + try { + $properties = $schema->properties(); + } catch (MissingRequiredFieldException) { + $properties = []; + } + + $nullable = $schema->nullable() ?? []; + $required = $schema->required() ?? []; + + foreach($properties as $propertyName => $property) { + $property = new Property( + $propertyName, + new Schema($property->toArray()), + in_array($property, $nullable, true), + in_array($property, $required, true) + ); + ComponentGeneratorFactory::create($schemaClass, $property, $this->definition->lexicon())->generate(); } diff --git a/src/Service/Lexicon/V1/DefGenerator/Primary/RecordGenerator.php b/src/Service/Lexicon/V1/DefGenerator/Primary/RecordGenerator.php index 02d8cd9..2f2d115 100644 --- a/src/Service/Lexicon/V1/DefGenerator/Primary/RecordGenerator.php +++ b/src/Service/Lexicon/V1/DefGenerator/Primary/RecordGenerator.php @@ -7,8 +7,11 @@ use Blugen\Service\Lexicon\ArraySerialization\ArraySerializationContext; use Blugen\Service\Lexicon\ArraySerialization\BuildsArraySerialization; use Blugen\Service\Lexicon\GeneratorInterface; +use Blugen\Service\Lexicon\V1\Exceptions\MissingRequiredFieldException; use Blugen\Service\Lexicon\V1\Factory\ComponentGeneratorFactory; +use Blugen\Service\Lexicon\V1\Property; use Blugen\Service\Lexicon\V1\Resolver\NamespaceResolver; +use Blugen\Service\Lexicon\V1\Schema; use Blugen\Service\Lexicon\V1\TypeSpecificDefinition\Primary\RecordTypeDefinition; use JsonSerializable; use Nette\InvalidArgumentException; @@ -49,7 +52,22 @@ public function generate(): string $this->class->addImplement(ArraySerializable::class) ->addImplement(JsonSerializable::class); - foreach ($this->definition->record()->properties() as $property) { + try { + $properties = $this->definition->record()->properties(); + } catch (MissingRequiredFieldException) { + $properties = []; + } + + $nullable = $this->definition->record()->nullable() ?? []; + $required = $this->definition->record()->required() ?? []; + + foreach ($properties as $propertyName => $property) { + $property = new Property( + $propertyName, + new Schema($property->toArray()), + in_array($propertyName, $nullable, true), + in_array($propertyName, $required, true) + ); ComponentGeneratorFactory::create($this->class, $property, $this->definition->lexicon(), $this)->generate(); } diff --git a/src/Service/Lexicon/V1/Definition.php b/src/Service/Lexicon/V1/Definition.php index 7efa3a1..a239af6 100644 --- a/src/Service/Lexicon/V1/Definition.php +++ b/src/Service/Lexicon/V1/Definition.php @@ -64,4 +64,15 @@ public function __get(string $name): mixed return $def; } + + public function toArray(): array + { + return array_filter($this->schema(), static fn ($value) => $value !== null); + } + + public function schema(): array + { + $defs = $this->lexicon->defs(); + return $defs[$this->name] ?? []; + } } diff --git a/src/Service/Lexicon/V1/Exceptions/MissingRequiredFieldException.php b/src/Service/Lexicon/V1/Exceptions/MissingRequiredFieldException.php new file mode 100644 index 0000000..d35548f --- /dev/null +++ b/src/Service/Lexicon/V1/Exceptions/MissingRequiredFieldException.php @@ -0,0 +1,9 @@ +schema['type'])) { + throw new MissingRequiredFieldException("Missing the required string property 'properties'"); + } + return $this->schema['type']; } diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Field/BlobSchema.php b/src/Service/Lexicon/V1/Schema/Concrete/BlobSchema.php similarity index 74% rename from src/Service/Lexicon/V1/TypeSpecificSchema/Field/BlobSchema.php rename to src/Service/Lexicon/V1/Schema/Concrete/BlobSchema.php index cbbaab1..707aadc 100644 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Field/BlobSchema.php +++ b/src/Service/Lexicon/V1/Schema/Concrete/BlobSchema.php @@ -1,11 +1,16 @@ factory ??= container()->get(SchemaFactory::class); + } + + /** + * @throws MissingRequiredFieldException + */ + public function properties(): array + { + if (! is_array($properties = $this->__get('properties'))) { + throw new MissingRequiredFieldException("Missing the required array property 'properties'"); + } + + return array_map( + fn (array $schemaContent) => $this->factory::create($schemaContent['type'], $schemaContent), + $properties + ); + } + + public function required(): ?array + { + return $this->__get('required'); + } + + public function nullable(): ?array + { + return $this->__get('nullable'); + } +} diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Field/ParamsSchema.php b/src/Service/Lexicon/V1/Schema/Container/ParamsSchema.php similarity index 83% rename from src/Service/Lexicon/V1/TypeSpecificSchema/Field/ParamsSchema.php rename to src/Service/Lexicon/V1/Schema/Container/ParamsSchema.php index a300ca1..e170877 100644 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Field/ParamsSchema.php +++ b/src/Service/Lexicon/V1/Schema/Container/ParamsSchema.php @@ -1,13 +1,18 @@ __get('parameters'))) { + return null; + } + + return new ParamsSchema(new Schema($schemaContent)); + } + + public function errors(): ?ErrorsSchema + { + if (is_null($schemaContent = $this->__get('errors'))) { + return null; + } + + return new ErrorsSchema(new Schema($schemaContent)); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Primary/ProcedureSchema.php b/src/Service/Lexicon/V1/Schema/Primary/ProcedureSchema.php new file mode 100644 index 0000000..91360aa --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Primary/ProcedureSchema.php @@ -0,0 +1,31 @@ +__get('input'); + + if (is_null($schemaContent)) { + return null; + } + + return new InputSchema(new Schema($schemaContent)); + } + + public function output(): ?OutputSchema + { + if (is_null($schemaContent = $this->__get('output'))) { + return null; + } + + return new OutputSchema(new Schema($schemaContent)); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Primary/QuerySchema.php b/src/Service/Lexicon/V1/Schema/Primary/QuerySchema.php new file mode 100644 index 0000000..ae223be --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Primary/QuerySchema.php @@ -0,0 +1,19 @@ +__get('output'))) { + return null; + } + + return new OutputSchema(new Schema($schemaContent)); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Primary/RecordSchema.php b/src/Service/Lexicon/V1/Schema/Primary/RecordSchema.php new file mode 100644 index 0000000..0750fdc --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Primary/RecordSchema.php @@ -0,0 +1,31 @@ +__get('key'); + } + + public function record(): ObjectSchema + { + return new ObjectSchema(new Schema($this->__get('record'))); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Primary/SubscriptionSchema.php b/src/Service/Lexicon/V1/Schema/Primary/SubscriptionSchema.php new file mode 100644 index 0000000..acfd8cd --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Primary/SubscriptionSchema.php @@ -0,0 +1,19 @@ +__get('message'))) { + return null; + } + + return new MessageSchema(new Schema($schemaContent)); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Support/ErrorSchema.php b/src/Service/Lexicon/V1/Schema/Support/ErrorSchema.php new file mode 100644 index 0000000..325d523 --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Support/ErrorSchema.php @@ -0,0 +1,35 @@ +__get('name'); + } + + public function description(): ?string + { + return $this->__get('description'); + } + + public function __get(string $name): mixed + { + return $this->schema->__get($name); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Support/ErrorsSchema.php b/src/Service/Lexicon/V1/Schema/Support/ErrorsSchema.php new file mode 100644 index 0000000..253e9d5 --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Support/ErrorsSchema.php @@ -0,0 +1,71 @@ +errors = array_map( + fn(array $content) => new ErrorSchema(new Schema($content)), + $this->toArray() + ); + } + + public function __get(string $name): mixed + { + return $this->schema->__get($name); + } + + /** @return ArrayIterator */ + public function getIterator(): \ArrayIterator + { + return new ArrayIterator($this->errors); + } + + public function count(): int + { + return count($this->errors); + } + + public function offsetExists(mixed $offset): bool + { + return isset($this->errors[$offset]); + } + + public function offsetGet(mixed $offset): ErrorSchema + { + if (!array_key_exists($offset, $this->errors)) { + throw new \OutOfBoundsException("Error schema offset '{$offset}' not found"); + } + + return $this->errors[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + throw new BadMethodCallException('Schema is read-only'); + } + + public function offsetUnset(mixed $offset): void + { + throw new BadMethodCallException('Schema is read-only'); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Support/IOAbstract.php b/src/Service/Lexicon/V1/Schema/Support/IOAbstract.php new file mode 100644 index 0000000..a5f5d89 --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Support/IOAbstract.php @@ -0,0 +1,49 @@ +schema->__get($name); + } + + public function description(): ?string + { + return $this->__get('description'); + } + + public function encoding(): string + { + return $this->__get('encoding'); + } + + public function schema(): ?array + { + return $this->__get('schema'); + } + + public function parameters(): ?array + { + return $this->__get('parameters'); + } + + public function toArray(): array + { + return $this->schema->toArray(); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Support/InputSchema.php b/src/Service/Lexicon/V1/Schema/Support/InputSchema.php new file mode 100644 index 0000000..429575a --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Support/InputSchema.php @@ -0,0 +1,9 @@ +schema->__get($name); + } + + public function schema(): UnionSchema + { + return new UnionSchema(new Schema($this->__get('schema'))); + } + + public function toArray(): array + { + return $this->schema->toArray(); + } +} diff --git a/src/Service/Lexicon/V1/Schema/Support/OutputSchema.php b/src/Service/Lexicon/V1/Schema/Support/OutputSchema.php new file mode 100644 index 0000000..cc06a94 --- /dev/null +++ b/src/Service/Lexicon/V1/Schema/Support/OutputSchema.php @@ -0,0 +1,9 @@ +schema(), static fn ($value) => $value !== null); + } +} diff --git a/src/Service/Lexicon/V1/Traits/DefinitionTrait.php b/src/Service/Lexicon/V1/Traits/DefinitionTrait.php new file mode 100644 index 0000000..5f9957d --- /dev/null +++ b/src/Service/Lexicon/V1/Traits/DefinitionTrait.php @@ -0,0 +1,21 @@ +definition instanceof DefinitionInterface) { + return $this->definition->schema(); + } + + throw new \LogicException(sprintf( + "%s::\$definition must implement %s", + self::class, + DefinitionInterface::class + )); + } +} diff --git a/src/Service/Lexicon/V1/Traits/RawSchemaAccessorTrait.php b/src/Service/Lexicon/V1/Traits/RawSchemaAccessorTrait.php new file mode 100644 index 0000000..634c7b8 --- /dev/null +++ b/src/Service/Lexicon/V1/Traits/RawSchemaAccessorTrait.php @@ -0,0 +1,27 @@ +schema instanceof SchemaInterface) { + return $this->schema->schema(); + } + + if ($propertyExists && is_array($this->schema)) { + return $this->schema; + } + + throw new \LogicException(sprintf( + "%s::\$schema must be an array or implement %s", + self::class, + SchemaInterface::class + )); + } +} diff --git a/src/Service/Lexicon/V1/Traits/SchemaTrait.php b/src/Service/Lexicon/V1/Traits/SchemaTrait.php new file mode 100644 index 0000000..db8960e --- /dev/null +++ b/src/Service/Lexicon/V1/Traits/SchemaTrait.php @@ -0,0 +1,21 @@ +schema->type(); + } + + public function description(): ?string + { + return $this->schema->description(); + } + + public function __get(string $name): mixed + { + return $this->schema->__get($name); + } +} diff --git a/src/Service/Lexicon/V1/Traits/SupportSchemaTrait.php b/src/Service/Lexicon/V1/Traits/SupportSchemaTrait.php new file mode 100644 index 0000000..182e250 --- /dev/null +++ b/src/Service/Lexicon/V1/Traits/SupportSchemaTrait.php @@ -0,0 +1,24 @@ +schema->type(); - } - - public function description(): ?string - { - return $this->schema->description() ?? null; - } - - public function __get(string $name): mixed - { - return $this->schema->__get($name); - } - - public function properties(): array - { - $nullable = $this->nullable() ?? []; - $required = $this->required() ?? []; - $properties = $this->__get('properties') ?? []; - - return array_map(fn (string $name, array $rawSchema) => new Property( - $name, - new Schema($rawSchema), - in_array($name, $nullable, true), - in_array($name, $required, true), - ), array_keys($properties), $properties); - } - - public function required(): ?array - { - return $this->__get('required'); - } - - public function nullable(): ?array - { - return $this->__get('nullable'); - } -} diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/ErrorsSchema.php b/src/Service/Lexicon/V1/TypeSpecificSchema/Support/ErrorsSchema.php deleted file mode 100644 index d9e63ca..0000000 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/ErrorsSchema.php +++ /dev/null @@ -1,53 +0,0 @@ -errors = []; - - $rawErrors = $this->schema->__get('errors') ?? []; - - foreach ($rawErrors as $error) { - $this->errors[] = $error; - } - } - - public function type(): string - { - return SupportTypeEnum::ERRORS->value; - } - - public function description(): ?string - { - return null; - } - - public function __get(string $name): mixed - { - return $this->schema->__get($name); - } - - /** - * @return ArrayIterator - */ - public function getIterator(): \Traversable - { - return new ArrayIterator($this->errors); - } - - public function count(): int - { - return count($this->errors); - } -} diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/InputSchema.php b/src/Service/Lexicon/V1/TypeSpecificSchema/Support/InputSchema.php deleted file mode 100644 index 99cc5d8..0000000 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/InputSchema.php +++ /dev/null @@ -1,56 +0,0 @@ -value; - } - - public function description(): ?string - { - return null; - } - - public function __get(string $name): mixed - { - return $this->schema->__get($name); - } - - public function encoding(): string - { - return $this->__get('encoding'); - } - - public function schema(): ObjectSchema|RefSchema|UnionSchema|null - { - $schema = $this->__get('schema'); - - if ($schema) { - $schema = new Schema($schema); - } else { - return new ObjectSchema(new Schema([])); - } - - $class = match ($schema->type()) { - 'object' => ObjectSchema::class, - 'union' => UnionSchema::class, - 'ref' => RefSchema::class, - }; - - return new $class($schema); - } -} diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/MessageSchema.php b/src/Service/Lexicon/V1/TypeSpecificSchema/Support/MessageSchema.php deleted file mode 100644 index b273813..0000000 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/MessageSchema.php +++ /dev/null @@ -1,37 +0,0 @@ -value; - } - - public function description(): ?string - { - return $this->schema->description() ?? null; - } - - public function __get(string $name): mixed - { - return $this->schema->__get($name); - } - - public function schema(): UnionSchema - { - /** @var array $schema */ - $schema = $this->__get('schema'); - return new UnionSchema(new Schema($schema)); - } -} diff --git a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/OutputSchema.php b/src/Service/Lexicon/V1/TypeSpecificSchema/Support/OutputSchema.php deleted file mode 100644 index 79d1f0c..0000000 --- a/src/Service/Lexicon/V1/TypeSpecificSchema/Support/OutputSchema.php +++ /dev/null @@ -1,55 +0,0 @@ -value; - } - - public function description(): ?string - { - return $this->schema->description(); - } - - public function __get(string $name): mixed - { - return $this->schema->__get($name); - } - - public function encoding(): string - { - return $this->__get('encoding'); - } - - public function schema(): ObjectSchema|RefSchema|UnionSchema|null - { - $schema = $this->__get('schema'); - - if ($schema) { - $schema = new Schema($schema); - } else { - return null; - } - - $class = match ($schema->type()) { - 'object' => ObjectSchema::class, - 'union' => UnionSchema::class, - 'ref' => RefSchema::class, - }; - - return new $class($schema); - } -} diff --git a/src/Service/Syntax/Constraints/ArrayType/ArrayTypeValidator.php b/src/Service/Syntax/Constraints/ArrayType/ArrayTypeValidator.php index 2d0cdf2..4c8c0f8 100644 --- a/src/Service/Syntax/Constraints/ArrayType/ArrayTypeValidator.php +++ b/src/Service/Syntax/Constraints/ArrayType/ArrayTypeValidator.php @@ -4,7 +4,7 @@ use Blugen\Service\Lexicon\SchemaInterface; use Blugen\Service\Lexicon\V1\Schema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ArraySchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ArraySchema; use Blugen\Service\Syntax\Constraints\LexConstraint; use Blugen\Service\Syntax\Factory\ConstraintFactory; use Blugen\Service\Syntax\Factory\SchemaFactory; diff --git a/src/Service/Syntax/Constraints/BooleanType/BooleanTypeValidator.php b/src/Service/Syntax/Constraints/BooleanType/BooleanTypeValidator.php index 7be8835..46adc1a 100644 --- a/src/Service/Syntax/Constraints/BooleanType/BooleanTypeValidator.php +++ b/src/Service/Syntax/Constraints/BooleanType/BooleanTypeValidator.php @@ -3,7 +3,7 @@ namespace Blugen\Service\Syntax\Constraints\BooleanType; use Blugen\Service\Lexicon\V1\Schema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BooleanSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BooleanSchema; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; diff --git a/src/Service/Syntax/Constraints/IntegerType/IntegerTypeValidator.php b/src/Service/Syntax/Constraints/IntegerType/IntegerTypeValidator.php index 2ae1ca7..54aebcf 100644 --- a/src/Service/Syntax/Constraints/IntegerType/IntegerTypeValidator.php +++ b/src/Service/Syntax/Constraints/IntegerType/IntegerTypeValidator.php @@ -3,7 +3,7 @@ namespace Blugen\Service\Syntax\Constraints\IntegerType; use Blugen\Service\Lexicon\V1\Schema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\IntegerSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\IntegerSchema; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; diff --git a/src/Service/Syntax/Constraints/StringType/StringTypeValidator.php b/src/Service/Syntax/Constraints/StringType/StringTypeValidator.php index 4777f61..6935155 100644 --- a/src/Service/Syntax/Constraints/StringType/StringTypeValidator.php +++ b/src/Service/Syntax/Constraints/StringType/StringTypeValidator.php @@ -3,7 +3,7 @@ namespace Blugen\Service\Syntax\Constraints\StringType; use Blugen\Service\Lexicon\V1\Schema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\StringSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\StringSchema; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; diff --git a/src/Service/Syntax/Factory/SchemaFactory.php b/src/Service/Syntax/Factory/SchemaFactory.php index 571e081..02816ca 100644 --- a/src/Service/Syntax/Factory/SchemaFactory.php +++ b/src/Service/Syntax/Factory/SchemaFactory.php @@ -4,19 +4,19 @@ use Blugen\Service\Lexicon\SchemaInterface; use Blugen\Service\Lexicon\V1\Schema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ArraySchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BlobSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BooleanSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BytesSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\CidLinkSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\IntegerSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\NullSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ObjectSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\RefSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\StringSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\TokenSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnionSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnknownSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BlobSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BooleanSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BytesSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\CidLinkSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\IntegerSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\NullSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\StringSchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ArraySchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ObjectSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\RefSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\TokenSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnionSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnknownSchema; class SchemaFactory { diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BlobSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BlobSchemaTest.php new file mode 100644 index 0000000..2347c06 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BlobSchemaTest.php @@ -0,0 +1,82 @@ + 'blob', + 'description' => 'Binary file', + 'accept' => ['image/png', 'image/jpeg'], + 'maxSize' => 1024, + 'nullable' => null, + ]; + + $blob = $this->make($schema); + + $this->assertSame('blob', $blob->type()); + $this->assertSame('Binary file', $blob->description()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'blob', + 'description' => 'Binary file', + 'accept' => ['image/png', 'image/jpeg'], + 'maxSize' => 1024, + ], $blob->toArray()); + } + + public function test_accept_and_max_size_accessors(): void + { + $schema = [ + 'type' => 'blob', + 'accept' => ['application/pdf'], + 'maxSize' => 2048, + ]; + + $blob = $this->make($schema); + + $this->assertSame(['application/pdf'], $blob->accept()); + $this->assertSame(2048, $blob->maxSize()); + // dotted access via underlying Schema::__get + $this->assertSame('application/pdf', $blob->__get('accept.0')); + } + + public function test_accept_and_max_size_nulls(): void + { + $schema = [ + 'type' => 'blob', + 'accept' => null, + 'maxSize' => null, + ]; + + $blob = $this->make($schema); + + $this->assertNull($blob->accept()); + $this->assertNull($blob->maxSize()); + $this->assertSame(['type' => 'blob'], $blob->toArray()); + } + + public function test_schema_returns_underlying_array(): void + { + $schema = [ + 'type' => 'blob', + 'description' => 'X', + ]; + + $blob = $this->make($schema); + $this->assertSame($schema, $blob->schema()); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BooleanSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BooleanSchemaTest.php new file mode 100644 index 0000000..f879087 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BooleanSchemaTest.php @@ -0,0 +1,77 @@ + 'boolean', + 'description' => 'A boolean flag', + 'default' => true, + 'const' => null, + ]; + + $bool = $this->make($schema); + + $this->assertSame('boolean', $bool->type()); + $this->assertSame('A boolean flag', $bool->description()); + + // toArray filters only top-level nulls (const) + $this->assertSame([ + 'type' => 'boolean', + 'description' => 'A boolean flag', + 'default' => true, + ], $bool->toArray()); + } + + public function test_default_and_const_accessors(): void + { + $schema = [ + 'type' => 'boolean', + 'default' => false, + 'const' => true, + ]; + + $bool = $this->make($schema); + + $this->assertFalse($bool->default()); + $this->assertTrue($bool->const()); + } + + public function test_accessors_return_null_when_not_set(): void + { + $schema = [ + 'type' => 'boolean', + ]; + + $bool = $this->make($schema); + + $this->assertNull($bool->default()); + $this->assertNull($bool->const()); + $this->assertSame(['type' => 'boolean'], $bool->toArray()); + } + + public function test_magic_get_supports_unknown_paths(): void + { + $schema = [ + 'type' => 'boolean', + 'meta' => ['note' => 'x'], + ]; + + $bool = $this->make($schema); + $this->assertSame('x', $bool->__get('meta.note')); + $this->assertNull($bool->__get('meta.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BytesSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BytesSchemaTest.php new file mode 100644 index 0000000..4981ea7 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/BytesSchemaTest.php @@ -0,0 +1,78 @@ + 'bytes', + 'description' => 'Opaque bytes', + 'minLength' => 2, + 'maxLength' => null, + ]; + + $bytes = $this->make($schema); + + $this->assertSame('bytes', $bytes->type()); + $this->assertSame('Opaque bytes', $bytes->description()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'bytes', + 'description' => 'Opaque bytes', + 'minLength' => 2, + ], $bytes->toArray()); + } + + public function test_min_max_length_accessors(): void + { + $schema = [ + 'type' => 'bytes', + 'minLength' => 0, + 'maxLength' => 4096, + ]; + + $bytes = $this->make($schema); + + $this->assertSame(0, $bytes->minLength()); + $this->assertSame(4096, $bytes->maxLength()); + } + + public function test_min_max_length_nulls(): void + { + $schema = [ + 'type' => 'bytes', + ]; + + $bytes = $this->make($schema); + + $this->assertNull($bytes->minLength()); + $this->assertNull($bytes->maxLength()); + $this->assertSame(['type' => 'bytes'], $bytes->toArray()); + } + + public function test_magic_get_dotted_path_and_schema(): void + { + $schema = [ + 'type' => 'bytes', + 'meta' => ['encoding' => ['name' => 'base64url']], + ]; + + $bytes = $this->make($schema); + $this->assertSame('base64url', $bytes->__get('meta.encoding.name')); + $this->assertNull($bytes->__get('meta.encoding.unknown')); + $this->assertSame($schema, $bytes->schema()); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/CidLinkSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/CidLinkSchemaTest.php new file mode 100644 index 0000000..b2b9a9a --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/CidLinkSchemaTest.php @@ -0,0 +1,64 @@ + 'cid-link', + 'description' => 'CID link field', + 'optional' => null, + ]; + + $cid = $this->make($schema); + + $this->assertSame('cid-link', $cid->type()); + $this->assertSame('CID link field', $cid->description()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'cid-link', + 'description' => 'CID link field', + ], $cid->toArray()); + } + + public function test_description_nullable_and_schema_return(): void + { + $schema = [ + 'type' => 'cid-link', + ]; + + $cid = $this->make($schema); + $this->assertNull($cid->description()); + $this->assertSame(['type' => 'cid-link'], $cid->toArray()); + $this->assertSame($schema, $cid->schema()); + } + + public function test_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'cid-link', + 'meta' => [ + 'codec' => [ + 'name' => 'dag-cbor', + ], + ], + ]; + + $cid = $this->make($schema); + $this->assertSame('dag-cbor', $cid->__get('meta.codec.name')); + $this->assertNull($cid->__get('meta.codec.unknown')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/IntegerSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/IntegerSchemaTest.php new file mode 100644 index 0000000..fbcd2aa --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/IntegerSchemaTest.php @@ -0,0 +1,92 @@ + 'integer', + 'description' => 'An integer field', + 'minimum' => 0, + 'maximum' => null, + 'enum' => [1, 2, 3], + 'default' => 0, + 'const' => null, + ]; + + $int = $this->make($schema); + + $this->assertSame('integer', $int->type()); + $this->assertSame('An integer field', $int->description()); + + // toArray should keep 0 values and drop only nulls + $this->assertSame([ + 'type' => 'integer', + 'description' => 'An integer field', + 'minimum' => 0, + 'enum' => [1, 2, 3], + 'default' => 0, + ], $int->toArray()); + } + + public function test_minimum_maximum_enum_default_const_accessors(): void + { + $schema = [ + 'type' => 'integer', + 'minimum' => -5, + 'maximum' => 10, + 'enum' => [0, 5, 10], + 'default' => 5, + 'const' => 10, + ]; + + $int = $this->make($schema); + + $this->assertSame(-5, $int->minimum()); + $this->assertSame(10, $int->maximum()); + $this->assertSame([0, 5, 10], $int->enum()); + $this->assertSame(5, $int->default()); + $this->assertSame(10, $int->const()); + } + + public function test_accessors_return_null_when_not_set(): void + { + $schema = [ + 'type' => 'integer', + ]; + + $int = $this->make($schema); + + $this->assertNull($int->minimum()); + $this->assertNull($int->maximum()); + $this->assertNull($int->enum()); + $this->assertNull($int->default()); + $this->assertNull($int->const()); + $this->assertSame(['type' => 'integer'], $int->toArray()); + } + + public function test_magic_get_dotted_path_and_schema(): void + { + $schema = [ + 'type' => 'integer', + 'meta' => ['range' => ['inclusive' => true]], + ]; + + $int = $this->make($schema); + $this->assertTrue($int->__get('meta.range.inclusive')); + $this->assertNull($int->__get('meta.range.unknown')); + $this->assertSame($schema, $int->schema()); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/NullSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/NullSchemaTest.php new file mode 100644 index 0000000..fbc8081 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/NullSchemaTest.php @@ -0,0 +1,60 @@ + 'null', + 'description' => 'Represents null', + 'extra' => null, + ]; + + $null = $this->make($schema); + + $this->assertSame('null', $null->type()); + $this->assertSame('Represents null', $null->description()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'null', + 'description' => 'Represents null', + ], $null->toArray()); + } + + public function test_description_nullable_and_schema_return(): void + { + $schema = [ + 'type' => 'null', + ]; + + $null = $this->make($schema); + $this->assertNull($null->description()); + $this->assertSame(['type' => 'null'], $null->toArray()); + $this->assertSame($schema, $null->schema()); + } + + public function test_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'null', + 'meta' => ['info' => ['tag' => 'none']], + ]; + + $null = $this->make($schema); + $this->assertSame('none', $null->__get('meta.info.tag')); + $this->assertNull($null->__get('meta.info.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Concrete/StringSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/StringSchemaTest.php new file mode 100644 index 0000000..aceb16a --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Concrete/StringSchemaTest.php @@ -0,0 +1,102 @@ + 'string', + 'description' => 'A string field', + 'format' => 'did', + 'minLength' => 0, + 'maxLength' => null, + 'minGraphemes' => null, + 'maxGraphemes' => 64, + 'knownValues' => ['a', 'b'], + 'enum' => ['x', 'y'], + 'default' => 'x', + 'const' => null, + ]; + + $str = $this->make($schema); + + $this->assertSame('string', $str->type()); + $this->assertSame('A string field', $str->description()); + + $this->assertSame([ + 'type' => 'string', + 'description' => 'A string field', + 'format' => 'did', + 'minLength' => 0, + 'maxGraphemes' => 64, + 'knownValues' => ['a', 'b'], + 'enum' => ['x', 'y'], + 'default' => 'x', + ], $str->toArray()); + } + + public function test_all_accessors_and_null_cases(): void + { + $schema = [ + 'type' => 'string', + 'format' => 'uri', + 'minLength' => 1, + 'maxLength' => 255, + 'minGraphemes' => 1, + 'maxGraphemes' => 255, + 'knownValues' => ['ok'], + 'enum' => ['ok', 'no'], + 'default' => 'ok', + 'const' => 'ok', + ]; + + $str = $this->make($schema); + + $this->assertSame('uri', $str->format()); + $this->assertSame(1, $str->minLength()); + $this->assertSame(255, $str->maxLength()); + $this->assertSame(1, $str->minGraphemes()); + $this->assertSame(255, $str->maxGraphemes()); + $this->assertSame(['ok'], $str->knownValues()); + $this->assertSame(['ok', 'no'], $str->enum()); + $this->assertSame('ok', $str->default()); + $this->assertSame('ok', $str->const()); + + $empty = $this->make(['type' => 'string']); + $this->assertNull($empty->format()); + $this->assertNull($empty->minLength()); + $this->assertNull($empty->maxLength()); + $this->assertNull($empty->minGraphemes()); + $this->assertNull($empty->maxGraphemes()); + $this->assertNull($empty->knownValues()); + $this->assertNull($empty->enum()); + $this->assertNull($empty->default()); + $this->assertNull($empty->const()); + $this->assertSame(['type' => 'string'], $empty->toArray()); + } + + public function test_magic_get_and_schema_return(): void + { + $schema = [ + 'type' => 'string', + 'meta' => ['info' => ['hint' => 'str']], + ]; + + $str = $this->make($schema); + $this->assertSame('str', $str->__get('meta.info.hint')); + $this->assertNull($str->__get('meta.info.missing')); + $this->assertSame($schema, $str->schema()); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Container/ArraySchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Container/ArraySchemaTest.php new file mode 100644 index 0000000..7f159b0 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Container/ArraySchemaTest.php @@ -0,0 +1,81 @@ + 'array', + 'description' => 'A list of strings', + 'items' => ['type' => 'string'], + 'minLength' => null, + 'maxLength' => 10, + ]; + + $arraySchema = $this->make($schema); + + $this->assertSame('array', $arraySchema->type()); + $this->assertSame('A list of strings', $arraySchema->description()); + + // toArray should filter out only top-level nulls (minLength) + $this->assertSame([ + 'type' => 'array', + 'description' => 'A list of strings', + 'items' => ['type' => 'string'], + 'maxLength' => 10, + ], $arraySchema->toArray()); + } + + public function test_items_min_max_accessors(): void + { + $schema = [ + 'type' => 'array', + 'items' => ['type' => 'integer'], + 'minLength' => 1, + 'maxLength' => 5, + ]; + + $arraySchema = $this->make($schema); + + $this->assertSame(['type' => 'integer'], $arraySchema->items()); + $this->assertSame(1, $arraySchema->minLength()); + $this->assertSame(5, $arraySchema->maxLength()); + } + + public function test_schema_returns_underlying_array(): void + { + $schema = [ + 'type' => 'array', + 'items' => ['type' => 'ref', 'ref' => 'com.example#item'], + ]; + + $arraySchema = $this->make($schema); + + $this->assertSame($schema, $arraySchema->schema()); + } + + public function test_magic_get_supports_nested_paths(): void + { + $schema = [ + 'type' => 'array', + 'items' => ['type' => 'object', 'properties' => ['name' => ['type' => 'string']]], + ]; + + $arraySchema = $this->make($schema); + + // Underlying Schema supports dotted access in __get + $this->assertSame('string', $arraySchema->__get('items.properties.name.type')); + $this->assertNull($arraySchema->__get('items.properties.missing')); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Container/ObjectSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Container/ObjectSchemaTest.php new file mode 100644 index 0000000..fbe9865 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Container/ObjectSchemaTest.php @@ -0,0 +1,101 @@ +schema([ + 'type' => 'object', + 'properties' => $properties + ]); + + foreach ($schema->properties() as $property) { + $this->assertInstanceOf($expected, $property); + } + } + + public static function propertyCaseProvider(): \Generator + { + yield 'string property' => [ + 'properties' => ['field1' => ['type' => 'string', 'format' => 'did']], + 'expected' => StringSchema::class, + ]; + } + + public function test_properties_is_required(): void + { + $schema = $this->schema([ + 'type' => 'object', + // missing properties + ]); + + $this->expectException(MissingRequiredFieldException::class); + $this->expectExceptionMessage("array property 'properties'"); + + $schema->properties(); + } + + public function test_nullable_returns_expected_value(): void + { + $schema = $this->schema([ + 'type' => 'object', + 'nullable' => ['field1', 'field2'], + ]); + + $this->assertSame(['field1', 'field2'], $schema->nullable()); + } + + public function test_nullable_is_optional(): void + { + $schema = $this->schema([ + 'type' => 'object', + // missing nullable + ]); + + $this->assertNull($schema->nullable()); + } + + public function test_required_returns_expected_value(): void + { + $schema = $this->schema([ + 'type' => 'object', + 'required' => ['field1', 'field2'], + ]); + + $this->assertSame(['field1', 'field2'], $schema->required()); + } + + public function test_required_is_optional(): void + { + $schema = $this->schema([ + 'type' => 'object', + // missing required + ]); + + $this->assertNull($schema->required()); + } + + private function schema(array $content): ObjectSchema + { + return new ObjectSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Container/ParamsSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Container/ParamsSchemaTest.php new file mode 100644 index 0000000..52e4bc0 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Container/ParamsSchemaTest.php @@ -0,0 +1,110 @@ + 'object', + 'description' => 'Query params', + 'properties' => [ + 'q' => ['type' => 'string'], + ], + 'required' => ['q'], + 'extra' => null, + ]; + + $params = $this->make($schema); + + $this->assertSame('object', $params->type()); + $this->assertSame('Query params', $params->description()); + + $this->assertSame([ + 'type' => 'object', + 'description' => 'Query params', + 'properties' => [ + 'q' => ['type' => 'string'], + ], + 'required' => ['q'], + ], $params->toArray()); + } + + public function test_required_accessor_and_schema_return(): void + { + $schema = [ + 'type' => 'object', + 'properties' => [], + 'required' => ['a', 'b'], + ]; + + $params = $this->make($schema); + $this->assertSame(['a', 'b'], $params->required()); + $this->assertSame($schema, $params->schema()); + } + + public function test_required_is_null_when_absent(): void + { + $schema = [ + 'type' => 'object', + 'properties' => [], + ]; + + $params = $this->make($schema); + $this->assertNull($params->required()); + } + + public function test_properties_map_to_property_objects_with_nullable_inverted_from_required(): void + { + $schema = [ + 'type' => 'object', + 'properties' => [ + 'limit' => ['type' => 'integer'], + 'cursor' => ['type' => 'string'], + ], + 'required' => ['limit'], + ]; + + $params = $this->make($schema); + $props = $params->properties(); + + $this->assertCount(2, $props); + $this->assertContainsOnlyInstancesOf(Property::class, $props); + + // preserve order: limit, cursor + $limit = $props[0]; + $cursor = $props[1]; + + $this->assertSame('limit', $limit->name()); + $this->assertFalse($limit->isNullable(), 'required field should not be nullable'); + $this->assertTrue($limit->isRequired()); + + $this->assertSame('cursor', $cursor->name()); + $this->assertTrue($cursor->isNullable(), 'non-required field should be nullable'); + $this->assertFalse($cursor->isRequired()); + } + + public function test_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'object', + 'meta' => ['info' => ['note' => 'params']], + ]; + + $params = $this->make($schema); + $this->assertSame('params', $params->__get('meta.info.note')); + $this->assertNull($params->__get('meta.info.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Meta/RefSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Meta/RefSchemaTest.php new file mode 100644 index 0000000..4a65ea5 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Meta/RefSchemaTest.php @@ -0,0 +1,52 @@ + 'ref', + 'description' => 'Reference to another def', + 'ref' => 'com.example.def#item', + 'extra' => null, + ]; + + $ref = $this->make($schema); + + $this->assertSame('ref', $ref->type()); + $this->assertSame('Reference to another def', $ref->description()); + $this->assertSame('com.example.def#item', $ref->ref()); + + $this->assertSame([ + 'type' => 'ref', + 'description' => 'Reference to another def', + 'ref' => 'com.example.def#item', + ], $ref->toArray()); + } + + public function test_schema_and_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'ref', + 'ref' => 'com.example#x', + 'meta' => ['info' => ['note' => 'ref']], + ]; + + $ref = $this->make($schema); + $this->assertSame($schema, $ref->schema()); + $this->assertSame('ref', $ref->__get('meta.info.note')); + $this->assertNull($ref->__get('meta.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Meta/TokenSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Meta/TokenSchemaTest.php new file mode 100644 index 0000000..cff7d4c --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Meta/TokenSchemaTest.php @@ -0,0 +1,64 @@ + 'token', + 'description' => 'Opaque token', + 'optional' => null, + ]; + + $token = $this->make($schema); + + $this->assertSame('token', $token->type()); + $this->assertSame('Opaque token', $token->description()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'token', + 'description' => 'Opaque token', + ], $token->toArray()); + } + + public function test_description_nullable_and_schema_return(): void + { + $schema = [ + 'type' => 'token', + ]; + + $token = $this->make($schema); + $this->assertNull($token->description()); + $this->assertSame(['type' => 'token'], $token->toArray()); + $this->assertSame($schema, $token->schema()); + } + + public function test_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'token', + 'meta' => [ + 'info' => [ + 'hint' => 'jwt', + ], + ], + ]; + + $token = $this->make($schema); + $this->assertSame('jwt', $token->__get('meta.info.hint')); + $this->assertNull($token->__get('meta.info.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnionSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnionSchemaTest.php new file mode 100644 index 0000000..baa5483 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnionSchemaTest.php @@ -0,0 +1,67 @@ + 'union', + 'description' => 'Union of refs', + 'refs' => ['com.example#a', 'com.example#b'], + 'closed' => true, + 'optional' => null, + ]; + + $union = $this->make($schema); + + $this->assertSame('union', $union->type()); + $this->assertSame('Union of refs', $union->description()); + $this->assertSame(['com.example#a', 'com.example#b'], $union->refs()); + $this->assertTrue($union->closed()); + + // toArray filters only top-level nulls + $this->assertSame([ + 'type' => 'union', + 'description' => 'Union of refs', + 'refs' => ['com.example#a', 'com.example#b'], + 'closed' => true, + ], $union->toArray()); + } + + public function test_defaults_when_refs_absent_and_closed_not_set(): void + { + $schema = [ + 'type' => 'union', + ]; + + $union = $this->make($schema); + $this->assertSame([], $union->refs()); + $this->assertFalse($union->closed()); + $this->assertSame(['type' => 'union'], $union->toArray()); + $this->assertSame($schema, $union->schema()); + } + + public function test_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'union', + 'meta' => ['info' => ['note' => 'u']], + ]; + + $union = $this->make($schema); + $this->assertSame('u', $union->__get('meta.info.note')); + $this->assertNull($union->__get('meta.info.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnknownSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnknownSchemaTest.php new file mode 100644 index 0000000..722dd9f --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Meta/UnknownSchemaTest.php @@ -0,0 +1,48 @@ + 'unknown', + 'description' => 'Unknown type', + 'extra' => null, + ]; + + $unknown = $this->make($schema); + + $this->assertSame('unknown', $unknown->type()); + $this->assertSame('Unknown type', $unknown->description()); + + $this->assertSame([ + 'type' => 'unknown', + 'description' => 'Unknown type', + ], $unknown->toArray()); + } + + public function test_schema_and_magic_get_dotted_paths(): void + { + $schema = [ + 'type' => 'unknown', + 'meta' => ['info' => ['hint' => 'u']], + ]; + + $unknown = $this->make($schema); + $this->assertSame($schema, $unknown->schema()); + $this->assertSame('u', $unknown->__get('meta.info.hint')); + $this->assertNull($unknown->__get('meta.info.missing')); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Primary/ProcedureSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Primary/ProcedureSchemaTest.php new file mode 100644 index 0000000..8f9a172 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Primary/ProcedureSchemaTest.php @@ -0,0 +1,94 @@ +schema([ + // missing field + ]); + + $this->assertNull($schema->{$fieldName}()); + } + + #[DataProvider('fieldExpectedValueProvider')] + public function test_field_returns_expected_value( + string $fieldName, + string $expectedInstanceOfFQCN, + array $expectedToArrayResult + ): void + { + $schema = $this->schema([ + $fieldName => $expectedToArrayResult + ]); + + $this->assertInstanceOf($expectedInstanceOfFQCN, $instance = $schema->{$fieldName}()); + $this->assertSame($expectedToArrayResult, $instance->toArray()); + } + + public static function fieldExpectedValueProvider(): \Generator + { + yield 'parameters field' => [ + 'fieldName' => 'parameters', + 'expectedInstanceOfFQCN' => ParamsSchema::class, + 'expectedToArrayResult' => ['foo', 'dag', 'cbor'] + ]; + + yield 'output field' => [ + 'fieldName' => 'output', + 'expectedInstanceOfFQCN' => OutputSchema::class, + 'expectedToArrayResult' => [[false, 'string', 0]] + ]; + + yield 'input field' => [ + 'fieldName' => 'input', + 'expectedInstanceOfFQCN' => InputSchema::class, + 'expectedToArrayResult' => [false, 'string', 0, true] + ]; + + yield 'errors field' => [ + 'fieldName' => 'errors', + 'expectedInstanceOfFQCN' => ErrorsSchema::class, + 'expectedToArrayResult' => [ + ['name' => 'error name #1', 'description' => 'error description #1'], + ['name' => 'error name #2', 'description' => 'error description #2'] + ] + ]; + } + + private function schema(array $content): ProcedureSchema + { + return new ProcedureSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Primary/QuerySchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Primary/QuerySchemaTest.php new file mode 100644 index 0000000..6d38871 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Primary/QuerySchemaTest.php @@ -0,0 +1,86 @@ +schema([ + // missing field + ]); + + $this->assertNull($schema->{$fieldName}()); + } + + #[DataProvider('fieldExpectedValueProvider')] + public function test_field_returns_expected_value( + string $fieldName, + string $expectedInstanceOfFQCN, + array $expectedToArrayResult + ): void + { + $schema = $this->schema([ + $fieldName => $expectedToArrayResult + ]); + + $this->assertInstanceOf($expectedInstanceOfFQCN, $instance = $schema->{$fieldName}()); + $this->assertSame($expectedToArrayResult, $instance->toArray()); + } + + public static function fieldExpectedValueProvider(): \Generator + { + yield 'parameters field' => [ + 'fieldName' => 'parameters', + 'expectedInstanceOfFQCN' => ParamsSchema::class, + 'expectedToArrayResult' => ['foo', 'dag', 'cbor'] + ]; + + yield 'output field' => [ + 'fieldName' => 'output', + 'expectedInstanceOfFQCN' => OutputSchema::class, + 'expectedToArrayResult' => [[false, 'string', 0]] + ]; + + yield 'errors field' => [ + 'fieldName' => 'errors', + 'expectedInstanceOfFQCN' => ErrorsSchema::class, + 'expectedToArrayResult' => [ + ['name' => 'error name #1', 'description' => 'error description #1'], + ['name' => 'error name #2', 'description' => 'error description #2'] + ] + ]; + } + + private function schema(array $content): QuerySchema + { + return new QuerySchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Primary/RecordSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Primary/RecordSchemaTest.php new file mode 100644 index 0000000..c5dcf79 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Primary/RecordSchemaTest.php @@ -0,0 +1,77 @@ +schema([ + 'key' => 'tid' + ]); + + $this->assertSame('tid', $schema->key()); + $this->assertSame(['key' => 'tid'], $schema->toArray()); + } + + public function test_key_is_required(): void + { + $schema = $this->schema([ + // missing key + ]); + + $this->expectException(\TypeError::class); + $schema->key(); + } + + public function test_record_returns_expected_value(): void + { + $schema = $this->schema([ + 'record' => [ + 'type' => 'object', + 'properties' => [ + 'field1' => [], + 'field2' => [], + ] + ] + ]); + + $this->assertInstanceOf(ObjectSchema::class, $objectSchema = $schema->record()); + $this->assertSame([ + 'type' => 'object', + 'properties' => [ + 'field1' => [], + 'field2' => [], + ] + ], $objectSchema->toArray()); + } + + public function test_record_is_required(): void + { + $schema = $this->schema([ + // missing record + ]); + + $this->expectException(\TypeError::class); + $schema->record(); + } + + private function schema(array $content): RecordSchema + { + return new RecordSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Primary/SubscriptionSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Primary/SubscriptionSchemaTest.php new file mode 100644 index 0000000..a3b9b8c --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Primary/SubscriptionSchemaTest.php @@ -0,0 +1,87 @@ +schema([ + // missing field + ]); + + $this->assertNull($schema->{$fieldName}()); + } + + #[DataProvider('fieldExpectedValueProvider')] + public function test_field_returns_expected_value( + string $fieldName, + string $expectedInstanceOfFQCN, + array $expectedToArrayResult + ): void + { + $schema = $this->schema([ + $fieldName => $expectedToArrayResult + ]); + + $this->assertInstanceOf($expectedInstanceOfFQCN, $instance = $schema->{$fieldName}()); + $this->assertSame($expectedToArrayResult, $instance->toArray()); + } + + public static function fieldExpectedValueProvider(): \Generator + { + yield 'parameters field' => [ + 'fieldName' => 'parameters', + 'expectedInstanceOfFQCN' => ParamsSchema::class, + 'expectedToArrayResult' => ['foo', 'dag', 'cbor'] + ]; + + yield 'message field' => [ + 'fieldName' => 'message', + 'expectedInstanceOfFQCN' => MessageSchema::class, + 'expectedToArrayResult' => [false, 'string', 0, true] + ]; + + yield 'errors field' => [ + 'fieldName' => 'errors', + 'expectedInstanceOfFQCN' => ErrorsSchema::class, + 'expectedToArrayResult' => [ + ['name' => 'error name #1', 'description' => 'error description #1'], + ['name' => 'error name #2', 'description' => 'error description #2'] + ] + ]; + } + + + private function schema(array $content): SubscriptionSchema + { + return new SubscriptionSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorSchemaTest.php new file mode 100644 index 0000000..d43eb0f --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorSchemaTest.php @@ -0,0 +1,68 @@ +schema([ + // missing name + ]); + + $this->expectException(\TypeError::class); + $schema->name(); + } + + public function test_name_returns_expected_value(): void + { + $schema = $this->schema([ + 'name' => 'error name' + ]); + + $this->assertSame('error name', $schema->name()); + } + + public function test_description_is_optional(): void + { + $schema = $this->schema([ + // missing description + ]); + + $this->assertNull($schema->description()); + } + + public function test_description_returns_expected_value(): void + { + $schema = $this->schema([ + 'description' => 'error description' + ]); + + $this->assertSame('error description', $schema->description()); + } + + public function test_description_throws_exception(): void + { + // Placeholder: ErrorSchema intentionally overrides description behavior. + // This test exists to ensure future consistency or documentation. + $this->assertTrue(true); + } + + private function schema(array $content): ErrorSchema + { + return new ErrorSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorsSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorsSchemaTest.php new file mode 100644 index 0000000..89f2380 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Support/ErrorsSchemaTest.php @@ -0,0 +1,118 @@ +schema([]); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage("Schema is read-only"); + + $schema->offsetSet('foo', 'bar'); + } + + public function test_offsetUnset_throws_badMethodCall_exception(): void + { + $schema = $this->schema([]); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage("Schema is read-only"); + + $schema->offsetUnset('foo'); + } + + #[DataProvider('offsetExistsCaseProvider')] + public function test_offsetExists_returns_expected_boolean(array $content, int $index, bool $expected): void + { + $schema = $this->schema($content); + + $this->assertSame($expected, $schema->offsetExists($index)); + } + + public static function offsetExistsCaseProvider(): array + { + $schema = [ + [], + ]; + + return [ + [$schema, 0, true], + [$schema, 1, false], + ]; + } + + public function test_offsetGet_returns_ErrorSchema_instance(): void + { + $schema = $this->schema([[]]); + + $this->assertInstanceOf(ErrorSchema::class, $schema->offsetGet(0)); + } + + public function test_offsetGet_throws_OutOfBoundsException_when_index_not_found(): void + { + $schema = $this->schema([]); + + $this->expectException(\OutOfBoundsException::class); + $this->expectExceptionMessage("Error schema offset 'foo' not found"); + + $schema->offsetGet('foo'); + } + + public static function countCaseProvider(): \Generator + { + $schema = []; + + foreach (range(1, 5) as $item) { + $schema[] = ['name' => 'error name', 'description' => 'error description']; + + yield ['schema' => $schema, 'expected' => $item]; + } + } + + #[DataProvider('countCaseProvider')] + public function test_count_returns_actual_count(array $schema, int $expected) + { + $schema = $this->schema($schema); + + $this->assertSame($expected, $schema->count()); + } + + public function test_getIterator_returns_ArrayIterator_instance(): void + { + $schema = $this->schema([]); + $this->assertInstanceOf(ArrayIterator::class, $schema->getIterator()); + } + + public function test_getIterator_yields_ErrorSchema_instances(): void + { + $schema = $this->schema([['name' => 'n']]); + + foreach ($schema as $item) { + $this->assertInstanceOf(ErrorSchema::class, $item); + } + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Support/InputSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Support/InputSchemaTest.php new file mode 100644 index 0000000..e97f31d --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Support/InputSchemaTest.php @@ -0,0 +1,134 @@ +assertTrue(true); + } + + public function test_description_returns_expected_value(): void + { + $schema = $this->schema(['description' => 'description of the input schema']); + + $this->assertSame('description of the input schema', $schema->description()); + } + + public function test_description_is_optional(): void + { + $schema = $this->schema([ + // missing description + ]); + + $this->assertNull($schema->description()); + } + + public function test_encoding_returns_expected_value(): void + { + $schema = $this->schema([ + 'encoding' => 'application/json', + ]); + + $this->assertSame('application/json', $schema->encoding()); + } + + public function test_encoding_is_required(): void + { + $schema = $this->schema([ + // missing encoding + ]); + + $this->expectException(TypeError::class); + + $schema->encoding(); + } + + public function test_schema_returns_expected_value(): void + { + $schema = ['type' => 'ref', 'ref' => 'com.example.schema']; + $inputSchema = $this->schema(['schema' => $schema]); + + $this->assertSame($schema, $inputSchema->schema()); + } + + public function test_schema_is_optional(): void + { + $schema = $this->schema([ + 'type' => 'input', + // missing schema + ]); + + $this->assertNull($schema->schema()); + } + + public function test_parameters_returns_expected_value(): void + { + $params = [ + 'required' => ['field1', 'field2'], + 'properties' => [ + [ + 'type' => 'string', + 'format' => 'did', + ], + [ + 'type' => 'array', + 'items' => [ + 'type' => 'integer', + 'minimum' => 30, + 'maximum' => 100, + ] + ] + ], + ]; + + $schema = $this->schema([ + 'parameters' => $params, + ]); + + $this->assertSame($params, $schema->parameters()); + } + + public function test_parameters_is_optional(): void + { + $schema = $this->schema([ + // missing parameters + ]); + + $this->assertNull($schema->parameters()); + } + + private function schema(array $content): InputSchema + { + return new InputSchema(new Schema($content)); + } + + public function test_toArray_delegates_to_schema_instance(): void + { + $expected = [ + 'encoding' => 'application/json', + 'schema' => ['type' => 'ref', 'ref' => 'com.example'], + 'parameters' => ['foo' => 'bar'], + ]; + + $schema = $this->schema($expected); + + $this->assertSame($expected, $schema->toArray()); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Support/MessageSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Support/MessageSchemaTest.php new file mode 100644 index 0000000..54eb870 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Support/MessageSchemaTest.php @@ -0,0 +1,61 @@ + 'bar', 'baz' => 'qux']; + $messageSchema = $this->schema(['schema' => $schema]); + + $this->assertInstanceOf(UnionSchema::class, $unionSchema = $messageSchema->schema()); + $this->assertSame($schema, $unionSchema->toArray()); + } + + public function test_schema_is_required(): void + { + $messageSchema = $this->schema([ + // missing schema + ]); + + $this->expectException(TypeError::class); + + $messageSchema->schema(); + } + + public function test_toArray_works_expected(): void + { + $schema = [ + 'description' => 'blah blah blah', + 'schema' => [ + 'type' => 'union', + 'refs' => ['app.com.example', 'example.reverse.dns'] + ] + ]; + + $messageSchema = $this->schema($schema); + + $this->assertSame($schema, $messageSchema->toArray()); + } + + private function schema(array $content = []): MessageSchema + { + return new MessageSchema(new Schema($content)); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Schema/Support/OutputSchemaTest.php b/tests/Unit/Service/Lexicon/V1/Schema/Support/OutputSchemaTest.php new file mode 100644 index 0000000..16182dc --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Schema/Support/OutputSchemaTest.php @@ -0,0 +1,134 @@ +assertTrue(true); + } + + public function test_description_returns_expected_value(): void + { + $schema = $this->schema(['description' => 'description of the output schema']); + + $this->assertSame('description of the output schema', $schema->description()); + } + + public function test_description_is_optional(): void + { + $schema = $this->schema([ + // missing description + ]); + + $this->assertNull($schema->description()); + } + + public function test_encoding_returns_expected_value(): void + { + $schema = $this->schema([ + 'encoding' => 'application/json', + ]); + + $this->assertSame('application/json', $schema->encoding()); + } + + public function test_encoding_is_required(): void + { + $schema = $this->schema([ + // missing encoding + ]); + + $this->expectException(TypeError::class); + + $schema->encoding(); + } + + public function test_schema_returns_expected_value(): void + { + $schema = ['type' => 'ref', 'ref' => 'com.example.schema']; + $outputSchema = $this->schema(['schema' => $schema]); + + $this->assertSame($schema, $outputSchema->schema()); + } + + public function test_schema_is_optional(): void + { + $schema = $this->schema([ + 'type' => 'output', + // missing schema + ]); + + $this->assertNull($schema->schema()); + } + + public function test_parameters_returns_expected_value(): void + { + $params = [ + 'required' => ['field1', 'field2'], + 'properties' => [ + [ + 'type' => 'string', + 'format' => 'did', + ], + [ + 'type' => 'array', + 'items' => [ + 'type' => 'integer', + 'minimum' => 30, + 'maximum' => 100, + ] + ] + ], + ]; + + $schema = $this->schema([ + 'parameters' => $params, + ]); + + $this->assertSame($params, $schema->parameters()); + } + + public function test_parameters_is_optional(): void + { + $schema = $this->schema([ + // missing parameters + ]); + + $this->assertNull($schema->parameters()); + } + + private function schema(array $content): OutputSchema + { + return new OutputSchema(new Schema($content)); + } + + public function test_toArray_delegates_to_schema_instance(): void + { + $expected = [ + 'encoding' => 'application/json', + 'schema' => ['type' => 'ref', 'ref' => 'com.example'], + 'parameters' => ['foo' => 'bar'], + ]; + + $schema = $this->schema($expected); + + $this->assertSame($expected, $schema->toArray()); + } +} diff --git a/tests/Unit/Service/Lexicon/V1/Traits/ArrayableTraitTest.php b/tests/Unit/Service/Lexicon/V1/Traits/ArrayableTraitTest.php new file mode 100644 index 0000000..a731859 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Traits/ArrayableTraitTest.php @@ -0,0 +1,91 @@ +schema; + } + }; + } + + public function test_to_array_filters_only_null_values(): void + { + $schema = [ + 'a' => 1, + 'b' => null, + 'c' => 0, + 'd' => '', + 'e' => false, + 'f' => [], + 'g' => null, + ]; + + $obj = $this->makeArrayable($schema); + + $this->assertSame([ + 'a' => 1, + 'c' => 0, + 'd' => '', + 'e' => false, + 'f' => [], + ], $obj->toArray()); + } + + public function test_to_array_preserves_nested_nulls(): void + { + $schema = [ + 'nested' => [ + 'x' => null, + 'y' => 1, + ], + 'top' => null, + ]; + + $obj = $this->makeArrayable($schema); + + $this->assertSame([ + 'nested' => [ + 'x' => null, + 'y' => 1, + ], + ], $obj->toArray()); + } + + public function test_to_array_preserves_numeric_keys(): void + { + $schema = [1, null, 2, null, 3]; + + $obj = $this->makeArrayable($schema); + + // array_filter with a callback preserves keys + $this->assertSame([ + 0 => 1, + 2 => 2, + 4 => 3, + ], $obj->toArray()); + } + + public function test_to_array_returns_empty_array_when_all_null(): void + { + $schema = ['a' => null, 'b' => null]; + $obj = $this->makeArrayable($schema); + + $this->assertSame([], $obj->toArray()); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Traits/DefinitionTraitTest.php b/tests/Unit/Service/Lexicon/V1/Traits/DefinitionTraitTest.php new file mode 100644 index 0000000..990636a --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Traits/DefinitionTraitTest.php @@ -0,0 +1,66 @@ + 'object', 'props' => ['a' => ['type' => 'string']]]; + + $lexicon = new class implements LexiconInterface { + public function nsid(): string { return 'com.example.test'; } + public function version(): int { return 1; } + public function description(): ?string { return null; } + public function defs(): array { return []; } + }; + + $definition = new class($schema, $lexicon) implements DefinitionInterface { + public function __construct(private array $schema, private LexiconInterface $lexicon) {} + public function name(): string { return 'com.example.test#def'; } + public function type(): string { return $this->schema['type']; } + public function description(): ?string { return $this->schema['description'] ?? null; } + public function lexicon(): LexiconInterface { return $this->lexicon; } + public function __get(string $name): mixed { return $this->schema[$name] ?? null; } + public function toArray(): array { return $this->schema; } + // Not required by interface, but used by DefinitionTrait + public function schema(): array { return $this->schema; } + }; + + $wrapper = new class($definition) { + use DefinitionTrait; + public function __construct(private DefinitionInterface $definition) {} + }; + + $this->assertSame($schema, $wrapper->schema()); + } + + public function test_schema_throws_when_definition_property_missing(): void + { + $wrapper = new class { + use DefinitionTrait; + }; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('::$definition must implement'); + $wrapper->schema(); + } + + public function test_schema_throws_when_definition_wrong_type(): void + { + $wrapper = new class('not-a-definition') { + use DefinitionTrait; + public function __construct(private string $definition) {} + }; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('::$definition must implement'); + $wrapper->schema(); + } +} + diff --git a/tests/Unit/Service/Lexicon/V1/Traits/SchemaTraitTest.php b/tests/Unit/Service/Lexicon/V1/Traits/SchemaTraitTest.php new file mode 100644 index 0000000..120c185 --- /dev/null +++ b/tests/Unit/Service/Lexicon/V1/Traits/SchemaTraitTest.php @@ -0,0 +1,70 @@ + 'object', 'description' => 'demo']; + + $obj = new class($data) { + use RawSchemaAccessorTrait; + + public function __construct(private array $schema) + { + } + }; + + $this->assertSame($data, $obj->schema()); + } + + public function test_schema_returns_array_when_backed_by_schema_interface(): void + { + $data = ['type' => 'object', 'description' => 'from-interface']; + + $obj = new class(new Schema($data)) { + use RawSchemaAccessorTrait; + + public function __construct(private SchemaInterface $schema) + { + } + }; + + $this->assertSame($data, $obj->schema()); + } + + public function test_schema_throws_when_property_missing(): void + { + $obj = new class { + use RawSchemaAccessorTrait; + }; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('::$schema must be an array or implement'); + + $obj->schema(); + } + + public function test_schema_throws_when_property_invalid_type(): void + { + $obj = new class('not-an-array-or-interface') { + use RawSchemaAccessorTrait; + + public function __construct(private string $schema) + { + } + }; + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('::$schema must be an array or implement'); + + $obj->schema(); + } +} + diff --git a/tests/Unit/Service/Syntax/Factory/SchemaFactoryTest.php b/tests/Unit/Service/Syntax/Factory/SchemaFactoryTest.php index 53a7b71..62dd136 100644 --- a/tests/Unit/Service/Syntax/Factory/SchemaFactoryTest.php +++ b/tests/Unit/Service/Syntax/Factory/SchemaFactoryTest.php @@ -2,18 +2,18 @@ namespace Blugen\Tests\Unit\Service\Syntax\Factory; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ArraySchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BlobSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BooleanSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\BytesSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\CidLinkSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\IntegerSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\NullSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\ObjectSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\StringSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\TokenSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnionSchema; -use Blugen\Service\Lexicon\V1\TypeSpecificSchema\Field\UnknownSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BlobSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BooleanSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\BytesSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\CidLinkSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\IntegerSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\NullSchema; +use Blugen\Service\Lexicon\V1\Schema\Concrete\StringSchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ArraySchema; +use Blugen\Service\Lexicon\V1\Schema\Container\ObjectSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\TokenSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnionSchema; +use Blugen\Service\Lexicon\V1\Schema\Meta\UnknownSchema; use Blugen\Service\Syntax\Factory\SchemaFactory; use Blugen\Tests\TestCase; use PHPUnit\Framework\Attributes\DataProvider; diff --git a/tests/Unit/Traits/WithArrayableTestTrait.php b/tests/Unit/Traits/WithArrayableTestTrait.php new file mode 100644 index 0000000..56a3aae --- /dev/null +++ b/tests/Unit/Traits/WithArrayableTestTrait.php @@ -0,0 +1,20 @@ + 'schema type', + 'description' => 'schema description', + 'foo' => 'bar', + 'bar' => 'qux' + ]]; + + $schema = $this->schema($expected); + + $this->assertSame($expected, $schema->toArray()); + } +} diff --git a/tests/Unit/Traits/WithGetTestTrait.php b/tests/Unit/Traits/WithGetTestTrait.php new file mode 100644 index 0000000..41681db --- /dev/null +++ b/tests/Unit/Traits/WithGetTestTrait.php @@ -0,0 +1,32 @@ +schema($arr); + + $this->assertSame($expected, $schema->__get($param)); + } + + public static function magicGetCaseProvider(): array + { + $arr = [ + 'app' => ['bsky' => ['feed' => ['post' => 'app.bsky.feed.post']]], + 'com' => ['atproto' => ['repo' => []]], + 'nine' => ['eight' => ['seven' => ['six' => null]]] + ]; + + return [ + [$arr, 'app.bsky.feed.post', 'app.bsky.feed.post'], + [$arr, 'com.atproto.repo', []], + [$arr, 'com.atproto.repo.createRecord', null], + [$arr, 'nine.eight.seven.six', null], + ]; + } +} diff --git a/tests/Unit/Traits/WithSchema.php b/tests/Unit/Traits/WithSchema.php new file mode 100644 index 0000000..19529cd --- /dev/null +++ b/tests/Unit/Traits/WithSchema.php @@ -0,0 +1,10 @@ +schema([ + // missing type + ]); + + $this->expectException(MissingRequiredFieldException::class); + $schema->type(); + } + + public function test_type_returns_expected_type(): void + { + $schema = $this->schema([ + 'type' => 'schema type' + ]); + + $this->assertSame('schema type', $schema->type()); + } + + public function test_description_is_optional(): void + { + $schema = $this->schema([ + // missing description + ]); + + $this->assertNull($schema->description()); + } + + public function test_description_returns_expected_value(): void + { + $schema = $this->schema([ + 'description' => 'schema description' + ]); + + $this->assertSame('schema description', $schema->description()); + } +} diff --git a/tests/Unit/Traits/WithSupportSchemaTestTrait.php b/tests/Unit/Traits/WithSupportSchemaTestTrait.php new file mode 100644 index 0000000..fe73918 --- /dev/null +++ b/tests/Unit/Traits/WithSupportSchemaTestTrait.php @@ -0,0 +1,26 @@ +schema([]); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage($schema::class . "::type() is not supported for support schemas"); + + $schema->type(); + } + + public function test_description_throws_exception(): void + { + $schema = $this->schema([]); + + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage($schema::class . "::description() is not supported for support schemas"); + + $schema->description(); + } +}