├── .docheader ├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── .php_cs ├── .travis.yml ├── LICENSE ├── README.md ├── autoload.php ├── bin ├── fpp ├── fpp.bat └── fpp.php ├── composer.json ├── demo ├── command.fpp ├── event.fpp └── example.fpp ├── docs ├── Configuration.md ├── Data Types.md ├── Home.md ├── Markers.md ├── Messages.md ├── PhpStorm-Integration.md └── img │ ├── phpstorm_1.png │ ├── phpstorm_2.png │ └── phpstorm_3.png ├── kahlan-config.php ├── spec ├── BasicParserSpec.php ├── FppParser │ ├── Bool_Spec.php │ ├── CommandSpec.php │ ├── ConstructorSeparatorSpec.php │ ├── DataSpec.php │ ├── EnumSpec.php │ ├── EventSpec.php │ ├── Float_Spec.php │ ├── GuidSpec.php │ ├── ImportSpec.php │ ├── Int_Spec.php │ ├── MarkerSpec.php │ ├── MultipleNamespacesSpec.php │ ├── SingleNamespaceSpec.php │ ├── String_Spec.php │ ├── TypeNameSpec.php │ └── UuidSpec.php └── FppSpec.php └── src ├── Argument.php ├── Configuration.php ├── Definition.php ├── Functions ├── basic_parser.php ├── fpp.php ├── fpp_parser.php └── scan.php ├── Namespace_.php ├── Parser.php ├── Type.php ├── Type ├── Bool_.php ├── Command.php ├── Data.php ├── DateTimeImmutable.php ├── Enum.php ├── Event.php ├── Float_.php ├── Guid.php ├── Int_.php ├── Marker.php ├── String_.php └── Uuid.php ├── TypeConfiguration.php └── TypeTrait.php /.docheader: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is part of prolic/fpp. 3 | * (c) 2018-%year% Sascha-Oliver Prolic 4 | * 5 | * For the full copyright and license information, please view the LICENSE 6 | * file that was distributed with this source code. 7 | */ 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: prolic 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | open-pull-requests-limit: 10 9 | reviewers: 10 | - prolic 11 | assignees: 12 | - prolic 13 | labels: 14 | - enhancement 15 | ignore: 16 | - dependency-name: kahlan/kahlan 17 | versions: 18 | - 5.0.6 19 | - 5.0.7 20 | - 5.0.8 21 | - 5.0.9 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.buildpath 4 | /vendor 5 | /build 6 | /demo/generated.php 7 | .idea 8 | .php_cs.cache 9 | nbproject 10 | composer.lock 11 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | getFinder()->in(__DIR__); 5 | 6 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; 7 | 8 | $config->setCacheFile($cacheDir . '/.php_cs.cache'); 9 | 10 | return $config; 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | fast_finish: true 5 | include: 6 | - php: 7.4 7 | env: 8 | - DEPENDENCIES="" 9 | - EXECUTE_CS_CHECK=true 10 | - TEST_COVERAGE=true 11 | - php: 7.4 12 | env: 13 | - DEPENDENCIES="--prefer-lowest --prefer-stable" 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | - $HOME/.php-cs-fixer 19 | - $HOME/.local 20 | 21 | before_script: 22 | - mkdir -p "$HOME/.php-cs-fixer" 23 | - phpenv config-rm xdebug.ini 24 | - composer self-update 25 | - composer update --prefer-dist $DEPENDENCIES 26 | 27 | script: 28 | - if [[ $TEST_COVERAGE == 'true' ]]; then mkdir -p build/logs; fi 29 | - if [[ $TEST_COVERAGE == 'true' ]]; then php -dzend_extension=xdebug.so -dxdebug.max_nesting_level=1000 -dmemory_limit=512M ./vendor/bin/kahlan --coverage-text --clover=./build/logs/clover.xml; else ./vendor/bin/kahlan; fi 30 | - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/php-cs-fixer fix -v --diff --dry-run; fi 31 | 32 | after_success: 33 | - if [[ $TEST_COVERAGE == 'true' ]]; then php vendor/bin/php-coveralls -v; fi 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020, Sascha-Oliver Prolic 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the prooph software GmbH nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FPP 2 | 3 | ## Functional PHP Preprocessor - Immutable data type generator 4 | 5 | This library can generate immutable data types based on fpp definitions. The syntax is inspired by Haskell. 6 | 7 | [![Build Status](https://travis-ci.org/prolic/fpp.svg?branch=master)](https://travis-ci.org/prolic/fpp) 8 | [![Coverage Status](https://coveralls.io/repos/github/prolic/fpp/badge.svg?branch=master)](https://coveralls.io/github/prolic/fpp?branch=master) 9 | 10 | ## Sponsor 11 | 12 | If you want to support my work, become a patron at [patreon.com/prolic](https://www.patreon.com/notifications). 13 | 14 | ## Credits 15 | 16 | [Marcello Duarte](https://github.com/MarcelloDuarte/) created the [ParserCombinators](https://github.com/MarcelloDuarte/ParserCombinators/) project in 2017. 17 | The rewrite of this library is heavily inspired by it and reuses some of its base functions. 18 | 19 | ## Docs 20 | 21 | [See the docs here](https://github.com/prolic/fpp/tree/master/docs/Home.md) 22 | 23 | ## Install 24 | 25 | ```console 26 | composer require prolic/fpp dev-master 27 | ``` 28 | 29 | ## Usage 30 | 31 | Disable xdebug or increase xdebug.max_nesting_level in your php.ini file. 32 | 33 | ```php 34 | php vendor/bin/fpp.php 35 | ``` 36 | 37 | ## Generate configuration 38 | 39 | ```php 40 | php vendor/bin/fpp.php --gen-config 41 | ``` 42 | 43 | You can then modify the config file to adjust to your needs. 44 | 45 | ## Changes from 0.1.0 Release 46 | 47 | This library has been rewritten from scratch. If you want to use the old version, pin your composer requirement to 0.1.0. 48 | 49 | The master branch is not compatible at all. 50 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | $autoloadFiles = [ 14 | __DIR__ . '/src/Argument.php', 15 | __DIR__ . '/src/Configuration.php', 16 | __DIR__ . '/src/Definition.php', 17 | __DIR__ . '/src/Namespace_.php', 18 | __DIR__ . '/src/Parser.php', 19 | __DIR__ . '/src/Type.php', 20 | __DIR__ . '/src/TypeConfiguration.php', 21 | __DIR__ . '/src/TypeTrait.php', 22 | __DIR__ . '/src/Functions/basic_parser.php', 23 | __DIR__ . '/src/Functions/fpp_parser.php', 24 | __DIR__ . '/src/Functions/fpp.php', 25 | __DIR__ . '/src/Functions/scan.php', 26 | __DIR__ . '/src/Type/Bool_.php', 27 | __DIR__ . '/src/Type/Command.php', 28 | __DIR__ . '/src/Type/Data.php', 29 | __DIR__ . '/src/Type/DateTimeImmutable.php', 30 | __DIR__ . '/src/Type/Enum.php', 31 | __DIR__ . '/src/Type/Event.php', 32 | __DIR__ . '/src/Type/Float_.php', 33 | __DIR__ . '/src/Type/Int_.php', 34 | __DIR__ . '/src/Type/Marker.php', 35 | __DIR__ . '/src/Type/String_.php', 36 | __DIR__ . '/src/Type/Uuid.php', 37 | __DIR__ . '/src/Type/Guid.php', 38 | ]; 39 | 40 | foreach ($autoloadFiles as $f) { 41 | require_once $f; 42 | } 43 | -------------------------------------------------------------------------------- /bin/fpp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use Nette\PhpGenerator\PsrPrinter; 16 | 17 | $path1 = \realpath(__DIR__ . '/../../../../'); 18 | $path2 = \realpath(__DIR__ . '/../'); 19 | $vendorName = 'vendor'; 20 | 21 | foreach ([$path1, $path2] as $path) { 22 | if (\file_exists($composerPath = "$path/composer.json")) { 23 | $composerJson = \json_decode(\file_get_contents($composerPath), true); 24 | $vendorName = isset($composerJson['config']['vendor-dir']) ? $composerJson['config']['vendor-dir'] : $vendorName; 25 | } 26 | 27 | break; 28 | } 29 | 30 | if (! \file_exists("$path/$vendorName/autoload.php")) { 31 | echo "\033[1;31mYou need to set up the project dependencies using the following commands: \033[0m" . PHP_EOL; 32 | echo 'curl -s http://getcomposer.org/installer | php' . PHP_EOL; 33 | echo 'php composer.phar install' . PHP_EOL; 34 | exit(1); 35 | } 36 | 37 | $autoloader = require "$path/$vendorName/autoload.php"; 38 | 39 | if (\file_exists("$path/$vendorName/prolic/fpp/autoload.php")) { 40 | $fppAutoloader = "$path/$vendorName/prolic/fpp/autoload.php"; 41 | } else { 42 | $fppAutoloader = "$path/autoload.php"; 43 | } 44 | 45 | require_once $fppAutoloader; 46 | 47 | $config = [ 48 | 'use_strict_types' => true, 49 | 'source' => '.', 50 | 'target' => '*', 51 | 'success_msg' => 'Successfully generated and written to disk', 52 | 'printer' => fn () => (new PsrPrinter())->setTypeResolving(false), 53 | 'file_parser' => parseFile, 54 | 'comment' => 'Generated by prolic/fpp - do not edit !!!', 55 | 'types' => [ 56 | Type\Command\Command::class => Type\Command\typeConfiguration(), 57 | Type\Data\Data::class => Type\Data\typeConfiguration(), 58 | Type\Enum\Enum::class => Type\Enum\typeConfiguration(), 59 | Type\Event\Event::class => Type\Event\typeConfiguration(), 60 | Type\String_\String_::class => Type\String_\typeConfiguration(), 61 | Type\Int_\Int_::class => Type\Int_\typeConfiguration(), 62 | Type\Float_\Float_::class => Type\Float_\typeConfiguration(), 63 | Type\Bool_\Bool_::class => Type\Bool_\typeConfiguration(), 64 | Type\Marker\Marker::class => Type\Marker\typeConfiguration(), 65 | Type\Uuid\Uuid::class => Type\Uuid\typeConfiguration(), 66 | Type\Guid\Guid::class => Type\Guid\typeConfiguration(), 67 | \DateTimeImmutable::class => Type\DateTimeImmutable\typeConfiguration(), 68 | ], 69 | ]; 70 | 71 | if ($path === '--gen-config') { 72 | $file = << true, 83 | 'source' => '.', 84 | 'target' => '*', // * = use composer settings, otherwise give path here 85 | 'success_msg' => 'Successfully generated and written to disk', 86 | 'printer' => fn () => (new PsrPrinter())->setTypeResolving(false), 87 | 'file_parser' => parseFile, 88 | 'comment' => 'Generated by prolic/fpp - do not edit !!!', // put `null` to disable 89 | 'types' => [ 90 | Type\Command\Command::class => Type\Command\\typeConfiguration(), 91 | Type\Data\Data::class => Type\Data\\typeConfiguration(), 92 | Type\Enum\Enum::class => Type\Enum\\typeConfiguration(), 93 | Type\Event\Event::class => Type\Event\\typeConfiguration(), 94 | Type\String_\String_::class => Type\String_\\typeConfiguration(), 95 | Type\Int_\Int_::class => Type\Int_\\typeConfiguration(), 96 | Type\Float_\Float_::class => Type\Float_\\typeConfiguration(), 97 | Type\Bool_\Bool_::class => Type\Bool_\\typeConfiguration(), 98 | Type\Marker\Marker::class => Type\Marker\\typeConfiguration(), 99 | Type\Uuid\Uuid::class => Type\Uuid\\typeConfiguration(), 100 | Type\Guid\Guid::class => Type\Guid\\typeConfiguration(), 101 | \DateTimeImmutable::class => Type\DateTimeImmutable\\typeConfiguration(), 102 | ], 103 | ]; 104 | 105 | CODE; 106 | 107 | \file_put_contents("$path/fpp-config.php", $file); 108 | 109 | echo "Default configuration written to $path/fpp-config.php\n"; 110 | exit(0); 111 | } 112 | 113 | if (\file_exists("$path/fpp-config.php")) { 114 | $config = require "$path/fpp-config.php"; 115 | } 116 | 117 | $config = Configuration::fromArray($config); 118 | 119 | runFpp($config, $autoloader); 120 | 121 | echo $config->successMessage() . "\n"; 122 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prolic/fpp", 3 | "description": "Functional PHP Preprocessor", 4 | "license": "BSD-3-Clause", 5 | "authors": [ 6 | { 7 | "name": "Sascha-Oliver Prolic", 8 | "email": "saschaprolic@googlemail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.4", 13 | "phunkie/phunkie": "^0.11.1", 14 | "nette/php-generator": "^3.4.0", 15 | "nette/utils": "^3.1.2" 16 | }, 17 | "require-dev": { 18 | "kahlan/kahlan": "^5.1.3", 19 | "prooph/php-cs-fixer-config": "^0.5.0", 20 | "satooshi/php-coveralls": "^2.2", 21 | "vimeo/psalm": "^4.0.1" 22 | }, 23 | "bin": [ 24 | "bin/fpp" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /demo/command.fpp: -------------------------------------------------------------------------------- 1 | namespace Foo\Bar\User { 2 | 3 | use Foo\Bar\Command; 4 | use Foo\Bar\CommandId; 5 | use Foo\Bar\Name; 6 | use Foo\Bar\Age; 7 | 8 | command UserCommand : Command (CommandId) = RegisteredUser as registered-user { Name $name, ?Age $age} 9 | | UpdateUserName as update-user-name { Name $name } 10 | | DeleteUser; 11 | } 12 | -------------------------------------------------------------------------------- /demo/event.fpp: -------------------------------------------------------------------------------- 1 | namespace Foo\Bar\User { 2 | 3 | use Foo\Bar\Event; 4 | use Foo\Bar\EventId; 5 | use Foo\Bar\UserId; 6 | use Foo\Bar\Name; 7 | use Foo\Bar\Age; 8 | 9 | event UserEvent : Event (EventId, UserId) = UserRegistered as user-registered { Name $name, ?Age $age} 10 | | UserNameUpdated as user-name-updated { Name $name } 11 | | UserDeleted; 12 | } 13 | -------------------------------------------------------------------------------- /demo/example.fpp: -------------------------------------------------------------------------------- 1 | namespace Foo\Bar { 2 | use DateTimeImmutable; 3 | 4 | marker Event; 5 | marker Command; 6 | 7 | data Person = 8 | NonPerson { 9 | DateTimeImmutable $registered, 10 | ?UserId $userId, 11 | Name[] $names, 12 | ?int $age = 18, 13 | string[] $emails = [], 14 | HumanId[] $humanIds, 15 | Address[] $addresses 16 | } 17 | | GoodPerson { UserId $userId, ?int $age = 18 }; 18 | 19 | data Address = { string $street, int $no }; 20 | 21 | string Name; 22 | float Money; 23 | enum Type = Master | Slave; 24 | bool Deleted; 25 | int Age; 26 | 27 | guid UserId; 28 | guid CommandId; 29 | guid EventId; 30 | } 31 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | # Functional PHP Preprocessor - Immutable data type generator 2 | 3 | ## Configuration 4 | 5 | So far we used FPP without any custom configuration, that's because 6 | FPP ships with a default build-in configuration. If you need to configure 7 | FPP, first you need to generate the default config file, so you can than 8 | adjust it to your needs: 9 | 10 | ```bash 11 | ./vendor/bin/fpp --gen-config 12 | ``` 13 | 14 | This will create a file `fpp-config.php` in your project's root folder. 15 | 16 | The first few options are pretty self-explanatory, but we'll go over it anyway: 17 | 18 | - `use_strict_types` whether or not to use strict types in the generated PHP code 19 | - `printer` a callback that creates a Printer for FPP to use. Usually the PSR 20 | Printer is exactly what you want, so you most likely won't change this ever. 21 | In case you are working with Nette Framework (we use their PhpGenerator component), 22 | you might want to use the `Nette\PhpGenerator\Printer` class instead. As FPP 23 | doesn't have any type hints on the printer used, you could also use your very 24 | own printer implementation here. 25 | - `file_parser` defines the function that will parse files for you. Unless you know 26 | the internals of FPP and want to mess around a little, you would never touch that 27 | at all. 28 | - `comment` defines the comment that is added on top of every generated file. 29 | You can put any string here or set to `null` to disable comments. 30 | - `types` is the list of available types in FPP as well as the `DateTimeImmutable` 31 | class. That's because FPP can use already `DateTimeImmutable`, as long as you 32 | import that class. 33 | 34 | ## Custom type configuration 35 | 36 | Maybe the easiest way to learn how to configure FPP is by copy & paste the example 37 | of `DateTimeImmutable`, add to the configuration and adjust. However, let's go 38 | through the various steps quickly. 39 | 40 | In the `types` section of the configuration, you need to provide a class name in 41 | the key of the config and a `TypeConfiguration` as value. The `TypeConfiguration` 42 | class has the following constructor: 43 | 44 | ``` 45 | public function __construct( 46 | ?callable $parse, 47 | ?callable $build, 48 | ?callable $fromPhpValue, 49 | ?callable $toPhpValue, 50 | ?callable $validator, 51 | ?callable $validationErrorMessage, 52 | ?callable $equals 53 | ) 54 | ``` 55 | 56 | As you can see, it expects a bunch of callable but all of them are optional. 57 | 58 | - `parse` defines the parse function to use in order to parse the given type. 59 | When importing already existing classes, you would provide `null` here. 60 | In case you want to add your very own FPP type, you need to provide the parser 61 | function here. 62 | - `build` defines the function that builds the PHP code that is then printed by 63 | the printer defined. Again, even when we are using the `Nette\PhpGenerator`, 64 | there are no type hints at all. So if you replace all the default builders 65 | shipped with FPP and provide a special printer, you can use FPP to even generate 66 | code in any other language, for example JavaScript. 67 | - `fromPhpValue` defines the function that will be used to transform a scalar or 68 | array PHP value to an object. If there is no function provided, the object will 69 | be required as is in the generated `fromArray` method. 70 | - `toPhpValue` defines the function that will be used to transform your object to 71 | a scalar or array value in PHP. If there is no function provided, the object 72 | will be returned as is in the generated `toArray` method. 73 | - `validator` defines the function that will be used to validate a given PHP scalar 74 | or array value. This will be used in the generated `fromArray` method. If left to 75 | `null` the value will be be validated at all. 76 | - `validationErrorMessage` is used to display the error message in the `fromArray` 77 | method, when the given `validator` fails. 78 | - `equals` defines how to compare of two of those objects are equal. 79 | 80 | Let's have an example here real quick: 81 | 82 | ``` 83 | Role::class => new TypeConfiguration( 84 | null, 85 | null, 86 | fn (string $type, string $paramName) => "$type::fromName($paramName)", 87 | fn (string $type, string $paramName) => $paramName . '->getName()', 88 | fn (string $type, string $paramName) => "\is_string($paramName)", 89 | fn (string $paramName) => "Error on \"$paramName\", string expected", 90 | fn (string $paramName, string $otherParamName) => "{$paramName}->equals($otherParamName)" 91 | ), 92 | ``` 93 | 94 | So far for the configuration, let's head to [Messages](Messages.md) next. 95 | -------------------------------------------------------------------------------- /docs/Data Types.md: -------------------------------------------------------------------------------- 1 | # Functional PHP Preprocessor - Immutable data type generator 2 | 3 | ## Data Types 4 | 5 | ### Basics 6 | 7 | Data types are the thing where FPP really shines. You've learned how to generate 8 | simple wrappers around PHP scalar types like string, int, bool, float as well as 9 | uuid, guid and enum. We can use those base types to generate more complex types. 10 | 11 | Let's say we need a class with the following constructor: 12 | 13 | ``` 14 | class Address 15 | { 16 | // properties here 17 | 18 | public function __construct( 19 | string $street, 20 | string $number, 21 | string $zipCode, 22 | string $country 23 | ) { 24 | // more code here 25 | } 26 | 27 | // a bunch of setters here 28 | 29 | // toArray-method 30 | 31 | // fromArray-constructor 32 | 33 | // equals method 34 | } 35 | ``` 36 | 37 | As you can see, this would be a lot of code to type, so let's see how the same thing 38 | can be generated by FPP without typing all this boilerplate. 39 | 40 | ``` 41 | namespace Foo; 42 | 43 | data Address = { string $street, string $number, string $zipCode, string $country }; 44 | ``` 45 | 46 | Of course, you can also add marker interfaces again: 47 | 48 | ``` 49 | namespace Foo; 50 | 51 | data Address : MyMarker = { string $street, string $number, string $zipCode, string $country }; 52 | ``` 53 | 54 | 55 | So that's a lot less code to type! We can also use our base types, so let's do this 56 | by example again and use an enum for our country parameter as well: 57 | 58 | ``` 59 | namespace Foo; 60 | 61 | string Street; 62 | string Number; 63 | string ZipCode; 64 | enum CountryName = US | PY | CA | UK; 65 | 66 | data Address = { Street $street, Number $number, ZipCode $zipCode, Country $country }; 67 | ``` 68 | 69 | As you might notice, using base types, the parameter name and type are usually the same, 70 | so we can also avoid it: 71 | 72 | ``` 73 | namespace Foo; 74 | 75 | data Address = { Street, Number, ZipCode, Country }; 76 | ``` 77 | 78 | But what happens if you need to use the same type twice in a constructor? 79 | For example we can think of a user having a user id and a parent user id. 80 | Let's have a look: 81 | 82 | ``` 83 | namespace Foo; 84 | 85 | uuid UserId; 86 | string FirstName; 87 | string LastName; 88 | int Age; 89 | 90 | data User = { UserId, UserId, FirstName, LastName, Age }; 91 | ``` 92 | 93 | This would generate the following parameter names: 94 | 95 | `$userId, $userId2, $firstName, $lastName, $age` 96 | 97 | That's already pretty good, but most likely you want to name the second parameter 98 | according to its purpose `$parentUserId`. That's no big deal at all! 99 | 100 | ``` 101 | namespace Foo; 102 | 103 | data User = { UserId, UserId $parentUserId, FirstName, LastName, Age }; 104 | ``` 105 | 106 | ### Nullable types, array-bracket syntax and default values 107 | 108 | Array-bracket syntax is very importing if you are dealing with lists of things, 109 | and nullable types when a property is optional. Of course, you can also add 110 | default values: 111 | 112 | ``` 113 | namespace Foo; 114 | 115 | data User = { string $name = 'Guest', ?int $parentUserId, string[] $hobbies }; 116 | ``` 117 | 118 | ### Importing classes from another namespace 119 | 120 | You can use the same syntax (also using aliases) as you would use in plain PHP. 121 | So the following would work and generate you usable PHP code: 122 | 123 | ``` 124 | namespace Foo { 125 | 126 | string FullName; 127 | string Hobbie; 128 | } 129 | 130 | namespace Foo\Bar { 131 | use Foo\FullName as Name; 132 | use Foo\Hobbie; 133 | 134 | data Person = { Name, Hobbie[] }; 135 | } 136 | ``` 137 | 138 | ### Subclasses 139 | 140 | Let's assume you need to have an `Animal` abstract class and its implementations 141 | `Cat`, `Dog` and `Bird`. Let's have a look: 142 | 143 | ``` 144 | namespace Foo; 145 | 146 | data Animal = 147 | Cat { string $name, int $age } 148 | | Dog { string $name, int $age, string $ownersName } 149 | | Bird { string $name }; 150 | ``` 151 | 152 | ### DateTimeImmutable 153 | 154 | You can reuse DateTimeImmutable class by default in FPP, just import it first: 155 | 156 | ``` 157 | namespace Foo; 158 | 159 | use DateTimeImmutable; 160 | 161 | uuid UserId; 162 | string Name; 163 | data User = { UserId, DateTimeImmutable $registeredAt, Name }; 164 | ``` 165 | 166 | Next, let's have look on how to reuse existing classes in FPP. In order to do that 167 | you need to configure FPP a little and make it aware on how those classes, external 168 | to FPP work. See [Configuration](Configuration.md). 169 | -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | # Functional PHP Preprocessor - Immutable data type generator 2 | 3 | This library can generate immutable data types in PHP. The syntax is very precise. 4 | You can type about 1/50 of the amount of code required in FPP compared to PHP. 5 | 6 | ## Basics 7 | 8 | Every fpp-file needs to have only one namespace, declared exactly like in PHP: 9 | 10 | ``` 11 | namespace Foo; 12 | ``` 13 | 14 | But you can also have multiple namespaces in a file, using curly bracket syntax: 15 | 16 | ``` 17 | namespace Foo { 18 | } 19 | 20 | namespace Bar { 21 | } 22 | ``` 23 | 24 | The namespaces you use, need to be available in your composer.json file, so FPP knows 25 | where to store the generated files. PSR-4 and PSR-0 are supported. 26 | 27 | Let's start with something simple: All we need are wrappers around scalar types in PHP. 28 | 29 | Why? Consider this code in PHP: 30 | 31 | ``` 32 | class Person { 33 | // maybe some properties here 34 | 35 | public function __construct( 36 | string $firstName, 37 | string $lastName, 38 | int $age, 39 | int $amountOfChildren, 40 | float $longitude, 41 | float $latitude 42 | ) { 43 | // more code here 44 | } 45 | } 46 | ``` 47 | 48 | Although we use PHP's type system, it's easy to break things. You can put first name 49 | and last name in the wrong order, same with age and amount of children, and so on. 50 | 51 | What we want to have, are distinct types for each property. Just like this: 52 | 53 | ``` 54 | class Person { 55 | // maybe some properties here 56 | 57 | public function __construct( 58 | FirstName $firstName, 59 | LastName $lastName, 60 | Age $age, 61 | AmountOfChildren $amountOfChildren, 62 | Longitude $longitude, 63 | Latitude $latitude 64 | ) { 65 | // more code here 66 | } 67 | } 68 | ``` 69 | 70 | Just to have the `FirstName` created, FPP will generate this PHP code for you: 71 | 72 | ``` 73 | value = $value; 86 | } 87 | 88 | public function value(): string 89 | { 90 | return $this->value; 91 | } 92 | 93 | public function equals(?self $other): bool 94 | { 95 | return null !== $other && $this->value === $other->value; 96 | } 97 | } 98 | ``` 99 | 100 | You can imagine the amount of boilerplate you have to write to create all those wrappers. 101 | 102 | FPP can generate you this boilerplate very quick without much typing. 103 | 104 | ``` 105 | namespace Foo; 106 | 107 | bool IsDeleted; 108 | float Longitude; 109 | int Age; 110 | string FirstName 111 | ``` 112 | 113 | Run `./vendor/bin/fpp ` and this is generated! 114 | 115 | ## Enums, Uuid's and more 116 | 117 | PHP doesn't have an enum type, however FPP can give you a precise syntax 118 | that will generate PHP code for you, so you have something to work with as enums. 119 | 120 | ``` 121 | namespace Foo; 122 | 123 | enum Color = Red | Green | Blue | Yellow; 124 | ``` 125 | 126 | If you're working with ramsey/uuid a lot, you might want to have custom types for 127 | your uuid's as well. So you can name them `UserId` instead of just Uuid. 128 | 129 | FPP can do that for you: 130 | 131 | ``` 132 | namespace Foo; 133 | 134 | uuid UserId; 135 | ``` 136 | 137 | In case you want Guid's, no problem: 138 | 139 | ``` 140 | namespace Foo; 141 | 142 | guid UserId; 143 | ``` 144 | 145 | ## Adding interfaces 146 | 147 | In case you have an existing interface you want to add to your generated class, 148 | you can do so by using the following syntax: 149 | 150 | ``` 151 | namespace Foo; 152 | 153 | string FirstName : MyMarker; 154 | ``` 155 | 156 | This will let the generated class `Foo\FirstName` have the `MyMarker` interface 157 | implemented. Note that if the interface has methods defined in it, that would 158 | not be generated as well by FPP, you'll get an invalid class you cannot use. 159 | Therefor it's mostly useful for marker interfaces only. 160 | 161 | ## Marker interfaces 162 | 163 | A marker interface is an interface without any defined methods or constants. 164 | Usually it is used to `mark` a class with a given interface. A good example for that 165 | would be the `Traversable` interface in PHP. 166 | 167 | You can define marker interfaces with the marker keyword: 168 | 169 | ``` 170 | namespace Foo; 171 | 172 | marker MyMarker; 173 | ``` 174 | 175 | And you can extend markers from other interfaces: 176 | 177 | ``` 178 | namespace Foo; 179 | 180 | marker MyMarker; 181 | marker MyMarker2; 182 | marker OtherMarker : MyMarker, MyMarker2; 183 | ``` 184 | 185 | So far for the basics, let's head to [Data Types](Data Types.md) next. 186 | -------------------------------------------------------------------------------- /docs/Markers.md: -------------------------------------------------------------------------------- 1 | Markers are PHP interfaces with no method. 2 | 3 | ## Definition 4 | 5 | They are defined using the `marker` keyword: 6 | 7 | ``` 8 | namespace Foo; 9 | marker MyMarker; 10 | ``` 11 | 12 | It is possible to extend marker with the following syntax: 13 | 14 | ``` 15 | namespace Foo; 16 | marker MyMarkerA; 17 | marker MyMarkerB : MyMarkerA; 18 | ``` 19 | 20 | It is also possible to extend marker located in another namespace: 21 | 22 | ``` 23 | namespace Foo; 24 | marker MyMarkerA; 25 | namespace Bar; 26 | marker MyMarkerB : \Foo\MyMarkerA; 27 | ``` 28 | 29 | And to extend existing markers: 30 | 31 | ```php 32 | namespace App; 33 | 34 | class MyMarker 35 | { 36 | } 37 | ``` 38 | 39 | ``` 40 | namespace Foo; 41 | marker MyMarker : \App\MyMarker; 42 | ``` 43 | 44 | And even to extend multiple markers: 45 | 46 | ``` 47 | namespace Foo; 48 | marker MyMarkerA; 49 | marker MyMarkerB; 50 | marker MyMarkerC : MyMarkerA, MyMarkerB; 51 | ``` 52 | 53 | ## Usage 54 | 55 | Use them on your `data` definition: 56 | 57 | ``` 58 | namespace Foo; 59 | marker MyMarkerA; 60 | marker MyMarkerB; 61 | marker MyMarkerC : MyMarkerA, MyMarkerB; 62 | marker MyMarkerD; 63 | data MyData : MyMarkerC, MyMarkerD = MyData; 64 | ``` 65 | 66 | Wanna see more? check [Derivings](Configuration.md) 67 | 68 | Wanna have [prooph components](http://getprooph.org/) integration? See the [prooph integration](prooph.md) 69 | -------------------------------------------------------------------------------- /docs/Messages.md: -------------------------------------------------------------------------------- 1 | # Functional PHP Preprocessor - Immutable data type generator 2 | 3 | ## Messages 4 | 5 | The following message types are available: 6 | 7 | - `command` 8 | - `event` 9 | 10 | Those are basically data types with some additional attributes. 11 | 12 | ### Command 13 | 14 | Assuming we have all the basic types already defined: 15 | 16 | ``` 17 | namespace Foo; 18 | 19 | command UserCommand : Command (CommandId) = 20 | RegisteredUser as registered-user { Name $name, ?Age $age} 21 | | UpdateUserName as update-user-name { Name $name } 22 | | DeleteUser; 23 | ``` 24 | 25 | A command can be defined using the `command` keyword, following by the abstract 26 | class (here `UserCommand`) generated, and optionally also one or more marker 27 | interfaces (here `Command`). A command can be distinguished from other commands 28 | by it's ID (here `CommandId`), usually that's a UUID or GUID. 29 | 30 | After the equals sign we defined here the various constructors (sub classes) and 31 | it's parameters (if any). 32 | 33 | The used alias `as` that you see here, f.e. `as registered-user` defines a command 34 | type. Each generated command has a command type property and getter-method that 35 | returns a string representation of the command type. If none provided it will 36 | default to the fully qualified class name. 37 | 38 | ### Event 39 | 40 | Events here are meant to be used for event sourcing, so they not only have an EventId 41 | similar to the CommandId for commands, but they also have an aggregate ID to specify 42 | to which aggregate root they belong. Therefor they have one argument more, but 43 | other than that look very similar: 44 | 45 | ``` 46 | namespace Foo; 47 | 48 | event UserEvent : Event (EventId, UserId) = 49 | UserRegistered as user-registered { Name $name, ?Age $age} 50 | | UserNameUpdated as user-name-updated { Name $name } 51 | | UserDeleted; 52 | ``` 53 | 54 | Actually the EventId and AggregateId (here called `UserId`) don't need to have 55 | different types. You could reuse the same GUID implementation everywhere: 56 | 57 | ``` 58 | namespace Foo; 59 | 60 | guid Guid; 61 | 62 | command UserCommand : Command (Guid) = 63 | RegisteredUser as registered-user { Name $name, ?Age $age} 64 | | UpdateUserName as update-user-name { Name $name } 65 | | DeleteUser; 66 | 67 | event UserEvent : Event (Guid, Guid) = 68 | UserRegistered as user-registered { Name $name, ?Age $age} 69 | | UserNameUpdated as user-name-updated { Name $name } 70 | | UserDeleted; 71 | ``` 72 | 73 | That's it, last but not least, you can have full [PhpStorm Integration](PhpStorm-Integration.md). 74 | -------------------------------------------------------------------------------- /docs/PhpStorm-Integration.md: -------------------------------------------------------------------------------- 1 | # Functional PHP Preprocessor - Immutable data type generator 2 | 3 | ## PhpStorm Interation 4 | 5 | Do the following after installing FPP into your project. 6 | 7 | 1) First you'll need to install the [File Watchers plugin](https://www.jetbrains.com/help/phpstorm/settings-tools-file-watchers.html), if you haven't already done so. 8 | To install the plugin, go to the PhpStorm Settings/Preferences -> "Plugins" -> and search for "File Watchers". Check off the plugin to install it. You'll need to restart PhpStorm. 9 | ![Enable the Plugin](https://raw.githubusercontent.com/prolic/fpp/master/docs/img/phpstorm_1.png) 10 | 11 | 2) Now we need to create an FPP file type. 12 | In the PhpStorm Settings/Preferences -> "Editor" -> "File Types". 13 | Add a new file type, click at the "Recognized File Types" section on the "+" icon. 14 | Put the name to "FPP" and description to "FPP files" (or whatever you'd like). 15 | Add `//` as Line comment, `/*` as Block comment start and `*/` as Block comment end. 16 | Set the keywords to: 17 | 18 | ``` 19 | namespace 20 | use 21 | data 22 | bool 23 | string 24 | float 25 | int 26 | enum 27 | uuid 28 | guid 29 | event 30 | command 31 | { 32 | | 33 | } 34 | = 35 | : 36 | ``` 37 | 38 | If you want to have more colors, put the following to the Tab `2` of keywords: 39 | 40 | ``` 41 | { 42 | | 43 | } 44 | = 45 | : 46 | ``` 47 | 48 | Then click on OK. 49 | In the "Registered Patterns" section (with FPP selected above), click on the "+" icon and add `*.fpp`, then click OK. 50 | ![Add FPP File Type](https://raw.githubusercontent.com/prolic/fpp/master/docs/img/phpstorm_2.png) 51 | 52 | 3) Configure the File Watcher. 53 | In the PhpStorm Settings/Preferences -> "Tools" -> "File Watchers". 54 | Click on the "+" icon and choose "" for the template. 55 | Put "FPP" as name. 56 | Select FPP as the File type. 57 | Select your Scope, if you don't have one, create it. Set it to `src` for example. 58 | Set "Programm" to `php`. 59 | Set this as "Arguments": `$ProjectFileDir$/vendor/prolic/fpp/bin/fpp.php $FilePath$` 60 | Enter an output path, such as `$Sourcepath$` (PHPStorm will refresh this path to look for changes to the generated classes) 61 | Disable the checkbox "Auto-save edited files to trigger the watcher" – in my experience this is really awkward. 62 | Select "On error" for "Show Console". 63 | Click OK. 64 | ![Configure the File Watcher](https://raw.githubusercontent.com/prolic/fpp/master/docs/img/phpstorm_3.png) 65 | 66 | Try it out! Have fun! 67 | 68 | ---- 69 | 70 | # Advanced Options 71 | 72 | ## If you have `.fpp` files that reference classes defined in other `.fpp` files 73 | 74 | The default "Arguments" setting works great if you only have one `.fpp` file *or* every `.fpp` file only references classes defined in that same file. 75 | 76 | If you have multiple `.fpp` files and one or more of them reference a class defined in another `.fpp`, you will get an error. To combat this, you can specify a directory as a part of the "Arguments" instead of the exact file that changed. 77 | 78 | For example, consider the following files that define value objects, events and commands for Accounts, Forms, Subscriptions and Users. 79 | 80 | ``` 81 | config 82 | └── model 83 | ├── account.fpp 84 | ├── form.fpp 85 | ├── subscription.fpp 86 | └── user.fpp 87 | ``` 88 | 89 | In this case, the commands and events for Form (from `form.fpp`) and Subscription (from `subscription.fpp`) reference `AccountId` (from `account.fpp`). 90 | 91 | Changes to `form.fpp` would trigger `$ProjectFileDir$/vendor/bin/fpp $ProjectFileDir$/config/model/form.fpp`. This will cause an error as `form.fpp` references `AccountId` which is not itself defined in `form.fpp`. However, if we change the "Arguments" to the following, everything works: 92 | 93 | > $ProjectFileDir$/vendor/bin/fpp $ProjectFileDir$/config/model 94 | 95 | This is because when `form.fpp` changes, all `.fpp` files in the directory are rebuilt together by `fpp`. This means `form.fpp` and `subscription.fpp` can safely find `AccountId` that is defined in `account.fpp`. 96 | -------------------------------------------------------------------------------- /docs/img/phpstorm_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prolic/fpp/4ad74c54526a621005075bdca6a636de43511234/docs/img/phpstorm_1.png -------------------------------------------------------------------------------- /docs/img/phpstorm_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prolic/fpp/4ad74c54526a621005075bdca6a636de43511234/docs/img/phpstorm_2.png -------------------------------------------------------------------------------- /docs/img/phpstorm_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prolic/fpp/4ad74c54526a621005075bdca6a636de43511234/docs/img/phpstorm_3.png -------------------------------------------------------------------------------- /kahlan-config.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | require_once 'autoload.php'; 14 | -------------------------------------------------------------------------------- /spec/BasicParserSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec; 14 | 15 | use function Fpp\alphanum; 16 | use function Fpp\char; 17 | use function Fpp\comma; 18 | use function Fpp\constructorSeparator; 19 | use function Fpp\digit; 20 | use function Fpp\el; 21 | use function Fpp\int; 22 | use function Fpp\ints; 23 | use function Fpp\item; 24 | use function Fpp\letter; 25 | use function Fpp\lower; 26 | use function Fpp\many; 27 | use function Fpp\many1; 28 | use function Fpp\manyList; 29 | use function Fpp\manyList1; 30 | use function Fpp\nat; 31 | use function Fpp\nl; 32 | use function Fpp\result; 33 | use function Fpp\sat; 34 | use function Fpp\sepBy1list; 35 | use function Fpp\sepByList; 36 | use function Fpp\seq; 37 | use function Fpp\spaces; 38 | use function Fpp\spaces1; 39 | use function Fpp\string; 40 | use function Fpp\upper; 41 | use function Fpp\word; 42 | use function Fpp\zero; 43 | 44 | describe("Fpp\Parser", function () { 45 | context('Basic parsers', function () { 46 | describe('result', function () { 47 | it('succeeds without consuming any of the string', function () { 48 | expect(result('hello')->run('world')) 49 | ->toEqual([Pair('hello', 'world')]); 50 | }); 51 | }); 52 | 53 | describe('zero', function () { 54 | it('always return an empty list, which means failure', function () { 55 | expect(zero()->run('world'))->toEqual([]); 56 | }); 57 | }); 58 | 59 | describe('item', function () { 60 | it('parses a string and returns the first character', function () { 61 | expect(item()->run('hello'))->toEqual([Pair('h', 'ello')]); 62 | }); 63 | it('returns an empty list for an empty string', function () { 64 | expect(item()->run(''))->toEqual([]); 65 | }); 66 | it('returns the character for a single character string', function () { 67 | expect(item()->run('h'))->toEqual([Pair('h', '')]); 68 | }); 69 | }); 70 | 71 | describe('seq', function () { 72 | it('applies parsers in sequence', function () { 73 | expect(seq(item(), item())->run('hello'))->toEqual([Pair(Pair('h', 'e'), 'llo')]); 74 | }); 75 | }); 76 | 77 | describe('sat', function () { 78 | it('parses one character when predicate matches', function () { 79 | expect(sat('is_numeric')->run('4L'))->toEqual([Pair('4', 'L')]); 80 | }); 81 | it('returns an empty list when predicate does not match', function () { 82 | expect(sat('is_numeric')->run('L4'))->toEqual([]); 83 | }); 84 | }); 85 | }); 86 | 87 | context('Sequencing combinators', function () { 88 | describe('char', function () { 89 | it('parses a character', function () { 90 | expect(char('h')->run('hello'))->toEqual([Pair('h', 'ello')]); 91 | }); 92 | it('returns an empty list when character does not match', function () { 93 | expect(char('x')->run('hello'))->toEqual([]); 94 | }); 95 | }); 96 | 97 | describe('digit', function () { 98 | it('parses a digit', function () { 99 | expect(digit()->run('42'))->toEqual([Pair('4', '2')]); 100 | }); 101 | it('returns an empty list when digit does not match', function () { 102 | expect(digit()->run('a42'))->toEqual([]); 103 | }); 104 | }); 105 | 106 | describe('lower', function () { 107 | it('parses a lowercase character', function () { 108 | expect(lower()->run('hello'))->toEqual([Pair('h', 'ello')]); 109 | }); 110 | it('returns an empty list when lower does not match', function () { 111 | expect(lower()->run('Hello'))->toEqual([]); 112 | }); 113 | }); 114 | 115 | describe('upper', function () { 116 | it('parses a upper case character', function () { 117 | expect(upper()->run('Hello'))->toEqual([Pair('H', 'ello')]); 118 | }); 119 | it('returns an empty list when upper does not match', function () { 120 | expect(upper()->run('eello'))->toEqual([]); 121 | }); 122 | }); 123 | }); 124 | 125 | describe('another use of flatMap', function () { 126 | it('can combine the result of 2 parsers', function () { 127 | expect( 128 | lower()->flatMap(function ($x) { 129 | return lower()->map(function ($y) use ($x) { 130 | return $x . $y; 131 | }); 132 | })->run('abcd'))->toEqual([Pair('ab', 'cd')]); 133 | }); 134 | }); 135 | 136 | context('Choice combinators', function () { 137 | describe('letter', function () { 138 | it('can combine parsers to parse letters', function () { 139 | expect(letter()->run('hello'))->toEqual([Pair('h', 'ello')]); 140 | expect(letter()->run('Hello'))->toEqual([Pair('H', 'ello')]); 141 | expect(letter()->run('5ello'))->toEqual([]); 142 | }); 143 | }); 144 | 145 | describe('alphanum', function () { 146 | it('can combine parsers to parse alphanum', function () { 147 | expect(alphanum()->run('hello'))->toEqual([Pair('h', 'ello')]); 148 | expect(alphanum()->run('Hello'))->toEqual([Pair('H', 'ello')]); 149 | expect(alphanum()->run('5ello'))->toEqual([Pair('5', 'ello')]); 150 | expect(alphanum()->run('#ello'))->toEqual([]); 151 | }); 152 | }); 153 | 154 | describe('nl', function () { 155 | it('can combine parsers to parse new line', function () { 156 | expect(nl()->run(" \n"))->toEqual([Pair("\n", '')]); 157 | expect(nl()->run("\n"))->toEqual([Pair("\n", '')]); 158 | expect(nl()->run("\nhello"))->toEqual([Pair("\n", 'hello')]); 159 | expect(nl()->run("\n\nhello"))->toEqual([Pair("\n", "\nhello")]); 160 | expect(nl()->run('hello'))->toEqual([]); 161 | }); 162 | }); 163 | }); 164 | 165 | context('Recursive combinators', function () { 166 | describe('word', function () { 167 | it('recognises entire words out of a string', function () { 168 | expect(word()->run('Yes!')) 169 | ->toEqual([ 170 | Pair('Yes', '!'), 171 | Pair('Ye', 's!'), 172 | Pair('Y', 'es!'), 173 | Pair('', 'Yes!'), 174 | ]); 175 | }); 176 | }); 177 | 178 | describe('string', function () { 179 | it('parses a string', function () { 180 | expect(string('hello')->run('helloworld'))->toEqual([Pair('hello', 'world')]); 181 | expect(string('helicopter')->run('helloworld'))->toEqual([]); 182 | }); 183 | }); 184 | 185 | describe('el', function () { 186 | it('can combine parsers to parse empty line', function () { 187 | expect(el()->run("\n\nhello"))->toEqual([ 188 | Pair("\n\n", 'hello'), 189 | Pair("\n", "\nhello"), 190 | ]); 191 | expect(el()->run("\n\n\nhello"))->toEqual([ 192 | Pair("\n\n\n", 'hello'), 193 | Pair("\n\n", "\nhello"), 194 | Pair("\n", "\n\nhello"), 195 | ]); 196 | expect(el()->run('hello'))->toEqual([]); 197 | }); 198 | }); 199 | }); 200 | 201 | context('Simple repetitions', function () { 202 | describe('many', function () { 203 | it('generalises repetition', function () { 204 | expect(many(char('t'))->run("ttthat's all folks"))->toEqual([ 205 | Pair('ttt', "hat's all folks"), 206 | Pair('tt', "that's all folks"), 207 | Pair('t', "tthat's all folks"), 208 | Pair('', "ttthat's all folks"), 209 | ]); 210 | }); 211 | it('never produces errors', function () { 212 | expect(many(char('x'))->run("ttthat's all folks"))->toEqual([ 213 | Pair('', "ttthat's all folks"), 214 | ]); 215 | }); 216 | }); 217 | describe('many1', function () { 218 | it('does not have the empty result of many', function () { 219 | expect(many1(char('t'))->run("ttthat's all folks"))->toEqual([ 220 | Pair('ttt', "hat's all folks"), 221 | Pair('tt', "that's all folks"), 222 | Pair('t', "tthat's all folks"), 223 | ]); 224 | }); 225 | it('may produce an error', function () { 226 | expect(many1(char('x'))->run("ttthat's all folks"))->toEqual([]); 227 | }); 228 | }); 229 | describe('manyList', function () { 230 | it('parses many things into a list', function () { 231 | expect(manyList(char('a'))->run('a')[0]->_1)->toEqual(['a']); 232 | expect(manyList(char('a'))->run('aa')[0]->_1)->toEqual(['a', 'a']); 233 | expect(manyList(char('a'))->run('b')[0]->_1)->toEqual([]); 234 | }); 235 | }); 236 | describe('manyList1', function () { 237 | it('parses many things into a list, but at least one', function () { 238 | expect(manyList1(char('a'))->run('a')[0]->_1)->toEqual(['a']); 239 | expect(manyList1(char('a'))->run('aa')[0]->_1)->toEqual(['a', 'a']); 240 | expect(manyList1(char('a'))->run('b'))->toEqual([]); 241 | }); 242 | }); 243 | describe('sepBylist', function () { 244 | it('can separate by parser and return a list for single element', function () { 245 | expect(sepByList(char('a'), constructorSeparator())->run('a')[0]->_1) 246 | ->toEqual(['a']); 247 | }); 248 | it('can separate by parser and return a list', function () { 249 | expect(sepByList(char('a'), constructorSeparator())->run('a|a|a')[0]->_1) 250 | ->toEqual(['a', 'a', 'a']); 251 | }); 252 | }); 253 | describe('sepBy1list', function () { 254 | it('can separate by parser and return a list', function () { 255 | expect(sepBy1list(char('a'), constructorSeparator())->run('a|a|a')[0]->_1) 256 | ->toEqual(['a', 'a', 'a']); 257 | }); 258 | }); 259 | describe('nat', function () { 260 | it('can be defined with repetition', function () { 261 | expect(nat()->run('34578748fff'))->toEqual([ 262 | Pair(34578748, 'fff'), 263 | Pair(3457874, '8fff'), 264 | Pair(345787, '48fff'), 265 | Pair(34578, '748fff'), 266 | Pair(3457, '8748fff'), 267 | Pair(345, '78748fff'), 268 | Pair(34, '578748fff'), 269 | Pair(3, '4578748fff'), 270 | ]); 271 | expect(nat()->run('34578748fff')[0]->_1)->toEqual(34578748); 272 | }); 273 | }); 274 | describe('int', function () { 275 | it("can be defined from char('-') and nat", function () { 276 | expect(int()->run('-251asdfasdf')[0]->_1)->toEqual(-251); 277 | expect(int()->run('251asdfasdf')[0]->_1)->toEqual(251); 278 | }); 279 | }); 280 | describe('spaces', function () { 281 | it('parses 0 or more spaces from a string', function () { 282 | expect(spaces()->run('abc')[0]->_1)->toBe(''); 283 | expect(spaces()->run(' abc')[0]->_1)->toBe(' '); 284 | expect(spaces()->run(' abc')[0]->_1)->toBe(' '); 285 | }); 286 | it('returns empty string for empty string', function () { 287 | expect(spaces()->run('')[0]->_1)->toBe(''); 288 | }); 289 | }); 290 | describe('spaces1', function () { 291 | it('parses 1 or more spaces from a string', function () { 292 | expect(spaces1()->run('abc'))->toEqual([]); 293 | expect(spaces1()->run(' abc')[0]->_1)->toBe(' '); 294 | expect(spaces1()->run(' abc')[0]->_1)->toBe(' '); 295 | }); 296 | it('returns Nil for empty string', function () { 297 | expect(spaces1()->run(''))->toEqual([]); 298 | }); 299 | }); 300 | describe('comma', function () { 301 | it('parses a comma', function () { 302 | expect(comma()->run(',')[0]->_1)->toBe(','); 303 | expect(comma()->run(' , ' . PHP_EOL)[0]->_1)->toBe(','); 304 | }); 305 | }); 306 | }); 307 | 308 | context('Repetition with separators', function () { 309 | describe('ints, a proper grammar [1,2,3,4]', function () { 310 | it('parses a string representing an array of ints', function () { 311 | expect(ints()->run('[1,2,3,4]'))->toEqual([Pair('1234', '')]); 312 | }); 313 | }); 314 | describe('sepby1', function () { 315 | it('applies a parser several times separated by another parser application', function () { 316 | expect(letter()->sepby1(digit())->run('a1b2c3d4'))->toEqual( 317 | [Pair('abcd', '4'), Pair('abc', '3d4'), Pair('ab', '2c3d4'), Pair('a', '1b2c3d4')] 318 | ); 319 | }); 320 | }); 321 | }); 322 | }); 323 | -------------------------------------------------------------------------------- /spec/FppParser/Bool_Spec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Bool_\Bool_; 16 | use function Fpp\Type\Bool_\parse; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('bool_', function () { 21 | it('can parse bool types', function () { 22 | expect(parse()->run('bool Truth;')[0]->_1)->toEqual( 23 | new Bool_( 24 | 'Truth', 25 | [] 26 | ) 27 | ); 28 | }); 29 | 30 | it('can parse bool types with markers', function () { 31 | expect(parse()->run('bool Truth : Foo, Bar;')[0]->_1)->toEqual( 32 | new Bool_( 33 | 'Truth', 34 | ['Foo', 'Bar'] 35 | ) 36 | ); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /spec/FppParser/CommandSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Argument; 16 | use Fpp\Type\Command\Command; 17 | use Fpp\Type\Command\Constructor; 18 | use function Fpp\Type\Command\parse; 19 | 20 | describe("Fpp\Parser", function () { 21 | context('FPP parsers', function () { 22 | describe('command', function () { 23 | it('can parse command types', function () { 24 | expect(parse()->run('command Cmd (Guid) = Go { string, int };')[0]->_1)->toEqual( 25 | new Command( 26 | 'Cmd', 27 | [], 28 | 'Guid', 29 | [ 30 | new Constructor( 31 | 'Go', 32 | '', 33 | [ 34 | new Argument( 35 | 'string', 36 | 'string', 37 | false, 38 | false, 39 | null), 40 | new Argument( 41 | 'int', 42 | 'int', 43 | false, 44 | false, 45 | null 46 | ), 47 | ] 48 | ), 49 | ] 50 | ) 51 | ); 52 | }); 53 | 54 | it('can parse command types with custom names', function () { 55 | expect(parse()->run('command Cmd (Guid) = Go as goo { string, int };')[0]->_1)->toEqual( 56 | new Command( 57 | 'Cmd', 58 | [], 59 | 'Guid', 60 | [ 61 | new Constructor( 62 | 'Go', 63 | 'goo', 64 | [ 65 | new Argument( 66 | 'string', 67 | 'string', 68 | false, 69 | false, 70 | null 71 | ), 72 | new Argument( 73 | 'int', 74 | 'int', 75 | false, 76 | false, 77 | null 78 | ), 79 | ] 80 | ), 81 | ] 82 | ) 83 | ); 84 | }); 85 | 86 | it('can parse command types with custom name, marker and multiple types', function () { 87 | expect(parse()->run('command Cmd : Marker (Guid) = Go as goo { string, int } 88 | | Go2 as goo2 { bool, float };')[0]->_1)->toEqual( 89 | new Command( 90 | 'Cmd', 91 | ['Marker'], 92 | 'Guid', 93 | [ 94 | new Constructor( 95 | 'Go', 96 | 'goo', 97 | [ 98 | new Argument( 99 | 'string', 100 | 'string', 101 | false, 102 | false, 103 | null 104 | ), 105 | new Argument( 106 | 'int', 107 | 'int', 108 | false, 109 | false, 110 | null 111 | ), 112 | ] 113 | ), 114 | new Constructor( 115 | 'Go2', 116 | 'goo2', 117 | [ 118 | new Argument( 119 | 'bool', 120 | 'bool', 121 | false, 122 | false, 123 | null 124 | ), 125 | new Argument( 126 | 'float', 127 | 'float', 128 | false, 129 | false, 130 | null 131 | ), 132 | ] 133 | ), 134 | ] 135 | ) 136 | ); 137 | }); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /spec/FppParser/ConstructorSeparatorSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\constructorSeparator; 16 | 17 | describe("Fpp\Parser", function () { 18 | context('FPP parsers', function () { 19 | describe('constructorSeparator', function () { 20 | it('can parse constructor separators', function () { 21 | expect(constructorSeparator()->run('|')[0]->_1)->toBe('|'); 22 | expect(constructorSeparator()->run(' | ')[0]->_1)->toBe('|'); 23 | expect(constructorSeparator()->run(' | ')[0]->_1)->toBe('|'); 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /spec/FppParser/DataSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Argument; 16 | use Fpp\Type\Data\Constructor; 17 | use Fpp\Type\Data\Data; 18 | use function Fpp\Type\Data\parse; 19 | 20 | describe("Fpp\Parser", function () { 21 | context('FPP parsers', function () { 22 | describe('data', function () { 23 | it('can parse simple data types', function () { 24 | expect(parse()->run('data Person = { $name, $age};')[0]->_1)->toEqual( 25 | new Data( 26 | 'Person', 27 | [], 28 | [ 29 | new Constructor('Person', [ 30 | new Argument('name', null, false, false, null), 31 | new Argument('age', null, false, false, null), 32 | ]), 33 | ] 34 | ) 35 | ); 36 | }); 37 | 38 | it('can parse simple data types with scalar types', function () { 39 | expect(parse()->run('data Person = { string $name, int $age};')[0]->_1)->toEqual( 40 | new Data( 41 | 'Person', 42 | [], 43 | [ 44 | new Constructor('Person', [ 45 | new Argument('name', 'string', false, false, null), 46 | new Argument('age', 'int', false, false, null), 47 | ]), 48 | ] 49 | ) 50 | ); 51 | }); 52 | 53 | it('can parse simple data types with types', function () { 54 | expect(parse()->run('data Person = { Name $name, Age $age};')[0]->_1)->toEqual( 55 | new Data( 56 | 'Person', 57 | [], 58 | [ 59 | new Constructor('Person', [ 60 | new Argument('name', 'Name', false, false, null), 61 | new Argument('age', 'Age', false, false, null), 62 | ]), 63 | ] 64 | ) 65 | ); 66 | }); 67 | 68 | it('can parse data types with nullable argument', function () { 69 | expect(parse()->run('data Person = { ?string $name, int $age};')[0]->_1)->toEqual( 70 | new Data( 71 | 'Person', 72 | [], 73 | [ 74 | new Constructor('Person', [ 75 | new Argument('name', 'string', true, false, null), 76 | new Argument('age', 'int', false, false, null), 77 | ]), 78 | ] 79 | ) 80 | ); 81 | }); 82 | 83 | it('can parse data types with two nullable arguments', function () { 84 | expect(parse()->run('data Person = { ?string $name, ?int $age};')[0]->_1)->toEqual( 85 | new Data( 86 | 'Person', 87 | [], 88 | [ 89 | new Constructor('Person', [ 90 | new Argument('name', 'string', true, false, null), 91 | new Argument('age', 'int', true, false, null), 92 | ]), 93 | ] 94 | ) 95 | ); 96 | }); 97 | 98 | it('can parse data types with default value argument', function () { 99 | expect(parse()->run('data Person = { string $name = \'prooph\', int $age = 18};')[0]->_1)->toEqual( 100 | new Data( 101 | 'Person', 102 | [], 103 | [ 104 | new Constructor('Person', [ 105 | new Argument('name', 'string', false, false, '\'prooph\''), 106 | new Argument('age', 'int', false, false, 18), 107 | ]), 108 | ] 109 | ) 110 | ); 111 | }); 112 | 113 | it('can parse data types with default value argument and nullable', function () { 114 | expect(parse()->run('data Person = { string $name, ?int $age = null};')[0]->_1)->toEqual( 115 | new Data( 116 | 'Person', 117 | [], 118 | [ 119 | new Constructor('Person', [ 120 | new Argument('name', 'string', false, false, null), 121 | new Argument('age', 'int', true, false, 'null'), 122 | ]), 123 | ] 124 | ) 125 | ); 126 | }); 127 | 128 | it('can parse simple data types with markers', function () { 129 | expect(parse()->run('data Person : Human = { Name $name, Age $age};')[0]->_1)->toEqual( 130 | new Data( 131 | 'Person', 132 | ['Human'], 133 | [ 134 | new Constructor('Person', [ 135 | new Argument('name', 'Name', false, false, null), 136 | new Argument('age', 'Age', false, false, null), 137 | ]), 138 | ] 139 | ) 140 | ); 141 | }); 142 | 143 | it('can parse data types without variable names', function () { 144 | expect(parse()->run('data Person : Human = { Name, Age };')[0]->_1)->toEqual( 145 | new Data( 146 | 'Person', 147 | ['Human'], 148 | [ 149 | new Constructor('Person', [ 150 | new Argument('name', 'Name', false, false, null), 151 | new Argument('age', 'Age', false, false, null), 152 | ]), 153 | ] 154 | ) 155 | ); 156 | }); 157 | 158 | it('can parse data types without variable names and repeating types', function () { 159 | expect(parse()->run('data Person : Human = { Name, Name };')[0]->_1)->toEqual( 160 | new Data( 161 | 'Person', 162 | ['Human'], 163 | [ 164 | new Constructor('Person', [ 165 | new Argument('name', 'Name', false, false, null), 166 | new Argument('name2', 'Name', false, false, null), 167 | ]), 168 | ] 169 | ) 170 | ); 171 | }); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /spec/FppParser/EnumSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Enum\Constructor; 16 | use Fpp\Type\Enum\Enum; 17 | use function Fpp\Type\Enum\parse; 18 | 19 | describe("Fpp\Parser", function () { 20 | context('FPP parsers', function () { 21 | describe('enum', function () { 22 | it('can parse enums', function () { 23 | expect(parse()->run('enum Color = Red | Green | Blue;')[0]->_1)->toEqual( 24 | new Enum( 25 | 'Color', 26 | [], 27 | [ 28 | new Constructor('Red'), 29 | new Constructor('Green'), 30 | new Constructor('Blue'), 31 | ] 32 | ) 33 | ); 34 | }); 35 | 36 | it('can parse enums with markers', function () { 37 | expect(parse()->run('enum Color : Enum = Red | Green | Blue;')[0]->_1)->toEqual( 38 | new Enum( 39 | 'Color', 40 | ['Enum'], 41 | [ 42 | new Constructor('Red'), 43 | new Constructor('Green'), 44 | new Constructor('Blue'), 45 | ] 46 | ) 47 | ); 48 | }); 49 | 50 | it('cannot parse enum constructors without semicolon ending', function () { 51 | expect(parse()->run('enum Color = Red | Green | Blue'))->toEqual([]); 52 | }); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/FppParser/EventSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Argument; 16 | use Fpp\Type\Event\Constructor; 17 | use Fpp\Type\Event\Event; 18 | use function Fpp\Type\Event\parse; 19 | 20 | describe("Fpp\Parser", function () { 21 | context('FPP parsers', function () { 22 | describe('event', function () { 23 | it('can parse event types', function () { 24 | expect(parse()->run('event Ev (Guid, int) = Go { string, int };')[0]->_1)->toEqual( 25 | new Event( 26 | 'Ev', 27 | [], 28 | 'Guid', 29 | 'int', 30 | [ 31 | new Constructor( 32 | 'Go', 33 | '', 34 | [ 35 | new Argument( 36 | 'string', 37 | 'string', 38 | false, 39 | false, 40 | null), 41 | new Argument( 42 | 'int', 43 | 'int', 44 | false, 45 | false, 46 | null 47 | ), 48 | ] 49 | ), 50 | ] 51 | ) 52 | ); 53 | }); 54 | 55 | it('can parse event types with custom names', function () { 56 | expect(parse()->run('event Ev (Guid, int) = Go as goo { string, int };')[0]->_1)->toEqual( 57 | new Event( 58 | 'Ev', 59 | [], 60 | 'Guid', 61 | 'int', 62 | [ 63 | new Constructor( 64 | 'Go', 65 | 'goo', 66 | [ 67 | new Argument( 68 | 'string', 69 | 'string', 70 | false, 71 | false, 72 | null 73 | ), 74 | new Argument( 75 | 'int', 76 | 'int', 77 | false, 78 | false, 79 | null 80 | ), 81 | ] 82 | ), 83 | ] 84 | ) 85 | ); 86 | }); 87 | 88 | it('can parse event types with custom name, marker and multiple types', function () { 89 | expect(parse()->run('event Ev : Marker (Guid, int) = Go as goo { string, int } 90 | | Go2 as goo2 { bool, float };')[0]->_1)->toEqual( 91 | new Event( 92 | 'Ev', 93 | ['Marker'], 94 | 'Guid', 95 | 'int', 96 | [ 97 | new Constructor( 98 | 'Go', 99 | 'goo', 100 | [ 101 | new Argument( 102 | 'string', 103 | 'string', 104 | false, 105 | false, 106 | null 107 | ), 108 | new Argument( 109 | 'int', 110 | 'int', 111 | false, 112 | false, 113 | null 114 | ), 115 | ] 116 | ), 117 | new Constructor( 118 | 'Go2', 119 | 'goo2', 120 | [ 121 | new Argument( 122 | 'bool', 123 | 'bool', 124 | false, 125 | false, 126 | null 127 | ), 128 | new Argument( 129 | 'float', 130 | 'float', 131 | false, 132 | false, 133 | null 134 | ), 135 | ] 136 | ), 137 | ] 138 | ) 139 | ); 140 | }); 141 | }); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /spec/FppParser/Float_Spec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Float_\Float_; 16 | use function Fpp\Type\Float_\parse; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('float_', function () { 21 | it('can parse float types', function () { 22 | expect(parse()->run('float Longitude;')[0]->_1)->toEqual( 23 | new Float_( 24 | 'Longitude', 25 | [] 26 | ) 27 | ); 28 | }); 29 | 30 | it('can parse float types with markers', function () { 31 | expect(parse()->run('float Longitude : Distance;')[0]->_1)->toEqual( 32 | new Float_( 33 | 'Longitude', 34 | ['Distance'] 35 | ) 36 | ); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /spec/FppParser/GuidSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Guid\Guid; 16 | use function Fpp\Type\Guid\parse; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('guid', function () { 21 | it('can parse guid types', function () { 22 | expect(parse()->run('guid UserId;')[0]->_1)->toEqual( 23 | new Guid('UserId', []) 24 | ); 25 | }); 26 | 27 | it('can parse guid types with markers', function () { 28 | expect(parse()->run('guid UserId : Stringify;')[0]->_1)->toEqual( 29 | new Guid('UserId', ['Stringify']) 30 | ); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/FppParser/ImportSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\imports; 16 | 17 | describe("Fpp\Parser", function () { 18 | context('FPP parsers', function () { 19 | describe('imports', function () { 20 | it('can parse use imports', function () { 21 | expect(imports()->run('use Foo;')[0]->_1)->toEqual(Pair('Foo', null)); 22 | expect(imports()->run("use Foo\Bar;")[0]->_1)->toEqual(Pair('Foo\Bar', null)); 23 | }); 24 | 25 | it('can parse aliased use imports', function () { 26 | expect(imports()->run('use Foo as F;')[0]->_1)->toEqual(Pair('Foo', 'F')); 27 | expect(imports()->run("use Foo\Bar as Baz;")[0]->_1)->toEqual(Pair('Foo\Bar', 'Baz')); 28 | }); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /spec/FppParser/Int_Spec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Int_\Int_; 16 | use function Fpp\Type\Int_\parse; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('int_', function () { 21 | it('can parse int types', function () { 22 | expect(parse()->run('int Age;')[0]->_1)->toEqual( 23 | new Int_( 24 | 'Age', 25 | [] 26 | ) 27 | ); 28 | }); 29 | 30 | it('can parse int types with markers', function () { 31 | expect(parse()->run('int Age : Number;')[0]->_1)->toEqual( 32 | new Int_( 33 | 'Age', 34 | ['Number'] 35 | ) 36 | ); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /spec/FppParser/MarkerSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use Fpp\Type\Marker\Marker; 16 | use function Fpp\Type\Marker\parse; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('marker', function () { 21 | it('can parse marker types', function () { 22 | expect(parse()->run('marker Foo;')[0]->_1)->toEqual( 23 | new Marker( 24 | 'Foo', 25 | [] 26 | ) 27 | ); 28 | }); 29 | 30 | it('can parse marker types extending another marker', function () { 31 | expect(parse()->run('marker Foo : Bar;')[0]->_1)->toEqual( 32 | new Marker( 33 | 'Foo', 34 | ['Bar'] 35 | ) 36 | ); 37 | }); 38 | 39 | it('can parse marker types extending multiple markers', function () { 40 | expect(parse()->run('marker Foo : Bar, Baz, Bam;')[0]->_1)->toEqual( 41 | new Marker( 42 | 'Foo', 43 | ['Bar', 'Baz', 'Bam'] 44 | ) 45 | ); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/FppParser/MultipleNamespacesSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\multipleNamespaces; 16 | use Fpp\Type\Enum\Enum; 17 | use function Fpp\Type\Enum\parse as enum; 18 | use Phunkie\Types\ImmMap; 19 | 20 | describe("Fpp\Parser", function () { 21 | context('FPP parsers', function () { 22 | describe('multipleNamespaces', function () { 23 | it('cannot parse default namespace', function () { 24 | expect(multipleNamespaces(enum())->run('namespace { }'))->toEqual([]); 25 | }); 26 | 27 | it('can parse namespace', function () { 28 | $testString = <<run($testString)[0]->_1; 35 | expect(isset($definition['Foo\Color']))->toBe(true); 36 | expect($definition['Foo\Color']->imports())->toEqual([]); 37 | }); 38 | 39 | it('can parse namespace with subnamespace', function () { 40 | $testString = <<run($testString)[0]->_1; 47 | expect(isset($definition['Foo\Bar\Color']))->toBe(true); 48 | expect($definition['Foo\Bar\Color']->imports())->toEqual([]); 49 | }); 50 | 51 | it('can parse namespace containing many enums', function () { 52 | $testString = <<run($testString)[0]->_1; 60 | expect(isset($definitions['Foo\Color']))->toBe(true); 61 | expect(isset($definitions['Foo\Human']))->toBe(true); 62 | 63 | expect($definitions['Foo\Color']->imports())->toEqual([]); 64 | expect($definitions['Foo\Human']->imports())->toEqual([]); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /spec/FppParser/SingleNamespaceSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\singleNamespace; 16 | use Fpp\Type\Enum\Enum; 17 | use function Fpp\Type\Enum\parse as enum; 18 | use Phunkie\Types\ImmMap; 19 | 20 | describe("Fpp\Parser", function () { 21 | context('FPP parsers', function () { 22 | describe('singleNamespace', function () { 23 | it('cannot parse second namespace when ending with ;', function () { 24 | expect(singleNamespace(enum())->run('namespace Foo;namespace Bar;')[0]->_2)->toBe('namespace Bar;'); 25 | }); 26 | 27 | it('can parse one namespace when ending with ;', function () { 28 | $testString = <<run($testString)[0]->_1; 35 | expect(isset($definition['Foo\Color']))->toBe(true); 36 | expect($definition['Foo\Color']->imports())->toEqual([]); 37 | }); 38 | 39 | it('can parse one namespace when ending with ; with use imports and an enum inside', function () { 40 | $testString = <<run($testString)[0]->_1; 48 | expect(isset($definition['Foo\Color']))->toBe(true); 49 | expect($definition['Foo\Color']->imports())->toEqual([ 50 | Pair('Foo\Bar', null), 51 | Pair('Foo\Baz', 'B'), 52 | ]); 53 | }); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /spec/FppParser/String_Spec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\Type\String_\parse; 16 | use Fpp\Type\String_\String_; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('string_', function () { 21 | it('can parse string types', function () { 22 | expect(parse()->run('string Username;')[0]->_1)->toEqual( 23 | new String_( 24 | 'Username', 25 | [] 26 | ) 27 | ); 28 | }); 29 | 30 | it('can parse string types with markers', function () { 31 | expect(parse()->run('string Username : LetterCollection;')[0]->_1)->toEqual( 32 | new String_( 33 | 'Username', 34 | ['LetterCollection'] 35 | ) 36 | ); 37 | }); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /spec/FppParser/TypeNameSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\typeName; 16 | 17 | describe("Fpp\Parser", function () { 18 | context('FPP parsers', function () { 19 | describe('typeName', function () { 20 | it('recognises type names out of a string', function () { 21 | expect(typeName()->run('Yes!')[0]->_1)->toEqual('Yes'); 22 | expect(typeName()->run('Yes2!')[0]->_1)->toEqual('Yes2'); 23 | expect(typeName()->run('Ye2s2!')[0]->_1)->toEqual('Ye2s2'); 24 | expect(typeName()->run('Ye_s2_!')[0]->_1)->toEqual('Ye_s2_'); 25 | expect(typeName()->run('2Yes!'))->toEqual([]); 26 | }); 27 | 28 | it('cannot parse php keyword as type name', function () { 29 | expect(typeName()->run('Public'))->toEqual([]); 30 | }); 31 | 32 | it('can parse strings containing a php keyword', function () { 33 | expect(typeName()->run('Publics')[0]->_1)->toBe('Publics'); 34 | expect(typeName()->run(' Final_')[0]->_1)->toBe('Final_'); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /spec/FppParser/UuidSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec\FppParser; 14 | 15 | use function Fpp\Type\Uuid\parse; 16 | use Fpp\Type\Uuid\Uuid; 17 | 18 | describe("Fpp\Parser", function () { 19 | context('FPP parsers', function () { 20 | describe('uuid', function () { 21 | it('can parse uuid types', function () { 22 | expect(parse()->run('uuid UserId;')[0]->_1)->toEqual( 23 | new Uuid('UserId', []) 24 | ); 25 | }); 26 | 27 | it('can parse guid types with markers', function () { 28 | expect(parse()->run('uuid UserId : Stringify;')[0]->_1)->toEqual( 29 | new Uuid('UserId', ['Stringify']) 30 | ); 31 | }); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /spec/FppSpec.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace FppSpec; 14 | 15 | use function describe; 16 | use function expect; 17 | use Fpp\Argument; 18 | use function Fpp\buildDefaultPhpFile; 19 | use function Fpp\calculateDefaultValue; 20 | use Fpp\Configuration; 21 | use Fpp\Definition; 22 | use function Fpp\flatMap; 23 | use function Fpp\isKeyword; 24 | use function Fpp\locatePsrPath; 25 | use Fpp\Type\Enum\Constructor; 26 | use Fpp\Type\Enum\Enum; 27 | use Nette\PhpGenerator\PsrPrinter; 28 | use function Pair; 29 | 30 | describe('Fpp', function () { 31 | context('Basic FPP helper functions', function () { 32 | describe('isKeyword', function () { 33 | it('returns true for any given php keyword', function () { 34 | expect(isKeyword('final'))->toBe(true); 35 | }); 36 | 37 | it('returns false for any given non-keyword string', function () { 38 | expect(isKeyword('somestring'))->toBe(false); 39 | }); 40 | 41 | it('returns false for an emptry string', function () { 42 | expect(isKeyword(''))->toBe(false); 43 | }); 44 | }); 45 | 46 | describe('flatMap', function () { 47 | it('flattens an array', function () { 48 | $fn = function ($x) { 49 | if (\is_array($x)) { 50 | $res = []; 51 | 52 | foreach ($x as $y) { 53 | $res[] = $y + 1; 54 | } 55 | 56 | return $res; 57 | } 58 | 59 | return $x + 1; 60 | }; 61 | 62 | $input = [1, 2, [3, 4]]; 63 | 64 | expect(flatMap($fn, $input))->toBe([2, 3, 4, 5]); 65 | }); 66 | }); 67 | 68 | describe('locatePsrPath', function () { 69 | it('can locate filename for psr4-autoloaded class', function () { 70 | $psr4 = [ 71 | 'Foo\\' => ['src'], 72 | ]; 73 | 74 | expect(locatePsrPath($psr4, [], 'Foo\\Bar'))->toBe('src/Bar.php'); 75 | }); 76 | 77 | it('can locate filename for psr0-autoloaded class', function () { 78 | $psr0 = [ 79 | 'Foo\\' => ['src/Foo'], 80 | ]; 81 | 82 | expect(locatePsrPath([], $psr0, 'Foo\\Bar'))->toBe('src/Foo/Bar.php'); 83 | }); 84 | }); 85 | 86 | describe('buildDefaultPhpFile', function () { 87 | it('can build default php file structure', function () { 88 | $definition = new Definition( 89 | 'Foo', 90 | new Enum('Color', [], [ 91 | new Constructor('Blue'), 92 | new Constructor('Red'), 93 | ]), 94 | [ 95 | Pair('LongName', 'LN'), 96 | ] 97 | ); 98 | $config = new Configuration( 99 | true, 100 | '.', 101 | '.', 102 | 'OK', 103 | fn () => new PsrPrinter(), 104 | fn () => null, 105 | null, 106 | [] 107 | ); 108 | 109 | $phpFile = buildDefaultPhpFile($definition, $config); 110 | 111 | expect($phpFile->hasStrictTypes())->toBe(true); 112 | expect($phpFile->getNamespaces())->not->toBeEmpty(); 113 | expect($phpFile->getNamespaces()['Foo']->getName())->toBe('Foo'); 114 | expect($phpFile->getNamespaces()['Foo']->getUses())->toEqual(['LN' => 'LongName']); 115 | }); 116 | }); 117 | 118 | describe('calculateDefaultValue', function () { 119 | it('can calculate default php values for a given argument', function () { 120 | $arg1 = new Argument('foo', null, false, false, null); 121 | 122 | expect(calculateDefaultValue($arg1))->toBe(null); 123 | 124 | $arg2 = new Argument('foo', 'string', false, false, '\'\''); 125 | 126 | expect(calculateDefaultValue($arg2))->toBe(''); 127 | 128 | $arg3 = new Argument('foo', 'int', false, false, '12'); 129 | 130 | expect(calculateDefaultValue($arg3))->toBe(12); 131 | 132 | $arg4 = new Argument('foo', null, false, true, '[]'); 133 | 134 | expect(calculateDefaultValue($arg4))->toBe([]); 135 | 136 | $arg5 = new Argument('foo', 'float', false, false, '12.3'); 137 | 138 | expect(calculateDefaultValue($arg5))->toBe(12.3); 139 | 140 | $arg6 = new Argument('foo', 'string', false, false, '\'foo\''); 141 | 142 | expect(calculateDefaultValue($arg6))->toBe('foo'); 143 | 144 | $arg7 = new Argument('foo', null, false, false, null); 145 | 146 | expect(calculateDefaultValue($arg7))->toBe(null); 147 | 148 | $arg8 = new Argument('foo', 'array', false, true, '[]'); 149 | 150 | expect(calculateDefaultValue($arg8))->toBe([]); 151 | 152 | $arg9 = new Argument('foo', 'string', true, false, null); 153 | 154 | expect(calculateDefaultValue($arg9))->toBe(null); 155 | }); 156 | }); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /src/Argument.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | class Argument 16 | { 17 | private string $name; 18 | private ?string $type; 19 | private bool $nullable; 20 | private bool $isList; 21 | /** @var mixed */ 22 | private $defaultValue; 23 | 24 | public function __construct(string $name, ?string $type, bool $nullable, bool $isList, $defaultValue) 25 | { 26 | if (empty($name)) { 27 | $name = \lcfirst($type); 28 | 29 | switch (true) { 30 | case $isList && \substr($name, -1) === 's': 31 | $name .= 'es'; 32 | break; 33 | case $isList && \substr($name, -1) === 'y': 34 | $name = \substr($name, 0, -1) . 'ies'; 35 | break; 36 | case $isList: 37 | $name .= 's'; 38 | } 39 | } 40 | 41 | $this->name = $name; 42 | $this->type = $type; 43 | $this->nullable = $nullable; 44 | $this->isList = $isList; 45 | $this->defaultValue = $defaultValue; 46 | } 47 | 48 | public function name(): string 49 | { 50 | return $this->name; 51 | } 52 | 53 | public function type(): ?string 54 | { 55 | return $this->type; 56 | } 57 | 58 | public function nullable(): bool 59 | { 60 | return $this->nullable; 61 | } 62 | 63 | public function isList(): bool 64 | { 65 | return $this->isList; 66 | } 67 | 68 | public function defaultValue() 69 | { 70 | return $this->defaultValue; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use Closure; 16 | use Composer\Autoload\ClassLoader; 17 | use InvalidArgumentException; 18 | use RuntimeException; 19 | 20 | class Configuration 21 | { 22 | private const availableTargets = ['composer', 'vfs']; 23 | 24 | private bool $useStrictTypes; 25 | private string $source; 26 | private string $target; 27 | private string $successMessage; 28 | private Closure $printer; 29 | private Closure $fileParser; 30 | private ?string $comment; 31 | /** @var array */ 32 | private array $types; 33 | 34 | /** @param array $types */ 35 | public function __construct(bool $useStrictTypes, string $source, string $target, string $successMessage, callable $printer, callable $fileParser, ?string $comment, array $types) 36 | { 37 | $this->useStrictTypes = $useStrictTypes; 38 | $this->source = $source; 39 | $this->target = $target; 40 | $this->successMessage = $successMessage; 41 | $this->printer = Closure::fromCallable($printer); 42 | $this->fileParser = Closure::fromCallable($fileParser); 43 | $this->comment = $comment; 44 | $this->types = $types; 45 | } 46 | 47 | public static function fromArray(array $data): self 48 | { 49 | if (! isset($data['use_strict_types'], $data['source'], $data['target'], $data['success_msg'], $data['printer'], $data['file_parser'], $data['comment'], $data['types'])) { 50 | throw new InvalidArgumentException( 51 | 'Missing keys in array configuration' 52 | ); 53 | } 54 | 55 | return new self( 56 | $data['use_strict_types'], 57 | $data['source'], 58 | $data['target'], 59 | $data['success_msg'], 60 | $data['printer'], 61 | $data['file_parser'], 62 | $data['comment'], 63 | $data['types'] 64 | ); 65 | } 66 | 67 | public function builderFor(Type $type): ?Closure 68 | { 69 | $class = \get_class($type); 70 | 71 | if (! isset($this->types[$class])) { 72 | throw new RuntimeException(\sprintf( 73 | 'No %s for %s found', 74 | 'builder function', 75 | $class 76 | )); 77 | } 78 | 79 | return $this->types[$class]->build(); 80 | } 81 | 82 | public function fromPhpValueFor(Type $type): ?Closure 83 | { 84 | $class = \get_class($type); 85 | 86 | if (! isset($this->types[$class])) { 87 | throw new RuntimeException(\sprintf( 88 | 'No %s for %s found', 89 | 'fromPhpValue function', 90 | $class 91 | )); 92 | } 93 | 94 | return $this->types[$class]->fromPhpValue(); 95 | } 96 | 97 | public function toPhpValueFor(Type $type): ?Closure 98 | { 99 | $class = \get_class($type); 100 | 101 | if (! isset($this->types[$class])) { 102 | throw new RuntimeException(\sprintf( 103 | 'No %s for %s found', 104 | 'toPhpValue function', 105 | $class 106 | )); 107 | } 108 | 109 | return $this->types[$class]->toPhpValue(); 110 | } 111 | 112 | public function validatorFor(Type $type): ?Closure 113 | { 114 | $class = \get_class($type); 115 | 116 | if (! isset($this->types[$class])) { 117 | throw new RuntimeException(\sprintf( 118 | 'No %s for %s found', 119 | 'validator function', 120 | $class 121 | )); 122 | } 123 | 124 | return $this->types[$class]->validator(); 125 | } 126 | 127 | public function validationErrorMessageFor(Type $type): ?Closure 128 | { 129 | $class = \get_class($type); 130 | 131 | if (! isset($this->types[$class])) { 132 | throw new RuntimeException(\sprintf( 133 | 'No %s for %s found', 134 | 'validation error message function', 135 | $class 136 | )); 137 | } 138 | 139 | return $this->types[$class]->validationErrorMessage(); 140 | } 141 | 142 | public function equalsFor(Type $type): ?Closure 143 | { 144 | $class = \get_class($type); 145 | 146 | if (! isset($this->types[$class])) { 147 | throw new RuntimeException(\sprintf( 148 | 'No %s for %s found', 149 | 'equals function', 150 | $class 151 | )); 152 | } 153 | 154 | return $this->types[$class]->equals(); 155 | } 156 | 157 | public function useStrictTypes(): bool 158 | { 159 | return $this->useStrictTypes; 160 | } 161 | 162 | public function source(): string 163 | { 164 | return $this->source; 165 | } 166 | 167 | public function target(): string 168 | { 169 | return $this->target; 170 | } 171 | 172 | public function locatePath(ClassLoader $classLoader): Closure 173 | { 174 | if ('*' !== $this->target) { 175 | return $this->locatePathFromTarget(); 176 | } 177 | 178 | return $this->locatePathFromComposer($classLoader); 179 | } 180 | 181 | private function locatePathFromComposer(ClassLoader $classLoader): Closure 182 | { 183 | $prefixesPsr4 = $classLoader->getPrefixesPsr4(); 184 | $prefixesPsr0 = $classLoader->getPrefixes(); 185 | 186 | return function (string $classname) use ($prefixesPsr4, $prefixesPsr0): string { 187 | return locatePsrPath($prefixesPsr4, $prefixesPsr0, $classname); 188 | }; 189 | } 190 | 191 | private function locatePathFromTarget(): Closure 192 | { 193 | return function (string $classname): string { 194 | return $this->target . DIRECTORY_SEPARATOR . \strtr($classname, '\\', DIRECTORY_SEPARATOR) . '.php'; 195 | }; 196 | } 197 | 198 | public function successMessage(): string 199 | { 200 | return $this->successMessage; 201 | } 202 | 203 | public function printer(): Closure 204 | { 205 | return $this->printer; 206 | } 207 | 208 | public function fileParser(): Closure 209 | { 210 | return $this->fileParser; 211 | } 212 | 213 | public function comment(): ?string 214 | { 215 | return $this->comment; 216 | } 217 | 218 | /** @return array */ 219 | public function types(): array 220 | { 221 | return $this->types; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /src/Definition.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | class Definition 16 | { 17 | private string $namespace; 18 | private Type $type; 19 | private array $imports; 20 | 21 | public function __construct(string $namespace, Type $type, array $imports) 22 | { 23 | $this->namespace = $namespace; 24 | $this->type = $type; 25 | $this->imports = $imports; 26 | } 27 | 28 | public function namespace(): string 29 | { 30 | return $this->namespace; 31 | } 32 | 33 | public function type(): Type 34 | { 35 | return $this->type; 36 | } 37 | 38 | public function imports(): array 39 | { 40 | return $this->imports; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Functions/basic_parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use const Phunkie\Functions\numbers\negate; 16 | 17 | function result($a): Parser 18 | { 19 | return new Parser(fn (string $s) => [Pair($a, $s)]); 20 | } 21 | 22 | function zero(): Parser 23 | { 24 | return new Parser(fn (string $s) => []); 25 | } 26 | 27 | function el(): Parser 28 | { 29 | return many1(nl())->map(fn ($xs) => $xs); 30 | } 31 | 32 | function item(): Parser 33 | { 34 | return new Parser(fn (string $s) => \strlen($s) === 0 ? [] : [Pair($s[0], \substr($s, 1))]); 35 | } 36 | 37 | function seq(Parser $p, Parser $q): Parser 38 | { 39 | return for_( 40 | __($x)->_($p), 41 | __($y)->_($q) 42 | )->yields($x, $y); 43 | } 44 | 45 | function sat(callable $predicate): Parser 46 | { 47 | return item()->flatMap( 48 | fn ($x) => $predicate($x) ? result($x) : zero() 49 | ); 50 | } 51 | 52 | function not($c): Parser 53 | { 54 | return sat(fn ($s) => $s !== $c); 55 | } 56 | 57 | function manyNot($c): Parser 58 | { 59 | return many1(not($c)); 60 | } 61 | 62 | function char($c): Parser 63 | { 64 | return sat(fn ($input) => $input === $c); 65 | } 66 | 67 | function digit(): Parser 68 | { 69 | return sat('is_numeric'); 70 | } 71 | 72 | function lower(): Parser 73 | { 74 | return sat('ctype_lower'); 75 | } 76 | 77 | function upper(): Parser 78 | { 79 | return sat('ctype_upper'); 80 | } 81 | 82 | function plus(Parser $p, Parser $q): Parser 83 | { 84 | return $p->or($q); 85 | } 86 | 87 | function letter(): Parser 88 | { 89 | return plus(lower(), upper()); 90 | } 91 | 92 | function nl(): Parser 93 | { 94 | return for_( 95 | __($_)->_(many(char(' '))), 96 | __($nl)->_(sat(fn (string $s) => $s === \PHP_EOL)) 97 | )->yields($nl); 98 | } 99 | 100 | function alphanum(): Parser 101 | { 102 | return plus(letter(), digit()); 103 | } 104 | 105 | function spaces(): Parser 106 | { 107 | return many(plus(char(' '), char("\n"))); 108 | } 109 | 110 | function spaces1(): Parser 111 | { 112 | return many1(plus(char(' '), char("\n"))); 113 | } 114 | 115 | function comma(): Parser 116 | { 117 | return for_( 118 | __($_)->_(spaces()), 119 | __($c)->_(char(',')), 120 | __($_)->_(spaces()), 121 | )->yields($c); 122 | } 123 | 124 | function word(): Parser 125 | { 126 | return plus(letter()->flatMap( 127 | fn ($x) => word()->map( 128 | fn ($xs) => $x . $xs 129 | ) 130 | ), result('')); 131 | } 132 | 133 | function string($s): Parser 134 | { 135 | return \strlen($s) 136 | ? for_( 137 | __($c)->_(char($s[0])), 138 | __($cs)->_(string(\substr($s, 1))) 139 | )->call(fn ($x, $xs) => $x . $xs, $c, $cs) 140 | : result(''); 141 | } 142 | 143 | function many(Parser $p): Parser 144 | { 145 | return plus($p->flatMap( 146 | fn ($x) => many($p)->map( 147 | fn ($xs) => $x . $xs) 148 | ), 149 | result('') 150 | ); 151 | } 152 | 153 | function many1(Parser $p): Parser 154 | { 155 | return for_( 156 | __($x)->_($p), 157 | __($xs)->_(many($p)) 158 | )->call(fn ($x, $xs) => $x . $xs, $x, $xs); 159 | } 160 | 161 | function manyList(Parser $p): Parser 162 | { 163 | return plus($p->flatMap( 164 | fn ($x) => manyList($p)->map( 165 | fn ($xs) => \is_array($xs) ? \array_merge([$x], $xs) : [$x, $xs] 166 | ) 167 | ), result([])); 168 | } 169 | 170 | function manyList1(Parser $p): Parser 171 | { 172 | return $p->flatMap( 173 | fn ($x) => manyList($p)->map( 174 | fn ($xs) => \is_array($xs) ? \array_merge([$x], $xs) : [$x, $xs] 175 | ) 176 | ); 177 | } 178 | 179 | function nat(): Parser 180 | { 181 | return many1(digit())->map(fn ($xs) => (int) $xs); 182 | } 183 | 184 | function int(): Parser 185 | { 186 | return plus(for_( 187 | __($_)->_(char('-')), 188 | __($n)->_(nat()) 189 | )->call(negate, $n), nat()); 190 | } 191 | 192 | function sepBy1(Parser $p, Parser $sep): Parser 193 | { 194 | return $p->sepBy1($sep); 195 | } 196 | 197 | function sepBy1With(Parser $p, Parser $sep): Parser 198 | { 199 | return $p->sepBy1With($sep); 200 | } 201 | 202 | function sepByList(Parser $p, Parser $sep): Parser 203 | { 204 | return $p->flatMap( 205 | fn ($x) => manyList($sep->flatMap( 206 | fn ($_) => $p->map(fn ($y) => $y) 207 | ))->map( 208 | fn ($xs) => \is_array($xs) ? \array_merge([$x], $xs) : [$x, $xs] 209 | ) 210 | ); 211 | } 212 | 213 | function sepBy1list(Parser $p, Parser $sep): Parser 214 | { 215 | return $p->flatMap( 216 | fn ($x) => manyList1($sep->flatMap( 217 | fn ($_) => $p->map(fn ($y) => $y) 218 | ))->map( 219 | fn ($xs) => \is_array($xs) ? \array_merge([$x], $xs) : [$x, $xs] 220 | ) 221 | ); 222 | } 223 | 224 | function surrounded(Parser $open, Parser $p, Parser $close): Parser 225 | { 226 | return for_( 227 | __($_)->_($open), 228 | __($ns)->_($p), 229 | __($_)->_($close) 230 | )->yields($ns); 231 | } 232 | 233 | function surroundedWith(Parser $open, Parser $p, Parser $close): Parser 234 | { 235 | return for_( 236 | __($o)->_($open), 237 | __($ns)->_($p), 238 | __($c)->_($close) 239 | )->call(fn ($o, $ns, $c) => $o . $ns . $c, $o, $ns, $c); 240 | } 241 | 242 | function ints(): Parser 243 | { 244 | return surrounded( 245 | char('['), 246 | int()->sepBy1(char(',')), 247 | char(']') 248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /src/Functions/fpp_parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | const assignment = 'Fpp\assignment'; 16 | 17 | function assignment(): Parser 18 | { 19 | return for_( 20 | __($_)->_(spaces()), 21 | __($a)->_(char('=')), 22 | __($_)->_(spaces()) 23 | )->yields($a); 24 | } 25 | 26 | const typeName = 'Fpp\typeName'; 27 | 28 | function typeName(): Parser 29 | { 30 | return for_( 31 | __($_)->_(spaces()), 32 | __($x)->_(plus(letter(), char('_'))), 33 | __($xs)->_(many(plus(alphanum(), char('_')))), 34 | __($t)->_(new Parser(function (string $s) use (&$x, &$xs) { 35 | return isKeyword($x . $xs) ? [] : [Pair($x . $xs, $s)]; 36 | })), 37 | )->yields($t); 38 | } 39 | 40 | const constructorSeparator = 'Fpp\constructorSeparator'; 41 | 42 | function constructorSeparator(): Parser 43 | { 44 | return for_( 45 | __($_)->_(spaces()), 46 | __($s)->_(char('|')), 47 | __($_)->_(spaces()) 48 | )->yields($s); 49 | } 50 | 51 | const imports = 'Fpp\imports'; 52 | 53 | function imports(): Parser 54 | { 55 | return plus( 56 | for_( 57 | __($_)->_(spaces()), 58 | __($_)->_(string('use')), 59 | __($_)->_(spaces1()), 60 | __($i)->_(sepBy1With(typeName(), char('\\'))), 61 | __($_)->_(spaces()), 62 | __($_)->_(char(';')) 63 | )->call(fn ($i) => Pair($i, null), $i), 64 | for_( 65 | __($_)->_(spaces()), 66 | __($_)->_(string('use')), 67 | __($_)->_(spaces1()), 68 | __($i)->_(sepBy1With(typeName(), char('\\'))), 69 | __($_)->_(spaces1()), 70 | __($_)->_(string('as')), 71 | __($_)->_(spaces1()), 72 | __($a)->_(typeName()), 73 | __($_)->_(spaces()), 74 | __($_)->_(char(';')) 75 | )->yields($i, $a) 76 | ); 77 | } 78 | 79 | const singleNamespace = 'Fpp\singleNamespace'; 80 | 81 | function singleNamespace(Parser $parserComposite): Parser 82 | { 83 | return for_( 84 | __($_)->_(spaces()), 85 | __($_)->_(string('namespace')), 86 | __($_)->_(spaces1()), 87 | __($n)->_(sepBy1With(typeName(), char('\\'))), 88 | __($_)->_(spaces()), 89 | __($_)->_(char(';')), 90 | __($_)->_(spaces()), 91 | __($is)->_(manyList(imports())), 92 | __($_)->_(spaces()), 93 | __($ts)->_(manyList($parserComposite)), 94 | )->call(function (string $n, array $ts, array $is): array { 95 | $ds = []; 96 | 97 | \array_map( 98 | function (Type $t) use (&$ds, $n, $is) { 99 | $ds[$n . '\\' . $t->classname()] = new Definition($n, $t, $is); 100 | }, 101 | $ts 102 | ); 103 | 104 | return $ds; 105 | }, $n, $ts, $is); 106 | } 107 | 108 | const multipleNamespaces = 'Fpp\multipleNamespaces'; 109 | 110 | function multipleNamespaces(Parser $parserComposite): Parser 111 | { 112 | return for_( 113 | __($_)->_(spaces()), 114 | __($_)->_(string('namespace')), 115 | __($_)->_(spaces1()), 116 | __($n)->_(sepBy1With(typeName(), char('\\'))), 117 | __($_)->_( 118 | for_( 119 | __($_)->_(spaces()), 120 | __($o)->_(char('{')), 121 | __($_)->_(spaces()) 122 | )->yields($o), 123 | ), 124 | __($is)->_(manyList(imports())), 125 | __($ts)->_(manyList($parserComposite)), 126 | __($_)->_( 127 | for_( 128 | __($_)->_(spaces()), 129 | __($c)->_(char('}')), 130 | __($_)->_(spaces()) 131 | )->yields($c) 132 | ) 133 | )->call(function (string $n, array $ts, array $is): array { 134 | $ds = []; 135 | 136 | \array_map( 137 | function (Type $t) use (&$ds, $n, $is) { 138 | $ds[$n . '\\' . $t->classname()] = new Definition($n, $t, $is); 139 | }, 140 | $ts 141 | ); 142 | 143 | return $ds; 144 | }, $n, $ts, $is); 145 | } 146 | -------------------------------------------------------------------------------- /src/Functions/scan.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use FilterIterator; 16 | use RecursiveDirectoryIterator; 17 | use RecursiveIteratorIterator; 18 | 19 | const scan = '\Fpp\scan'; 20 | 21 | /** 22 | * @param string $directoryOrFile 23 | * 24 | * @return list 25 | */ 26 | function scan(string $directoryOrFile): array 27 | { 28 | if (! \is_readable($directoryOrFile)) { 29 | return []; 30 | } 31 | 32 | if (\is_file($directoryOrFile)) { 33 | return [$directoryOrFile]; 34 | } 35 | 36 | $iterator = new class(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directoryOrFile))) extends FilterIterator { 37 | public function __construct($directoryOrFile) 38 | { 39 | parent::__construct($directoryOrFile); 40 | } 41 | 42 | public function accept(): bool 43 | { 44 | $file = $this->getInnerIterator()->current(); 45 | 46 | if (! $file->isFile()) { 47 | return false; 48 | } 49 | 50 | if (! $file->isReadable()) { 51 | return false; 52 | } 53 | 54 | return $file->getExtension() === 'fpp'; 55 | } 56 | }; 57 | 58 | $files = []; 59 | 60 | foreach ($iterator as $f => $i) { 61 | $files[] = $f; 62 | } 63 | 64 | return $files; 65 | } 66 | -------------------------------------------------------------------------------- /src/Namespace_.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | class Namespace_ 16 | { 17 | private string $name; 18 | private array $imports; 19 | private array $types; 20 | 21 | public function __construct(string $namespaceName, array $imports, array $types) 22 | { 23 | $this->name = $namespaceName; 24 | $this->imports = $imports; 25 | $this->types = $types; 26 | } 27 | 28 | public function name(): string 29 | { 30 | return $this->name; 31 | } 32 | 33 | public function imports(): array 34 | { 35 | return $this->imports; 36 | } 37 | 38 | public function types(): array 39 | { 40 | return $this->types; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Parser.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use Phunkie\Cats\Monad; 16 | use function Phunkie\Functions\show\showType; 17 | use Phunkie\Types\Kind; 18 | use Phunkie\Types\Pair; 19 | 20 | /** 21 | * Class Parser 22 | * wraps: 23 | * string -> List 24 | */ 25 | class Parser implements Monad, Kind 26 | { 27 | /** 28 | * @var callable string -> List 29 | */ 30 | private $run; 31 | 32 | public function __construct(callable $run) 33 | { 34 | $this->run = $run; 35 | } 36 | 37 | /** 38 | * string -> array 39 | */ 40 | public function run(string $input): array 41 | { 42 | return ($this->run)($input); 43 | } 44 | 45 | /** 46 | * (A => Parser) => Parser(B) 47 | */ 48 | public function flatMap(callable $f): Kind 49 | { 50 | return new Parser(function (string $s) use ($f) { 51 | return flatMap( 52 | fn (Pair $result) => $f($result->_1)->run($result->_2), 53 | $this->run($s) 54 | ); 55 | }); 56 | 57 | return new Parser(fn (string $s) => $this->run($s)->flatMap( 58 | fn (Pair $result) => $f($result->_1)->run($result->_2) 59 | )); 60 | } 61 | 62 | /** 63 | * (A => B) => Parser 64 | */ 65 | public function map(callable $f): Kind 66 | { 67 | return new Parser(fn (string $s) => \array_map( 68 | fn (Pair $result) => \Pair($f($result->_1), $result->_2), 69 | $this->run($s) 70 | )); 71 | } 72 | 73 | /** 74 | * (A => B) => (Parser => Parser) 75 | */ 76 | public function lift($f): callable 77 | { 78 | return fn (Parser $a) => new Parser(fn ($x) => $a->run($f($x))); 79 | } 80 | 81 | /** 82 | * B => Parser 83 | */ 84 | public function as($b): Kind 85 | { 86 | return result($b); 87 | } 88 | 89 | /** 90 | * () => Parser 91 | */ 92 | public function void(): Kind 93 | { 94 | return result(Unit()); 95 | } 96 | 97 | /** 98 | * (A => B) => Parser> 99 | */ 100 | public function zipWith($f): Kind 101 | { 102 | return new Parser(fn ($x) => [Pair(Pair($x, $f($x))), '']); 103 | } 104 | 105 | public function imap(callable $f, callable $g): Kind 106 | { 107 | return $this->map($f); 108 | } 109 | 110 | /** 111 | * Parser> => Parser 112 | */ 113 | public function flatten(): Kind 114 | { 115 | return $this->run(_)->head->_1; 116 | } 117 | 118 | public function or(Parser $another): Parser 119 | { 120 | return new Parser(fn (string $s) => \array_merge($this->run($s), $another->run($s))); 121 | } 122 | 123 | // @todo do we need this? 124 | public function sepBy1(Parser $sep) 125 | { 126 | return for_( 127 | __($x)->_($this), 128 | __($xs)->_(many(for_( 129 | __($_)->_($sep), 130 | __($y)->_($this) 131 | )->yields($y))) 132 | )->call(fn ($x, $xs) => $x . $xs, $x, $xs); 133 | } 134 | 135 | public function sepBy1With(Parser $sep) 136 | { 137 | return for_( 138 | __($x)->_($this), 139 | __($xs)->_(many(for_( 140 | __($s)->_($sep), 141 | __($y)->_($this) 142 | )->call(fn ($x, $y) => $x . $y, $s, $y))) 143 | )->call(fn ($x, $xs) => $x . $xs, $x, $xs); 144 | } 145 | 146 | public function getTypeArity(): int 147 | { 148 | return 1; 149 | } 150 | 151 | public function getTypeVariables(): array 152 | { 153 | return [showType($this->run)]; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Type.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | interface Type 16 | { 17 | public function classname(): string; 18 | 19 | public function markers(): array; 20 | } 21 | -------------------------------------------------------------------------------- /src/Type/Bool_.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Bool_; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\Bool_\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('bool')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new Bool_($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\Bool_\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof Bool_) { 70 | throw new \InvalidArgumentException( 71 | 'Can only build definitions of ' . Bool_::class 72 | ); 73 | } 74 | 75 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 76 | 77 | $file = buildDefaultPhpFile($definition, $config); 78 | 79 | $class = $file->addClass($fqcn) 80 | ->setFinal() 81 | ->setImplements($type->markers()); 82 | 83 | $class->addProperty('value')->setType(Type::BOOL)->setPrivate(); 84 | 85 | $constructor = $class->addMethod('__construct'); 86 | $constructor->addParameter('value')->setType(Type::BOOL); 87 | $constructor->setBody('$this->value = $value;'); 88 | 89 | $boolConstructor = $class->addMethod('true')->setReturnType(Type::SELF)->setStatic(); 90 | $boolConstructor->setBody('return new self(true);'); 91 | 92 | $falseConstructor = $class->addMethod('false')->setReturnType(Type::SELF)->setStatic(); 93 | $falseConstructor->setBody('return new self(false);'); 94 | 95 | $method = $class->addMethod('value')->setReturnType(Type::BOOL); 96 | $method->setBody('return $this->value;'); 97 | 98 | $method = $class->addMethod('equals')->setReturnType(Type::BOOL); 99 | $method->addParameter('other')->setType(Type::SELF)->setNullable(); 100 | $method->setBody('return null !== $other && $this->value === $other->value;'); 101 | 102 | return [$fqcn => $file]; 103 | } 104 | 105 | const fromPhpValue = 'Fpp\Type\Bool_\fromPhpValue'; 106 | 107 | function fromPhpValue(Bool_ $type, string $paramName): string 108 | { 109 | return 'new ' . $type->classname() . '(' . $paramName . ')'; 110 | } 111 | 112 | const toPhpValue = 'Fpp\Type\Bool_\toPhpValue'; 113 | 114 | function toPhpValue(Bool_ $type, string $paramName): string 115 | { 116 | return $paramName . '->value()'; 117 | } 118 | 119 | const validator = 'Fpp\Type\Bool_\validator'; 120 | 121 | function validator(string $type, string $paramName): string 122 | { 123 | return "\is_bool($paramName)"; 124 | } 125 | 126 | const validationErrorMessage = 'Fpp\Type\Bool_\validationErrorMessage'; 127 | 128 | function validationErrorMessage(string $paramName): string 129 | { 130 | return "Error on \"$paramName\", bool expected"; 131 | } 132 | 133 | const equals = 'Fpp\Type\Bool_\equals'; 134 | 135 | function equals(string $paramName, string $otherParamName): string 136 | { 137 | return "{$paramName}->equals($otherParamName)"; 138 | } 139 | 140 | class Bool_ implements FppType 141 | { 142 | use TypeTrait; 143 | } 144 | -------------------------------------------------------------------------------- /src/Type/Command.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Command; 14 | 15 | use function Fpp\alphanum; 16 | use Fpp\Argument; 17 | use function Fpp\assignment; 18 | use function Fpp\buildDefaultPhpFile; 19 | use function Fpp\char; 20 | use Fpp\Configuration; 21 | use function Fpp\constructorSeparator; 22 | use Fpp\Definition; 23 | use function Fpp\generateFromPhpValueFor; 24 | use function Fpp\generateValidationFor; 25 | use function Fpp\many1; 26 | use function Fpp\parseArguments; 27 | use Fpp\Parser; 28 | use function Fpp\plus; 29 | use function Fpp\renameDuplicateArgumentNames; 30 | use function Fpp\resolveType; 31 | use function Fpp\result; 32 | use function Fpp\sepByList; 33 | use function Fpp\spaces; 34 | use function Fpp\spaces1; 35 | use function Fpp\string; 36 | use Fpp\Type as FppType; 37 | use function Fpp\Type\Marker\markers; 38 | use Fpp\TypeConfiguration; 39 | use function Fpp\typeName; 40 | use Fpp\TypeTrait; 41 | use Nette\PhpGenerator\PhpFile; 42 | use Nette\PhpGenerator\Type; 43 | 44 | function typeConfiguration(): TypeConfiguration 45 | { 46 | return new TypeConfiguration( 47 | parse, 48 | build, 49 | fromPhpValue, 50 | toPhpValue, 51 | validator, 52 | validationErrorMessage, 53 | equals 54 | ); 55 | } 56 | 57 | const parse = 'Fpp\Type\Command\parse'; 58 | 59 | function parse(): Parser 60 | { 61 | return for_( 62 | __($_)->_(spaces()), 63 | __($_)->_(string('command')), 64 | __($_)->_(spaces1()), 65 | __($t)->_(typeName()), 66 | __($_)->_(spaces()), 67 | __($ms)->_( 68 | plus(markers(), result([])) 69 | ), 70 | __($_)->_(spaces()), 71 | __($_)->_(char('(')), 72 | __($_)->_(spaces()), 73 | __($cid)->_(typeName()), 74 | __($_)->_(spaces()), 75 | __($_)->_(char(')')), 76 | __($_)->_(assignment()), 77 | __($cs)->_( 78 | sepByList( 79 | for_( 80 | __($c)->_(typeName()), 81 | __($_)->_(spaces()), 82 | __($en)->_( 83 | for_( 84 | __($_)->_(string('as')), 85 | __($_)->_(spaces1()), 86 | __($en)->_( 87 | many1( 88 | alphanum() 89 | ->or(char('\\')) 90 | ->or(char('-')) 91 | ->or(char('.')) 92 | ->or(char(':')) 93 | ->or(char('_')) 94 | ) 95 | ), 96 | __($_)->_(spaces1()), 97 | )->yields($en) 98 | ->or(result('')) 99 | ), 100 | __($as)->_( 101 | parseArguments()->or(result([])) 102 | ) 103 | )->call(fn ($c, $en, $as) => new Constructor($c, $en, $as), $c, $en, $as), 104 | constructorSeparator() 105 | ) 106 | ), 107 | __($_)->_(spaces()), 108 | __($_)->_(char(';')) 109 | )->call( 110 | fn ($t, $ms, $cid, $cs) => new Command($t, $ms, $cid, $cs), 111 | $t, 112 | $ms, 113 | $cid, 114 | $cs 115 | ); 116 | } 117 | 118 | const build = 'Fpp\Type\Command\build'; 119 | 120 | function build(Definition $definition, array $definitions, Configuration $config): array 121 | { 122 | $type = $definition->type(); 123 | 124 | if (! $type instanceof Command) { 125 | throw new \InvalidArgumentException('Can only build definitions of ' . Command::class); 126 | } 127 | 128 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 129 | 130 | $file = buildDefaultPhpFile($definition, $config); 131 | 132 | $class = $file->addClass($fqcn) 133 | ->setAbstract() 134 | ->setImplements($type->markers()); 135 | 136 | $class->addProperty('commandId') 137 | ->setProtected() 138 | ->setType($type->commandIdType()); 139 | 140 | $constructor = $class->addMethod('__construct'); 141 | 142 | $constructor->addParameter('commandId') 143 | ->setType($type->commandIdType()) 144 | ->setNullable(); 145 | $constructor->addParameter('payload') 146 | ->setType(Type::ARRAY); 147 | $constructor->addParameter('metadata') 148 | ->setType(Type::ARRAY) 149 | ->setDefaultValue([]); 150 | $constructor->setBody(<<commandId = \$commandId ?? {$type->commandIdType()}::generate(); 152 | \$this->metadata = \$metadata; 153 | \$this->setPayload(\$payload); 154 | CODE 155 | ); 156 | 157 | $class->addMethod('commandType') 158 | ->setAbstract() 159 | ->setReturnType(Type::STRING); 160 | 161 | $class->addMethod('setPayload') 162 | ->setProtected() 163 | ->setReturnType(Type::VOID) 164 | ->setBody('$this->payload = $payload;') 165 | ->addParameter('payload')->setType(Type::ARRAY); 166 | 167 | $class->addMethod('commandId') 168 | ->setBody('return $this->commandId;') 169 | ->setReturnType($type->commandIdType()); 170 | 171 | $class->addProperty('payload') 172 | ->setProtected() 173 | ->setType(Type::ARRAY); 174 | 175 | $class->addProperty('metadata') 176 | ->setProtected() 177 | ->setType(Type::ARRAY); 178 | 179 | $class->addMethod('payload') 180 | ->setReturnType(Type::ARRAY) 181 | ->setBody('return $this->payload;'); 182 | 183 | $class->addMethod('metadata') 184 | ->setReturnType(Type::ARRAY) 185 | ->setBody('return $this->metadata;'); 186 | 187 | $fromArrayMethod = $class->addMethod('fromArray') 188 | ->setStatic() 189 | ->setReturnType(Type::SELF) 190 | ->setComment('@psalm-suppress MoreSpecificReturnType'); 191 | 192 | $fromArrayMethod->setReturnType(Type::SELF) 193 | ->addParameter('data') 194 | ->setType(Type::ARRAY); 195 | 196 | $fromArrayBody = "switch(\$data['command_type']) {\n"; 197 | 198 | foreach ($type->constructors() as $constructor) { 199 | $fromArrayBody .= " case '" . commandType($constructor, $definition->namespace()) . "':\n"; 200 | $fromArrayBody .= " \$classname = '{$constructor->classname()}';\n"; 201 | $fromArrayBody .= " break;\n"; 202 | } 203 | 204 | $fromArrayBody .= <<commandIdType()}::fromString(\$data['command_id']), 219 | \$data['payload'], 220 | \$data['metadata'] 221 | ); 222 | 223 | CODE; 224 | 225 | $fromArrayMethod->setBody($fromArrayBody); 226 | 227 | $class->addMethod('toArray') 228 | ->setReturnType('array') 229 | ->setBody(<< \$this->commandType(), 232 | 'command_id' => \$this->commandId->toString(), 233 | 'payload' => \$this->payload, 234 | 'metadata' => \$this->metadata, 235 | ]; 236 | 237 | CODE 238 | ); 239 | 240 | $equalsMethod = $class->addMethod('equals') 241 | ->setReturnType(Type::BOOL); 242 | $equalsMethod->setBody(<<commandId->equals(\$other->commandId); 248 | 249 | CODE 250 | ); 251 | 252 | $equalsMethod->addParameter('other') 253 | ->setType(Type::SELF) 254 | ->setNullable(); 255 | 256 | $map = [$fqcn => $file]; 257 | 258 | $constructors = $type->constructors(); 259 | 260 | $singleConstructor = \count($constructors) === 1; 261 | 262 | foreach ($constructors as $constructor) { 263 | /** @var Constructor $constructor */ 264 | if ($constructor->classname() === $type->classname() && ! $singleConstructor) { 265 | throw new \LogicException(\sprintf( 266 | 'Invalid command type: "%s" has a subtype defined with the same name', 267 | $fqcn 268 | )); 269 | } 270 | 271 | $fqcn = $definition->namespace() . '\\' . $constructor->classname(); 272 | $map[$fqcn] = buildSubType($definition, $constructor, $definitions, $config); 273 | } 274 | 275 | return $map; 276 | } 277 | 278 | const fromPhpValue = 'Fpp\Type\Command\fromPhpValue'; 279 | 280 | function fromPhpValue(Command $type, string $paramName): string 281 | { 282 | return $type->classname() . '::fromArray(' . $paramName . ')'; 283 | } 284 | 285 | const toPhpValue = 'Fpp\Type\Command\toPhpValue'; 286 | 287 | function toPhpValue(Command $type, string $paramName): string 288 | { 289 | return $paramName . '->toArray()'; 290 | } 291 | 292 | const validator = 'Fpp\Type\Command\validator'; 293 | 294 | function validator(string $type, string $paramName): string 295 | { 296 | return "\is_array($paramName)"; 297 | } 298 | 299 | const validationErrorMessage = 'Fpp\Type\Command\validationErrorMessage'; 300 | 301 | function validationErrorMessage(string $paramName): string 302 | { 303 | return "Error on \"$paramName\", array expected"; 304 | } 305 | 306 | const equals = 'Fpp\Type\Command\equals'; 307 | 308 | function equals(string $paramName, string $otherParamName): string 309 | { 310 | return "{$paramName}->equals($otherParamName)"; 311 | } 312 | 313 | class Command implements FppType 314 | { 315 | use TypeTrait; 316 | 317 | /** @var list */ 318 | private array $constructors; 319 | 320 | private string $commandIdType; 321 | 322 | /** @param list $constructors */ 323 | public function __construct( 324 | string $classname, 325 | array $markers, 326 | string $commandIdType, 327 | array $constructors 328 | ) { 329 | $this->classname = $classname; 330 | $this->markers = $markers; 331 | $this->commandIdType = $commandIdType; 332 | $this->constructors = $constructors; 333 | } 334 | 335 | /** @return list */ 336 | public function constructors(): array 337 | { 338 | return $this->constructors; 339 | } 340 | 341 | public function commandIdType(): string 342 | { 343 | return $this->commandIdType; 344 | } 345 | } 346 | 347 | class Constructor 348 | { 349 | private string $classname; 350 | private string $commandType; 351 | /** @var list */ 352 | private array $arguments; 353 | 354 | /** @param list $arguments */ 355 | public function __construct(string $classname, string $commandType, array $arguments) 356 | { 357 | $this->classname = $classname; 358 | $this->commandType = $commandType; 359 | $this->arguments = renameDuplicateArgumentNames( 360 | [ 361 | $commandType => 1, 362 | ], 363 | $arguments 364 | ); 365 | } 366 | 367 | public function classname(): string 368 | { 369 | return $this->classname; 370 | } 371 | 372 | public function commandType(): string 373 | { 374 | return $this->commandType; 375 | } 376 | 377 | /** @return list */ 378 | public function arguments(): array 379 | { 380 | return $this->arguments; 381 | } 382 | } 383 | 384 | // helper functions for build 385 | 386 | function buildSubType( 387 | Definition $definition, 388 | Constructor $constructor, 389 | array $definitions, 390 | Configuration $config 391 | ): PhpFile { 392 | $fqcn = $definition->namespace() . '\\' . $constructor->classname(); 393 | 394 | $file = buildDefaultPhpFile($definition, $config); 395 | 396 | $class = $file->addClass($fqcn) 397 | ->setFinal(); 398 | 399 | if ($definition->type()->classname() === $constructor->classname()) { 400 | $class->setImplements($definition->type()->markers()); 401 | } else { 402 | $class->setExtends($definition->type()->classname()); 403 | } 404 | 405 | $class->addProperty('commandType') 406 | ->setType(Type::STRING) 407 | ->setPrivate() 408 | ->setValue(commandType($constructor, $definition->namespace())); 409 | 410 | $class->addMethod('commandType') 411 | ->setReturnType(Type::STRING) 412 | ->setBody('return $this->commandType;'); 413 | 414 | $setPayloadMethod = $class->addMethod('setPayload') 415 | ->setProtected() 416 | ->setReturnType(Type::VOID); 417 | 418 | $setPayloadMethod->addParameter('payload')->setType(Type::ARRAY); 419 | $setPayloadMethodBody = ''; 420 | 421 | \array_map( 422 | function (Argument $a) use ( 423 | $class, 424 | $definition, 425 | $definitions, 426 | $config, 427 | &$setPayloadMethodBody 428 | ) { 429 | $resolvedType = resolveType($a->type(), $definition); 430 | $fromPhpValue = generateFromPhpValueFor($a, '$this->payload', 0, $resolvedType, $definitions, $config); 431 | $setPayloadMethodBody .= generateValidationFor($a, '$payload', $resolvedType, $definitions, $config); 432 | 433 | $psalmAnnotation = ''; 434 | 435 | if ($a->isList()) { 436 | $psalmAnnotation = " /** @psalm-suppress MissingClosureParamType */\n"; 437 | } 438 | 439 | $method = $class->addMethod($a->name()); 440 | $method->setBody($psalmAnnotation . "return $fromPhpValue;"); 441 | 442 | if ($a->isList()) { 443 | if ($a->type()) { 444 | $method->addComment('@return list<' . $a->type() . '>'); 445 | } 446 | $method->setReturnType('array'); 447 | } else { 448 | $method->setReturnType($a->type()); 449 | $method->setReturnNullable($a->nullable()); 450 | } 451 | }, 452 | $constructor->arguments() 453 | ); 454 | 455 | $setPayloadMethodBody .= 'parent::setPayload($payload);'; 456 | $setPayloadMethod->setBody($setPayloadMethodBody); 457 | 458 | return $file; 459 | } 460 | 461 | function commandType(Constructor $constructor, string $namespace): string 462 | { 463 | return empty($constructor->commandType()) 464 | ? $namespace . '\\' . $constructor->classname() 465 | : $constructor->commandType(); 466 | } 467 | -------------------------------------------------------------------------------- /src/Type/Data.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Data; 14 | 15 | use Fpp\Argument; 16 | use function Fpp\assignment; 17 | use function Fpp\buildDefaultPhpFile; 18 | use function Fpp\calculateDefaultValue; 19 | use function Fpp\char; 20 | use Fpp\Configuration; 21 | use function Fpp\constructorSeparator; 22 | use Fpp\Definition; 23 | use function Fpp\generateFromPhpValueFor; 24 | use function Fpp\generateToArrayBodyFor; 25 | use function Fpp\generateValidationFor; 26 | use function Fpp\parseArguments; 27 | use Fpp\Parser; 28 | use function Fpp\plus; 29 | use function Fpp\renameDuplicateArgumentNames; 30 | use function Fpp\resolveType; 31 | use function Fpp\result; 32 | use function Fpp\sepBy1list; 33 | use function Fpp\spaces; 34 | use function Fpp\spaces1; 35 | use function Fpp\string; 36 | use Fpp\Type as FppType; 37 | use function Fpp\Type\Marker\markers; 38 | use Fpp\TypeConfiguration; 39 | use function Fpp\typeName; 40 | use Fpp\TypeTrait; 41 | use Nette\PhpGenerator\PhpFile; 42 | use Nette\PhpGenerator\Type; 43 | 44 | function typeConfiguration(): TypeConfiguration 45 | { 46 | return new TypeConfiguration( 47 | parse, 48 | build, 49 | fromPhpValue, 50 | toPhpValue, 51 | validator, 52 | validationErrorMessage, 53 | equals 54 | ); 55 | } 56 | 57 | const parse = 'Fpp\Type\Data\parse'; 58 | 59 | function parse(): Parser 60 | { 61 | return parseSimplified()->or(parseWitSubTypes()); 62 | } 63 | 64 | const build = 'Fpp\Type\Data\build'; 65 | 66 | function build(Definition $definition, array $definitions, Configuration $config): array 67 | { 68 | $type = $definition->type(); 69 | 70 | if (! $type instanceof Data) { 71 | throw new \InvalidArgumentException('Can only build definitions of ' . Data::class); 72 | } 73 | 74 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 75 | 76 | $file = buildDefaultPhpFile($definition, $config); 77 | 78 | $class = $file->addClass($fqcn) 79 | ->setAbstract() 80 | ->setImplements($type->markers()); 81 | 82 | $fromArrayBody = <<constructors() as $constructor) { 94 | $fromArrayBody .= " case '" . $constructor->className() . "':\n"; 95 | $fromArrayBody .= " \$classname = '{$constructor->classname()}';\n"; 96 | $fromArrayBody .= " break;\n"; 97 | } 98 | 99 | $fromArrayBody .= <<addMethod('fromArray') 117 | ->setStatic() 118 | ->setReturnType(Type::SELF) 119 | ->setBody($fromArrayBody) 120 | ->addParameter('data') 121 | ->setType(Type::ARRAY); 122 | 123 | $class->addMethod('toArray') 124 | ->setReturnType('array') 125 | ->setAbstract(); 126 | 127 | $class->addMethod('equals') 128 | ->setReturnType(Type::BOOL) 129 | ->setAbstract() 130 | ->addParameter('other') 131 | ->setType(Type::SELF) 132 | ->setNullable(); 133 | 134 | $map = [$fqcn => $file]; 135 | 136 | $constructors = $type->constructors(); 137 | 138 | $singleConstructor = \count($constructors) === 1; 139 | 140 | foreach ($constructors as $constructor) { 141 | /** @var Constructor $constructor */ 142 | if ($constructor->classname() === $type->classname() && ! $singleConstructor) { 143 | throw new \LogicException(\sprintf( 144 | 'Invalid data type: "%s" has a subtype defined with the same name', 145 | $fqcn 146 | )); 147 | } 148 | 149 | $fqcn = $definition->namespace() . '\\' . $constructor->classname(); 150 | $map[$fqcn] = buildSubType($definition, $constructor, $definitions, $config); 151 | } 152 | 153 | return $map; 154 | } 155 | 156 | const fromPhpValue = 'Fpp\Type\Data\fromPhpValue'; 157 | 158 | function fromPhpValue(Data $type, string $paramName): string 159 | { 160 | return $type->classname() . '::fromArray(' . $paramName . ')'; 161 | } 162 | 163 | const toPhpValue = 'Fpp\Type\Data\toPhpValue'; 164 | 165 | function toPhpValue(Data $type, string $paramName): string 166 | { 167 | return $paramName . '->toArray()'; 168 | } 169 | 170 | const validator = 'Fpp\Type\Data\validator'; 171 | 172 | function validator(string $type, string $paramName): string 173 | { 174 | return "\is_array($paramName)"; 175 | } 176 | 177 | const validationErrorMessage = 'Fpp\Type\Data\validationErrorMessage'; 178 | 179 | function validationErrorMessage(string $paramName): string 180 | { 181 | return "Error on \"$paramName\", array expected"; 182 | } 183 | 184 | const equals = 'Fpp\Type\Data\equals'; 185 | 186 | function equals(string $paramName, string $otherParamName): string 187 | { 188 | return "{$paramName}->equals($otherParamName)"; 189 | } 190 | 191 | class Data implements FppType 192 | { 193 | use TypeTrait; 194 | 195 | /** @var list */ 196 | private array $constructors; 197 | 198 | /** @param list $constructors */ 199 | public function __construct(string $classname, array $markers, array $constructors) 200 | { 201 | $this->classname = $classname; 202 | $this->markers = $markers; 203 | $this->constructors = $constructors; 204 | } 205 | 206 | /** @return list */ 207 | public function constructors(): array 208 | { 209 | return $this->constructors; 210 | } 211 | } 212 | 213 | class Constructor 214 | { 215 | private string $classname; 216 | /** @var list */ 217 | private array $arguments; 218 | 219 | /** @param list $arguments */ 220 | public function __construct(string $classname, array $arguments) 221 | { 222 | $this->classname = $classname; 223 | $this->arguments = renameDuplicateArgumentNames([], $arguments); 224 | } 225 | 226 | public function classname(): string 227 | { 228 | return $this->classname; 229 | } 230 | 231 | /** @return list */ 232 | public function arguments(): array 233 | { 234 | return $this->arguments; 235 | } 236 | } 237 | 238 | // helper functions for parse 239 | 240 | function parseSimplified(): Parser 241 | { 242 | return for_( 243 | __($_)->_(spaces()), 244 | __($_)->_(string('data')), 245 | __($_)->_(spaces1()), 246 | __($t)->_(typeName()), 247 | __($_)->_(spaces()), 248 | __($ms)->_( 249 | plus(markers(), result([])) 250 | ), 251 | __($_)->_(assignment()), 252 | __($as)->_(parseArguments()), 253 | __($_)->_(spaces()), 254 | __($_)->_(char(';')) 255 | )->call(fn ($t, $ms, $as) => new Data($t, $ms, [new Constructor($t, $as)]), $t, $ms, $as); 256 | } 257 | 258 | function parseWitSubTypes(): Parser 259 | { 260 | return for_( 261 | __($_)->_(spaces()), 262 | __($_)->_(string('data')), 263 | __($_)->_(spaces1()), 264 | __($t)->_(typeName()), 265 | __($_)->_(spaces()), 266 | __($ms)->_( 267 | plus(markers(), result([])) 268 | ), 269 | __($_)->_(assignment()), 270 | __($cs)->_( 271 | sepBy1list( 272 | for_( 273 | __($c)->_(typeName()), 274 | __($_)->_(spaces()), 275 | __($as)->_( 276 | parseArguments() 277 | ) 278 | )->call(fn ($c, $as) => new Constructor($c, $as), $c, $as), 279 | constructorSeparator() 280 | ) 281 | ), 282 | __($_)->_(spaces()), 283 | __($_)->_(char(';')) 284 | )->call(fn ($t, $ms, $cs) => new Data($t, $ms, $cs), $t, $ms, $cs); 285 | } 286 | 287 | // helper functions for build 288 | 289 | function buildSubType( 290 | Definition $definition, 291 | Constructor $constr, 292 | array $definitions, 293 | Configuration $config 294 | ): PhpFile { 295 | $fqcn = $definition->namespace() . '\\' . $constr->classname(); 296 | 297 | $file = buildDefaultPhpFile($definition, $config); 298 | 299 | $class = $file->addClass($fqcn) 300 | ->setFinal(); 301 | 302 | if ($definition->type()->classname() === $constr->classname()) { 303 | $class->setImplements($definition->type()->markers()); 304 | } else { 305 | $class->setExtends($definition->type()->classname()); 306 | } 307 | 308 | $constructor = $class->addMethod('__construct'); 309 | 310 | $constructorBody = ''; 311 | $fromArrayValidationBody = ''; 312 | $fromArrayBody = "return new self(\n"; 313 | $toArrayBody = "return [\n"; 314 | 315 | if (\count($definition->type()->constructors()) > 1) { 316 | $toArrayBody .= " '_type' => '{$constr->classname()}',\n"; 317 | } 318 | 319 | $equalsBody = <<addProperty($a->name())->setPrivate()->setNullable($a->nullable()); 341 | 342 | $resolvedType = resolveType($a->type(), $definition); 343 | $defaultValue = calculateDefaultValue($a); 344 | $fromArrayValidationBody .= generateValidationFor($a, '$data', $resolvedType, $definitions, $config); 345 | $fromArrayBody .= generateFromPhpValueFor($a, '$data', 1, $resolvedType, $definitions, $config) . ",\n"; 346 | $toArrayBody .= generateToArrayBodyFor($a, '$this->', $resolvedType, $definitions, $config); 347 | $equalsBody .= equalsBodyFor($a, $resolvedType, $definitions, $config); 348 | 349 | if (null !== $defaultValue) { 350 | $param = $constructor->addParameter($a->name(), $defaultValue); 351 | } else { 352 | $param = $constructor->addParameter($a->name()); 353 | } 354 | 355 | $param->setNullable($a->nullable()); 356 | 357 | $constructorBody .= "\$this->{$a->name()} = \${$a->name()};\n"; 358 | 359 | $getter = $class->addMethod($a->name()); 360 | $getter->setBody("return \$this->{$a->name()};"); 361 | 362 | $setter = $class->addMethod('with' . \ucfirst($a->name())); 363 | $setter->setReturnType(Type::SELF) 364 | ->addParameter($a->name()) 365 | ->setType($a->isList() ? 'array' : $a->type()) 366 | ->setNullable($a->nullable()); 367 | 368 | $setterBody = "return new self(\n"; 369 | 370 | foreach ($constr->arguments() as $arg) { 371 | /** @var Argument $arg */ 372 | if ($arg->name() === $a->name()) { 373 | $setterBody .= " \${$arg->name()},\n"; 374 | } else { 375 | $setterBody .= " \$this->{$arg->name()},\n"; 376 | } 377 | } 378 | 379 | $setter->setBody(\substr($setterBody, 0, -2) . "\n);"); 380 | 381 | if ($a->isList()) { 382 | $property->setType('array'); 383 | $param->setType('array'); 384 | 385 | if ($a->type()) { 386 | $constructor->addComment('@param list<' . $a->type() . '> $' . $a->name()); 387 | $setter->addComment('@param list<' . $a->type() . '> $' . $a->name()); 388 | $getter->addComment('@return list<' . $a->type() . '>'); 389 | } 390 | $getter->setReturnType('array'); 391 | } else { 392 | $property->setType($a->type()); 393 | $param->setType($a->type()); 394 | $getter->setReturnType($a->type()); 395 | $getter->setReturnNullable($a->nullable()); 396 | } 397 | 398 | if (null !== $a->type() && $a->isList()) { 399 | $property->setType('array'); 400 | $property->addComment('@return list<' . $a->type() . '>'); 401 | } 402 | }, 403 | $constr->arguments() 404 | ); 405 | 406 | $constructor->setBody( 407 | \strlen($constructorBody) > 0 408 | ? \substr($constructorBody, 0, -1) 409 | : $constructorBody 410 | ); 411 | 412 | $fromArrayBody .= ');'; 413 | $toArrayBody .= '];'; 414 | 415 | $fromArray = $class->addMethod('fromArray')->setStatic()->setReturnType(Type::SELF); 416 | $fromArray->addParameter('data')->setType(Type::ARRAY); 417 | $fromArray->setBody($fromArrayValidationBody . $fromArrayBody); 418 | 419 | $toArray = $class->addMethod('toArray')->setReturnType(Type::ARRAY); 420 | $toArray->setBody($toArrayBody); 421 | 422 | $equals = $class->addMethod('equals')->setReturnType(Type::BOOL); 423 | $equals->addParameter('other')->setType($definition->type()->classname())->setNullable(); 424 | $equals->setBody($equalsBody . <<type()) { 436 | case null: 437 | case 'int': 438 | case 'float': 439 | case 'bool': 440 | case 'string': 441 | case 'array': 442 | // yes all above are treated the same 443 | return <<{$a->name()} !== \$other->{$a->name()}) { 446 | return false; 447 | } 448 | 449 | CODE; 450 | default: 451 | $definition = $definitions[$resolvedType] ?? null; 452 | 453 | if (null === $definition) { 454 | /** @var TypeConfiguration|null $typeConfiguration */ 455 | $typeConfiguration = $config->types()[$resolvedType] ?? null; 456 | 457 | if (null === $typeConfiguration) { 458 | $equalsBuilder = fn (string $paramName, string $otherParamName) => "$paramName === $otherParamName->{$a->name()}"; 459 | } else { 460 | $equalsBuilder = $typeConfiguration->equals(); 461 | } 462 | } else { 463 | $equalsBuilder = $config->equalsFor($definition->type()); 464 | } 465 | 466 | if ($a->isList()) { 467 | $equals = $equalsBuilder('$v', '$other->' . $a->name() . '[$k]'); 468 | 469 | return <<{$a->name()}) !== \count(\$other->{$a->name()})) { 472 | return false; 473 | } 474 | 475 | foreach (\$this->{$a->name()} as \$k => \$v) { 476 | if (! $equals) { 477 | return false; 478 | } 479 | } 480 | 481 | CODE; 482 | } 483 | 484 | $equals = $equalsBuilder('$this->' . $a->name(), '$other->' . $a->name()); 485 | 486 | if ($a->nullable()) { 487 | return <<{$a->name()}) { 490 | if (null !== \$other->{$a->name()}) { 491 | return false; 492 | } 493 | } elseif (! $equals) { 494 | return false; 495 | } 496 | 497 | CODE; 498 | } 499 | 500 | return << 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\DateTimeImmutable; 14 | 15 | use Fpp\Type as FppType; 16 | use Fpp\TypeConfiguration; 17 | use Fpp\TypeTrait; 18 | 19 | function typeConfiguration(): TypeConfiguration 20 | { 21 | return new TypeConfiguration( 22 | null, 23 | null, 24 | fromPhpValue, 25 | toPhpValue, 26 | validator, 27 | validationErrorMessage, 28 | equals 29 | ); 30 | } 31 | 32 | const fromPhpValue = 'Fpp\Type\DateTimeImmutable\fromPhpValue'; 33 | 34 | function fromPhpValue(string $type, string $paramName): string 35 | { 36 | if (0 === \strncmp($paramName, '$this->', 7)) { 37 | $import = ''; 38 | } else { 39 | $m = []; 40 | 41 | \preg_match('/^(\$\w+)\[.+$/', $paramName, $m); 42 | 43 | if (empty($m)) { 44 | $shortParamName = $paramName; 45 | } else { 46 | $shortParamName = $m[1]; 47 | } 48 | 49 | $import = " use ($shortParamName) "; 50 | } 51 | 52 | return <<format(\'Y-m-d\TH:i:s.uP\')'; 70 | } 71 | 72 | const validator = 'Fpp\Type\DateTimeImmutable\validator'; 73 | 74 | function validator(string $type, string $paramName): string 75 | { 76 | return << 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Enum; 14 | 15 | use function Fpp\assignment; 16 | use function Fpp\buildDefaultPhpFile; 17 | use function Fpp\char; 18 | use Fpp\Configuration; 19 | use function Fpp\constructorSeparator; 20 | use Fpp\Definition; 21 | use Fpp\Parser; 22 | use function Fpp\plus; 23 | use function Fpp\result; 24 | use function Fpp\sepBy1list; 25 | use function Fpp\spaces; 26 | use function Fpp\spaces1; 27 | use function Fpp\string; 28 | use Fpp\Type as FppType; 29 | use function Fpp\Type\Marker\markers; 30 | use Fpp\TypeConfiguration; 31 | use function Fpp\typeName; 32 | use Fpp\TypeTrait; 33 | use Nette\PhpGenerator\Type; 34 | 35 | function typeConfiguration(): TypeConfiguration 36 | { 37 | return new TypeConfiguration( 38 | parse, 39 | build, 40 | fromPhpValue, 41 | toPhpValue, 42 | validator, 43 | validationErrorMessage, 44 | equals 45 | ); 46 | } 47 | 48 | const parse = 'Fpp\Type\Enum\parse'; 49 | 50 | function parse(): Parser 51 | { 52 | return for_( 53 | __($_)->_(spaces()), 54 | __($_)->_(string('enum')), 55 | __($_)->_(spaces1()), 56 | __($t)->_(typeName()), 57 | __($_)->_(spaces()), 58 | __($ms)->_( 59 | plus(markers(), result([])) 60 | ), 61 | __($_)->_(assignment()), 62 | __($cs)->_( 63 | for_( 64 | __($constructors)->_(sepBy1list(typeName(), constructorSeparator())), 65 | __($_)->_(spaces()), 66 | __($_)->_(char(';')) 67 | )->call( 68 | fn ($c) => \array_map( 69 | fn ($c) => new Constructor($c), 70 | $c 71 | ), 72 | $constructors 73 | ) 74 | ), 75 | )->call(fn ($t, $ms, $cs) => new Enum($t, $ms, $cs), $t, $ms, $cs); 76 | } 77 | 78 | const build = 'Fpp\Type\Enum\build'; 79 | 80 | function build(Definition $definition, array $definitions, Configuration $config): array 81 | { 82 | $type = $definition->type(); 83 | 84 | if (! $type instanceof Enum) { 85 | throw new \InvalidArgumentException('Can only build definitions of ' . Enum::class); 86 | } 87 | 88 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 89 | 90 | $file = buildDefaultPhpFile($definition, $config); 91 | 92 | $class = $file->addClass($fqcn) 93 | ->setFinal() 94 | ->setImplements($type->markers()); 95 | 96 | $options = []; 97 | $i = 0; 98 | 99 | \array_map( 100 | function ($c) use ($class, &$options, &$i) { 101 | $class->addConstant($c->name(), $i)->setPublic(); 102 | 103 | $options[] = $c->name(); 104 | 105 | $method = $class->addMethod($c->name())->setPublic()->setStatic()->setReturnType('self'); 106 | $method->setBody("return new self('{$c->name()}', $i);"); 107 | 108 | ++$i; 109 | }, 110 | $type->constructors() 111 | ); 112 | 113 | $class->addConstant('Options', $options)->setPublic(); 114 | 115 | $class->addProperty('name')->setType(Type::STRING)->setPrivate(); 116 | $class->addProperty('value')->setType(Type::INT)->setPrivate(); 117 | 118 | $constructor = $class->addMethod('__construct')->setPrivate(); 119 | $constructor->addParameter('name')->setType(Type::STRING); 120 | $constructor->addParameter('value')->setType(Type::INT); 121 | $constructor->setBody("\$this->name = \$name;\n\$this->value = \$value;"); 122 | 123 | $method = $class->addMethod('fromName')->setPublic()->setStatic()->setReturnType('self'); 124 | $method->addParameter('name')->setType(Type::STRING); 125 | $method->setBody(<< \$n) { 127 | if (\$n === \$name) { 128 | return new self(\$n, \$i); 129 | } 130 | } 131 | 132 | throw new \InvalidArgumentException('Unknown enum name given'); 133 | CODE 134 | ); 135 | 136 | $method = $class->addMethod('fromValue')->setPublic()->setStatic()->setReturnType('self'); 137 | $method->addParameter('value')->setType(Type::INT); 138 | $method->setBody(<<addMethod('equals')->setPublic()->setReturnType(Type::BOOL); 148 | $method->addParameter('other')->setType(Type::SELF)->setNullable(); 149 | $method->setBody('return null !== $other && $this->name === $other->name;'); 150 | 151 | $method = $class->addMethod('name')->setPublic()->setReturnType(Type::STRING); 152 | $method->setBody('return $this->name;'); 153 | 154 | $method = $class->addMethod('value')->setPublic()->setReturnType(Type::INT); 155 | $method->setBody('return $this->value;'); 156 | 157 | $method = $class->addMethod('__toString')->setPublic()->setReturnType(Type::STRING); 158 | $method->setBody('return $this->name;'); 159 | 160 | $method = $class->addMethod('toString')->setPublic()->setReturnType(Type::STRING); 161 | $method->setBody('return $this->name;'); 162 | 163 | return [$fqcn => $file]; 164 | } 165 | 166 | const fromPhpValue = 'Fpp\Type\Enum\fromPhpValue'; 167 | 168 | function fromPhpValue(Enum $type, string $paramName): string 169 | { 170 | return $type->classname() . '::fromName(' . $paramName . ')'; 171 | } 172 | 173 | const toPhpValue = 'Fpp\Type\Enum\toPhpValue'; 174 | 175 | function toPhpValue(Enum $type, string $paramName): string 176 | { 177 | return $paramName . '->name()'; 178 | } 179 | 180 | const validator = 'Fpp\Type\Enum\validator'; 181 | 182 | function validator(string $type, string $paramName): string 183 | { 184 | return "\is_string($paramName)"; 185 | } 186 | 187 | const validationErrorMessage = 'Fpp\Type\Enum\validationErrorMessage'; 188 | 189 | function validationErrorMessage(string $paramName): string 190 | { 191 | return "Error on \"$paramName\", string expected"; 192 | } 193 | 194 | const equals = 'Fpp\Type\Enum\equals'; 195 | 196 | function equals(string $paramName, string $otherParamName): string 197 | { 198 | return "{$paramName}->equals($otherParamName)"; 199 | } 200 | 201 | class Enum implements FppType 202 | { 203 | use TypeTrait; 204 | 205 | /** @var list */ 206 | private array $constructors; 207 | 208 | /** @param list $constructors */ 209 | public function __construct(string $classname, array $markers, array $constructors) 210 | { 211 | $this->classname = $classname; 212 | $this->markers = $markers; 213 | $this->constructors = $constructors; 214 | } 215 | 216 | /** 217 | * @return list 218 | */ 219 | public function constructors(): array 220 | { 221 | return $this->constructors; 222 | } 223 | } 224 | 225 | class Constructor 226 | { 227 | private string $name; 228 | 229 | public function __construct(string $name) 230 | { 231 | $this->name = $name; 232 | } 233 | 234 | public function name(): string 235 | { 236 | return $this->name; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Type/Event.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Event; 14 | 15 | use Fpp\Argument; 16 | use function Fpp\assignment; 17 | use function Fpp\buildDefaultPhpFile; 18 | use function Fpp\calculateDefaultValue; 19 | use function Fpp\char; 20 | use function Fpp\comma; 21 | use Fpp\Configuration; 22 | use function Fpp\constructorSeparator; 23 | use Fpp\Definition; 24 | use function Fpp\generateFromPhpValueFor; 25 | use function Fpp\generateToArrayBodyFor; 26 | use function Fpp\many1; 27 | use function Fpp\not; 28 | use function Fpp\parseArguments; 29 | use Fpp\Parser; 30 | use function Fpp\plus; 31 | use function Fpp\renameDuplicateArgumentNames; 32 | use function Fpp\resolveType; 33 | use function Fpp\result; 34 | use function Fpp\sepByList; 35 | use function Fpp\spaces; 36 | use function Fpp\spaces1; 37 | use function Fpp\string; 38 | use Fpp\Type as FppType; 39 | use function Fpp\Type\Marker\markers; 40 | use Fpp\TypeConfiguration; 41 | use function Fpp\typeName; 42 | use Fpp\TypeTrait; 43 | use Nette\PhpGenerator\PhpFile; 44 | use Nette\PhpGenerator\Type; 45 | 46 | function typeConfiguration(): TypeConfiguration 47 | { 48 | return new TypeConfiguration( 49 | parse, 50 | build, 51 | fromPhpValue, 52 | toPhpValue, 53 | validator, 54 | validationErrorMessage, 55 | equals 56 | ); 57 | } 58 | 59 | const parse = 'Fpp\Type\Event\parse'; 60 | 61 | function parse(): Parser 62 | { 63 | return for_( 64 | __($_)->_(spaces()), 65 | __($_)->_(string('event')), 66 | __($_)->_(spaces1()), 67 | __($t)->_(typeName()), 68 | __($_)->_(spaces()), 69 | __($ms)->_( 70 | plus(markers(), result([])) 71 | ), 72 | __($_)->_(spaces()), 73 | __($_)->_(char('(')), 74 | __($_)->_(spaces()), 75 | __($eid)->_(typeName()), 76 | __($_)->_(comma()), 77 | __($aid)->_(typeName()), 78 | __($_)->_(spaces()), 79 | __($_)->_(char(')')), 80 | __($_)->_(assignment()), 81 | __($cs)->_( 82 | sepByList( 83 | for_( 84 | __($c)->_(typeName()), 85 | __($_)->_(spaces()), 86 | __($en)->_( 87 | for_( 88 | __($_)->_(string('as')), 89 | __($_)->_(spaces1()), 90 | __($en)->_(many1(not(' '))), 91 | __($_)->_(spaces1()), 92 | )->yields($en) 93 | ->or(result('')) 94 | ), 95 | __($as)->_( 96 | parseArguments()->or(result([])) 97 | ) 98 | )->call(fn ($c, $en, $as) => new Constructor($c, $en, $as), $c, $en, $as), 99 | constructorSeparator() 100 | ) 101 | ), 102 | __($_)->_(spaces()), 103 | __($_)->_(char(';')) 104 | )->call( 105 | fn ($t, $ms, $eid, $aid, $cs) => new Event($t, $ms, $eid, $aid, $cs), 106 | $t, 107 | $ms, 108 | $eid, 109 | $aid, 110 | $cs 111 | ); 112 | } 113 | 114 | const build = 'Fpp\Type\Event\build'; 115 | 116 | function build(Definition $definition, array $definitions, Configuration $config): array 117 | { 118 | $type = $definition->type(); 119 | 120 | if (! $type instanceof Event) { 121 | throw new \InvalidArgumentException('Can only build definitions of ' . Event::class); 122 | } 123 | 124 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 125 | 126 | $file = buildDefaultPhpFile($definition, $config); 127 | 128 | $class = $file->addClass($fqcn) 129 | ->setAbstract() 130 | ->setImplements($type->markers()); 131 | 132 | $class->addProperty('eventId') 133 | ->setProtected() 134 | ->setType($type->eventIdType()); 135 | 136 | $class->addProperty('aggregateId') 137 | ->setProtected() 138 | ->setType($type->aggregateIdType()); 139 | 140 | $constructor = $class->addMethod('__construct') 141 | ->setProtected(); 142 | 143 | $constructor->addParameter('eventId') 144 | ->setType($type->eventIdType()) 145 | ->setNullable(); 146 | $constructor->addParameter('aggregateId') 147 | ->setType($type->aggregateIdType()); 148 | $constructor->addParameter('payload') 149 | ->setType(Type::ARRAY); 150 | $constructor->addParameter('metadata') 151 | ->setType(Type::ARRAY) 152 | ->setDefaultValue([]); 153 | $constructor->setBody(<<eventId = \$eventId ?? {$type->eventIdType()}::generate(); 155 | \$this->aggregateId = \$aggregateId; 156 | \$this->payload = \$payload; 157 | \$this->metadata = \$metadata; 158 | CODE 159 | ); 160 | 161 | $class->addMethod('eventType') 162 | ->setAbstract() 163 | ->setReturnType(Type::STRING); 164 | 165 | $class->addMethod('eventId') 166 | ->setBody('return $this->eventId;') 167 | ->setReturnType($type->eventIdType()); 168 | 169 | $class->addMethod('aggregateId') 170 | ->setBody('return $this->aggregateId;') 171 | ->setReturnType($type->aggregateIdType()); 172 | 173 | $class->addProperty('payload') 174 | ->setProtected() 175 | ->setType(Type::ARRAY); 176 | 177 | $class->addProperty('metadata') 178 | ->setProtected() 179 | ->setType(Type::ARRAY); 180 | 181 | $class->addMethod('payload') 182 | ->setReturnType(Type::ARRAY) 183 | ->setBody('return $this->payload;'); 184 | 185 | $class->addMethod('metadata') 186 | ->setReturnType(Type::ARRAY) 187 | ->setBody('return $this->metadata;'); 188 | 189 | $fromArrayMethod = $class->addMethod('fromArray') 190 | ->setStatic() 191 | ->setReturnType(Type::SELF) 192 | ->setComment('@psalm-suppress MoreSpecificReturnType'); 193 | 194 | $fromArrayMethod->setReturnType(Type::SELF) 195 | ->addParameter('data') 196 | ->setType(Type::ARRAY); 197 | 198 | $fromArrayBody = "switch(\$data['event_type']) {\n"; 199 | 200 | foreach ($type->constructors() as $constructor) { 201 | $fromArrayBody .= " case '" . eventType($constructor, $definition->namespace()) . "':\n"; 202 | $fromArrayBody .= " \$classname = '{$constructor->classname()}';\n"; 203 | $fromArrayBody .= " break;\n"; 204 | } 205 | 206 | $eventIdType = $definitions[resolveType($type->eventIdType(), $definition)] ?? null; 207 | $aggregateIdType = $definitions[resolveType($type->aggregateIdType(), $definition)] ?? null; 208 | 209 | $eventIdFromPhpValue = $config->fromPhpValueFor($eventIdType->type()); 210 | $aggregateIdFromPhpValue = $config->fromPhpValueFor($aggregateIdType->type()); 211 | 212 | $fromArrayBody .= <<type(), '$data[\'event_id\']')}, 227 | {$aggregateIdFromPhpValue($aggregateIdType->type(), '$data[\'aggregate_id\']')}, 228 | \$data['payload'], 229 | \$data['metadata'] 230 | ); 231 | 232 | CODE; 233 | 234 | $eventIdToPhpValue = $config->toPhpValueFor($eventIdType->type()); 235 | $aggregateIdToPhpValue = $config->toPhpValueFor($aggregateIdType->type()); 236 | 237 | $fromArrayMethod->setBody($fromArrayBody); 238 | 239 | $class->addMethod('toArray') 240 | ->setReturnType('array') 241 | ->setBody(<< \$this->eventType(), 244 | 'event_id' => {$eventIdToPhpValue($eventIdType->type(), '$this->eventId')}, 245 | 'aggregate_id' => {$aggregateIdToPhpValue($aggregateIdType->type(), '$this->aggregateId')}, 246 | 'payload' => \$this->payload, 247 | 'metadata' => \$this->metadata, 248 | ]; 249 | 250 | CODE 251 | ); 252 | 253 | $equalsMethod = $class->addMethod('equals') 254 | ->setReturnType(Type::BOOL); 255 | $equalsMethod->setBody(<<eventId->equals(\$other->eventId); 261 | 262 | CODE 263 | ); 264 | 265 | $equalsMethod->addParameter('other') 266 | ->setType(Type::SELF) 267 | ->setNullable(); 268 | 269 | $map = [$fqcn => $file]; 270 | 271 | $constructors = $type->constructors(); 272 | 273 | $singleConstructor = \count($constructors) === 1; 274 | 275 | foreach ($constructors as $constructor) { 276 | /** @var Constructor $constructor */ 277 | if ($constructor->classname() === $type->classname() && ! $singleConstructor) { 278 | throw new \LogicException(\sprintf( 279 | 'Invalid event type: "%s" has a subtype defined with the same name', 280 | $fqcn 281 | )); 282 | } 283 | 284 | $fqcn = $definition->namespace() . '\\' . $constructor->classname(); 285 | $map[$fqcn] = buildSubType($definition, $constructor, $definitions, $config); 286 | } 287 | 288 | return $map; 289 | } 290 | 291 | const fromPhpValue = 'Fpp\Type\Event\fromPhpValue'; 292 | 293 | function fromPhpValue(Event $type, string $paramName): string 294 | { 295 | return $type->classname() . '::fromArray(' . $paramName . ')'; 296 | } 297 | 298 | const toPhpValue = 'Fpp\Type\Event\toPhpValue'; 299 | 300 | function toPhpValue(Event $type, string $paramName): string 301 | { 302 | return $paramName . '->toArray()'; 303 | } 304 | 305 | const validator = 'Fpp\Type\Event\validator'; 306 | 307 | function validator(string $type, string $paramName): string 308 | { 309 | return "\is_array($paramName)"; 310 | } 311 | 312 | const validationErrorMessage = 'Fpp\Type\Event\validationErrorMessage'; 313 | 314 | function validationErrorMessage(string $paramName): string 315 | { 316 | return "Error on \"$paramName\", array expected"; 317 | } 318 | 319 | const equals = 'Fpp\Type\Event\equals'; 320 | 321 | function equals(string $paramName, string $otherParamName): string 322 | { 323 | return "{$paramName}->equals($otherParamName)"; 324 | } 325 | 326 | class Event implements FppType 327 | { 328 | use TypeTrait; 329 | 330 | /** @var list */ 331 | private array $constructors; 332 | 333 | private string $eventIdType; 334 | 335 | private string $aggregateIdType; 336 | 337 | /** @param list $constructors */ 338 | public function __construct( 339 | string $classname, 340 | array $markers, 341 | string $eventIdType, 342 | string $aggregateIdType, 343 | array $constructors 344 | ) { 345 | $this->classname = $classname; 346 | $this->markers = $markers; 347 | $this->eventIdType = $eventIdType; 348 | $this->aggregateIdType = $aggregateIdType; 349 | $this->constructors = $constructors; 350 | } 351 | 352 | /** @return list */ 353 | public function constructors(): array 354 | { 355 | return $this->constructors; 356 | } 357 | 358 | public function eventIdType(): string 359 | { 360 | return $this->eventIdType; 361 | } 362 | 363 | public function aggregateIdType(): string 364 | { 365 | return $this->aggregateIdType; 366 | } 367 | } 368 | 369 | class Constructor 370 | { 371 | private string $classname; 372 | private string $eventType; 373 | /** @var list */ 374 | private array $arguments; 375 | 376 | /** @param list $arguments */ 377 | public function __construct(string $classname, string $eventType, array $arguments) 378 | { 379 | $this->classname = $classname; 380 | $this->eventType = $eventType; 381 | 382 | $this->arguments = renameDuplicateArgumentNames( 383 | [ 384 | $eventType => 1, 385 | ], 386 | $arguments 387 | ); 388 | } 389 | 390 | public function classname(): string 391 | { 392 | return $this->classname; 393 | } 394 | 395 | public function eventType(): string 396 | { 397 | return $this->eventType; 398 | } 399 | 400 | /** @return list */ 401 | public function arguments(): array 402 | { 403 | return $this->arguments; 404 | } 405 | } 406 | 407 | // helper functions for build 408 | 409 | function buildSubType( 410 | Definition $definition, 411 | Constructor $constructor, 412 | array $definitions, 413 | Configuration $config 414 | ): PhpFile { 415 | $fqcn = $definition->namespace() . '\\' . $constructor->classname(); 416 | 417 | $file = buildDefaultPhpFile($definition, $config); 418 | 419 | $class = $file->addClass($fqcn) 420 | ->setFinal(); 421 | 422 | if ($definition->type()->classname() === $constructor->classname()) { 423 | $class->setImplements($definition->type()->markers()); 424 | } else { 425 | $class->setExtends($definition->type()->classname()); 426 | } 427 | 428 | $class->addProperty('eventType') 429 | ->setType(Type::STRING) 430 | ->setPrivate() 431 | ->setValue(eventType($constructor, $definition->namespace())); 432 | 433 | $class->addMethod('eventType') 434 | ->setReturnType(Type::STRING) 435 | ->setBody('return $this->eventType;'); 436 | 437 | $occur = $class->addMethod('occur') 438 | ->setStatic() 439 | ->setReturnType(Type::SELF); 440 | 441 | /** @var Event $event */ 442 | $event = $definition->type(); 443 | 444 | $occur->addParameter('aggregateId') 445 | ->setType($event->aggregateIdType()); 446 | 447 | $occurBody = <<addProperty($a->name())->setPrivate()->setNullable()->setInitialized(); 467 | 468 | $resolvedType = resolveType($a->type(), $definition); 469 | $defaultValue = calculateDefaultValue($a); 470 | $fromPhpValue = generateFromPhpValueFor($a, '$this->payload', 0, $resolvedType, $definitions, $config); 471 | $toPhpValue = generateToArrayBodyFor($a, '$', $resolvedType, $definitions, $config); 472 | 473 | if (null !== $defaultValue) { 474 | $param = $occur->addParameter($a->name(), $defaultValue); 475 | } else { 476 | $param = $occur->addParameter($a->name()); 477 | } 478 | 479 | $param->setNullable($a->nullable()); 480 | 481 | $occurBody .= " $toPhpValue"; 482 | $occurBody2 .= "\$_event->{$a->name()} = \${$a->name()};\n"; 483 | 484 | $psalmAnnotation = ''; 485 | 486 | if ($a->isList()) { 487 | $psalmAnnotation = ' /** @psalm-suppress MissingClosureParamType */'; 488 | } 489 | 490 | $method = $class->addMethod($a->name()); 491 | $method->setBody(<<{$a->name()}) { 493 | $psalmAnnotation 494 | \$this->{$a->name()} = $fromPhpValue; 495 | } 496 | 497 | return \$this->{$a->name()}; 498 | 499 | CODE 500 | ); 501 | 502 | if ($a->isList()) { 503 | $property->setType('array'); 504 | $param->setType('array'); 505 | 506 | if ($a->type()) { 507 | $occur->addComment('@param list<' . $a->type() . '> $' . $a->name()); 508 | $method->addComment('@return list<' . $a->type() . '>'); 509 | } 510 | $method->setReturnType('array'); 511 | } else { 512 | $property->setType($a->type()); 513 | $param->setType($a->type()); 514 | $method->setReturnType($a->type()); 515 | $method->setReturnNullable($a->nullable()); 516 | } 517 | 518 | if (null !== $a->type() && $a->isList()) { 519 | $property->setType('array'); 520 | $property->addComment('@var null|list<' . $a->type() . '>'); 521 | } 522 | }, 523 | $constructor->arguments() 524 | ); 525 | 526 | $occurBody .= " ]\n);\n"; 527 | 528 | if ($occurBody2 !== '') { 529 | $occurBody .= "\n$occurBody2"; 530 | } 531 | 532 | $occurBody .= "\nreturn \$_event;"; 533 | 534 | $occur->setBody($occurBody); 535 | 536 | return $file; 537 | } 538 | 539 | function eventType(Constructor $constructor, string $namespace): string 540 | { 541 | return empty($constructor->eventType()) 542 | ? $namespace . '\\' . $constructor->classname() 543 | : $constructor->eventType(); 544 | } 545 | -------------------------------------------------------------------------------- /src/Type/Float_.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Float_; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\Float_\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('float')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new Float_($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\Float_\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof Float_) { 70 | throw new \InvalidArgumentException('Can only build definitions of ' . Float_::class); 71 | } 72 | 73 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 74 | 75 | $file = buildDefaultPhpFile($definition, $config); 76 | 77 | $class = $file->addClass($fqcn) 78 | ->setFinal() 79 | ->setImplements($type->markers()); 80 | 81 | $class->addProperty('value')->setType(Type::FLOAT)->setPrivate(); 82 | 83 | $constructor = $class->addMethod('__construct'); 84 | $constructor->addParameter('value')->setType(Type::FLOAT); 85 | $constructor->setBody('$this->value = $value;'); 86 | 87 | $method = $class->addMethod('value')->setReturnType(Type::FLOAT); 88 | $method->setBody('return $this->value;'); 89 | 90 | $method = $class->addMethod('equals')->setPublic()->setReturnType(Type::BOOL); 91 | $method->addParameter('other')->setType(Type::SELF)->setNullable(); 92 | $method->setBody('return null !== $other && $this->value === $other->value;'); 93 | 94 | return [$fqcn => $file]; 95 | } 96 | 97 | const fromPhpValue = 'Fpp\Type\Float_\fromPhpValue'; 98 | 99 | function fromPhpValue(Float_ $type, string $paramName): string 100 | { 101 | return 'new ' . $type->classname() . '(' . $paramName . ')'; 102 | } 103 | 104 | const toPhpValue = 'Fpp\Type\Float_\toPhpValue'; 105 | 106 | function toPhpValue(Float_ $type, string $paramName): string 107 | { 108 | return $paramName . '->value()'; 109 | } 110 | 111 | const validator = 'Fpp\Type\Float_\validator'; 112 | 113 | function validator(string $type, string $paramName): string 114 | { 115 | return "\is_float($paramName)"; 116 | } 117 | 118 | const validationErrorMessage = 'Fpp\Type\Float_\validationErrorMessage'; 119 | 120 | function validationErrorMessage(string $paramName): string 121 | { 122 | return "Error on \"$paramName\", float expected"; 123 | } 124 | 125 | const equals = 'Fpp\Type\Float_\equals'; 126 | 127 | function equals(string $paramName, string $otherParamName): string 128 | { 129 | return "{$paramName}->equals($otherParamName)"; 130 | } 131 | 132 | class Float_ implements FppType 133 | { 134 | use TypeTrait; 135 | } 136 | -------------------------------------------------------------------------------- /src/Type/Guid.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Guid; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\Guid\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('guid')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new Guid($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\Guid\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof Guid) { 70 | throw new \InvalidArgumentException('Can only build definitions of ' . Guid::class); 71 | } 72 | 73 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 74 | 75 | $file = buildDefaultPhpFile($definition, $config); 76 | 77 | $class = $file->addClass($fqcn) 78 | ->setFinal() 79 | ->setImplements($type->markers()); 80 | 81 | $namespace = $file->getNamespaces()[$definition->namespace()]; 82 | $namespace->addUse('Ramsey\Uuid\UuidFactory'); 83 | $namespace->addUse('Ramsey\Uuid\FeatureSet'); 84 | $namespace->addUse('Ramsey\Uuid\UuidInterface'); 85 | 86 | $class->addProperty('uuid')->setType('UuidInterface')->setPrivate(); 87 | $class->addProperty('factory')->setType('UuidFactory')->setNullable()->setStatic()->setPrivate()->setInitialized(); 88 | 89 | $constructor = $class->addMethod('__construct'); 90 | $constructor->addParameter('uuid')->setType('UuidInterface'); 91 | $constructor->setBody('$this->uuid = $uuid;'); 92 | $constructor->setPrivate(); 93 | 94 | $generate = $class->addMethod('generate')->setReturnType('self'); 95 | $generate->setBody('return new self(self::factory()->uuid4());'); 96 | $generate->setStatic(); 97 | 98 | $fromString = $class->addMethod('fromString')->setReturnType('self'); 99 | $fromString->addParameter('uuid')->setType('string'); 100 | $fromString->setBody('return new self(self::factory()->fromString($uuid));'); 101 | $fromString->setStatic(); 102 | 103 | $fromBinary = $class->addMethod('fromBinary')->setReturnType('self'); 104 | $fromBinary->addParameter('bytes')->setType('string'); 105 | $fromBinary->setBody('return new self(self::factory()->fromBytes($bytes));'); 106 | $fromBinary->setStatic(); 107 | 108 | $toString = $class->addMethod('toString')->setReturnType(Type::STRING); 109 | $toString->setBody('return $this->uuid->toString();'); 110 | 111 | $__toString = $class->addMethod('__toString')->setReturnType(Type::STRING); 112 | $__toString->setBody('return $this->uuid->toString();'); 113 | 114 | $toBinary = $class->addMethod('toBinary')->setReturnType(Type::STRING); 115 | $toBinary->setBody('return $this->uuid->getBytes();'); 116 | 117 | $equals = $class->addMethod('equals')->setReturnType(Type::BOOL); 118 | $equals->addParameter('other')->setType('self')->setNullable(); 119 | $equals->setBody('return null !== $other && $this->uuid->equals($other->uuid);'); 120 | 121 | $factory = $class->addMethod('factory')->setReturnType('UuidFactory'); 122 | $factory->setPrivate()->setStatic(); 123 | $factory->setBody(<< $file]; 133 | } 134 | 135 | const fromPhpValue = 'Fpp\Type\Guid\fromPhpValue'; 136 | 137 | function fromPhpValue(Guid $type, string $paramName): string 138 | { 139 | return $type->classname() . '::fromString(' . $paramName . ')'; 140 | } 141 | 142 | const toPhpValue = 'Fpp\Type\Guid\toPhpValue'; 143 | 144 | function toPhpValue(Guid $type, string $paramName): string 145 | { 146 | return $paramName . '->toString()'; 147 | } 148 | 149 | const validator = 'Fpp\Type\Guid\validator'; 150 | 151 | function validator(string $type, string $paramName): string 152 | { 153 | return "\is_string($paramName)"; 154 | } 155 | 156 | const validationErrorMessage = 'Fpp\Type\Guid\validationErrorMessage'; 157 | 158 | function validationErrorMessage(string $paramName): string 159 | { 160 | return "Error on \"$paramName\", string expected"; 161 | } 162 | 163 | const equals = 'Fpp\Type\Guid\equals'; 164 | 165 | function equals(string $paramName, string $otherParamName): string 166 | { 167 | return "{$paramName}->equals($otherParamName)"; 168 | } 169 | 170 | class Guid implements FppType 171 | { 172 | use TypeTrait; 173 | } 174 | -------------------------------------------------------------------------------- /src/Type/Int_.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Int_; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\Int_\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('int')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new Int_($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\Int_\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof Int_) { 70 | throw new \InvalidArgumentException('Can only build definitions of ' . Int_::class); 71 | } 72 | 73 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 74 | 75 | $file = buildDefaultPhpFile($definition, $config); 76 | 77 | $class = $file->addClass($fqcn) 78 | ->setFinal() 79 | ->setImplements($type->markers()); 80 | 81 | $class->addProperty('value')->setType(Type::INT)->setPrivate(); 82 | 83 | $constructor = $class->addMethod('__construct'); 84 | $constructor->addParameter('value')->setType(Type::INT); 85 | $constructor->setBody('$this->value = $value;'); 86 | 87 | $method = $class->addMethod('value')->setReturnType(Type::INT); 88 | $method->setBody('return $this->value;'); 89 | 90 | $method = $class->addMethod('equals')->setPublic()->setReturnType(Type::BOOL); 91 | $method->addParameter('other')->setType(Type::SELF)->setNullable(); 92 | $method->setBody('return null !== $other && $this->value === $other->value;'); 93 | 94 | return [$fqcn => $file]; 95 | } 96 | 97 | const fromPhpValue = 'Fpp\Type\Int_\fromPhpValue'; 98 | 99 | function fromPhpValue(Int_ $type, string $paramName): string 100 | { 101 | return 'new ' . $type->classname() . '(' . $paramName . ')'; 102 | } 103 | 104 | const toPhpValue = 'Fpp\Type\Int_\toPhpValue'; 105 | 106 | function toPhpValue(Int_ $type, string $paramName): string 107 | { 108 | return $paramName . '->value()'; 109 | } 110 | 111 | const validator = 'Fpp\Type\Int_\validator'; 112 | 113 | function validator(string $type, string $paramName): string 114 | { 115 | return "\is_int($paramName)"; 116 | } 117 | 118 | const validationErrorMessage = 'Fpp\Type\Int_\validationErrorMessage'; 119 | 120 | function validationErrorMessage(string $paramName): string 121 | { 122 | return "Error on \"$paramName\", int expected"; 123 | } 124 | 125 | const equals = 'Fpp\Type\Int_\equals'; 126 | 127 | function equals(string $paramName, string $otherParamName): string 128 | { 129 | return "{$paramName}->equals($otherParamName)"; 130 | } 131 | 132 | class Int_ implements FppType 133 | { 134 | use TypeTrait; 135 | } 136 | -------------------------------------------------------------------------------- /src/Type/Marker.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Marker; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use function Fpp\comma; 18 | use Fpp\Configuration; 19 | use Fpp\Definition; 20 | use Fpp\Parser; 21 | use function Fpp\plus; 22 | use function Fpp\sepByList; 23 | use function Fpp\spaces; 24 | use function Fpp\spaces1; 25 | use function Fpp\string; 26 | use Fpp\Type as FppType; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | 31 | function typeConfiguration(): TypeConfiguration 32 | { 33 | return new TypeConfiguration( 34 | parse, 35 | build, 36 | null, 37 | null, 38 | null, 39 | null, 40 | null 41 | ); 42 | } 43 | 44 | const parse = 'Fpp\Type\Marker\parse'; 45 | 46 | function parse(): Parser 47 | { 48 | return plus( 49 | for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('marker')), 52 | __($_)->_(spaces1()), 53 | __($m)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($_)->_(char(';')) 56 | )->call(fn ($m) => new Marker($m, []), $m), 57 | for_( 58 | __($_)->_(spaces()), 59 | __($_)->_(string('marker')), 60 | __($_)->_(spaces1()), 61 | __($m)->_(typeName()), 62 | __($_)->_(spaces()), 63 | __($_)->_(char(':')), 64 | __($_)->_(spaces()), 65 | __($p)->_(sepByList(typeName(), comma())), 66 | __($_)->_(spaces()), 67 | __($_)->_(char(';')) 68 | )->call(fn ($m, $p) => new Marker($m, $p), $m, $p), 69 | ); 70 | } 71 | 72 | const markers = 'Fpp\Type\Marker\markers'; 73 | 74 | function markers(): Parser 75 | { 76 | return for_( 77 | __($_)->_(char(':')), 78 | __($_)->_(spaces()), 79 | __($ms)->_(sepByList(typeName(), comma())), 80 | )->yields($ms); 81 | } 82 | 83 | const build = 'Fpp\Type\Marker\build'; 84 | 85 | function build(Definition $definition, array $definitions, Configuration $config): array 86 | { 87 | $type = $definition->type(); 88 | 89 | if (! $type instanceof Marker) { 90 | throw new \InvalidArgumentException('Can only build definitions of ' . Marker::class); 91 | } 92 | 93 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 94 | 95 | $file = buildDefaultPhpFile($definition, $config); 96 | 97 | $file->addClass($fqcn) 98 | ->setInterface() 99 | ->setExtends($type->markers()); 100 | 101 | return [$fqcn => $file]; 102 | } 103 | 104 | class Marker implements FppType 105 | { 106 | use TypeTrait; 107 | } 108 | -------------------------------------------------------------------------------- /src/Type/String_.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\String_; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\String_\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('string')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new String_($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\String_\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof String_) { 70 | throw new \InvalidArgumentException('Can only build definitions of ' . String_::class); 71 | } 72 | 73 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 74 | 75 | $file = buildDefaultPhpFile($definition, $config); 76 | 77 | $class = $file->addClass($fqcn) 78 | ->setFinal() 79 | ->setImplements($type->markers()); 80 | 81 | $class->addProperty('value')->setType(Type::STRING)->setPrivate(); 82 | 83 | $constructor = $class->addMethod('__construct'); 84 | $constructor->addParameter('value')->setType(Type::STRING); 85 | $constructor->setBody('$this->value = $value;'); 86 | 87 | $method = $class->addMethod('value')->setReturnType(Type::STRING); 88 | $method->setBody('return $this->value;'); 89 | 90 | $method = $class->addMethod('equals')->setPublic()->setReturnType(Type::BOOL); 91 | $method->addParameter('other')->setType(Type::SELF)->setNullable(); 92 | $method->setBody('return null !== $other && $this->value === $other->value;'); 93 | 94 | return [$fqcn => $file]; 95 | } 96 | 97 | const fromPhpValue = 'Fpp\Type\String_\fromPhpValue'; 98 | 99 | function fromPhpValue(String_ $type, string $paramName): string 100 | { 101 | return 'new ' . $type->classname() . '(' . $paramName . ')'; 102 | } 103 | 104 | const toPhpValue = 'Fpp\Type\String_\toPhpValue'; 105 | 106 | function toPhpValue(String_ $type, string $paramName): string 107 | { 108 | return $paramName . '->value()'; 109 | } 110 | 111 | const validator = 'Fpp\Type\String_\validator'; 112 | 113 | function validator(string $type, string $paramName): string 114 | { 115 | return "\is_string($paramName)"; 116 | } 117 | 118 | const validationErrorMessage = 'Fpp\Type\String_\validationErrorMessage'; 119 | 120 | function validationErrorMessage(string $paramName): string 121 | { 122 | return "Error on \"$paramName\", string expected"; 123 | } 124 | 125 | const equals = 'Fpp\Type\String_\equals'; 126 | 127 | function equals(string $paramName, string $otherParamName): string 128 | { 129 | return "{$paramName}->equals($otherParamName)"; 130 | } 131 | 132 | class String_ implements FppType 133 | { 134 | use TypeTrait; 135 | } 136 | -------------------------------------------------------------------------------- /src/Type/Uuid.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp\Type\Uuid; 14 | 15 | use function Fpp\buildDefaultPhpFile; 16 | use function Fpp\char; 17 | use Fpp\Configuration; 18 | use Fpp\Definition; 19 | use Fpp\Parser; 20 | use function Fpp\plus; 21 | use function Fpp\result; 22 | use function Fpp\spaces; 23 | use function Fpp\spaces1; 24 | use function Fpp\string; 25 | use Fpp\Type as FppType; 26 | use function Fpp\Type\Marker\markers; 27 | use Fpp\TypeConfiguration; 28 | use function Fpp\typeName; 29 | use Fpp\TypeTrait; 30 | use Nette\PhpGenerator\Type; 31 | 32 | function typeConfiguration(): TypeConfiguration 33 | { 34 | return new TypeConfiguration( 35 | parse, 36 | build, 37 | fromPhpValue, 38 | toPhpValue, 39 | validator, 40 | validationErrorMessage, 41 | equals 42 | ); 43 | } 44 | 45 | const parse = 'Fpp\Type\Uuid\parse'; 46 | 47 | function parse(): Parser 48 | { 49 | return for_( 50 | __($_)->_(spaces()), 51 | __($_)->_(string('uuid')), 52 | __($_)->_(spaces1()), 53 | __($t)->_(typeName()), 54 | __($_)->_(spaces()), 55 | __($ms)->_( 56 | plus(markers(), result([])) 57 | ), 58 | __($_)->_(spaces()), 59 | __($_)->_(char(';')) 60 | )->call(fn ($t, $ms) => new Uuid($t, $ms), $t, $ms); 61 | } 62 | 63 | const build = 'Fpp\Type\Uuid\build'; 64 | 65 | function build(Definition $definition, array $definitions, Configuration $config): array 66 | { 67 | $type = $definition->type(); 68 | 69 | if (! $type instanceof Uuid) { 70 | throw new \InvalidArgumentException('Can only build definitions of ' . Uuid::class); 71 | } 72 | 73 | $fqcn = $definition->namespace() . '\\' . $type->classname(); 74 | 75 | $file = buildDefaultPhpFile($definition, $config); 76 | 77 | $class = $file->addClass($fqcn) 78 | ->setFinal() 79 | ->setImplements($type->markers()); 80 | 81 | $namespace = $file->getNamespaces()[$definition->namespace()]; 82 | $namespace->addUse('Ramsey\Uuid\Uuid'); 83 | $namespace->addUse('Ramsey\Uuid\UuidInterface'); 84 | 85 | $class->addProperty('uuid')->setType('UuidInterface')->setPrivate(); 86 | 87 | $constructor = $class->addMethod('__construct'); 88 | $constructor->addParameter('uuid')->setType('UuidInterface'); 89 | $constructor->setBody('$this->uuid = $uuid;'); 90 | $constructor->setPrivate(); 91 | 92 | $generate = $class->addMethod('generate')->setReturnType('self'); 93 | $generate->setBody('return new self(Uuid::uuid4());'); 94 | $generate->setStatic(); 95 | 96 | $fromString = $class->addMethod('fromString')->setReturnType('self'); 97 | $fromString->addParameter('uuid')->setType('string'); 98 | $fromString->setBody('return new self(Uuid::fromString($uuid));'); 99 | $fromString->setStatic(); 100 | 101 | $fromBinary = $class->addMethod('fromBinary')->setReturnType('self'); 102 | $fromBinary->addParameter('bytes')->setType('string'); 103 | $fromBinary->setBody('return new self(Uuid::fromBytes($bytes));'); 104 | $fromBinary->setStatic(); 105 | 106 | $toString = $class->addMethod('toString')->setReturnType(Type::STRING); 107 | $toString->setBody('return $this->uuid->toString();'); 108 | 109 | $__toString = $class->addMethod('__toString')->setReturnType(Type::STRING); 110 | $__toString->setBody('return $this->uuid->toString();'); 111 | 112 | $toBinary = $class->addMethod('toBinary')->setReturnType(Type::STRING); 113 | $toBinary->setBody('return $this->uuid->getBytes();'); 114 | 115 | $equals = $class->addMethod('equals')->setReturnType(Type::BOOL); 116 | $equals->addParameter('other')->setType('self')->setNullable(); 117 | $equals->setBody('return null !== $other && $this->uuid->equals($other->uuid);'); 118 | 119 | return [$fqcn => $file]; 120 | } 121 | 122 | const fromPhpValue = 'Fpp\Type\Uuid\fromPhpValue'; 123 | 124 | function fromPhpValue(Uuid $type, string $paramName): string 125 | { 126 | return $type->classname() . '::fromString(' . $paramName . ')'; 127 | } 128 | 129 | const toPhpValue = 'Fpp\Type\Uuid\toPhpValue'; 130 | 131 | function toPhpValue(Uuid $type, string $paramName): string 132 | { 133 | return $paramName . '->toString()'; 134 | } 135 | 136 | const validator = 'Fpp\Type\Uuid\validator'; 137 | 138 | function validator(string $type, string $paramName): string 139 | { 140 | return "\is_string($paramName)"; 141 | } 142 | 143 | const validationErrorMessage = 'Fpp\Type\Uuid\validationErrorMessage'; 144 | 145 | function validationErrorMessage(string $paramName): string 146 | { 147 | return "Error on \"$paramName\", string expected"; 148 | } 149 | 150 | const equals = 'Fpp\Type\Uuid\equals'; 151 | 152 | function equals(string $paramName, string $otherParamName): string 153 | { 154 | return "{$paramName}->equals($otherParamName)"; 155 | } 156 | 157 | class Uuid implements FppType 158 | { 159 | use TypeTrait; 160 | } 161 | -------------------------------------------------------------------------------- /src/TypeConfiguration.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | use Closure; 16 | 17 | class TypeConfiguration 18 | { 19 | private ?Closure $parse; 20 | private ?Closure $build; 21 | private ?Closure $fromPhpValue; 22 | private ?Closure $toPhpValue; 23 | private ?Closure $validator; 24 | private ?Closure $validationErrorMessage; 25 | private ?Closure $equals; 26 | 27 | public function __construct( 28 | ?callable $parse, 29 | ?callable $build, 30 | ?callable $fromPhpValue, 31 | ?callable $toPhpValue, 32 | ?callable $validator, 33 | ?callable $validationErrorMessage, 34 | ?callable $equals 35 | ) { 36 | $this->parse = $parse ? Closure::fromCallable($parse) : null; 37 | $this->build = $build ? Closure::fromCallable($build) : null; 38 | $this->fromPhpValue = $fromPhpValue ? Closure::fromCallable($fromPhpValue) : null; 39 | $this->toPhpValue = $toPhpValue ? Closure::fromCallable($toPhpValue) : null; 40 | $this->validator = $validator ? Closure::fromCallable($validator) : null; 41 | $this->validationErrorMessage = $validationErrorMessage ? Closure::fromCallable($validationErrorMessage) : null; 42 | $this->equals = $equals ? Closure::fromCallable($equals) : null; 43 | } 44 | 45 | public function parse(): ?Closure 46 | { 47 | return $this->parse; 48 | } 49 | 50 | public function build(): ?Closure 51 | { 52 | return $this->build; 53 | } 54 | 55 | public function fromPhpValue(): ?Closure 56 | { 57 | return $this->fromPhpValue; 58 | } 59 | 60 | public function toPhpValue(): ?Closure 61 | { 62 | return $this->toPhpValue; 63 | } 64 | 65 | public function validator(): ?Closure 66 | { 67 | return $this->validator; 68 | } 69 | 70 | public function validationErrorMessage(): ?Closure 71 | { 72 | return $this->validationErrorMessage; 73 | } 74 | 75 | public function equals(): ?Closure 76 | { 77 | return $this->equals; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/TypeTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Fpp; 14 | 15 | trait TypeTrait 16 | { 17 | private string $classname; 18 | private array $markers; 19 | 20 | public function __construct(string $classname, array $markers) 21 | { 22 | $this->classname = $classname; 23 | $this->markers = $markers; 24 | } 25 | 26 | public function classname(): string 27 | { 28 | return $this->classname; 29 | } 30 | 31 | public function markers(): array 32 | { 33 | return $this->markers; 34 | } 35 | } 36 | --------------------------------------------------------------------------------