From 98dc0e983eb33ec6a915314dbaa343b6acfc9d0f Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 19:20:29 +0100 Subject: [PATCH 01/12] Refactor: Rename Schemas to Schema and remove config - Rename src/Schemas directory to src/Schema (singular) - Update all namespaces from Blaspsoft\Forerunner\Schemas to Blaspsoft\Forerunner\Schema - Remove config/forerunner.php (not needed at this time) - Remove config publishing from service provider - Update all use statements throughout codebase - Update stub template with new namespace - Update README examples with new namespace - Remove Configuration section from README - All 120 tests passing - PHPStan Level 9 clean --- README.md | 28 +++++++-------------- config/forerunner.php | 16 ------------ src/ForerunnerServiceProvider.php | 11 +------- src/{Schemas => Schema}/Builder.php | 2 +- src/{Schemas => Schema}/PropertyBuilder.php | 2 +- src/{Schemas => Schema}/Struct.php | 2 +- stubs/struct.stub | 4 +-- tests/Feature/IntendedUsageTest.php | 4 +-- tests/Feature/MakeStructCommandTest.php | 4 +-- tests/Feature/SchemaFacadeTest.php | 4 +-- tests/Unit/AdvancedFeaturesTest.php | 6 ++--- tests/Unit/BuilderTest.php | 4 +-- tests/Unit/PropertyBuilderTest.php | 4 +-- tests/Unit/StructTest.php | 4 +-- 14 files changed, 30 insertions(+), 65 deletions(-) delete mode 100644 config/forerunner.php rename src/{Schemas => Schema}/Builder.php (99%) rename src/{Schemas => Schema}/PropertyBuilder.php (99%) rename src/{Schemas => Schema}/Struct.php (98%) diff --git a/README.md b/README.md index ca4fd4a..a9742fc 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ This creates a structure class at `app/Structures/UserProfile.php`: namespace App\Structures; -use Blaspsoft\Forerunner\Schemas\Struct; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Struct; +use Blaspsoft\Forerunner\Schema\Builder; class UserProfile { @@ -64,8 +64,8 @@ class UserProfile Define a schema using the `Struct` class or `Schema` facade: ```php -use Blaspsoft\Forerunner\Schemas\Struct; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Struct; +use Blaspsoft\Forerunner\Schema\Builder; $schema = Struct::define('User', function (Builder $builder) { $builder->string('name', 'The user\'s full name')->required(); @@ -79,7 +79,7 @@ Or using the facade: ```php use Blaspsoft\Forerunner\Facades\Schema; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Builder; $schema = Schema::define('User', function (Builder $builder) { $builder->string('name')->required(); @@ -352,8 +352,8 @@ $builder->string('email') ### Complete Advanced Example ```php -use Blaspsoft\Forerunner\Schemas\Struct; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Struct; +use Blaspsoft\Forerunner\Schema\Builder; $schema = Struct::define('AdvancedUser', function (Builder $builder) { // Schema metadata @@ -458,8 +458,8 @@ This generates: ### User Profile with Nested Objects ```php -use Blaspsoft\Forerunner\Schemas\Struct; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Struct; +use Blaspsoft\Forerunner\Schema\Builder; $schema = Struct::define('UserProfile', function (Builder $builder) { $builder->string('name', 'The user\'s full name') @@ -637,16 +637,6 @@ $json = UserProfile::toJson(); // Returns JSON string $json = UserProfile::schema()->toJson(); ``` -## Configuration - -Publish the configuration file: - -```bash -php artisan vendor:publish --tag="forerunner-config" -``` - -This will create `config/forerunner.php` where you can customize package settings. - ## Testing Run the test suite: diff --git a/config/forerunner.php b/config/forerunner.php deleted file mode 100644 index 280c2dc..0000000 --- a/config/forerunner.php +++ /dev/null @@ -1,16 +0,0 @@ - [ - // Add your default configuration options here - ], -]; diff --git a/src/ForerunnerServiceProvider.php b/src/ForerunnerServiceProvider.php index b976f29..cfe8667 100644 --- a/src/ForerunnerServiceProvider.php +++ b/src/ForerunnerServiceProvider.php @@ -3,7 +3,7 @@ namespace Blaspsoft\Forerunner; use Blaspsoft\Forerunner\Commands\MakeStructCommand; -use Blaspsoft\Forerunner\Schemas\Struct; +use Blaspsoft\Forerunner\Schema\Struct; use Illuminate\Support\ServiceProvider; class ForerunnerServiceProvider extends ServiceProvider @@ -13,11 +13,6 @@ class ForerunnerServiceProvider extends ServiceProvider */ public function register(): void { - $this->mergeConfigFrom( - __DIR__.'/../config/forerunner.php', - 'forerunner' - ); - $this->app->singleton('forerunner.schema', function () { return new class { @@ -46,10 +41,6 @@ public function __call(string $method, array $args): mixed public function boot(): void { if ($this->app->runningInConsole()) { - $this->publishes([ - __DIR__.'/../config/forerunner.php' => config_path('forerunner.php'), - ], 'forerunner-config'); - $this->commands([ MakeStructCommand::class, ]); diff --git a/src/Schemas/Builder.php b/src/Schema/Builder.php similarity index 99% rename from src/Schemas/Builder.php rename to src/Schema/Builder.php index fd80a88..1f52867 100644 --- a/src/Schemas/Builder.php +++ b/src/Schema/Builder.php @@ -1,6 +1,6 @@ diff --git a/stubs/struct.stub b/stubs/struct.stub index 20b9587..8114880 100644 --- a/stubs/struct.stub +++ b/stubs/struct.stub @@ -2,8 +2,8 @@ namespace {{ namespace }}; -use Blaspsoft\Forerunner\Schemas\Struct; -use Blaspsoft\Forerunner\Schemas\Builder; +use Blaspsoft\Forerunner\Schema\Struct; +use Blaspsoft\Forerunner\Schema\Builder; class {{ class }} { diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index bc94e0d..3d1c539 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -1,7 +1,7 @@ testFilePath); - expect($content)->toContain('use Blaspsoft\Forerunner\Schemas\Struct;') - ->and($content)->toContain('use Blaspsoft\Forerunner\Schemas\Builder;'); + expect($content)->toContain('use Blaspsoft\Forerunner\Schema\Struct;') + ->and($content)->toContain('use Blaspsoft\Forerunner\Schema\Builder;'); }); it('generates struct with schema method', function () { diff --git a/tests/Feature/SchemaFacadeTest.php b/tests/Feature/SchemaFacadeTest.php index 0043a13..a6e7244 100644 --- a/tests/Feature/SchemaFacadeTest.php +++ b/tests/Feature/SchemaFacadeTest.php @@ -1,8 +1,8 @@ Date: Fri, 17 Oct 2025 19:46:06 +0100 Subject: [PATCH 02/12] Simplify Struct API and add OpenAI format support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add OpenAI structured output format support when strict() is enabled - Wraps schema with {name, strict: true, schema: {...}} structure - Returns flat schema when strict mode is not used - Remove redundant methods from Struct class - Remove toJson() method (users can use json_encode() directly) - Remove ArrayAccess implementation (58 lines reduced from 105) - Keep JsonSerializable for native json_encode() support - Simplify generated struct classes - schema() method now returns array directly via ->toArray() - Remove toArray() and toJson() helper methods from stub - More explicit and cleaner API - Add strict mode tracking to Builder - New isStrict() method to check if strict mode is enabled - Used by Struct to determine output format - Update all tests to use ->toArray() explicitly - 118 tests passing with 400 assertions - PHPStan Level 9 clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Schema/Builder.php | 11 +++++ src/Schema/Struct.php | 65 ++++++------------------- stubs/struct.stub | 26 ++-------- tests/Feature/IntendedUsageTest.php | 4 +- tests/Feature/MakeStructCommandTest.php | 15 ++---- tests/Feature/SchemaFacadeTest.php | 33 ++++++------- tests/Unit/AdvancedFeaturesTest.php | 45 ++++++++++++----- tests/Unit/StructTest.php | 59 +++++----------------- 8 files changed, 96 insertions(+), 162 deletions(-) diff --git a/src/Schema/Builder.php b/src/Schema/Builder.php index 1f52867..bc71495 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Builder.php @@ -20,6 +20,8 @@ class Builder protected ?string $title = null; + protected bool $isStrict = false; + public function __construct(string $name) { $this->name = $name; @@ -226,6 +228,7 @@ public function additionalProperties(bool $allowed = true): self */ public function strict(): self { + $this->isStrict = true; $this->additionalProperties = false; // Mark all properties as required @@ -236,6 +239,14 @@ public function strict(): self return $this; } + /** + * Check if strict mode is enabled. + */ + public function isStrict(): bool + { + return $this->isStrict; + } + /** * Set the JSON Schema version. */ diff --git a/src/Schema/Struct.php b/src/Schema/Struct.php index e098a25..0036766 100644 --- a/src/Schema/Struct.php +++ b/src/Schema/Struct.php @@ -2,10 +2,7 @@ namespace Blaspsoft\Forerunner\Schema; -/** - * @implements \ArrayAccess - */ -class Struct implements \ArrayAccess, \JsonSerializable +class Struct implements \JsonSerializable { protected Builder $builder; @@ -33,64 +30,30 @@ public static function define(string $name, callable $callback): self /** * Convert the schema to an array. + * If strict mode is enabled, wraps the schema in OpenAI's format. * * @return array */ public function toArray(): array { if ($this->cache === null) { - $this->cache = $this->builder->toArray(); + $builderArray = $this->builder->toArray(); + + // If strict mode is enabled, wrap in OpenAI's format + if ($this->builder->isStrict()) { + $this->cache = [ + 'name' => $this->name, + 'strict' => true, + 'schema' => $builderArray, + ]; + } else { + $this->cache = $builderArray; + } } return $this->cache; } - /** - * Convert the schema to a JSON string. - * - * @throws \JsonException - */ - public function toJson(): string - { - return $this->builder->toJson(); - } - - /** - * Check if an offset exists (ArrayAccess). - * - * @param string $offset - */ - public function offsetExists(mixed $offset): bool - { - return isset($this->toArray()[$offset]); - } - - /** - * Get an offset value (ArrayAccess). - * - * @param string $offset - */ - public function offsetGet(mixed $offset): mixed - { - return $this->toArray()[$offset]; - } - - /** - * Set an offset value (ArrayAccess) - not supported. - */ - public function offsetSet(mixed $offset, mixed $value): void - { - throw new \BadMethodCallException('Struct schemas are immutable'); - } - - /** - * Unset an offset (ArrayAccess) - not supported. - */ - public function offsetUnset(mixed $offset): void - { - throw new \BadMethodCallException('Struct schemas are immutable'); - } - /** * Specify data which should be serialized to JSON. * diff --git a/stubs/struct.stub b/stubs/struct.stub index 8114880..fb21f02 100644 --- a/stubs/struct.stub +++ b/stubs/struct.stub @@ -10,35 +10,15 @@ class {{ class }} /** * Define the structure schema. * - * @return Struct + * @return array */ - public static function schema(): Struct + public static function schema(): array { return Struct::define('{{ structName }}', function (Builder $builder) { $builder->string('example_field'); // Add your fields here $builder->strict(); // All fields required + no additional properties - }); - } - - /** - * Get the schema as a PHP array. - * - * @return array - */ - public static function toArray(): array - { - return static::schema()->toArray(); - } - - /** - * Get the schema as JSON string. - * - * @return string - */ - public static function toJson(): string - { - return static::schema()->toJson(); + })->toArray(); } } \ No newline at end of file diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index 3d1c539..f4d56bf 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -17,9 +17,9 @@ $table->string('city', 'City name')->required(); $table->string('zip', 'ZIP code')->required(); }, 'The address of the user'); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['type'])->toBe('object') ->and($schema['properties'])->toHaveKey('name') ->and($schema['properties'])->toHaveKey('age') diff --git a/tests/Feature/MakeStructCommandTest.php b/tests/Feature/MakeStructCommandTest.php index 8bee947..e3f1685 100644 --- a/tests/Feature/MakeStructCommandTest.php +++ b/tests/Feature/MakeStructCommandTest.php @@ -58,18 +58,9 @@ $content = File::get($this->testFilePath); - expect($content)->toContain('public static function schema(): Struct') - ->and($content)->toContain("Struct::define('test_user_struct'"); -}); - -it('generates struct with toJson method', function () { - $this->artisan('make:struct', ['name' => 'TestUserStruct']) - ->assertExitCode(0); - - $content = File::get($this->testFilePath); - - expect($content)->toContain('public static function toJson(): string') - ->and($content)->toContain('static::schema()->toJson()'); + expect($content)->toContain('public static function schema(): array') + ->and($content)->toContain("Struct::define('test_user_struct'") + ->and($content)->toContain('->toArray()'); }); it('generates struct with strict mode by default', function () { diff --git a/tests/Feature/SchemaFacadeTest.php b/tests/Feature/SchemaFacadeTest.php index a6e7244..5698636 100644 --- a/tests/Feature/SchemaFacadeTest.php +++ b/tests/Feature/SchemaFacadeTest.php @@ -2,16 +2,15 @@ use Blaspsoft\Forerunner\Facades\Schema; use Blaspsoft\Forerunner\Schema\Builder; -use Blaspsoft\Forerunner\Schema\Struct; describe('Schema Facade', function () { it('can define a schema using the facade', function () { $schema = Schema::define('User', function (Builder $builder) { $builder->string('name')->required(); $builder->string('email')->required(); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['type'])->toBe('object') ->and($schema['properties'])->toHaveKey('name') ->and($schema['properties'])->toHaveKey('email') @@ -29,9 +28,9 @@ })->required(); $builder->array('tags')->items('string'); $builder->enum('status', ['draft', 'published', 'archived'])->default('draft'); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['type'])->toBe('object') ->and($schema['properties'])->toHaveKey('title') ->and($schema['properties'])->toHaveKey('author') @@ -48,9 +47,9 @@ $builder->boolean('isActive'); $builder->array('items'); $builder->enum('role', ['admin', 'user']); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['properties']['text']['type'])->toBe('string') ->and($schema['properties']['count']['type'])->toBe('integer') ->and($schema['properties']['price']['type'])->toBe('number') @@ -73,9 +72,9 @@ $builder->array('tags') ->minItems(1) ->maxItems(10); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['properties']['username'])->toHaveKey('minLength', 3) ->and($schema['properties']['username'])->toHaveKey('maxLength', 50) ->and($schema['properties']['username'])->toHaveKey('pattern') @@ -96,9 +95,9 @@ $coords->float('longitude'); }); }); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['properties']['address']['type'])->toBe('object') ->and($schema['properties']['address']['properties'])->toHaveKey('coordinates') ->and($schema['properties']['address']['properties']['coordinates']['type'])->toBe('object') @@ -113,9 +112,9 @@ $user->string('email')->required(); $user->int('age'); }); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['properties']['users']['type'])->toBe('array') ->and($schema['properties']['users']['items']['type'])->toBe('object') ->and($schema['properties']['users']['items']['properties'])->toHaveKey('name') @@ -128,9 +127,9 @@ $builder->description('A schema with descriptions'); $builder->string('name')->description('The user name'); $builder->string('email')->description('The user email address'); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema)->toHaveKey('description', 'A schema with descriptions') ->and($schema['properties']['name'])->toHaveKey('description', 'The user name') ->and($schema['properties']['email'])->toHaveKey('description', 'The user email address'); @@ -141,9 +140,9 @@ $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); - }); + })->toArray(); - expect($schema)->toBeInstanceOf(Struct::class) + expect($schema)->toBeArray() ->and($schema['properties']['notifications']['default'])->toBe(true) ->and($schema['properties']['theme']['default'])->toBe('light') ->and($schema['properties']['pageSize']['default'])->toBe(10); diff --git a/tests/Unit/AdvancedFeaturesTest.php b/tests/Unit/AdvancedFeaturesTest.php index a81d78e..f570e65 100644 --- a/tests/Unit/AdvancedFeaturesTest.php +++ b/tests/Unit/AdvancedFeaturesTest.php @@ -340,7 +340,7 @@ }); describe('complete schema with all features', function () { - it('generates comprehensive schema with all features', function () { + it('generates comprehensive schema with all features in OpenAI format when strict', function () { $schema = Struct::define('CompleteExample', function (Builder $builder) { $builder->schemaVersion(); $builder->title('Complete Schema Example'); @@ -365,18 +365,41 @@ $nested->string('version')->required(); $nested->int('count')->min(0); })->nullable(); - }); + })->toArray(); + + // Check OpenAI format wrapper + expect($schema)->toHaveKey('name', 'CompleteExample') + ->and($schema)->toHaveKey('strict', true) + ->and($schema)->toHaveKey('schema'); + + // Check the nested schema structure + $nestedSchema = $schema['schema']; + expect($nestedSchema)->toHaveKey('$schema') + ->and($nestedSchema)->toHaveKey('title', 'Complete Schema Example') + ->and($nestedSchema)->toHaveKey('description') + ->and($nestedSchema)->toHaveKey('additionalProperties', false) + ->and($nestedSchema['properties']['id']['format'])->toBe('uuid') + ->and($nestedSchema['properties']['email']['format'])->toBe('email') + ->and($nestedSchema['properties']['website']['type'])->toBe(['string', 'null']) + ->and($nestedSchema['properties']['tags']['uniqueItems'])->toBeTrue() + ->and($nestedSchema['properties']['metadata']['type'])->toBe(['object', 'null']) + ->and($nestedSchema['required'])->toContain('id', 'email', 'created_at'); + }); + + it('generates normal schema without strict mode', function () { + $schema = Struct::define('NormalExample', function (Builder $builder) { + $builder->title('Normal Schema Example'); + $builder->uuid('id')->required(); + $builder->email('email')->required(); + })->toArray(); - expect($schema)->toHaveKey('$schema') - ->and($schema)->toHaveKey('title', 'Complete Schema Example') - ->and($schema)->toHaveKey('description') - ->and($schema)->toHaveKey('additionalProperties', false) + // Without strict(), should return normal flat schema + expect($schema)->toHaveKey('type', 'object') + ->and($schema)->toHaveKey('title', 'Normal Schema Example') + ->and($schema)->not->toHaveKey('name') + ->and($schema)->not->toHaveKey('strict') ->and($schema['properties']['id']['format'])->toBe('uuid') - ->and($schema['properties']['email']['format'])->toBe('email') - ->and($schema['properties']['website']['type'])->toBe(['string', 'null']) - ->and($schema['properties']['tags']['uniqueItems'])->toBeTrue() - ->and($schema['properties']['metadata']['type'])->toBe(['object', 'null']) - ->and($schema['required'])->toContain('id', 'email', 'created_at'); + ->and($schema['properties']['email']['format'])->toBe('email'); }); }); }); diff --git a/tests/Unit/StructTest.php b/tests/Unit/StructTest.php index f3c69ba..5582cc3 100644 --- a/tests/Unit/StructTest.php +++ b/tests/Unit/StructTest.php @@ -32,26 +32,13 @@ $builder->string('name')->required(); $builder->string('email')->required(); $builder->int('age'); - }); + })->toArray(); - // Can be used like an array expect($schema['required'])->toContain('name') ->and($schema['required'])->toContain('email') ->and($schema['required'])->not->toContain('age'); }); - it('can access struct as array via ArrayAccess', function () { - $schema = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); - }); - - // Array access works - expect(isset($schema['type']))->toBeTrue() - ->and($schema['type'])->toBe('object') - ->and(isset($schema['properties']))->toBeTrue() - ->and(isset($schema['properties']['name']))->toBeTrue(); - }); - it('can define schema with nested objects', function () { $schema = Struct::define('Post', function (Builder $builder) { $builder->string('title')->required(); @@ -59,7 +46,7 @@ $nested->string('name')->required(); $nested->string('email')->required(); })->required(); - }); + })->toArray(); expect($schema['properties'])->toHaveKey('author') ->and($schema['properties']['author']['type'])->toBe('object') @@ -73,7 +60,7 @@ $schema = Struct::define('Product', function (Builder $builder) { $builder->string('name'); $builder->array('tags')->items('string'); - }); + })->toArray(); expect($schema['properties'])->toHaveKey('tags') ->and($schema['properties']['tags']['type'])->toBe('array') @@ -84,7 +71,7 @@ $schema = Struct::define('User', function (Builder $builder) { $builder->string('name'); $builder->enum('role', ['admin', 'user', 'guest']); - }); + })->toArray(); expect($schema['properties'])->toHaveKey('role') ->and($schema['properties']['role']['type'])->toBe('string') @@ -98,7 +85,7 @@ $builder->float('price'); $builder->boolean('isActive'); $builder->array('items'); - }); + })->toArray(); expect($schema['properties']['text']['type'])->toBe('string') ->and($schema['properties']['count']['type'])->toBe('integer') @@ -116,7 +103,7 @@ $builder->int('age') ->min(0) ->max(150); - }); + })->toArray(); expect($schema['properties']['username'])->toHaveKey('minLength', 3) ->and($schema['properties']['username'])->toHaveKey('maxLength', 50) @@ -128,7 +115,7 @@ $schema = Struct::define('User', function (Builder $builder) { $builder->description('A user object'); $builder->string('name'); - }); + })->toArray(); expect($schema)->toHaveKey('description', 'A user object'); }); @@ -149,7 +136,7 @@ }); $builder->array('tags')->items('string'); $builder->enum('status', ['draft', 'published', 'archived'])->default('draft'); - }); + })->toArray(); expect($schema['type'])->toBe('object') ->and($schema['properties'])->toHaveKey('title') @@ -167,7 +154,7 @@ it('can define empty schema', function () { $schema = Struct::define('Empty', function (Builder $builder) { // No properties - }); + })->toArray(); expect($schema['type'])->toBe('object') ->and($schema['properties'])->toBeArray() @@ -180,7 +167,7 @@ $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); - }); + })->toArray(); expect($schema['properties']['notifications']['default'])->toBe(true) ->and($schema['properties']['theme']['default'])->toBe('light') @@ -191,7 +178,7 @@ $schema = Struct::define('Contact', function (Builder $builder) { $builder->string('email')->pattern('^[^@]+@[^@]+\.[^@]+$'); $builder->string('phone')->pattern('^\d{3}-\d{3}-\d{4}$'); - }); + })->toArray(); expect($schema['properties']['email'])->toHaveKey('pattern') ->and($schema['properties']['phone'])->toHaveKey('pattern'); @@ -208,7 +195,7 @@ $coords->float('longitude'); }); }); - }); + })->toArray(); expect($schema['properties']['address']['type'])->toBe('object') ->and($schema['properties']['address']['properties'])->toHaveKey('coordinates') @@ -223,33 +210,13 @@ ->minItems(1) ->maxItems(10) ->items('string'); - }); + })->toArray(); expect($schema['properties']['items'])->toHaveKey('minItems', 1) ->and($schema['properties']['items'])->toHaveKey('maxItems', 10) ->and($schema['properties']['items']['items'])->toBe(['type' => 'string']); }); - it('can generate JSON string directly with toJson method', function () { - $json = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); - $builder->email('email')->required(); - })->toJson(); - - expect($json)->toBeString() - ->and($json)->toContain('"type": "object"') - ->and($json)->toContain('"name"') - ->and($json)->toContain('"email"') - ->and($json)->toContain('"format": "email"') - ->and($json)->toContain('"required"'); - - $decoded = json_decode($json, true); - expect($decoded)->toBeArray() - ->and($decoded['type'])->toBe('object') - ->and($decoded['properties'])->toHaveKey('name') - ->and($decoded['properties'])->toHaveKey('email'); - }); - it('can be serialized with json_encode', function () { $schema = Struct::define('User', function (Builder $builder) { $builder->string('name')->required(); From 2696acea9de89461579081d687ec464a9dff38db Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 22:19:31 +0100 Subject: [PATCH 03/12] Add declare(strict_types=1) to all PHP files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add strict types declaration to all src files - src/Schema/Struct.php - src/Schema/Builder.php - src/Schema/PropertyBuilder.php - src/ForerunnerServiceProvider.php - src/Commands/MakeStructCommand.php - src/Facades/Schema.php - Add strict types declaration to all test files - tests/Unit/*.php - tests/Feature/*.php - tests/*.php - All 118 tests passing (400 assertions) - PHPStan Level 9 clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Commands/MakeStructCommand.php | 2 ++ src/Facades/Schema.php | 2 ++ src/ForerunnerServiceProvider.php | 2 ++ src/Schema/Builder.php | 2 ++ src/Schema/PropertyBuilder.php | 2 ++ src/Schema/Struct.php | 2 ++ tests/Feature/ExampleTest.php | 1 + tests/Feature/IntendedUsageTest.php | 1 + tests/Feature/MakeStructCommandTest.php | 1 + tests/Feature/SchemaFacadeTest.php | 1 + tests/Pest.php | 1 + tests/TestCase.php | 1 + tests/Unit/AdvancedFeaturesTest.php | 1 + tests/Unit/BuilderTest.php | 1 + tests/Unit/PropertyBuilderTest.php | 1 + tests/Unit/StructTest.php | 1 + 16 files changed, 22 insertions(+) diff --git a/src/Commands/MakeStructCommand.php b/src/Commands/MakeStructCommand.php index 32a7bdd..43f8e57 100644 --- a/src/Commands/MakeStructCommand.php +++ b/src/Commands/MakeStructCommand.php @@ -1,5 +1,7 @@ toBeTrue(); }); diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index f4d56bf..5ddef73 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -1,5 +1,6 @@ in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php index e055f90..5a6297d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,5 +1,6 @@ Date: Fri, 17 Oct 2025 22:24:04 +0100 Subject: [PATCH 04/12] Add optional description parameter to Struct::define() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add optional third parameter to Struct::define($name, $callback, $description) - Automatically sets schema description when provided - Backward compatible - existing code without description continues to work - All 118 tests passing (400 assertions) - PHPStan Level 9 clean Example usage: ```php Struct::define( 'user_profile', fn($b) => $b->string('name'), 'A user profile schema' ); ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Schema/Struct.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Schema/Struct.php b/src/Schema/Struct.php index 3749299..faedc8a 100644 --- a/src/Schema/Struct.php +++ b/src/Schema/Struct.php @@ -22,9 +22,14 @@ protected function __construct(string $name, Builder $builder) /** * Define a new structure schema. */ - public static function define(string $name, callable $callback): self + public static function define(string $name, callable $callback, ?string $description = null): self { $builder = new Builder($name); + + if ($description !== null) { + $builder->description($description); + } + $callback($builder); return new self($name, $builder); From f64dc42366d9c629a4e29001cd4e64f666e25f24 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 22:29:31 +0100 Subject: [PATCH 05/12] Reorder Struct::define() parameters for better API ergonomics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move description parameter to second position for more intuitive usage: - Before: Struct::define($name, $callback, $description) - After: Struct::define($name, $description, $callback) This ordering aligns with common API patterns where optional metadata comes before the main callback function. Tests pass null when description is not needed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Schema/Struct.php | 2 +- tests/Feature/IntendedUsageTest.php | 2 +- tests/Feature/SchemaFacadeTest.php | 16 +++++++-------- tests/Unit/AdvancedFeaturesTest.php | 4 ++-- tests/Unit/StructTest.php | 32 ++++++++++++++--------------- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Schema/Struct.php b/src/Schema/Struct.php index faedc8a..f1fabdc 100644 --- a/src/Schema/Struct.php +++ b/src/Schema/Struct.php @@ -22,7 +22,7 @@ protected function __construct(string $name, Builder $builder) /** * Define a new structure schema. */ - public static function define(string $name, callable $callback, ?string $description = null): self + public static function define(string $name, ?string $description, callable $callback): self { $builder = new Builder($name); diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index 5ddef73..6ecf05f 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -5,7 +5,7 @@ use Blaspsoft\Forerunner\Schema\Struct; it('works with the intended API usage', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->string('name', 'The name of the user')->minLength(1)->maxLength(100)->required(); $builder->int('age', 'The age of the user')->min(0)->max(150); $builder->boolean('is_active', 'Is the user active?')->default(true); diff --git a/tests/Feature/SchemaFacadeTest.php b/tests/Feature/SchemaFacadeTest.php index c814dc2..a303682 100644 --- a/tests/Feature/SchemaFacadeTest.php +++ b/tests/Feature/SchemaFacadeTest.php @@ -6,7 +6,7 @@ describe('Schema Facade', function () { it('can define a schema using the facade', function () { - $schema = Schema::define('User', function (Builder $builder) { + $schema = Schema::define('User', null, function (Builder $builder) { $builder->string('name')->required(); $builder->string('email')->required(); })->toArray(); @@ -20,7 +20,7 @@ }); it('can define complex schemas using the facade', function () { - $schema = Schema::define('BlogPost', function (Builder $builder) { + $schema = Schema::define('BlogPost', null, function (Builder $builder) { $builder->string('title')->required(); $builder->string('content')->required(); $builder->object('author', function (Builder $author) { @@ -41,7 +41,7 @@ }); it('can define schemas with all field types', function () { - $schema = Schema::define('CompleteExample', function (Builder $builder) { + $schema = Schema::define('CompleteExample', null, function (Builder $builder) { $builder->string('text'); $builder->int('count'); $builder->float('price'); @@ -61,7 +61,7 @@ }); it('can define schemas with validation constraints', function () { - $schema = Schema::define('ValidationExample', function (Builder $builder) { + $schema = Schema::define('ValidationExample', null, function (Builder $builder) { $builder->string('username') ->required() ->minLength(3) @@ -86,7 +86,7 @@ }); it('can define schemas with nested objects', function () { - $schema = Schema::define('Company', function (Builder $builder) { + $schema = Schema::define('Company', null, function (Builder $builder) { $builder->string('name')->required(); $builder->object('address', function (Builder $address) { $address->string('street'); @@ -107,7 +107,7 @@ }); it('can define schemas with array of objects', function () { - $schema = Schema::define('UserList', function (Builder $builder) { + $schema = Schema::define('UserList', null, function (Builder $builder) { $builder->array('users')->items('object', function (Builder $user) { $user->string('name')->required(); $user->string('email')->required(); @@ -124,7 +124,7 @@ }); it('can define schemas with descriptions', function () { - $schema = Schema::define('DescribedSchema', function (Builder $builder) { + $schema = Schema::define('DescribedSchema', null, function (Builder $builder) { $builder->description('A schema with descriptions'); $builder->string('name')->description('The user name'); $builder->string('email')->description('The user email address'); @@ -137,7 +137,7 @@ }); it('can define schemas with default values', function () { - $schema = Schema::define('DefaultsExample', function (Builder $builder) { + $schema = Schema::define('DefaultsExample', null, function (Builder $builder) { $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); diff --git a/tests/Unit/AdvancedFeaturesTest.php b/tests/Unit/AdvancedFeaturesTest.php index f4cf500..debc1e1 100644 --- a/tests/Unit/AdvancedFeaturesTest.php +++ b/tests/Unit/AdvancedFeaturesTest.php @@ -342,7 +342,7 @@ describe('complete schema with all features', function () { it('generates comprehensive schema with all features in OpenAI format when strict', function () { - $schema = Struct::define('CompleteExample', function (Builder $builder) { + $schema = Struct::define('CompleteExample', null, function (Builder $builder) { $builder->schemaVersion(); $builder->title('Complete Schema Example'); $builder->description('A comprehensive schema demonstrating all features'); @@ -388,7 +388,7 @@ }); it('generates normal schema without strict mode', function () { - $schema = Struct::define('NormalExample', function (Builder $builder) { + $schema = Struct::define('NormalExample', null, function (Builder $builder) { $builder->title('Normal Schema Example'); $builder->uuid('id')->required(); $builder->email('email')->required(); diff --git a/tests/Unit/StructTest.php b/tests/Unit/StructTest.php index 9bb1663..39e8685 100644 --- a/tests/Unit/StructTest.php +++ b/tests/Unit/StructTest.php @@ -6,7 +6,7 @@ describe('Struct', function () { it('can define a simple schema', function () { - $struct = Struct::define('User', function (Builder $builder) { + $struct = Struct::define('User', null, function (Builder $builder) { $builder->string('name'); $builder->string('email'); }); @@ -21,7 +21,7 @@ }); it('returns a Struct instance from define method', function () { - $struct = Struct::define('Simple', function (Builder $builder) { + $struct = Struct::define('Simple', null, function (Builder $builder) { $builder->string('field'); }); @@ -29,7 +29,7 @@ }); it('can define schema with required fields', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->string('name')->required(); $builder->string('email')->required(); $builder->int('age'); @@ -41,7 +41,7 @@ }); it('can define schema with nested objects', function () { - $schema = Struct::define('Post', function (Builder $builder) { + $schema = Struct::define('Post', null, function (Builder $builder) { $builder->string('title')->required(); $builder->object('author', function (Builder $nested) { $nested->string('name')->required(); @@ -58,7 +58,7 @@ }); it('can define schema with array fields', function () { - $schema = Struct::define('Product', function (Builder $builder) { + $schema = Struct::define('Product', null, function (Builder $builder) { $builder->string('name'); $builder->array('tags')->items('string'); })->toArray(); @@ -69,7 +69,7 @@ }); it('can define schema with enum fields', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->string('name'); $builder->enum('role', ['admin', 'user', 'guest']); })->toArray(); @@ -80,7 +80,7 @@ }); it('can define schema with all primitive types', function () { - $schema = Struct::define('AllTypes', function (Builder $builder) { + $schema = Struct::define('AllTypes', null, function (Builder $builder) { $builder->string('text'); $builder->int('count'); $builder->float('price'); @@ -96,7 +96,7 @@ }); it('can define schema with property constraints', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->string('username') ->required() ->minLength(3) @@ -113,7 +113,7 @@ }); it('can define schema with description', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->description('A user object'); $builder->string('name'); })->toArray(); @@ -122,7 +122,7 @@ }); it('can define complex nested schema', function () { - $schema = Struct::define('BlogPost', function (Builder $builder) { + $schema = Struct::define('BlogPost', null, function (Builder $builder) { $builder->string('title')->required(); $builder->string('content')->required(); $builder->object('author', function (Builder $author) { @@ -153,7 +153,7 @@ }); it('can define empty schema', function () { - $schema = Struct::define('Empty', function (Builder $builder) { + $schema = Struct::define('Empty', null, function (Builder $builder) { // No properties })->toArray(); @@ -164,7 +164,7 @@ }); it('can define schema with default values', function () { - $schema = Struct::define('Settings', function (Builder $builder) { + $schema = Struct::define('Settings', null, function (Builder $builder) { $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); @@ -176,7 +176,7 @@ }); it('can define schema with pattern validation', function () { - $schema = Struct::define('Contact', function (Builder $builder) { + $schema = Struct::define('Contact', null, function (Builder $builder) { $builder->string('email')->pattern('^[^@]+@[^@]+\.[^@]+$'); $builder->string('phone')->pattern('^\d{3}-\d{3}-\d{4}$'); })->toArray(); @@ -186,7 +186,7 @@ }); it('can define schema with deeply nested objects', function () { - $schema = Struct::define('Company', function (Builder $builder) { + $schema = Struct::define('Company', null, function (Builder $builder) { $builder->string('name'); $builder->object('address', function (Builder $address) { $address->string('street'); @@ -206,7 +206,7 @@ }); it('can define schema with array constraints', function () { - $schema = Struct::define('List', function (Builder $builder) { + $schema = Struct::define('List', null, function (Builder $builder) { $builder->array('items') ->minItems(1) ->maxItems(10) @@ -219,7 +219,7 @@ }); it('can be serialized with json_encode', function () { - $schema = Struct::define('User', function (Builder $builder) { + $schema = Struct::define('User', null, function (Builder $builder) { $builder->string('name')->required(); }); From 766c5d749305d76ee8517429c3752f7c0c34a578 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 22:39:28 +0100 Subject: [PATCH 06/12] Make description parameter required in Struct::define() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes description parameter from optional (?string) to required (string) for better LLM integration: - OpenAI Structured Outputs: Benefits from clear schema descriptions - Anthropic Claude Tool Use: Requires description as critical field - Forces developers to document schemas for better LLM understanding Updated signature: Struct::define(string $name, string $description, callable $callback) All tests updated with meaningful descriptions. The stub template now includes a placeholder description for generated structs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Schema/Struct.php | 7 ++----- stubs/struct.stub | 2 +- tests/Feature/IntendedUsageTest.php | 2 +- tests/Feature/SchemaFacadeTest.php | 16 +++++++-------- tests/Unit/AdvancedFeaturesTest.php | 4 ++-- tests/Unit/StructTest.php | 32 ++++++++++++++--------------- 6 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/Schema/Struct.php b/src/Schema/Struct.php index f1fabdc..cffd2dd 100644 --- a/src/Schema/Struct.php +++ b/src/Schema/Struct.php @@ -22,13 +22,10 @@ protected function __construct(string $name, Builder $builder) /** * Define a new structure schema. */ - public static function define(string $name, ?string $description, callable $callback): self + public static function define(string $name, string $description, callable $callback): self { $builder = new Builder($name); - - if ($description !== null) { - $builder->description($description); - } + $builder->description($description); $callback($builder); diff --git a/stubs/struct.stub b/stubs/struct.stub index fb21f02..f941853 100644 --- a/stubs/struct.stub +++ b/stubs/struct.stub @@ -14,7 +14,7 @@ class {{ class }} */ public static function schema(): array { - return Struct::define('{{ structName }}', function (Builder $builder) { + return Struct::define('{{ structName }}', 'Description of {{ structName }}', function (Builder $builder) { $builder->string('example_field'); // Add your fields here diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index 6ecf05f..9bb8fa0 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -5,7 +5,7 @@ use Blaspsoft\Forerunner\Schema\Struct; it('works with the intended API usage', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user object with personal information', function (Builder $builder) { $builder->string('name', 'The name of the user')->minLength(1)->maxLength(100)->required(); $builder->int('age', 'The age of the user')->min(0)->max(150); $builder->boolean('is_active', 'Is the user active?')->default(true); diff --git a/tests/Feature/SchemaFacadeTest.php b/tests/Feature/SchemaFacadeTest.php index a303682..a515717 100644 --- a/tests/Feature/SchemaFacadeTest.php +++ b/tests/Feature/SchemaFacadeTest.php @@ -6,7 +6,7 @@ describe('Schema Facade', function () { it('can define a schema using the facade', function () { - $schema = Schema::define('User', null, function (Builder $builder) { + $schema = Schema::define('User', 'A user schema', function (Builder $builder) { $builder->string('name')->required(); $builder->string('email')->required(); })->toArray(); @@ -20,7 +20,7 @@ }); it('can define complex schemas using the facade', function () { - $schema = Schema::define('BlogPost', null, function (Builder $builder) { + $schema = Schema::define('BlogPost', 'A blog post schema', function (Builder $builder) { $builder->string('title')->required(); $builder->string('content')->required(); $builder->object('author', function (Builder $author) { @@ -41,7 +41,7 @@ }); it('can define schemas with all field types', function () { - $schema = Schema::define('CompleteExample', null, function (Builder $builder) { + $schema = Schema::define('CompleteExample', 'Complete example schema', function (Builder $builder) { $builder->string('text'); $builder->int('count'); $builder->float('price'); @@ -61,7 +61,7 @@ }); it('can define schemas with validation constraints', function () { - $schema = Schema::define('ValidationExample', null, function (Builder $builder) { + $schema = Schema::define('ValidationExample', 'Validation example schema', function (Builder $builder) { $builder->string('username') ->required() ->minLength(3) @@ -86,7 +86,7 @@ }); it('can define schemas with nested objects', function () { - $schema = Schema::define('Company', null, function (Builder $builder) { + $schema = Schema::define('Company', 'Company information', function (Builder $builder) { $builder->string('name')->required(); $builder->object('address', function (Builder $address) { $address->string('street'); @@ -107,7 +107,7 @@ }); it('can define schemas with array of objects', function () { - $schema = Schema::define('UserList', null, function (Builder $builder) { + $schema = Schema::define('UserList', 'A list of users', function (Builder $builder) { $builder->array('users')->items('object', function (Builder $user) { $user->string('name')->required(); $user->string('email')->required(); @@ -124,7 +124,7 @@ }); it('can define schemas with descriptions', function () { - $schema = Schema::define('DescribedSchema', null, function (Builder $builder) { + $schema = Schema::define('DescribedSchema', 'A schema with descriptions', function (Builder $builder) { $builder->description('A schema with descriptions'); $builder->string('name')->description('The user name'); $builder->string('email')->description('The user email address'); @@ -137,7 +137,7 @@ }); it('can define schemas with default values', function () { - $schema = Schema::define('DefaultsExample', null, function (Builder $builder) { + $schema = Schema::define('DefaultsExample', 'Schema with defaults', function (Builder $builder) { $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); diff --git a/tests/Unit/AdvancedFeaturesTest.php b/tests/Unit/AdvancedFeaturesTest.php index debc1e1..b4efaa2 100644 --- a/tests/Unit/AdvancedFeaturesTest.php +++ b/tests/Unit/AdvancedFeaturesTest.php @@ -342,7 +342,7 @@ describe('complete schema with all features', function () { it('generates comprehensive schema with all features in OpenAI format when strict', function () { - $schema = Struct::define('CompleteExample', null, function (Builder $builder) { + $schema = Struct::define('CompleteExample', 'Complete example with all features', function (Builder $builder) { $builder->schemaVersion(); $builder->title('Complete Schema Example'); $builder->description('A comprehensive schema demonstrating all features'); @@ -388,7 +388,7 @@ }); it('generates normal schema without strict mode', function () { - $schema = Struct::define('NormalExample', null, function (Builder $builder) { + $schema = Struct::define('NormalExample', 'Normal example without strict mode', function (Builder $builder) { $builder->title('Normal Schema Example'); $builder->uuid('id')->required(); $builder->email('email')->required(); diff --git a/tests/Unit/StructTest.php b/tests/Unit/StructTest.php index 39e8685..defd623 100644 --- a/tests/Unit/StructTest.php +++ b/tests/Unit/StructTest.php @@ -6,7 +6,7 @@ describe('Struct', function () { it('can define a simple schema', function () { - $struct = Struct::define('User', null, function (Builder $builder) { + $struct = Struct::define('User', 'A user schema', function (Builder $builder) { $builder->string('name'); $builder->string('email'); }); @@ -21,7 +21,7 @@ }); it('returns a Struct instance from define method', function () { - $struct = Struct::define('Simple', null, function (Builder $builder) { + $struct = Struct::define('Simple', 'A simple schema', function (Builder $builder) { $builder->string('field'); }); @@ -29,7 +29,7 @@ }); it('can define schema with required fields', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user with required fields', function (Builder $builder) { $builder->string('name')->required(); $builder->string('email')->required(); $builder->int('age'); @@ -41,7 +41,7 @@ }); it('can define schema with nested objects', function () { - $schema = Struct::define('Post', null, function (Builder $builder) { + $schema = Struct::define('Post', 'A blog post schema', function (Builder $builder) { $builder->string('title')->required(); $builder->object('author', function (Builder $nested) { $nested->string('name')->required(); @@ -58,7 +58,7 @@ }); it('can define schema with array fields', function () { - $schema = Struct::define('Product', null, function (Builder $builder) { + $schema = Struct::define('Product', 'A product schema', function (Builder $builder) { $builder->string('name'); $builder->array('tags')->items('string'); })->toArray(); @@ -69,7 +69,7 @@ }); it('can define schema with enum fields', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user schema', function (Builder $builder) { $builder->string('name'); $builder->enum('role', ['admin', 'user', 'guest']); })->toArray(); @@ -80,7 +80,7 @@ }); it('can define schema with all primitive types', function () { - $schema = Struct::define('AllTypes', null, function (Builder $builder) { + $schema = Struct::define('AllTypes', 'Schema with all types', function (Builder $builder) { $builder->string('text'); $builder->int('count'); $builder->float('price'); @@ -96,7 +96,7 @@ }); it('can define schema with property constraints', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user schema', function (Builder $builder) { $builder->string('username') ->required() ->minLength(3) @@ -113,7 +113,7 @@ }); it('can define schema with description', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user schema', function (Builder $builder) { $builder->description('A user object'); $builder->string('name'); })->toArray(); @@ -122,7 +122,7 @@ }); it('can define complex nested schema', function () { - $schema = Struct::define('BlogPost', null, function (Builder $builder) { + $schema = Struct::define('BlogPost', 'A blog post with comments', function (Builder $builder) { $builder->string('title')->required(); $builder->string('content')->required(); $builder->object('author', function (Builder $author) { @@ -153,7 +153,7 @@ }); it('can define empty schema', function () { - $schema = Struct::define('Empty', null, function (Builder $builder) { + $schema = Struct::define('Empty', 'An empty schema', function (Builder $builder) { // No properties })->toArray(); @@ -164,7 +164,7 @@ }); it('can define schema with default values', function () { - $schema = Struct::define('Settings', null, function (Builder $builder) { + $schema = Struct::define('Settings', 'Application settings', function (Builder $builder) { $builder->boolean('notifications')->default(true); $builder->string('theme')->default('light'); $builder->int('pageSize')->default(10); @@ -176,7 +176,7 @@ }); it('can define schema with pattern validation', function () { - $schema = Struct::define('Contact', null, function (Builder $builder) { + $schema = Struct::define('Contact', 'Contact information', function (Builder $builder) { $builder->string('email')->pattern('^[^@]+@[^@]+\.[^@]+$'); $builder->string('phone')->pattern('^\d{3}-\d{3}-\d{4}$'); })->toArray(); @@ -186,7 +186,7 @@ }); it('can define schema with deeply nested objects', function () { - $schema = Struct::define('Company', null, function (Builder $builder) { + $schema = Struct::define('Company', 'Company information', function (Builder $builder) { $builder->string('name'); $builder->object('address', function (Builder $address) { $address->string('street'); @@ -206,7 +206,7 @@ }); it('can define schema with array constraints', function () { - $schema = Struct::define('List', null, function (Builder $builder) { + $schema = Struct::define('List', 'A list of items', function (Builder $builder) { $builder->array('items') ->minItems(1) ->maxItems(10) @@ -219,7 +219,7 @@ }); it('can be serialized with json_encode', function () { - $schema = Struct::define('User', null, function (Builder $builder) { + $schema = Struct::define('User', 'A user schema', function (Builder $builder) { $builder->string('name')->required(); }); From a3b5a8fcd976c079370c9c80300f7fb392910989 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 22:53:54 +0100 Subject: [PATCH 07/12] Rename Builder to Property for better API semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major refactoring to improve code readability and match the fluent API pattern: Changes: - Renamed Builder.php → Property.php - Updated class name from Builder to Property - Changed all callback parameters from $builder to $property - Updated all imports across codebase - Updated README documentation with new syntax New API pattern: ```php Struct::define('User', 'User schema', function (Property $property) { $property->string('name', 'Full name')->required(); $property->email('email', 'Email address')->required(); }); ``` This better reflects that we're defining properties of a schema, not building the entire structure. The Property class provides methods to define individual schema properties with their constraints. All tests passing (118/118), PHPStan Level 9 clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 206 +++++++++++------------ src/Schema/{Builder.php => Property.php} | 4 +- src/Schema/PropertyBuilder.php | 6 +- src/Schema/Struct.php | 6 +- stubs/struct.stub | 8 +- tests/Feature/IntendedUsageTest.php | 20 +-- tests/Feature/MakeStructCommandTest.php | 4 +- tests/Feature/SchemaFacadeTest.php | 70 ++++---- tests/Unit/AdvancedFeaturesTest.php | 198 +++++++++++----------- tests/Unit/BuilderTest.php | 134 +++++++-------- tests/Unit/PropertyBuilderTest.php | 12 +- tests/Unit/StructTest.php | 108 ++++++------ 12 files changed, 388 insertions(+), 388 deletions(-) rename src/Schema/{Builder.php => Property.php} (99%) diff --git a/README.md b/README.md index a9742fc..98dd617 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,13 @@ This creates a structure class at `app/Structures/UserProfile.php`: namespace App\Structures; use Blaspsoft\Forerunner\Schema\Struct; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; class UserProfile { public static function schema(): array { - return Struct::define('user_profile', function (Builder $table) { + return Struct::define('user_profile', function (Property $table) { $table->string('example_field')->required(); // Add your fields here }); @@ -65,13 +65,13 @@ Define a schema using the `Struct` class or `Schema` facade: ```php use Blaspsoft\Forerunner\Schema\Struct; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('name', 'The user\'s full name')->required(); - $builder->string('email', 'The user\'s email address')->required(); - $builder->int('age', 'The user\'s age')->min(0)->max(150); - $builder->boolean('is_active', 'Is the user account active?')->default(true); +$schema = Struct::define('User', function (Property $property) { + $property->string('name', 'The user\'s full name')->required(); + $property->string('email', 'The user\'s email address')->required(); + $property->int('age', 'The user\'s age')->min(0)->max(150); + $property->boolean('is_active', 'Is the user account active?')->default(true); }); ``` @@ -79,11 +79,11 @@ Or using the facade: ```php use Blaspsoft\Forerunner\Facades\Schema; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; -$schema = Schema::define('User', function (Builder $builder) { - $builder->string('name')->required(); - $builder->string('email')->required(); +$schema = Schema::define('User', function (Property $property) { + $property->string('name')->required(); + $property->string('email')->required(); }); ``` @@ -92,7 +92,7 @@ $schema = Schema::define('User', function (Builder $builder) { ### String Fields ```php -$builder->string('username', 'The username') +$property->string('username', 'The username') ->minLength(3) ->maxLength(50) ->pattern('^[a-zA-Z0-9_]+$') @@ -102,47 +102,47 @@ $builder->string('username', 'The username') ### Integer Fields ```php -$builder->int('age', 'User age') +$property->int('age', 'User age') ->min(0) ->max(150) ->default(18); // Alias -$builder->integer('count'); +$property->integer('count'); ``` ### Float/Number Fields ```php -$builder->float('price', 'Product price') +$property->float('price', 'Product price') ->min(0.0) ->max(9999.99); // Alias -$builder->number('rating')->min(0)->max(5); +$property->number('rating')->min(0)->max(5); ``` ### Boolean Fields ```php -$builder->boolean('is_active', 'Account status') +$property->boolean('is_active', 'Account status') ->default(true); // Alias -$builder->bool('verified'); +$property->bool('verified'); ``` ### Array Fields ```php // Simple array -$builder->array('tags', 'User tags') +$property->array('tags', 'User tags') ->items('string') ->minItems(1) ->maxItems(10); // Array of objects -$builder->array('addresses')->items('object', function (Builder $item) { +$property->array('addresses')->items('object', function (Property $item) { $item->string('street')->required(); $item->string('city')->required(); $item->string('zip')->required(); @@ -152,20 +152,20 @@ $builder->array('addresses')->items('object', function (Builder $item) { ### Enum Fields ```php -$builder->enum('role', ['admin', 'user', 'guest'], 'User role') +$property->enum('role', ['admin', 'user', 'guest'], 'User role') ->default('user'); -$builder->enum('status', ['draft', 'published', 'archived']); +$property->enum('status', ['draft', 'published', 'archived']); ``` ### Object Fields ```php -$builder->object('address', function (Builder $nested) { +$property->object('address', function (Property $nested) { $nested->string('street', 'Street address')->required(); $nested->string('city', 'City name')->required(); $nested->string('zip', 'ZIP code')->required(); - $nested->object('coordinates', function (Builder $coords) { + $nested->object('coordinates', function (Property $coords) { $coords->float('latitude')->required(); $coords->float('longitude')->required(); }); @@ -177,7 +177,7 @@ $builder->object('address', function (Builder $nested) { ### String Constraints ```php -$builder->string('username') +$property->string('username') ->minLength(3) // Minimum length ->maxLength(50) // Maximum length ->pattern('^[a-zA-Z0-9]+$') // Regex pattern @@ -187,7 +187,7 @@ $builder->string('username') ### Numeric Constraints ```php -$builder->int('age') +$property->int('age') ->min(0) // Minimum value ->max(150) // Maximum value ->default(18); // Default value @@ -196,7 +196,7 @@ $builder->int('age') ### Array Constraints ```php -$builder->array('tags') +$property->array('tags') ->items('string') // Type of array items ->minItems(1) // Minimum array length ->maxItems(10); // Maximum array length @@ -205,7 +205,7 @@ $builder->array('tags') ### General Constraints ```php -$builder->string('field') +$property->string('field') ->required() // Mark as required ->optional() // Mark as optional (default) ->default('value') // Set default value @@ -220,31 +220,31 @@ Forerunner provides convenient helper methods for commonly used field formats: ```php // Email field with automatic format validation -$builder->email('email')->required(); +$property->email('email')->required(); // URL field -$builder->url('website'); +$property->url('website'); // UUID field -$builder->uuid('id')->required(); +$property->uuid('id')->required(); // Date-time field (ISO 8601) -$builder->datetime('created_at'); +$property->datetime('created_at'); // Date field -$builder->date('birth_date'); +$property->date('birth_date'); // Time field -$builder->time('start_time'); +$property->time('start_time'); // IPv4 address -$builder->ipv4('ip_address'); +$property->ipv4('ip_address'); // IPv6 address -$builder->ipv6('ipv6_address'); +$property->ipv6('ipv6_address'); // Hostname -$builder->hostname('server_name'); +$property->hostname('server_name'); ``` ### String Format Validation @@ -252,9 +252,9 @@ $builder->hostname('server_name'); You can also set custom formats on string fields: ```php -$builder->string('email')->format('email'); -$builder->string('website')->format('uri'); -$builder->string('id')->format('uuid'); +$property->string('email')->format('email'); +$property->string('website')->format('uri'); +$property->string('id')->format('uuid'); ``` Supported formats: `email`, `uri`, `url`, `uuid`, `date`, `date-time`, `time`, `ipv4`, `ipv6`, `hostname`, and more. @@ -264,10 +264,10 @@ Supported formats: `email`, `uri`, `url`, `uuid`, `date`, `date-time`, `time`, ` Mark fields as nullable to allow both the specified type and null: ```php -$builder->string('middle_name')->nullable(); +$property->string('middle_name')->nullable(); // Generates: {"type": ["string", "null"]} -$builder->object('address', function (Builder $nested) { +$property->object('address', function (Property $nested) { $nested->string('street')->required(); $nested->string('city')->required(); })->nullable(); @@ -279,7 +279,7 @@ $builder->object('address', function (Builder $nested) { Ensure array items are unique: ```php -$builder->array('tags') +$property->array('tags') ->items('string') ->uniqueItems(); ``` @@ -290,13 +290,13 @@ Control whether objects can have properties not defined in the schema: ```php // Allow additional properties -$builder->additionalProperties(true); +$property->additionalProperties(true); // Disallow additional properties -$builder->additionalProperties(false); // This is the default +$property->additionalProperties(false); // This is the default // Or use the convenient strict() helper -$builder->strict(); // Disallows additional properties AND marks all fields as required +$property->strict(); // Disallows additional properties AND marks all fields as required ``` #### Strict Mode for LLM APIs @@ -307,13 +307,13 @@ The `strict()` method is particularly useful for LLM APIs like **OpenAI Structur ```php // Perfect for OpenAI Structured Outputs -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('fullname'); - $builder->email('email'); - $builder->int('age')->min(0)->max(120); - $builder->string('location'); +$schema = Struct::define('User', function (Property $property) { + $property->string('fullname'); + $property->email('email'); + $property->int('age')->min(0)->max(120); + $property->string('location'); - $builder->strict(); // Makes all fields required + disallows extra properties + $property->strict(); // Makes all fields required + disallows extra properties }); ``` @@ -334,15 +334,15 @@ This generates: Add metadata to your schemas: ```php -$builder->title('User Schema'); -$builder->description('Schema for user data validation'); -$builder->schemaVersion('https://json-schema.org/draft/2020-12/schema'); +$property->title('User Schema'); +$property->description('Schema for user data validation'); +$property->schemaVersion('https://json-schema.org/draft/2020-12/schema'); ``` You can also add titles to individual fields: ```php -$builder->string('email') +$property->string('email') ->title('Email Address') ->description('User\'s primary email address') ->format('email') @@ -353,36 +353,36 @@ $builder->string('email') ```php use Blaspsoft\Forerunner\Schema\Struct; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('AdvancedUser', function (Builder $builder) { +$schema = Struct::define('AdvancedUser', function (Property $property) { // Schema metadata - $builder->schemaVersion(); - $builder->title('Advanced User Schema'); - $builder->description('Comprehensive user data structure'); - $builder->strict(); // Disallow additional properties + $property->schemaVersion(); + $property->title('Advanced User Schema'); + $property->description('Comprehensive user data structure'); + $property->strict(); // Disallow additional properties // Helper methods - $builder->uuid('id')->required(); - $builder->email('email')->required(); - $builder->url('website')->nullable(); - $builder->datetime('created_at')->required(); + $property->uuid('id')->required(); + $property->email('email')->required(); + $property->url('website')->nullable(); + $property->datetime('created_at')->required(); // Nullable nested object - $builder->object('profile', function (Builder $profile) { + $property->object('profile', function (Property $profile) { $profile->string('bio')->maxLength(500); $profile->string('avatar_url')->format('uri'); })->nullable(); // Array with unique items - $builder->array('tags') + $property->array('tags') ->items('string') ->uniqueItems() ->minItems(1) ->maxItems(10); // Advanced field configuration - $builder->string('username') + $property->string('username') ->title('Username') ->description('Unique username for the account') ->minLength(3) @@ -459,31 +459,31 @@ This generates: ```php use Blaspsoft\Forerunner\Schema\Struct; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('UserProfile', function (Builder $builder) { - $builder->string('name', 'The user\'s full name') +$schema = Struct::define('UserProfile', function (Property $property) { + $property->string('name', 'The user\'s full name') ->minLength(1) ->maxLength(100) ->required(); - $builder->string('email', 'The user\'s email') + $property->string('email', 'The user\'s email') ->pattern('^[^\s@]+@[^\s@]+\.[^\s@]+$') ->required(); - $builder->int('age', 'The user\'s age') + $property->int('age', 'The user\'s age') ->min(0) ->max(150); - $builder->boolean('is_active', 'Is the account active?') + $property->boolean('is_active', 'Is the account active?') ->default(true); - $builder->array('tags', 'User tags') + $property->array('tags', 'User tags') ->items('string') ->minItems(0) ->maxItems(10); - $builder->object('address', function (Builder $address) { + $property->object('address', function (Property $address) { $address->string('street', 'Street name')->required(); $address->string('city', 'City name')->required(); $address->string('state', 'State/Province')->required(); @@ -491,7 +491,7 @@ $schema = Struct::define('UserProfile', function (Builder $builder) { $address->string('country', 'Country code')->required(); }, 'User\'s address'); - $builder->enum('role', ['admin', 'moderator', 'user'], 'User role') + $property->enum('role', ['admin', 'moderator', 'user'], 'User role') ->default('user'); }); ``` @@ -499,30 +499,30 @@ $schema = Struct::define('UserProfile', function (Builder $builder) { ### Blog Post with Comments ```php -$schema = Struct::define('BlogPost', function (Builder $builder) { - $builder->string('title')->required(); - $builder->string('content')->required(); - $builder->string('slug')->pattern('^[a-z0-9-]+$')->required(); +$schema = Struct::define('BlogPost', function (Property $property) { + $property->string('title')->required(); + $property->string('content')->required(); + $property->string('slug')->pattern('^[a-z0-9-]+$')->required(); - $builder->object('author', function (Builder $author) { + $property->object('author', function (Property $author) { $author->string('name')->required(); $author->string('email')->required(); $author->string('bio'); })->required(); - $builder->array('comments')->items('object', function (Builder $comment) { + $property->array('comments')->items('object', function (Property $comment) { $comment->string('text')->required(); $comment->string('author_name')->required(); $comment->string('author_email')->required(); $comment->int('timestamp')->required(); }); - $builder->array('tags')->items('string')->minItems(1); + $property->array('tags')->items('string')->minItems(1); - $builder->enum('status', ['draft', 'published', 'archived']) + $property->enum('status', ['draft', 'published', 'archived']) ->default('draft'); - $builder->int('views')->min(0)->default(0); + $property->int('views')->min(0)->default(0); }); ``` @@ -535,9 +535,9 @@ The `Struct::define()` method returns a `Struct` object that provides multiple w Forerunner schemas support both array-like access and object methods for maximum flexibility: ```php -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); - $builder->string('email')->required(); +$schema = Struct::define('User', function (Property $property) { + $property->string('name')->required(); + $property->string('email')->required(); }); // Access as an array (for backward compatibility) @@ -549,16 +549,16 @@ $array = $schema->toArray(); // Get as PHP array $json = $schema->toJson(); // Get as JSON string // Method chaining works too! -$json = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); +$json = Struct::define('User', function (Property $property) { + $property->string('name')->required(); })->toJson(); ``` ### Convert to Array ```php -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); +$schema = Struct::define('User', function (Property $property) { + $property->string('name')->required(); }); // Use as array directly (implements ArrayAccess) @@ -574,14 +574,14 @@ $array = $schema->toArray(); ```php // Direct method chaining -$json = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); - $builder->email('email')->required(); +$json = Struct::define('User', function (Property $property) { + $property->string('name')->required(); + $property->email('email')->required(); })->toJson(); // Or call toJson() on the schema object -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); +$schema = Struct::define('User', function (Property $property) { + $property->string('name')->required(); }); $json = $schema->toJson(); @@ -593,8 +593,8 @@ $json = $schema->toJson(); The `Struct` object implements `JsonSerializable`, so you can use it directly with `json_encode()`: ```php -$schema = Struct::define('User', function (Builder $builder) { - $builder->string('name')->required(); +$schema = Struct::define('User', function (Property $property) { + $property->string('name')->required(); }); // Automatic JSON serialization @@ -611,7 +611,7 @@ class UserProfile { public static function schema(): Struct { - return Struct::define('user_profile', function (Builder $table) { + return Struct::define('user_profile', function (Property $table) { $table->string('name')->required(); $table->string('email')->required(); }); diff --git a/src/Schema/Builder.php b/src/Schema/Property.php similarity index 99% rename from src/Schema/Builder.php rename to src/Schema/Property.php index 33fd6ea..65e8ed0 100644 --- a/src/Schema/Builder.php +++ b/src/Schema/Property.php @@ -4,7 +4,7 @@ namespace Blaspsoft\Forerunner\Schema; -class Builder +class Property { protected string $name; @@ -98,7 +98,7 @@ public function array(string $name, ?string $description = null): PropertyBuilde */ public function object(string $name, callable $callback, ?string $description = null): PropertyBuilder { - $nestedBuilder = new Builder($name); + $nestedBuilder = new Property($name); $callback($nestedBuilder); $builder = new PropertyBuilder($name, 'object', $description); diff --git a/src/Schema/PropertyBuilder.php b/src/Schema/PropertyBuilder.php index a6f9868..d638cc6 100644 --- a/src/Schema/PropertyBuilder.php +++ b/src/Schema/PropertyBuilder.php @@ -17,7 +17,7 @@ class PropertyBuilder /** @var array|null */ protected ?array $enum = null; - protected ?Builder $nestedBuilder = null; + protected ?Property $nestedBuilder = null; /** @var array|null */ protected ?array $items = null; @@ -101,7 +101,7 @@ public function enum(array $values): self public function items(string $type, ?callable $callback = null): self { if ($callback && $type === 'object') { - $nestedBuilder = new Builder($this->name.'_item'); + $nestedBuilder = new Property($this->name.'_item'); $callback($nestedBuilder); $this->items = $nestedBuilder->toArray(); } else { @@ -234,7 +234,7 @@ public function nullable(bool $nullable = true): self /** * Set a nested builder for object types. */ - public function setNestedBuilder(Builder $builder): void + public function setNestedBuilder(Property $builder): void { $this->nestedBuilder = $builder; } diff --git a/src/Schema/Struct.php b/src/Schema/Struct.php index cffd2dd..f1321c1 100644 --- a/src/Schema/Struct.php +++ b/src/Schema/Struct.php @@ -6,14 +6,14 @@ class Struct implements \JsonSerializable { - protected Builder $builder; + protected Property $builder; protected string $name; /** @var array|null */ protected ?array $cache = null; - protected function __construct(string $name, Builder $builder) + protected function __construct(string $name, Property $builder) { $this->name = $name; $this->builder = $builder; @@ -24,7 +24,7 @@ protected function __construct(string $name, Builder $builder) */ public static function define(string $name, string $description, callable $callback): self { - $builder = new Builder($name); + $builder = new Property($name); $builder->description($description); $callback($builder); diff --git a/stubs/struct.stub b/stubs/struct.stub index f941853..bb42bd6 100644 --- a/stubs/struct.stub +++ b/stubs/struct.stub @@ -3,7 +3,7 @@ namespace {{ namespace }}; use Blaspsoft\Forerunner\Schema\Struct; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; class {{ class }} { @@ -14,11 +14,11 @@ class {{ class }} */ public static function schema(): array { - return Struct::define('{{ structName }}', 'Description of {{ structName }}', function (Builder $builder) { - $builder->string('example_field'); + return Struct::define('{{ structName }}', 'Description of {{ structName }}', function (Property $property) { + $property->string('example_field'); // Add your fields here - $builder->strict(); // All fields required + no additional properties + $property->strict(); // All fields required + no additional properties })->toArray(); } } \ No newline at end of file diff --git a/tests/Feature/IntendedUsageTest.php b/tests/Feature/IntendedUsageTest.php index 9bb8fa0..6cc4567 100644 --- a/tests/Feature/IntendedUsageTest.php +++ b/tests/Feature/IntendedUsageTest.php @@ -1,22 +1,22 @@ string('name', 'The name of the user')->minLength(1)->maxLength(100)->required(); - $builder->int('age', 'The age of the user')->min(0)->max(150); - $builder->boolean('is_active', 'Is the user active?')->default(true); - $builder->array('tags', 'Tags associated with the user') + $schema = Struct::define('User', 'A user object with personal information', function (Property $property) { + $property->string('name', 'The name of the user')->minLength(1)->maxLength(100)->required(); + $property->int('age', 'The age of the user')->min(0)->max(150); + $property->boolean('is_active', 'Is the user active?')->default(true); + $property->array('tags', 'Tags associated with the user') ->items('string') ->minItems(0) ->maxItems(10); - $builder->object('address', function (Builder $table) { - $table->string('street', 'Street name')->required(); - $table->string('city', 'City name')->required(); - $table->string('zip', 'ZIP code')->required(); + $property->object('address', function (Property $address) { + $address->string('street', 'Street name')->required(); + $address->string('city', 'City name')->required(); + $address->string('zip', 'ZIP code')->required(); }, 'The address of the user'); })->toArray(); diff --git a/tests/Feature/MakeStructCommandTest.php b/tests/Feature/MakeStructCommandTest.php index 0fc7851..98f4524 100644 --- a/tests/Feature/MakeStructCommandTest.php +++ b/tests/Feature/MakeStructCommandTest.php @@ -50,7 +50,7 @@ $content = File::get($this->testFilePath); expect($content)->toContain('use Blaspsoft\Forerunner\Schema\Struct;') - ->and($content)->toContain('use Blaspsoft\Forerunner\Schema\Builder;'); + ->and($content)->toContain('use Blaspsoft\Forerunner\Schema\Property;'); }); it('generates struct with schema method', function () { @@ -70,7 +70,7 @@ $content = File::get($this->testFilePath); - expect($content)->toContain('$builder->strict()'); + expect($content)->toContain('$property->strict()'); }); it('converts PascalCase to snake_case for struct name', function () { diff --git a/tests/Feature/SchemaFacadeTest.php b/tests/Feature/SchemaFacadeTest.php index a515717..c66a18c 100644 --- a/tests/Feature/SchemaFacadeTest.php +++ b/tests/Feature/SchemaFacadeTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); use Blaspsoft\Forerunner\Facades\Schema; -use Blaspsoft\Forerunner\Schema\Builder; +use Blaspsoft\Forerunner\Schema\Property; describe('Schema Facade', function () { it('can define a schema using the facade', function () { - $schema = Schema::define('User', 'A user schema', function (Builder $builder) { - $builder->string('name')->required(); - $builder->string('email')->required(); + $schema = Schema::define('User', 'A user schema', function (Property $property) { + $property->string('name')->required(); + $property->string('email')->required(); })->toArray(); expect($schema)->toBeArray() @@ -20,15 +20,15 @@ }); it('can define complex schemas using the facade', function () { - $schema = Schema::define('BlogPost', 'A blog post schema', function (Builder $builder) { - $builder->string('title')->required(); - $builder->string('content')->required(); - $builder->object('author', function (Builder $author) { + $schema = Schema::define('BlogPost', 'A blog post schema', function (Property $property) { + $property->string('title')->required(); + $property->string('content')->required(); + $property->object('author', function (Property $author) { $author->string('name')->required(); $author->string('email')->required(); })->required(); - $builder->array('tags')->items('string'); - $builder->enum('status', ['draft', 'published', 'archived'])->default('draft'); + $property->array('tags')->items('string'); + $property->enum('status', ['draft', 'published', 'archived'])->default('draft'); })->toArray(); expect($schema)->toBeArray() @@ -41,13 +41,13 @@ }); it('can define schemas with all field types', function () { - $schema = Schema::define('CompleteExample', 'Complete example schema', function (Builder $builder) { - $builder->string('text'); - $builder->int('count'); - $builder->float('price'); - $builder->boolean('isActive'); - $builder->array('items'); - $builder->enum('role', ['admin', 'user']); + $schema = Schema::define('CompleteExample', 'Complete example schema', function (Property $property) { + $property->string('text'); + $property->int('count'); + $property->float('price'); + $property->boolean('isActive'); + $property->array('items'); + $property->enum('role', ['admin', 'user']); })->toArray(); expect($schema)->toBeArray() @@ -61,16 +61,16 @@ }); it('can define schemas with validation constraints', function () { - $schema = Schema::define('ValidationExample', 'Validation example schema', function (Builder $builder) { - $builder->string('username') + $schema = Schema::define('ValidationExample', 'Validation example schema', function (Property $property) { + $property->string('username') ->required() ->minLength(3) ->maxLength(50) ->pattern('^[a-zA-Z0-9_]+$'); - $builder->int('age') + $property->int('age') ->min(0) ->max(150); - $builder->array('tags') + $property->array('tags') ->minItems(1) ->maxItems(10); })->toArray(); @@ -86,12 +86,12 @@ }); it('can define schemas with nested objects', function () { - $schema = Schema::define('Company', 'Company information', function (Builder $builder) { - $builder->string('name')->required(); - $builder->object('address', function (Builder $address) { + $schema = Schema::define('Company', 'Company information', function (Property $property) { + $property->string('name')->required(); + $property->object('address', function (Property $address) { $address->string('street'); $address->string('city'); - $address->object('coordinates', function (Builder $coords) { + $address->object('coordinates', function (Property $coords) { $coords->float('latitude'); $coords->float('longitude'); }); @@ -107,8 +107,8 @@ }); it('can define schemas with array of objects', function () { - $schema = Schema::define('UserList', 'A list of users', function (Builder $builder) { - $builder->array('users')->items('object', function (Builder $user) { + $schema = Schema::define('UserList', 'A list of users', function (Property $property) { + $property->array('users')->items('object', function (Property $user) { $user->string('name')->required(); $user->string('email')->required(); $user->int('age'); @@ -124,10 +124,10 @@ }); it('can define schemas with descriptions', function () { - $schema = Schema::define('DescribedSchema', 'A schema with descriptions', function (Builder $builder) { - $builder->description('A schema with descriptions'); - $builder->string('name')->description('The user name'); - $builder->string('email')->description('The user email address'); + $schema = Schema::define('DescribedSchema', 'A schema with descriptions', function (Property $property) { + $property->description('A schema with descriptions'); + $property->string('name')->description('The user name'); + $property->string('email')->description('The user email address'); })->toArray(); expect($schema)->toBeArray() @@ -137,10 +137,10 @@ }); it('can define schemas with default values', function () { - $schema = Schema::define('DefaultsExample', 'Schema with defaults', function (Builder $builder) { - $builder->boolean('notifications')->default(true); - $builder->string('theme')->default('light'); - $builder->int('pageSize')->default(10); + $schema = Schema::define('DefaultsExample', 'Schema with defaults', function (Property $property) { + $property->boolean('notifications')->default(true); + $property->string('theme')->default('light'); + $property->int('pageSize')->default(10); })->toArray(); expect($schema)->toBeArray() diff --git a/tests/Unit/AdvancedFeaturesTest.php b/tests/Unit/AdvancedFeaturesTest.php index b4efaa2..44ec190 100644 --- a/tests/Unit/AdvancedFeaturesTest.php +++ b/tests/Unit/AdvancedFeaturesTest.php @@ -1,60 +1,60 @@ string('name'); - $builder->additionalProperties(true); + $property = new Property('Test'); + $property->string('name'); + $property->additionalProperties(true); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('additionalProperties', true); }); it('can set additionalProperties to false', function () { - $builder = new Builder('Test'); - $builder->string('name'); - $builder->additionalProperties(false); + $property = new Property('Test'); + $property->string('name'); + $property->additionalProperties(false); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('additionalProperties', false); }); it('can use strict() helper for disallowing additional properties', function () { - $builder = new Builder('Test'); - $builder->string('name'); - $builder->strict(); + $property = new Property('Test'); + $property->string('name'); + $property->strict(); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('additionalProperties', false); }); it('strict() makes all fields required', function () { - $builder = new Builder('Test'); - $builder->string('name'); - $builder->email('email'); - $builder->int('age')->min(0); - $builder->strict(); + $property = new Property('Test'); + $property->string('name'); + $property->email('email'); + $property->int('age')->min(0); + $property->strict(); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('additionalProperties', false) ->and($schema['required'])->toBe(['name', 'email', 'age']); }); it('defaults additionalProperties to false', function () { - $builder = new Builder('Test'); - $builder->string('name'); + $property = new Property('Test'); + $property->string('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('additionalProperties', false); }); @@ -62,28 +62,28 @@ describe('uniqueItems', function () { it('can set uniqueItems on array fields', function () { - $builder = new Builder('Test'); - $builder->array('tags')->items('string')->uniqueItems(); + $property = new Property('Test'); + $property->array('tags')->items('string')->uniqueItems(); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['tags'])->toHaveKey('uniqueItems', true); }); it('can explicitly set uniqueItems to false', function () { - $builder = new Builder('Test'); - $builder->array('tags')->items('string')->uniqueItems(false); + $property = new Property('Test'); + $property->array('tags')->items('string')->uniqueItems(false); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['tags'])->toHaveKey('uniqueItems', false); }); it('does not include uniqueItems when not set', function () { - $builder = new Builder('Test'); - $builder->array('tags')->items('string'); + $property = new Property('Test'); + $property->array('tags')->items('string'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['tags'])->not->toHaveKey('uniqueItems'); }); @@ -91,10 +91,10 @@ describe('format', function () { it('can set format on string fields', function () { - $builder = new Builder('Test'); - $builder->string('email')->format('email'); + $property = new Property('Test'); + $property->string('email')->format('email'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['email'])->toHaveKey('format', 'email'); }); @@ -178,30 +178,30 @@ describe('schema version', function () { it('can set JSON Schema version', function () { - $builder = new Builder('Test'); - $builder->string('name'); - $builder->schemaVersion('https://json-schema.org/draft/2020-12/schema'); + $property = new Property('Test'); + $property->string('name'); + $property->schemaVersion('https://json-schema.org/draft/2020-12/schema'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('$schema', 'https://json-schema.org/draft/2020-12/schema'); }); it('uses default version when called without arguments', function () { - $builder = new Builder('Test'); - $builder->string('name'); - $builder->schemaVersion(); + $property = new Property('Test'); + $property->string('name'); + $property->schemaVersion(); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('$schema', 'https://json-schema.org/draft/2020-12/schema'); }); it('does not include $schema when not set', function () { - $builder = new Builder('Test'); - $builder->string('name'); + $property = new Property('Test'); + $property->string('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->not->toHaveKey('$schema'); }); @@ -209,11 +209,11 @@ describe('title', function () { it('can set title on schema', function () { - $builder = new Builder('Test'); - $builder->title('User Schema'); - $builder->string('name'); + $property = new Property('Test'); + $property->title('User Schema'); + $property->string('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('title', 'User Schema'); }); @@ -228,10 +228,10 @@ }); it('does not include title when not set', function () { - $builder = new Builder('Test'); - $builder->string('name'); + $property = new Property('Test'); + $property->string('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->not->toHaveKey('title'); }); @@ -239,10 +239,10 @@ describe('helper methods', function () { it('can add email field', function () { - $builder = new Builder('Test'); - $builder->email('email', 'User email address'); + $property = new Property('Test'); + $property->email('email', 'User email address'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['email']['type'])->toBe('string') ->and($schema['properties']['email']['format'])->toBe('email') @@ -250,90 +250,90 @@ }); it('can add url field', function () { - $builder = new Builder('Test'); - $builder->url('website'); + $property = new Property('Test'); + $property->url('website'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['website']['type'])->toBe('string') ->and($schema['properties']['website']['format'])->toBe('uri'); }); it('can add uuid field', function () { - $builder = new Builder('Test'); - $builder->uuid('id'); + $property = new Property('Test'); + $property->uuid('id'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['id']['type'])->toBe('string') ->and($schema['properties']['id']['format'])->toBe('uuid'); }); it('can add datetime field', function () { - $builder = new Builder('Test'); - $builder->datetime('created_at'); + $property = new Property('Test'); + $property->datetime('created_at'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['created_at']['type'])->toBe('string') ->and($schema['properties']['created_at']['format'])->toBe('date-time'); }); it('can add date field', function () { - $builder = new Builder('Test'); - $builder->date('birth_date'); + $property = new Property('Test'); + $property->date('birth_date'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['birth_date']['type'])->toBe('string') ->and($schema['properties']['birth_date']['format'])->toBe('date'); }); it('can add time field', function () { - $builder = new Builder('Test'); - $builder->time('start_time'); + $property = new Property('Test'); + $property->time('start_time'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['start_time']['type'])->toBe('string') ->and($schema['properties']['start_time']['format'])->toBe('time'); }); it('can add ipv4 field', function () { - $builder = new Builder('Test'); - $builder->ipv4('ip_address'); + $property = new Property('Test'); + $property->ipv4('ip_address'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['ip_address']['type'])->toBe('string') ->and($schema['properties']['ip_address']['format'])->toBe('ipv4'); }); it('can add ipv6 field', function () { - $builder = new Builder('Test'); - $builder->ipv6('ipv6_address'); + $property = new Property('Test'); + $property->ipv6('ipv6_address'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['ipv6_address']['type'])->toBe('string') ->and($schema['properties']['ipv6_address']['format'])->toBe('ipv6'); }); it('can add hostname field', function () { - $builder = new Builder('Test'); - $builder->hostname('server'); + $property = new Property('Test'); + $property->hostname('server'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['server']['type'])->toBe('string') ->and($schema['properties']['server']['format'])->toBe('hostname'); }); it('helper methods support chaining', function () { - $builder = new Builder('Test'); - $builder->email('email')->required(); + $property = new Property('Test'); + $property->email('email')->required(); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['email']['format'])->toBe('email') ->and($schema['required'])->toContain('email'); @@ -342,27 +342,27 @@ describe('complete schema with all features', function () { it('generates comprehensive schema with all features in OpenAI format when strict', function () { - $schema = Struct::define('CompleteExample', 'Complete example with all features', function (Builder $builder) { - $builder->schemaVersion(); - $builder->title('Complete Schema Example'); - $builder->description('A comprehensive schema demonstrating all features'); - $builder->strict(); - - $builder->uuid('id')->required(); - $builder->email('email')->required(); - $builder->url('website')->nullable(); - $builder->datetime('created_at')->required(); - $builder->string('status') + $schema = Struct::define('CompleteExample', 'Complete example with all features', function (Property $property) { + $property->schemaVersion(); + $property->title('Complete Schema Example'); + $property->description('A comprehensive schema demonstrating all features'); + $property->strict(); + + $property->uuid('id')->required(); + $property->email('email')->required(); + $property->url('website')->nullable(); + $property->datetime('created_at')->required(); + $property->string('status') ->enum(['active', 'inactive']) ->default('active'); - $builder->array('tags') + $property->array('tags') ->items('string') ->uniqueItems() ->minItems(1) ->maxItems(5); - $builder->object('metadata', function (Builder $nested) { + $property->object('metadata', function (Property $nested) { $nested->string('version')->required(); $nested->int('count')->min(0); })->nullable(); @@ -388,10 +388,10 @@ }); it('generates normal schema without strict mode', function () { - $schema = Struct::define('NormalExample', 'Normal example without strict mode', function (Builder $builder) { - $builder->title('Normal Schema Example'); - $builder->uuid('id')->required(); - $builder->email('email')->required(); + $schema = Struct::define('NormalExample', 'Normal example without strict mode', function (Property $property) { + $property->title('Normal Schema Example'); + $property->uuid('id')->required(); + $property->email('email')->required(); })->toArray(); // Without strict(), should return normal flat schema diff --git a/tests/Unit/BuilderTest.php b/tests/Unit/BuilderTest.php index 52f8baf..5850afc 100644 --- a/tests/Unit/BuilderTest.php +++ b/tests/Unit/BuilderTest.php @@ -1,82 +1,82 @@ toBeInstanceOf(Builder::class); + expect($property)->toBeInstanceOf(Property::class); }); it('can add a string property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->string('username'); + $property = new Property('TestSchema'); + $property = $property->string('username'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add an integer property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->int('age'); + $property = new Property('TestSchema'); + $property = $property->int('age'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add an integer property using integer alias', function () { - $builder = new Builder('TestSchema'); - $property = $builder->integer('count'); + $property = new Property('TestSchema'); + $property = $property->integer('count'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add a float property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->float('price'); + $property = new Property('TestSchema'); + $property = $property->float('price'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add a number property using number alias', function () { - $builder = new Builder('TestSchema'); - $property = $builder->number('amount'); + $property = new Property('TestSchema'); + $property = $property->number('amount'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add a boolean property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->boolean('isActive'); + $property = new Property('TestSchema'); + $property = $property->boolean('isActive'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add a boolean property using bool alias', function () { - $builder = new Builder('TestSchema'); - $property = $builder->bool('enabled'); + $property = new Property('TestSchema'); + $property = $property->bool('enabled'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add an array property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->array('tags'); + $property = new Property('TestSchema'); + $property = $property->array('tags'); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add an enum property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->enum('status', ['active', 'inactive', 'pending']); + $property = new Property('TestSchema'); + $property = $property->enum('status', ['active', 'inactive', 'pending']); expect($property)->toBeInstanceOf(PropertyBuilder::class); }); it('can add a nested object property', function () { - $builder = new Builder('TestSchema'); - $property = $builder->object('user', function (Builder $nested) { + $property = new Property('TestSchema'); + $property = $property->object('user', function (Property $nested) { $nested->string('name'); $nested->string('email'); }); @@ -85,18 +85,18 @@ }); it('can set a description on the schema', function () { - $builder = new Builder('TestSchema'); - $result = $builder->description('Test schema description'); + $property = new Property('TestSchema'); + $result = $property->description('Test schema description'); - expect($result)->toBe($builder); + expect($result)->toBe($property); }); it('generates correct JSON schema array for simple properties', function () { - $builder = new Builder('TestSchema'); - $builder->string('name'); - $builder->int('age'); + $property = new Property('TestSchema'); + $property->string('name'); + $property->int('age'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('type', 'object') ->and($schema)->toHaveKey('properties') @@ -107,22 +107,22 @@ }); it('includes description in schema when set', function () { - $builder = new Builder('TestSchema'); - $builder->description('User schema'); - $builder->string('name'); + $property = new Property('TestSchema'); + $property->description('User schema'); + $property->string('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('description', 'User schema'); }); it('includes required fields in schema', function () { - $builder = new Builder('TestSchema'); - $builder->string('name')->required(); - $builder->string('email')->required(); - $builder->int('age'); + $property = new Property('TestSchema'); + $property->string('name')->required(); + $property->string('email')->required(); + $property->int('age'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->toHaveKey('required') ->and($schema['required'])->toContain('name') @@ -131,23 +131,23 @@ }); it('does not include required key when no fields are required', function () { - $builder = new Builder('TestSchema'); - $builder->string('name'); - $builder->int('age'); + $property = new Property('TestSchema'); + $property->string('name'); + $property->int('age'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema)->not->toHaveKey('required'); }); it('generates correct schema for nested objects', function () { - $builder = new Builder('TestSchema'); - $builder->object('user', function (Builder $nested) { + $property = new Property('TestSchema'); + $property->object('user', function (Property $nested) { $nested->string('name'); $nested->string('email'); }); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['user']['type'])->toBe('object') ->and($schema['properties']['user']['properties'])->toHaveKey('name') @@ -155,20 +155,20 @@ }); it('generates correct schema for enum fields', function () { - $builder = new Builder('TestSchema'); - $builder->enum('status', ['active', 'inactive']); + $property = new Property('TestSchema'); + $property->enum('status', ['active', 'inactive']); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['status']['type'])->toBe('string') ->and($schema['properties']['status']['enum'])->toBe(['active', 'inactive']); }); it('can convert schema to JSON string', function () { - $builder = new Builder('TestSchema'); - $builder->string('name')->required(); + $property = new Property('TestSchema'); + $property->string('name')->required(); - $json = $builder->toJson(); + $json = $property->toJson(); expect($json)->toBeString() ->and($json)->toContain('"type": "object"') @@ -177,13 +177,13 @@ }); it('supports method chaining for properties', function () { - $builder = new Builder('TestSchema'); - $builder->string('username') + $property = new Property('TestSchema'); + $property->string('username') ->required() ->minLength(3) ->maxLength(50); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties']['username'])->toHaveKey('minLength', 3) ->and($schema['properties']['username'])->toHaveKey('maxLength', 50) @@ -191,14 +191,14 @@ }); it('can handle multiple properties with mixed types', function () { - $builder = new Builder('TestSchema'); - $builder->string('name')->required(); - $builder->int('age')->min(0)->max(150); - $builder->boolean('isActive')->default(true); - $builder->array('tags'); - $builder->enum('role', ['admin', 'user', 'guest']); + $property = new Property('TestSchema'); + $property->string('name')->required(); + $property->int('age')->min(0)->max(150); + $property->boolean('isActive')->default(true); + $property->array('tags'); + $property->enum('role', ['admin', 'user', 'guest']); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['properties'])->toHaveCount(5) ->and($schema['properties']['name']['type'])->toBe('string') @@ -209,14 +209,14 @@ }); it('avoids duplicate required fields', function () { - $builder = new Builder('TestSchema'); - $builder->string('name')->required(); + $property = new Property('TestSchema'); + $property->string('name')->required(); // Manually mark as required again - $builder->markRequired('name'); - $builder->markRequired('name'); + $property->markRequired('name'); + $property->markRequired('name'); - $schema = $builder->toArray(); + $schema = $property->toArray(); expect($schema['required'])->toHaveCount(1) ->and($schema['required'])->toContain('name'); diff --git a/tests/Unit/PropertyBuilderTest.php b/tests/Unit/PropertyBuilderTest.php index 6161c97..db8dfa2 100644 --- a/tests/Unit/PropertyBuilderTest.php +++ b/tests/Unit/PropertyBuilderTest.php @@ -1,7 +1,7 @@ items('object', function (Builder $builder) { - $builder->string('name'); - $builder->string('email'); + $result = $property->items('object', function (Property $property) { + $property->string('name'); + $property->string('email'); }); expect($result)->toBe($property); @@ -170,7 +170,7 @@ }); it('can set nested builder for objects', function () { - $builder = new Builder('nested'); + $builder = new Property('nested'); $builder->string('field'); $property = new PropertyBuilder('data', 'object'); @@ -251,7 +251,7 @@ }); it('merges nested builder schema correctly', function () { - $builder = new Builder('user'); + $builder = new Property('user'); $builder->string('name')->required(); $builder->string('email')->required(); diff --git a/tests/Unit/StructTest.php b/tests/Unit/StructTest.php index defd623..d3d25b8 100644 --- a/tests/Unit/StructTest.php +++ b/tests/Unit/StructTest.php @@ -1,14 +1,14 @@ string('name'); - $builder->string('email'); + $struct = Struct::define('User', 'A user schema', function (Property $property) { + $property->string('name'); + $property->string('email'); }); expect($struct)->toBeInstanceOf(Struct::class); @@ -21,18 +21,18 @@ }); it('returns a Struct instance from define method', function () { - $struct = Struct::define('Simple', 'A simple schema', function (Builder $builder) { - $builder->string('field'); + $struct = Struct::define('Simple', 'A simple schema', function (Property $property) { + $property->string('field'); }); expect($struct)->toBeInstanceOf(Struct::class); }); it('can define schema with required fields', function () { - $schema = Struct::define('User', 'A user with required fields', function (Builder $builder) { - $builder->string('name')->required(); - $builder->string('email')->required(); - $builder->int('age'); + $schema = Struct::define('User', 'A user with required fields', function (Property $property) { + $property->string('name')->required(); + $property->string('email')->required(); + $property->int('age'); })->toArray(); expect($schema['required'])->toContain('name') @@ -41,9 +41,9 @@ }); it('can define schema with nested objects', function () { - $schema = Struct::define('Post', 'A blog post schema', function (Builder $builder) { - $builder->string('title')->required(); - $builder->object('author', function (Builder $nested) { + $schema = Struct::define('Post', 'A blog post schema', function (Property $property) { + $property->string('title')->required(); + $property->object('author', function (Property $nested) { $nested->string('name')->required(); $nested->string('email')->required(); })->required(); @@ -58,9 +58,9 @@ }); it('can define schema with array fields', function () { - $schema = Struct::define('Product', 'A product schema', function (Builder $builder) { - $builder->string('name'); - $builder->array('tags')->items('string'); + $schema = Struct::define('Product', 'A product schema', function (Property $property) { + $property->string('name'); + $property->array('tags')->items('string'); })->toArray(); expect($schema['properties'])->toHaveKey('tags') @@ -69,9 +69,9 @@ }); it('can define schema with enum fields', function () { - $schema = Struct::define('User', 'A user schema', function (Builder $builder) { - $builder->string('name'); - $builder->enum('role', ['admin', 'user', 'guest']); + $schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->string('name'); + $property->enum('role', ['admin', 'user', 'guest']); })->toArray(); expect($schema['properties'])->toHaveKey('role') @@ -80,12 +80,12 @@ }); it('can define schema with all primitive types', function () { - $schema = Struct::define('AllTypes', 'Schema with all types', function (Builder $builder) { - $builder->string('text'); - $builder->int('count'); - $builder->float('price'); - $builder->boolean('isActive'); - $builder->array('items'); + $schema = Struct::define('AllTypes', 'Schema with all types', function (Property $property) { + $property->string('text'); + $property->int('count'); + $property->float('price'); + $property->boolean('isActive'); + $property->array('items'); })->toArray(); expect($schema['properties']['text']['type'])->toBe('string') @@ -96,12 +96,12 @@ }); it('can define schema with property constraints', function () { - $schema = Struct::define('User', 'A user schema', function (Builder $builder) { - $builder->string('username') + $schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->string('username') ->required() ->minLength(3) ->maxLength(50); - $builder->int('age') + $property->int('age') ->min(0) ->max(150); })->toArray(); @@ -113,30 +113,30 @@ }); it('can define schema with description', function () { - $schema = Struct::define('User', 'A user schema', function (Builder $builder) { - $builder->description('A user object'); - $builder->string('name'); + $schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->description('A user object'); + $property->string('name'); })->toArray(); expect($schema)->toHaveKey('description', 'A user object'); }); it('can define complex nested schema', function () { - $schema = Struct::define('BlogPost', 'A blog post with comments', function (Builder $builder) { - $builder->string('title')->required(); - $builder->string('content')->required(); - $builder->object('author', function (Builder $author) { + $schema = Struct::define('BlogPost', 'A blog post with comments', function (Property $property) { + $property->string('title')->required(); + $property->string('content')->required(); + $property->object('author', function (Property $author) { $author->string('name')->required(); $author->string('email')->required(); $author->string('bio'); })->required(); - $builder->array('comments')->items('object', function (Builder $comment) { + $property->array('comments')->items('object', function (Property $comment) { $comment->string('text')->required(); $comment->string('authorName')->required(); $comment->int('timestamp')->required(); }); - $builder->array('tags')->items('string'); - $builder->enum('status', ['draft', 'published', 'archived'])->default('draft'); + $property->array('tags')->items('string'); + $property->enum('status', ['draft', 'published', 'archived'])->default('draft'); })->toArray(); expect($schema['type'])->toBe('object') @@ -153,7 +153,7 @@ }); it('can define empty schema', function () { - $schema = Struct::define('Empty', 'An empty schema', function (Builder $builder) { + $schema = Struct::define('Empty', 'An empty schema', function (Property $property) { // No properties })->toArray(); @@ -164,10 +164,10 @@ }); it('can define schema with default values', function () { - $schema = Struct::define('Settings', 'Application settings', function (Builder $builder) { - $builder->boolean('notifications')->default(true); - $builder->string('theme')->default('light'); - $builder->int('pageSize')->default(10); + $schema = Struct::define('Settings', 'Application settings', function (Property $property) { + $property->boolean('notifications')->default(true); + $property->string('theme')->default('light'); + $property->int('pageSize')->default(10); })->toArray(); expect($schema['properties']['notifications']['default'])->toBe(true) @@ -176,9 +176,9 @@ }); it('can define schema with pattern validation', function () { - $schema = Struct::define('Contact', 'Contact information', function (Builder $builder) { - $builder->string('email')->pattern('^[^@]+@[^@]+\.[^@]+$'); - $builder->string('phone')->pattern('^\d{3}-\d{3}-\d{4}$'); + $schema = Struct::define('Contact', 'Contact information', function (Property $property) { + $property->string('email')->pattern('^[^@]+@[^@]+\.[^@]+$'); + $property->string('phone')->pattern('^\d{3}-\d{3}-\d{4}$'); })->toArray(); expect($schema['properties']['email'])->toHaveKey('pattern') @@ -186,12 +186,12 @@ }); it('can define schema with deeply nested objects', function () { - $schema = Struct::define('Company', 'Company information', function (Builder $builder) { - $builder->string('name'); - $builder->object('address', function (Builder $address) { + $schema = Struct::define('Company', 'Company information', function (Property $property) { + $property->string('name'); + $property->object('address', function (Property $address) { $address->string('street'); $address->string('city'); - $address->object('coordinates', function (Builder $coords) { + $address->object('coordinates', function (Property $coords) { $coords->float('latitude'); $coords->float('longitude'); }); @@ -206,8 +206,8 @@ }); it('can define schema with array constraints', function () { - $schema = Struct::define('List', 'A list of items', function (Builder $builder) { - $builder->array('items') + $schema = Struct::define('List', 'A list of items', function (Property $property) { + $property->array('items') ->minItems(1) ->maxItems(10) ->items('string'); @@ -219,8 +219,8 @@ }); it('can be serialized with json_encode', function () { - $schema = Struct::define('User', 'A user schema', function (Builder $builder) { - $builder->string('name')->required(); + $schema = Struct::define('User', 'A user schema', function (Property $property) { + $property->string('name')->required(); }); $json = json_encode($schema, JSON_PRETTY_PRINT); From 569456280ee3af4322a2840d407cb35553d1f3f9 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 23:10:33 +0100 Subject: [PATCH 08/12] Fix README to remove toJson() references and add missing descriptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add required description parameter to all Struct::define() calls - Remove all references to toJson() method (was removed from Struct class) - Simplify "Working with Generated Schemas" section - Fix variable name from $table to $property in structure class example - Add ->toArray() calls where needed for consistency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 129 +++++++++++++++--------------------------------------- 1 file changed, 36 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 98dd617..721f471 100644 --- a/README.md +++ b/README.md @@ -51,10 +51,12 @@ class UserProfile { public static function schema(): array { - return Struct::define('user_profile', function (Property $table) { - $table->string('example_field')->required(); + return Struct::define('user_profile', 'Description of user_profile', function (Property $property) { + $property->string('example_field'); // Add your fields here - }); + + $property->strict(); // All fields required + no additional properties + })->toArray(); } } ``` @@ -67,24 +69,24 @@ Define a schema using the `Struct` class or `Schema` facade: use Blaspsoft\Forerunner\Schema\Struct; use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('User', function (Property $property) { +$schema = Struct::define('User', 'A user schema', function (Property $property) { $property->string('name', 'The user\'s full name')->required(); $property->string('email', 'The user\'s email address')->required(); $property->int('age', 'The user\'s age')->min(0)->max(150); $property->boolean('is_active', 'Is the user account active?')->default(true); -}); +})->toArray(); ``` -Or using the facade: +Or using the facade for a cleaner syntax: ```php use Blaspsoft\Forerunner\Facades\Schema; use Blaspsoft\Forerunner\Schema\Property; -$schema = Schema::define('User', function (Property $property) { - $property->string('name')->required(); - $property->string('email')->required(); -}); +$schema = Schema::define('User', 'A user schema', function (Property $property) { + $property->string('name', 'The user\'s full name')->required(); + $property->string('email', 'The user\'s email address')->required(); +})->toArray(); ``` ## Available Field Types @@ -307,14 +309,14 @@ The `strict()` method is particularly useful for LLM APIs like **OpenAI Structur ```php // Perfect for OpenAI Structured Outputs -$schema = Struct::define('User', function (Property $property) { +$schema = Struct::define('User', 'A user schema', function (Property $property) { $property->string('fullname'); $property->email('email'); $property->int('age')->min(0)->max(120); $property->string('location'); $property->strict(); // Makes all fields required + disallows extra properties -}); +})->toArray(); ``` This generates: @@ -355,11 +357,10 @@ $property->string('email') use Blaspsoft\Forerunner\Schema\Struct; use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('AdvancedUser', function (Property $property) { +$schema = Struct::define('AdvancedUser', 'Comprehensive user data structure', function (Property $property) { // Schema metadata $property->schemaVersion(); $property->title('Advanced User Schema'); - $property->description('Comprehensive user data structure'); $property->strict(); // Disallow additional properties // Helper methods @@ -389,7 +390,7 @@ $schema = Struct::define('AdvancedUser', function (Property $property) { ->maxLength(30) ->pattern('^[a-zA-Z0-9_]+$') ->required(); -}); +})->toArray(); ``` This generates: @@ -461,7 +462,7 @@ This generates: use Blaspsoft\Forerunner\Schema\Struct; use Blaspsoft\Forerunner\Schema\Property; -$schema = Struct::define('UserProfile', function (Property $property) { +$schema = Struct::define('UserProfile', 'A complete user profile schema', function (Property $property) { $property->string('name', 'The user\'s full name') ->minLength(1) ->maxLength(100) @@ -493,13 +494,13 @@ $schema = Struct::define('UserProfile', function (Property $property) { $property->enum('role', ['admin', 'moderator', 'user'], 'User role') ->default('user'); -}); +})->toArray(); ``` ### Blog Post with Comments ```php -$schema = Struct::define('BlogPost', function (Property $property) { +$schema = Struct::define('BlogPost', 'A blog post with author and comments', function (Property $property) { $property->string('title')->required(); $property->string('content')->required(); $property->string('slug')->pattern('^[a-z0-9-]+$')->required(); @@ -523,69 +524,23 @@ $schema = Struct::define('BlogPost', function (Property $property) { ->default('draft'); $property->int('views')->min(0)->default(0); -}); +})->toArray(); ``` ## Working with Generated Schemas -The `Struct::define()` method returns a `Struct` object that provides multiple ways to access your schema data. - -### Flexible API: Array Access + Object Methods - -Forerunner schemas support both array-like access and object methods for maximum flexibility: - -```php -$schema = Struct::define('User', function (Property $property) { - $property->string('name')->required(); - $property->string('email')->required(); -}); - -// Access as an array (for backward compatibility) -$type = $schema['type']; // 'object' -$properties = $schema['properties']; // array of properties - -// Or use object methods for a fluent API -$array = $schema->toArray(); // Get as PHP array -$json = $schema->toJson(); // Get as JSON string - -// Method chaining works too! -$json = Struct::define('User', function (Property $property) { - $property->string('name')->required(); -})->toJson(); -``` +The `Struct::define()` method returns a `Struct` object that can be converted to an array or JSON. ### Convert to Array ```php -$schema = Struct::define('User', function (Property $property) { - $property->string('name')->required(); -}); - -// Use as array directly (implements ArrayAccess) -foreach ($schema['properties'] as $name => $property) { - // Process properties -} - -// Or explicitly convert to array -$array = $schema->toArray(); -``` - -### Convert to JSON String - -```php -// Direct method chaining -$json = Struct::define('User', function (Property $property) { - $property->string('name')->required(); - $property->email('email')->required(); -})->toJson(); - -// Or call toJson() on the schema object -$schema = Struct::define('User', function (Property $property) { +$struct = Struct::define('User', 'A user schema', function (Property $property) { $property->string('name')->required(); + $property->string('email')->required(); }); -$json = $schema->toJson(); -// Both return formatted JSON strings +// Convert to array +$array = $struct->toArray(); ``` ### JSON Serialization @@ -593,48 +548,36 @@ $json = $schema->toJson(); The `Struct` object implements `JsonSerializable`, so you can use it directly with `json_encode()`: ```php -$schema = Struct::define('User', function (Property $property) { +$struct = Struct::define('User', 'A user schema', function (Property $property) { $property->string('name')->required(); }); // Automatic JSON serialization -$json = json_encode($schema, JSON_PRETTY_PRINT); +$json = json_encode($struct, JSON_PRETTY_PRINT); ``` ### Using Structure Classes -When using the `make:struct` command, you can leverage the new object methods: +When using the `make:struct` command, you can create reusable schema classes: ```php // In your structure class (generated by make:struct) class UserProfile { - public static function schema(): Struct - { - return Struct::define('user_profile', function (Property $table) { - $table->string('name')->required(); - $table->string('email')->required(); - }); - } - - public static function toJson(): string - { - return static::schema()->toJson(); - } - - public static function toArray(): array + public static function schema(): array { - return static::schema()->toArray(); + return Struct::define('user_profile', 'A user profile schema', function (Property $property) { + $property->string('name')->required(); + $property->string('email')->required(); + })->toArray(); } } // Using the structure -$schema = UserProfile::schema(); // Returns Struct object -$array = UserProfile::toArray(); // Returns array -$json = UserProfile::toJson(); // Returns JSON string +$array = UserProfile::schema(); // Returns array -// Or chain methods directly -$json = UserProfile::schema()->toJson(); +// For JSON, use json_encode +$json = json_encode(UserProfile::schema(), JSON_PRETTY_PRINT); ``` ## Testing From f1e7353fece7988f368e7240477d707eb7086aa2 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 23:15:50 +0100 Subject: [PATCH 09/12] Fix PHPStan config to remove deleted config directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The config directory was removed in earlier refactoring, but PHPStan configuration still referenced it, causing CI failures. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- phpstan.neon | 1 - 1 file changed, 1 deletion(-) diff --git a/phpstan.neon b/phpstan.neon index 2310cea..ee178b1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -5,5 +5,4 @@ parameters: level: 9 paths: - src - - config tmpDir: build/phpstan From a995aaabc6b135d9b64c145eb351695bad47a121 Mon Sep 17 00:00:00 2001 From: deemonic Date: Fri, 17 Oct 2025 23:17:46 +0100 Subject: [PATCH 10/12] Fix Laravel Pint formatting: add blank line before namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added missing blank line between declare(strict_types=1) and namespace declaration to comply with blank_lines_before_namespace rule. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- tests/TestCase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TestCase.php b/tests/TestCase.php index 5a6297d..b745aa2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,6 +1,7 @@ Date: Sat, 18 Oct 2025 12:54:02 +0100 Subject: [PATCH 11/12] Address CodeRabbit review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix strict() mode to mark fields added after strict() as required - Make toArray() non-mutating by computing required array per-call - Update facade docblock to reference correct namespace These changes ensure strict mode works correctly even when fields are added after calling strict(), and prevent toArray() from causing side effects that could affect subsequent calls. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Facades/Schema.php | 2 +- src/Schema/Property.php | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Facades/Schema.php b/src/Facades/Schema.php index 140ae73..4baf966 100644 --- a/src/Facades/Schema.php +++ b/src/Facades/Schema.php @@ -9,7 +9,7 @@ /** * @method static array define(string $name, callable $callback) * - * @see \Blaspsoft\Forerunner\Schemas\Struct + * @see \Blaspsoft\Forerunner\Schema\Struct */ class Schema extends Facade { diff --git a/src/Schema/Property.php b/src/Schema/Property.php index 65e8ed0..c1ba084 100644 --- a/src/Schema/Property.php +++ b/src/Schema/Property.php @@ -265,6 +265,12 @@ public function schemaVersion(string $version = 'https://json-schema.org/draft/2 protected function addProperty(string $name, string $type, ?string $description): PropertyBuilder { $builder = new PropertyBuilder($name, $type, $description); + + // In strict mode, all fields must be required, including those added after strict() + if ($this->isStrict) { + $builder->required(); + } + $this->properties[$name] = $builder; return $builder; @@ -304,16 +310,18 @@ public function toArray(): array $schema['description'] = $this->description; } + // Build required array from property states without mutating $this->required + $required = []; foreach ($this->properties as $name => $builder) { $schema['properties'][$name] = $builder->toArray(); if ($builder->isRequired()) { - $this->markRequired($name); + $required[] = $name; } } - if (! empty($this->required)) { - $schema['required'] = $this->required; + if (! empty($required)) { + $schema['required'] = $required; } $schema['additionalProperties'] = $this->additionalProperties; From 35ccefe1beef5e75d209c714a7dbe6b15be09eb4 Mon Sep 17 00:00:00 2001 From: deemonic Date: Sat, 18 Oct 2025 13:00:11 +0100 Subject: [PATCH 12/12] Address additional CodeRabbit feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove dead code: $this->required property and markRequired() method - Update facade PHPDoc to include required description parameter - Fix README examples to call strict() after defining all fields - Update test to verify toArray() idempotency instead of markRequired() - Add documentation note about calling strict() at the end These changes ensure the API is cleaner, documentation is accurate, and strict() usage follows best practices by being called after all fields are defined. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 9 +++++++-- src/Facades/Schema.php | 2 +- src/Schema/Property.php | 13 ------------- tests/Unit/BuilderTest.php | 16 ++++++++-------- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 721f471..30c164d 100644 --- a/README.md +++ b/README.md @@ -307,6 +307,8 @@ The `strict()` method is particularly useful for LLM APIs like **OpenAI Structur 1. `additionalProperties: false` 2. All properties in the `required` array +**Important:** Call `strict()` after defining all your fields to ensure all of them are marked as required. + ```php // Perfect for OpenAI Structured Outputs $schema = Struct::define('User', 'A user schema', function (Property $property) { @@ -315,6 +317,7 @@ $schema = Struct::define('User', 'A user schema', function (Property $property) $property->int('age')->min(0)->max(120); $property->string('location'); + // Call strict() at the end to mark all fields as required $property->strict(); // Makes all fields required + disallows extra properties })->toArray(); ``` @@ -329,7 +332,7 @@ This generates: } ``` -> **Note**: By default, `additionalProperties` is already set to `false`. Use `strict()` when you also need all fields to be required (like for OpenAI). +> **Note**: By default, `additionalProperties` is already set to `false`. Use `strict()` when you also need all fields to be required (like for OpenAI). Call it after defining fields to ensure all are marked as required. ### Schema Metadata @@ -361,7 +364,6 @@ $schema = Struct::define('AdvancedUser', 'Comprehensive user data structure', fu // Schema metadata $property->schemaVersion(); $property->title('Advanced User Schema'); - $property->strict(); // Disallow additional properties // Helper methods $property->uuid('id')->required(); @@ -390,6 +392,9 @@ $schema = Struct::define('AdvancedUser', 'Comprehensive user data structure', fu ->maxLength(30) ->pattern('^[a-zA-Z0-9_]+$') ->required(); + + // Call strict() after all fields to mark them all as required + $property->strict(); // Disallow extras + mark all defined fields as required })->toArray(); ``` diff --git a/src/Facades/Schema.php b/src/Facades/Schema.php index 4baf966..eb0d3f9 100644 --- a/src/Facades/Schema.php +++ b/src/Facades/Schema.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Facade; /** - * @method static array define(string $name, callable $callback) + * @method static \Blaspsoft\Forerunner\Schema\Struct define(string $name, string $description, callable $callback) * * @see \Blaspsoft\Forerunner\Schema\Struct */ diff --git a/src/Schema/Property.php b/src/Schema/Property.php index c1ba084..61f263f 100644 --- a/src/Schema/Property.php +++ b/src/Schema/Property.php @@ -11,9 +11,6 @@ class Property /** @var array */ protected array $properties = []; - /** @var array */ - protected array $required = []; - protected ?string $description = null; protected bool $additionalProperties = false; @@ -276,16 +273,6 @@ protected function addProperty(string $name, string $type, ?string $description) return $builder; } - /** - * Mark a field as required. - */ - public function markRequired(string $name): void - { - if (! in_array($name, $this->required)) { - $this->required[] = $name; - } - } - /** * Convert the builder to a JSON schema array. * diff --git a/tests/Unit/BuilderTest.php b/tests/Unit/BuilderTest.php index 5850afc..03e8f69 100644 --- a/tests/Unit/BuilderTest.php +++ b/tests/Unit/BuilderTest.php @@ -212,13 +212,13 @@ $property = new Property('TestSchema'); $property->string('name')->required(); - // Manually mark as required again - $property->markRequired('name'); - $property->markRequired('name'); - - $schema = $property->toArray(); - - expect($schema['required'])->toHaveCount(1) - ->and($schema['required'])->toContain('name'); + // Calling toArray() multiple times should not duplicate required fields + $schema1 = $property->toArray(); + $schema2 = $property->toArray(); + + expect($schema1['required'])->toHaveCount(1) + ->and($schema1['required'])->toContain('name') + ->and($schema2['required'])->toHaveCount(1) + ->and($schema2['required'])->toContain('name'); }); });