├── .gitattributes ├── hh_autoload.json ├── .gitignore ├── CODE_OF_CONDUCT.md ├── src ├── interfaces │ ├── SupportsGetRequests.hack │ ├── SupportsPostRequests.hack │ └── IncludeInUriMap.hack ├── privateimpl │ ├── TestsBypassVisibility.hack │ ├── RequestParameterRequirementState.hack │ ├── ClassFacts.hack │ └── ControllerFacts.hack ├── uribuilder │ ├── UriBuilderCodegen.hack │ ├── UriBuilderCodegenWithStandardUriBuilder.hack │ └── UriBuilderCodegenBase.hack ├── uriparameters │ ├── RequestParametersCodegen.hack │ ├── spec │ │ ├── UriParameterCodegenSpec.hack │ │ ├── IntParameterCodegenSpec.hack │ │ ├── StringParameterCodegenSpec.hack │ │ ├── RequestParameterCodegenSpec.hack │ │ ├── UriParameterCodegenArgumentSpec.hack │ │ ├── SimpleParameterCodegenSpec.hack │ │ └── EnumParameterCodegenSpec.hack │ ├── RequestParametersCodegenBase.hack │ └── RequestParameterCodegenBuilder.hack ├── UriMapBuilder.hack ├── RequestParametersCodegenBuilderBase.hack ├── RouterCodegenBuilder.hack ├── UriBuilderCodegenBuilder.hack ├── RequestParametersCodegenBuilder.hack ├── RouterCLILookupCodegenBuilder.hack └── Codegen.hack ├── hhast-lint.json ├── .github └── workflows │ └── build-and-test.yml ├── .devcontainer └── devcontainer.json ├── LICENSE ├── composer.json ├── CONTRIBUTING.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | tests/ export-ignore 2 | examples/ export-ignore 3 | .hhconfig export-ignore 4 | .hhvmconfig.hdf export-ignore 5 | -------------------------------------------------------------------------------- /hh_autoload.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "src/" 4 | ], 5 | "devRoots": [ 6 | "tests/" 7 | ], 8 | "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler", 9 | "useFactsIfAvailable": true 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .var 3 | *.swp 4 | www.pid 5 | .DS_Store 6 | composer.lock 7 | .*.hhast.*cache 8 | 9 | # Elastic Beanstalk Files 10 | .elasticbeanstalk/* 11 | !.elasticbeanstalk/*.cfg.yml 12 | !.elasticbeanstalk/*.global.yml 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | -------------------------------------------------------------------------------- /src/interfaces/SupportsGetRequests.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | interface SupportsGetRequests extends HasUriPattern { 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/SupportsPostRequests.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | interface SupportsPostRequests extends HasUriPattern { 13 | } 14 | -------------------------------------------------------------------------------- /src/privateimpl/TestsBypassVisibility.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | final class TestsBypassVisibility implements \HH\MethodAttribute { 13 | } 14 | -------------------------------------------------------------------------------- /src/uribuilder/UriBuilderCodegen.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class UriBuilderCodegen 13 | extends UriBuilderCodegenWithStandardUriBuilder { 14 | } 15 | -------------------------------------------------------------------------------- /src/uriparameters/RequestParametersCodegen.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class RequestParametersCodegen 13 | extends RequestParametersCodegenBase { 14 | } 15 | -------------------------------------------------------------------------------- /src/privateimpl/RequestParameterRequirementState.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter\PrivateImpl; 11 | 12 | enum RequestParameterRequirementState: int { 13 | IS_REQUIRED = 1; 14 | IS_OPTIONAL = 0; 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/IncludeInUriMap.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | /** Any non-abstract classes implementing this inteface will be added to 13 | * the generated URI map. They 14 | */ 15 | interface IncludeInUriMap extends HasUriPattern { 16 | } 17 | -------------------------------------------------------------------------------- /src/uriparameters/spec/UriParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class UriParameterCodegenSpec extends RequestParameterCodegenSpec { 13 | abstract public static function getSetterSpec( 14 | UriParameter $param, 15 | ): self::TSpec; 16 | } 17 | -------------------------------------------------------------------------------- /hhast-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ "src/", "tests/" ], 3 | "builtinLinters": "all", 4 | "disabledLinters": [ 5 | "Facebook\\HHAST\\Linters\\CamelCasedMethodsUnderscoredFunctionsLinter", 6 | "Facebook\\HHAST\\Linters\\UseStatementWithAsLinter" 7 | ], 8 | "overrides": [ 9 | { 10 | "patterns": [ "tests/examples/codegen/*" ], 11 | "disableAllAutoFixes": true 12 | }, 13 | { 14 | "patterns": [ "tests/examples/codegen/lookup-path.php" ], 15 | "disabledLinters": [ 16 | "Facebook\\HHAST\\Linters\\StrictModeOnlyLinter" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/uriparameters/RequestParametersCodegenBase.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class RequestParametersCodegenBase<+T as RequestParametersBase> { 13 | public function __construct( 14 | private T $parameters, 15 | ) { 16 | } 17 | 18 | final protected function getParameters(): T { 19 | return $this->parameters; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/uribuilder/UriBuilderCodegenWithStandardUriBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class UriBuilderCodegenWithStandardUriBuilder 13 | extends UriBuilderCodegenBase { 14 | <<__Override>> 15 | final protected static function createInnerBuilder(): UriBuilder { 16 | return new UriBuilder(static::getParts()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/uriparameters/spec/IntParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | final class IntParameterCodegenSpec extends SimpleParameterCodegenSpec { 13 | <<__Override>> 14 | protected static function getSimpleSpec(): self::TSimpleSpec { 15 | return shape( 16 | 'type' => 'int', 17 | 'accessorSuffix' => 'Int', 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/uriparameters/spec/StringParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | final class StringParameterCodegenSpec extends SimpleParameterCodegenSpec { 13 | <<__Override>> 14 | protected static function getSimpleSpec(): self::TSimpleSpec { 15 | return shape( 16 | 'type' => 'string', 17 | 'accessorSuffix' => 'String', 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/uriparameters/spec/RequestParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class RequestParameterCodegenSpec { 13 | const type TSpec = shape( 14 | 'type' => string, 15 | 'accessorSuffix' => string, 16 | 'args' => ImmVector, 17 | ); 18 | 19 | public abstract static function getGetterSpec( 20 | RequestParameter $param, 21 | ): self::TSpec; 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '42 15 * * *' 7 | jobs: 8 | build: 9 | name: HHVM ${{matrix.hhvm}} - ${{matrix.os}} 10 | strategy: 11 | # Run tests on all OS's and HHVM versions, even if one fails 12 | fail-fast: false 13 | matrix: 14 | os: [ ubuntu ] 15 | hhvm: 16 | - '4.128' 17 | - latest 18 | - nightly 19 | runs-on: ${{matrix.os}}-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: hhvm/actions/hack-lint-test@master 23 | with: 24 | hhvm: ${{matrix.hhvm}} 25 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. 2 | { 3 | "name": "Hack", 4 | "runArgs": [ 5 | "--init" 6 | ], 7 | "image": "hhvm/hhvm:latest", 8 | 9 | // Set *default* container specific settings.json values on container create. 10 | "userEnvProbe": "loginShell", 11 | 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": [ 14 | "pranayagarwal.vscode-hack" 15 | ], 16 | 17 | // Use 'postCreateCommand' to run commands after the container is created. 18 | "postCreateCommand": "curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer && composer install" 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/uribuilder/UriBuilderCodegenBase.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | abstract class UriBuilderCodegenBase<+T as UriBuilderBase> { 13 | abstract const classname CONTROLLER; 14 | 15 | abstract protected static function createInnerBuilder(): T; 16 | 17 | final protected static function getParts( 18 | ): ImmVector { 19 | $controller = static::CONTROLLER; 20 | return $controller::getUriPattern()->getParts(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present, Facebook, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "facebook/hack-router-codegen", 3 | "description": "URI routing for Hack with codegen", 4 | "keywords": ["hack", "router", "routing", "hhvm"], 5 | "homepage": "https://github.com/hhvm/hack-router-codegen", 6 | "require": { 7 | "hhvm": "^4.128", 8 | "facebook/hack-router": "^0.19", 9 | "facebook/hack-http-request-response-interfaces": "^0.2|^0.3", 10 | "facebook/hack-codegen": "^4.5.0", 11 | "facebook/definition-finder": "^2.7", 12 | "hhvm/type-assert": "^3.0|^4.0" 13 | }, 14 | "require-dev": { 15 | "hhvm/hhast": "^4.6", 16 | "hhvm/hhvm-autoload": "^2.0|^3.0", 17 | "hhvm/hacktest": "^2.0", 18 | "facebook/fbexpect": "^2.6.1", 19 | "usox/hackttp": "^0.5" 20 | }, 21 | "extra": { 22 | "branch-alias": { 23 | "dev-master": "1.x-dev", 24 | "dev-main": "1.x-dev" 25 | } 26 | }, 27 | "autoload": { 28 | "classmap": [ "src/" ] 29 | }, 30 | "autoload-dev": { 31 | "classmap": [ "tests/" ] 32 | }, 33 | "config": { 34 | "allow-plugins": { 35 | "hhvm/hhvm-autoload": true 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/uriparameters/spec/UriParameterCodegenArgumentSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | final class UriParameterCodegenArgumentSpec { 13 | const type TRenderer = (function(RequestParameter,?string):string); 14 | 15 | private function __construct( 16 | private self::TRenderer $renderer, 17 | ) { 18 | } 19 | 20 | public function render( 21 | RequestParameter $param, 22 | ?string $value_variable = null, 23 | ): string { 24 | $renderer = $this->renderer; 25 | return $renderer($param, $value_variable); 26 | } 27 | 28 | public static function ParameterName(): this { 29 | return new self( 30 | ($param, $_value) ==> \var_export($param->getName(), true), 31 | ); 32 | } 33 | 34 | public static function ParameterValue(): this { 35 | return new self( 36 | ($_param, $value) ==> { 37 | invariant( 38 | $value !== null, 39 | '%s should never be used for getters', 40 | __FUNCTION__, 41 | ); 42 | return $value; 43 | }, 44 | ); 45 | } 46 | 47 | public static function Custom(self::TRenderer $renderer): this { 48 | return new self($renderer); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/uriparameters/spec/SimpleParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackRouter\UriParameterCodegenArgumentSpec as Args; 13 | 14 | abstract class SimpleParameterCodegenSpec extends UriParameterCodegenSpec { 15 | const type TSimpleSpec = shape( 16 | 'type' => string, 17 | 'accessorSuffix' => string, 18 | ); 19 | abstract protected static function getSimpleSpec(): self::TSimpleSpec; 20 | 21 | <<__Override>> 22 | final public static function getGetterSpec( 23 | RequestParameter $_, 24 | ): self::TSpec { 25 | $spec = static::getSimpleSpec(); 26 | return shape( 27 | 'type' => $spec['type'], 28 | 'accessorSuffix' => $spec['accessorSuffix'], 29 | 'args' => ImmVector { 30 | Args::ParameterName(), 31 | }, 32 | ); 33 | } 34 | 35 | <<__Override>> 36 | public static function getSetterSpec( 37 | UriParameter $_, 38 | ): self::TSpec { 39 | $spec = static::getSimpleSpec(); 40 | return shape( 41 | 'type' => $spec['type'], 42 | 'accessorSuffix' => $spec['accessorSuffix'], 43 | 'args' => ImmVector { 44 | Args::ParameterName(), 45 | Args::ParameterValue(), 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/UriMapBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackRouter\PrivateImpl\ControllerFacts; 13 | 14 | final class UriMapBuilder { 15 | 16 | public function __construct( 17 | private ControllerFacts $controllerFacts, 18 | ) { 19 | } 20 | 21 | public function getUriMap( 22 | ): ImmMap>> { 23 | $map = Map { }; 24 | foreach (HttpMethod::getValues() as $method) { 25 | $map[$method] = Map { }; 26 | } 27 | 28 | $controllers = $this->controllerFacts->getControllers(); 29 | foreach ($controllers as $controller => $methods) { 30 | $path = $controller::getUriPattern()->getFastRouteFragment(); 31 | foreach ($methods as $method) { 32 | invariant( 33 | !$map[$method]->containsKey($path), 34 | "Duplicate entry for path '%s': '%s' and '%s'", 35 | $path, 36 | $map[$method]->at($path), 37 | $controller, 38 | ); 39 | $map[$method][$path] = $controller; 40 | } 41 | } 42 | 43 | foreach ($map as $submap) { 44 | \natsort(inout $submap); 45 | } 46 | 47 | return $map 48 | ->filter($submap ==> !$submap->isEmpty()) 49 | ->map($submap ==> $submap->immutable()) 50 | ->immutable(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to hack-router-codegen 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Our Development Process 6 | 7 | All development is direclty on GitHub. 8 | 9 | ## Pull Requests 10 | We actively welcome your pull requests. 11 | 12 | 1. Fork the repo and create your branch from `main`. 13 | 2. If you've added code that should be tested, add tests. 14 | 3. If you've changed APIs, update the documentation. 15 | 4. Ensure the test suite passes. 16 | 5. Make sure your code lints. 17 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 18 | 19 | ## Contributor License Agreement ("CLA") 20 | In order to accept your pull request, we need you to submit a CLA. You only need 21 | to do this once to work on any of Facebook's open source projects. 22 | 23 | Complete your CLA here: 24 | 25 | ## Issues 26 | We use GitHub issues to track public bugs. Please ensure your description is 27 | clear and has sufficient instructions to be able to reproduce the issue. 28 | 29 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 30 | disclosure of security bugs. In those cases, please go through the process 31 | outlined on that page and do not file a public issue. 32 | 33 | ## Coding Style 34 | 35 | Coding should match `hh_format`/`hackfmt` where practical; the key parts are: 36 | 37 | * 2 spaces for indentation rather than tabs 38 | * 80 character line length 39 | * indent, don't align 40 | 41 | ## License 42 | 43 | By contributing to hack-router-codegen, you agree that your contributions will be licensed 44 | under the LICENSE file in the root directory of this source tree. 45 | -------------------------------------------------------------------------------- /src/uriparameters/RequestParameterCodegenBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | HackCodegenFactory, IHackCodegenConfig}; 14 | 15 | final class RequestParameterCodegenBuilder { 16 | protected HackCodegenFactory $cg; 17 | 18 | public function __construct( 19 | IHackCodegenConfig $config, 20 | ) { 21 | $this->cg = new HackCodegenFactory($config); 22 | } 23 | 24 | protected static function getParameterSpecs( 25 | ): ImmMap< 26 | classname, 27 | classname, 28 | > { 29 | return ImmMap { 30 | IntRequestParameter::class => IntParameterCodegenSpec::class, 31 | StringRequestParameter::class => StringParameterCodegenSpec::class, 32 | EnumRequestParameter::class => EnumParameterCodegenSpec::class, 33 | }; 34 | } 35 | 36 | public static function getRequestSpec( 37 | RequestParameter $param, 38 | ): classname { 39 | $specs = self::getParameterSpecs(); 40 | $type = \get_class($param); 41 | invariant( 42 | $specs->containsKey($type), 43 | "Don't know how to render a %s", 44 | $type, 45 | ); 46 | return $specs->at($type); 47 | } 48 | 49 | public static function getUriSpec( 50 | UriParameter $param, 51 | ): classname { 52 | $spec = self::getRequestSpec($param); 53 | invariant( 54 | \is_subclass_of($spec, UriParameterCodegenSpec::class), 55 | 'Expected %s to be a %s', 56 | $spec, 57 | UriParameterCodegenSpec::class, 58 | ); 59 | /* HH_FIXME[4110] can't coerce classnames */ 60 | return $spec; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/uriparameters/spec/EnumParameterCodegenSpec.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackRouter\UriParameterCodegenArgumentSpec as Args; 13 | 14 | final class EnumParameterCodegenSpec extends UriParameterCodegenSpec { 15 | private static function cast( 16 | RequestParameter $param, 17 | ): EnumRequestParameter { 18 | invariant( 19 | $param is EnumRequestParameter<_>, 20 | 'Expected %s to be an enum parameter, got %s', 21 | $param->getName(), 22 | \get_class($param), 23 | ); 24 | return /* HH_FIXME[4110] unsafe generic */ $param; 25 | } 26 | 27 | private static function getType( 28 | RequestParameter $param, 29 | ): string { 30 | return '\\'.self::cast($param)->getEnumName(); 31 | } 32 | 33 | private static function getTypeName( 34 | RequestParameter $param, 35 | ): string { 36 | return self::getType($param).'::class'; 37 | } 38 | 39 | <<__Override>> 40 | public static function getGetterSpec( 41 | RequestParameter $param, 42 | ): self::TSpec { 43 | return shape( 44 | 'type' => self::getType($param), 45 | 'accessorSuffix' => 'Enum', 46 | 'args' => ImmVector { 47 | Args::Custom(($_, $_) ==> self::getTypeName($param)), 48 | Args::ParameterName(), 49 | }, 50 | ); 51 | } 52 | 53 | <<__Override>> 54 | public static function getSetterSpec( 55 | UriParameter $param, 56 | ): self::TSpec { 57 | $param = self::cast($param); 58 | return shape( 59 | 'type' => self::getType($param), 60 | 'accessorSuffix' => 'Enum', 61 | 'args' => ImmVector { 62 | Args::Custom(($_, $_) ==> self::getTypeName($param)), 63 | Args::ParameterName(), 64 | Args::ParameterValue(), 65 | }, 66 | ); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hack-Router-Codegen 2 | =================== 3 | 4 | [![Continuous Integration](https://github.com/hhvm/hack-router-codegen/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/hhvm/hack-router-codegen/actions/workflows/build-and-test.yml) 5 | 6 | Code generation for controller classes using the `UriPattern` system from 7 | [`hhvm/hack-router`](https://github.com/hhvm/hack-router) 8 | 9 | This currently supports generating: 10 | - Request routing maps 11 | - Hack request routing classes for your site 12 | 13 | For now, looking at the unit tests is the best way to learn how to use 14 | it. 15 | 16 | Building a Request Router 17 | ========================= 18 | 19 | ```Hack 20 | WebController::class, 29 | 'router' => shape( 30 | 'abstract' => false, 31 | 'file' => __DIR__.'/../codegen/Router.php', 32 | 'class' => 'Router', 33 | ), 34 | ), 35 | )->build; 36 | } 37 | } 38 | ``` 39 | 40 | 41 | This will generate a class called 'Router', complete with an 42 | automatically-generated route map, based on the URI patterns in your 43 | controllers. 44 | 45 | `WebController` is the root controller for your site, and must implement 46 | `Facebook\HackRouter\IncludeInUriMap`, which in turn requires 47 | `Facebook\HackRouter\HasUriPattern` - for example: 48 | 49 | ```Hack 50 | public static function getUriPattern(): UriPattern { 51 | return (new UriPattern()) 52 | ->literal('/') 53 | ->string('MyString') 54 | ->literal('/') 55 | ->int('MyInt') 56 | ->literal('/') 57 | ->enum(MyEnum::class, 'MyEnum'); 58 | } 59 | ``` 60 | 61 | Commit Your Codegen! 62 | ==================== 63 | 64 | This is unusual advice, but it's the best approach for Hack code as you 65 | otherwise have a circular dependency: 66 | - HHVM will not execute hack code if there are references to undefined classes 67 | - Once you use the codegen, you reference the codegen classes 68 | - ... so you can't build them if you don't already have them 69 | 70 | Contributing 71 | ============ 72 | 73 | We welcome GitHub issues and pull requests - please see CONTRIBUTING.md for details. 74 | 75 | License 76 | ======= 77 | 78 | hack-router-codegen is MIT-licensed. 79 | -------------------------------------------------------------------------------- /src/privateimpl/ClassFacts.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter\PrivateImpl; 11 | 12 | use type Facebook\DefinitionFinder\{BaseParser, ScannedClass, ScannedClassish}; 13 | 14 | final class ClassFacts { 15 | private ImmMap $classes; 16 | 17 | public function __construct( 18 | BaseParser $parser, 19 | ) { 20 | $classes = Map { }; 21 | 22 | foreach ($parser->getClasses() as $class) { 23 | $classes[$class->getName()] = $class; 24 | } 25 | foreach ($parser->getInterfaces() as $interface) { 26 | $classes[$interface->getName()] = $interface; 27 | } 28 | foreach ($parser->getTraits() as $trait) { 29 | $classes[$trait->getName()] = $trait; 30 | } 31 | $this->classes = $classes->immutable(); 32 | } 33 | 34 | public function getSubclassesOf( 35 | classname $wanted, 36 | ): ImmMap, ScannedClass> { 37 | $mappable = Map { }; 38 | foreach ($this->classes as $class) { 39 | if (!$class is ScannedClass) { 40 | continue; 41 | } 42 | $name = $this->asClassname($wanted, $class->getName()); 43 | if ($name === null) { 44 | continue; 45 | } 46 | $mappable[$name] = $class; 47 | } 48 | return $mappable->immutable(); 49 | } 50 | 51 | public function asClassname( 52 | classname $wanted, 53 | string $name, 54 | ): ?classname { 55 | if ($this->doesImplement($wanted, $name)) { 56 | /* HH_IGNORE_ERROR[4110] */ 57 | return $name; 58 | } 59 | return null; 60 | } 61 | 62 | <<__Memoize>> 63 | public function doesImplement( 64 | classname $wanted, 65 | string $name, 66 | ): bool { 67 | if (\substr($name, 0, 1) === '\\') { 68 | $name = \substr($name, 1); 69 | } 70 | 71 | if ($name === $wanted) { 72 | return true; 73 | } 74 | $class = $this->classes[($name)] ?? null; 75 | if (!$class) { 76 | return false; 77 | } 78 | 79 | foreach ($class->getInterfaceNames() as $interface) { 80 | if ($this->doesImplement($wanted, $interface)) { 81 | return true; 82 | } 83 | } 84 | 85 | foreach ($class->getTraitNames() as $trait) { 86 | if ($this->doesImplement($wanted, $trait)) { 87 | return true; 88 | } 89 | } 90 | 91 | $parent = $class->getParentClassName(); 92 | if ($parent !== null && $this->doesImplement($wanted, $parent)) { 93 | return true; 94 | } 95 | 96 | return false; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/RequestParametersCodegenBuilderBase.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | CodegenClass, 14 | CodegenFile, 15 | CodegenFileResult, 16 | CodegenFileType, 17 | CodegenGeneratedFrom, 18 | CodegenTrait, 19 | HackCodegenFactory, 20 | IHackCodegenConfig 21 | }; 22 | 23 | abstract class RequestParametersCodegenBuilderBase { 24 | const type TTraitSpec = shape( 25 | 'name' => string, 26 | 'method' => string, 27 | ); 28 | const type TSpec = shape( 29 | 'controller' => classname, 30 | 'namespace' => ?string, 31 | 'class' => shape( 32 | 'name' => string, 33 | ), 34 | 'trait' => ?self::TTraitSpec, 35 | ); 36 | 37 | protected CodegenGeneratedFrom $generatedFrom; 38 | protected HackCodegenFactory $cg; 39 | 40 | public function __construct( 41 | IHackCodegenConfig $hackCodegenConfig, 42 | protected classname $base, 43 | protected RequestParameterCodegenBuilder $parameterBuilder, 44 | ) { 45 | $this->cg = new HackCodegenFactory($hackCodegenConfig); 46 | $this->generatedFrom = $this->cg->codegenGeneratedFromScript(); 47 | } 48 | 49 | final public function renderToFile( 50 | string $path, 51 | self::TSpec $spec, 52 | ): CodegenFileResult { 53 | return $this->getCodegenFile($path, $spec)->save(); 54 | } 55 | 56 | final public function setGeneratedFrom( 57 | CodegenGeneratedFrom $generated_from, 58 | ): this { 59 | $this->generatedFrom = $generated_from; 60 | return $this; 61 | } 62 | 63 | private bool $discardChanges = false; 64 | 65 | final public function setDiscardChanges(bool $discard): this { 66 | $this->discardChanges = $discard; 67 | return $this; 68 | } 69 | 70 | private function getCodegenFile( 71 | string $path, 72 | self::TSpec $spec, 73 | ): CodegenFile { 74 | $file = ($this->cg->codegenFile($path) 75 | ->setDoClobber($this->discardChanges) 76 | ->setFileType(CodegenFileType::HACK_STRICT) 77 | ->setGeneratedFrom($this->generatedFrom) 78 | ->addClass($this->getCodegenClass($spec)) 79 | ); 80 | $namespace = Shapes::idx($spec, 'namespace'); 81 | if ($namespace !== null) { 82 | $file->setNamespace($namespace); 83 | } 84 | if (Shapes::idx($spec, 'trait')) { 85 | $file->addTrait($this->getCodegenTrait($spec)); 86 | } 87 | return $file; 88 | } 89 | 90 | protected abstract function getCodegenClass( 91 | self::TSpec $spec, 92 | ): CodegenClass; 93 | 94 | protected abstract function getCodegenTrait( 95 | self::TSpec $spec, 96 | ): CodegenTrait; 97 | } 98 | -------------------------------------------------------------------------------- /src/privateimpl/ControllerFacts.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter\PrivateImpl; 11 | 12 | use type Facebook\DefinitionFinder\{ScannedClass, ScannedClassish}; 13 | use type Facebook\HackRouter\{ 14 | HttpMethod, 15 | IncludeInUriMap, 16 | SupportsGetRequests, 17 | SupportsPostRequests, 18 | TestsBypassVisibility, 19 | }; 20 | 21 | final class ControllerFacts { 22 | public function __construct( 23 | private classname $baseClass, 24 | private ClassFacts $classFacts, 25 | ) { 26 | } 27 | 28 | public function getControllers( 29 | ): ImmMap, ImmSet> { 30 | $controllers = Map { }; 31 | $subclasses = $this->classFacts->getSubclassesOf($this->baseClass); 32 | foreach ($subclasses as $name => $def) { 33 | if (!$this->isUriMappable($def)) { 34 | continue; 35 | } 36 | $controllers[$name] = $this->getHttpMethodsForController($name); 37 | } 38 | return $controllers->immutable(); 39 | } 40 | 41 | <> 42 | private function isUriMappable( 43 | ScannedClassish $class 44 | ): bool { 45 | if (!$class is ScannedClass) { 46 | return false; 47 | } 48 | if ($class->isAbstract()) { 49 | return false; 50 | } 51 | 52 | $cf = $this->classFacts; 53 | if (!$cf->doesImplement(IncludeInUriMap::class, $class->getName())) { 54 | return false; 55 | } 56 | 57 | // This is also me being opinionated. 58 | invariant( 59 | $class->isFinal(), 60 | 'Classes implementing IncludeInUriMap should be abstract or final; '. 61 | '%s is neither', 62 | $class->getName(), 63 | ); 64 | return true; 65 | } 66 | 67 | <> 68 | private function getHttpMethodsForController( 69 | classname $classname, 70 | ): ImmSet { 71 | $supported = Set { }; 72 | $cf = $this->classFacts; 73 | if ($cf->doesImplement(SupportsGetRequests::class, $classname)) { 74 | $supported[] = HttpMethod::GET; 75 | } 76 | if ($cf->doesImplement(SupportsPostRequests::class, $classname)) { 77 | $supported[] = HttpMethod::POST; 78 | } 79 | 80 | invariant( 81 | !$supported->isEmpty(), 82 | '%s implements %s, but does not implement %s or %s', 83 | $classname, 84 | IncludeInUriMap::class, 85 | SupportsGetRequests::class, 86 | SupportsPostRequests::class, 87 | ); 88 | 89 | /* This is me being opinionated, not a technical limitation: 90 | * 91 | * I think each controller should do one thing. Multiple HTTP methods 92 | * implies it does multiple things. 93 | * 94 | * Returning a set instead of a single method so it's easy to change 95 | * if someone convinces me that this is a bad idea. 96 | */ 97 | invariant( 98 | $supported->count() === 1, 99 | '%s is marked as supporting multiple HTTP methods; build 1 controller '. 100 | 'per method instead, refactoring common code out (eg to a trait).', 101 | $classname, 102 | ); 103 | 104 | return $supported->immutable(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/RouterCodegenBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | CodegenClass, 14 | CodegenFile, 15 | CodegenFileResult, 16 | CodegenFileType, 17 | CodegenGeneratedFrom, 18 | HackBuilderKeys, 19 | HackBuilderValues, 20 | HackCodegenFactory, 21 | IHackCodegenConfig 22 | }; 23 | 24 | final class RouterCodegenBuilder { 25 | private CodegenGeneratedFrom $generatedFrom; 26 | private HackCodegenFactory $cg; 27 | private bool $createAbstract = false; 28 | 29 | public function __construct( 30 | private IHackCodegenConfig $codegenConfig, 31 | private classname $responderClass, 32 | private ImmMap>> $uriMap, 33 | ) { 34 | $this->cg = new HackCodegenFactory($codegenConfig); 35 | $this->generatedFrom = $this->cg->codegenGeneratedFromScript(); 36 | } 37 | 38 | public function setCreateAbstractClass(bool $abstract): this { 39 | $this->createAbstract = $abstract; 40 | return $this; 41 | } 42 | 43 | public function setGeneratedFrom( 44 | CodegenGeneratedFrom $generated_from, 45 | ): this { 46 | $this->generatedFrom = $generated_from; 47 | return $this; 48 | } 49 | 50 | public function renderToFile( 51 | string $path, 52 | ?string $namespace, 53 | string $classname, 54 | ): CodegenFileResult { 55 | return $this->getCodegenFile($path, $namespace, $classname)->save(); 56 | } 57 | 58 | private bool $discardChanges = false; 59 | 60 | public function setDiscardChanges(bool $discard): this { 61 | $this->discardChanges = $discard; 62 | return $this; 63 | } 64 | 65 | <> 66 | private function getCodegenFile( 67 | string $path, 68 | ?string $namespace, 69 | string $classname, 70 | ): CodegenFile{ 71 | $file = $this->cg->codegenFile($path) 72 | ->setDoClobber($this->discardChanges) 73 | ->setFileType(CodegenFileType::HACK_STRICT) 74 | ->setGeneratedFrom($this->generatedFrom) 75 | ->addClass($this->getCodegenClass($classname)); 76 | if ($namespace !== null) { 77 | $file->setNamespace($namespace); 78 | } 79 | return $file; 80 | } 81 | 82 | private function getCodegenClass( 83 | string $classname, 84 | ): CodegenClass { 85 | return $this->cg->codegenClass($classname) 86 | ->setIsAbstract($this->createAbstract) 87 | ->setIsFinal(!$this->createAbstract) 88 | ->setExtends(\sprintf( 89 | '\\%s>', 90 | BaseRouter::class, 91 | $this->responderClass, 92 | )) 93 | ->addMethod( 94 | $this->cg->codegenMethod('getRoutes') 95 | // method should be final only if the class is not already final 96 | ->setIsFinal($this->createAbstract) 97 | ->setIsOverride(true) 98 | ->setReturnTypef( 99 | 'ImmMap<\\%s, ImmMap>>', 100 | HttpMethod::class, 101 | $this->responderClass, 102 | ) 103 | ->setBody($this->getUriMapBody()) 104 | ); 105 | } 106 | 107 | private function getUriMapBody(): string { 108 | $map = $this->uriMap; 109 | 110 | return $this->cg->codegenHackBuilder() 111 | ->addAssignment( 112 | '$map', 113 | $map, 114 | HackBuilderValues::immMap( 115 | HackBuilderKeys::lambda(($_config, $method) ==> 116 | \sprintf( 117 | '\\%s::%s', 118 | HttpMethod::class, 119 | $method, 120 | ), 121 | ), 122 | HackBuilderValues::immMap( 123 | HackBuilderKeys::export(), 124 | HackBuilderValues::classname(), 125 | ), 126 | ), 127 | ) 128 | ->addReturnf('$map') 129 | ->getCode(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/UriBuilderCodegenBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | CodegenClass, 14 | CodegenShapeMember, 15 | CodegenTrait, 16 | HackBuilderValues, 17 | IHackCodegenConfig 18 | }; 19 | 20 | final class UriBuilderCodegenBuilder 21 | extends RequestParametersCodegenBuilderBase> { 22 | public function __construct( 23 | IHackCodegenConfig $hackCodegenConfig, 24 | classname> $base, 25 | RequestParameterCodegenBuilder $parameterBuilder, 26 | private string $uriGetter, 27 | private string $uriType, 28 | ) { 29 | parent::__construct($hackCodegenConfig, $base, $parameterBuilder); 30 | } 31 | 32 | <<__Override>> 33 | protected function getCodegenClass(self::TSpec $spec): CodegenClass { 34 | $param_builder = $this->parameterBuilder; 35 | $controller = $spec['controller']; 36 | $param_shape = vec[]; 37 | 38 | $body = $this->cg 39 | ->codegenHackBuilder() 40 | ->addLine('return self::createInnerBuilder()') 41 | ->indent(); 42 | 43 | foreach ($controller::getUriPattern()->getParameters() as $param) { 44 | $param_spec = $param_builder::getUriSpec($param); 45 | $setter_spec = $param_spec::getSetterSpec($param); 46 | $param_shape[] = 47 | new CodegenShapeMember($param->getName(), $setter_spec['type']); 48 | $body 49 | ->ensureNewLine() 50 | ->addMultilineCall( 51 | '->set'.$setter_spec['accessorSuffix'], 52 | $setter_spec['args']->map( 53 | $arg ==> $arg->render( 54 | $param, 55 | \sprintf('$parameters[\'%s\']', $param->getName()), 56 | ), 57 | ), 58 | /* semicolon at end = */ false, 59 | ); 60 | } 61 | $body 62 | ->addLinef('->%s();', $this->uriGetter) 63 | ->unindent(); 64 | 65 | $method = $this->cg 66 | ->codegenMethod($this->uriGetter) 67 | ->setReturnType($this->uriType) 68 | ->setIsStatic(true) 69 | ->setBody($body->getCode()); 70 | if (\count($param_shape) !== 0) { 71 | $method->addParameter('self::TParameters $parameters'); 72 | } 73 | 74 | $common = $this->cg 75 | ->codegenClass($spec['class']['name']) 76 | ->addConstant( 77 | $this->cg->codegenClassConstant('CONTROLLER') 78 | ->setTypef('classname<\\%s>', HasUriPattern::class) 79 | ->setValue($controller, HackBuilderValues::classname()), 80 | ) 81 | ->addTypeConstant( 82 | $this->cg->codegenTypeConstant('TParameters') 83 | ->setValue( 84 | $this->cg->codegenShape(...$param_shape), 85 | HackBuilderValues::codegen(), 86 | ) 87 | ) 88 | ->addMethod($method) 89 | ->setIsAbstract(true) 90 | ->setIsFinal(true) 91 | ->setExtends('\\'.$this->base); 92 | 93 | return $common; 94 | } 95 | 96 | <<__Override>> 97 | protected function getCodegenTrait(self::TSpec $spec): CodegenTrait { 98 | $trait = Shapes::idx($spec, 'trait'); 99 | invariant( 100 | $trait !== null, 101 | "Can't codegen a trait without a trait spec", 102 | ); 103 | 104 | $controller = $spec['controller']; 105 | $parameters = $controller::getUriPattern()->getParameters(); 106 | 107 | $method = $this->cg 108 | ->codegenMethod($trait['method']) 109 | ->setIsFinal(true) 110 | ->setIsStatic(true) 111 | ->setReturnType($this->uriType); 112 | if ($parameters->isEmpty()) { 113 | $method->setBodyf( 114 | 'return %s::%s();', 115 | $spec['class']['name'], 116 | $this->uriGetter, 117 | ); 118 | } else { 119 | $method 120 | ->addParameterf( 121 | '%s::TParameters $parameters', 122 | $spec['class']['name'], 123 | ) 124 | ->setBodyf( 125 | 'return %s::%s($parameters);', 126 | $spec['class']['name'], 127 | $this->uriGetter, 128 | ); 129 | } 130 | 131 | return $this->cg 132 | ->codegenTrait($trait['name']) 133 | ->addMethod($method); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/RequestParametersCodegenBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use namespace HH\Lib\C; 13 | use type Facebook\HackCodegen\{ 14 | CodegenClass, 15 | CodegenShapeMember, 16 | CodegenTrait, 17 | HackBuilderValues, 18 | IHackCodegenConfig, 19 | }; 20 | 21 | final class RequestParametersCodegenBuilder 22 | extends RequestParametersCodegenBuilderBase> { 23 | const type TGetParameters = (function( 24 | classname, 25 | ): ImmVector RequestParameter, 27 | 'optional' => bool, 28 | )>); 29 | 30 | private Vector> $traitRequiredClasses = Vector {}; 31 | private Vector> $traitRequiredInterfaces = Vector {}; 32 | 33 | public function __construct( 34 | IHackCodegenConfig $codegen_config, 35 | private self::TGetParameters $getParameters, 36 | private string $getRawParametersCode, 37 | classname> $base, 38 | RequestParameterCodegenBuilder $parameterBuilder, 39 | ) { 40 | parent::__construct($codegen_config, $base, $parameterBuilder); 41 | } 42 | 43 | <<__Override>> 44 | protected function getCodegenClass(self::TSpec $spec): CodegenClass { 45 | $param_builder = $this->parameterBuilder; 46 | $controller = $spec['controller']; 47 | $getParameters = $this->getParameters; 48 | $parameters = $getParameters($controller); 49 | 50 | $body = $this->cg->codegenHackBuilder(); 51 | 52 | if (!C\is_empty($parameters)) { 53 | // avoid generating an assignment to an unused variable 54 | $body->addAssignment( 55 | '$p', 56 | '$this->getParameters()', 57 | HackBuilderValues::literal(), 58 | ); 59 | } 60 | 61 | $body 62 | ->addLine('return shape(') 63 | ->indent(); 64 | 65 | $param_shape = vec[]; 66 | foreach ($parameters as $parameter) { 67 | $param_spec = $parameter['spec']; 68 | $request_spec = $param_builder::getRequestSpec($param_spec); 69 | $getter_spec = $request_spec::getGetterSpec($param_spec); 70 | 71 | $type = $getter_spec['type']; 72 | if ($parameter['optional']) { 73 | $type = '?'.$type; 74 | } 75 | 76 | $param_shape[] = new CodegenShapeMember($param_spec->getName(), $type); 77 | $body 78 | ->ensureNewLine() 79 | ->addf("'%s' => ", $param_spec->getName()) 80 | ->addMultilineCall( 81 | \sprintf( 82 | '$p->get%s%s', 83 | $parameter['optional'] ? 'Optional' : '', 84 | $getter_spec['accessorSuffix'], 85 | ), 86 | $getter_spec['args']->map($arg ==> $arg->render($param_spec)), 87 | /* semicolon at end = */ false, 88 | ) 89 | ->add(','); 90 | } 91 | $body 92 | ->ensureNewLine() 93 | ->unindent() 94 | ->addLine(');'); 95 | 96 | return $this->cg 97 | ->codegenClass($spec['class']['name']) 98 | ->setIsFinal(true) 99 | ->setExtends('\\'.$this->base) 100 | ->addTypeConstant( 101 | $this->cg 102 | ->codegenTypeConstant('TParameters') 103 | ->setValue( 104 | $this->cg->codegenShape(...$param_shape), 105 | HackBuilderValues::codegen(), 106 | ), 107 | ) 108 | ->addMethod( 109 | $this->cg 110 | ->codegenMethod('get') 111 | ->setReturnType('self::TParameters') 112 | ->setBody($body->getCode()), 113 | ); 114 | } 115 | 116 | <<__Override>> 117 | protected function getCodegenTrait(self::TSpec $spec): CodegenTrait { 118 | $trait = Shapes::idx($spec, 'trait'); 119 | invariant($trait !== null, "Can't codegen a trait without a trait spec"); 120 | 121 | $trait = $this->cg 122 | ->codegenTrait($trait['name']) 123 | ->addMethod( 124 | $this->cg 125 | ->codegenMethod($trait['method']) 126 | ->setIsFinal(true) 127 | ->setProtected() 128 | ->setIsMemoized(true) 129 | ->setReturnTypef('%s::TParameters', $spec['class']['name']) 130 | ->setBody( 131 | $this->cg 132 | ->codegenHackBuilder() 133 | ->addAssignment( 134 | '$raw', 135 | $this->getRawParametersCode, 136 | HackBuilderValues::literal(), 137 | ) 138 | ->addLinef('return (new %s($raw))', $spec['class']['name']) 139 | ->indent() 140 | ->addLine('->get();') 141 | ->getCode(), 142 | ), 143 | ); 144 | 145 | foreach ($this->traitRequiredClasses as $class) { 146 | $trait->addRequireClass('\\'.$class); 147 | } 148 | foreach ($this->traitRequiredInterfaces as $class) { 149 | $trait->addRequireInterface('\\'.$class); 150 | } 151 | return $trait; 152 | } 153 | 154 | public function traitRequireExtends(classname $what): this { 155 | $this->traitRequiredClasses[] = $what; 156 | return $this; 157 | } 158 | 159 | public function traitRequireImplements(classname $what): this { 160 | $this->traitRequiredInterfaces[] = $what; 161 | return $this; 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /src/RouterCLILookupCodegenBuilder.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | CodegenClass, 14 | CodegenFile, 15 | CodegenFileResult, 16 | CodegenFileType, 17 | CodegenGeneratedFrom, 18 | CodegenMethod, 19 | HackBuilderValues, 20 | HackCodegenFactory, 21 | IHackCodegenConfig 22 | }; 23 | 24 | final class RouterCLILookupCodegenBuilder { 25 | private CodegenGeneratedFrom $generatedFrom; 26 | private HackCodegenFactory $cg; 27 | 28 | public function __construct( 29 | private IHackCodegenConfig $codegenConfig, 30 | ) { 31 | $this->cg = new HackCodegenFactory($codegenConfig); 32 | $this->generatedFrom = $this->cg->codegenGeneratedFromScript(); 33 | } 34 | 35 | public function setGeneratedFrom( 36 | CodegenGeneratedFrom $generated_from, 37 | ): this { 38 | $this->generatedFrom = $generated_from; 39 | return $this; 40 | } 41 | 42 | public function renderToFile( 43 | string $path, 44 | ?string $namespace, 45 | string $router_classname, 46 | string $utility_classname, 47 | ): CodegenFileResult { 48 | return $this->getCodegenFile( 49 | $path, 50 | $namespace, 51 | $router_classname, 52 | $utility_classname, 53 | )->save(); 54 | } 55 | 56 | private bool $discardChanges = false; 57 | 58 | public function setDiscardChanges(bool $discard): this { 59 | $this->discardChanges = $discard; 60 | return $this; 61 | } 62 | 63 | <> 64 | private function getCodegenFile( 65 | string $path, 66 | ?string $namespace, 67 | string $router_classname, 68 | string $utility_classname, 69 | ): CodegenFile { 70 | $file = $this->cg->codegenFile($path) 71 | ->setDoClobber($this->discardChanges) 72 | ->setShebangLine('#!/usr/bin/env hhvm') 73 | ->setFileType(CodegenFileType::HACK_STRICT) 74 | ->setGeneratedFrom($this->generatedFrom) 75 | ->addClass($this->getCodegenClass($router_classname, $utility_classname)) 76 | ->addFunction( 77 | $this->cg->codegenFunction('hack_router_cli_lookup_generated_main') 78 | ->addEmptyUserAttribute('__EntryPoint') 79 | ->setReturnType('void') 80 | ->setBodyf( 81 | "%s\n". 82 | '$argv = '. 83 | '\\Facebook\\TypeAssert\\matches>('. 84 | "\\HH\\global_get('argv'));\n". 85 | "(new %s())->main(\$argv);\n", 86 | $this->getInitCode(), 87 | $utility_classname, 88 | ), 89 | ); 90 | 91 | if ($namespace !== null) { 92 | $file->setNamespace($namespace); 93 | } 94 | return $file; 95 | } 96 | 97 | private function getCodegenClass( 98 | string $router_classname, 99 | string $utility_classname, 100 | ): CodegenClass { 101 | return $this->cg->codegenClass($utility_classname) 102 | ->setIsFinal(true) 103 | ->addMethod( 104 | $this->cg->codegenMethod('getRouter') 105 | ->setReturnType('\\'.$router_classname) 106 | ->setPrivate() 107 | ->setManualBody(true) 108 | ->setBodyf( 109 | 'return new \\%s();', 110 | $router_classname, 111 | ) 112 | ) 113 | ->addMethod( 114 | $this->cg->codegenMethod('prettifyControllerName') 115 | ->addParameter('string $controller') 116 | ->setReturnType('string') 117 | ->setPrivate() 118 | ->setManualBody(true) 119 | ->setBody('return $controller;') 120 | ) 121 | ->addMethod($this->getControllersForPathMethod()) 122 | ->addMethod($this->getMainMethod()); 123 | } 124 | 125 | private function getControllersForPathMethod(): CodegenMethod { 126 | return $this->cg->codegenMethod('getControllersForPath') 127 | ->addParameter('string $path') 128 | ->setReturnTypef( 129 | 'ImmMap<\\%s, string>', 130 | HttpMethod::class, 131 | ) 132 | ->setPrivate() 133 | ->setBody( 134 | $this->cg->codegenHackBuilder() 135 | ->addAssignment( 136 | '$router', 137 | '$this->getRouter()', 138 | HackBuilderValues::literal(), 139 | ) 140 | ->startTryBlock() 141 | ->addAssignment( 142 | '$controllers', 143 | 'Map { }', 144 | HackBuilderValues::literal(), 145 | ) 146 | ->startForeachLoop( 147 | \sprintf('\\%s::getValues()', HttpMethod::class), 148 | null, 149 | '$method', 150 | ) 151 | ->startTryBlock() 152 | ->addLine('list($controller, $_params) =') 153 | ->indent() 154 | ->addLine('$router->routeMethodAndPath($method, $path);') 155 | ->unindent() 156 | ->addLine('$controllers[$method] = $controller;') 157 | ->addCatchBlock('\\'.MethodNotAllowedException::class, '$_') 158 | ->addInlineComment('Ignore') 159 | ->endTryBlock() 160 | ->endForeachLoop() 161 | ->addReturn('$controllers->immutable()', HackBuilderValues::literal()) 162 | ->addCatchBlock('\\'.NotFoundException::class, '$_') 163 | ->addReturn('ImmMap { }', HackBuilderValues::literal()) 164 | ->endTryBlock() 165 | ->getCode() 166 | ); 167 | } 168 | 169 | private function getInitCode(): string { 170 | $autoloader_dirs = ImmSet { 171 | '/', 172 | '/vendor/', 173 | '/../vendor/', 174 | '/../', 175 | }; 176 | $autoloader_files = ImmSet { 177 | 'autoload.hack', 178 | }; 179 | $full_autoloader_files = Set { }; 180 | foreach ($autoloader_files as $file) { 181 | foreach ($autoloader_dirs as $dir) { 182 | $full_autoloader_files[] = \sprintf( 183 | '__DIR__.%s', 184 | \var_export($dir.$file, true), 185 | ); 186 | } 187 | } 188 | 189 | return $this->cg->codegenHackBuilder() 190 | ->startManualSection('init') 191 | ->addAssignment( 192 | '$autoloader', 193 | 'null', 194 | HackBuilderValues::literal(), 195 | ) 196 | ->addAssignment( 197 | '$autoloader_candidates', 198 | $full_autoloader_files->immutable(), 199 | HackBuilderValues::immSet(HackBuilderValues::literal()), 200 | ) 201 | ->startForeachLoop('$autoloader_candidates', null, '$candidate') 202 | ->startIfBlock('\\file_exists($candidate)') 203 | ->addAssignment('$autoloader', '$candidate', HackBuilderValues::literal()) 204 | ->addLine('break;') 205 | ->endIfBlock() 206 | ->endForeachLoop() 207 | ->startIfBlock('$autoloader === null') 208 | ->addLine('\\fwrite(\\STDERR, "Can\'t find autoloader.\n");') 209 | ->addLine('exit(1);') 210 | ->endIfBlock() 211 | ->addLine('require_once($autoloader);') 212 | ->addLine('\\Facebook\\AutoloadMap\\initialize();') 213 | ->endManualSection() 214 | ->getCode(); 215 | } 216 | 217 | private function getMainMethod(): CodegenMethod { 218 | return $this->cg->codegenMethod('main') 219 | ->addParameter('KeyedContainer $argv') 220 | ->setReturnType('void') 221 | ->setBody( 222 | $this->cg->codegenHackBuilder() 223 | ->addAssignment( 224 | '$path', 225 | '$argv[1] ?? null', 226 | HackBuilderValues::literal(), 227 | ) 228 | ->startIfBlock('$path === null') 229 | ->addLine('\\fprintf(\\STDERR, "Usage: %s PATH\n", $argv[0]);') 230 | ->addLine('exit(1);') 231 | ->endIfBlock() 232 | ->addAssignment( 233 | '$controllers', 234 | '$this->getControllersForPath($path)', 235 | HackBuilderValues::literal(), 236 | ) 237 | ->startIfBlock('$controllers->isEmpty()') 238 | ->addLine('\\printf("No controller found for \'%s\'.\n", $path);') 239 | ->addLine('exit(1);') 240 | ->endIfBlock() 241 | ->startForeachLoop('$controllers', '$method', '$controller') 242 | ->addAssignment( 243 | '$pretty', 244 | '$this->prettifyControllerName($controller)', 245 | HackBuilderValues::literal(), 246 | ) 247 | ->addLine('\\printf("%-8s %s\n", $method.\':\', $pretty);') 248 | ->endForeachLoop() 249 | ->getCode() 250 | ); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Codegen.hack: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | * 8 | */ 9 | 10 | namespace Facebook\HackRouter; 11 | 12 | use type Facebook\HackCodegen\{ 13 | CodegenGeneratedFrom, 14 | HackCodegenConfig, 15 | HackCodegenFactory, 16 | IHackCodegenConfig, 17 | }; 18 | use type Facebook\DefinitionFinder\{BaseParser, TreeParser}; 19 | use type Facebook\HackRouter\PrivateImpl\{ClassFacts, ControllerFacts}; 20 | 21 | final class Codegen { 22 | const type TUriBuilderOutput = shape( 23 | 'file' => string, 24 | ?'namespace' => string, 25 | 'class' => shape( 26 | 'name' => string, 27 | ), 28 | ?'trait' => shape( 29 | 'name' => string, 30 | 'method' => string, 31 | ), 32 | ); 33 | 34 | const type TUriBuilderCodegenConfig = shape( 35 | ?'baseClass' => classname>, 36 | ?'parameterCodegenBuilder' => RequestParameterCodegenBuilder, 37 | ?'returnSpec' => shape( 38 | 'type' => string, 39 | 'getter' => string, 40 | ), 41 | 'output' => (function( 42 | classname, 43 | ): ?self::TUriBuilderOutput), 44 | ); 45 | 46 | const type TRequestParametersOutput = shape( 47 | 'file' => string, 48 | ?'namespace' => string, 49 | 'class' => shape( 50 | 'name' => string, 51 | ), 52 | 'trait' => shape( 53 | 'name' => string, 54 | ), 55 | ); 56 | 57 | const type TRequestParametersCodegenConfig = shape( 58 | ?'getParameters' => RequestParametersCodegenBuilder::TGetParameters, 59 | ?'baseClass' => 60 | classname>, 61 | ?'parameterCodegenBuilder' => RequestParameterCodegenBuilder, 62 | 'trait' => shape( 63 | 'methodName' => string, 64 | 'getRawParametersCode' => string, 65 | ?'requireExtends' => ImmSet>, 66 | ?'requireImplements' => ImmSet>, 67 | ), 68 | 'output' => (function( 69 | classname, 70 | ): ?self::TRequestParametersOutput), 71 | ); 72 | 73 | const type TRouterCodegenConfig = shape( 74 | 'file' => string, 75 | ?'namespace' => string, 76 | 'class' => string, 77 | 'abstract' => bool, 78 | ?'cliLookup' => shape( 79 | 'class' => string, 80 | 'file' => string, 81 | ), 82 | ); 83 | 84 | const type TCodegenConfig = shape( 85 | ?'hackCodegenConfig' => IHackCodegenConfig, 86 | ?'controllerBase' => classname, 87 | ?'generatedFrom' => CodegenGeneratedFrom, 88 | ?'router' => self::TRouterCodegenConfig, 89 | ?'uriBuilders' => self::TUriBuilderCodegenConfig, 90 | ?'requestParameters' => self::TRequestParametersCodegenConfig, 91 | ?'discardChanges' => bool, 92 | ); 93 | 94 | public static function forTree( 95 | string $source_root, 96 | self::TCodegenConfig $config, 97 | ): Codegen { 98 | // leaving for now as it's a public API 99 | /* HHAST_IGNORE_ERROR[DontUseAsioJoin] fix before final release */ 100 | return 101 | new self(\HH\Asio\join(TreeParser::fromPathAsync($source_root)), $config); 102 | } 103 | 104 | <<__Memoize>> 105 | private function getGeneratedFrom(): CodegenGeneratedFrom { 106 | return $this->config['generatedFrom'] ?? 107 | $this->getHackCodegenFactory()->codegenGeneratedFromScript(); 108 | } 109 | 110 | public function build(): void { 111 | $this->buildRouter(); 112 | $this->buildUriBuilders(); 113 | $this->buildRequestParameters(); 114 | } 115 | 116 | private ControllerFacts $controllerFacts; 117 | 118 | private function getControllerBase(): classname { 119 | return $this->config['controllerBase'] ?? IncludeInUriMap::class; 120 | } 121 | 122 | <<__Memoize>> 123 | private function getHackCodegenConfig(): IHackCodegenConfig { 124 | return $this->config['hackCodegenConfig'] ?? new HackCodegenConfig(); 125 | } 126 | 127 | <<__Memoize>> 128 | private function getHackCodegenFactory(): HackCodegenFactory { 129 | return new HackCodegenFactory($this->getHackCodegenConfig()); 130 | } 131 | 132 | private function __construct( 133 | BaseParser $parser, 134 | private self::TCodegenConfig $config, 135 | ) { 136 | $this->controllerFacts = ( 137 | new ControllerFacts($this->getControllerBase(), new ClassFacts($parser)) 138 | ); 139 | } 140 | 141 | private function buildRouter(): void { 142 | $config = Shapes::idx($this->config, 'router'); 143 | if ($config === null) { 144 | return; 145 | } 146 | 147 | $uri_map = (new UriMapBuilder($this->controllerFacts))->getUriMap(); 148 | 149 | ( 150 | new RouterCodegenBuilder( 151 | $this->getHackCodegenConfig(), 152 | $this->getControllerBase(), 153 | $uri_map, 154 | ) 155 | ) 156 | ->setCreateAbstractClass($config['abstract']) 157 | ->setGeneratedFrom($this->getGeneratedFrom()) 158 | ->setDiscardChanges($this->config['discardChanges'] ?? false) 159 | ->renderToFile( 160 | $config['file'], 161 | Shapes::idx($config, 'namespace'), 162 | $config['class'], 163 | ); 164 | 165 | $cli_config = $config['cliLookup'] ?? null; 166 | if ($cli_config === null) { 167 | return; 168 | } 169 | (new RouterCLILookupCodegenBuilder($this->getHackCodegenConfig())) 170 | ->setGeneratedFrom($this->getGeneratedFrom()) 171 | ->setDiscardChanges($this->config['discardChanges'] ?? false) 172 | ->renderToFile( 173 | $cli_config['file'], 174 | Shapes::idx($config, 'namespace'), 175 | $config['class'], 176 | $cli_config['class'], 177 | ); 178 | } 179 | 180 | private function buildUriBuilders(): void { 181 | $config = Shapes::idx($this->config, 'uriBuilders'); 182 | if ($config === null) { 183 | return; 184 | } 185 | $base = $config['baseClass'] ?? UriBuilderCodegen::class; 186 | $param_builder = $config['parameterCodegenBuilder'] ?? 187 | new RequestParameterCodegenBuilder($this->getHackCodegenConfig()); 188 | $get_output = $config['output']; 189 | $return_spec = $config['returnSpec'] ?? 190 | shape( 191 | 'getter' => 'getPath', 192 | 'type' => 'string', 193 | ); 194 | 195 | $builder = ( 196 | new UriBuilderCodegenBuilder( 197 | $this->getHackCodegenConfig(), 198 | $base, 199 | $param_builder, 200 | $return_spec['getter'], 201 | $return_spec['type'], 202 | ) 203 | ) 204 | ->setGeneratedFrom($this->getGeneratedFrom()) 205 | ->setDiscardChanges( 206 | Shapes::idx($this->config ?? shape(), 'discardChanges', false), 207 | ); 208 | 209 | $controllers = $this->controllerFacts->getControllers()->keys(); 210 | foreach ($controllers as $controller) { 211 | $output = $get_output($controller); 212 | if ($output === null) { 213 | continue; 214 | } 215 | 216 | $builder->renderToFile( 217 | $output['file'], 218 | shape( 219 | 'controller' => $controller, 220 | 'namespace' => Shapes::idx($output, 'namespace'), 221 | 'class' => $output['class'], 222 | 'trait' => $output['trait'] ?? null, 223 | ), 224 | ); 225 | } 226 | } 227 | 228 | private function buildRequestParameters(): void { 229 | $config = Shapes::idx($this->config, 'requestParameters'); 230 | if ($config === null) { 231 | return; 232 | } 233 | $base = $config['baseClass'] ?? RequestParametersCodegen::class; 234 | $param_builder = $config['parameterCodegenBuilder'] ?? 235 | new RequestParameterCodegenBuilder($this->getHackCodegenConfig()); 236 | $get_output = $config['output']; 237 | $getParameters = $config['getParameters'] ?? 238 | (classname $class) ==> { 239 | return $class::getUriPattern()->getParameters() 240 | ->map($param ==> shape('spec' => $param, 'optional' => false)); 241 | }; 242 | 243 | $builder = ( 244 | new RequestParametersCodegenBuilder( 245 | $this->getHackCodegenConfig(), 246 | $getParameters, 247 | $config['trait']['getRawParametersCode'], 248 | $base, 249 | $param_builder, 250 | ) 251 | ) 252 | ->setDiscardChanges( 253 | Shapes::idx($this->config ?? shape(), 'discardChanges', false), 254 | ) 255 | ->setGeneratedFrom($this->getGeneratedFrom()); 256 | foreach ($config['trait']['requireExtends'] ?? vec[] as $what) { 257 | $builder->traitRequireExtends($what); 258 | } 259 | foreach ($config['trait']['requireImplements'] ?? vec[] as $what) { 260 | $builder->traitRequireImplements($what); 261 | } 262 | 263 | $controllers = $this->controllerFacts->getControllers()->keys(); 264 | foreach ($controllers as $controller) { 265 | $output = $get_output($controller); 266 | if ($output === null) { 267 | continue; 268 | } 269 | $builder->renderToFile( 270 | $output['file'], 271 | shape( 272 | 'controller' => $controller, 273 | 'namespace' => Shapes::idx($output, 'namespace'), 274 | 'class' => shape( 275 | 'name' => $output['class']['name'], 276 | ), 277 | 'trait' => shape( 278 | 'name' => $output['trait']['name'], 279 | 'method' => $config['trait']['methodName'], 280 | ), 281 | ), 282 | ); 283 | } 284 | } 285 | } 286 | --------------------------------------------------------------------------------