diff --git a/src/Commands/GraphqlGenerateCommand.php b/src/Commands/GraphqlGenerateCommand.php index cd823695..2aecd863 100644 --- a/src/Commands/GraphqlGenerateCommand.php +++ b/src/Commands/GraphqlGenerateCommand.php @@ -344,23 +344,21 @@ protected function mapFieldToGraphQLType($field, bool $isInput = false): string $fieldClass = get_class($field); $fieldClassName = class_basename($fieldClass); - // Use the field's built-in type guessing if available - if (method_exists($field, 'guessFieldType')) { + // Use the field's built-in type guessing if available (Laravel 12+ only) + if (method_exists($field, 'guessFieldType') && method_exists($field, 'hasJsonSchemaSupport') && $field::hasJsonSchemaSupport()) { $fieldType = $field->guessFieldType(app(RepositoryStoreRequest::class)); - switch ($fieldType) { - case 'boolean': - return 'Boolean'; - case 'number': - case 'integer': - return 'Int'; - case 'array': - return $isInput ? '[String!]' : '[String!]'; - case 'object': - return $isInput ? 'JSON' : 'JSON'; - case 'string': - default: - return 'String'; + if ($fieldType !== null) { + // Check the Type object class to determine GraphQL type + $typeClass = class_basename(get_class($fieldType)); + + return match ($typeClass) { + 'BooleanType' => 'Boolean', + 'NumberType', 'IntegerType' => 'Int', + 'ArrayType' => $isInput ? '[String!]' : '[String!]', + 'ObjectType' => $isInput ? 'JSON' : 'JSON', + default => 'String', + }; } } diff --git a/src/MCP/Concerns/FieldMcpSchemaDetection.php b/src/MCP/Concerns/FieldMcpSchemaDetection.php index f9986fc3..bf03506f 100644 --- a/src/MCP/Concerns/FieldMcpSchemaDetection.php +++ b/src/MCP/Concerns/FieldMcpSchemaDetection.php @@ -6,24 +6,35 @@ use Binaryk\LaravelRestify\Http\Requests\RestifyRequest; use Binaryk\LaravelRestify\MCP\Actions\JsonSchemaFromRulesAction; use Binaryk\LaravelRestify\Repositories\Repository; -use Illuminate\JsonSchema\JsonSchema; -use Illuminate\JsonSchema\JsonSchemaTypeFactory; -use Illuminate\JsonSchema\Types\ArrayType; -use Illuminate\JsonSchema\Types\BooleanType; -use Illuminate\JsonSchema\Types\NumberType; -use Illuminate\JsonSchema\Types\Type; /** * @mixin \Binaryk\LaravelRestify\Fields\Field */ trait FieldMcpSchemaDetection { + /** + * Check if Laravel 12+ JsonSchema classes are available. + */ + public static function hasJsonSchemaSupport(): bool + { + // Check for the interface first - this is what Laravel 12 provides + // The class_exists check on JsonSchemaTypeFactory would trigger autoloading + // which fails on Laravel 11 because the interface doesn't exist + return interface_exists(\Illuminate\Contracts\JsonSchema\JsonSchema::class); + } + /** * Guess the field type based on validation rules, field class, and attribute patterns. + * + * @return \Illuminate\JsonSchema\Types\Type|null Returns Type on Laravel 12+, null on Laravel 11 */ - public function guessFieldType(RestifyRequest $request): Type + public function guessFieldType(RestifyRequest $request): mixed { - $schema = new JsonSchemaTypeFactory; + if (! static::hasJsonSchemaSupport()) { + return null; + } + + $schema = new \Illuminate\JsonSchema\JsonSchemaTypeFactory; $rules = $this->getRulesForRequest($request); @@ -84,8 +95,8 @@ public function getDescription(RestifyRequest $request, Repository $repository): } } - // Add examples based on field type and name - if ($this->jsonSchema instanceof Type) { + // Add examples based on field type and name (Laravel 12+ only) + if (static::hasJsonSchemaSupport() && $this->jsonSchema instanceof \Illuminate\JsonSchema\Types\Type) { $examples = $this->generateFieldExamples($this->jsonSchema); if (! empty($examples)) { @@ -140,20 +151,22 @@ protected function formatValidationRules(array $rules): array /** * Generate examples for the field. + * + * @param \Illuminate\JsonSchema\JsonSchema $fieldType */ - protected function generateFieldExamples(JsonSchema $fieldType): array + protected function generateFieldExamples(mixed $fieldType): array { $attribute = strtolower($this->attribute); - if ($fieldType instanceof BooleanType) { + if ($fieldType instanceof \Illuminate\JsonSchema\Types\BooleanType) { return ['true', 'false']; } - if ($fieldType instanceof NumberType) { + if ($fieldType instanceof \Illuminate\JsonSchema\Types\NumberType) { return $this->getNumberExamples($attribute); } - if ($fieldType instanceof ArrayType) { + if ($fieldType instanceof \Illuminate\JsonSchema\Types\ArrayType) { return ['["item1", "item2"]', '{"key": "value"}']; } @@ -278,8 +291,11 @@ protected function hasAnyRule(array $ruleStrings, array $rulesToCheck): bool /** * Guess type from attribute name patterns. + * + * @param \Illuminate\JsonSchema\JsonSchema $schema + * @return \Illuminate\JsonSchema\Types\Type|null */ - protected function guessTypeFromAttributeName(JsonSchema $schema): ?Type + protected function guessTypeFromAttributeName(mixed $schema): mixed { $attribute = $this->attribute; diff --git a/tests/MCP/FieldSchemaValidationTest.php b/tests/MCP/FieldSchemaValidationTest.php index 8e6f93b5..a5af9076 100644 --- a/tests/MCP/FieldSchemaValidationTest.php +++ b/tests/MCP/FieldSchemaValidationTest.php @@ -7,59 +7,65 @@ use Binaryk\LaravelRestify\Tests\Fixtures\Post\PostRepository; use Binaryk\LaravelRestify\Tests\IntegrationTestCase; use Illuminate\Contracts\Validation\ValidationRule; -use Illuminate\JsonSchema\JsonSchemaTypeFactory; -use Illuminate\JsonSchema\Types\ArrayType; -use Illuminate\JsonSchema\Types\BooleanType; -use Illuminate\JsonSchema\Types\IntegerType; -use Illuminate\JsonSchema\Types\NumberType; -use Illuminate\JsonSchema\Types\StringType; +/** + * @requires Laravel 12+ + */ class FieldSchemaValidationTest extends IntegrationTestCase { + protected function setUp(): void + { + parent::setUp(); + + if (! interface_exists(\Illuminate\Contracts\JsonSchema\JsonSchema::class)) { + $this->markTestSkipped('JsonSchema classes are only available in Laravel 12+'); + } + } + public function test_field_rules_convert_to_correct_schema_types(): void { // Test string field type detection $titleField = Field::make('title')->rules(['required', 'string', 'max:255']); - $this->assertInstanceOf(StringType::class, $titleField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $titleField->guessFieldType(new McpStoreRequest)); // Test integer field type detection $priorityField = Field::make('priority')->rules(['required', 'integer', 'min:1']); - $this->assertInstanceOf(IntegerType::class, $priorityField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\IntegerType::class, $priorityField->guessFieldType(new McpStoreRequest)); // Test boolean field type detection $publishedField = Field::make('is_published')->rules(['boolean']); - $this->assertInstanceOf(BooleanType::class, $publishedField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\BooleanType::class, $publishedField->guessFieldType(new McpStoreRequest)); // Test numeric field type detection $ratingField = Field::make('rating')->rules(['numeric', 'between:0,5']); - $this->assertInstanceOf(NumberType::class, $ratingField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\NumberType::class, $ratingField->guessFieldType(new McpStoreRequest)); // Test email field (should be string type) $emailField = Field::make('author_email')->rules(['required', 'email']); - $this->assertInstanceOf(StringType::class, $emailField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $emailField->guessFieldType(new McpStoreRequest)); // Test array field type detection $tagsField = Field::make('tags')->rules(['array']); - $this->assertInstanceOf(ArrayType::class, $tagsField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\ArrayType::class, $tagsField->guessFieldType(new McpStoreRequest)); // Test default type for custom validation $slugField = Field::make('slug')->rules(['required', 'unique:posts,slug']); - $this->assertInstanceOf(StringType::class, $slugField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $slugField->guessFieldType(new McpStoreRequest)); } public function test_field_validation_rules_format(): void { // Test field with 'in' validation $statusField = Field::make('status')->rules(['required', 'string', 'in:draft,published,archived']); - $this->assertInstanceOf(StringType::class, $statusField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $statusField->guessFieldType(new McpStoreRequest)); // Test field with min/max rules $wordCountField = Field::make('word_count')->rules(['integer', 'min:100', 'max:5000']); - $this->assertInstanceOf(IntegerType::class, $wordCountField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\IntegerType::class, $wordCountField->guessFieldType(new McpStoreRequest)); // Test numeric field with between rule $ratingField = Field::make('rating')->rules(['numeric', 'between:1,10']); - $this->assertInstanceOf(NumberType::class, $ratingField->guessFieldType(new McpStoreRequest)); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\NumberType::class, $ratingField->guessFieldType(new McpStoreRequest)); // Test required field detection $requiredField = Field::make('name')->rules(['required', 'string']); @@ -75,7 +81,7 @@ public function test_field_validation_rules_format(): void public function test_field_json_schema_has_description(): void { $request = new McpStoreRequest; - $schemaFactory = new JsonSchemaTypeFactory; + $schemaFactory = new \Illuminate\JsonSchema\JsonSchemaTypeFactory; $repository = PostRepository::partialMock(); $field = field('published_at')->rules(['nullable', 'date', 'after:2020-01-01'])->resolveJsonSchema( $schemaFactory, @@ -90,7 +96,7 @@ public function test_field_json_schema_has_description(): void public function test_field_json_schema_prioritize_user_description(): void { $request = new McpStoreRequest; - $schemaFactory = new JsonSchemaTypeFactory; + $schemaFactory = new \Illuminate\JsonSchema\JsonSchemaTypeFactory; $repository = PostRepository::partialMock(); $field = field('published_at')->rules(['nullable', 'date', 'after:2020-01-01']) ->description('This is a custom description.') @@ -109,7 +115,7 @@ public function test_can_validate_custom_rule(): void $field = field('published_at')->rules([new UniqueClientCompanyNameRule]); $type = $field->guessFieldType(new McpStoreRequest); - $this->assertInstanceOf(StringType::class, $type); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $type); } } class UniqueClientCompanyNameRule implements ValidationRule diff --git a/tests/MCP/JsonSchemaFromRulesActionTest.php b/tests/MCP/JsonSchemaFromRulesActionTest.php index 4a92d811..9b596e22 100644 --- a/tests/MCP/JsonSchemaFromRulesActionTest.php +++ b/tests/MCP/JsonSchemaFromRulesActionTest.php @@ -4,16 +4,25 @@ use Binaryk\LaravelRestify\MCP\Actions\JsonSchemaFromRulesAction; use Binaryk\LaravelRestify\Tests\IntegrationTestCase; -use Illuminate\JsonSchema\JsonSchemaTypeFactory; -use Illuminate\JsonSchema\Types\IntegerType; -use Illuminate\JsonSchema\Types\StringType; +/** + * @requires Laravel 12+ + */ class JsonSchemaFromRulesActionTest extends IntegrationTestCase { + protected function setUp(): void + { + parent::setUp(); + + if (! interface_exists(\Illuminate\Contracts\JsonSchema\JsonSchema::class)) { + $this->markTestSkipped('JsonSchema classes are only available in Laravel 12+'); + } + } + public function test_before_date_rule_generates_correct_schema(): void { $action = new JsonSchemaFromRulesAction; - $schema = new JsonSchemaTypeFactory; + $schema = new \Illuminate\JsonSchema\JsonSchemaTypeFactory; $rules = [ 'event_date' => ['required', 'date', 'before:2025-12-31'], @@ -22,7 +31,7 @@ public function test_before_date_rule_generates_correct_schema(): void $result = $action($schema, $rules); $this->assertArrayHasKey('event_date', $result); - $this->assertInstanceOf(StringType::class, $result['event_date']); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\StringType::class, $result['event_date']); $serialized = $result['event_date']->toArray(); @@ -34,7 +43,7 @@ public function test_before_date_rule_generates_correct_schema(): void public function test_integer_rule_generates_correct_schema(): void { $action = new JsonSchemaFromRulesAction; - $schema = new JsonSchemaTypeFactory; + $schema = new \Illuminate\JsonSchema\JsonSchemaTypeFactory; $rules = [ 'age' => ['required', 'integer', 'min:18'], @@ -43,7 +52,7 @@ public function test_integer_rule_generates_correct_schema(): void $result = $action($schema, $rules); $this->assertArrayHasKey('age', $result); - $this->assertInstanceOf(IntegerType::class, $result['age']); + $this->assertInstanceOf(\Illuminate\JsonSchema\Types\IntegerType::class, $result['age']); $serialized = $result['age']->toArray();