From 06291e269628cb5197c83da5b6c39b7b9675384f Mon Sep 17 00:00:00 2001 From: hatsu Date: Tue, 17 Mar 2026 21:25:11 +0500 Subject: [PATCH 1/2] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 205 +--------- composer.json | 15 +- config/openrpc.php | 75 ++-- src/AutoDiscoverController.php | 16 - src/Cache/CacheInterface.php | 10 + src/Cache/FileCache.php | 42 ++ src/Commands/Cache.php | 42 +- src/Commands/CacheClear.php | 31 +- src/Contracts/ErrorReferenceInterface.php | 10 - .../ExamplePairingReferenceInterface.php | 10 - src/Contracts/ExampleReferenceInterface.php | 10 - src/Contracts/LinkReferenceInterface.php | 10 - src/Contracts/SchemaHandlerPipeInterface.php | 10 - src/Contracts/SchemaReferenceInterface.php | 10 - src/Contracts/TagReferenceInterface.php | 10 - src/DTO/Components.php | 20 +- src/DTO/Contact.php | 4 +- src/DTO/ContentDescriptor.php | 23 +- src/DTO/Error.php | 10 +- src/DTO/Example.php | 10 +- src/DTO/ExamplePairing.php | 42 -- src/DTO/ExternalDocumentation.php | 24 -- src/DTO/Info.php | 4 +- src/DTO/License.php | 4 +- src/DTO/Link.php | 10 +- src/DTO/{Method.php => MethodDescriptor.php} | 52 +-- src/DTO/OpenRpc.php | 25 +- src/DTO/ParameterDescriptor.php | 8 + src/DTO/Reference.php | 29 -- src/DTO/References/AbstractReference.php | 56 --- .../References/ContentDescriptorReference.php | 22 - src/DTO/References/ErrorReference.php | 22 - .../References/ExamplePairingReference.php | 22 - src/DTO/References/ExampleReference.php | 22 - src/DTO/References/LinkReference.php | 22 - src/DTO/References/SchemaReference.php | 22 - src/DTO/References/TagReference.php | 22 - src/DTO/ResultDescriptor.php | 8 + src/DTO/Schema.php | 130 +++--- src/DTO/Server.php | 7 +- src/DTO/ServerVariable.php | 4 +- src/DTO/Tag.php | 17 +- src/Descriptors/Method/MethodContext.php | 107 +++++ src/Descriptors/Method/Pipes/MethodPipe.php | 26 ++ .../Method/Pipes/ParameterPipe.php | 42 ++ .../Method/Pipes/PipeInterface.php | 10 + src/Descriptors/Method/Pipes/ResultPipe.php | 32 ++ .../Type/Handlers/CollectionHandler.php | 35 ++ .../Type/Handlers/DateTimeHandler.php | 23 ++ src/Descriptors/Type/Handlers/EnumHandler.php | 35 ++ .../Type/Handlers/HandlerInterface.php | 24 ++ .../Type/Handlers/ModelHandler.php | 75 ++++ .../Type/Handlers/ObjectHandler.php | 57 +++ .../Type/Handlers/TimeZoneHandler.php | 24 ++ src/Descriptors/Type/SingularTypeInfo.php | 15 + src/Descriptors/Type/TypeDescriptor.php | 202 +++++++++ src/Facades/MethodDescription.php | 24 -- src/Facades/OpenRpc.php | 16 - src/Handlers/OpenRpcCacheHandler.php | 31 -- src/Handlers/OpenRpcGenerator.php | 139 +++---- src/MethodDescriptionGenerator.php | 384 ------------------ src/OpenRpc.php | 69 ++++ src/OpenRpcServiceProvider.php | 71 ---- src/Pipes/ArrayShapePipe.php | 114 ------ src/Pipes/BackedEnumPipe.php | 78 ---- src/Pipes/BenSampoEnumPipe.php | 102 ----- src/Pipes/ClassPropertyFromPhpDocPipe.php | 80 ---- src/Pipes/DateTimePipe.php | 31 -- src/Pipes/ExpectedValuesPipe.php | 38 -- src/Pipes/ModelPipe.php | 127 ------ src/Pipes/ValueExamplePipe.php | 47 --- src/Support/AliasedDictionary.php | 106 ----- src/Support/ClassContext.php | 20 - src/Support/Context.php | 18 - src/Support/ContextTypeEnum.php | 17 - src/Support/DataTransferObject.php | 102 ----- src/Support/ExpectedSchemaPipeObject.php | 30 -- src/Support/MethodContext.php | 26 -- src/Support/OpenRpcConfig.php | 86 ++++ src/Support/ReferenceCleaner.php | 50 --- src/Support/StrSupport.php | 29 -- src/Support/VirtualContext.php | 11 - 82 files changed, 1184 insertions(+), 2516 deletions(-) delete mode 100644 src/AutoDiscoverController.php create mode 100644 src/Cache/CacheInterface.php create mode 100644 src/Cache/FileCache.php delete mode 100644 src/Contracts/ErrorReferenceInterface.php delete mode 100644 src/Contracts/ExamplePairingReferenceInterface.php delete mode 100644 src/Contracts/ExampleReferenceInterface.php delete mode 100644 src/Contracts/LinkReferenceInterface.php delete mode 100644 src/Contracts/SchemaHandlerPipeInterface.php delete mode 100644 src/Contracts/SchemaReferenceInterface.php delete mode 100644 src/Contracts/TagReferenceInterface.php delete mode 100644 src/DTO/ExamplePairing.php delete mode 100644 src/DTO/ExternalDocumentation.php rename src/DTO/{Method.php => MethodDescriptor.php} (60%) create mode 100644 src/DTO/ParameterDescriptor.php delete mode 100644 src/DTO/Reference.php delete mode 100644 src/DTO/References/AbstractReference.php delete mode 100644 src/DTO/References/ContentDescriptorReference.php delete mode 100644 src/DTO/References/ErrorReference.php delete mode 100644 src/DTO/References/ExamplePairingReference.php delete mode 100644 src/DTO/References/ExampleReference.php delete mode 100644 src/DTO/References/LinkReference.php delete mode 100644 src/DTO/References/SchemaReference.php delete mode 100644 src/DTO/References/TagReference.php create mode 100644 src/DTO/ResultDescriptor.php create mode 100644 src/Descriptors/Method/MethodContext.php create mode 100644 src/Descriptors/Method/Pipes/MethodPipe.php create mode 100644 src/Descriptors/Method/Pipes/ParameterPipe.php create mode 100644 src/Descriptors/Method/Pipes/PipeInterface.php create mode 100644 src/Descriptors/Method/Pipes/ResultPipe.php create mode 100644 src/Descriptors/Type/Handlers/CollectionHandler.php create mode 100644 src/Descriptors/Type/Handlers/DateTimeHandler.php create mode 100644 src/Descriptors/Type/Handlers/EnumHandler.php create mode 100644 src/Descriptors/Type/Handlers/HandlerInterface.php create mode 100644 src/Descriptors/Type/Handlers/ModelHandler.php create mode 100644 src/Descriptors/Type/Handlers/ObjectHandler.php create mode 100644 src/Descriptors/Type/Handlers/TimeZoneHandler.php create mode 100644 src/Descriptors/Type/SingularTypeInfo.php create mode 100644 src/Descriptors/Type/TypeDescriptor.php delete mode 100644 src/Facades/MethodDescription.php delete mode 100644 src/Facades/OpenRpc.php delete mode 100644 src/Handlers/OpenRpcCacheHandler.php delete mode 100644 src/MethodDescriptionGenerator.php create mode 100644 src/OpenRpc.php delete mode 100644 src/Pipes/ArrayShapePipe.php delete mode 100644 src/Pipes/BackedEnumPipe.php delete mode 100644 src/Pipes/BenSampoEnumPipe.php delete mode 100644 src/Pipes/ClassPropertyFromPhpDocPipe.php delete mode 100644 src/Pipes/DateTimePipe.php delete mode 100644 src/Pipes/ExpectedValuesPipe.php delete mode 100644 src/Pipes/ModelPipe.php delete mode 100644 src/Pipes/ValueExamplePipe.php delete mode 100644 src/Support/AliasedDictionary.php delete mode 100644 src/Support/ClassContext.php delete mode 100644 src/Support/Context.php delete mode 100644 src/Support/ContextTypeEnum.php delete mode 100644 src/Support/DataTransferObject.php delete mode 100644 src/Support/ExpectedSchemaPipeObject.php delete mode 100644 src/Support/MethodContext.php create mode 100644 src/Support/OpenRpcConfig.php delete mode 100644 src/Support/ReferenceCleaner.php delete mode 100644 src/Support/StrSupport.php delete mode 100644 src/Support/VirtualContext.php diff --git a/README.md b/README.md index 81d96a6..b85ba89 100644 --- a/README.md +++ b/README.md @@ -46,78 +46,37 @@ $app->register(\Tochka\OpenRpc\OpenRpcServiceProvider::class); $app->withFacades(); ``` -# Настройка точки входа -Укажите в конфигурации `openrpc.endpoint` точку входа для получения схемы OpenRpc: +# Настройка маршрута + ```php -'endpoint' => '/api/openrpc.json' +Route::get('/api/v1/public/openrpc', function () { + $openRpc = new Tochka\OpenRpc\OpenRpc( + OpenRpcConfig::makeFromConfigFile('default'), + new Router(ServerConfig::makeFromConfigFile('default')) + ); + + return response($openRpc->handle())->header('Content-Type', 'application/json'); +}); ``` -В дальнейшем эта точка входа будет использована для Service Discovery Method. # Кеширование OpenRpc может кешировать схему с описанием JsonRpc, чтобы не собирать ее каждый раз при вызове endpoint. Для создания кеша используйте команду artisan: ```shell -php artisan openrpc:cache +php artisan openrpc:cache {server?} ``` После выполнения этой команды будет выполнена сборка схемы и сохранена в файл кеша. При каждом следующем обращении к OpenRpc будет использован именно этот файл, повторной пересборки происходить не будет. +Если указать имя сервера, то будет закеширован указанный сервер, иначе будут закешированы все сервера из конфигурации. Если файла с кешем нет - схема каждый раз будет собираться заново. Чтобы очистить кеш - используйте команду artisan: ```shell -php artisan openrpc:clear +php artisan openrpc:clear {server?} ``` Рекомендуется запускать команду кеширования сразу после деплоя перед запуском приложения. -# Как использовать - -## Аннотации и атрибуты -Некоторые пометки или уточнения для классов, методов и полей могут использовать аннотации -(https://www.doctrine-project.org/projects/doctrine-annotations/en/1.10/index.html) -или атрибуты (https://www.php.net/manual/ru/language.attributes.overview.php). -Все классы аннотаций/атрибутов обратно совместимы и могут работать и как аннотации (для версии PHP<8), и как атрибуты -(для версии PHP>=8). -Примеры использования аннотаций: -```php -use Tochka\JsonRpc\Annotations\ApiIgnore; -use Tochka\JsonRpc\Annotations\ApiValueExample; -use Tochka\JsonRpc\Annotations\ApiArrayShape; - -/** -* @ApiIgnore() -*/ -class TestDTO -{ - /** - * @ApiValueExample(examples={1, 5, 6}) - */ - public ?int $int; - - /** - * @ApiArrayShape(shape={"test": "string", "foo": "int", "bar": "array", "object": FooObject::class}) - */ - public array $testShape; -} -``` - -Пример использования атрибутов: -```php -use Tochka\JsonRpc\Annotations\ApiIgnore; -use Tochka\JsonRpc\Annotations\ApiValueExample; -use Tochka\JsonRpc\Annotations\ApiArrayShape; - -#[ApiIgnore] -class TestDTO -{ - #[ApiValueExample(examples: [1, 5, 6])] - public ?int $int; - - #[ApiArrayShape(shape: ['test' => 'string', 'foo' => 'int', 'bar' => 'array', 'object' => FooObject::class])] - public array $testShape; -} -``` - ## Расширенное описание Во многих объектах спецификации OpenRpc есть поле description, которое позволяет использовать MarkDown разметку. Для более удобной организации есть возможность выносить эти описания в отдельные файлы с расширением .md, и затем в @@ -151,19 +110,10 @@ JsonRpc-сервера описываются в конфигурации `jsonr OpenRpc собирает информацию о доступных методах, получая информацию о [маршрутах из JsonRpc-сервера](https://github.com/tochka-developers/jsonrpc#%D0%BC%D0%B0%D1%80%D1%88%D1%80%D1%83%D1%82%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F-%D1%80%D0%BE%D1%83%D1%82%D0%B8%D0%BD%D0%B3). -## Информация о формате запроса -Если в методе используется аннотация/атрибут -[`ApiMapRequestToObject`](https://github.com/tochka-developers/jsonrpc#%D0%BC%D0%B0%D0%BF%D0%BF%D0%B8%D0%BD%D0%B3-%D0%BF%D0%B0%D1%80%D0%B0%D0%BC%D0%B5%D1%82%D1%80%D0%BE%D0%B2-%D0%B2-dto-%D0%B8-%D0%BF%D0%BE%D0%BB%D1%83%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BF%D0%BE%D0%BB%D0%BD%D0%BE%D0%B3%D0%BE-%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%B0) -, то в качестве параметров метода будут указаны поля класса, в который JsonRpc будет маппить запрос. - -Если используется стандартное прокидывание параметров запроса в параметры метода - то OpenRpc будет собирать информацию -об используемых параметрах из этих параметров метода. При этом учитывается типизация, а также дополнительной описание -параметров с помощью PhpDoc (тег `@param`). - Вы можете уточнять тип вложенных в массив элементов также с помощью тегов `@param` и `@return`: ```php /** - * @param array $foo Описание параметра FOO + * @param array $foo Описание параметра FOO * @param string[] $bar Описание параметра BAR * @return Result[] */ @@ -174,8 +124,7 @@ public function myMethod(array $foo, array $bar): array ``` Если в качестве типа указан какой-либо класс - OpenRpc попытается описать внутреннюю структуру класса. -По умолчанию выбираются все публичные поля класса. Кроме того, учитываются поля, описанные в phpDoc -(помеченные атрибутом `@property`). +По умолчанию выбираются все публичные поля класса. В качестве типа аргумента всегда выбирается тип поля. Поле считается обязательным, если у него нет дефолтного значения. @@ -191,133 +140,23 @@ class TestDTO } ``` -Если в качестве типа указан экземпляр `BenSampo\Enum\Enum` - OpenRpc автоматически получит все варианты значений для поля, -и попытается вычислить тип. Информация о возможных значениях будет отражена в схеме. - Если в качестве типа указан экземпляр `Illuminate\Database\Eloquent\Model` - OpenRpc получит все возможные поля из phpDoc (как указано выше), а затем попытается отфильтровать их в соответствии с правилами, указанными в hidden и visible (https://laravel.com/docs/8.x/eloquent-serialization#hiding-attributes-from-json) -Для указания примеров значений для аргумента используйте аннотацию/атрибут `ApiValueExample`: -```php -use Tochka\JsonRpc\Annotations\ApiValueExample; - -class TestDTO -{ - #[ApiValueExample(examples: [1, 3, 5])] - public int $field; - - /** - * @ApiValueExample(examples={"foo", "bar"}} - */ - public string $foo; -} -``` - -Для указания вариантов возможных значений (если не используется Enum) - используйте аннотацию/атрибут `ApiExpectedValues`: -```php -use Tochka\JsonRpc\Annotations\ApiExpectedValues; - -class TestDTO -{ - #[ApiExpectedValues(values: [1, 3, 5])] - public int $field; - - /** - * @ApiExpectedValues(values={"foo", "bar"}} - */ - public string $foo; -} -``` - -Для указания формата объекта (если для него нет отдельного класса) - используйте аннотацию/атрибут `ApiArrayShape`: -```php -use Tochka\JsonRpc\Annotations\ApiArrayShape; - -class TestDTO -{ - #[ApiArrayShape(shape: ['test' => 'string', 'foo' => 'int', 'bar' => 'array', 'object' => FooObject::class])] - public int $field; - - /** - * @ApiArrayShape(shape={"test": "string", "foo": "int", "bar": "array", "object": FooObject::class}} - */ - public string $foo; -} -``` - -Первая строка описания поля в phpDoc считается summary - и отображается в кратком описании аргумента в схеме. -Вторая и следующие строки описания поля в phpDoc считаются description - и отображаются в полном описании аргумента в -схеме. При этом разрешено ссылаться на MarkDown файл: -```php -class TestDTO -{ - /** - * Это будет summary - * А вот это будет description - */ - public int $someField; - - /** - * Это будет summary - * $views/docs/description.md // содержимое этого файла будет description - */ - public string $foo; -} -``` - ### Информация об ответе OpenRpc забирает информацию о формате ответа из типа, указанного в качестве результата метода. При этом тип может быть указан также в phpDoc этого метода. При этом все возможности OpenRpc, описанные в предыдущем пункте для запросов - будут также работать и для описания формата ответа. -Кроме того, дополнительно есть возможность описать формат ответа с помощью атрибута/аннотации `ApiArrayShape`: -```php -use Tochka\JsonRpc\Annotations\ApiArrayShape; -class TestController -{ - #[ApiArrayShape(shape: ['test' => 'string', 'foo' => 'int', 'bar' => 'array', 'object' => FooObject::class])] - public function someMethod(): array - { - return []; - } - - /** - * @ApiArrayShape(shape={"test": "string", "foo": "int", "bar": "array", "object": FooObject::class}} - */ - public function fooMethod(): array - { - return []; - } -} -``` - -Также аннотацию/атрибут `ApiArrayShape` можно использовать для описания структуры класса: -```php -use Tochka\JsonRpc\Annotations\ApiArrayShape; - -/** - * @ApiArrayShape(shape={"test": "string", "foo": "int", "bar": "array", "object": FooObject::class}) - */ -class MyResultClass -{ - //... -} -``` - -Также эту аннотацию можно использовать для переопределения структуры какого либо свойства класса: -```php -use Tochka\JsonRpc\Annotations\ApiArrayShape; - -class MyResultClass -{ - #[ApiArrayShape(shape: ['test' => 'string', 'foo' => 'int', 'bar' => 'array', 'object' => FooObject::class])] - public SomeClass $property; -} -``` -### Примеры запросов и ответов -*В разработке...* +### Расширение +*Скоро...* ### Ошибки *В разработке...* + +Обновление с v1 до v2 +- v2 Требует версию пакета tochka-developers/jsonrpc v5 и выше +- обновите конфигурацию на новый формат +- добавьте маршрут для обработки запроса, как указано выше diff --git a/composer.json b/composer.json index 25b3a5a..8ace4a9 100644 --- a/composer.json +++ b/composer.json @@ -16,17 +16,16 @@ "require": { "php": "8.1.*|8.2.*|8.3.*|8.4.*", "ext-json": "*", - "illuminate/support": "^9.0|^10.0|^11.0|^12.0", - "illuminate/console": "^9.0|^10.0|^11.0|^12.0", - "illuminate/container": "^9.0|^10.0|^11.0|^12.0", - "illuminate/pipeline": "^9.0|^10.0|^11.0|^12.0", - "spiral/attributes": "^2.8|^3.0", - "tochka-developers/jsonrpc": "^4.0", - "tochka-developers/jsonrpc-annotations": "^1.1" + "illuminate/support": "^11.0|^12.0", + "illuminate/console": "^11.0|^12.0", + "illuminate/container": "^11.0|^12.0", + "illuminate/pipeline": "^^11.0|^12.0", + "tochka-developers/jsonrpc": "^5.0", + "phpdocumentor/reflection-docblock": "^6.0" }, "require-dev": { "roave/security-advisories": "dev-latest", - "phpunit/phpunit": "^9.5|^10.0|^11.0", + "phpunit/phpunit": "^10.0|^11.0", "mockery/mockery": "^1.3" }, "autoload": { diff --git a/config/openrpc.php b/config/openrpc.php index 1891e05..77e94ec 100644 --- a/config/openrpc.php +++ b/config/openrpc.php @@ -1,44 +1,41 @@ 'Some JsonRpc', - - /** Endpoint for OpenRpc schema */ - 'endpoint' => '/api/openrpc.json', - - /** - * A verbose description of the application. GitHub Flavored Markdown syntax MAY be used for rich text representation - * You can use a link to a * .md file with a description (relative to the directory resource/) - * $views/docs/description.md - * (for file at resource/views/docs/description.md) - */ - 'description' => 'Description of JsonRpc Server', - /** The version of the OpenRPC document (which is distinct from the OpenRPC Specification version or the API implementation version) */ - 'version' => env('VERSION', '1.0.0'), - /** A URL to the Terms of Service for the API. MUST be in the format of a URL */ - 'termsOfService' => 'https://terms.com/', - /** The contact information for the exposed API */ - 'contact' => [ - /** The email address of the contact person/organization. MUST be in the format of an email address */ - 'email' => 'contact@mail.com', - /** The identifying name of the contact person/organization */ - 'name' => 'Ivan Ivanov', - /** The URL pointing to the contact information. MUST be in the format of a URL */ - 'url' => 'http://mysite.com', + 'default' => [ + 'server' => [ + 'name' => 'server', + 'url' => 'https://127.0.0.1/api/v1/openrpc', + 'summary' => 'summary', + 'description' => 'description', + ], + 'info' => [ + 'version' => '1.0.0', + 'title' => 'service', + 'description' => 'description', + 'contact' => [ + 'name' => 'name', + 'url' => 'https://127.0.0.1/contact', + 'email' => 'name@example.com', + ], + 'license' => [ + 'name' => 'MIT', + 'url' => 'http://mit.com', + ], + 'termsOfService' => 'https://127.0.0.1/termsOfService', + ], + 'methodPipes' => [ + \Tochka\OpenRpc\Descriptors\Method\Pipes\MethodPipe::class, + \Tochka\OpenRpc\Descriptors\Method\Pipes\ParameterPipe::class, + \Tochka\OpenRpc\Descriptors\Method\Pipes\ResultPipe::class, + ], + 'typeDescriptors' => [ + \Tochka\OpenRpc\Descriptors\Type\Handlers\EnumHandler::class, + \Tochka\OpenRpc\Descriptors\Type\Handlers\DateTimeHandler::class, + \Tochka\OpenRpc\Descriptors\Type\Handlers\ModelHandler::class, + \Tochka\OpenRpc\Descriptors\Type\Handlers\TimeZoneHandler::class, + \Tochka\OpenRpc\Descriptors\Type\Handlers\CollectionHandler::class, + \Tochka\OpenRpc\Descriptors\Type\Handlers\ObjectHandler::class, + ], + 'cache' => \Tochka\JsonRpc\Helpers\ArrayFileCache::class, ], - /** The license information for the exposed API */ - 'license' => [ - /** The license name used for the API */ - 'name' => 'MIT', - /** A URL to the license used for the API. MUST be in the format of a URL */ - 'url' => 'http://mit.com' - ], - /** Additional external documentation */ - 'externalDocumentation' => [ - /** The URL for the target documentation. Value MUST be in the format of a URL */ - 'url' => 'https://google.com', - /** A verbose explanation of the target documentation. GitHub Flavored Markdown syntax MAY be used for rich text representation */ - 'description' => 'Description of external documentation', - ] ]; diff --git a/src/AutoDiscoverController.php b/src/AutoDiscoverController.php deleted file mode 100644 index d488c4c..0000000 --- a/src/AutoDiscoverController.php +++ /dev/null @@ -1,16 +0,0 @@ - 'OpenRPC Schema', - 'schema' => OpenRpc::handle(), - ]; - } -} diff --git a/src/Cache/CacheInterface.php b/src/Cache/CacheInterface.php new file mode 100644 index 0000000..b48425b --- /dev/null +++ b/src/Cache/CacheInterface.php @@ -0,0 +1,10 @@ +filePath = $cachePath . '/' . $cacheName . '.json'; + } + + public function get(): ?string + { + if (file_exists($this->filePath)) { + return file_get_contents($this->filePath); + } + + return null; + } + + /** + * @throws \JsonException + */ + public function set(object|array $data): void + { + $data = json_encode($data, JSON_THROW_ON_ERROR); + file_put_contents($this->filePath, $data); + } + + public function clear(): void + { + if (file_exists($this->filePath)) { + unlink($this->filePath); + } + } +} diff --git a/src/Commands/Cache.php b/src/Commands/Cache.php index 7227009..553c7e3 100644 --- a/src/Commands/Cache.php +++ b/src/Commands/Cache.php @@ -3,28 +3,48 @@ namespace Tochka\OpenRpc\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Facades\App; -use Psr\SimpleCache\CacheInterface; +use Illuminate\Support\Facades\Config; use Psr\SimpleCache\InvalidArgumentException; -use Tochka\OpenRpc\Facades\OpenRpc; +use Tochka\JsonRpc\Router\Router; +use Tochka\JsonRpc\Support\ServerConfig; +use Tochka\OpenRpc\OpenRpc; +use Tochka\OpenRpc\Support\OpenRpcConfig; class Cache extends Command { - protected $signature = 'openrpc:cache'; + protected $signature = 'openrpc:cache {server?}'; protected $description = 'Make and cache OpenRpc schema'; /** + * @throws \ReflectionException * @throws InvalidArgumentException */ public function handle(): void { - /** @var CacheInterface $cache */ - $cache = App::make('OpenRpcCache'); - - $cache->clear(); - $this->info('OpenRpc cache cleared!'); + $serverName = $this->argument('server'); + if ($serverName) { + $this->handleOne($serverName); + } else { + $configs = Config::get('openrpc', []); + foreach ($configs as $name => $_) { + $this->handleOne($name); + } + } + } - $cache->set('schema', OpenRpc::handle()); - $this->info('OpenRpc cached successfully!'); + /** + * @throws \ReflectionException + * @throws InvalidArgumentException + */ + protected function handleOne(string $serverName): void + { + $openRpc = new OpenRpc( + OpenRpcConfig::makeFromConfigFile($serverName), + new Router(ServerConfig::makeFromConfigFile($serverName)) + ); + $openRpc->cacheClear(); + $this->info('Server: ' . $serverName . ' OpenRpc cache cleared!'); + $openRpc->cacheMake(); + $this->info('Server: ' . $serverName . ' OpenRpc cached successfully!'); } } diff --git a/src/Commands/CacheClear.php b/src/Commands/CacheClear.php index d8ca01c..c15305d 100644 --- a/src/Commands/CacheClear.php +++ b/src/Commands/CacheClear.php @@ -3,20 +3,37 @@ namespace Tochka\OpenRpc\Commands; use Illuminate\Console\Command; -use Illuminate\Support\Facades\App; -use Psr\SimpleCache\CacheInterface; +use Illuminate\Support\Facades\Config; +use Tochka\JsonRpc\Router\Router; +use Tochka\JsonRpc\Support\ServerConfig; +use Tochka\OpenRpc\OpenRpc; +use Tochka\OpenRpc\Support\OpenRpcConfig; class CacheClear extends Command { - protected $signature = 'openrpc:clear'; + protected $signature = 'openrpc:clear {server?}'; protected $description = 'Clear cached OpenRpc schema'; public function handle(): void { - /** @var CacheInterface $cache */ - $cache = App::make('OpenRpcCache'); + $serverName = $this->argument('server'); + if ($serverName) { + $this->handleOne($serverName); + } else { + $configs = Config::get('openrpc', []); + foreach ($configs as $name => $_) { + $this->handleOne($name); + } + } + } - $cache->clear(); - $this->info('OpenRpc cache cleared!'); + protected function handleOne(string $serverName): void + { + $openRpc = new OpenRpc( + OpenRpcConfig::makeFromConfigFile($serverName), + new Router(ServerConfig::makeFromConfigFile($serverName)) + ); + $openRpc->cacheClear(); + $this->info('Server: ' . $serverName . ' OpenRpc cache cleared!'); } } diff --git a/src/Contracts/ErrorReferenceInterface.php b/src/Contracts/ErrorReferenceInterface.php deleted file mode 100644 index a8e0cd0..0000000 --- a/src/Contracts/ErrorReferenceInterface.php +++ /dev/null @@ -1,10 +0,0 @@ - @@ -51,12 +39,6 @@ final class Components extends DataTransferObject */ public array $errors = []; - /** - * An object to hold reusable Example Pairing Objects. - * @var array - */ - public array $examplePairingObjects = []; - /** * An object to hold reusable Tag Objects. * @var array diff --git a/src/DTO/Contact.php b/src/DTO/Contact.php index e524981..3f2d63d 100644 --- a/src/DTO/Contact.php +++ b/src/DTO/Contact.php @@ -2,12 +2,10 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * Contact information for the exposed API. */ -final class Contact extends DataTransferObject +final class Contact { /** * The identifying name of the contact person/organization. diff --git a/src/DTO/ContentDescriptor.php b/src/DTO/ContentDescriptor.php index 251458d..3635dbc 100644 --- a/src/DTO/ContentDescriptor.php +++ b/src/DTO/ContentDescriptor.php @@ -2,26 +2,18 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\ContentDescriptorReferenceInterface; -use Tochka\OpenRpc\Contracts\SchemaReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * Content Descriptors are objects that do just as they suggest - describe content. * They are reusable ways of describing either parameters or result. They MUST have a schema. */ -final class ContentDescriptor extends DataTransferObject implements ContentDescriptorReferenceInterface +class ContentDescriptor { - protected array $onlyNotEmptyKeys = [ - 'summary', - 'description', - ]; /** * REQUIRED. Name of the content that is being described. If the content described is a method parameter * assignable by-name, this field SHALL define the parameter’s key (ie name). */ - public string $name; + public readonly string $name; /** * A short summary of the content that is being described. @@ -35,28 +27,23 @@ final class ContentDescriptor extends DataTransferObject implements ContentDescr public ?string $description; /** - * Determines if the content is a required field. Default value is false. + * Determines if the content is a required field. */ public bool $required; /** * REQUIRED. Schema that describes the content. */ - public SchemaReferenceInterface $schema; + public Schema $schema; /** * Specifies that the content is deprecated and SHOULD be transitioned out of usage. Default value is false. */ public bool $deprecated; - public function __construct(string $name, SchemaReferenceInterface $schema) + public function __construct(string $name, Schema $schema) { $this->name = $name; $this->schema = $schema; } - - public function getContentDescriptor(): ContentDescriptor - { - return $this; - } } diff --git a/src/DTO/Error.php b/src/DTO/Error.php index 94a731f..c97fbd6 100644 --- a/src/DTO/Error.php +++ b/src/DTO/Error.php @@ -2,13 +2,10 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\ErrorReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * Defines an application level error. */ -final class Error extends DataTransferObject implements ErrorReferenceInterface +final class Error { /** * REQUIRED. A Number that indicates the error type that occurred. This MUST be an integer. The error codes @@ -37,9 +34,4 @@ public function __construct(int $code, string $message) unset($this->data); } - - public function getError(): Error - { - return $this; - } } diff --git a/src/DTO/Example.php b/src/DTO/Example.php index 7dfa3c5..f713b48 100644 --- a/src/DTO/Example.php +++ b/src/DTO/Example.php @@ -2,15 +2,12 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\ExampleReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * The Example object is an object the defines an example that is intended to match a given Content Descriptor Schema. * If the Content Descriptor Schema includes examples, the value from this Example Object supercedes the value * of the schema example. */ -final class Example extends DataTransferObject implements ExampleReferenceInterface +final class Example { /** * Canonical name of the example. @@ -41,9 +38,4 @@ final class Example extends DataTransferObject implements ExampleReferenceInterf * cannot easily be included in JSON documents. The value field and externalValue field are mutually exclusive. */ public ?string $externalValue = null; - - public function getExample(): Example - { - return $this; - } } diff --git a/src/DTO/ExamplePairing.php b/src/DTO/ExamplePairing.php deleted file mode 100644 index 122b7f3..0000000 --- a/src/DTO/ExamplePairing.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public array $params; - - /** - * Example result. - */ - public ?ExampleReferenceInterface $result; - - public function getExamplePairing(): ExamplePairing - { - return $this; - } -} diff --git a/src/DTO/ExternalDocumentation.php b/src/DTO/ExternalDocumentation.php deleted file mode 100644 index e743c10..0000000 --- a/src/DTO/ExternalDocumentation.php +++ /dev/null @@ -1,24 +0,0 @@ -url = $url; - } -} diff --git a/src/DTO/Info.php b/src/DTO/Info.php index 9aa08c2..180dddb 100644 --- a/src/DTO/Info.php +++ b/src/DTO/Info.php @@ -2,13 +2,11 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * The object provides metadata about the API. The metadata MAY be used by the clients if needed, and MAY be * presented in editing or documentation generation tools for convenience. */ -final class Info extends DataTransferObject +final class Info { /** * REQUIRED. The title of the application. diff --git a/src/DTO/License.php b/src/DTO/License.php index efe9e2d..e91ef4d 100644 --- a/src/DTO/License.php +++ b/src/DTO/License.php @@ -2,12 +2,10 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * License information for the exposed API. */ -final class License extends DataTransferObject +final class License { /** * REQUIRED. The license name used for the API. diff --git a/src/DTO/Link.php b/src/DTO/Link.php index 452bf36..dea2792 100644 --- a/src/DTO/Link.php +++ b/src/DTO/Link.php @@ -2,9 +2,6 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\LinkReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * The Link object represents a possible design-time link for a result. The presence of a link does not guarantee * the caller’s ability to successfully invoke it, rather it provides a known relationship and traversal mechanism @@ -16,7 +13,7 @@ * For computing links, and providing instructions to execute them, a runtime expression is used for accessing * values in an method and using them as parameters while invoking the linked method. */ -final class Link extends DataTransferObject implements LinkReferenceInterface +final class Link { /** * REQUIRED. Canonical name of the link. @@ -58,9 +55,4 @@ public function __construct(string $name) { $this->name = $name; } - - public function getLink(): Link - { - return $this; - } } diff --git a/src/DTO/Method.php b/src/DTO/MethodDescriptor.php similarity index 60% rename from src/DTO/Method.php rename to src/DTO/MethodDescriptor.php index 08b8197..098265f 100644 --- a/src/DTO/Method.php +++ b/src/DTO/MethodDescriptor.php @@ -2,18 +2,11 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\ContentDescriptorReferenceInterface; -use Tochka\OpenRpc\Contracts\ErrorReferenceInterface; -use Tochka\OpenRpc\Contracts\ExamplePairingReferenceInterface; -use Tochka\OpenRpc\Contracts\LinkReferenceInterface; -use Tochka\OpenRpc\Contracts\TagReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * Describes the interface for the given method name. The method name is used as the method field of the JSON-RPC body. * It therefore MUST be unique. */ -final class Method extends DataTransferObject +final class MethodDescriptor { /** * REQUIRED. The canonical name for the method. The name MUST be unique within the methods array. @@ -24,7 +17,7 @@ final class Method extends DataTransferObject * A list of tags for API documentation control. Tags can be used for logical grouping of methods by * resources or any other qualifier. * - * @var array + * @var array */ public ?array $tags; @@ -39,27 +32,21 @@ final class Method extends DataTransferObject */ public ?string $description; - /** - * Additional external documentation for this method. - */ - public ?ExternalDocumentation $externalDocs; - /** * REQUIRED. A list of parameters that are applicable for this method. The list MUST NOT include duplicated * parameters and therefore require name to be unique. The list can use the Reference Object to link to * parameters that are defined by the Content Descriptor Object. All optional params (content descriptor * objects with “required”: false) MUST be positioned after all required params in the list. * - * @var array + * @var array */ - public array $params; + public array $params = []; /** * REQUIRED. The description of the result returned by the method. It MUST be a Content Descriptor. * - * @var ContentDescriptorReferenceInterface */ - public ContentDescriptorReferenceInterface $result; + public ResultDescriptor $result; /** * Declares this method to be deprecated. Consumers SHOULD refrain from usage of the declared method. @@ -67,23 +54,15 @@ final class Method extends DataTransferObject */ public bool $deprecated; - /** - * An alternative servers array to service this method. If an alternative servers array is specified at the - * Root level, it will be overridden by this value. - * - * @var array - */ - public array $servers = []; - /** * A list of custom application defined errors that MAY be returned. The Errors MUST have unique error codes. - * @var array|null + * @var array|null */ public ?array $errors; /** * A list of possible links from this method call. - * @var array|null + * @var array|null */ public ?array $links; @@ -94,26 +73,15 @@ final class Method extends DataTransferObject * whose params field is an object. Further, the key names of the params object MUST be the same as the * contentDescriptor.names for the given method. Defaults to "either". * - * @var string|null + * @var string */ public string $paramStructure = 'either'; - - /** - * Array of Example Pairing Object where each example includes a valid params-to-result Content Descriptor pairing. - * - * @var array|null - */ - public ?array $examples; - + /** * @param string $name - * @param array $params - * @param ContentDescriptorReferenceInterface $result */ - public function __construct(string $name, array $params, ContentDescriptorReferenceInterface $result) + public function __construct(string $name) { $this->name = $name; - $this->params = $params; - $this->result = $result; } } diff --git a/src/DTO/OpenRpc.php b/src/DTO/OpenRpc.php index d0d7344..e81a243 100644 --- a/src/DTO/OpenRpc.php +++ b/src/DTO/OpenRpc.php @@ -2,13 +2,7 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - -/** - * This is the root object of the OpenRPC document. The contents of this object represent a whole OpenRPC - * document. How this object is constructed or stored is outside the scope of the OpenRPC Specification. - */ -final class OpenRpc extends DataTransferObject +final class OpenRpc { /** * REQUIRED. This string MUST be the semantic version number of the OpenRPC Specification version that the @@ -35,30 +29,23 @@ final class OpenRpc extends DataTransferObject * REQUIRED. The available methods for the API. While it is required, the array may be empty (to handle * security filtering, for example). * - * @var array + * @var array */ - public array $methods; + public array $methods = []; /** * An element to hold various schemas for the specification. */ public ?Components $components; - /** - * Additional external documentation. - */ - public ?ExternalDocumentation $externalDocumentation; - /** * OpenRpc constructor. - * @param string $openrpc + * @param string $version * @param Info $info - * @param array $methods */ - public function __construct(string $openrpc, Info $info, array $methods) + public function __construct(string $version, Info $info) { - $this->openrpc = $openrpc; + $this->openrpc = $version; $this->info = $info; - $this->methods = $methods; } } diff --git a/src/DTO/ParameterDescriptor.php b/src/DTO/ParameterDescriptor.php new file mode 100644 index 0000000..5ec912f --- /dev/null +++ b/src/DTO/ParameterDescriptor.php @@ -0,0 +1,8 @@ +ref = $ref; - } - - public function toArray(): array - { - return [ - '$ref' => $this->ref, - ]; - } -} diff --git a/src/DTO/References/AbstractReference.php b/src/DTO/References/AbstractReference.php deleted file mode 100644 index ca0a03c..0000000 --- a/src/DTO/References/AbstractReference.php +++ /dev/null @@ -1,56 +0,0 @@ - */ - private AliasedDictionary $dictionary; - - /** - * @param string $name - * @param AliasedDictionary $dictionary - */ - public function __construct(string $name, AliasedDictionary $dictionary) - { - $this->name = $name; - $this->dictionary = $dictionary; - } - - /** - * @return T - */ - public function getItem(): object - { - return $this->getDictionary()->getItem($this->getName()); - } - - /** - * @return AliasedDictionary - */ - public function getDictionary(): AliasedDictionary - { - return $this->dictionary; - } - - public function getName(): string - { - return $this->name; - } - - public function toArray(): array - { - return (new Reference(trim($this->getPath(), '/') . '/' . $this->dictionary->getAlias($this->name))) - ->toArray(); - } - - abstract protected function getPath(): string; -} diff --git a/src/DTO/References/ContentDescriptorReference.php b/src/DTO/References/ContentDescriptorReference.php deleted file mode 100644 index 2412756..0000000 --- a/src/DTO/References/ContentDescriptorReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class ContentDescriptorReference extends AbstractReference implements ContentDescriptorReferenceInterface -{ - protected function getPath(): string - { - return '#/components/contentDescriptors'; - } - - public function getContentDescriptor(): ContentDescriptor - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/ErrorReference.php b/src/DTO/References/ErrorReference.php deleted file mode 100644 index a8ab2c1..0000000 --- a/src/DTO/References/ErrorReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class ErrorReference extends AbstractReference implements ErrorReferenceInterface -{ - protected function getPath(): string - { - return '#/components/errors'; - } - - public function getError(): Error - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/ExamplePairingReference.php b/src/DTO/References/ExamplePairingReference.php deleted file mode 100644 index 09bd0db..0000000 --- a/src/DTO/References/ExamplePairingReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class ExamplePairingReference extends AbstractReference implements ExamplePairingReferenceInterface -{ - protected function getPath(): string - { - return '#/components/examplePairingObjects'; - } - - public function getExamplePairing(): ExamplePairing - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/ExampleReference.php b/src/DTO/References/ExampleReference.php deleted file mode 100644 index b9ef9bf..0000000 --- a/src/DTO/References/ExampleReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class ExampleReference extends AbstractReference implements ExampleReferenceInterface -{ - protected function getPath(): string - { - return '#/components/examples'; - } - - public function getExample(): Example - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/LinkReference.php b/src/DTO/References/LinkReference.php deleted file mode 100644 index ccafe95..0000000 --- a/src/DTO/References/LinkReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class LinkReference extends AbstractReference implements LinkReferenceInterface -{ - protected function getPath(): string - { - return '#/components/links'; - } - - public function getLink(): Link - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/SchemaReference.php b/src/DTO/References/SchemaReference.php deleted file mode 100644 index 786574c..0000000 --- a/src/DTO/References/SchemaReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class SchemaReference extends AbstractReference implements SchemaReferenceInterface -{ - public function getPath(): string - { - return '#/components/schemas'; - } - - public function getSchema(): Schema - { - return $this->getItem(); - } -} diff --git a/src/DTO/References/TagReference.php b/src/DTO/References/TagReference.php deleted file mode 100644 index 83f9ae9..0000000 --- a/src/DTO/References/TagReference.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -final class TagReference extends AbstractReference implements TagReferenceInterface -{ - public function getPath(): string - { - return '#/components/tags'; - } - - public function getTag(): Tag - { - return $this->getItem(); - } -} diff --git a/src/DTO/ResultDescriptor.php b/src/DTO/ResultDescriptor.php new file mode 100644 index 0000000..e04afa4 --- /dev/null +++ b/src/DTO/ResultDescriptor.php @@ -0,0 +1,8 @@ + */ public array $required = []; - public self $additionalProperties; - /** @var array */ - public array $definitions; /** @var array */ - public array $properties = []; - /** @var array */ - public array $patternProperties; - /** @var array */ - public array $dependencies; - public self $propertyNames; - /** @var mixed */ - public $const; - /** @var array */ + public ?array $properties; public array $enum; - /** @var array|string */ - public $type; - public string $format; - public string $contentMediaType; - public string $contentEncoding; - public self $if; - public self $then; - public self $else; - /** @var array */ - public array $allOf; + /** @var string[] */ + public array $type = ['mixed']; /** @var array */ public array $anyOf; - /** @var array */ - public array $oneOf; - public self $not; - - public function __construct() + public string $format; + public self $items; + /** @var array */ + public array $examples; + + public function jsonSerialize(): object { - /** - * Чтобы при сериализации в массив не выводились эти поля, если не были явно установлены - * Так как проверка на вывод поле идет с помощью ReflectionProperty::isInitialized, который - * возвращает false для неинициализированных свойств, для которых явно задан тип, и для полей, к которым - * применили unset - */ - unset($this->default, $this->const, $this->type); + $result = (array)$this; + $result['type'] = array_values(array_unique($result['type'])); + $result['type'] = self::convertType($result['type']); + // если больше одно элемента, и там есть mixed, надо его удалить + if (count($result['type']) > 1) { + $key = array_search('mixed', $result['type']); + if ($key !== false) { + unset($result['type'][$key]); + $result['type'] = array_values($result['type']); + } + } + + if (count($result['type']) === 1) { + $result['type'] = $result['type'][0]; + } + + if ($result['type'] === 'void') { + $result['type'] = 'null'; + } + + if ($result['type'] === 'mixed') { + unset($result['type']); + } + + if (empty($result['required'])) { + unset($result['required']); + } + + return (object)$result; } - public function getSchema(): Schema + protected static function convertType(array $type): array { - return $this; + $result = []; + foreach ($type as $value) { + $result[] = match ($value) { + 'int' => 'integer', + 'bool' => 'boolean', + 'float' => 'number', + // object/array/string/mixed/null/unknown + default => $value, + }; + } + + return $result; } } diff --git a/src/DTO/Server.php b/src/DTO/Server.php index 2189b0f..0001a31 100644 --- a/src/DTO/Server.php +++ b/src/DTO/Server.php @@ -2,12 +2,7 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - -/** - * An object representing a Server. - */ -final class Server extends DataTransferObject +final class Server { /** * REQUIRED. A name to be used as the canonical name for the server. diff --git a/src/DTO/ServerVariable.php b/src/DTO/ServerVariable.php index 93f9afb..b0e1f10 100644 --- a/src/DTO/ServerVariable.php +++ b/src/DTO/ServerVariable.php @@ -2,12 +2,10 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * An object representing a Server Variable for server URL template substitution. */ -final class ServerVariable extends DataTransferObject +final class ServerVariable { /** * An enumeration of string values to be used if the substitution options are from a limited set. diff --git a/src/DTO/Tag.php b/src/DTO/Tag.php index a2c471a..925e0d0 100644 --- a/src/DTO/Tag.php +++ b/src/DTO/Tag.php @@ -2,14 +2,11 @@ namespace Tochka\OpenRpc\DTO; -use Tochka\OpenRpc\Contracts\TagReferenceInterface; -use Tochka\OpenRpc\Support\DataTransferObject; - /** * Adds metadata to a single tag that is used by the Method Object. It is not mandatory to have a Tag Object per * tag defined in the Method Object instances. */ -final class Tag extends DataTransferObject implements TagReferenceInterface +final class Tag { /** * REQUIRED. The name of the tag. @@ -25,19 +22,9 @@ final class Tag extends DataTransferObject implements TagReferenceInterface * A verbose explanation for the tag. GitHub Flavored Markdown syntax MAY be used for rich text representation. */ public ?string $description; - - /** - * Additional external documentation for this tag. - */ - public ?ExternalDocumentation $externalDocs; - + public function __construct(string $name) { $this->name = $name; } - - public function getTag(): Tag - { - return $this; - } } diff --git a/src/Descriptors/Method/MethodContext.php b/src/Descriptors/Method/MethodContext.php new file mode 100644 index 0000000..514d11e --- /dev/null +++ b/src/Descriptors/Method/MethodContext.php @@ -0,0 +1,107 @@ + */ + protected readonly array $paramsReflectors; + protected readonly TypeDescriptor $typeDescriptor; + + /** + * @throws \ReflectionException + */ + public function __construct( + Pipeline $pipeline, + Route $route, + TypeDescriptor $typeDescriptor, + ) { + $this->pipeline = $pipeline; + $this->route = $route; + $this->typeDescriptor = $typeDescriptor; + $this->result = new MethodDescriptor($this->route->name); + $this->reflectionMethod = new \ReflectionMethod($route->controllerClass, $route->controllerMethod); + // docblock + $comment = $this->reflectionMethod->getDocComment(); + $contextFactory = new ContextFactory(); + $context = $contextFactory->createFromReflector($this->reflectionMethod); + $this->docBlock = DocBlockFactory::createInstance()->create(is_string($comment) ? $comment : '/** */', $context); + + $this->paramsReflectors = collect($this->reflectionMethod->getParameters())->keyBy( + fn(\ReflectionParameter $param) => $param->getName() + )->toArray(); + } + + public function describeMethod(): void + { + $this->descriptor = new MethodDescriptor($this->route->name); + /** @var self $result */ + $result = $this->pipeline->send($this)->thenReturn(); + $this->result = $result->descriptor; + } + + public function describeResult(): void + { + $this->descriptor = new ResultDescriptor($this->route->name . '_return', new Schema()); + $result = $this->pipeline->send($this)->thenReturn(); + $this->result->result = $result->descriptor; + } + + public function describeParameters(): void + { + foreach ($this->route->getParams() as $param) { + if ($param->propType === PropType::DI) { + continue; + } + $this->descriptor = new ParameterDescriptor($param->name, new Schema()); + /** @var MethodContext $parameterResult */ + $parameterResult = $this->pipeline->send($this)->thenReturn(); + // if this RequestObject all property of this object is params + if ($param->propType === PropType::RequestObject) { + foreach ($parameterResult->descriptor->schema->properties as $name => $innerProp) { + $contentDescriptor = new ContentDescriptor($name, $innerProp); + $contentDescriptor->summary = $innerProp->summary ?? ''; + $this->result->params[] = $contentDescriptor; + } + break; + } else { + $this->result->params[] = $parameterResult->descriptor; + } + } + } + + public function describeType(?\ReflectionType $type, Schema $schema, ?Type $phpDocType = null): Schema + { + return $this->typeDescriptor->describe($type, $schema, $phpDocType); + } + + public function getParameterReflector(string $name): ?\ReflectionParameter + { + return $this->paramsReflectors[$name] ?? null; + } + + public function getResult(): MethodDescriptor + { + return $this->result; + } +} diff --git a/src/Descriptors/Method/Pipes/MethodPipe.php b/src/Descriptors/Method/Pipes/MethodPipe.php new file mode 100644 index 0000000..2424b09 --- /dev/null +++ b/src/Descriptors/Method/Pipes/MethodPipe.php @@ -0,0 +1,26 @@ +descriptor instanceof MethodDescriptor) { + $context->descriptor->summary = $context->docBlock->getSummary(); + $context->descriptor->description = $context->docBlock->getDescription(); + $context->descriptor->deprecated = $context->docBlock->hasTag('deprecated'); + $group = explode('_', $context->route->name)[0] ?? null; + if ($group) { + $context->descriptor->tags[] = new Tag($group); + } + } + + return $next($context); + } +} diff --git a/src/Descriptors/Method/Pipes/ParameterPipe.php b/src/Descriptors/Method/Pipes/ParameterPipe.php new file mode 100644 index 0000000..00333e3 --- /dev/null +++ b/src/Descriptors/Method/Pipes/ParameterPipe.php @@ -0,0 +1,42 @@ +descriptor instanceof ParameterDescriptor) { + $dockBlockType = null; + /** @var Property|null $tag */ + foreach ($context->docBlock->getTagsByName('param') as $tag) { + if ($tag->getVariableName() === $context->descriptor->name) { + $context->descriptor->schema->title = $tag->getDescription()->render(); + $dockBlockType = $tag->getType(); + break; + } + } + $reflector = $context->getParameterReflector($context->descriptor->name); + if ($reflector) { + if ($reflector->isDefaultValueAvailable()) { + $context->descriptor->schema->default = $reflector->getDefaultValue(); + } + $context->descriptor->required = !$reflector->isOptional(); + $context->descriptor->schema = $context->describeType( + $reflector->getType(), + $context->descriptor->schema, + $dockBlockType, + ); + } + } + + return $next($context); + } +} diff --git a/src/Descriptors/Method/Pipes/PipeInterface.php b/src/Descriptors/Method/Pipes/PipeInterface.php new file mode 100644 index 0000000..53c1c91 --- /dev/null +++ b/src/Descriptors/Method/Pipes/PipeInterface.php @@ -0,0 +1,10 @@ +descriptor instanceof ResultDescriptor) { + $docBlock = $context->docBlock; + /** @var Return_|null $tag */ + $tag = $docBlock->getTagsByName('return')[0] ?? null; + $description = $tag?->getDescription()->render(); + if ($description) { + $context->descriptor->description = $description; + } + $context->descriptor->schema->default = null; + //$context->descriptor->summary = 'dfdfdfdfdf'; + $context->descriptor->schema = $context->describeType( + $context->reflectionMethod->getReturnType(), + $context->descriptor->schema, + $tag?->getType(), + ); + } + + return $next($context); + } +} diff --git a/src/Descriptors/Type/Handlers/CollectionHandler.php b/src/Descriptors/Type/Handlers/CollectionHandler.php new file mode 100644 index 0000000..f8866bf --- /dev/null +++ b/src/Descriptors/Type/Handlers/CollectionHandler.php @@ -0,0 +1,35 @@ +className === '\Illuminate\Support\Collection'; + } + + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + if ($info->phpDocType instanceof Generic) { + $types = $info->phpDocType->getTypes(); + $valueType = $types[1]; + if ($valueType) { + $schema->title = ''; + $schema->type = ['array']; + $schema->items = $typeDescriptor->describeFromPHPDoc($valueType, new Schema()); + } + + return $schema; + } + + $schema->type = ['array']; + + return $schema; + } +} diff --git a/src/Descriptors/Type/Handlers/DateTimeHandler.php b/src/Descriptors/Type/Handlers/DateTimeHandler.php new file mode 100644 index 0000000..84fa3eb --- /dev/null +++ b/src/Descriptors/Type/Handlers/DateTimeHandler.php @@ -0,0 +1,23 @@ +className && is_subclass_of($info->className, \DateTime::class); + } + + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + $schema->type = ['string']; + $schema->format = 'datetime'; + + return $schema; + } +} diff --git a/src/Descriptors/Type/Handlers/EnumHandler.php b/src/Descriptors/Type/Handlers/EnumHandler.php new file mode 100644 index 0000000..361cc20 --- /dev/null +++ b/src/Descriptors/Type/Handlers/EnumHandler.php @@ -0,0 +1,35 @@ +className && enum_exists($info->className); + } + + /** + * @throws \ReflectionException + */ + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + /** @var \BackedEnum $className */ + $className = $info->className; + $schema->enum = $className::cases(); + + $reflector = new \ReflectionEnum($className); + $valueType = $reflector->getBackingType(); + if ($valueType === null) { + $schema->type = ['string', 'integer']; + } else { + $schema->type = [$valueType->getName()]; + } + + return $schema; + } +} diff --git a/src/Descriptors/Type/Handlers/HandlerInterface.php b/src/Descriptors/Type/Handlers/HandlerInterface.php new file mode 100644 index 0000000..853f9e8 --- /dev/null +++ b/src/Descriptors/Type/Handlers/HandlerInterface.php @@ -0,0 +1,24 @@ +className && is_subclass_of($info->className, 'Illuminate\Database\Eloquent\Model'); + } + + /** + * @throws \ReflectionException + */ + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + $reflector = new \ReflectionClass($info->className); + $docText = $reflector->getDocComment(); + if (!$docText) { + return $schema; + } + $contextFactory = new ContextFactory(); + $context = $contextFactory->createFromReflector($reflector); + $docBlock = DocBlockFactory::createInstance()->create($docText, $context); + $properties = $this->filterTags($docBlock->getTags()); + + $model = new $info->className(); + $hidden = $model->getHidden(); + foreach ($properties as $property) { + $name = $property->getVariableName(); + // skip hidden fields + if (\in_array($name, $hidden)) { + continue; + } + $result = $typeDescriptor->describe(null, new Schema(), $property->getType()); + + $schema->properties[$property->getVariableName()] = $result; + if ($property instanceof Property) { + $schema->required[] = $property->getVariableName(); + } + } + + return $schema; + } + + /** + * @param array $tags + * + * @return array + */ + protected function filterTags(array $tags): array + { + $result = []; + + foreach ($tags as $tag) { + if (!\in_array($tag->getName(), ['property', 'property-read'])) { + continue; + } + + $result[] = $tag; + } + + return $result; + } +} diff --git a/src/Descriptors/Type/Handlers/ObjectHandler.php b/src/Descriptors/Type/Handlers/ObjectHandler.php new file mode 100644 index 0000000..86c37d9 --- /dev/null +++ b/src/Descriptors/Type/Handlers/ObjectHandler.php @@ -0,0 +1,57 @@ +className && class_exists($info->className); + } + + /** + * @throws \ReflectionException + */ + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + $reflector = new \ReflectionClass($info->className); + + $props = $reflector->getProperties(\ReflectionProperty::IS_PUBLIC); + foreach ($props as $prop) { + if ($prop->isStatic()) { + continue; + } + + $phpDocType = null; + $tag = null; + $docComment = $prop->getDocComment(); + if ($docComment) { + $docBlock = DocBlockFactory::createInstance()->create($docComment); + /** @var Var_ $tag */ + $tag = $docBlock->getTagsByName('var')[0] ?? null; + $phpDocType = $tag?->getType(); + } + + $result = $typeDescriptor->describe($prop->getType(), new Schema(), $phpDocType); + + if ($tag) { + $result->title = $tag->getDescription(); + } + + if (!$prop->hasDefaultValue()) { + $schema->required[] = $prop->getName(); + } + + $schema->properties[$prop->name] = $result; + } + + return $schema; + } +} diff --git a/src/Descriptors/Type/Handlers/TimeZoneHandler.php b/src/Descriptors/Type/Handlers/TimeZoneHandler.php new file mode 100644 index 0000000..582c79a --- /dev/null +++ b/src/Descriptors/Type/Handlers/TimeZoneHandler.php @@ -0,0 +1,24 @@ +className && is_subclass_of($info->className, DateTimeZone::class); + } + + public function handle(SingularTypeInfo $info, Schema $schema, TypeDescriptor $typeDescriptor): Schema + { + $schema->type = ['string']; + $schema->format = 'IANA'; + + return $schema; + } +} diff --git a/src/Descriptors/Type/SingularTypeInfo.php b/src/Descriptors/Type/SingularTypeInfo.php new file mode 100644 index 0000000..c893fb6 --- /dev/null +++ b/src/Descriptors/Type/SingularTypeInfo.php @@ -0,0 +1,15 @@ + */ + protected array $handlers = []; + /** @var array */ + protected array $classStack = []; + + public function __construct(?HandlerInterface ...$handlers) + { + $this->handlers = $handlers; + } + + public function addHandler(HandlerInterface $handler): self + { + $this->handlers[] = $handler; + + return $this; + } + + public function describe(?\ReflectionType $type, Schema $schema, ?Type $phpDocType = null): Schema + { + if ($phpDocType) { + $this->describeFromPHPDoc($phpDocType, $schema); + + return $schema; + } + + if ($type instanceof \ReflectionIntersectionType) { + return $schema; + } + + if ($type instanceof \ReflectionNamedType) { + if ($type->getName() === 'mixed') { + return $schema; + } + + if ($type->getName() === 'array') { + return $this->describeFromPHPDoc($phpDocType, $schema); + } + + if ($type->isBuiltin()) { + $schema->type = [$type->getName()]; + if ($type->allowsNull()) { + $schema->type = [...$schema->type, 'null']; + } + + return $schema; + } + + if (class_exists($type->getName())) { + $schema = $this->handleClassByName(new SingularTypeInfo($type->getName(), $type, $phpDocType), $schema); + if ($type->allowsNull()) { + $anyOfSchema = new Schema(); + $nullSchema = new Schema(); + $nullSchema->type = ['null']; + $anyOfSchema->anyOf = [$nullSchema, $schema]; + + return $anyOfSchema; + } + + return $schema; + } + + return $schema; + } + + if ($type instanceof \ReflectionUnionType) { + if ($this->allTypesIsPrimitive($type)) { + foreach ($type->getTypes() as $t) { + $schema->type = [...$schema->type, $t->getName()]; + } + if ($type->allowsNull()) { + $schema->type = [...$schema->type, 'null']; + } + } else { + foreach ($type->getTypes() as $type) { + return $this->describe($type, $schema); + } + } + } + + return $schema; + } + + protected function allTypesIsPrimitive(\ReflectionUnionType $type): bool + { + foreach ($type->getTypes() as $t) { + if (!$t->isBuiltin()) { + return false; + } + } + + return true; + } + + protected function handleClassByName(SingularTypeInfo $info, Schema $schema): Schema + { + $schema->title = $info->className; + if (\in_array($info->className, $this->classStack, true)) { + $schema->type = ['object']; + + return $schema; + } + + $this->classStack[] = $info->className; + + foreach ($this->handlers as $handler) { + if ($handler->shouldHandle($info)) { + $schema->type = ['object']; + + $result = $handler->handle($info, $schema, $this); + array_pop($this->classStack); + + return $result; + } + } + + return $schema; + } + + public function describeFromPHPDoc(?Type $phpDocType, Schema $schema): Schema + { + if (!$phpDocType) { + return $schema; + } + + if (\in_array( + $phpDocType::class, + [ + Integer::class, + Float_::class, + Null_::class, + Void_::class, + String_::class, + Boolean::class, + ] + )) { + $schema->type = [(string) $phpDocType]; + + return $schema; + } + if ($phpDocType instanceof Compound) { + foreach ($phpDocType->getIterator() as $type) { + $schema->anyOf[] = $this->describeFromPHPDoc($type, new Schema()); + } + + return $schema; + } + + if ($phpDocType instanceof Nullable) { + $schema->type = [...$schema->type, 'null']; + + return $this->describeFromPHPDoc($phpDocType->getActualType(), $schema); + } + + if ($phpDocType instanceof Generic) { + $singularType = new SingularTypeInfo($phpDocType->getFqsen(), null, $phpDocType); + + return $this->handleClassByName($singularType, $schema); + } + + if ($phpDocType instanceof Object_) { + // кейс когда указан не класс, а просто object + if ($phpDocType->getFqsen() === null) { + $schema->type = ['object']; + + return $schema; + } + + return $this->handleClassByName(new SingularTypeInfo($phpDocType->getFqsen(), null, $phpDocType), $schema); + } + + + if ($phpDocType instanceof Array_) { + $schema->type = ['array']; + $schema->items = $this->describeFromPHPDoc($phpDocType->getValueType(), new Schema()); + + return $schema; + } + + return $schema; + } +} diff --git a/src/Facades/MethodDescription.php b/src/Facades/MethodDescription.php deleted file mode 100644 index eba5839..0000000 --- a/src/Facades/MethodDescription.php +++ /dev/null @@ -1,24 +0,0 @@ -handler = $handler; - $this->cache = $cache; - } - - /** - * @throws InvalidArgumentException - */ - public function handle(): array - { - if ($this->cache->has('schema')) { - return $this->cache->get('schema'); - } - - return $this->handler->handle(); - } -} diff --git a/src/Handlers/OpenRpcGenerator.php b/src/Handlers/OpenRpcGenerator.php index 5a40f9b..49b534e 100644 --- a/src/Handlers/OpenRpcGenerator.php +++ b/src/Handlers/OpenRpcGenerator.php @@ -2,124 +2,79 @@ namespace Tochka\OpenRpc\Handlers; -use Illuminate\Support\Facades\Config; +use Illuminate\Pipeline\Pipeline; +use Illuminate\Support\Facades\App; +use Psr\SimpleCache\InvalidArgumentException; +use Tochka\JsonRpc\Router\Route; +use Tochka\JsonRpc\Router\Router; use Tochka\OpenRpc\Contracts\OpenRpcHandlerInterface; -use Tochka\OpenRpc\DTO\Components; -use Tochka\OpenRpc\DTO\Contact; -use Tochka\OpenRpc\DTO\ExternalDocumentation; -use Tochka\OpenRpc\DTO\Info; -use Tochka\OpenRpc\DTO\License; -use Tochka\OpenRpc\DTO\Method; +use Tochka\OpenRpc\Descriptors\Method\MethodContext; +use Tochka\OpenRpc\Descriptors\Method\Pipes\PipeInterface; +use Tochka\OpenRpc\Descriptors\Type\Handlers\HandlerInterface; +use Tochka\OpenRpc\Descriptors\Type\TypeDescriptor; use Tochka\OpenRpc\DTO\OpenRpc; -use Tochka\OpenRpc\DTO\Server; -use Tochka\OpenRpc\Facades\MethodDescription; -use Tochka\OpenRpc\Support\ReferenceCleaner; -use Tochka\OpenRpc\Support\StrSupport; +use Tochka\OpenRpc\Support\OpenRpcConfig; class OpenRpcGenerator implements OpenRpcHandlerInterface { public const OPEN_RPC_VERSION = '1.2.6'; - private array $openRpcConfig; - private array $jsonRpcConfig; + private OpenRpcConfig $openRpcConfig; + private Router $router; + private Pipeline $methodPipeline; + private TypeDescriptor $typeDescriptor; - /** @var array */ - private array $servers = []; - - public function __construct(array $openRpcConfig, array $jsonRpcConfig) + public function __construct(OpenRpcConfig $openRpcConfig, Router $router) { $this->openRpcConfig = $openRpcConfig; - $this->jsonRpcConfig = $jsonRpcConfig; - } - - public function handle(): array - { - $openRpc = new OpenRpc(self::OPEN_RPC_VERSION, $this->getInfo(), []); + $this->router = $router; - $externalDocumentation = $this->getExternalDocumentation(); - if ($externalDocumentation) { - $openRpc->externalDocumentation = $externalDocumentation; + $this->methodPipeline = App::make(Pipeline::class); + foreach ($this->openRpcConfig->methodPipes as $class) { + if (!is_subclass_of($class, PipeInterface::class)) { + throw new \TypeError($class . ' must implement ' . PipeInterface::class); + } + $this->methodPipeline->pipe(new $class()); } - $openRpc->servers = $this->getServers(); - $openRpc->methods = $this->getMethods(); - $openRpc->components = $this->getComponents(); - - ReferenceCleaner::clean($openRpc); - - return $openRpc->toArray(); - } - - protected function getInfo(): Info - { - $info = new Info( - data_get($this->openRpcConfig, 'title', Config::get('app.name', 'JsonRpc API')), - data_get($this->openRpcConfig, 'version', '1.0.0') - ); - - $info->description = StrSupport::resolveRef(data_get($this->openRpcConfig, 'description')); - $info->termsOfService = data_get($this->openRpcConfig, 'termsOfService'); - - if (!empty($this->openRpcConfig['contact'])) { - $info->contact = new Contact(); - $info->contact->email = data_get($this->openRpcConfig, 'contact.email'); - $info->contact->name = data_get($this->openRpcConfig, 'contact.name'); - $info->contact->url = data_get($this->openRpcConfig, 'contact.url'); + $this->typeDescriptor = new TypeDescriptor(); + foreach ($this->openRpcConfig->typeDescriptors as $class) { + if (!is_subclass_of($class, HandlerInterface::class)) { + throw new \TypeError($class . ' must implement ' . HandlerInterface::class); + } + $this->typeDescriptor->addHandler(new $class()); } - - if (!empty($this->openRpcConfig['license']['name'])) { - $info->license = new License($this->openRpcConfig['license']['name']); - $info->license->url = data_get($this->openRpcConfig, 'license.url'); - } - - return $info; - } - - protected function getExternalDocumentation(): ?ExternalDocumentation - { - $instance = null; - $url = data_get($this->openRpcConfig, 'externalDocumentation.url'); - - if ($url) { - $instance = new ExternalDocumentation($url); - $instance->description = StrSupport::resolveRef( - data_get($this->openRpcConfig, 'externalDocumentation.description') - ); - } - - return $instance; } /** - * @return array + * @throws \ReflectionException + * @throws InvalidArgumentException */ - private function getMethods(): array - { - return MethodDescription::generate($this->jsonRpcConfig, $this->servers); - } - - private function getComponents(): Components + public function handle(): array { - return MethodDescription::getComponents(); + $openRpc = new OpenRpc(self::OPEN_RPC_VERSION, $this->openRpcConfig->info); + $openRpc->servers[] = $this->openRpcConfig->server; + $openRpc = $this->routesDescribe($openRpc, $this->router); + + return (array)$openRpc; } /** - * @return array + * @throws \ReflectionException + * @throws InvalidArgumentException */ - private function getServers(): array + protected function routesDescribe(OpenRpc $openRpc, Router $router): OpenRpc { - foreach ($this->jsonRpcConfig as $name => $server) { - $url = trim(Config::get('app.url'), '/') . '/' . trim($server['endpoint'] ?? '', '/'); - $openRpcServer = new Server($name, $url); - $openRpcServer->summary = data_get($server, 'summary'); - $openRpcServer->description = StrSupport::resolveRef(data_get($server, 'description')); + /** @var Route $route $route */ + foreach ($router->getAll() as $route) { + $context = new MethodContext($this->methodPipeline, $route, $this->typeDescriptor); + $context->describeMethod(); + $context->describeResult(); + $context->describeParameters(); - $this->servers[$name] = $openRpcServer; + $openRpc->methods[] = $context->getResult(); } - return array_values($this->servers); + return $openRpc; } } - - - diff --git a/src/MethodDescriptionGenerator.php b/src/MethodDescriptionGenerator.php deleted file mode 100644 index af2af47..0000000 --- a/src/MethodDescriptionGenerator.php +++ /dev/null @@ -1,384 +0,0 @@ - */ - private AliasedDictionary $contentDescriptorsDictionary; - /** @var AliasedDictionary */ - private AliasedDictionary $schemasDictionary; - /** @var AliasedDictionary */ - private AliasedDictionary $examplesDictionary; - /** @var AliasedDictionary */ - private AliasedDictionary $tagDictionary; - /** @var array */ - private array $pipes = []; - - /** - * @throws BindingResolutionException - */ - public function __construct(Container $container) - { - $this->schemasDictionary = new AliasedDictionary(); - $this->examplesDictionary = new AliasedDictionary(); - $this->tagDictionary = new AliasedDictionary(); - $this->contentDescriptorsDictionary = new AliasedDictionary(); - $this->pipeline = $container->make(Pipeline::class); - } - - public function addPipe(SchemaHandlerPipeInterface $pipe): void - { - $this->pipes[] = $pipe; - } - - public function generate(array $jsonRpcConfig, array $servers): array - { - $routes = JsonRpcRouteAggregator::getRoutes(); - $serverConfigs = []; - - foreach ($jsonRpcConfig as $serverName => $serverConfig) { - $serverConfigs[$serverName] = new ServerConfig($serverConfig); - } - - /** @var array $methods */ - $methods = []; - foreach ($routes as $route) { - if (!isset($serverConfigs[$route->serverName])) { - throw new \RuntimeException( - sprintf( - 'Error while map route with server configuration. Route [%s], server name [%s]', - $route->getRouteName(), - $route->serverName - ) - ); - } - $methodKey = $route->controllerClass . '@' . $route->controllerMethod; - $controllerSuffix = $serverConfigs[$route->serverName]->controllerSuffix; - - if (array_key_exists($methodKey, $methods)) { - $methods[$methodKey]->servers[] = $servers[$route->serverName]; - continue; - } - $docBlock = JsonRpcDocBlockFactory::makeForMethod($route->controllerClass, $route->controllerMethod); - - $method = new Method($route->jsonRpcMethodName, [], new ContentDescriptor('result', new Schema())); - if ($docBlock !== null) { - $method->summary = $docBlock->getSummary(); - $method->description = StrSupport::resolveRef($docBlock->getDescription()); - $method->deprecated = $this->openRpcDocBlockIsDeprecated($docBlock); - } - - $method->servers[] = $servers[$route->serverName]; - $method->tags[] = $this->getTagForGroup( - $route->controllerClass, - $controllerSuffix, - $route->group, - $route->action - ); - - $method->params = $this->getParametersFromRoute($route); - $method->result = $this->getResultFromRoute($route); - - $methods[$methodKey] = $method; - } - - return array_values($methods); - } - - /** - * @param JsonRpcRoute $route - * @return array - */ - private function getParametersFromRoute(JsonRpcRoute $route): array - { - $contentDescriptors = []; - foreach ($route->parameters as $parameter) { - if ($parameter->castFromDI) { - continue; - } - - if ($parameter->castFullRequest) { - return $this->getContentDescriptorsForRequest($parameter); - } - - $contentDescriptors[] = $this->getContentDescriptor( - $parameter, - new MethodContext($route->controllerClass, $route->controllerMethod) - ); - } - - return $contentDescriptors; - } - - private function getResultFromRoute(JsonRpcRoute $route): ContentDescriptorReferenceInterface - { - return $this->getContentDescriptor( - $route->result, - new MethodContext($route->controllerClass, $route->controllerMethod), - true - ); - } - - /** - * @param Parameter $parameter - * @return array - */ - private function getContentDescriptorsForRequest(Parameter $parameter): array - { - $contentDescriptors = []; - - $parameterObject = JsonRpcParamsResolver::getParameterObject($parameter->className); - if ($parameterObject === null) { - return $contentDescriptors; - } - - foreach ($parameterObject->properties as $property) { - $contentDescriptorName = $parameter->className . '::' . $property->name; - $contentDescriptors[] = $this->contentDescriptorsDictionary->getReference( - $contentDescriptorName, - ContentDescriptorReference::class, - function () use ($property, $parameter) { - return $this->getContentDescriptor($property, new ClassContext($parameter->className)); - } - ); - } - - return $contentDescriptors; - } - - private function getContentDescriptor( - Parameter $parameter, - Context $context, - bool $isResult = false - ): ?ContentDescriptorReferenceInterface { - $contentDescriptor = new ContentDescriptor( - $parameter->name, - $this->getSchemaWithPipes($parameter, $context, $isResult) - ); - $contentDescriptor->summary = $parameter->description; - $contentDescriptor->description = $parameter->description; - $contentDescriptor->required = $parameter->required; - - if ($context instanceof ClassContext) { - $propertyDocBlock = JsonRpcDocBlockFactory::makeForProperty($context->getClassName(), $parameter->name); - if ($propertyDocBlock !== null) { - $contentDescriptor->deprecated = $this->openRpcDocBlockIsDeprecated($propertyDocBlock); - - $summary = $propertyDocBlock->getSummary(); - $description = StrSupport::resolveRef($propertyDocBlock->getDescription()); - if (!empty($summary)) { - $contentDescriptor->summary = $summary; - } - if (!empty($description)) { - $contentDescriptor->description = $description; - } - } - } - - return $contentDescriptor; - } - - public function getSchemaWithPipes( - Parameter $parameter, - Context $context, - bool $isResult = false - ): ?SchemaReferenceInterface { - $schema = new Schema(); - $schema->type = $parameter->type->toJsonType(); - $schema->title = $parameter->description; - $schema->description = $parameter->description; - - if (!$parameter->required) { - $schema->default = $parameter->defaultValue; - } - - if ($context instanceof ClassContext) { - $propertyDocBlock = JsonRpcDocBlockFactory::makeForProperty($context->getClassName(), $parameter->name); - - if ($propertyDocBlock !== null) { - $summary = $propertyDocBlock->getSummary(); - $description = StrSupport::resolveRef($propertyDocBlock->getDescription()); - if (!empty($summary)) { - $schema->title = $summary; - } - if (!empty($description)) { - $schema->description = $description; - } - } - } - - $expected = new ExpectedSchemaPipeObject($schema, $this->schemasDictionary, $parameter, $context, $isResult); - - /** @var ExpectedSchemaPipeObject $result */ - $result = $this->pipeline->send($expected) - ->through($this->pipes) - ->then( - function (ExpectedSchemaPipeObject $expected) { - $expected->schema = $this->getSchema( - $expected->schema, - $expected->parameter, - $expected->context - ); - return $expected; - } - ); - - return $result->schema; - } - - private function getSchema( - SchemaReferenceInterface $schemaReference, - Parameter $parameter, - Context $context - ): SchemaReferenceInterface { - $result = $schemaReference; - - if ($parameter->className !== null && $parameter->type->is(ParameterTypeEnum::TYPE_OBJECT())) { - /** @var SchemaReference $result */ - $result = $this->schemasDictionary->getReference( - StrSupport::fullyQualifiedClassName($parameter->className), - SchemaReference::class, - function () use ($schemaReference, $parameter) { - return $this->getSchemaForClass($schemaReference->getSchema(), $parameter); - } - ); - } - - $schema = $result->getSchema(); - - if ( - $parameter->parametersInArray !== null - && $parameter->type->is(ParameterTypeEnum::TYPE_ARRAY()) - ) { - $schema->items = $this->getSchemaWithPipes($parameter->parametersInArray, $context); - } - - return $result; - } - - private function getSchemaForClass(Schema $schema, Parameter $parameter): Schema - { - $classDocBlock = JsonRpcDocBlockFactory::makeForClass($parameter->className); - - if ($classDocBlock !== null) { - $schema->title = $classDocBlock->getSummary(); - $schema->description = StrSupport::resolveRef($classDocBlock->getDescription()); - } - - $parameterObject = JsonRpcParamsResolver::getParameterObject($parameter->className); - if ($parameterObject === null || $parameterObject->properties === null) { - return $schema; - } - - foreach ($parameterObject->properties as $property) { - if ($property->required) { - $schema->required[] = $property->name; - } - - $schema->properties[$property->name] = $this->getSchemaWithPipes( - $property, - new ClassContext($parameter->className) - ); - } - - return $schema; - } - - /** - * Возвращает ссылку на тег для группы методов - */ - private function getTagForGroup( - string $controller, - string $controllerSuffix, - ?string $group = null, - ?string $action = null - ): TagReferenceInterface { - $groupName = implode( - '-', - array_filter([ - $group, - $action, - $this->getShortNameForController($controller, $controllerSuffix) - ]) - ); - - return $this->tagDictionary->getReference( - $groupName, - TagReference::class, - function () use ($groupName, $controller) { - $tag = new Tag($groupName); - - $docBlock = JsonRpcDocBlockFactory::makeForClass($controller); - if ($docBlock !== null) { - $tag->summary = $docBlock->getSummary(); - $tag->description = StrSupport::resolveRef($docBlock->getDescription()); - } - - return $tag; - } - ); - } - - /** - * Возвращает список всех подготовленных компонент - * - * @return Components - */ - public function getComponents(): Components - { - $components = new Components(); - - $components->tags = $this->tagDictionary->getAliasedItems(); - $components->schemas = $this->schemasDictionary->getAliasedItems(); - $components->examples = $this->examplesDictionary->getAliasedItems(); - $components->contentDescriptors = $this->contentDescriptorsDictionary->getAliasedItems(); - - return $components; - } - - private function getShortNameForController(string $name, string $controllerSuffix): string - { - return Str::camel(Str::replaceLast($controllerSuffix, '', class_basename($name))); - } - - private function openRpcDocBlockIsDeprecated(JsonRpcDocBlock $docBlock): bool - { - return $docBlock->hasTag(DocBlock\Tags\Deprecated::class) || $docBlock->hasAnnotation(Deprecated::class); - } -} diff --git a/src/OpenRpc.php b/src/OpenRpc.php new file mode 100644 index 0000000..2d130a0 --- /dev/null +++ b/src/OpenRpc.php @@ -0,0 +1,69 @@ +config = $openRpcConfig; + $this->router = $router; + if ($this->config->cacheHandler) { + if (!is_subclass_of($this->config->cacheHandler, CacheInterface::class)) { + throw new \TypeError($this->config->cacheHandler . ' must implement ' . CacheInterface::class); + } + $this->cache = new $this->config->cacheHandler('openrpc_' . $this->config->serverName); + } + } + + + /** + * @throws \ReflectionException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function handle(): string + { + if ($this->cache) { + $cacheResult = $this->cache->get(); + if ($cacheResult) { + return $cacheResult; + } + } + $generator = new OpenRpcGenerator($this->config, $this->router); + + return json_encode($generator->handle()); + } + + + /** + * @throws \ReflectionException + * @throws \Psr\SimpleCache\InvalidArgumentException + */ + public function cacheMake(): void + { + if (!$this->cache) { + throw new \RuntimeException('Cache handler not set'); + } + $generator = new OpenRpcGenerator($this->config, $this->router); + + $this->cache->set($generator->handle()); + } + + public function cacheClear(): void + { + if (!$this->cache) { + throw new \RuntimeException('Cache handler not set'); + } + + $this->cache->clear(); + } +} diff --git a/src/OpenRpcServiceProvider.php b/src/OpenRpcServiceProvider.php index 30f927c..41a1a86 100644 --- a/src/OpenRpcServiceProvider.php +++ b/src/OpenRpcServiceProvider.php @@ -2,68 +2,12 @@ namespace Tochka\OpenRpc; -use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; -use Psr\SimpleCache\CacheInterface; -use Tochka\JsonRpc\Facades\JsonRpcRouter; -use Tochka\JsonRpc\Helpers\ArrayFileCache; -use Tochka\JsonRpc\Route\JsonRpcRoute; use Tochka\OpenRpc\Commands\Cache; use Tochka\OpenRpc\Commands\CacheClear; -use Tochka\OpenRpc\Facades\MethodDescription; -use Tochka\OpenRpc\Facades\OpenRpc; -use Tochka\OpenRpc\Handlers\OpenRpcCacheHandler; -use Tochka\OpenRpc\Handlers\OpenRpcGenerator; -use Tochka\OpenRpc\Pipes\ArrayShapePipe; -use Tochka\OpenRpc\Pipes\BackedEnumPipe; -use Tochka\OpenRpc\Pipes\ClassPropertyFromPhpDocPipe; -use Tochka\OpenRpc\Pipes\BenSampoEnumPipe; -use Tochka\OpenRpc\Pipes\DateTimePipe; -use Tochka\OpenRpc\Pipes\ExpectedValuesPipe; -use Tochka\OpenRpc\Pipes\ModelPipe; -use Tochka\OpenRpc\Pipes\ValueExamplePipe; class OpenRpcServiceProvider extends ServiceProvider { - public function register(): void - { - $this->app->singleton('OpenRpcCache', function () { - return new ArrayFileCache('openrpc'); - }); - - $this->app->singleton( - OpenRpc::class, - function () { - $openRpcConfig = Config::get('openrpc', []); - $jsonRpcConfig = Config::get('jsonrpc', []); - $handler = new OpenRpcGenerator($openRpcConfig, $jsonRpcConfig); - - /** @var CacheInterface $cache */ - $cache = $this->app->make('OpenRpcCache'); - - return new OpenRpcCacheHandler($handler, $cache); - } - ); - - $this->app->singleton( - MethodDescription::class, - function () { - $instance = new MethodDescriptionGenerator($this->app); - $instance->addPipe(new ExpectedValuesPipe()); - $instance->addPipe(new ValueExamplePipe()); - $instance->addPipe(new ArrayShapePipe()); - $instance->addPipe(new DateTimePipe()); - $instance->addPipe(new BackedEnumPipe()); - $instance->addPipe(new BenSampoEnumPipe()); - $instance->addPipe(new ModelPipe()); - $instance->addPipe(new ClassPropertyFromPhpDocPipe()); - - return $instance; - } - ); - } - /** * Perform post-registration booting of services. * @@ -77,20 +21,5 @@ public function boot(): void // Publish configuration $this->publishes([__DIR__ . '/../config/openrpc.php' => config_path('openrpc.php')], 'openrpc-config'); - - // Add Http Route - $routePath = Config::get('openrpc.endpoint', '/api/openrpc.json'); - Route::get( - $routePath, - static function () { - return OpenRpc::handle(); - } - ); - - // Add JsonRpc route for autodiscovery - $route = new JsonRpcRoute('default', 'rpc.discover'); - $route->controllerClass = AutoDiscoverController::class; - $route->controllerMethod = 'discover'; - JsonRpcRouter::add($route); } } diff --git a/src/Pipes/ArrayShapePipe.php b/src/Pipes/ArrayShapePipe.php deleted file mode 100644 index 83dc338..0000000 --- a/src/Pipes/ArrayShapePipe.php +++ /dev/null @@ -1,114 +0,0 @@ -context instanceof MethodContext && $result->isResult) { - $annotation = $this->getAnnotationFromDocBlock( - JsonRpcDocBlockFactory::makeForMethod( - $result->context->getClassName(), - $result->context->getMethodName() - ) - ); - - if ($annotation !== null) { - $result->schema = $this->getSchema($result, $annotation->shape, new VirtualContext()); - - return $result; - } - } - - // если ArrayShape находится в описании свойства класса - if ($result->context instanceof ClassContext && !empty($result->parameter->name)) { - $annotation = $this->getAnnotationFromDocBlock( - JsonRpcDocBlockFactory::makeForProperty($result->context->getClassName(), $result->parameter->name) - ); - - if ($annotation !== null) { - $result->schema = $this->getSchema($result, $annotation->shape, new VirtualContext()); - - return $result; - } - } - - // если ArrayShape находится в описании класса - if ($result->parameter->className !== null) { - $annotation = $this->getAnnotationFromDocBlock( - JsonRpcDocBlockFactory::makeForClass($result->parameter->className) - ); - - if ($annotation !== null) { - $result->schema = $result->schemasDictionary->getReference( - StrSupport::fullyQualifiedClassName($result->parameter->className . 'ArrayShape'), - SchemaReference::class, - fn() => $this->getSchema($result, $annotation->shape, new ClassContext($result->parameter->className)) - ); - } - } - - return $result; - } - - private function getAnnotationFromDocBlock(?JsonRpcDocBlock $docBlock): ?ApiArrayShape - { - if ($docBlock !== null) { - return $docBlock->firstAnnotation(ApiArrayShape::class); - } - - return null; - } - - private function getSchema(ExpectedSchemaPipeObject $expected, array $shape, Context $context): Schema - { - $schema = new Schema(); - - $schema->type = ParameterTypeEnum::TYPE_OBJECT()->toJsonType(); - $schema->properties = $this->getPropertiesFromShape($expected, $shape, $context); - $schema->required = array_keys($shape); - - return $schema; - } - - private function getPropertiesFromShape(ExpectedSchemaPipeObject $expected, array $shape, Context $context): array - { - $properties = []; - - foreach ($shape as $field => $type) { - $typeEnum = ParameterTypeEnum::fromVarType($type); - $parameter = new Parameter($field, $typeEnum); - - if (class_exists($type) && $typeEnum->is(ParameterTypeEnum::TYPE_MIXED())) { - $parameter->className = $type; - $parameter->type = ParameterTypeEnum::TYPE_OBJECT(); - $parameter->required = true; - } - - $properties[$field] = MethodDescription::getSchemaWithPipes($parameter, $context, $expected->isResult); - } - - return $properties; - } -} diff --git a/src/Pipes/BackedEnumPipe.php b/src/Pipes/BackedEnumPipe.php deleted file mode 100644 index b76dc00..0000000 --- a/src/Pipes/BackedEnumPipe.php +++ /dev/null @@ -1,78 +0,0 @@ -parameter->className !== null - && $result->parameter->type->is(ParameterTypeEnum::TYPE_OBJECT) - && is_subclass_of($result->parameter->className, '\BackedEnum') - ) { - $this->setSchemaEnumProperty($result->schema->getSchema(), $result->parameter->className); - } - - return $result; - } - - /** - * @param Schema $schema - * @param class-string<\BackedEnum> $className - */ - private function setSchemaEnumProperty(Schema $schema, string $className): void - { - $schema->enum = array_map(fn($enum) => $enum->value, $className::cases()); - - $classDocBlock = JsonRpcDocBlockFactory::makeForClass($className); - if ($classDocBlock !== null) { - $schema->title = $classDocBlock->getSummary(); - $schema->description = StrSupport::resolveRef($classDocBlock->getDescription()); - - $reflector = $classDocBlock->getReflector(); - if ($reflector instanceof \ReflectionClass) { - $schema->oneOf = $this->getConstSchemasForValues($reflector); - } - } - - if (is_int($className::cases()[0]->value)) { - $schema->type = ParameterTypeEnum::TYPE_INTEGER()->toJsonType(); - } elseif (is_string($className::cases()[0]->value)) { - $schema->type = ParameterTypeEnum::TYPE_STRING()->toJsonType(); - } else { - $schema->type = ParameterTypeEnum::TYPE_MIXED()->toJsonType(); - } - } - - private function getConstSchemasForValues(\ReflectionClass $reflector): array - { - $schemas = []; - - $constants = $reflector->getReflectionConstants(); - foreach ($constants as $constant) { - $docBlock = JsonRpcDocBlockFactory::make($constant); - - $schema = new Schema(); - $schema->const = $constant->getValue(); - - if ($docBlock !== null) { - $schema->description = $docBlock->getSummary(); - } - - $schemas[] = $schema; - } - - return $schemas; - } -} diff --git a/src/Pipes/BenSampoEnumPipe.php b/src/Pipes/BenSampoEnumPipe.php deleted file mode 100644 index 801ef9b..0000000 --- a/src/Pipes/BenSampoEnumPipe.php +++ /dev/null @@ -1,102 +0,0 @@ -parameter->className !== null - && $result->parameter->type->is(ParameterTypeEnum::TYPE_OBJECT) - && is_subclass_of($result->parameter->className, Enum::class) - ) { - $this->setSchemaEnumProperty($result->schema->getSchema(), $result->parameter->className); - } - - return $result; - } - - /** - * @param Schema $schema - * @param class-string $className - */ - private function setSchemaEnumProperty(Schema $schema, string $className): void - { - /** @var Enum $className */ - $schema->enum = $className::getValues(); - - $classDocBlock = JsonRpcDocBlockFactory::makeForClass($className); - if ($classDocBlock !== null) { - $schema->title = $classDocBlock->getSummary(); - $schema->description = StrSupport::resolveRef($classDocBlock->getDescription()); - - $reflector = $classDocBlock->getReflector(); - if ($reflector instanceof \ReflectionClass) { - $schema->oneOf = $this->getConstSchemasForValues($reflector); - } - } - - /** @var ParameterTypeEnum $type */ - $type = array_reduce( - $schema->enum, - function (?ParameterTypeEnum $carry, $item) { - switch (gettype($item)) { - case 'string': - return ($carry === null || $carry->is(ParameterTypeEnum::TYPE_STRING())) - ? ParameterTypeEnum::TYPE_STRING() - : ParameterTypeEnum::TYPE_MIXED(); - case 'integer': - return ($carry === null || $carry->is(ParameterTypeEnum::TYPE_INTEGER())) - ? ParameterTypeEnum::TYPE_INTEGER() - : ParameterTypeEnum::TYPE_MIXED(); - case 'double': - return ($carry === null || $carry->is(ParameterTypeEnum::TYPE_FLOAT())) - ? ParameterTypeEnum::TYPE_FLOAT() - : ParameterTypeEnum::TYPE_MIXED(); - case 'boolean': - return ($carry === null || $carry->is(ParameterTypeEnum::TYPE_BOOLEAN())) - ? ParameterTypeEnum::TYPE_BOOLEAN() - : ParameterTypeEnum::TYPE_MIXED(); - default: - return ParameterTypeEnum::TYPE_MIXED(); - } - }, - null - ); - - $schema->type = $type->toJsonType(); - } - - private function getConstSchemasForValues(\ReflectionClass $reflector): array - { - $schemas = []; - - $constants = $reflector->getReflectionConstants(); - foreach ($constants as $constant) { - $docBlock = JsonRpcDocBlockFactory::make($constant); - - $schema = new Schema(); - $schema->const = $constant->getValue(); - - if ($docBlock !== null) { - $schema->description = $docBlock->getSummary(); - } - - $schemas[] = $schema; - } - - return $schemas; - } -} diff --git a/src/Pipes/ClassPropertyFromPhpDocPipe.php b/src/Pipes/ClassPropertyFromPhpDocPipe.php deleted file mode 100644 index 8dc04b4..0000000 --- a/src/Pipes/ClassPropertyFromPhpDocPipe.php +++ /dev/null @@ -1,80 +0,0 @@ -parameter->className === null) { - return $result; - } - - $docBlock = JsonRpcDocBlockFactory::makeForClass($result->parameter->className); - if ($docBlock === null) { - return $result; - } - - $schemaReference = $result->schemasDictionary->getReference( - StrSupport::fullyQualifiedClassName($expected->parameter->className), - SchemaReference::class, - function () { - $schema = new Schema(); - $schema->type = ParameterTypeEnum::TYPE_OBJECT()->toJsonType(); - - return $schema; - } - ); - - $schema = $schemaReference->getSchema(); - - /** @var Property[]|PropertyWrite[]|PropertyRead[] $tags */ - $tags = $docBlock->getTags(null, - fn($tag) => $tag instanceof Property - || $tag instanceof PropertyRead - || $tag instanceof PropertyWrite - ); - - foreach ($tags as $tag) { - if (!in_array($tag->getVariableName(), $schema->required, true)) { - $schema->required[] = $tag->getVariableName(); - } - $type = ParameterTypeEnum::fromVarType($tag->getType()); - - $parameter = new Parameter($tag->getVariableName(), $type); - - if ($type->is(ParameterTypeEnum::TYPE_MIXED) && class_exists($tag->getType())) { - $parameter->type = ParameterTypeEnum::TYPE_OBJECT(); - $parameter->className = $tag->getType(); - } - - if ($tag->getDescription() !== null) { - $parameter->description = $tag->getDescription()->getBodyTemplate(); - } - - $schema->properties[$tag->getVariableName()] = MethodDescription::getSchemaWithPipes( - $parameter, - $result->context, - $result->isResult - ); - } - - return $result; - } -} diff --git a/src/Pipes/DateTimePipe.php b/src/Pipes/DateTimePipe.php deleted file mode 100644 index 2078bb3..0000000 --- a/src/Pipes/DateTimePipe.php +++ /dev/null @@ -1,31 +0,0 @@ -parameter->className, \DateTime::class)) { - return $result; - } - - $schema = $result->schema->getSchema(); - - $schema->type = ParameterTypeEnum::TYPE_STRING()->toJsonType(); - $schema->required = []; - $schema->properties = []; - $schema->format = 'datetime'; - $schema->title = 'DateTime'; - $schema->description = null; - - return $result; - } -} diff --git a/src/Pipes/ExpectedValuesPipe.php b/src/Pipes/ExpectedValuesPipe.php deleted file mode 100644 index fab6528..0000000 --- a/src/Pipes/ExpectedValuesPipe.php +++ /dev/null @@ -1,38 +0,0 @@ -parameter->name) - || !$result->context instanceof ClassContext - ) { - return $result; - } - - $docBlock = JsonRpcDocBlockFactory::makeForProperty($result->context->getClassName(), $result->parameter->name); - if ($docBlock === null) { - return $result; - } - - $annotation = $docBlock->firstAnnotation(ApiExpectedValues::class); - - if (!empty($annotation)) { - $result->schema->getSchema()->enum = $annotation->values; - } - - return $result; - } -} diff --git a/src/Pipes/ModelPipe.php b/src/Pipes/ModelPipe.php deleted file mode 100644 index 1e65858..0000000 --- a/src/Pipes/ModelPipe.php +++ /dev/null @@ -1,127 +0,0 @@ -parameter->className, '\Illuminate\Database\Eloquent\Model')) { - return $result; - } - - $docBlock = JsonRpcDocBlockFactory::makeForClass($result->parameter->className); - if ($docBlock === null) { - return $result; - } - - $schema = $result->schema->getSchema(); - $schema->type = ParameterTypeEnum::TYPE_OBJECT()->toJsonType(); - $schema->required = []; - $schema->properties = []; - - /** @var Property[]|PropertyWrite[]|PropertyRead[] $tags */ - $tags = $docBlock->getTags(null, - fn($tag) => $tag instanceof Property - || $tag instanceof PropertyRead - || $tag instanceof PropertyWrite - ); - - $reflector = $docBlock->getReflector(); - if (!$reflector instanceof \ReflectionClass) { - return $result; - } - - $defaultProperties = $reflector->getDefaultProperties(); - - foreach ($tags as $tag) { - if ($this->isHiddenAttribute($defaultProperties, $tag->getVariableName())) { - continue; - } - - if (!in_array($tag->getVariableName(), $schema->required, true)) { - $schema->required[] = $tag->getVariableName(); - } - $type = ParameterTypeEnum::fromVarType($tag->getType()); - - $parameter = new Parameter($tag->getVariableName(), $type); - - if ($type->is(ParameterTypeEnum::TYPE_MIXED) && class_exists($tag->getType())) { - $parameter->type = ParameterTypeEnum::TYPE_OBJECT(); - $parameter->className = $tag->getType(); - } - - if ($tag->getDescription() !== null) { - $parameter->description = $tag->getDescription()->getBodyTemplate(); - } - - $dateFormat = $this->dateAttribute($defaultProperties, $tag->getVariableName()); - if ($dateFormat !== null) { - $schemaProperty = new Schema(); - $schemaProperty->type = ParameterTypeEnum::TYPE_STRING()->toJsonType(); - $schemaProperty->format = $dateFormat; - if ($tag->getDescription() !== null) { - $schemaProperty->title = $tag->getDescription()->getBodyTemplate(); - $schemaProperty->description = $tag->getDescription()->getBodyTemplate(); - } - } else { - $schemaProperty = MethodDescription::getSchemaWithPipes( - $parameter, - $result->context, - $result->isResult - ); - } - - $schema->properties[$tag->getVariableName()] = $schemaProperty; - } - - return $result; - } - - private function isHiddenAttribute(array $defaultProperties, string $attributeName): bool - { - if (!empty($defaultProperties['visible']) && !\in_array($attributeName, $defaultProperties['visible'], true)) { - return true; - } - if (in_array($attributeName, $defaultProperties['hidden'] ?? [], true)) { - return true; - } - - return false; - } - - private function dateAttribute(array $defaultProperties, string $attributeName): ?string - { - $defaultFormat = 'Y-m-d H:i:s'; - $dates = $defaultProperties['dates'] ?? []; - $casts = $defaultProperties['casts'] ?? []; - - if (\in_array($attributeName, $dates, true)) { - return $defaultFormat; - } - - if (array_key_exists($attributeName, $casts)) { - $cast = $casts[$attributeName]; - $type = explode(':', $cast); - if ($type[0] === 'date' || $type[0] === 'datetime' || $type[0] === 'time' || $type[0] === 'timestamp') { - return count($type) > 1 ? $type[1] : $defaultFormat; - } - } - - return null; - } -} diff --git a/src/Pipes/ValueExamplePipe.php b/src/Pipes/ValueExamplePipe.php deleted file mode 100644 index ce4f74c..0000000 --- a/src/Pipes/ValueExamplePipe.php +++ /dev/null @@ -1,47 +0,0 @@ -context instanceof MethodContext && $result->isResult) { - $docBlock = JsonRpcDocBlockFactory::makeForMethod( - $result->context->getClassName(), - $result->context->getMethodName() - ); - } - - if ($result->context instanceof ClassContext && !empty($result->parameter->name)) { - $docBlock = JsonRpcDocBlockFactory::makeForProperty( - $result->context->getClassName(), - $result->parameter->name - ); - } - - if ($docBlock === null) { - return $result; - } - - $annotation = $docBlock->firstAnnotation(ApiValueExample::class); - - if (!empty($annotation)) { - $result->schema->getSchema()->examples = $annotation->examples; - } - - return $result; - } -} diff --git a/src/Support/AliasedDictionary.php b/src/Support/AliasedDictionary.php deleted file mode 100644 index 83741c6..0000000 --- a/src/Support/AliasedDictionary.php +++ /dev/null @@ -1,106 +0,0 @@ - */ - private array $items = []; - - /** @var array */ - private array $aliases = []; - - /** - * @param T $instance - * @param string $name - */ - public function addItem(object $instance, string $name): void - { - $this->items[$name] = $instance; - } - - public function hasItem(string $name): bool - { - return array_key_exists($name, $this->items); - } - - /** - * @param string $name - * @return T|null - */ - public function getItem(string $name): ?object - { - return $this->items[$name] ?? null; - } - - /** - * @return array - */ - public function getItems(): array - { - return $this->items; - } - - /** - * @template Y - * @param string $name - * @param callable $makeCall - * @param class-string $referenceClass - * @return Y - */ - public function getReference(string $name, string $referenceClass, callable $makeCall): object - { - if (!$this->hasItem($name)) { - $item = $makeCall(); - $this->addItem($item, $name); - } - - return new $referenceClass($name, $this); - } - - public function getAliasedItems(): array - { - if (empty($this->aliases)) { - $this->aliases = $this->generateAliases(); - } - - $aliasedSchemas = []; - - foreach ($this->items as $name => $item) { - $aliasedSchemas[$this->getAlias($name)] = $item; - } - - return $aliasedSchemas; - } - - public function getAlias(string $name): string - { - if (empty($this->aliases)) { - $this->aliases = $this->generateAliases(); - } - - return $this->aliases[$name] ?? $name; - } - - private function generateAliases(): array - { - $aliases = []; - foreach ($this->items as $name => $item) { - $shortClassName = class_basename($name); - if (\in_array($shortClassName, $aliases, true)) { - $i = 1; - while (in_array($shortClassName . '_' . $i, $aliases, true)) { - $i++; - } - $shortClassName .= '_' . $i; - } - - $aliases[$name] = $shortClassName; - } - - return $aliases; - } -} diff --git a/src/Support/ClassContext.php b/src/Support/ClassContext.php deleted file mode 100644 index c0916b6..0000000 --- a/src/Support/ClassContext.php +++ /dev/null @@ -1,20 +0,0 @@ -className = $className; - } - - public function getClassName(): string - { - return $this->className; - } -} diff --git a/src/Support/Context.php b/src/Support/Context.php deleted file mode 100644 index be84968..0000000 --- a/src/Support/Context.php +++ /dev/null @@ -1,18 +0,0 @@ -type = $type; - } - - public function getType(): ContextTypeEnum - { - return $this->type; - } -} diff --git a/src/Support/ContextTypeEnum.php b/src/Support/ContextTypeEnum.php deleted file mode 100644 index fba6b79..0000000 --- a/src/Support/ContextTypeEnum.php +++ /dev/null @@ -1,17 +0,0 @@ -getProperties(\ReflectionProperty::IS_PUBLIC); - - foreach ($properties as $reflectionProperty) { - // Skip static properties and not initialized properties - if ( - $reflectionProperty->isStatic() - || !$reflectionProperty->isInitialized($this) - || ( - $reflectionProperty->getValue($this) === null - && !\in_array($reflectionProperty->getName(), $this->nullableKeys, true) - ) - || ( - empty($reflectionProperty->getValue($this)) - && \in_array($reflectionProperty->getName(), $this->onlyNotEmptyKeys, true) - ) - ) { - continue; - } - - $data[$reflectionProperty->getName()] = $reflectionProperty->getValue($this); - } - - return $data; - } - - /** - * @param string ...$keys - * - * @return static - */ - public function only(string ...$keys): self - { - $dataTransferObject = clone $this; - - $dataTransferObject->onlyKeys = [...$this->onlyKeys, ...$keys]; - - return $dataTransferObject; - } - - /** - * @param string ...$keys - * - * @return static - */ - public function except(string ...$keys): self - { - $dataTransferObject = clone $this; - - $dataTransferObject->exceptKeys = [...$this->exceptKeys, ...$keys]; - - return $dataTransferObject; - } - - public function toArray(): array - { - if (count($this->onlyKeys)) { - $array = Arr::only($this->all(), $this->onlyKeys); - } else { - $array = Arr::except($this->all(), $this->exceptKeys); - } - - return $this->parseArray($array); - } - - protected function parseArray(array $array): array - { - foreach ($array as $key => $value) { - if ($value instanceof self) { - $array[$key] = $value->toArray(); - - continue; - } - - if (!is_array($value)) { - continue; - } - - $array[$key] = $this->parseArray($value); - } - - return $array; - } -} diff --git a/src/Support/ExpectedSchemaPipeObject.php b/src/Support/ExpectedSchemaPipeObject.php deleted file mode 100644 index a1819e9..0000000 --- a/src/Support/ExpectedSchemaPipeObject.php +++ /dev/null @@ -1,30 +0,0 @@ - */ - public AliasedDictionary $schemasDictionary; - public Parameter $parameter; - public Context $context; - public bool $isResult = false; - - public function __construct( - SchemaReferenceInterface $schema, - AliasedDictionary $schemas, - Parameter $parameter, - Context $context, - bool $isResult = false - ) { - $this->schema = $schema; - $this->schemasDictionary = $schemas; - $this->parameter = $parameter; - $this->context = $context; - $this->isResult = $isResult; - } -} diff --git a/src/Support/MethodContext.php b/src/Support/MethodContext.php deleted file mode 100644 index 61a79fa..0000000 --- a/src/Support/MethodContext.php +++ /dev/null @@ -1,26 +0,0 @@ -className = $className; - $this->methodName = $methodName; - } - - public function getClassName(): string - { - return $this->className; - } - - public function getMethodName(): string - { - return $this->methodName; - } -} diff --git a/src/Support/OpenRpcConfig.php b/src/Support/OpenRpcConfig.php new file mode 100644 index 0000000..b788014 --- /dev/null +++ b/src/Support/OpenRpcConfig.php @@ -0,0 +1,86 @@ +description = data_get($data, 'description'); + $server->summary = data_get($data, 'summary'); + + return $server; + } + + protected static function makeInfo(?array $data): ?Info + { + if ($data === null) { + return null; + } + $info = new Info( + data_get($data, 'title', ''), + data_get($data, 'version', '1.0.0'), + ); + $info->description = data_get($data, 'description', ''); + + // contact + $contactData = data_get($data, 'contact'); + if ($contactData) { + $contact = new Contact(); + $contact->name = data_get($contactData, 'name'); + $contact->email = data_get($contactData, 'email'); + $contact->url = data_get($contactData, 'url'); + $info->contact = $contact; + } + + // licence + $licenseData = data_get($data, 'license'); + if ($licenseData) { + $license = new License(data_get($licenseData, 'name')); + $license->url = data_get($licenseData, 'url', ''); + $info->license = $license; + } + + $info->termsOfService = data_get($data, 'termsOfService', null); + + return $info; + } +} diff --git a/src/Support/ReferenceCleaner.php b/src/Support/ReferenceCleaner.php deleted file mode 100644 index d6458e5..0000000 --- a/src/Support/ReferenceCleaner.php +++ /dev/null @@ -1,50 +0,0 @@ -toArray(); - - $schema->components->contentDescriptors = self::check($schema->components->contentDescriptors, '#/components/contentDescriptors/', $arraySchema); - $schema->components->tags = self::check($schema->components->tags, '#/components/tags/', $arraySchema); - $schema->components->schemas = self::check($schema->components->schemas, '#/components/schemas/', $arraySchema); - $schema->components->examples = self::check($schema->components->examples, '#/components/examples/', $arraySchema); - } - - private static function check(array $references, string $path, array $schema): array - { - $touched = []; - $touchedInternal = []; - - foreach ($schema as $key => $item) { - if ($key === '$ref') { - $name = self::parseReference($item, $path); - if ($name !== null) { - $touched[$name] = $references[$name]; - } - - continue; - } - - if (is_array($item)) { - $touchedInternal[] = self::check($references, $path, $item); - } - } - - return array_merge($touched, ...$touchedInternal); - } - - private static function parseReference(string $reference, string $path): ?string - { - if (str_starts_with($reference, $path)) { - return str_replace($path, '', $reference); - } - - return null; - } -} diff --git a/src/Support/StrSupport.php b/src/Support/StrSupport.php deleted file mode 100644 index 4f6aef7..0000000 --- a/src/Support/StrSupport.php +++ /dev/null @@ -1,29 +0,0 @@ - Date: Mon, 23 Mar 2026 15:06:07 +0500 Subject: [PATCH 2/2] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=BB?= =?UTF-8?q?=D0=B8=D1=88=D0=BD=D0=B8=D0=B9=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Descriptors/Method/Pipes/MethodPipe.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Descriptors/Method/Pipes/MethodPipe.php b/src/Descriptors/Method/Pipes/MethodPipe.php index 2424b09..f5cfc6e 100644 --- a/src/Descriptors/Method/Pipes/MethodPipe.php +++ b/src/Descriptors/Method/Pipes/MethodPipe.php @@ -2,7 +2,6 @@ namespace Tochka\OpenRpc\Descriptors\Method\Pipes; -use phpDocumentor\Reflection\DocBlock\Tags\Deprecated; use Tochka\OpenRpc\Descriptors\Method\MethodContext; use Tochka\OpenRpc\DTO\MethodDescriptor; use Tochka\OpenRpc\DTO\Tag;