├── .gitattributes ├── .github └── stale.yml ├── .gitignore ├── .hhconfig ├── .travis.sh ├── .travis.yml ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── format ├── composer.json ├── composer.lock ├── hh_autoload.json ├── hhast-lint.json ├── src └── Nuxed │ ├── Asset │ ├── Context │ │ ├── Context.hack │ │ ├── IContext.hack │ │ └── NullContext.hack │ ├── Exception │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ ├── LogicException.hack │ │ └── RuntimeException.hack │ ├── IPackage.hack │ ├── Package.hack │ ├── Packages.hack │ ├── PathPackage.hack │ ├── UrlPackage.hack │ └── VersionStrategy │ │ ├── EmptyVersionStrategy.hack │ │ ├── IVersionStrategy.hack │ │ ├── JsonManifestVersionStrategy.hack │ │ └── StaticVersionStrategy.hack │ ├── Cache │ ├── Cache.hack │ ├── Exception │ │ ├── CacheException.hack │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ └── LogicException.hack │ ├── ICache.hack │ ├── Serializer │ │ ├── ISerializer.hack │ │ ├── JsonSerializer.hack │ │ └── NativeSerializer.hack │ ├── Store │ │ ├── AbstractStore.hack │ │ ├── ApcStore.hack │ │ ├── ArrayStore.hack │ │ ├── FilesystemStore.hack │ │ ├── IStore.hack │ │ ├── MCRouterStore.hack │ │ ├── NullStore.hack │ │ └── RedisStore.hack │ └── private.hack │ ├── Container │ ├── ContainerBuilder.hack │ ├── Exception │ │ ├── ContainerException.hack │ │ ├── IException.hack │ │ └── NotFoundException.hack │ ├── IFactory.hack │ ├── IInflector.hack │ ├── IServiceContainerAware.hack │ ├── IServiceProvider.hack │ ├── ReflectionServiceContainer.hack │ ├── Service │ │ ├── CallableFactoryDecorator.hack │ │ ├── CallableInflectorDecorator.hack │ │ ├── Newable.hack │ │ └── NewableFactoryDecorator.hack │ ├── ServiceContainer.hack │ ├── ServiceContainerAwareTrait.hack │ ├── ServiceDefinition.hack │ └── functions.hack │ ├── Contract │ └── IReset.hack │ ├── Environment │ ├── Exception │ │ ├── IException.hack │ │ └── InvalidArgumentException.hack │ ├── _Private │ │ ├── Parser.hack │ │ └── State.hack │ ├── add.hack │ ├── contains.hack │ ├── forget.hack │ ├── get.hack │ ├── is_debug.hack │ ├── is_dev.hack │ ├── load.hack │ ├── parse.hack │ └── put.hack │ ├── EventDispatcher │ ├── CallableEventListener.hack │ ├── ErrorEvent.hack │ ├── EventDispatcher.hack │ ├── Exception │ │ ├── IException.hack │ │ └── InvalidListenerException.hack │ ├── IEvent.hack │ ├── IEventDispatcher.hack │ ├── IEventListener.hack │ ├── IStoppableEvent.hack │ ├── ListenerProvider │ │ ├── AttachableListenerProvider.hack │ │ ├── IAttachableListenerProvider.hack │ │ ├── IListenerProvider.hack │ │ ├── IPrioritizedListenerProvider.hack │ │ ├── IRandomizedListenerProvider.hack │ │ ├── IReifiedListenerProvider.hack │ │ ├── ListenerProviderAggregate.hack │ │ ├── PrioritizedListenerProvider.hack │ │ ├── RandomizedListenerProvider.hack │ │ └── ReifiedListenerProvider.hack │ └── functional.hack │ ├── Filesystem │ ├── Exception │ │ ├── ExistingNodeException.hack │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ ├── InvalidPathException.hack │ │ ├── MissingNodeException.hack │ │ ├── OutOfRangeException.hack │ │ ├── ReadErrorException.hack │ │ ├── RuntimeException.hack │ │ ├── UnreadableNodeException.hack │ │ ├── UnwritableNodeException.hack │ │ └── WriteErrorException.hack │ ├── File.hack │ ├── Folder.hack │ ├── Lines.hack │ ├── Node.hack │ ├── OperationType.hack │ └── Path.hack │ ├── Http │ ├── Client │ │ ├── CurlHttpClient.hack │ │ ├── Exception │ │ │ ├── IException.hack │ │ │ ├── InvalidArgumentException.hack │ │ │ ├── NetworkException.hack │ │ │ └── RequestException.hack │ │ ├── HttpClient.hack │ │ ├── HttpClientOptions.hack │ │ ├── IHttpClient.hack │ │ ├── MockHttpClient.hack │ │ └── _Private │ │ │ └── Structure.hack │ ├── Emitter │ │ ├── Emitter.hack │ │ ├── Exception │ │ │ ├── EmitterException.hack │ │ │ └── IException.hack │ │ ├── IEmitter.hack │ │ ├── SapiEmitter.hack │ │ └── SapiStreamEmitter.hack │ ├── Error │ │ ├── ErrorHandler.hack │ │ ├── IErrorHandler.hack │ │ └── Middleware │ │ │ └── ErrorMiddleware.hack │ ├── Flash │ │ ├── Exception │ │ │ ├── IException.hack │ │ │ └── InvalidHopsValueException.hack │ │ ├── FlashMessages.hack │ │ └── FlashMessagesMiddleware.hack │ ├── Message │ │ ├── Cookie.hack │ │ ├── CookieSameSite.hack │ │ ├── Exception │ │ │ ├── ConflictingHeadersException.hack │ │ │ ├── IException.hack │ │ │ ├── InvalidArgumentException.hack │ │ │ ├── RuntimeException.hack │ │ │ ├── SuspiciousOperationException.hack │ │ │ ├── UnreadableStreamException.hack │ │ │ ├── UnrecognizedProtocolVersionException.hack │ │ │ ├── UnseekableStreamException.hack │ │ │ ├── UntellableStreamException.hack │ │ │ ├── UnwritableStreamException.hack │ │ │ ├── UploadedFileAlreadyMovedException.hack │ │ │ └── UploadedFileErrorException.hack │ │ ├── IStream.hack │ │ ├── IpUtils.hack │ │ ├── MessageTrait.hack │ │ ├── Request.hack │ │ ├── Request │ │ │ └── functions.hack │ │ ├── RequestMethod.hack │ │ ├── RequestTrait.hack │ │ ├── Response.hack │ │ ├── Response │ │ │ ├── JsonResponse.hack │ │ │ └── functions.hack │ │ ├── ServerRequest.hack │ │ ├── StatusCode.hack │ │ ├── Stream.hack │ │ ├── Stream │ │ │ └── functions.hack │ │ ├── StreamSeekWhence.hack │ │ ├── UploadedFile.hack │ │ ├── UploadedFileError.hack │ │ ├── Uri.hack │ │ ├── _Private │ │ │ ├── HeadersMarshaler.hack │ │ │ ├── ProtocolVersionMarshaler.hack │ │ │ ├── UriMarshaler.hack │ │ │ ├── create_server_request_from_globals.hack │ │ │ └── inject_content_type_in_headers.hack │ │ └── functions.hack │ ├── Router │ │ ├── Exception │ │ │ ├── DuplicateRouteException.hack │ │ │ ├── IException.hack │ │ │ ├── InvalidArgumentException.hack │ │ │ └── RuntimeException.hack │ │ ├── Generator │ │ │ ├── IUriGenerator.hack │ │ │ └── UriGenerator.hack │ │ ├── IRouteCollector.hack │ │ ├── IRouter.hack │ │ ├── Matcher │ │ │ ├── IRequestMatcher.hack │ │ │ └── RequestMatcher.hack │ │ ├── Middleware │ │ │ ├── DispatchMiddleware.hack │ │ │ ├── ImplicitHeadMiddleware.hack │ │ │ ├── ImplicitOptionsMiddleware.hack │ │ │ ├── MethodNotAllowedMiddleware.hack │ │ │ └── RouteMiddleware.hack │ │ ├── Route.hack │ │ ├── RouteCollector.hack │ │ ├── RouteCollectorTrait.hack │ │ ├── RouteResult.hack │ │ ├── Router.hack │ │ └── _Private │ │ │ ├── Ref.hack │ │ │ └── map.hack │ ├── Server │ │ ├── Exception │ │ │ ├── EmptyStackException.hack │ │ │ ├── IException.hack │ │ │ ├── InvalidMiddlewareException.hack │ │ │ ├── RuntimeException.hack │ │ │ └── ServerException.hack │ │ ├── Handler │ │ │ ├── CallableHandlerDecorator.hack │ │ │ ├── NextMiddlewareHandler.hack │ │ │ └── NotFoundHandler.hack │ │ ├── HandlerMiddlewareTrait.hack │ │ ├── IHandler.hack │ │ ├── IMiddleware.hack │ │ ├── IMiddlewareStack.hack │ │ ├── Middleware │ │ │ ├── CallableMiddlewareDecorator.hack │ │ │ ├── HostMiddlewareDecorator.hack │ │ │ ├── OriginalMessagesMiddleware.hack │ │ │ ├── PathMiddlewareDecorator.hack │ │ │ └── RequestHandlerMiddlewareDecorator.hack │ │ ├── MiddlewareStack.hack │ │ ├── helpers.hack │ │ └── types.hack │ └── Session │ │ ├── CacheLimiter.hack │ │ ├── Exception │ │ ├── IException.hack │ │ └── InvalidArgumentException.hack │ │ ├── Persistence │ │ ├── AbstractSessionPersistence.hack │ │ ├── CacheSessionPersistence.hack │ │ └── ISessionPersistence.hack │ │ ├── Session.hack │ │ └── SessionMiddleware.hack │ ├── Jwt │ ├── Builder.hack │ ├── Exception │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ └── RuntimeException.hack │ ├── IBuilder.hack │ ├── IParser.hack │ ├── ISigner.hack │ ├── IToken.hack │ ├── Parser.hack │ ├── Signer │ │ ├── Ecdsa.hack │ │ ├── Ecdsa │ │ │ ├── ISignatureConverter.hack │ │ │ ├── MultibyteStringConverter.hack │ │ │ ├── Sha256.hack │ │ │ ├── Sha384.hack │ │ │ └── Sha512.hack │ │ ├── Hmac.hack │ │ ├── Hmac │ │ │ ├── Sha256.hack │ │ │ ├── Sha384.hack │ │ │ └── Sha512.hack │ │ ├── Key.hack │ │ ├── None.hack │ │ ├── OpenSSL.hack │ │ ├── Rsa.hack │ │ └── Rsa │ │ │ ├── Sha256.hack │ │ │ ├── Sha384.hack │ │ │ └── Sha512.hack │ ├── Token.hack │ └── Token │ │ ├── Claims.hack │ │ ├── Headers.hack │ │ └── Signature.hack │ ├── Log │ ├── AbstractLogger.hack │ ├── BufferingLogger.hack │ ├── Exception │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ ├── LogicException.hack │ │ └── UnexpectedValueException.hack │ ├── Formatter │ │ ├── IFormatter.hack │ │ └── LineFormatter.hack │ ├── Handler │ │ ├── AbstractHandler.hack │ │ ├── FormattableHandlerTrait.hack │ │ ├── IFormattableHandler.hack │ │ ├── IHandler.hack │ │ ├── RotatingFileHandler.hack │ │ ├── StreamHandler.hack │ │ ├── SysLogFacility.hack │ │ └── SysLogHandler.hack │ ├── ILogger.hack │ ├── ILoggerAware.hack │ ├── LogLevel.hack │ ├── Logger.hack │ ├── LoggerAwareTrait.hack │ ├── LoggerTrait.hack │ ├── NullLogger.hack │ ├── Processor │ │ ├── CallableProcessor.hack │ │ ├── ContextProcessor.hack │ │ ├── IProcessor.hack │ │ └── MessageLengthProcessor.hack │ └── Record.hack │ ├── Markdown │ ├── Environment.hack │ ├── Extension │ │ ├── AbstractExtension.hack │ │ └── IExtension.hack │ └── XHPElement.hack │ ├── Mercure │ ├── Exception │ │ ├── IException.hack │ │ └── InvalidArgumentException.hack │ ├── IJwtProvider.hack │ ├── Provider │ │ └── StaticJwtProvider.hack │ ├── Publisher.hack │ └── Update.hack │ ├── Stopwatch │ ├── Event.hack │ ├── Exception │ │ ├── IException.hack │ │ └── LogicException.hack │ ├── Period.hack │ ├── Section.hack │ └── Stopwatch.hack │ ├── Translation │ ├── Catalogue │ │ ├── AbstractOperation.hack │ │ ├── IOperation.hack │ │ ├── MergeOperation.hack │ │ └── TargetOperation.hack │ ├── Exception │ │ ├── IException.hack │ │ ├── InvalidArgumentException.hack │ │ ├── InvalidResourceException.hack │ │ ├── LogicException.hack │ │ ├── NotFoundResourceException.hack │ │ └── RuntimeException.hack │ ├── Format.hack │ ├── Formatter │ │ ├── IMessageFormatter.hack │ │ └── MessageFormatter.hack │ ├── ILocaleAware.hack │ ├── ITranslator.hack │ ├── ITranslatorBag.hack │ ├── Loader │ │ ├── FileLoader.hack │ │ ├── ILoader.hack │ │ ├── IniFileLoader.hack │ │ ├── JsonFileLoader.hack │ │ └── TreeLoader.hack │ ├── LoggingTranslator.hack │ ├── MessageCatalogue.hack │ ├── Reader │ │ ├── ITranslationReader.hack │ │ └── TranslationReader.hack │ ├── Translator.hack │ └── _Private │ │ ├── LoaderContainer.hack │ │ └── Parents.hack │ └── Util │ ├── Exception │ └── IException.hack │ ├── Inflector.hack │ ├── Json │ ├── Errors.hack │ ├── Exception │ │ ├── JsonDecodeException.hack │ │ └── JsonEncodeException.hack │ ├── decode.hack │ ├── encode.hack │ ├── spec.hack │ └── structure.hack │ ├── Jsonable.hack │ ├── Stringable.hack │ ├── StringableTrait.hack │ ├── alternatives.hack │ └── stringify.hack └── tests └── Nuxed ├── Asset ├── PackageTest.hack ├── PackagesTest.hack ├── PathPackageTest.hack └── UrlPackageTest.hack ├── Container ├── ContainerBuilderTest.hack ├── ServiceContainerTest.hack └── ServiceDefinitionTest.hack ├── EventDispatcher ├── EventDispatcherTest.hack ├── Fixture │ ├── OrderCanceledEvent.hack │ ├── OrderCanceledEventListener.hack │ ├── OrderCreatedEvent.hack │ └── OrderCreatedEventListener.hack └── ListenerProvider │ ├── AttachableListenerProviderTest.hack │ ├── ListenerProviderAggregateTest.hack │ ├── PrioritizedListenerProviderTest.hack │ ├── RandomizedListenerProviderTest.hack │ └── ReifiedListenerProviderTest.hack ├── Filesystem ├── FileTest.hack ├── FolderTest.hack ├── IoTestTrait.hack ├── LinesTest.hack ├── NodeTest.hack ├── NodeTestTrait.hack └── PathTest.hack ├── Http ├── Message │ ├── CookieTest.hack │ ├── RequestTest.hack │ ├── ResponseTest.hack │ ├── ServerRequestTest.hack │ ├── StreamTest.hack │ ├── UploadedFileTest.hack │ └── UriTest.hack ├── Server │ ├── Handler │ │ └── CallableHandlerDecorator.hack │ ├── Middleware │ │ ├── CallableMiddlewareDecoratorTest.hack │ │ ├── HostMiddlewareDecoratorTest.hack │ │ ├── OriginalMessageMiddlewareTest.hack │ │ ├── PathMiddlewareDecoratorTest.hack │ │ ├── RequestFactoryTestTrait.hack │ │ └── RequestHandlerMiddlewareDecoratorTest.hack │ └── MiddlewareStackTest.hack └── Session │ ├── Persistence │ ├── AbstractSessionPersistenceTest.hack │ └── CacheSessionPersistenceTest.hack │ ├── SessionMiddlewareTest.hack │ └── SessionTest.hack ├── Mercure ├── PublisherTest.hack └── UpdateTest.hack ├── Translation ├── Catalogue │ ├── AbstractOperationTest.hack │ ├── MergeOperationTest.hack │ └── TargetOperationTest.hack ├── Formatter │ └── MessageFormatterTest.hack ├── Loader │ ├── IniFileLoaderTest.hack │ ├── JsonFileLoaderTest.hack │ ├── LoaderTest.hack │ └── TreeLoaderTest.hack ├── LoggingTranslatorTest.hack ├── MessageCatalogueTest.hack ├── Reader │ └── TranslationReaderTest.hack ├── TranslatorTest.hack └── fixtures │ ├── messages.en.ini │ ├── messages.en.json │ ├── messages.en.yaml │ ├── messages.fr.ini │ ├── messages.fr.json │ ├── messages.fr.yaml │ ├── user.en.ini │ ├── user.en.json │ └── user.en.yaml └── Util ├── AlternativesTest.hack ├── InflectorTest.hack └── JsonTest.hack /.gitattributes: -------------------------------------------------------------------------------- 1 | *.hack linguist-language=Hack 2 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: Stale 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Local folder 3 | local/ 4 | 5 | # Composer vendor folder 6 | vendor/ 7 | 8 | # HHAST parser cache files 9 | *.hhast.parser-cache 10 | 11 | # Temporary directory used for tests. 12 | tests/tmp/ 13 | -------------------------------------------------------------------------------- /.hhconfig: -------------------------------------------------------------------------------- 1 | assume_php=false 2 | enable_experimental_tc_features = reified_generics 3 | safe_array = true 4 | safe_vector_array = true 5 | unsafe_rx = false 6 | ignored_paths = [ "vendor/.+/tests/.+", "vendor/.+/bin/.+" ] -------------------------------------------------------------------------------- /.travis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | apt update -y 4 | DEBIAN_FRONTEND=noninteractive apt install -y php-cli zip unzip 5 | hhvm --version 6 | php --version 7 | 8 | ( 9 | cd $(mktemp -d) 10 | curl https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 11 | ) 12 | 13 | composer --version 14 | 15 | composer install 16 | 17 | composer check 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: generic 4 | services: docker 5 | env: 6 | - HHVM_VERSION=latest 7 | install: 8 | - docker pull hhvm/hhvm:$HHVM_VERSION 9 | script: 10 | - docker run --rm -w /var/source -v $(pwd):/var/source hhvm/hhvm:$HHVM_VERSION ./.travis.sh 11 | notifications: 12 | slack: 13 | secure: fZ74Yt9xxelrIZiWBe74X6zCNV+RCbI2aD0EjB8P6d3ovIdyHc+JOe/AFDwUwU8tuKBYt1DpiMnw/rFQQwu2Y6CQnAjgGtG+ScCCVhy5OJvHqFTmMt/XMs9Hrgdylak3IofaI6D/4Du+E9ZMXHgXGVgjQQr0SNMsj1s70sSd97oiW4t4Kn5hxlAbZK7EWCs2BWwyTtVJD96UOEJrBK59lD0wQvfv0wSV948Wwnms70cPgO26Fa+pdBGsv4Ho475Dzu/y4JuO/kqMMzodZtMSm7FNDrppwqgX3qYkfGBI/foQ5IpBn5gGcG5w3RhZLXsNhLDrULcEHtF1Ptfo5PQGUArN8KPRf91Mju2CGICg3wy6GMEm+iXHdPWzUkCaQPhw4ty6ix+fm2ELatXW4BGGXANJzL6UNUGuQPh4Z2oVeX8zEFpUAA+PJRzd6FPYdQDdI3Xj8P445x/KQ+Mg4f2wCR/YKTkjWbkYKzqvjvssgrDGkbQfhXWAOr5/NgKj0/vRovT66Tra14UncjZdM6yHUOqeMq5KfDboEBXoj7+jZG1cQmtSErUoFF2CUyI/Jqva7symsJbjOYSVTKv6BAgoL+CncdLcTPGFLzavLiavkqp4Gd3ErWoeOWqFY0ZYByY4cLcnwLtL0TrY6Y9fdzFeJLmN6xhxTteuw3Ils66K/fw= 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2019 Saif Eddin Gmati 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 | License 9 |

10 | 11 | # Nuxed 12 | 13 | ## High Performance, Asynchronous, Hack Framework for building web applications with expressive, elegant syntax. 14 | 15 | --- 16 | 17 | ### Security 18 | 19 | For information on reporting security vulnerabilities in Nuxed, see [SECURITY.md](SECURITY.md). 20 | 21 | --- 22 | 23 | ### License 24 | 25 | The Nuxed framework is open-sourced software licensed under the MIT-licensed. 26 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | If you discover a security vulnerability within Nuxed, please send an e-mail to Saif Eddin Gmati via azjezz@protonmail.com. 4 | 5 | Please withhold public disclosure until after we have addressed the vulnerability. 6 | 7 | There are no hard and fast rules to determine if a bug is worth reporting as a security issue. 8 | -------------------------------------------------------------------------------- /bin/format: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env hhvm 2 | > 14 | async function main(): Awaitable { 15 | Facebook\AutoloadMap\initialize(); 16 | $stdout = IO\request_output(); 17 | $files = await Asio\v( 18 | vec[ 19 | new Filesystem\Folder(__DIR__.'/../src/Nuxed'), 20 | new Filesystem\Folder(__DIR__.'/../tests/Nuxed'), 21 | ] 22 | |> Vec\map($$, ($node) ==> $node->files(false, true)), 23 | ); 24 | 25 | await ( 26 | Vec\concat(...$files) 27 | |> Vec\map($$, ($file) ==> format($file, $stdout, false)) 28 | |> Asio\v( 29 | Vec\concat( 30 | $$, 31 | vec[format(Filesystem\Node::load(__FILE__), $stdout, true)], 32 | ), 33 | ) 34 | ); 35 | 36 | await $stdout->writeAsync("\n"); 37 | exit(0); 38 | } 39 | 40 | /** 41 | * exec() blocks, so this is not actually async. 42 | */ 43 | async function format( 44 | Filesystem\File $file, 45 | IO\WriteHandle $stdout, 46 | bool $ignoreExtension = false, 47 | ): Awaitable { 48 | if ($ignoreExtension || 'hack' === $file->extension()) { 49 | $command = Str\format( 50 | 'bash -c "hackfmt -i %s >> /dev/null 2>&1 &"', 51 | $file->path()->toString(), 52 | ); 53 | 54 | concurrent { 55 | await async { 56 | exec($command); 57 | }; 58 | 59 | await $stdout->writeAsync('.'); 60 | } 61 | } else { 62 | await $stdout->writeAsync('S'); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /hh_autoload.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ 3 | "src" 4 | ], 5 | "devRoots": [ 6 | "tests" 7 | ], 8 | "devFailureHandler": "Facebook\\AutoloadMap\\HHClientFallbackHandler" 9 | } 10 | -------------------------------------------------------------------------------- /hhast-lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "roots": [ "src/" ], 3 | "builtinLinters": "all", 4 | "disabledLinters": [ 5 | "Facebook\\HHAST\\Linters\\LicenseHeaderLinter", 6 | "Facebook\\HHAST\\Linters\\AsyncFunctionAndMethodLinter", 7 | "Facebook\\HHAST\\Linters\\UseStatementWithAsLinter", 8 | "Facebook\\HHAST\\Linters\\CamelCasedMethodsUnderscoredFunctionsLinter" 9 | ], 10 | "disableAllAutoFixes": false, 11 | "overrides": [ 12 | { 13 | "patterns": [ 14 | "src/Nuxed/Http/Message/Stream.hack", 15 | "src/Nuxed/Http/Message/UploadedFile.hack", 16 | "src/Nuxed/Http/Emitter/SapiStreamEmitter.hack" 17 | ], 18 | "disabledLinters": [ 19 | "Facebook\\HHAST\\Linters\\DontAwaitInALoopLinter" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /src/Nuxed/Asset/Context/Context.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Context; 2 | 3 | class Context implements IContext { 4 | public function __construct(private string $basePath, private bool $secure) {} 5 | 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | public function getBasePath(): string { 10 | return $this->basePath; 11 | } 12 | 13 | /** 14 | * {@inheritdoc} 15 | */ 16 | public function isSecure(): bool { 17 | return $this->secure; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Context/IContext.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Context; 2 | 3 | interface IContext { 4 | /** 5 | * Gets the base path. 6 | * 7 | * @return string The base path 8 | */ 9 | public function getBasePath(): string; 10 | 11 | /** 12 | * Checks whether the request is secure or not. 13 | * 14 | * @return bool true if the request is secure, false otherwise 15 | */ 16 | public function isSecure(): bool; 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Context/NullContext.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Context; 2 | 3 | class NullContext implements IContext { 4 | /** 5 | * {@inheritdoc} 6 | */ 7 | public function getBasePath(): string { 8 | return ''; 9 | } 10 | 11 | /** 12 | * {@inheritdoc} 13 | */ 14 | public function isSecure(): bool { 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Exception; 2 | 3 | class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Exception/LogicException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Exception; 2 | 3 | class LogicException extends \LogicException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\Exception; 2 | 3 | class RuntimeException extends \RuntimeException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/IPackage.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset; 2 | 3 | interface IPackage { 4 | /** 5 | * Returns the asset version for an asset. 6 | * 7 | * @param string $path A path 8 | * 9 | * @return string The version string 10 | */ 11 | public function getVersion(string $path): string; 12 | 13 | /** 14 | * Returns an absolute or root-relative public path. 15 | * 16 | * @param string $path A path 17 | * 18 | * @return string The public path 19 | */ 20 | public function getUrl(string $path): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/Package.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset; 2 | 3 | use namespace HH\Lib\Str; 4 | 5 | /** 6 | * Basic package that adds a version to asset URLs. 7 | */ 8 | class Package implements IPackage { 9 | public function __construct( 10 | private VersionStrategy\IVersionStrategy $versionStrategy, 11 | private Context\IContext $context = new Context\NullContext(), 12 | ) { 13 | } 14 | 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function getVersion(string $path): string { 19 | return $this->versionStrategy->getVersion($path); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function getUrl(string $path): string { 26 | if ($this->isAbsoluteUrl($path)) { 27 | return $path; 28 | } 29 | 30 | return $this->versionStrategy->applyVersion($path); 31 | } 32 | 33 | protected function getContext(): Context\IContext { 34 | return $this->context; 35 | } 36 | 37 | protected function getVersionStrategy(): VersionStrategy\IVersionStrategy { 38 | return $this->versionStrategy; 39 | } 40 | 41 | protected function isAbsoluteUrl(string $url): bool { 42 | return Str\contains($url, '://') || Str\starts_with($url, '//'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/VersionStrategy/EmptyVersionStrategy.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\VersionStrategy; 2 | 3 | /** 4 | * Disable version for all assets. 5 | */ 6 | class EmptyVersionStrategy implements IVersionStrategy { 7 | /** 8 | * {@inheritdoc} 9 | */ 10 | public function getVersion(string $_path): string { 11 | return ''; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | public function applyVersion(string $path): string { 18 | return $path; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/VersionStrategy/IVersionStrategy.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\VersionStrategy; 2 | 3 | interface IVersionStrategy { 4 | /** 5 | * Returns the asset version for an asset. 6 | * 7 | * @param string $path A path 8 | * 9 | * @return string The version string 10 | */ 11 | public function getVersion(string $path): string; 12 | 13 | /** 14 | * Applies version to the supplied path. 15 | * 16 | * @param string $path A path 17 | * 18 | * @return string The versionized path 19 | */ 20 | public function applyVersion(string $path): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/VersionStrategy/JsonManifestVersionStrategy.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\VersionStrategy; 2 | 3 | use namespace HH\Asio; 4 | use namespace HH\Lib\Str; 5 | use namespace Nuxed\Filesystem; 6 | use namespace Nuxed\Util\Json; 7 | use namespace Nuxed\Asset\Exception; 8 | 9 | /** 10 | * Reads the versioned path of an asset from a JSON manifest file. 11 | * 12 | * For example, the manifest file might look like this: 13 | * { 14 | * "main.js": "main.abc123.js", 15 | * "css/styles.css": "css/styles.555abc.css" 16 | * } 17 | * 18 | * You could then ask for the version of "main.js" or "css/styles.css". 19 | */ 20 | class JsonManifestVersionStrategy implements IVersionStrategy { 21 | const type TManifest = KeyedContainer; 22 | private ?KeyedContainer $manifestData; 23 | 24 | public function __construct(private Filesystem\File $manifest) { 25 | } 26 | 27 | /** 28 | * With a manifest, we don't really know or care about what 29 | * the version is. Instead, this returns the path to the 30 | * versioned file. 31 | */ 32 | public function getVersion(string $path): string { 33 | return $this->applyVersion($path); 34 | } 35 | 36 | public function applyVersion(string $path): string { 37 | return $this->getManifestPath($path) ?? $path; 38 | } 39 | 40 | private function getManifestPath(string $path): ?string { 41 | if ($this->manifestData is null) { 42 | if (!$this->manifest->exists()) { 43 | throw new Exception\RuntimeException(Str\format( 44 | 'Asset manifest file "%s" does not exist.', 45 | $this->manifest->path()->toString(), 46 | )); 47 | } 48 | 49 | $this->manifestData = Json\structure( 50 | Asio\join($this->manifest->read()), 51 | type_structure($this, 'TManifest'), 52 | ); 53 | } 54 | 55 | return idx($this->manifestData, $path, null); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Nuxed/Asset/VersionStrategy/StaticVersionStrategy.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Asset\VersionStrategy; 2 | 3 | use namespace HH\Lib\Str; 4 | 5 | class StaticVersionStrategy implements IVersionStrategy { 6 | private string $format; 7 | 8 | /** 9 | * @param string $version Version number 10 | * @param string $format Url format 11 | */ 12 | public function __construct(private string $version, ?string $format = null) { 13 | $this->format = $format is nonnull && $format !== '' ? $format : '%s?%s'; 14 | } 15 | 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function getVersion(string $_path): string { 21 | return $this->version; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function applyVersion(string $path): string { 28 | $versionized = Str\format( 29 | /* HH_IGNORE_ERROR[4027] */ 30 | /* HH_IGNORE_ERROR[4110] */ 31 | $this->format, 32 | Str\trim_left($path, '/'), 33 | $this->getVersion($path), 34 | ); 35 | 36 | if ('' !== $path && '/' === $path[0]) { 37 | return '/'.$versionized; 38 | } 39 | 40 | return $versionized; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Exception/CacheException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Exception; 2 | 3 | class CacheException extends \Exception implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Exception; 2 | 3 | class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Exception/LogicException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Exception; 2 | 3 | class LogicException extends \LogicException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Serializer/ISerializer.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Serializer; 2 | 3 | /** 4 | * Serializes/Unserializes Hack values. 5 | * 6 | * Implementations of this interface MUST deal with errors carefully. They MUST 7 | * also deal with forward and backward compatibility at the storage format level. 8 | */ 9 | interface ISerializer { 10 | /** 11 | * Serialize a value. 12 | * 13 | * When serialization fails, no exception should be 14 | * thrown. Instead, this method should return null. 15 | */ 16 | public function serialize(mixed $value): ?string; 17 | 18 | /** 19 | * Unserializes a single value and throws and exception if anything goes wrong. 20 | */ 21 | public function unserialize(string $value): dynamic; 22 | } 23 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Serializer/JsonSerializer.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Serializer; 2 | 3 | use namespace Nuxed\Util\Json; 4 | 5 | class JsonSerializer implements ISerializer { 6 | /** 7 | * Serialize a value. 8 | * 9 | * When serialization fails, no exception should be 10 | * thrown. Instead, this method should return null. 11 | */ 12 | public function serialize(mixed $value): ?string { 13 | try { 14 | return Json\encode($value, false); 15 | } catch (Json\Exception\JsonEncodeException $e) { 16 | return null; 17 | } 18 | } 19 | 20 | /** 21 | * Unserializes a single value and throws and exception if anything goes wrong. 22 | */ 23 | public function unserialize(string $value): dynamic { 24 | return Json\decode($value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Serializer/NativeSerializer.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Serializer; 2 | 3 | class NativeSerializer implements ISerializer { 4 | /** 5 | * Serialize a value. 6 | * 7 | * When serialization fails, no exception should be 8 | * thrown. Instead, this method should return null. 9 | */ 10 | public function serialize(mixed $value): ?string { 11 | try { 12 | $val = @\serialize($value); 13 | if (false === $val) { 14 | return null; 15 | } 16 | 17 | return $val as string; 18 | } catch (\Throwable $e) { 19 | return null; 20 | } 21 | } 22 | 23 | /** 24 | * Unserializes a single value and throws and exception if anything goes wrong. 25 | */ 26 | public function unserialize(string $value): dynamic { 27 | if ('b:0;' === $value) { 28 | return false; 29 | } 30 | 31 | if ('N;' === $value) { 32 | return null; 33 | } 34 | 35 | try { 36 | $unserialized = \unserialize($value); 37 | 38 | if (false !== $unserialized) { 39 | return $unserialized; 40 | } 41 | 42 | $error = \error_get_last(); 43 | $message = (false === $error) || ($error['message'] is null) 44 | ? 'Failed to unserialize values' 45 | : $error['message'] as string; 46 | throw new \DomainException($message); 47 | } catch (\Error $e) { 48 | throw new \ErrorException( 49 | $e->getMessage(), 50 | (int)$e->getCode(), 51 | \E_ERROR, 52 | $e->getFile(), 53 | $e->getLine(), 54 | ); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Store/ApcStore.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Store; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Cache\Serializer; 5 | 6 | class ApcStore extends AbstractStore { 7 | public function __construct( 8 | string $namespace = '', 9 | int $defaultTtl = 0, 10 | protected Serializer\ISerializer $serializer = 11 | new Serializer\NativeSerializer(), 12 | ) { 13 | parent::__construct($namespace, $defaultTtl); 14 | } 15 | 16 | <<__Override>> 17 | protected async function doStore( 18 | string $id, 19 | mixed $value, 20 | int $ttl = 0, 21 | ): Awaitable { 22 | return \apc_store($id, $this->serializer->serialize($value), $ttl); 23 | } 24 | 25 | <<__Override>> 26 | protected async function doContains(string $id): Awaitable { 27 | return \apc_exists($id); 28 | } 29 | 30 | <<__Override>> 31 | protected async function doDelete(string $id): Awaitable { 32 | return \apc_delete($id); 33 | } 34 | 35 | <<__Override>> 36 | protected async function doGet(string $id): Awaitable { 37 | $exist = await $this->doContains($id); 38 | if (!$exist) { 39 | return null; 40 | } 41 | 42 | return $this->serializer->unserialize((string)\apc_fetch($id)); 43 | } 44 | 45 | <<__Override>> 46 | protected function doClear(string $namespace): Awaitable { 47 | if (Str\is_empty($namespace)) { 48 | return \apc_clear_cache(); 49 | } 50 | 51 | /* HH_IGNORE_ERROR[2049] */ 52 | $iterator = new APCIterator( 53 | Str\format('/^%s/', \preg_quote($namespace, '/')), 54 | /* HH_IGNORE_ERROR[2049] */ 55 | /* HH_IGNORE_ERROR[4106] */ 56 | APC_ITER_KEY, 57 | ); 58 | 59 | return \apc_delete($iterator); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Store/IStore.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Store; 2 | 3 | interface IStore { 4 | /** 5 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. 6 | */ 7 | public function store( 8 | string $id, 9 | T $value, 10 | ?int $ttl = null, 11 | ): Awaitable; 12 | 13 | /** 14 | * Sets a cache item to be persisted later. 15 | */ 16 | public function defer(string $id, mixed $value, ?int $ttl = null): bool; 17 | 18 | /** 19 | * Determines whether an item is present in the cache. 20 | */ 21 | public function contains(string $id): Awaitable; 22 | 23 | /** 24 | * Delete an item from the cache by its unique key. 25 | */ 26 | public function delete(string $id): Awaitable; 27 | 28 | /** 29 | * Fetches a value from the cache. 30 | */ 31 | public function get(string $id): Awaitable; 32 | 33 | /** 34 | * Wipes clean the entire cache's keys. 35 | */ 36 | public function clear(): Awaitable; 37 | 38 | /** 39 | * Persists any deferred cache items. 40 | */ 41 | public function commit(): Awaitable; 42 | } 43 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Store/MCRouterStore.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Store; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Cache\Serializer; 5 | 6 | class MCRouterStore extends AbstractStore { 7 | public function __construct( 8 | protected \MCRouter $mc, 9 | string $namespace = '', 10 | int $defaultTtl = 0, 11 | protected Serializer\ISerializer $serializer = 12 | new Serializer\NativeSerializer(), 13 | ) { 14 | parent::__construct($namespace, $defaultTtl); 15 | } 16 | 17 | <<__Override>> 18 | protected async function doGet(string $id): Awaitable { 19 | return $this->serializer->unserialize(await $this->mc->get($id)); 20 | } 21 | 22 | <<__Override>> 23 | protected async function doDelete(string $id): Awaitable { 24 | await $this->mc->del($id); 25 | return true; 26 | } 27 | 28 | <<__Override>> 29 | protected async function doContains(string $id): Awaitable { 30 | try { 31 | await $this->mc->get($id); 32 | return true; 33 | } catch (\MCRouterException $e) { 34 | return false; 35 | } 36 | } 37 | 38 | <<__Override>> 39 | protected async function doStore( 40 | string $id, 41 | mixed $value, 42 | int $ttl = 0, 43 | ): Awaitable { 44 | $value = $this->serializer->serialize($value); 45 | if ($value is null) { 46 | return false; 47 | } 48 | 49 | if (0 >= $ttl) { 50 | await $this->mc->set($id, $value); 51 | } else { 52 | await $this->mc->set($id, $value, 0, $ttl); 53 | } 54 | 55 | return true; 56 | } 57 | 58 | <<__Override>> 59 | protected async function doClear(string $namespace): Awaitable { 60 | if (Str\is_empty($namespace)) { 61 | await $this->mc->flushAll(); 62 | return true; 63 | } 64 | 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/Store/NullStore.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\Store; 2 | 3 | class NullStore implements IStore { 4 | /** 5 | * Persists data in the cache, uniquely referenced by a key with an optional expiration TTL time. 6 | */ 7 | public async function store( 8 | string $_id, 9 | mixed $_value, 10 | ?int $_ttl = null, 11 | ): Awaitable { 12 | return false; 13 | } 14 | 15 | /** 16 | * Sets a cache item to be persisted later. 17 | */ 18 | public function defer(string $_id, mixed $_value, ?int $_ttl = null): bool { 19 | return false; 20 | } 21 | 22 | /** 23 | * Determines whether an item is present in the cache. 24 | */ 25 | public async function contains(string $_id): Awaitable { 26 | return false; 27 | } 28 | 29 | /** 30 | * Delete an item from the cache by its unique key. 31 | */ 32 | public async function delete(string $_id): Awaitable { 33 | return false; 34 | } 35 | 36 | /** 37 | * Fetches a value from the cache. 38 | */ 39 | public async function get(string $_id): Awaitable { 40 | return null; 41 | } 42 | 43 | /** 44 | * Wipes clean the entire cache's keys. 45 | */ 46 | public async function clear(): Awaitable { 47 | return false; 48 | } 49 | 50 | /** 51 | * Persists any deferred cache items. 52 | */ 53 | public async function commit(): Awaitable { 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nuxed/Cache/private.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Cache\_Private; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Cache\Exception; 5 | 6 | 7 | /** 8 | * Validates a cache key. 9 | * 10 | * @throws InvalidArgumentException When $key is not valid 11 | */ 12 | function validate_key(string $key): void { 13 | if ('' === $key) { 14 | throw new Exception\InvalidArgumentException( 15 | 'Cache key length must be greater than zero', 16 | ); 17 | } 18 | 19 | foreach (vec['{', '}', '(', ')', '/', '\\', '@', ':'] as $c) { 20 | if (Str\contains($key, $c)) { 21 | throw new Exception\InvalidArgumentException(Str\format( 22 | 'Cache key "%s" contains reserved characters {}()/\@:', 23 | $key, 24 | )); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Nuxed/Container/ContainerBuilder.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | use namespace HH\Lib\{C, Dict, Str}; 4 | 5 | final class ContainerBuilder { 6 | private dict $definitions = dict[]; 7 | 8 | public function register(IServiceProvider $provider): this { 9 | $provider->register($this); 10 | 11 | return $this; 12 | } 13 | 14 | public function add( 15 | typename $service, 16 | IFactory $factory, 17 | bool $shared = true, 18 | ): this { 19 | $definition = new ServiceDefinition($service, $factory, $shared); 20 | $this->addDefinition($definition); 21 | return $this; 22 | } 23 | 24 | public function inflect( 25 | typename $service, 26 | IInflector $inflector, 27 | ): this { 28 | $definition = $this->getDefinition($service); 29 | $definition->inflect($inflector); 30 | 31 | return $this; 32 | } 33 | 34 | private function addDefinition(ServiceDefinition $definition): void { 35 | $this->definitions[$definition->getId()] = $definition; 36 | } 37 | 38 | private function getDefinition( 39 | typename $service, 40 | ): ServiceDefinition { 41 | if (C\contains_key($this->definitions, $service)) { 42 | /* HH_FIXME[4110] */ 43 | return $this->definitions[$service]; 44 | } 45 | 46 | throw new Exception\NotFoundException(Str\format( 47 | 'Container builder doesn\'t contain definition for service (%s).', 48 | $service, 49 | )); 50 | } 51 | 52 | public function build( 53 | Container $delegates = vec[], 54 | ): IServiceContainer { 55 | $definitions = Dict\map( 56 | $this->definitions, 57 | ($definition) ==> { 58 | $definition as ServiceDefinition<_>; 59 | return clone $definition; 60 | }, 61 | ); 62 | 63 | return new ServiceContainer( 64 | /* HH_IGNORE_ERROR[4110] */ 65 | $definitions, 66 | $delegates, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Exception/ContainerException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Exception; 2 | 3 | final class ContainerException extends \Exception implements IException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Exception; 2 | 3 | use namespace His\Container\Exception; 4 | 5 | interface IException extends Exception\ContainerExceptionInterface {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Exception/NotFoundException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Exception; 2 | 3 | use namespace His\Container\Exception; 4 | 5 | final class NotFoundException 6 | extends \Exception 7 | implements IException, Exception\NotFoundExceptionInterface {} 8 | -------------------------------------------------------------------------------- /src/Nuxed/Container/IFactory.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | interface IFactory { 4 | public function create(IServiceContainer $container): T; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Container/IInflector.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | interface IInflector { 4 | public function inflect(T $service, IServiceContainer $container): T; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Container/IServiceContainerAware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | interface IServiceContainerAware { 4 | public function setServiceContainer(IServiceContainer $container): void; 5 | 6 | public function hasServiceContainer(): bool; 7 | 8 | public function getServiceContainer(): IServiceContainer; 9 | } 10 | -------------------------------------------------------------------------------- /src/Nuxed/Container/IServiceProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | interface IServiceProvider { 4 | public function register(ContainerBuilder $builder): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Service/CallableFactoryDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Service; 2 | 3 | use namespace Nuxed\Container; 4 | 5 | class CallableFactoryDecorator implements Container\IFactory { 6 | public function __construct( 7 | private (function(Container\IServiceContainer): T) $call, 8 | ) {} 9 | 10 | public function create(Container\IServiceContainer $container): T { 11 | $call = $this->call; 12 | 13 | return $call($container); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Service/CallableInflectorDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Service; 2 | 3 | use namespace Nuxed\Container; 4 | 5 | class CallableInflectorDecorator implements Container\IInflector { 6 | public function __construct( 7 | private (function(T, Container\IServiceContainer): T) $call, 8 | ) {} 9 | 10 | public function inflect( 11 | T $service, 12 | Container\IServiceContainer $container, 13 | ): T { 14 | $call = $this->call; 15 | 16 | return $call($service, $container); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Service/Newable.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Service; 2 | 3 | <<__ConsistentConstruct>> 4 | abstract class Newable { 5 | public function __construct() {} 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Container/Service/NewableFactoryDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container\Service; 2 | 3 | use namespace Nuxed\Container; 4 | 5 | class NewableFactoryDecorator<<<__Newable>> T as Newable> 6 | implements Container\IFactory { 7 | 8 | public function __construct(private classname $service) {} 9 | 10 | public function create(?Container\IServiceContainer $_ = null): T { 11 | $class = $this->service; 12 | return new $class(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Nuxed/Container/ServiceContainerAwareTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | trait ServiceContainerAwareTrait implements IServiceContainerAware { 4 | public ?IServiceContainer $container = null; 5 | 6 | public function setServiceContainer(IServiceContainer $container): void { 7 | $this->container = $container; 8 | } 9 | 10 | public function hasServiceContainer(): bool { 11 | return $this->container is nonnull; 12 | } 13 | 14 | public function getServiceContainer(): IServiceContainer { 15 | return $this->container as nonnull; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Container/ServiceDefinition.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | 4 | final class ServiceDefinition { 5 | private vec> $inflectors = vec[]; 6 | private ?T $resolved = null; 7 | 8 | public function __construct( 9 | private typename $id, 10 | private IFactory $factory, 11 | private bool $shared = true, 12 | ) {} 13 | 14 | public function resolve(IServiceContainer $container): T { 15 | if ($this->isShared() && $this->resolved is nonnull) { 16 | return $this->resolved; 17 | } 18 | 19 | $object = $this->factory->create($container); 20 | foreach ($this->inflectors as $inflector) { 21 | $object = $inflector->inflect($object, $container); 22 | } 23 | 24 | return $this->resolved = $object; 25 | } 26 | 27 | public function getId(): typename { 28 | return $this->id; 29 | } 30 | 31 | public function getFactory(): IFactory { 32 | return $this->factory; 33 | } 34 | 35 | public function setFactory(IFactory $factory): this { 36 | $this->factory = $factory; 37 | $this->resolved = null; 38 | 39 | return $this; 40 | } 41 | 42 | public function isShared(): bool { 43 | return $this->shared; 44 | } 45 | 46 | public function setShared(bool $shared = true): this { 47 | $this->shared = $shared; 48 | 49 | return $this; 50 | } 51 | 52 | public function getInflectors(): Container> { 53 | return $this->inflectors; 54 | } 55 | 56 | public function inflect(IInflector $inflector): this { 57 | $this->inflectors[] = $inflector; 58 | $this->resolved = null; 59 | 60 | return $this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Nuxed/Container/functions.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Container; 2 | 3 | function factory((function(IServiceContainer): T) $factory): IFactory { 4 | return new Service\CallableFactoryDecorator($factory); 5 | } 6 | 7 | function newable<<<__Newable>> T as Service\Newable>( 8 | classname $service, 9 | ): Service\NewableFactoryDecorator { 10 | return new Service\NewableFactoryDecorator($service); 11 | } 12 | 13 | function inflector( 14 | (function(T, IServiceContainer): T) $inflector, 15 | ): IInflector { 16 | return new Service\CallableInflectorDecorator($inflector); 17 | } 18 | 19 | function alias(classname $alias): IFactory { 20 | return factory(($container) ==> $container->get($alias)); 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Contract/IReset.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Contract; 2 | 3 | /** 4 | * Provides a way to reset an object to its initial state. 5 | * 6 | * When calling the "reset()" method on an object, it should be put back to its 7 | * initial state. This usually means clearing any internal buffers and forwarding 8 | * the call to internal dependencies. All properties of the object should be put 9 | * back to the same state it had when it was first ready to use. 10 | * 11 | * This method could be called, for example, to recycle objects that are used as 12 | * services, so that they can be used to handle several requests in the same 13 | * process loop (note that we advise making your services stateless instead of 14 | * implementing this interface when possible.) 15 | */ 16 | interface IReset { 17 | public function reset(): void; 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment\Exception; 2 | 3 | <<__Sealed(InvalidArgumentException::class)>> 4 | interface IException {} 5 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment\Exception; 2 | 3 | final class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/_Private/State.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment\_Private; 2 | 3 | enum State: int { 4 | INITIAL = 0; 5 | UNQUOTED = 1; 6 | QUOTED = 2; 7 | ESCAPE = 3; 8 | WHITESPACE = 4; 9 | COMMENT = 5; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/add.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * add a variable to the environment if it doesn't exist. 5 | */ 6 | function add(string $name, string $value): void { 7 | if (!contains($name)) { 8 | put($name, $value); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/contains.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * Determine if a variable exists in the environment. 5 | */ 6 | function contains(string $name): bool { 7 | return get($name, null) is nonnull; 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/forget.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * Remove a variable from the environment. 5 | */ 6 | function forget(string $name): void { 7 | \putenv(_Private\Parser::parseName($name)); 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/get.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * Fetches a variable from the environment. 5 | */ 6 | function get(string $name, ?string $default = null): ?string { 7 | $value = \getenv(_Private\Parser::parseName($name)); 8 | if ($value is bool) { 9 | return $default; 10 | } 11 | 12 | return _Private\Parser::parseValue($value); 13 | } 14 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/is_debug.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | use namespace HH\Lib\Str; 4 | 5 | function is_debug(): bool { 6 | $debug = get('APP_DEBUG'); 7 | if ($debug is null) { 8 | return false; 9 | } 10 | 11 | $debug = Str\lowercase($debug); 12 | if ($debug === 'false' || $debug === 'off') { 13 | return false; 14 | } 15 | 16 | return (bool)$debug; 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/is_dev.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | use namespace HH\Lib\Str; 4 | 5 | function is_dev(): bool { 6 | return Str\starts_with( 7 | Str\lowercase(get('APP_ENV', 'prod') as string), 8 | 'dev', 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/load.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | use namespace Nuxed\Filesystem; 4 | use namespace HH\Asio; 5 | use namespace HH\Lib\Str; 6 | 7 | /** 8 | * Load a .env file into the current environment. 9 | */ 10 | async function load(string $file, bool $override = false): Awaitable { 11 | $file = Filesystem\Path::create($file); 12 | $file = new Filesystem\File($file, false); 13 | $lines = await $file->lines(); 14 | $variables = vec[]; 15 | foreach ($lines as $line) { 16 | $variables[] = async { 17 | $trimmed = Str\trim($line); 18 | // ignore comments and empty lines 19 | if (Str\starts_with($trimmed, '#') || Str\is_empty($trimmed)) { 20 | return; 21 | } 22 | 23 | list($name, $value) = parse($line); 24 | if ($value is nonnull) { 25 | $override ? put($name, $value) : add($name, $value); 26 | } 27 | }; 28 | } 29 | 30 | await Asio\v($variables); 31 | } 32 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/parse.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * Parse the given environment variable entry into a name and value. 5 | */ 6 | function parse(string $entry): (string, ?string) { 7 | return _Private\Parser::parse($entry); 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Environment/put.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Environment; 2 | 3 | /** 4 | * Store a variable in the environment. 5 | */ 6 | function put(string $name, string $value): void { 7 | list($name, $value) = parse($name.'='.$value); 8 | \putenv($name.'='.$value); 9 | } 10 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/CallableEventListener.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | final class CallableEventListener implements IEventListener { 4 | public function __construct( 5 | private (function(T): Awaitable) $listener, 6 | ) {} 7 | 8 | public function process(T $event): Awaitable { 9 | return ($this->listener)($event); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ErrorEvent.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | use namespace HH\ReifiedGenerics; 4 | 5 | final class ErrorEvent extends \Exception implements IEvent { 6 | public function __construct( 7 | private T $event, 8 | private IEventListener $listener, 9 | private \Exception $e, 10 | ) { 11 | parent::__construct($e->getMessage(), $e->getCode(), $e); 12 | } 13 | 14 | public function getEventType(): classname { 15 | /* HH_FIXME[2049] */ 16 | /* HH_FIXME[4107] */ 17 | return ReifiedGenerics\getClassname(); 18 | } 19 | 20 | public function getEvent(): T { 21 | return $this->event; 22 | } 23 | 24 | public function getListener(): IEventListener { 25 | return $this->listener; 26 | } 27 | 28 | public function getException(): \Exception { 29 | return $this->e; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\Exception; 2 | 3 | <<__Sealed(InvalidListenerException::class)>> 4 | interface IException { 5 | require extends \Exception; 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/Exception/InvalidListenerException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\Exception; 2 | 3 | final class InvalidListenerException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/IEvent.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | /** 4 | * Marker interface indicating an event instance. 5 | * 6 | * Event instances may contain zero methods, or as many methods as they 7 | * want. The interface MUST be implemented, however, to provide type-safety 8 | * to both listeners as well as the dispatcher. 9 | */ 10 | interface IEvent {} 11 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/IEventDispatcher.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | /** 4 | * Defines a dispatcher for events. 5 | */ 6 | interface IEventDispatcher { 7 | /** 8 | * Provide all relevant listeners with an event to process. 9 | * 10 | * @template T as IEvent 11 | * 12 | * @return T The Event that was passed, now modified by listeners. 13 | */ 14 | public function dispatch(T $event): Awaitable; 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/IEventListener.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | /** 4 | * Defines a listener for an event. 5 | */ 6 | interface IEventListener { 7 | /** 8 | * Process the given event. 9 | */ 10 | public function process(T $event): Awaitable; 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/IStoppableEvent.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | /** 4 | * An Event whose processing may be interrupted when the event has been handled. 5 | * 6 | * A Dispatcher implementation MUST check to determine an Event 7 | * is marked as stopped after each listener is called. If it is then it should 8 | * return immediately without calling any further Listeners. 9 | */ 10 | interface IStoppableEvent extends IEvent { 11 | /** 12 | * Is propagation stopped? 13 | * 14 | * This will typically only be used by the Dispatcher to determine if the 15 | * previous listener halted propagation. 16 | * 17 | * @return bool 18 | * True if the Event is complete and no further listeners should be called. 19 | * False to continue calling listeners. 20 | */ 21 | public function isPropagationStopped(): bool; 22 | } 23 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/AttachableListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace HH\Lib\C; 4 | use namespace Nuxed\EventDispatcher; 5 | 6 | class AttachableListenerProvider implements IAttachableListenerProvider { 7 | private dict< 8 | classname, 9 | vec>, 10 | > $listeners = dict[]; 11 | 12 | public function listen( 13 | classname $event, 14 | EventDispatcher\IEventListener $listener, 15 | ): void { 16 | $listeners = $this->listeners[$event] ?? vec[]; 17 | if (C\contains($listeners, $listener)) { 18 | // duplicate detected 19 | return; 20 | } 21 | 22 | $listeners[] = $listener; 23 | /* HH_FIXME[4110] */ 24 | $this->listeners[$event] = $listeners; 25 | } 26 | 27 | public async function getListeners( 28 | T $event, 29 | ): AsyncIterator> { 30 | foreach ($this->listeners as $type => $listeners) { 31 | if (\is_a($event, $type)) { 32 | foreach ($listeners as $listener) { 33 | /* HH_FIXME[4110] */ 34 | yield $listener; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/IAttachableListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | interface IAttachableListenerProvider extends IListenerProvider { 6 | public function listen( 7 | classname $event, 8 | EventDispatcher\IEventListener $listener, 9 | ): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/IListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | /** 6 | * Mapper from an event to the listeners that are applicable to that event. 7 | */ 8 | interface IListenerProvider { 9 | /** 10 | * @template T as EventDispatcher\IEvent 11 | * 12 | * @param T $event 13 | * An event for which to return the relevant listeners. 14 | * @return AsyncIterator> 15 | * An async iterator (usually an async generator) of listeners. Each 16 | * listener MUST be type-compatible with $event. 17 | */ 18 | public function getListeners( 19 | T $event, 20 | ): AsyncIterator>; 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/IPrioritizedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | interface IPrioritizedListenerProvider extends IAttachableListenerProvider { 6 | public function listen( 7 | classname $event, 8 | EventDispatcher\IEventListener $listener, 9 | int $priority = 1, 10 | ): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/IRandomizedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | interface IRandomizedListenerProvider extends IAttachableListenerProvider {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/IReifiedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | interface IReifiedListenerProvider extends IListenerProvider { 6 | /** 7 | * Attach a listener 8 | * 9 | * Note: IReifiedListenerProvider::listen must use reified generics. 10 | * 11 | * use RefiedGenerics\getClassname to determine the event type. 12 | */ 13 | public function listen<<<__Enforceable>> T as EventDispatcher\IEvent>( 14 | EventDispatcher\IEventListener $listener, 15 | ): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/ListenerProviderAggregate.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | final class ListenerProviderAggregate implements IListenerProvider { 6 | private vec $providers = vec[]; 7 | 8 | public async function getListeners( 9 | T $event, 10 | ): AsyncIterator> { 11 | foreach ($this->providers as $provider) { 12 | foreach ($provider->getListeners($event) await as $listener) { 13 | yield $listener; 14 | } 15 | } 16 | } 17 | 18 | public function attach(IListenerProvider $provider): void { 19 | $this->providers[] = $provider; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/PrioritizedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace HH\Lib\{C, Str, Vec}; 4 | use namespace Nuxed\EventDispatcher; 5 | 6 | class PrioritizedListenerProvider implements IPrioritizedListenerProvider { 7 | private dict, 9 | vec>, 10 | >> $listeners = dict[]; 11 | 12 | public function listen( 13 | classname $event, 14 | EventDispatcher\IEventListener $listener, 15 | int $priority = 1, 16 | ): void { 17 | $priority = Str\format('%d.0', $priority); 18 | if ( 19 | C\contains_key($this->listeners, $priority) && 20 | C\contains_key($this->listeners[$priority], $event) && 21 | C\contains($this->listeners[$priority][$event], $listener) 22 | ) { 23 | return; 24 | } 25 | 26 | $priorityListeners = $this->listeners[$priority] ?? dict[]; 27 | $eventListeners = $priorityListeners[$event] ?? vec[]; 28 | $eventListeners[] = $listener; 29 | $priorityListeners[$event] = $eventListeners; 30 | /* HH_FIXME[4110] */ 31 | $this->listeners[$priority] = $priorityListeners; 32 | } 33 | 34 | public async function getListeners( 35 | T $event, 36 | ): AsyncIterator> { 37 | $priorities = Vec\keys($this->listeners) 38 | |> Vec\sort($$, ($a, $b) ==> $a <=> $b); 39 | 40 | foreach ($priorities as $priority) { 41 | foreach ($this->listeners[$priority] as $eventName => $listeners) { 42 | if (\is_a($event, $eventName)) { 43 | foreach ($listeners as $listener) { 44 | /* HH_FIXME[4110] */ 45 | yield $listener; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/RandomizedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace HH\Lib\{C, Vec}; 4 | use namespace Nuxed\EventDispatcher; 5 | 6 | class RandomizedListenerProvider implements IRandomizedListenerProvider { 7 | private dict< 8 | classname, 9 | vec>, 10 | > $listeners = dict[]; 11 | 12 | public function listen( 13 | classname $event, 14 | EventDispatcher\IEventListener $listener, 15 | ): void { 16 | $listeners = $this->listeners[$event] ?? vec[]; 17 | if (C\contains($listeners, $listener)) { 18 | // duplicate detected 19 | return; 20 | } 21 | 22 | $listeners[] = $listener; 23 | /* HH_FIXME[4110] */ 24 | $this->listeners[$event] = $listeners; 25 | } 26 | 27 | public async function getListeners( 28 | T $event, 29 | ): AsyncIterator> { 30 | $listeners = vec[]; 31 | foreach ($this->listeners as $type => $eventListeners) { 32 | if (\is_a($event, $type)) { 33 | $listeners = Vec\concat($listeners, $eventListeners); 34 | } 35 | } 36 | 37 | foreach (Vec\shuffle($listeners) as $listener) { 38 | /* HH_FIXME[4110] */ 39 | yield $listener; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/ListenerProvider/ReifiedListenerProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher\ListenerProvider; 2 | 3 | use namespace HH\Lib\C; 4 | use namespace HH\ReifiedGenerics; 5 | use namespace Nuxed\EventDispatcher; 6 | 7 | class ReifiedListenerProvider implements IReifiedListenerProvider { 8 | private dict< 9 | classname, 10 | vec>, 11 | > $listeners = dict[]; 12 | 13 | public function listen<<<__Enforceable>> reify T as EventDispatcher\IEvent>( 14 | EventDispatcher\IEventListener $listener, 15 | ): void { 16 | /* HH_FIXME[2049] */ 17 | /* HH_FIXME[4107] */ 18 | $event = ReifiedGenerics\getClassname(); 19 | 20 | $listeners = $this->listeners[$event] ?? vec[]; 21 | if (C\contains($listeners, $listener)) { 22 | // duplicate detected 23 | return; 24 | } 25 | 26 | $listeners[] = $listener; 27 | /* HH_FIXME[4110] */ 28 | $this->listeners[$event] = $listeners; 29 | } 30 | 31 | public async function getListeners( 32 | T $event, 33 | ): AsyncIterator> { 34 | foreach ($this->listeners as $type => $listeners) { 35 | if (\is_a($event, $type)) { 36 | foreach ($listeners as $listener) { 37 | /* HH_FIXME[4110] */ 38 | yield $listener; 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Nuxed/EventDispatcher/functional.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\EventDispatcher; 2 | 3 | use namespace His\Container; 4 | 5 | /** 6 | * Helper function to create an event listener, 7 | * from a callable. 8 | */ 9 | function f( 10 | (function(T): Awaitable) $listener, 11 | ): IEventListener { 12 | return new CallableEventListener($listener); 13 | } 14 | 15 | /** 16 | * Helper function to create a lazy loaded event listener. 17 | */ 18 | function lazy( 19 | Container\ContainerInterface $container, 20 | classname> $service, 21 | ): IEventListener { 22 | return f(($event) ==> { 23 | return $container->get($service)->process($event); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/ExistingNodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception thrown when a target node destination already exists. 5 | */ 6 | class ExistingNodeException extends RuntimeException implements IException { 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | use type InvalidArgumentException as ParentException; 4 | 5 | class InvalidArgumentException extends ParentException implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/InvalidPathException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception thrown when an invalid file path is used. 5 | */ 6 | class InvalidPathException extends RuntimeException implements IException { 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/MissingNodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception thrown when a node does not exist. 5 | */ 6 | class MissingNodeException extends RuntimeException implements IException { 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/OutOfRangeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | use type OutOfRangeException as ParentException; 4 | 5 | class OutOfRangeException extends ParentException implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/ReadErrorException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception thrown when a reading a files fails. 5 | */ 6 | class ReadErrorException extends RuntimeException implements IException { 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | use type RuntimeException as ParentException; 4 | 5 | class RuntimeException extends ParentException implements IException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/UnreadableNodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception throw when trying to read or retrieve 5 | * a read handle of an unreadable node. 6 | */ 7 | class UnreadableNodeException extends RuntimeException implements IException {} 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/UnwritableNodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception throw when trying to write or retrieve 5 | * a write handle of an unwritable node. 6 | */ 7 | class UnwritableNodeException extends RuntimeException implements IException {} 8 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Exception/WriteErrorException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem\Exception; 2 | 3 | /** 4 | * Exception thrown when a writing a files fails. 5 | */ 6 | class WriteErrorException extends RuntimeException implements IException {} 7 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/Lines.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem; 2 | 3 | use namespace HH\Lib\{C, Str, Vec}; 4 | use type HH\InvariantException; 5 | use type Nuxed\Util\StringableTrait; 6 | use type Iterator; 7 | use type Countable; 8 | use type IteratorAggregate; 9 | 10 | final class Lines implements Countable, IteratorAggregate { 11 | use StringableTrait; 12 | 13 | public function __construct(private Container $lines) { 14 | } 15 | 16 | public function count(): int { 17 | return C\count($this->lines); 18 | } 19 | 20 | public function first(): string { 21 | try { 22 | return C\firstx($this->lines); 23 | } catch (InvariantException $e) { 24 | throw new Exception\OutOfRangeException( 25 | 'Lines instance is empty.', 26 | $e->getCode(), 27 | $e, 28 | ); 29 | } 30 | } 31 | 32 | /** 33 | * @return tuple(string, Lines) a tuple of the first line and the rest of 34 | * the lines as a new Lines instance. 35 | */ 36 | public function jump(): (string, Lines) { 37 | return tuple($this->first(), new self(Vec\drop($this->lines, 1))); 38 | } 39 | 40 | public static function blank(string $line): bool { 41 | return Str\trim($line, " \t") === ''; 42 | } 43 | 44 | public function getIterator(): Iterator { 45 | return (new Vector($this->lines))->getIterator(); 46 | } 47 | 48 | public function toString(): string { 49 | return Str\join($this->lines, "\n"); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Nuxed/Filesystem/OperationType.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Filesystem; 2 | 3 | enum OperationType: int { 4 | /* 5 | * Will overwrite the destination file if one exists (file and folder) 6 | */ 7 | OVERWRITE = 0; 8 | 9 | /* 10 | * Will merge folders together if they exist at the same location (folder only) 11 | */ 12 | MERGE = 1; 13 | 14 | /* 15 | * Will not overwrite the destination file if it exists (file and folder) 16 | */ 17 | SKIP = 2; 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client\Exception; 2 | 3 | /** 4 | * Every HTTP client related exception MUST implement this interface. 5 | */ 6 | interface IException { 7 | require extends \Exception; 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client\Exception; 2 | 3 | final class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/Exception/NetworkException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client\Exception; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | /** 6 | * Thrown when the request cannot be completed because of network issues. 7 | * 8 | * There is no response object as this exception is thrown when no response has been received. 9 | * 10 | * Example: the target host name can not be resolved or the connection failed. 11 | */ 12 | final class NetworkException extends \RuntimeException implements IException { 13 | public function __construct( 14 | private Message\Request $request, 15 | string $message = '', 16 | int $code = 0, 17 | ?\Exception $previous = null, 18 | ) { 19 | parent::__construct($message, $code, $previous); 20 | } 21 | 22 | /** 23 | * Returns the request. 24 | * 25 | * The request object MAY be a different object from the one passed to IHttpClient::sendRequest() 26 | * 27 | * @return Message\Request 28 | */ 29 | public function getRequest(): Message\Request { 30 | return $this->request; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/Exception/RequestException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client\Exception; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | /** 6 | * Exception for when a request failed. 7 | * 8 | * Examples: 9 | * - Request is invalid (e.g. method is missing) 10 | * - Runtime request errors (e.g. the body stream is not seekable) 11 | */ 12 | final class RequestException extends \RuntimeException implements IException { 13 | public function __construct( 14 | private Message\Request $request, 15 | string $message = '', 16 | int $code = 0, 17 | ?\Exception $previous = null, 18 | ) { 19 | parent::__construct($message, $code, $previous); 20 | } 21 | 22 | /** 23 | * Returns the request. 24 | * 25 | * The request object MAY be a different object from the one passed to IHttpClient::sendRequest() 26 | * 27 | * @return Message\Request 28 | */ 29 | public function getRequest(): Message\Request { 30 | return $this->request; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/IHttpClient.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | interface IHttpClient { 6 | /** 7 | * Sends a request and returns a response. 8 | * 9 | * @throws Exception\IException If an error happens while processing the request. 10 | */ 11 | public function send( 12 | Message\Request $request, 13 | HttpClientOptions $options = shape(), 14 | ): Awaitable; 15 | 16 | /** 17 | * Create and send an HTTP request. 18 | * 19 | * Use an absolute path to override the base path of the client, or a 20 | * relative path to append to the base path of the client. The URL can 21 | * contain the query string as well. 22 | * 23 | * @throws Exception\IException If an error happens while processing the request. 24 | */ 25 | public function request( 26 | string $method, 27 | string $uri, 28 | HttpClientOptions $options = shape(), 29 | ): Awaitable; 30 | } 31 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/MockHttpClient.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | final class MockHttpClient extends HttpClient { 6 | public function __construct( 7 | private (function(Message\Request): Awaitable) $handler, 8 | HttpClientOptions $options = shape(), 9 | ) { 10 | parent::__construct($options); 11 | } 12 | 13 | /** 14 | * Process the request and returns a response. 15 | * 16 | * @throws Exception\IException If an error happens while processing the request. 17 | */ 18 | <<__Override>> 19 | public function process( 20 | Message\Request $request, 21 | ): Awaitable { 22 | return ($this->handler)($request); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Client/_Private/Structure.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Client\_Private; 2 | 3 | use namespace Nuxed\Http\Client; 4 | 5 | final abstract class Structure { 6 | const type HttpClientOptions = Client\HttpClientOptions; 7 | 8 | public static function HttpClientOptions( 9 | ): TypeStructure { 10 | return type_structure(static::class, 'HttpClientOptions'); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Emitter/Emitter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Emitter; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | final class Emitter implements IEmitter { 6 | private IEmitter $sapi; 7 | private IEmitter $stream; 8 | 9 | public function __construct(MaxBufferLength $length = 8192) { 10 | $this->sapi = new SapiEmitter(); 11 | $this->stream = new SapiStreamEmitter($length); 12 | } 13 | 14 | public function emit(Message\Response $response): Awaitable { 15 | if ( 16 | !$response->hasHeader('Content-Disposition') && 17 | !$response->hasHeader('Content-Range') 18 | ) { 19 | return $this->sapi->emit($response); 20 | } 21 | 22 | return $this->stream->emit($response); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Emitter/Exception/EmitterException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Emitter\Exception; 2 | 3 | final class EmitterException extends \RuntimeException implements IException { 4 | public static function forHeadersSent(): this { 5 | return new static('Unable to emit response; headers already sent'); 6 | } 7 | 8 | public static function forOutputSent(): this { 9 | return new static( 10 | 'Output has been emitted previously; cannot emit response', 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Emitter/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Emitter\Exception; 2 | 3 | /** 4 | * Marker interface for component exceptions. 5 | */ 6 | interface IException { 7 | require extends \Exception; 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Emitter/IEmitter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Emitter; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | interface IEmitter { 6 | /** 7 | * Emit a response. 8 | * 9 | * Emits a response, including status line, headers, and the message body, 10 | * according to the environment. 11 | * 12 | * Implementations of this method may be written in such a way as to have 13 | * side effects, such as usage of header() or pushing output to the 14 | * output buffer. 15 | * 16 | * Implementations MAY raise exceptions if they are unable to emit the 17 | * response; e.g., if headers have already been sent. 18 | * 19 | * Implementations MUST return a boolean. A boolean `true` indicates that 20 | * the emitter was able to emit the response, while `false` indicates 21 | * it was not. 22 | */ 23 | public function emit(Message\Response $response): Awaitable; 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Error/IErrorHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Error; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | interface IErrorHandler { 6 | /** 7 | * Handle the error and return a response instance. 8 | */ 9 | public function handle( 10 | \Throwable $error, 11 | Message\ServerRequest $request, 12 | ): Awaitable; 13 | } 14 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Error/Middleware/ErrorMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Error\Middleware; 2 | 3 | use namespace Nuxed\Http\{Error, Message, Server}; 4 | 5 | class ErrorMiddleware implements Server\IMiddleware { 6 | public function __construct(private Error\IErrorHandler $handler) {} 7 | 8 | public async function process( 9 | Message\ServerRequest $request, 10 | Server\IHandler $handler, 11 | ): Awaitable { 12 | try { 13 | return await $handler->handle($request); 14 | } catch (\Throwable $e) { 15 | return await $this->handler->handle($e, $request); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Flash/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Flash\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Flash/Exception/InvalidHopsValueException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Flash\Exception; 2 | 3 | class InvalidHopsValueException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Flash/FlashMessagesMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Flash; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | final class FlashMessagesMiddleware implements Server\IMiddleware { 6 | public function __construct( 7 | private string $key = FlashMessages::FLASH_NEXT, 8 | ) {} 9 | 10 | public async function process( 11 | Message\ServerRequest $request, 12 | Server\IHandler $handler, 13 | ): Awaitable { 14 | $session = $request->getSession(); 15 | $flash = FlashMessages::create($session, $this->key); 16 | return await $handler->handle($request->withFlash($flash)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/CookieSameSite.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | /** 4 | * Enum representing the cookie Same-Site values. 5 | * 6 | * @link https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1 7 | */ 8 | enum CookieSameSite: string { 9 | LAX = 'Lax'; 10 | STRICT = 'Strict'; 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/ConflictingHeadersException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | /** 4 | * The HTTP request contains headers with conflicting information. 5 | */ 6 | class ConflictingHeadersException 7 | extends \UnexpectedValueException 8 | implements IException { 9 | } 10 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | /** 4 | * Marker interface for component-specific exceptions. 5 | */ 6 | <<__Sealed( 7 | RuntimeException::class, 8 | InvalidArgumentException::class, 9 | UnrecognizedProtocolVersionException::class, 10 | ConflictingHeadersException::class, 11 | SuspiciousOperationException::class, 12 | )>> 13 | interface IException { 14 | } 15 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | <<__Sealed( 4 | UnreadableStreamException::class, 5 | UnwritableStreamException::class, 6 | UntellableStreamException::class, 7 | UploadedFileErrorException::class, 8 | UploadedFileAlreadyMovedException::class, 9 | )>> 10 | class RuntimeException extends \RuntimeException implements IException { 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/SuspiciousOperationException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | /** 4 | * Raised when a user has performed an operation that should be considered 5 | * suspicious from a security perspective. 6 | */ 7 | class SuspiciousOperationException 8 | extends \UnexpectedValueException 9 | implements IException { 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UnreadableStreamException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UnreadableStreamException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UnrecognizedProtocolVersionException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UnrecognizedProtocolVersionException 4 | extends \UnexpectedValueException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UnseekableStreamException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | use type RuntimeException; 4 | 5 | final class UnseekableStreamException extends RuntimeException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UntellableStreamException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UntellableStreamException extends RuntimeException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UnwritableStreamException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UnwritableStreamException extends RuntimeException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UploadedFileAlreadyMovedException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UploadedFileAlreadyMovedException extends RuntimeException { 4 | public function __construct( 5 | string $message = 'Cannot retrieve stream after it has already moved.', 6 | int $code = 0, 7 | ?\Exception $previous = null, 8 | ) { 9 | parent::__construct($message, $code, $previous); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Exception/UploadedFileErrorException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Exception; 2 | 3 | final class UploadedFileErrorException extends RuntimeException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/IStream.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | use namespace HH\Lib\Experimental\IO; 4 | 5 | /** 6 | * Describes a data stream. 7 | * 8 | * Typically, an instance will wrap a Hack stream; this interface provides 9 | * a wrapper around the most common operations. 10 | */ 11 | interface IStream extends IO\ReadHandle, IO\WriteHandle { 12 | /** 13 | * Seek to a position in the stream. 14 | */ 15 | public function seek( 16 | int $offset, 17 | StreamSeekWhence $whence = StreamSeekWhence::SET, 18 | ): void; 19 | 20 | /** 21 | * Seek to the beginning of the stream. 22 | * 23 | * If the stream is not seekable, this method will raise an exception; 24 | * otherwise, it will perform a seek(0). 25 | */ 26 | public function rewind(): void; 27 | 28 | /** 29 | * Returns the current position of the file read/write pointer 30 | * 31 | * @return int Position of the file pointer 32 | */ 33 | public function tell(): int; 34 | 35 | /** 36 | * Returns whether or not the stream is writable. 37 | */ 38 | public function isWritable(): bool; 39 | 40 | /** 41 | * Returns whether or not the stream is readable. 42 | */ 43 | public function isReadable(): bool; 44 | 45 | /** 46 | * Returns whether or not the stream is seekable. 47 | */ 48 | public function isSeekable(): bool; 49 | } 50 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Request.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | use namespace HH\Lib\{C, Dict}; 4 | 5 | class Request { 6 | use RequestTrait; 7 | 8 | public function __construct( 9 | string $method, 10 | Uri $uri, 11 | KeyedContainer> $headers = dict[], 12 | ?IStream $body = null, 13 | string $version = '1.1', 14 | ) { 15 | $this->method = $method; 16 | $this->uri = $uri; 17 | $this->setHeaders($headers); 18 | $this->protocol = $version; 19 | 20 | if (!$this->hasHeader('Host')) { 21 | $this->updateHostFromUri(); 22 | } 23 | 24 | if ($body is nonnull) { 25 | $this->stream = $body; 26 | } 27 | } 28 | 29 | <<__Override>> 30 | protected function updateHostFromUri(): void { 31 | $host = $this->uri->getHost(); 32 | if ('' === $host) { 33 | return; 34 | } 35 | 36 | $port = $this->uri->getPort(); 37 | 38 | if ($port is nonnull) { 39 | $host .= ':'.((string)$port); 40 | } 41 | 42 | if (C\contains_key($this->headerNames, 'host')) { 43 | $header = $this->headerNames['host']; 44 | } else { 45 | $header = 'Host'; 46 | $this->headerNames['host'] = 'Host'; 47 | } 48 | 49 | if (C\contains_key($this->headers, $header)) { 50 | unset($this->headers[$header]); 51 | } 52 | 53 | $this->headers = Dict\merge(dict[$header => vec[$host]], $this->headers); 54 | } 55 | 56 | public function __clone(): void { 57 | $this->messageClone(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Request/functions.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Request; 2 | 3 | use namespace Nuxed\Util\Json; 4 | use namespace Nuxed\Http\Message; 5 | 6 | function json( 7 | Message\Uri $uri, 8 | mixed $data, 9 | string $method = 'POST', 10 | KeyedContainer> $headers = dict[], 11 | string $version = '1.1', 12 | ): Message\Request { 13 | $flags = \JSON_HEX_TAG | \JSON_HEX_APOS | \JSON_HEX_AMP | \JSON_HEX_QUOT; 14 | $stream = Message\stream(Json\encode($data, false, $flags)); 15 | $headers = dict($headers); 16 | $headers['content-type'] ??= vec['application/json']; 17 | return Message\request($method, $uri, $headers, $stream, $version); 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/RequestMethod.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | /** 4 | * Defines constants for common HTTP request methods. 5 | */ 6 | final abstract class RequestMethod { 7 | const string METHOD_HEAD = 'HEAD'; 8 | const string METHOD_GET = 'GET'; 9 | const string METHOD_POST = 'POST'; 10 | const string METHOD_PUT = 'PUT'; 11 | const string METHOD_PATCH = 'PATCH'; 12 | const string METHOD_DELETE = 'DELETE'; 13 | const string METHOD_PURGE = 'PURGE'; 14 | const string METHOD_OPTIONS = 'OPTIONS'; 15 | const string METHOD_TRACE = 'TRACE'; 16 | const string METHOD_CONNECT = 'CONNECT'; 17 | const string METHOD_REPORT = 'REPORT'; 18 | const string METHOD_LOCK = 'LOCK'; 19 | const string METHOD_UNLOCK = 'UNLOCK'; 20 | const string METHOD_COPY = 'COPY'; 21 | const string METHOD_MOVE = 'MOVE'; 22 | const string METHOD_MERGE = 'MERGE'; 23 | const string METHOD_NOTIFY = 'NOTIFY'; 24 | const string METHOD_SUBSCRIBE = 'SUBSCRIBE'; 25 | const string METHOD_UNSUBSCRIBE = 'UNSUBSCRIBE'; 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/Stream/functions.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\Stream; 2 | 3 | use namespace Nuxed\Filesystem; 4 | use namespace Nuxed\Http\Message; 5 | 6 | function file( 7 | Filesystem\File $file 8 | ): Message\IStream { 9 | return new Message\Stream( 10 | \fopen($file->path()->toString(), 'wb+', false) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/StreamSeekWhence.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | /** 4 | * Specifies how the cursor position will be calculated 5 | * based on the seek offset. Valid values are identical to the built-in 6 | * Hack $whence values for `fseek()`. 7 | */ 8 | enum StreamSeekWhence: int { 9 | /** 10 | * Set position equal to offset bytes. 11 | */ 12 | SET = \SEEK_SET; 13 | /** 14 | * Set position to current location plus offset. 15 | */ 16 | CURRENT = \SEEK_CUR; 17 | /** 18 | * Set position to end-of-file plus offset. 19 | */ 20 | END = \SEEK_END; 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/UploadedFileError.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | enum UploadedFileError: int { 4 | ERROR_OK = 0; 5 | ERROR_EXCEEDS_MAX_INI_SIZE = 1; 6 | ERROR_EXCEEDS_MAX_FORM_SIZE = 2; 7 | ERROR_INCOMPLETE = 3; 8 | ERROR_NO_FILE = 4; 9 | ERROR_TMP_DIR_NOT_SPECIFIED = 6; 10 | ERROR_TMP_DIR_NOT_WRITEABLE = 7; 11 | ERROR_CANCELED_BY_EXTENSION = 8; 12 | } 13 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/_Private/HeadersMarshaler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\_Private; 2 | 3 | use namespace HH\Lib\{C, Str}; 4 | 5 | final class HeadersMarshaler { 6 | public function marshal( 7 | KeyedContainer $server, 8 | ): KeyedContainer> { 9 | $headers = dict[]; 10 | 11 | $valid = (mixed $value): bool ==> 12 | $value is Container<_> ? C\count($value) > 0 : ((string)$value) !== ''; 13 | 14 | foreach ($server as $key => $value) { 15 | // Apache prefixes environment variables with REDIRECT_ 16 | // if they are added by rewrite rules 17 | if (Str\search($key, 'REDIRECT_') === 0) { 18 | $key = Str\slice($key, 9); 19 | // We will not overwrite existing variables with the 20 | // prefixed versions, though 21 | if (C\contains_key($server, $key)) { 22 | continue; 23 | } 24 | } 25 | 26 | if (!$valid($value)) { 27 | continue; 28 | } 29 | 30 | if (Str\search($key, 'HTTP_') === 0) { 31 | $name = \strtr(Str\lowercase(Str\slice($key, 5)), '_', '-'); 32 | 33 | if (!$value is Container<_>) { 34 | $value = vec[(string)$value]; 35 | } 36 | 37 | $val = vec[]; 38 | foreach ($value as $v) { 39 | $val[] = (string)$v; 40 | } 41 | 42 | $headers[$name] = $val; 43 | continue; 44 | } 45 | 46 | if (Str\search($key, 'CONTENT_') === 0) { 47 | $name = 'content-'.Str\lowercase(Str\slice($key, 8)); 48 | if (!$value is Container<_>) { 49 | $value = vec[(string)$value]; 50 | } 51 | $headers[$name] = vec[]; 52 | foreach ($value as $v) { 53 | $headers[$name][] = (string)$v; 54 | } 55 | continue; 56 | } 57 | } 58 | 59 | return $headers; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/_Private/ProtocolVersionMarshaler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\_Private; 2 | 3 | use namespace HH\Lib\{Regex, Str}; 4 | use namespace Nuxed\Http\Message\Exception; 5 | 6 | final class ProtocolVersionMarshaler { 7 | public function marshal(KeyedContainer $server): string { 8 | $protocol = (string)$server['SERVER_PROTOCOL'] ?? '1.1'; 9 | if ( 10 | !Regex\matches($protocol, re"#^(HTTP/)?(?P[1-9]\d*(?:\.\d)?)$#") 11 | ) { 12 | throw new Exception\UnrecognizedProtocolVersionException( 13 | Str\format('Unrecognized protocol version (%s).', $protocol), 14 | ); 15 | } 16 | 17 | $matches = Regex\first_match( 18 | $protocol, 19 | re"#^(HTTP/)?(?P[1-9]\d*(?:\.\d)?)$#", 20 | ) as nonnull; 21 | 22 | return $matches['version']; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/_Private/inject_content_type_in_headers.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message\_Private; 2 | 3 | use namespace HH\Lib\{C, Str}; 4 | 5 | /** 6 | * Inject the provided Content-Type, if none is already present. 7 | */ 8 | function inject_content_type_in_headers( 9 | string $contentType, 10 | KeyedContainer> $headers, 11 | ): KeyedContainer> { 12 | $headers = dict($headers); 13 | 14 | $hasContentType = C\reduce_with_key( 15 | $headers, 16 | ($carry, $key, $item) ==> 17 | $carry ?: (Str\lowercase($key) === 'content-type'), 18 | false, 19 | ); 20 | 21 | if (false === $hasContentType) { 22 | $headers['content-type'] = vec[ 23 | $contentType, 24 | ]; 25 | } 26 | 27 | return $headers; 28 | } 29 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Message/functions.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Message; 2 | 3 | function cookie( 4 | string $value, 5 | ?\DateTimeInterface $expires = null, 6 | ?string $path = null, 7 | ?string $domain = null, 8 | bool $secure = false, 9 | bool $httpOnly = false, 10 | ?CookieSameSite $sameSite = null, 11 | ): Cookie { 12 | return new Cookie( 13 | $value, 14 | $expires, 15 | $path, 16 | $domain, 17 | $secure, 18 | $httpOnly, 19 | $sameSite, 20 | ); 21 | } 22 | 23 | function request( 24 | string $method, 25 | Uri $uri, 26 | KeyedContainer> $headers = dict[], 27 | ?IStream $body = null, 28 | string $version = '1.1', 29 | ): Request { 30 | return new Request($method, $uri, $headers, $body, $version); 31 | } 32 | 33 | function response( 34 | int $status = 200, 35 | KeyedContainer> $headers = dict[], 36 | ?IStream $body = null, 37 | string $version = '1.1', 38 | ?string $reason = null, 39 | ): Response { 40 | return new Response($status, $headers, $body, $version, $reason); 41 | } 42 | 43 | function stream(string $content): IStream { 44 | $handle = \fopen('php://memory', 'wb+'); 45 | \fwrite($handle, $content); 46 | $stream = new Stream($handle); 47 | $stream->rewind(); 48 | return $stream; 49 | } 50 | 51 | function uri(string $uri): Uri { 52 | return new Uri($uri); 53 | } 54 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Exception/DuplicateRouteException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Exception; 2 | 3 | class DuplicateRouteException extends \DomainException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Exception; 2 | 3 | class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Exception; 2 | 3 | class RuntimeException extends \RuntimeException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Generator/IUriGenerator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Generator; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | interface IUriGenerator { 6 | /** 7 | * Generate a URI from the named route. 8 | * 9 | * Takes the named route and any substitutions, and attempts to generate a 10 | * URI from it. 11 | * 12 | * The URI generated MUST NOT be escaped. If you wish to escape any part of 13 | * the URI, this should be performed afterwards; 14 | */ 15 | public function generate( 16 | string $route, 17 | KeyedContainer $substitutions = dict[], 18 | ): Message\Uri; 19 | } 20 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/IRouter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router; 2 | 3 | interface IRouter extends Generator\IUriGenerator, Matcher\IRequestMatcher { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Matcher/IRequestMatcher.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Matcher; 2 | 3 | use namespace Nuxed\Http\{Message, Router}; 4 | 5 | interface IRequestMatcher { 6 | /** 7 | * Match a request against the known routes. 8 | * 9 | * Implementations will aggregate required information from the provided 10 | * request instance, and pass them to the underlying router implementation; 11 | * when done, they will then marshal a `Router\RouteResult` instance indicating 12 | * the results of the matching operation and return it to the caller. 13 | */ 14 | public function match(Message\Request $request): Router\RouteResult; 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Middleware/DispatchMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Router, Server}; 4 | 5 | /** 6 | * Default dispatch middleware. 7 | * 8 | * Checks for a composed route result in the request. If none is provided, 9 | * delegates request processing to the handler. 10 | * 11 | * Otherwise, it delegates processing to the route result. 12 | */ 13 | class DispatchMiddleware implements Server\IMiddleware { 14 | public async function process( 15 | Message\ServerRequest $request, 16 | Server\IHandler $handler, 17 | ): Awaitable { 18 | $routeResult = $request->getAttribute(Router\RouteResult::class); 19 | 20 | if ($routeResult is Router\RouteResult) { 21 | $route = $routeResult->getMatchedRoute(); 22 | if ($route is nonnull) { 23 | return await $route->getMiddleware()->process($request, $handler); 24 | } 25 | } 26 | 27 | return await $handler->handle($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Middleware/MethodNotAllowedMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Router, Server}; 4 | 5 | /** 6 | * Emit a 405 Method Not Allowed response 7 | * 8 | * If the request composes a route result, and the route result represents a 9 | * failure due to request method, this middleware will emit a 405 response, 10 | * along with an Allow header indicating allowed methods, as reported by the 11 | * route result. 12 | * 13 | * If no route result is composed, and/or it's not the result of a method 14 | * failure, it passes handling to the provided handler. 15 | */ 16 | class MethodNotAllowedMiddleware implements Server\IMiddleware { 17 | public async function process( 18 | Message\ServerRequest $request, 19 | Server\IHandler $handler, 20 | ): Awaitable { 21 | $routeResult = $request->getAttribute(Router\RouteResult::class); 22 | 23 | if ( 24 | !$routeResult is Router\RouteResult || !$routeResult->isMethodFailure() 25 | ) { 26 | return await $handler->handle($request); 27 | } 28 | 29 | return Message\response() 30 | ->withStatus(Message\StatusCode::METHOD_NOT_ALLOWED) 31 | ->withHeader('Allow', $routeResult->getAllowedMethods() as nonnull); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Middleware/RouteMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Router, Server}; 4 | 5 | /** 6 | * Default routing middleware. 7 | * 8 | * Uses the composed router to match against the incoming request, and 9 | * injects the request passed to the handler with the `RouteResult` instance 10 | * returned (using the `RouteResult` class name as the attribute name). 11 | * 12 | * If routing succeeds, injects the request passed to the handler with any 13 | * matched parameters as well. 14 | */ 15 | class RouteMiddleware implements Server\IMiddleware { 16 | public function __construct( 17 | protected Router\Matcher\IRequestMatcher $matcher, 18 | ) {} 19 | 20 | public function process( 21 | Message\ServerRequest $request, 22 | Server\IHandler $handler, 23 | ): Awaitable { 24 | $result = $this->matcher->match($request); 25 | 26 | // Inject the actual route result, as well as individual matched parameters. 27 | $request = $request->withAttribute(Router\RouteResult::class, $result); 28 | 29 | if ($result->isSuccess()) { 30 | foreach ($result->getMatchedParams() as $param => $value) { 31 | $request = $request->withAttribute($param, $value); 32 | } 33 | } 34 | 35 | return $handler->handle($request); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/RouteCollector.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router; 2 | 3 | final class RouteCollector implements IRouteCollector { 4 | use RouteCollectorTrait; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/Router.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | final class Router implements IRouter { 6 | public function __construct( 7 | private Matcher\IRequestMatcher $matcher, 8 | private Generator\IUriGenerator $generator, 9 | ) {} 10 | 11 | /** 12 | * Match a request against the known routes. 13 | */ 14 | public function match(Message\Request $request): RouteResult { 15 | return $this->matcher->match($request); 16 | } 17 | 18 | /** 19 | * Generate a URI from the named route. 20 | * 21 | * Takes the named route and any substitutions, and attempts to generate a 22 | * URI from it. 23 | * 24 | * The URI generated MUST NOT be escaped. If you wish to escape any part of 25 | * the URI, this should be performed afterwards; 26 | */ 27 | public function generate( 28 | string $route, 29 | KeyedContainer $substitutions = dict[], 30 | ): Message\Uri { 31 | return $this->generator->generate($route, $substitutions); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/_Private/Ref.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\_Private; 2 | 3 | final class Ref { 4 | public function __construct(public T $value) {} 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Router/_Private/map.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Router\_Private; 2 | 3 | use namespace HH\Lib\{Dict, Vec}; 4 | use namespace Facebook\HackRouter; 5 | use namespace Nuxed\Http\Router; 6 | 7 | function map( 8 | Container $routes, 9 | ): KeyedContainer< 10 | HackRouter\HttpMethod, 11 | HackRouter\PrefixMatching\PrefixMap, 12 | > { 13 | $result = new Ref(dict[]); 14 | Vec\map($routes, ($route) ==> { 15 | $methods = $route->getAllowedMethods(); 16 | if ($methods is null) { 17 | $methods = HackRouter\HttpMethod::getValues(); 18 | } else { 19 | $methods = HackRouter\HttpMethod::assertAll($methods); 20 | } 21 | 22 | Vec\map($methods, ($method) ==> { 23 | $result->value[$method] ??= dict[]; 24 | $result->value[$method][$route->getPath()] = $route; 25 | }); 26 | }); 27 | 28 | return Dict\map( 29 | $result->value, 30 | ($map) ==> HackRouter\PrefixMatching\PrefixMap::fromFlatMap($map), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Exception/EmptyStackException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Exception; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Http\Server; 5 | 6 | final class EmptyStackException 7 | extends \OutOfBoundsException 8 | implements IException { 9 | public static function forClass( 10 | classname $class, 11 | ): this { 12 | return new static(Str\format( 13 | '%s cannot handle request; no middleware available to process the request.', 14 | $class, 15 | )); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Exception; 2 | 3 | <<__Sealed( 4 | InvalidMiddlewareException::class, 5 | EmptyStackException::class, 6 | RuntimeException::class, 7 | )>> 8 | interface IException { 9 | require extends \Exception; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Exception/InvalidMiddlewareException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Exception; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Http\Server; 5 | 6 | final class InvalidMiddlewareException 7 | extends \InvalidArgumentException 8 | implements IException { 9 | public static function forMiddleware(mixed $middleware): this { 10 | return new static(Str\format( 11 | 'Middleware "%s" is neither a string service name, a "%s" instance, or a "%s" instance.', 12 | \is_object($middleware) ? \get_class($middleware) : \gettype($middleware), 13 | Server\IMiddleware::class, 14 | Server\IHandler::class, 15 | )); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Exception; 2 | 3 | <<__Sealed(ServerException::class)>> 4 | class RuntimeException extends \RuntimeException implements IException { 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Exception/ServerException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Exception; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | final class ServerException extends RuntimeException { 6 | public function __construct( 7 | protected int $status = Message\StatusCode::INTERNAL_SERVER_ERROR, 8 | protected KeyedContainer> $headers = dict[], 9 | protected ?Message\IStream $body = null 10 | ) { 11 | parent::__construct( 12 | Message\Response::$phrases[$status] ?? Message\Response::$phrases[500] 13 | ); 14 | } 15 | 16 | public function getStatusCode(): int { 17 | return $this->status; 18 | } 19 | 20 | public function getHeaders( 21 | ): KeyedContainer> { 22 | return $this->headers; 23 | } 24 | 25 | public function getBody(): ?Message\IStream { 26 | return $this->body; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Handler/CallableHandlerDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Handler; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | final class CallableHandlerDecorator implements Server\IHandler { 6 | public function __construct( 7 | private Server\CallableHandler $callback, 8 | ) {} 9 | 10 | public function handle( 11 | Message\ServerRequest $request, 12 | ): Awaitable { 13 | $fun = $this->callback; 14 | return $fun($request); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Handler/NextMiddlewareHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Handler; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | class NextMiddlewareHandler implements Server\IHandler { 6 | private \SplPriorityQueue $queue; 7 | 8 | public function __construct( 9 | \SplPriorityQueue $queue, 10 | private Server\IHandler $handler, 11 | ) { 12 | $this->queue = clone $queue; 13 | } 14 | 15 | public async function handle( 16 | Message\ServerRequest $request, 17 | ): Awaitable { 18 | if (0 === $this->queue->count()) { 19 | return await $this->handler->handle($request); 20 | } 21 | 22 | $middleware = $this->queue->extract(); 23 | 24 | return await $middleware->process($request, $this); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Handler/NotFoundHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Handler; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | class NotFoundHandler implements Server\IHandler { 6 | public async function handle( 7 | Message\ServerRequest $_request, 8 | ): Awaitable { 9 | throw new Server\Exception\ServerException(404, dict[]); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/HandlerMiddlewareTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | trait HandlerMiddlewareTrait implements IMiddleware { 6 | require implements IHandler; 7 | 8 | public function process( 9 | Message\ServerRequest $request, 10 | IHandler $_handler, 11 | ): Awaitable { 12 | return $this->handle($request); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/IHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | /** 6 | * An HTTP request handler process a HTTP request and produces an HTTP response. 7 | * This interface defines the methods require to use the request handler. 8 | */ 9 | interface IHandler { 10 | /** 11 | * Handle the request and return a response. 12 | */ 13 | public function handle( 14 | Message\ServerRequest $request, 15 | ): Awaitable; 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/IMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | /** 6 | * An HTTP middleware component participates in processing an HTTP message, 7 | * either by acting on the request or the response. This interface defines the 8 | * methods required to use the middleware. 9 | */ 10 | interface IMiddleware { 11 | /** 12 | * Process an incoming server request and return a response, optionally delegating 13 | * response creation to a handler. 14 | */ 15 | public function process( 16 | Message\ServerRequest $request, 17 | IHandler $handler, 18 | ): Awaitable; 19 | } 20 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/IMiddlewareStack.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server; 2 | 3 | /** 4 | * Stack middleware like unix pipes. 5 | * 6 | * This interface represents a stack of middleware, which can be attached using 7 | * the `stack()` method, and is itself middleware. 8 | * 9 | * It creates an instance of `NextMiddlewareProcessor` internally, invoking it with the provided 10 | * request and response instances, passing the original request and the returned 11 | * response to the `$next` argument when complete. 12 | * 13 | * Inspired by Sencha Connect. 14 | * 15 | * @see https://github.com/senchalabs/connect 16 | */ 17 | interface IMiddlewareStack extends IHandler, IMiddleware { 18 | /** 19 | * Attach middleware to the stack. 20 | */ 21 | public function stack(IMiddleware $middleware, int $priority = 0): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Middleware/CallableMiddlewareDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | final class CallableMiddlewareDecorator implements Server\IMiddleware { 6 | public function __construct(private Server\CallableMiddleware $middleware) {} 7 | 8 | public function process( 9 | Message\ServerRequest $request, 10 | Server\IHandler $handler, 11 | ): Awaitable { 12 | $fun = $this->middleware; 13 | return $fun($request, $handler); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Middleware/HostMiddlewareDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Middleware; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Http\{Message, Server}; 5 | 6 | class HostMiddlewareDecorator implements Server\IMiddleware { 7 | public function __construct( 8 | private string $host, 9 | private Server\IMiddleware $middleware, 10 | ) {} 11 | 12 | public async function process( 13 | Message\ServerRequest $request, 14 | Server\IHandler $handler, 15 | ): Awaitable { 16 | $host = $request->getUri()->getHost(); 17 | 18 | if ($host !== Str\lowercase($this->host)) { 19 | return await $handler->handle($request); 20 | } 21 | 22 | return await $this->middleware->process($request, $handler); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Middleware/OriginalMessagesMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | class OriginalMessagesMiddleware implements Server\IMiddleware { 6 | public async function process( 7 | Message\ServerRequest $request, 8 | Server\IHandler $handler, 9 | ): Awaitable { 10 | return await $handler->handle( 11 | $request 12 | ->withAttribute('OriginalUri', $request->getUri()) 13 | ->withAttribute('OriginalRequest', $request), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/Middleware/RequestHandlerMiddlewareDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\{Message, Server}; 4 | 5 | /** 6 | * Decorate a request handler as middleware. 7 | * 8 | * When pulling handlers from a container, or creating pipelines, it's 9 | * simplest if everything is of the same type, so we do not need to worry 10 | * about varying execution based on type. 11 | * 12 | * To manage this, this class decorates request handlers as middleware, so that 13 | * they may be piped or routed to. When processed, they delegate handling to the 14 | * decorated handler, which will return a response. 15 | */ 16 | final class HandlerMiddlewareDecorator 17 | implements Server\IMiddleware, Server\IHandler { 18 | public function __construct(private Server\IHandler $handler) {} 19 | 20 | /** 21 | * Proxies to decorated handler to handle the request. 22 | */ 23 | public function handle( 24 | Message\ServerRequest $request, 25 | ): Awaitable { 26 | return $this->handler->handle($request); 27 | } 28 | 29 | /** 30 | * Proxies to decorated handler to handle the request. 31 | */ 32 | public function process( 33 | Message\ServerRequest $request, 34 | Server\IHandler $_, 35 | ): Awaitable { 36 | return $this->handler->handle($request); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Server/types.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Server; 2 | 3 | use namespace Nuxed\Http\Message; 4 | 5 | type CallableMiddleware = (function( 6 | Message\ServerRequest, 7 | IHandler, 8 | ): Awaitable); 9 | 10 | type CallableHandler = (function( 11 | Message\ServerRequest, 12 | ): Awaitable); 13 | 14 | type FunctionalMiddleware = (function( 15 | Message\ServerRequest, 16 | CallableHandler, 17 | ): Awaitable); 18 | 19 | type DoublePassMiddleware = (function( 20 | Message\ServerRequest, 21 | Message\Response, 22 | IHandler, 23 | ): Awaitable); 24 | 25 | type DoublePassFunctionalMiddleware = (function( 26 | Message\ServerRequest, 27 | Message\Response, 28 | CallableHandler, 29 | ): Awaitable); 30 | 31 | type DoublePassHandler = (function( 32 | Message\ServerRequest, 33 | Message\Response, 34 | ): Awaitable); 35 | 36 | type LazyMiddleware = (function(): IMiddleware); 37 | 38 | type LazyHandler = (function(): IHandler); 39 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Session/CacheLimiter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Session; 2 | 3 | enum CacheLimiter: string { 4 | NOCACHE = 'nocache'; 5 | PUBLIC = 'public'; 6 | PRIVATE = 'private'; 7 | PRIVATE_NO_EXPIRE = 'private_no_expire'; 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Session/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Session\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Session/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Session\Exception; 2 | 3 | class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Session/Persistence/ISessionPersistence.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Session\Persistence; 2 | 3 | use namespace Nuxed\Http\{Message, Session}; 4 | 5 | interface ISessionPersistence { 6 | /** 7 | * Generate a session data instance based on the request. 8 | */ 9 | public function initialize( 10 | Message\ServerRequest $request, 11 | ): Awaitable; 12 | 13 | /** 14 | * Persist the session data instance 15 | * 16 | * Persists the session data, returning a response instance with any 17 | * artifacts required to return to the client. 18 | */ 19 | public function persist( 20 | Session\Session $session, 21 | Message\Response $response, 22 | ): Awaitable; 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Http/Session/SessionMiddleware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Http\Session; 2 | 3 | use type Nuxed\Http\Server\{IHandler, IMiddleware}; 4 | use type Nuxed\Http\Message\{Response, ServerRequest}; 5 | 6 | class SessionMiddleware implements IMiddleware { 7 | public function __construct( 8 | private Persistence\ISessionPersistence $persistence, 9 | ) {} 10 | 11 | public async function process( 12 | ServerRequest $request, 13 | IHandler $handler, 14 | ): Awaitable { 15 | $session = await $this->persistence->initialize($request); 16 | $request = $request->withSession($session); 17 | $response = await $handler->handle($request); 18 | 19 | return await $this->persistence->persist($request->getSession(), $response); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Exception; 2 | 3 | final class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Exception; 2 | 3 | final class RuntimeException extends \RuntimeException implements IException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/IBuilder.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt; 2 | 3 | interface IBuilder { 4 | /** 5 | * Appends new items to audience 6 | */ 7 | public function permittedFor(string ...$audiences): IBuilder; 8 | 9 | /** 10 | * Configures the expiration time 11 | */ 12 | public function expiresAt(int $expiration): IBuilder; 13 | 14 | /** 15 | * Configures the token id 16 | */ 17 | public function identifiedBy(string $id): IBuilder; 18 | 19 | /** 20 | * Configures the time that the token was issued 21 | */ 22 | public function issuedAt(int $issuedAt): IBuilder; 23 | 24 | /** 25 | * Configures the issuer 26 | */ 27 | public function issuedBy(string $issuer): IBuilder; 28 | 29 | /** 30 | * Configures the time before which the token cannot be accepted 31 | */ 32 | public function canOnlyBeUsedAfter(int $notBefore): IBuilder; 33 | 34 | /** 35 | * Configures the subject 36 | */ 37 | public function relatedTo(string $subject): IBuilder; 38 | 39 | /** 40 | * Configures a header item 41 | * 42 | * @param mixed $value 43 | */ 44 | public function withHeader(string $name, mixed $value): IBuilder; 45 | 46 | /** 47 | * Configures a claim item 48 | * 49 | * @param mixed $value 50 | * 51 | * @throws InvalidArgumentException When trying to set a registered claim. 52 | */ 53 | public function withClaim(string $name, mixed $value): IBuilder; 54 | 55 | /** 56 | * Returns a signed token to be used 57 | */ 58 | public function getToken(ISigner $signer, Signer\Key $key): IToken; 59 | } 60 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/IParser.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt; 2 | 3 | interface IParser { 4 | /** 5 | * Parses the JWT and returns a token 6 | * 7 | * @throws Exception\InvalidArgumentException 8 | */ 9 | public function parse(string $jwt): IToken; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/ISigner.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt; 2 | 3 | interface ISigner { 4 | /** 5 | * Returns the algorithm id 6 | */ 7 | public function getAlgorithmId(): string; 8 | 9 | /** 10 | * Creates a hash for the given payload 11 | * 12 | * @throws Exception\InvalidArgumentException When given key is invalid. 13 | */ 14 | public function sign(string $payload, Signer\Key $key): string; 15 | 16 | /** 17 | * Returns if the expected hash matches with the data and key 18 | * 19 | * @throws Exception\InvalidArgumentException When given key is invalid. 20 | */ 21 | public function verify( 22 | string $expected, 23 | string $payload, 24 | Signer\Key $key, 25 | ): bool; 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/IToken.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt; 2 | 3 | use namespace Nuxed\Util; 4 | 5 | interface IToken extends Util\Stringable { 6 | /** 7 | * Returns the token headers 8 | */ 9 | public function getHeaders(): Token\Headers; 10 | 11 | /** 12 | * Returns the token claims 13 | */ 14 | public function getClaims(): Token\Claims; 15 | 16 | /** 17 | * Returns the token signature 18 | */ 19 | public function getSignature(): Token\Signature; 20 | 21 | /** 22 | * Returns the token payload 23 | */ 24 | public function getPayload(): string; 25 | 26 | /** 27 | * Returns if the token is allowed to be used by the audience 28 | */ 29 | public function isPermittedFor(string $audience): bool; 30 | 31 | /** 32 | * Returns if the token has the given id 33 | */ 34 | public function isIdentifiedBy(string $id): bool; 35 | 36 | /** 37 | * Returns if the token has the given subject 38 | */ 39 | public function isRelatedTo(string $subject): bool; 40 | 41 | /** 42 | * Returns if the token was issued by any of given issuers 43 | */ 44 | public function hasBeenIssuedBy(string ...$issuers): bool; 45 | 46 | /** 47 | * Returns if the token was issued before of given time 48 | * 49 | * Returns NULL if the token doesn't contain the `iat` claim. 50 | */ 51 | public function hasBeenIssuedBefore(int $now): ?bool; 52 | 53 | /** 54 | * Returns if the token minimum time is before than given time 55 | * 56 | * Returns NULL if the token doesn't contain the `nbf` claim. 57 | */ 58 | public function isMinimumTimeBefore(int $now): ?bool; 59 | 60 | /** 61 | * Returns if the token is expired 62 | */ 63 | public function isExpired(int $now): bool; 64 | 65 | /** 66 | * Returns an encoded representation of the token 67 | */ 68 | public function toString(): string; 69 | } 70 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Ecdsa.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer; 2 | 3 | << 4 | __ConsistentConstruct, 5 | __Sealed(Ecdsa\Sha256::class, Ecdsa\Sha384::class, Ecdsa\Sha512::class) 6 | >> 7 | abstract class Ecdsa extends OpenSSL { 8 | public function __construct(private Ecdsa\ISignatureConverter $converter) {} 9 | 10 | public static function create(): Ecdsa { 11 | return new static(new Ecdsa\MultibyteStringConverter()); 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | final public function sign(string $payload, Key $key): string { 19 | return $this->converter->fromAsn1( 20 | $this->createSignature( 21 | $key->getContent(), 22 | $key->getPassphrase(), 23 | $payload, 24 | ), 25 | $this->getKeyLength(), 26 | ); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | <<__Override>> 33 | final public function verify( 34 | string $expected, 35 | string $payload, 36 | Key $key, 37 | ): bool { 38 | return $this->verifySignature( 39 | $this->converter->toAsn1($expected, $this->getKeyLength()), 40 | $payload, 41 | $key->getContent(), 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | <<__Override>> 49 | final public function getKeyType(): int { 50 | return \OPENSSL_KEYTYPE_EC; 51 | } 52 | 53 | /** 54 | * Returns the length of each point in the signature, so that we can calculate and verify R and S points properly 55 | * 56 | * @internal 57 | */ 58 | abstract public function getKeyLength(): int; 59 | } 60 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Ecdsa/ISignatureConverter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Ecdsa; 2 | 3 | /** 4 | * Manipulates the result of a ECDSA signature (points R and S) according to the 5 | * JWA specs. 6 | * 7 | * OpenSSL creates a signature using the ASN.1 format and, according the JWA specs, 8 | * the signature for JWTs must be the concatenated values of points R and S (in 9 | * big-endian octet order). 10 | * 11 | * @internal 12 | * 13 | * @see https://tools.ietf.org/html/rfc7518#page-9 14 | * @see https://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One 15 | */ 16 | interface ISignatureConverter { 17 | /** 18 | * Converts the signature generated by OpenSSL into what JWA defines 19 | */ 20 | public function fromAsn1(string $signature, int $length): string; 21 | 22 | /** 23 | * Converts the JWA signature into something OpenSSL understands 24 | */ 25 | public function toAsn1(string $points, int $length): string; 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Ecdsa/Sha256.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Ecdsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha256 extends Signer\Ecdsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'ES256'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA256; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | <<__Override>> 26 | public function getKeyLength(): int { 27 | return 64; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Ecdsa/Sha384.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Ecdsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha384 extends Signer\Ecdsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'ES384'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA384; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | <<__Override>> 26 | public function getKeyLength(): int { 27 | return 96; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Ecdsa/Sha512.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Ecdsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha512 extends Signer\Ecdsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'ES512'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA512; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | <<__Override>> 26 | public function getKeyLength(): int { 27 | return 132; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Hmac.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer; 2 | 3 | use namespace Nuxed\Jwt; 4 | 5 | <<__Sealed(Hmac\Sha256::class, Hmac\Sha384::class, Hmac\Sha512::class)>> 6 | abstract class Hmac implements Jwt\ISigner { 7 | /** 8 | * {@inheritdoc} 9 | */ 10 | <<__Override>> 11 | final public function sign(string $payload, Key $key): string { 12 | return \hash_hmac( 13 | $this->getAlgorithm(), 14 | $payload, 15 | $key->getContent(), 16 | true, 17 | ); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | final public function verify( 24 | string $expected, 25 | string $payload, 26 | Key $key, 27 | ): bool { 28 | return \hash_equals($expected, $this->sign($payload, $key)); 29 | } 30 | 31 | abstract public function getAlgorithm(): string; 32 | } 33 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Hmac/Sha256.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Hmac; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha256 extends Signer\Hmac { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'HS256'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): string { 19 | return 'sha256'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Hmac/Sha384.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Hmac; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha384 extends Signer\Hmac { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'HS384'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): string { 19 | return 'sha384'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Hmac/Sha512.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Hmac; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha512 extends Signer\Hmac { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'HS512'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): string { 19 | return 'sha512'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Key.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer; 2 | 3 | final class Key { 4 | public function __construct( 5 | private string $content, 6 | private string $passphrase = '', 7 | ) {} 8 | 9 | public function getContent(): string { 10 | return $this->content; 11 | } 12 | 13 | public function getPassphrase(): string { 14 | return $this->passphrase; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/None.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer; 2 | 3 | use namespace Nuxed\Jwt; 4 | 5 | final class None implements Jwt\ISigner { 6 | /** 7 | * {@inhertdoc} 8 | */ 9 | public function getAlgorithmId(): string { 10 | return 'none'; 11 | } 12 | 13 | /** 14 | * {@inhertdoc} 15 | */ 16 | public function sign(string $_payload, Key $_key): string { 17 | return ''; 18 | } 19 | 20 | /** 21 | * {@inhertdoc} 22 | */ 23 | public function verify(string $expected, string $_payload, Key $_key): bool { 24 | return $expected === ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Rsa.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer; 2 | 3 | <<__Sealed(Rsa\Sha256::class, Rsa\Sha384::class, Rsa\Sha512::class)>> 4 | abstract class Rsa extends OpenSSL { 5 | /** 6 | * {@inheritdoc} 7 | */ 8 | <<__Override>> 9 | final public function sign(string $payload, Key $key): string { 10 | return $this->createSignature( 11 | $key->getContent(), 12 | $key->getPassphrase(), 13 | $payload, 14 | ); 15 | } 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | <<__Override>> 21 | final public function verify( 22 | string $expected, 23 | string $payload, 24 | Key $key, 25 | ): bool { 26 | return $this->verifySignature($expected, $payload, $key->getContent()); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ <<__Override>> 32 | final public function getKeyType(): int { 33 | return \OPENSSL_KEYTYPE_RSA; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Rsa/Sha256.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Rsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha256 extends Signer\Rsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'RS256'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA256; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Rsa/Sha384.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Rsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha384 extends Signer\Rsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'RS384'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA384; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Signer/Rsa/Sha512.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Signer\Rsa; 2 | 3 | use namespace Nuxed\Jwt\Signer; 4 | 5 | final class Sha512 extends Signer\Rsa { 6 | /** 7 | * {@inheritdoc} 8 | */ 9 | <<__Override>> 10 | public function getAlgorithmId(): string { 11 | return 'RS512'; 12 | } 13 | 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | <<__Override>> 18 | public function getAlgorithm(): int { 19 | return \OPENSSL_ALGO_SHA512; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Token/Headers.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Token; 2 | 3 | use namespace HH\Lib\C; 4 | use namespace Nuxed\Util; 5 | 6 | final class Headers { 7 | use Util\StringableTrait; 8 | 9 | private dict $data = dict[]; 10 | 11 | public function __construct( 12 | KeyedContainer $data, 13 | private string $encoded, 14 | ) { 15 | foreach ($data as $key => $value) { 16 | $this->data[$key] = $value as dynamic; 17 | } 18 | } 19 | 20 | public function contains(string $header): bool { 21 | return C\contains_key($this->data, $header); 22 | } 23 | 24 | public function get(string $header, mixed $default = null): dynamic { 25 | return $this->data[$header] ?? $default; 26 | } 27 | 28 | public function toString(): string { 29 | return $this->encoded; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Nuxed/Jwt/Token/Signature.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Jwt\Token; 2 | 3 | use namespace Nuxed\Util; 4 | 5 | final class Signature { 6 | use Util\StringableTrait; 7 | 8 | public function __construct(private string $hash, private string $encoded) {} 9 | 10 | public function getHash(): string { 11 | return $this->hash; 12 | } 13 | 14 | public function toString(): string { 15 | return $this->encoded; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Log/AbstractLogger.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | /** 4 | * This is a simple Logger implementation that other Loggers can inherit from. 5 | * 6 | * It simply delegates all log-level-specific methods to the `log` method to 7 | * reduce boilerplate code that a simple Logger that does the same thing with 8 | * messages regardless of the error level has to implement. 9 | */ 10 | abstract class AbstractLogger implements ILogger { 11 | use LoggerTrait; 12 | } 13 | -------------------------------------------------------------------------------- /src/Nuxed/Log/BufferingLogger.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | use namespace Nuxed\Log; 4 | 5 | /** 6 | * A buffering logger that stacks logs for later. 7 | */ 8 | class BufferingLogger extends Log\AbstractLogger { 9 | private vec Log\LogLevel, 11 | 'message' => string, 12 | 'context' => KeyedContainer, 13 | ... 14 | )> $logs = vec[]; 15 | 16 | <<__Override>> 17 | public function log( 18 | Log\LogLevel $level, 19 | string $message, 20 | KeyedContainer $context = dict[], 21 | ): void { 22 | $this->logs[] = shape( 23 | 'level' => $level, 24 | 'message' => $message, 25 | 'context' => $context, 26 | ); 27 | } 28 | 29 | public function cleanLogs( 30 | ): vec Log\LogLevel, 32 | 'message' => string, 33 | 'context' => KeyedContainer, 34 | ... 35 | )> { 36 | $logs = $this->logs; 37 | $this->reset(); 38 | return $logs; 39 | } 40 | 41 | <<__Override>> 42 | public function reset(): void { 43 | $this->logs = vec[]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Exception; 2 | 3 | class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Exception/LogicException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Exception; 2 | 3 | class LogicException extends \LogicException implements IException { 4 | } 5 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Exception/UnexpectedValueException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Exception; 2 | 3 | class UnexpectedValueException 4 | extends \UnexpectedValueException 5 | implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Formatter/IFormatter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Formatter; 2 | 3 | use namespace Nuxed\Log; 4 | 5 | interface IFormatter { 6 | public function format(Log\LogRecord $record): Log\LogRecord; 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/AbstractHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | use namespace Nuxed\Log; 4 | use type Nuxed\Contract\IReset; 5 | 6 | abstract class AbstractHandler implements IFormattableHandler, IReset { 7 | use FormattableHandlerTrait; 8 | 9 | const dict LEVELS = dict[ 10 | Log\LogLevel::DEBUG => 0, 11 | Log\LogLevel::INFO => 1, 12 | Log\LogLevel::NOTICE => 2, 13 | Log\LogLevel::WARNING => 3, 14 | Log\LogLevel::ERROR => 4, 15 | Log\LogLevel::CRITICAL => 5, 16 | Log\LogLevel::ALERT => 6, 17 | Log\LogLevel::EMERGENCY => 7, 18 | ]; 19 | 20 | /** 21 | * @param Log\LogLevel $level The minimum logging level at which this handler will be triggered 22 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not 23 | */ 24 | public function __construct( 25 | public Log\LogLevel $level = Log\LogLevel::DEBUG, 26 | public bool $bubble = true, 27 | ) {} 28 | 29 | public function isHandling(Log\LogRecord $record): bool { 30 | $minimum = static::LEVELS[$this->level]; 31 | $level = static::LEVELS[$record['level']]; 32 | 33 | return $level >= $minimum; 34 | } 35 | 36 | public function handle(Log\LogRecord $record): bool { 37 | if (!$this->isHandling($record)) { 38 | return false; 39 | } 40 | 41 | $record = $this->getFormatter()->format($record); 42 | 43 | $this->write($record); 44 | 45 | return false === $this->bubble; 46 | } 47 | 48 | /** 49 | * Writes the record down to the log of the implementing handler 50 | */ 51 | abstract protected function write(Log\LogRecord $record): void; 52 | 53 | public function close(): void { 54 | } 55 | 56 | public function reset(): void { 57 | if ($this->formatter is IReset) { 58 | $this->formatter->reset(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/FormattableHandlerTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | use namespace Nuxed\Log\Formatter; 4 | 5 | trait FormattableHandlerTrait { 6 | require implements IFormattableHandler; 7 | 8 | protected ?Formatter\IFormatter $formatter = null; 9 | 10 | public function setFormatter(Formatter\IFormatter $formatter): this { 11 | $this->formatter = $formatter; 12 | 13 | return $this; 14 | } 15 | 16 | public function getFormatter(): Formatter\IFormatter { 17 | if (!$this->formatter) { 18 | $this->formatter = $this->getDefaultFormatter(); 19 | } 20 | 21 | return $this->formatter; 22 | } 23 | 24 | /** 25 | * Gets the default formatter. 26 | * 27 | * Overwrite this if the LineFormatter is not a good default for your handler. 28 | */ 29 | protected function getDefaultFormatter(): Formatter\IFormatter { 30 | return new Formatter\LineFormatter(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/IFormattableHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | use namespace Nuxed\Log\Formatter; 4 | 5 | interface IFormattableHandler extends IHandler { 6 | /** 7 | * Sets the formatter. 8 | */ 9 | public function setFormatter(Formatter\IFormatter $formatter): this; 10 | 11 | /** 12 | * Gets the formatter. 13 | */ 14 | public function getFormatter(): Formatter\IFormatter; 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/IHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | use type Nuxed\Log\LogRecord; 4 | 5 | interface IHandler { 6 | /** 7 | * Checks whether the given record will be handled by this handler. 8 | * 9 | * This is mostly done for performance reasons, to avoid calling processors for nothing. 10 | * 11 | * Handlers should still check the record levels within handle(), returning false in isHandling() 12 | * is no guarantee that handle() will not be called, and isHandling() might not be called 13 | * for a given record. 14 | * 15 | * @param LogRecord $record Partial log record containing only a level key 16 | * 17 | * @return bool 18 | */ 19 | public function isHandling(LogRecord $record): bool; 20 | 21 | /** 22 | * Handles a record. 23 | * 24 | * All records may be passed to this method, and the handler should discard 25 | * those that it does not want to handle. 26 | * 27 | * The return value of this function controls the bubbling process of the handler stack. 28 | * Unless the bubbling is interrupted (by returning true), the Logger class will keep on 29 | * calling further handlers in the stack with a given log record. 30 | * 31 | * @param record $record The record to handle 32 | * @return bool true means that this handler handled the record, and that bubbling is not permitted. 33 | * false means the record was either not processed or that this handler allows bubbling. 34 | */ 35 | public function handle(LogRecord $record): bool; 36 | 37 | /** 38 | * Closes the handler. 39 | * 40 | * Ends a log cycle and frees all resources used by the handler. 41 | * 42 | * Closing a Handler means flushing all buffers and freeing any open resources/handles. 43 | * 44 | * Implementations have to be idempotent (i.e. it should be possible to call close several times without breakage) 45 | * and ideally handlers should be able to reopen themselves on handle() after they have been closed. 46 | */ 47 | public function close(): void; 48 | } 49 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/SysLogFacility.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | enum SysLogFacility: int { 4 | AUTH = \LOG_AUTH; 5 | AUTHPRIV = \LOG_AUTHPRIV; 6 | CRON = \LOG_CRON; 7 | DAEMON = \LOG_DAEMON; 8 | KERN = \LOG_KERN; 9 | LPR = \LOG_LPR; 10 | MAIL = \LOG_MAIL; 11 | NEWS = \LOG_NEWS; 12 | SYSLOG = \LOG_SYSLOG; 13 | USER = \LOG_USER; 14 | UUCP = \LOG_UUCP; 15 | LOCAL0 = \LOG_LOCAL0; 16 | LOCAL1 = \LOG_LOCAL1; 17 | LOCAL2 = \LOG_LOCAL2; 18 | LOCAL3 = \LOG_LOCAL3; 19 | LOCAL4 = \LOG_LOCAL4; 20 | LOCAL5 = \LOG_LOCAL5; 21 | LOCAL6 = \LOG_LOCAL6; 22 | LOCAL7 = \LOG_LOCAL7; 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Handler/SysLogHandler.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Handler; 2 | 3 | use namespace Nuxed\Log; 4 | use namespace HH\Lib\Str; 5 | use namespace Nuxed\Log\Exception; 6 | 7 | class SysLogHandler extends AbstractHandler { 8 | /** 9 | * Translates Monolog log levels to syslog log priorities. 10 | */ 11 | protected dict $logLevels = dict[ 12 | Log\LogLevel::DEBUG => \LOG_DEBUG, 13 | Log\LogLevel::INFO => \LOG_INFO, 14 | Log\LogLevel::NOTICE => \LOG_NOTICE, 15 | Log\LogLevel::WARNING => \LOG_WARNING, 16 | Log\LogLevel::ERROR => \LOG_ERR, 17 | Log\LogLevel::CRITICAL => \LOG_CRIT, 18 | Log\LogLevel::ALERT => \LOG_ALERT, 19 | Log\LogLevel::EMERGENCY => \LOG_EMERG, 20 | ]; 21 | 22 | /** 23 | * @param Log\LogLevel $level The minimum logging level at which this handler will be triggered 24 | * @param bool $bubble Whether the messages that are handled can bubble up the stack or not 25 | */ 26 | public function __construct( 27 | protected string $ident, 28 | protected SysLogFacility $facility = SysLogFacility::USER, 29 | Log\LogLevel $level = Log\LogLevel::DEBUG, 30 | bool $bubble = true, 31 | protected int $options = \LOG_PID, 32 | ) { 33 | parent::__construct($level, $bubble); 34 | } 35 | 36 | <<__Override>> 37 | public function write(Log\LogRecord $record): void { 38 | if (!\openlog($this->ident, $this->options, (int)$this->facility)) { 39 | throw new Exception\LogicException(Str\format( 40 | "Can't open syslog for ident %s and facility %d", 41 | $this->ident, 42 | (int)$this->facility, 43 | )); 44 | } 45 | 46 | \syslog( 47 | $this->logLevels[$record['level']], 48 | $record['formatted'] ?? $record['message'], 49 | ); 50 | } 51 | 52 | <<__Override>> 53 | public function close(): void { 54 | \closelog(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nuxed/Log/ILoggerAware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | /** 4 | * Describes a logger-aware instance. 5 | */ 6 | interface ILoggerAware { 7 | /** 8 | * Sets a logger instance on the object. 9 | */ 10 | public function setLogger(ILogger $logger): void; 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Log/LogLevel.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | /** 4 | * Describes log levels. 5 | */ 6 | enum LogLevel: string { 7 | EMERGENCY = 'emergency'; 8 | ALERT = 'alert'; 9 | CRITICAL = 'critical'; 10 | ERROR = 'error'; 11 | WARNING = 'warning'; 12 | NOTICE = 'notice'; 13 | INFO = 'info'; 14 | DEBUG = 'debug'; 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Log/LoggerAwareTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | /** 4 | * Basic Implementation of ILoggerAware. 5 | */ 6 | trait LoggerAwareTrait implements ILoggerAware { 7 | /** 8 | * The logger instance. 9 | */ 10 | protected ?ILogger $logger; 11 | 12 | /** 13 | * Sets a logger. 14 | */ 15 | public function setLogger(ILogger $logger): void { 16 | $this->logger = $logger; 17 | } 18 | 19 | /** 20 | * Gets a logger. 21 | */ 22 | protected function getLogger(): ILogger { 23 | if ($this->logger is null) { 24 | $this->logger = new NullLogger(); 25 | } 26 | 27 | return $this->logger; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Log/NullLogger.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | /** 4 | * This Logger can be used to avoid conditional log calls. 5 | * 6 | * Logging should always be optional, and if no logger is provided to your 7 | * library creating a NullLogger instance to have something to throw logs at 8 | * is a good way to avoid littering your code with `if ($this->logger) { }` 9 | * blocks. 10 | */ 11 | class NullLogger extends AbstractLogger { 12 | /** 13 | * Logs with an arbitrary level. 14 | */ 15 | <<__Override>> 16 | public function log( 17 | LogLevel $_level, 18 | string $_message, 19 | KeyedContainer $_context = dict[], 20 | ): void { 21 | // noop 22 | } 23 | 24 | <<__Override>> 25 | public function reset(): void { 26 | // noop 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Processor/CallableProcessor.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Processor; 2 | 3 | use type Nuxed\Log\LogRecord; 4 | 5 | class CallableProcessor implements IProcessor { 6 | public function __construct( 7 | protected (function(LogRecord): LogRecord) $callable, 8 | ) {} 9 | 10 | public function process(LogRecord $record): LogRecord { 11 | $fun = $this->callable; 12 | 13 | return $fun($record); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Processor/ContextProcessor.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Processor; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Nuxed\Util; 5 | use type Nuxed\Log\LogRecord; 6 | 7 | class ContextProcessor implements IProcessor { 8 | public function process(LogRecord $record): LogRecord { 9 | if (!Str\contains($record['message'], '}')) { 10 | return $record; 11 | } 12 | 13 | foreach ($record['context'] as $key => $value) { 14 | $placeholder = '{'.$key.'}'; 15 | 16 | if (!Str\contains($record['message'], $placeholder)) { 17 | continue; 18 | } 19 | 20 | $record['message'] = Str\replace( 21 | $record['message'], 22 | $placeholder, 23 | Util\stringify($value), 24 | ); 25 | } 26 | 27 | return $record; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Processor/IProcessor.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Processor; 2 | 3 | use namespace Nuxed\Log; 4 | 5 | interface IProcessor { 6 | public function process(Log\LogRecord $record): Log\LogRecord; 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Processor/MessageLengthProcessor.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log\Processor; 2 | 3 | use namespace HH\Lib\Str; 4 | use type Nuxed\Log\LogRecord; 5 | 6 | 7 | class MessageLengthProcessor implements IProcessor { 8 | public function __construct(protected int $maxLength = 1024) {} 9 | 10 | public function process(LogRecord $record): LogRecord { 11 | if (Str\length($record['message']) <= $this->maxLength) { 12 | return $record; 13 | } 14 | 15 | $record['message'] = Str\slice($record['message'], 0, $this->maxLength). 16 | '[...]'; 17 | 18 | return $record; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Nuxed/Log/Record.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Log; 2 | 3 | use type Nuxed\Log\LogLevel; 4 | use type DateTime; 5 | 6 | type LogRecord = shape( 7 | 'level' => LogLevel, 8 | 'message' => string, 9 | 'context' => dict, 10 | 'time' => DateTime, 11 | 'extra' => dict, 12 | ?'formatted' => string, 13 | ... 14 | ); 15 | -------------------------------------------------------------------------------- /src/Nuxed/Markdown/Extension/AbstractExtension.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Markdown\Extension; 2 | 3 | use namespace Facebook\Markdown; 4 | use namespace Facebook\Markdown\{Inlines, UnparsedBlocks}; 5 | 6 | class AbstractExtension implements IExtension { 7 | /** 8 | * @see Facebook\Markdown\RenderContext::appendFilters() 9 | */ 10 | public function getRenderFilters(): Container { 11 | return vec[]; 12 | } 13 | 14 | /** 15 | * @see Facebook\Markdown\Inlines\Context::prependInlineTypes() 16 | */ 17 | public function getInlineTypes(): Container> { 18 | return vec[]; 19 | } 20 | 21 | /** 22 | * @see Facebook\Markdown\UnparsedBlocks\Context::prependBlockTypes() 23 | */ 24 | public function getBlockProducers( 25 | ): Container> { 26 | return vec[]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nuxed/Markdown/Extension/IExtension.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Markdown\Extension; 2 | 3 | use namespace Facebook\Markdown; 4 | use namespace Facebook\Markdown\{Inlines, UnparsedBlocks}; 5 | 6 | interface IExtension { 7 | /** 8 | * @see Facebook\Markdown\RenderContext::appendFilters() 9 | */ 10 | public function getRenderFilters(): Container; 11 | 12 | /** 13 | * @see Facebook\Markdown\Inlines\Context::prependInlineTypes() 14 | */ 15 | public function getInlineTypes(): Container>; 16 | 17 | /** 18 | * @see Facebook\Markdown\UnparsedBlocks\Context::prependBlockTypes() 19 | */ 20 | public function getBlockProducers( 21 | ): Container>; 22 | } 23 | -------------------------------------------------------------------------------- /src/Nuxed/Markdown/XHPElement.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Markdown; 2 | 3 | use namespace Nuxed\Util; 4 | 5 | // Probably don't need XHPAlwaysValidChild - this is likely to be in a
6 | // or other similarly liberal container 7 | final class XHPElement implements \XHPUnsafeRenderable { 8 | use Util\StringableTrait; 9 | 10 | public function __construct( 11 | private string $markdown, 12 | private Environment $env, 13 | ) { 14 | } 15 | 16 | public function toHTMLString(): string { 17 | return $this->env->convert($this->markdown); 18 | } 19 | 20 | public function toString(): string { 21 | return $this->toHTMLString(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Mercure/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Mercure\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Mercure/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Mercure\Exception; 2 | 3 | final class InvalidArgumentException 4 | extends \InvalidArgumentException 5 | implements IException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Mercure/IJwtProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Mercure; 2 | 3 | interface IJwtProvider { 4 | public function getJwt(): string; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Mercure/Provider/StaticJwtProvider.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Mercure\Provider; 2 | 3 | use namespace Nuxed\Mercure; 4 | 5 | final class StaticJwtProvider implements Mercure\IJwtProvider { 6 | public function __construct(private string $jwt) {} 7 | 8 | public function getJwt(): string { 9 | return $this->jwt; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Nuxed/Mercure/Update.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Mercure; 2 | 3 | /** 4 | * Represents an update to send to the hub. 5 | * 6 | * @see https://github.com/dunglas/mercure/blob/master/spec/mercure.md#hub 7 | * @see https://github.com/dunglas/mercure/blob/master/hub/update.go 8 | */ 9 | final class Update { 10 | public function __construct( 11 | private Container $topics, 12 | private string $data, 13 | private Container $targets = vec[], 14 | private ?string $id = null, 15 | private ?string $type = null, 16 | private ?int $retry = null, 17 | ) {} 18 | 19 | public function getTopics(): Container { 20 | return $this->topics; 21 | } 22 | 23 | public function getData(): string { 24 | return $this->data; 25 | } 26 | 27 | public function getTargets(): Container { 28 | return $this->targets; 29 | } 30 | 31 | public function getId(): ?string { 32 | return $this->id; 33 | } 34 | 35 | public function getType(): ?string { 36 | return $this->type; 37 | } 38 | 39 | public function getRetry(): ?int { 40 | return $this->retry; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Nuxed/Stopwatch/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Stopwatch\Exception; 2 | 3 | use type Exception; 4 | 5 | interface IException { 6 | require extends Exception; 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Stopwatch/Exception/LogicException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Stopwatch\Exception; 2 | 3 | use type LogicException as ParentException; 4 | 5 | class LogicException extends ParentException implements IException { 6 | } 7 | -------------------------------------------------------------------------------- /src/Nuxed/Stopwatch/Period.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Stopwatch; 2 | 3 | 4 | class Period { 5 | private num $start; 6 | private num $end; 7 | private num $memory; 8 | 9 | /** 10 | * @param num $start The relative time of the start of the period (in milliseconds) 11 | * @param num $end The relative time of the end of the period (in milliseconds) 12 | * @param bool $morePrecision If true, time is stored as float to keep the original microsecond precision 13 | */ 14 | public function __construct( 15 | num $start, 16 | num $end, 17 | bool $morePrecision = true, 18 | ) { 19 | $this->start = $morePrecision ? (float)$start : (int)$start; 20 | $this->end = $morePrecision ? (float)$end : (int)$end; 21 | $this->memory = \memory_get_usage(true); 22 | } 23 | 24 | /** 25 | * Gets the relative time of the start of the period. 26 | * 27 | * @return num The time (in milliseconds) 28 | */ 29 | public function getStartTime(): num { 30 | return $this->start; 31 | } 32 | /** 33 | * Gets the relative time of the end of the period. 34 | * 35 | * @return num The time (in milliseconds) 36 | */ 37 | public function getEndTime(): num { 38 | return $this->end; 39 | } 40 | 41 | /** 42 | * Gets the time spent in this period. 43 | * 44 | * @return num The period duration (in milliseconds) 45 | */ 46 | public function getDuration(): num { 47 | return $this->end - $this->start; 48 | } 49 | 50 | /** 51 | * Gets the memory usage. 52 | * 53 | * @return int The memory usage (in bytes) 54 | */ 55 | public function getMemory(): num { 56 | return $this->memory; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Catalogue/IOperation.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Catalogue; 2 | 3 | use namespace Nuxed\Translation; 4 | 5 | /** 6 | * Represents an operation on catalogue(s). 7 | * 8 | * An instance of this interface performs an operation on one or more catalogues and 9 | * stores intermediate and final results of the operation. 10 | * 11 | * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and 12 | * the following results are stored: 13 | * 14 | * Messages: also called 'all', are valid messages for the given domain after the operation is performed. 15 | * 16 | * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}). 17 | * 18 | * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}). 19 | * 20 | * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'. 21 | */ 22 | interface IOperation { 23 | /** 24 | * Returns domains affected by operation. 25 | */ 26 | public function getDomains(): Container; 27 | 28 | /** 29 | * Returns all valid messages ('all') after operation. 30 | */ 31 | public function getMessages(string $domain): KeyedContainer; 32 | 33 | /** 34 | * Returns new messages ('new') after operation. 35 | */ 36 | public function getNewMessages( 37 | string $domain, 38 | ): KeyedContainer; 39 | 40 | /** 41 | * Returns obsolete messages ('obsolete') after operation. 42 | */ 43 | public function getObsoleteMessages( 44 | string $domain, 45 | ): KeyedContainer; 46 | 47 | /** 48 | * Returns resulting catalogue ('result'). 49 | */ 50 | public function getResult(): Translation\MessageCatalogue; 51 | } 52 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Catalogue/MergeOperation.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Catalogue; 2 | 3 | /** 4 | * Merge operation between two catalogues as follows: 5 | * all = source ∪ target = {x: x ∈ source ∨ x ∈ target} 6 | * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} 7 | * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅ 8 | * Basically, the result contains messages from both catalogues. 9 | */ 10 | final class MergeOperation extends AbstractOperation { 11 | /** 12 | * {@inheritdoc} 13 | */ 14 | <<__Override>> 15 | protected function processDomain(string $domain): void { 16 | $this->messages[$domain] = shape( 17 | 'all' => dict[], 18 | 'new' => dict[], 19 | 'obsolete' => dict[], 20 | ); 21 | foreach ($this->source->domain($domain) as $id => $message) { 22 | $this->messages[$domain]['all'][$id] = $message; 23 | $this->result->add([$id => $message], $domain); 24 | } 25 | 26 | foreach ($this->target->domain($domain) as $id => $message) { 27 | if (!$this->source->has($id, $domain)) { 28 | $this->messages[$domain]['all'][$id] = $message; 29 | $this->messages[$domain]['new'][$id] = $message; 30 | $this->result->add([$id => $message], $domain); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | interface IException { 4 | require extends \Exception; 5 | } 6 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/InvalidArgumentException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | <<__Sealed(InvalidResourceException::class)>> 4 | class InvalidArgumentException 5 | extends \InvalidArgumentException 6 | implements IException {} 7 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/InvalidResourceException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | final class InvalidResourceException extends InvalidArgumentException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/LogicException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | final class LogicException extends \LogicException implements IException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/NotFoundResourceException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | final class NotFoundResourceException 4 | extends \RuntimeException 5 | implements IException {} 6 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Exception/RuntimeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Exception; 2 | 3 | final class RuntimeException extends \RuntimeException implements IException {} 4 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Format.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation; 2 | 3 | final class Format { 4 | const classname> 5 | Json = Loader\JsonFileLoader::class, 6 | Ini = Loader\IniFileLoader::class; 7 | 8 | const classname>> Tree = 9 | Loader\TreeLoader::class; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Formatter/IMessageFormatter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Formatter; 2 | 3 | interface IMessageFormatter { 4 | /** 5 | * Formats a localized message pattern with given arguments. 6 | */ 7 | public function format( 8 | string $message, 9 | string $locale, 10 | KeyedContainer $parameters = dict[], 11 | ): string; 12 | } 13 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Formatter/MessageFormatter.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Formatter; 2 | 3 | use namespace HH\Lib\{C, Str}; 4 | use namespace Nuxed\Translation\Exception; 5 | 6 | final class MessageFormatter implements IMessageFormatter { 7 | 8 | 9 | private dict> $cache = dict[]; 10 | 11 | /** 12 | * {@inheritdoc} 13 | */ 14 | public function format( 15 | string $message, 16 | string $locale, 17 | KeyedContainer $parameters = dict[], 18 | ): string { 19 | $formatter = $this->cache[$locale][$message] ?? null; 20 | if ($formatter is null) { 21 | try { 22 | $formatter = new \MessageFormatter($locale, $message); 23 | if (!C\contains_key($this->cache, $locale)) { 24 | $this->cache[$locale] = dict[]; 25 | } 26 | $this->cache[$locale][$message] = $formatter; 27 | } catch (\Throwable $e) { 28 | throw new Exception\InvalidArgumentException(Str\format( 29 | 'Invalid message format (error #%d): %s.', 30 | \intl_get_error_code(), 31 | \intl_get_error_message(), 32 | )); 33 | } 34 | } 35 | 36 | $params = []; 37 | foreach ($parameters as $key => $value) { 38 | if (C\contains(['%', '{'], $key[0])) { 39 | $params[Str\trim($key, '%{ }')] = $value; 40 | } else { 41 | $params[$key] = $value; 42 | } 43 | } 44 | 45 | $message = $formatter->format($params); 46 | if (!$message is string) { 47 | throw new Exception\InvalidArgumentException(Str\format( 48 | 'Unable to format message (error #%s): %s.', 49 | $formatter->getErrorCode(), 50 | $formatter->getErrorMessage(), 51 | )); 52 | } 53 | 54 | return $message; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/ILocaleAware.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation; 2 | 3 | interface ILocaleAware { 4 | /** 5 | * Sets the current locale. 6 | * 7 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters 8 | */ 9 | public function setLocale(string $locale): void; 10 | 11 | /** 12 | * Returns the current locale. 13 | */ 14 | public function getLocale(): string; 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/ITranslator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation; 2 | 3 | interface ITranslator { 4 | /** 5 | * Translates the given message. 6 | * 7 | * @param string $id 8 | * The message id (may also be an object that can be cast to string) 9 | * @param KeyedContainer $parameters 10 | * An array of parameters for the message 11 | * @param string|null $domain 12 | * The domain for the message or null to use the default 13 | * @param string|null $locale 14 | * The locale or null to use the default 15 | * 16 | * @return string The translated string 17 | * 18 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters 19 | */ 20 | public function trans( 21 | string $id, 22 | KeyedContainer $parameters = dict[], 23 | ?string $domain = null, 24 | ?string $locale = null, 25 | ): string; 26 | } 27 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/ITranslatorBag.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation; 2 | 3 | interface ITranslatorBag { 4 | /** 5 | * Gets the catalogue by locale 6 | * 7 | * @throws Exception\InvalidArgumentException If the locale contains invalid characters 8 | */ 9 | public function getCatalogue(?string $locale = null): MessageCatalogue; 10 | } 11 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Loader/FileLoader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Loader; 2 | 3 | use namespace Nuxed\{Filesystem, Translation}; 4 | use namespace HH\Lib\Str; 5 | use namespace Nuxed\Translation\Exception; 6 | 7 | abstract class FileLoader implements ILoader { 8 | public function load( 9 | string $resource, 10 | string $locale, 11 | string $domain = 'messages', 12 | ): Translation\MessageCatalogue { 13 | $resource = Filesystem\Path::create($resource); 14 | if (!$resource->exists()) { 15 | throw new Exception\NotFoundResourceException( 16 | Str\format('File (%s) not found.', $resource->toString()), 17 | ); 18 | } 19 | 20 | $resource = $this->loadResource($resource); 21 | return new TreeLoader() |> $$->load($resource, $locale, $domain); 22 | } 23 | 24 | /** 25 | * @return tree 26 | */ 27 | abstract protected function loadResource( 28 | Filesystem\Path $resource, 29 | ): KeyedContainer; 30 | } 31 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Loader/ILoader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Loader; 2 | 3 | use namespace Nuxed\Translation; 4 | 5 | interface ILoader { 6 | /** 7 | * Loads a locale. 8 | * 9 | * @throws Translation\Exception\NotFoundResourceException when the resource cannot be found 10 | * @throws Translation\Exception\InvalidResourceException when the resource cannot be loaded 11 | */ 12 | public function load( 13 | T $resource, 14 | string $locale, 15 | string $domain = 'messages', 16 | ): Translation\MessageCatalogue; 17 | } 18 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Loader/IniFileLoader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Loader; 2 | 3 | use namespace Nuxed\Filesystem; 4 | use namespace HH\Lib\Str; 5 | use namespace Facebook\TypeSpec; 6 | use namespace Nuxed\Translation\Exception; 7 | 8 | final class IniFileLoader extends FileLoader { 9 | <<__Override>> 10 | public function loadResource( 11 | Filesystem\Path $resource, 12 | ): KeyedContainer { 13 | $messages = @\parse_ini_file($resource->toString(), true); 14 | if (false === $messages) { 15 | throw new Exception\InvalidResourceException( 16 | Str\format('Error parsing ini file (%s).', $resource->toString()), 17 | ); 18 | } 19 | 20 | return TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed()) 21 | ->coerceType($messages ?? dict[]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Loader/JsonFileLoader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Loader; 2 | 3 | use namespace HH\Asio; 4 | use namespace HH\Lib\Str; 5 | use namespace Nuxed\Filesystem; 6 | use namespace Nuxed\Util\Json; 7 | use namespace Facebook\TypeSpec; 8 | use namespace Nuxed\Translation\Exception; 9 | 10 | final class JsonFileLoader extends FileLoader { 11 | <<__Override>> 12 | public function loadResource( 13 | Filesystem\Path $resoruce, 14 | ): KeyedContainer { 15 | $file = Filesystem\Node::load($resoruce) as Filesystem\File; 16 | 17 | try { 18 | $contents = Asio\join($file->read()); 19 | $messages = Json\decode($contents); 20 | return TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed()) 21 | ->coerceType($messages ?? dict[]); 22 | } catch (Filesystem\Exception\IException $e) { 23 | throw new Exception\InvalidResourceException( 24 | Str\format('Unable to load file (%s).', $resoruce->toString()), 25 | $e->getCode(), 26 | $e, 27 | ); 28 | } catch (Json\Exception\JsonDecodeException $e) { 29 | throw new Exception\InvalidResourceException( 30 | Str\format('Error parsing json file (%s).', $resoruce->toString()), 31 | $e->getCode(), 32 | $e, 33 | ); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Loader/TreeLoader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Loader; 2 | 3 | use namespace HH\Lib\Str; 4 | use namespace Facebook\TypeSpec; 5 | use namespace Nuxed\Translation; 6 | 7 | final class TreeLoader implements ILoader> { 8 | /** 9 | * @param tree $resource 10 | */ 11 | public function load( 12 | KeyedContainer $resource, 13 | string $locale, 14 | string $domain = 'messages', 15 | ): Translation\MessageCatalogue { 16 | $catalogue = new Translation\MessageCatalogue($locale); 17 | $catalogue->add($this->flatten($resource), $domain); 18 | return $catalogue; 19 | } 20 | 21 | final protected function flatten( 22 | KeyedContainer $tree, 23 | ): KeyedContainer { 24 | $result = dict[]; 25 | foreach ($tree as $key => $value) { 26 | if ($value is arraykey || $value is num) { 27 | $result[$key] = $value is num 28 | ? Str\format_number($value, 2) 29 | : (string)$value; 30 | } else { 31 | $value = TypeSpec\dict(TypeSpec\string(), TypeSpec\mixed()) 32 | ->coerceType($value); 33 | foreach ($this->flatten($value) as $k => $v) { 34 | $result[$key.'.'.$k] = $v; 35 | } 36 | } 37 | } 38 | 39 | return $result; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Reader/ITranslationReader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Reader; 2 | 3 | use namespace Nuxed\Translation; 4 | 5 | /** 6 | * TranslationReader reads translation messages from translation files. 7 | */ 8 | interface ITranslationReader { 9 | /** 10 | * Reads translation messages from a directory to the catalogue. 11 | */ 12 | public function read( 13 | string $directory, 14 | Translation\MessageCatalogue $catalogue, 15 | ): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/Reader/TranslationReader.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\Reader; 2 | 3 | use namespace HH\Asio; 4 | use namespace HH\Lib\Str; 5 | use namespace Nuxed\{Filesystem, Translation}; 6 | use namespace Nuxed\Translation\Loader; 7 | 8 | /** 9 | * TranslationReader reads translation messages from translation files. 10 | */ 11 | final class TranslationReader implements ITranslationReader { 12 | /** 13 | * Loaders used for import. 14 | */ 15 | private dict> $loaders = dict[]; 16 | 17 | /** 18 | * Adds a loader to the translation reader. 19 | */ 20 | public function addLoader( 21 | string $format, 22 | Loader\ILoader $loader, 23 | ): this { 24 | $this->loaders[$format] = $loader; 25 | return $this; 26 | } 27 | 28 | /** 29 | * Reads translation messages from a directory to the catalogue. 30 | */ 31 | public function read( 32 | string $directory, 33 | Translation\MessageCatalogue $catalogue, 34 | ): void { 35 | try { 36 | $folder = Filesystem\Node::load(Filesystem\Path::create($directory)) as Filesystem\Folder; 37 | } catch (\Throwable $e) { 38 | return; 39 | } 40 | 41 | $files = Asio\join(Asio\wrap($folder->files(false, true))); 42 | if ($files->isFailed()) { 43 | return; 44 | } else { 45 | $files = $files->getResult(); 46 | } 47 | 48 | foreach ($this->loaders as $format => $loader) { 49 | $extension = Str\format('.%s.%s', $catalogue->getLocale(), $format); 50 | foreach ($files as $file) { 51 | $basename = $file->path()->basename(); 52 | if (Str\ends_with($basename, $extension)) { 53 | $domain = Str\strip_suffix($basename, $extension); 54 | $catalogue->addCatalogue( 55 | $loader->load( 56 | $file->path()->toString(), 57 | $catalogue->getLocale(), 58 | $domain, 59 | ), 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Nuxed/Translation/_Private/LoaderContainer.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Translation\_Private; 2 | 3 | use namespace HH\Lib\{C, Str}; 4 | use namespace Nuxed\Translation\{Exception, Loader}; 5 | 6 | final class LoaderContainer { 7 | private dict $loaders = dict[]; 8 | 9 | public function getLoader( 10 | classname> $format, 11 | ): Loader\ILoader { 12 | if (!C\contains_key($this->loaders, $format)) { 13 | throw new Exception\RuntimeException( 14 | Str\format('The "%s" translation loader is not registered.', $format), 15 | ); 16 | } 17 | 18 | /* HH_IGNORE_ERROR[4110] */ 19 | return $this->loaders[$format]; 20 | } 21 | 22 | public function addLoader( 23 | classname> $format, 24 | Loader\ILoader $loader, 25 | ): void { 26 | $this->loaders[$format] = $loader; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Exception/IException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Exception; 2 | 3 | use type Exception; 4 | 5 | interface IException { 6 | require extends Exception; 7 | } 8 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/Errors.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json; 2 | 3 | const dict Errors = dict[ 4 | \JSON_ERROR_NONE => 'No error', 5 | \JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 6 | \JSON_ERROR_STATE_MISMATCH => 'State mismatch (invalid or malformed JSON)', 7 | \JSON_ERROR_CTRL_CHAR => 8 | 'Control character error, possibly incorrectly encoded', 9 | \JSON_ERROR_SYNTAX => 'Syntax error', 10 | \JSON_ERROR_UTF8 => 11 | 'Malformed UTF-8 characters, possibly incorrectly encoded', 12 | \JSON_ERROR_INF_OR_NAN => 'Inf and NaN cannot be JSON encoded', 13 | \JSON_ERROR_UNSUPPORTED_TYPE => 14 | 'A value of a type that cannot be encoded was given', 15 | ]; 16 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/Exception/JsonDecodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json\Exception; 2 | 3 | use namespace Nuxed\Util\Exception; 4 | 5 | class JsonDecodeException 6 | extends \InvalidArgumentException 7 | implements Exception\IException { 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/Exception/JsonEncodeException.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json\Exception; 2 | 3 | use namespace Nuxed\Util\Exception; 4 | 5 | class JsonEncodeException 6 | extends \InvalidArgumentException 7 | implements Exception\IException { 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/decode.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json; 2 | 3 | function decode(string $json, bool $assoc = true): dynamic { 4 | try { 5 | $value = \json_decode( 6 | $json, 7 | $assoc, 8 | 512, 9 | \JSON_BIGINT_AS_STRING | \JSON_FB_HACK_ARRAYS, 10 | ); 11 | $error = \json_last_error(); 12 | if (\JSON_ERROR_NONE !== $error) { 13 | throw new Exception\JsonDecodeException(Errors[$error], $error); 14 | } 15 | 16 | return $value; 17 | } catch (\Throwable $e) { 18 | throw new Exception\JsonDecodeException( 19 | $e->getMessage(), 20 | (int)$e->getCode(), 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/encode.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json; 2 | 3 | use type Nuxed\Util\Jsonable; 4 | 5 | function encode(mixed $value, bool $pretty = false, int $flags = 0): string { 6 | if ($value is Jsonable) { 7 | return $value->toJson($pretty); 8 | } 9 | 10 | $flags |= \JSON_UNESCAPED_UNICODE | 11 | \JSON_UNESCAPED_SLASHES | 12 | \JSON_PRESERVE_ZERO_FRACTION; 13 | if ($pretty) { 14 | $flags |= \JSON_PRETTY_PRINT; 15 | } 16 | 17 | $json = \json_encode($value, $flags); 18 | $error = \json_last_error(); 19 | if (\JSON_ERROR_NONE !== $error) { 20 | throw new Exception\JsonEncodeException(Errors[$error], $error); 21 | } 22 | 23 | return $json; 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/spec.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json; 2 | 3 | use namespace Facebook\{TypeAssert, TypeSpec}; 4 | 5 | function spec( 6 | string $json, 7 | TypeSpec\TypeSpec $spec, 8 | bool $assert = false, 9 | ): T { 10 | $value = decode($json); 11 | try { 12 | if ($assert) { 13 | return $spec->assertType($value); 14 | } 15 | 16 | return $spec->coerceType($value); 17 | } catch (TypeAssert\TypeCoercionException $e) { 18 | throw new Exception\JsonDecodeException( 19 | $e->getMessage(), 20 | $e->getCode(), 21 | $e, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Json/structure.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util\Json; 2 | 3 | use namespace Facebook\TypeAssert; 4 | 5 | function structure(string $json, TypeStructure $structure): T { 6 | try { 7 | return TypeAssert\matches_type_structure($structure, decode($json)); 8 | } catch (TypeAssert\IncorrectTypeException $e) { 9 | throw new Exception\JsonDecodeException( 10 | $e->getMessage(), 11 | $e->getCode(), 12 | $e, 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Jsonable.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util; 2 | 3 | interface Jsonable { 4 | /** 5 | * Return a valid json string. 6 | * the implementation MUST not call Nuxed\Util\Json::encode 7 | * on it self, instead the inner data. 8 | * 9 | * e.g : 10 | * 11 | * public function toJson(): string 12 | * { 13 | * return \Nuxed\Util\Json::encode($this->data); 14 | * } 15 | * 16 | */ 17 | public function toJson(bool $pretty = false): string; 18 | } 19 | -------------------------------------------------------------------------------- /src/Nuxed/Util/Stringable.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util; 2 | 3 | interface Stringable { 4 | /** 5 | * Return a string representing the current object. 6 | */ 7 | public function toString(): string; 8 | } 9 | -------------------------------------------------------------------------------- /src/Nuxed/Util/StringableTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util; 2 | 3 | trait StringableTrait implements Stringable, \Stringish { 4 | abstract public function toString(): string; 5 | 6 | public function __toString(): string { 7 | try { 8 | return $this->toString(); 9 | } catch (\Throwable $e) { 10 | return ''; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Nuxed/Util/alternatives.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util; 2 | 3 | use namespace HH\Lib\{Dict, Str, Vec}; 4 | 5 | /** 6 | * @param string $name The original name of the item that does not exist 7 | * @param Container $items a container of possible items 8 | */ 9 | <<__Memoize>> 10 | function alternatives( 11 | string $name, 12 | Container $items, 13 | ): Container { 14 | $alternatives = dict[]; 15 | foreach ($items as $item) { 16 | $lev = \levenshtein($name, $item); 17 | if ($lev <= Str\length($name) / 3 || Str\contains($item, $name)) { 18 | $alternatives[$item] = $lev; 19 | } 20 | } 21 | 22 | return Vec\keys(Dict\sort($alternatives)); 23 | } 24 | -------------------------------------------------------------------------------- /src/Nuxed/Util/stringify.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Util; 2 | 3 | use namespace HH\Lib\Str; 4 | 5 | function stringify(mixed $value): string { 6 | if ($value is bool) { 7 | $value = ($value ? 'true' : 'false'); 8 | } else if ($value is string) { 9 | $value = '"'.$value.'"'; 10 | } else if ($value is num) { 11 | $value = $value is int ? $value : Str\format_number($value, 1); 12 | } else if ($value is resource) { 13 | $value = 'resource['.\get_resource_type($value).']'; 14 | } else if ($value is null) { 15 | $value = 'null'; 16 | } else if (\is_object($value) && !$value is Container<_>) { 17 | if ($value is \Throwable) { 18 | $value = \get_class($value). 19 | '['. 20 | 'message='. 21 | stringify($value->getMessage()). 22 | ', code='. 23 | stringify($value->getCode()). 24 | ', file='. 25 | stringify($value->getFile()). 26 | ', line='. 27 | stringify($value->getLine()). 28 | ', trace= '. 29 | stringify($value->getTrace()). 30 | ', previous='. 31 | stringify($value->getPrevious()). 32 | ']'; 33 | } else if ($value is \DateTimeInterface) { 34 | $value = \get_class($value).'['.$value->format("Y-m-d\TH:i:s.uP").']'; 35 | } else { 36 | $value = 'object['.\get_class($value).']'; 37 | } 38 | } else if ($value is Container<_>) { 39 | $value = Json\encode($value, false); 40 | } else { 41 | $value = '!'.\gettype($value).Json\encode($value, false); 42 | } 43 | 44 | return (string)$value; 45 | } 46 | -------------------------------------------------------------------------------- /tests/Nuxed/EventDispatcher/Fixture/OrderCanceledEvent.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\EventDispatcher\Fixture; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | final class OrderCanceledEvent implements EventDispatcher\IStoppableEvent { 6 | public bool $handled = false; 7 | 8 | public function __construct(public string $orderId) {} 9 | 10 | public function isPropagationStopped(): bool { 11 | return $this->handled; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Nuxed/EventDispatcher/Fixture/OrderCanceledEventListener.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\EventDispatcher\Fixture; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | final class OrderCanceledEventListener 6 | implements EventDispatcher\IEventListener { 7 | 8 | public function __construct( 9 | public string $append, 10 | private bool $handle = false, 11 | ) {} 12 | 13 | public async function process(OrderCanceledEvent $event): Awaitable { 14 | $event->orderId .= $this->append; 15 | if ($this->handle) { 16 | $event->handled = true; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Nuxed/EventDispatcher/Fixture/OrderCreatedEvent.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\EventDispatcher\Fixture; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | final class OrderCreatedEvent implements EventDispatcher\IEvent { 6 | public function __construct(public string $orderId) {} 7 | } 8 | -------------------------------------------------------------------------------- /tests/Nuxed/EventDispatcher/Fixture/OrderCreatedEventListener.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\EventDispatcher\Fixture; 2 | 3 | use namespace Nuxed\EventDispatcher; 4 | 5 | class OrderCreatedEventListener 6 | implements EventDispatcher\IEventListener { 7 | public async function process(OrderCreatedEvent $event): Awaitable { 8 | throw new \Exception("Error Processing Event"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/Nuxed/EventDispatcher/ListenerProvider/ListenerProviderAggregateTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\EventDispatcher\ListenerProvider; 2 | 3 | use namespace HH\Lib\C; 4 | use namespace Facebook\HackTest; 5 | use namespace Nuxed\Test\EventDispatcher\Fixture; 6 | use namespace Nuxed\EventDispatcher\ListenerProvider; 7 | use function Facebook\FBExpect\expect; 8 | 9 | class ListenerProviderAggregateTest extends HackTest\HackTest { 10 | public async function testAttachAndGetListeners(): Awaitable { 11 | $aggregate = new ListenerProvider\ListenerProviderAggregate(); 12 | $attachableProvider = new ListenerProvider\AttachableListenerProvider(); 13 | $reifiedProvider = new ListenerProvider\ReifiedListenerProvider(); 14 | $aggregate->attach($attachableProvider); 15 | $aggregate->attach($reifiedProvider); 16 | 17 | $listeners = vec[ 18 | new Fixture\OrderCanceledEventListener('foo'), 19 | new Fixture\OrderCanceledEventListener('baz'), 20 | new Fixture\OrderCanceledEventListener('qux'), 21 | ]; 22 | foreach ($listeners as $listener) { 23 | $attachableProvider->listen(Fixture\OrderCanceledEvent::class, $listener); 24 | $reifiedProvider->listen($listener); 25 | } 26 | $attachableProvider->listen( 27 | Fixture\OrderCreatedEvent::class, 28 | new Fixture\OrderCreatedEventListener(), 29 | ); 30 | 31 | $event = new Fixture\OrderCanceledEvent('bar'); 32 | $i = 0; 33 | foreach ( 34 | $aggregate->getListeners($event) await as 35 | $listener 36 | ) { 37 | expect($listeners)->toContain($listener); 38 | $i++; 39 | } 40 | 41 | expect($i)->toBeSame(6); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Nuxed/Filesystem/IoTestTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Filesystem; 2 | 3 | use namespace HH\Asio; 4 | use namespace HH\Lib\PseudoRandom; 5 | use namespace Nuxed\Filesystem; 6 | use type Facebook\HackTest\HackTest; 7 | 8 | trait IoTestTrait { 9 | require extends HackTest; 10 | public static async function beforeFirstTestAsync(): Awaitable { 11 | $tmp = static::temporaryFolder(); 12 | if (!$tmp->exists()) { 13 | await $tmp->create(); 14 | } 15 | } 16 | 17 | public static async function afterLastTestAsync(): Awaitable { 18 | $tmp = static::temporaryFolder(); 19 | if ($tmp->exists()) { 20 | await $tmp->delete(); 21 | } 22 | } 23 | 24 | protected static function temporaryFolder(): Filesystem\Folder { 25 | return new Filesystem\Folder(Filesystem\Path::create(__DIR__.'/../../tmp')); 26 | } 27 | 28 | protected static function createPath(): Filesystem\Path { 29 | $path = static::temporaryFolder()->path()->toString(). 30 | '/'. 31 | PseudoRandom\string(32, 'qwertyuiopasdfghjklzxcvbnm123456789'); 32 | return Filesystem\Path::create($path); 33 | } 34 | 35 | protected static function createFile(): Filesystem\File { 36 | return Asio\join( 37 | Filesystem\File::temporary('io_file_', static::temporaryFolder()->path()), 38 | ); 39 | } 40 | 41 | protected static function createFolder(): Filesystem\Folder { 42 | return new Filesystem\Folder(static::createPath(), true); 43 | } 44 | 45 | protected static function createSymlink(): Filesystem\File { 46 | $file = static::createFile(); 47 | $symlink = static::createPath(); 48 | return Asio\join($file->symlink($symlink)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Server/Handler/CallableHandlerDecorator.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Server\Handler; 2 | 3 | use namespace Nuxed\Http\Server; 4 | use namespace Nuxed\Http\Message; 5 | use type Facebook\HackTest\HackTest; 6 | use function Facebook\FBExpect\expect; 7 | 8 | class CallableHandlerDecoratorTest extends HackTest { 9 | public async function testCallableMiddleware(): Awaitable { 10 | $resposne = Message\Response\text('foo'); 11 | $call = async ($request) ==> $resposne; 12 | $handler = new Server\Handler\CallableHandlerDecorator($call); 13 | $return = await $handler->handle( 14 | new Message\ServerRequest('GET', new Message\Uri('/')), 15 | ); 16 | expect($return)->toBeSame($resposne); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Server/Middleware/CallableMiddlewareDecoratorTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\Server; 4 | use type Facebook\HackTest\HackTest; 5 | use function Facebook\FBExpect\expect; 6 | 7 | class CallableMiddlewareDecoratorTest extends HackTest { 8 | use RequestFactoryTestTrait; 9 | 10 | public async function testCallableMiddleware(): Awaitable { 11 | $call = ($request, $handler) ==> 12 | $handler->handle($request->withAttribute('foo', 'bar')); 13 | 14 | $middleware = new Server\Middleware\CallableMiddlewareDecorator($call); 15 | 16 | $handler = Server\dh(async ($request, $response) ==> { 17 | await $response->getBody() 18 | ->writeAsync($request->getAttribute('foo') as string); 19 | return $response; 20 | }); 21 | 22 | $response = await $middleware->process($this->request('/'), $handler); 23 | $body = $response->getBody(); 24 | $body->rewind(); 25 | $content = await $body->readAsync(); 26 | expect($content)->toBeSame('bar'); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Server/Middleware/OriginalMessageMiddlewareTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\Server; 4 | use type Facebook\HackTest\HackTest; 5 | use function Facebook\FBExpect\expect; 6 | 7 | class OriginalMessageMiddlewareTest extends HackTest { 8 | use RequestFactoryTestTrait; 9 | 10 | public async function testOriginalMessage(): Awaitable { 11 | $originalRequest = $this->request('/foo/bar'); 12 | 13 | $hanlder = Server\dh(async ($request, $response) ==> { 14 | expect($request->getHeader('X-FOO'))->toBeSame(vec['bar']); 15 | expect($request->getUri()->getPort())->toBeSame(8080); 16 | 17 | expect($request->getUri()) 18 | ->toNotBeSame($originalRequest->getUri()); 19 | expect($request) 20 | ->toNotBeSame($originalRequest); 21 | expect($request->getAttribute('OriginalUri')) 22 | ->toBeSame($originalRequest->getUri()); 23 | expect($request->getAttribute('OriginalRequest')) 24 | ->toBeSame($originalRequest); 25 | 26 | await $response->getBody()->writeAsync('pass.'); 27 | return $response; 28 | }); 29 | 30 | $RequestModifier = Server\cm( 31 | ($request, $handler) ==> 32 | $handler->handle($request->withAddedHeader('X-FOO', vec['bar'])), 33 | ); 34 | $UriModifier = Server\cm( 35 | ($request, $handler) ==> 36 | $handler->handle($request->withUri($request->getUri()->withPort(8080))), 37 | ); 38 | 39 | $middleware = Server\stack( 40 | new Server\Middleware\OriginalMessagesMiddleware(), 41 | $RequestModifier, 42 | $UriModifier, 43 | ); 44 | 45 | $response = await $middleware->process($originalRequest, $hanlder); 46 | $body = $response->getBody(); 47 | $body->rewind(); 48 | $content = await $body->readAsync(); 49 | expect($content)->toBeSame('pass.'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Server/Middleware/RequestFactoryTestTrait.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\Server; 4 | use namespace Nuxed\Http\Message; 5 | use type Facebook\HackTest\HackTest; 6 | 7 | trait RequestFactoryTestTrait { 8 | require extends HackTest; 9 | 10 | final protected function request(string $uri): Message\ServerRequest { 11 | return new Message\ServerRequest('GET', Message\uri($uri)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Server/Middleware/RequestHandlerMiddlewareDecoratorTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Server\Middleware; 2 | 3 | use namespace Nuxed\Http\Server; 4 | use type Facebook\HackTest\HackTest; 5 | use function Facebook\FBExpect\expect; 6 | 7 | class HandlerMiddlewareDecoratorTest extends HackTest { 8 | use RequestFactoryTestTrait; 9 | 10 | public async function testHandlerMiddleware(): Awaitable { 11 | $handler = Server\dh(async ($request, $resposne) ==> { 12 | await $resposne->getBody()->writeAsync('foo'); 13 | return $resposne; 14 | }); 15 | 16 | $middleware = new Server\Middleware\HandlerMiddlewareDecorator( 17 | $handler, 18 | ); 19 | 20 | expect($middleware)->toBeInstanceOf(Server\IMiddleware::class); 21 | expect($middleware)->toBeInstanceOf(Server\IHandler::class); 22 | 23 | $response = await $middleware->process( 24 | $this->request('/'), 25 | Server\dh(async ($request, $response) ==> $response), 26 | ); 27 | $body = $response->getBody(); 28 | $body->rewind(); 29 | $content = await $body->readAsync(); 30 | expect($content)->toBeSame('foo'); 31 | $response = await $middleware->handle($this->request('/')); 32 | $body = $response->getBody(); 33 | $body->rewind(); 34 | $content = await $body->readAsync(); 35 | $body = $response->getBody(); 36 | $body->rewind(); 37 | $content = await $body->readAsync(); 38 | expect($content)->toBeSame('foo'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Session/Persistence/CacheSessionPersistenceTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Session\Persistence; 2 | 3 | use namespace HH\Asio; 4 | use namespace Nuxed\Cache; 5 | use namespace Nuxed\Http\Session; 6 | 7 | class CacheSessionPersistenceTest extends AbstractSessionPersistenceTest { 8 | protected async function createSessionPersistence( 9 | TCookieOptions $cookie, 10 | ?Session\CacheLimiter $limiter, 11 | int $expiry, 12 | ): Awaitable { 13 | $cache = new Cache\Cache(new Cache\Store\ArrayStore()); 14 | $persistence = new Session\Persistence\CacheSessionPersistence( 15 | $cache, 16 | $cookie, 17 | $limiter, 18 | $expiry, 19 | ); 20 | return $persistence; 21 | } 22 | 23 | public async function createSessionPersistenceWithPreviousData( 24 | TCookieOptions $cookie, 25 | ?Session\CacheLimiter $limiter, 26 | int $expiry, 27 | string $id, 28 | KeyedContainer $data, 29 | ): Awaitable { 30 | $cache = new Cache\Cache(new Cache\Store\ArrayStore()); 31 | $persistence = new Session\Persistence\CacheSessionPersistence( 32 | $cache, 33 | $cookie, 34 | $limiter, 35 | $expiry, 36 | ); 37 | Asio\join($cache->forever($id, $data)); 38 | return $persistence; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Nuxed/Http/Session/SessionMiddlewareTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Http\Session; 2 | 3 | use namespace Nuxed\Http; 4 | use namespace Nuxed\Http\Session; 5 | use namespace Nuxed\Http\Server; 6 | use namespace Nuxed\Http\Message; 7 | use type Facebook\HackTest\HackTest; 8 | use function Facebook\FBExpect\expect; 9 | 10 | class SessionMiddlewareTest extends HackTest { 11 | public async function testSessionMiddleware(): Awaitable { 12 | $persistence = new DummyPersistence(); 13 | $middleware = new Session\SessionMiddleware($persistence); 14 | $handler = Server\dh( 15 | async ($request, $response) ==> { 16 | expect($request->hasSession())->toBeTrue(); 17 | $session = $request->getSession(); 18 | expect($session->get('foo'))->toBeSame('bar'); 19 | return $response->withAddedHeader('foo', vec['bar']); 20 | }, 21 | ); 22 | 23 | $resposne = await $middleware->process( 24 | new Message\ServerRequest('GET', new Message\Uri('/foo')), 25 | $handler, 26 | ); 27 | 28 | $body = $resposne->getBody(); 29 | await $body->flushAsync(); 30 | $body->rewind(); 31 | $content = await $body->readAsync(); 32 | expect($content)->toBeSame('foo'); 33 | expect($resposne->getHeaderLine('foo'))->toBeSame('bar'); 34 | } 35 | } 36 | 37 | class DummyPersistence implements Session\Persistence\ISessionPersistence { 38 | public async function initialize( 39 | Message\ServerRequest $request, 40 | ): Awaitable { 41 | return new Session\Session(dict['foo' => 'bar']); 42 | } 43 | 44 | public async function persist( 45 | Session\Session $session, 46 | Message\Response $response, 47 | ): Awaitable { 48 | await $response->getBody()->writeAsync('foo'); 49 | return $response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Nuxed/Mercure/UpdateTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Mercure; 2 | 3 | use namespace Nuxed\Mercure; 4 | use namespace Nuxed\Http\Client; 5 | use namespace Nuxed\Http\Message; 6 | use namespace Facebook\HackTest; 7 | use function Facebook\FBExpect\expect; 8 | 9 | class UpdateTest extends HackTest\HackTest { 10 | <> 11 | public function testCreateUpdate( 12 | Container $topics, 13 | string $data, 14 | Container $targets = vec[], 15 | ?string $id = null, 16 | ?string $type = null, 17 | ?int $retry = null, 18 | ): void { 19 | $update = new Mercure\Update($topics, $data, $targets, $id, $type, $retry); 20 | expect($topics)->toBeSame($update->getTopics()); 21 | expect($data)->toBeSame($update->getData()); 22 | expect($targets)->toBeSame($update->getTargets()); 23 | expect($id)->toBeSame($update->getId()); 24 | expect($type)->toBeSame($update->getType()); 25 | expect($retry)->toBeSame($update->getRetry()); 26 | } 27 | 28 | public function updateProvider( 29 | ): Container< 30 | (Container, string, Container, ?string, ?string, ?int), 31 | > { 32 | return vec[ 33 | tuple( 34 | vec['http://example.com/foo'], 35 | 'payload', 36 | vec['user-1', 'group-a'], 37 | 'id', 38 | 'type', 39 | 1936, 40 | ), 41 | tuple( 42 | vec['https://mercure.rocks', 'https://github.com/dunglas/mercure'], 43 | 'payload', 44 | vec[], 45 | null, 46 | null, 47 | null, 48 | ), 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/Loader/IniFileLoaderTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Translation\Loader; 2 | 3 | use namespace Facebook\HackTest; 4 | use namespace Nuxed\Translation\Loader; 5 | use namespace Nuxed\Translation\Exception; 6 | use function Facebook\FBExpect\expect; 7 | 8 | class IniFileLoaderTest extends LoaderTest { 9 | protected function getLoader(): Loader\ILoader { 10 | return new Loader\IniFileLoader(); 11 | } 12 | 13 | public function provideLoadData( 14 | ): Container<(string, string, string, KeyedContainer)> { 15 | $en = dict[ 16 | "layout.home" => "Home", 17 | "layout.login" => "Login", 18 | "layout.register" => "Register", 19 | "layout.logout" => "Logout", 20 | "layout.settings" => "Settings", 21 | "layout.forgot_password" => "Forgot your password ?", 22 | "layout.logout_confirmation" => "Are you sure you want to logout ?", 23 | "layout.password_recover" => "Recover password", 24 | "layout.password_reset" => "Reset password", 25 | ]; 26 | 27 | $fr = dict[ 28 | "layout.home" => "Accueil", 29 | "layout.login" => "S'identifier", 30 | "layout.register" => "Registre", 31 | "layout.logout" => "Connectez - Out", 32 | "layout.settings" => "Paramètres", 33 | "layout.forgot_password" => "Mot de passe oublié ?", 34 | "layout.logout_confirmation" => 35 | "Êtes-vous sûr de vouloir vous déconnecter ?", 36 | "layout.password_recover" => "Récupérer mot de passe", 37 | "layout.password_reset" => "Réinitialiser le mot de passe", 38 | ]; 39 | 40 | return vec[ 41 | tuple(__DIR__.'/../fixtures/messages.en.ini', 'en', 'messages', $en), 42 | tuple(__DIR__.'/../fixtures/messages.fr.ini', 'fr', 'messages', $fr), 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/Loader/JsonFileLoaderTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Translation\Loader; 2 | 3 | use namespace Facebook\HackTest; 4 | use namespace Nuxed\Translation\Loader; 5 | use namespace Nuxed\Translation\Exception; 6 | use function Facebook\FBExpect\expect; 7 | 8 | class JsonFileLoaderTest extends LoaderTest { 9 | protected function getLoader(): Loader\ILoader { 10 | return new Loader\JsonFileLoader(); 11 | } 12 | 13 | public function provideLoadData( 14 | ): Container<(string, string, string, KeyedContainer)> { 15 | $en = dict[ 16 | "layout.home" => "Home", 17 | "layout.login" => "Login", 18 | "layout.register" => "Register", 19 | "layout.logout" => "Logout", 20 | "layout.settings" => "Settings", 21 | "layout.forgot_password" => "Forgot your password ?", 22 | "layout.logout_confirmation" => "Are you sure you want to logout ?", 23 | "layout.password_recover" => "Recover password", 24 | "layout.password_reset" => "Reset password", 25 | ]; 26 | 27 | $fr = dict[ 28 | "layout.home" => "Accueil", 29 | "layout.login" => "S'identifier", 30 | "layout.register" => "Registre", 31 | "layout.logout" => "Connectez - Out", 32 | "layout.settings" => "Paramètres", 33 | "layout.forgot_password" => "Mot de passe oublié ?", 34 | "layout.logout_confirmation" => 35 | "Êtes-vous sûr de vouloir vous déconnecter ?", 36 | "layout.password_recover" => "Récupérer mot de passe", 37 | "layout.password_reset" => "Réinitialiser le mot de passe", 38 | ]; 39 | 40 | return vec[ 41 | tuple(__DIR__.'/../fixtures/messages.en.json', 'en', 'messages', $en), 42 | tuple(__DIR__.'/../fixtures/messages.fr.json', 'fr', 'messages', $fr), 43 | ]; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/Loader/LoaderTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Translation\Loader; 2 | 3 | use namespace Facebook\HackTest; 4 | use namespace Nuxed\Translation\Loader; 5 | use function Facebook\FBExpect\expect; 6 | 7 | abstract class LoaderTest extends HackTest\HackTest { 8 | abstract protected function getLoader(): Loader\ILoader; 9 | 10 | <> 11 | public function testLoad( 12 | T $resource, 13 | string $locale, 14 | string $domain, 15 | KeyedContainer $expected, 16 | ): void { 17 | $loader = $this->getLoader(); 18 | $catalogue = $loader->load($resource, $locale, $domain); 19 | expect($catalogue->getLocale())->toBeSame($locale); 20 | expect($catalogue->getDomains())->toContain($domain); 21 | expect($catalogue->all()) 22 | ->toBeSame(dict[ 23 | $domain => $expected, 24 | ]); 25 | } 26 | 27 | abstract public function provideLoadData( 28 | ): Container<(T, string, string, KeyedContainer)>; 29 | } 30 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.en.ini: -------------------------------------------------------------------------------- 1 | [layout] 2 | home = "Home" 3 | login = "Login" 4 | register = "Register" 5 | logout = "Logout" 6 | settings = "Settings" 7 | forgot_password = "Forgot your password ?" 8 | logout_confirmation = "Are you sure you want to logout ?" 9 | password_recover = "Recover password" 10 | password_reset = "Reset password" 11 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": { 3 | "home": "Home", 4 | "login": "Login", 5 | "register": "Register", 6 | "logout": "Logout", 7 | "settings": "Settings", 8 | "forgot_password": "Forgot your password ?", 9 | "logout_confirmation": "Are you sure you want to logout ?", 10 | "password_recover": "Recover password", 11 | "password_reset": "Reset password" 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.en.yaml: -------------------------------------------------------------------------------- 1 | layout: 2 | home: 'Home' 3 | login: 'Login' 4 | register: 'Register' 5 | logout: 'Logout' 6 | settings: 'Settings' 7 | forgot_password: 'Forgot your password ?' 8 | logout_confirmation: 'Are you sure you want to logout ?' 9 | password_recover: 'Recover password' 10 | password_reset: 'Reset password' -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.fr.ini: -------------------------------------------------------------------------------- 1 | [layout] 2 | home = "Accueil" 3 | login = "S'identifier" 4 | register = "Registre" 5 | logout = "Connectez - Out" 6 | settings = "Paramètres" 7 | forgot_password = "Mot de passe oublié ?" 8 | logout_confirmation = "Êtes-vous sûr de vouloir vous déconnecter ?" 9 | password_recover = "Récupérer mot de passe" 10 | password_reset = "Réinitialiser le mot de passe" 11 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": { 3 | "home": "Accueil", 4 | "login": "S'identifier", 5 | "register": "Registre", 6 | "logout": "Connectez - Out", 7 | "settings": "Paramètres", 8 | "forgot_password": "Mot de passe oublié ?", 9 | "logout_confirmation": "Êtes-vous sûr de vouloir vous déconnecter ?", 10 | "password_recover": "Récupérer mot de passe", 11 | "password_reset": "Réinitialiser le mot de passe" 12 | } 13 | } -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/messages.fr.yaml: -------------------------------------------------------------------------------- 1 | layout: 2 | home: Accueil 3 | login: S'identifier 4 | register: Registre 5 | logout: Connectez - Out 6 | settings: Paramètres 7 | forgot_password: Mot de passe oublié ? 8 | logout_confirmation: Êtes-vous sûr de vouloir vous déconnecter ? 9 | password_recover: Récupérer mot de passe 10 | password_reset: Réinitialiser le mot de passe 11 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/user.en.ini: -------------------------------------------------------------------------------- 1 | ; Group 2 | [group.edit] 3 | submit = "Update group" 4 | [group.show] 5 | name = "Group name" 6 | [group.new] 7 | submit = "Create group" 8 | [group.flash] 9 | updated = "The group has been updated." 10 | created = "The group has been created." 11 | deleted = "The group has been deleted." 12 | 13 | ; Secuirty 14 | [security.login] 15 | username = "Username" 16 | password = "Password" 17 | remember_me = "Remember me" 18 | submit = "Log in" 19 | 20 | ; Profile 21 | [profile.show] 22 | username = "Username" 23 | email = "Email" 24 | [profile.edit] 25 | submit = "Update" 26 | [profile.flash] 27 | updated = "The profile has been updated." -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/user.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "group": { 3 | "edit": { 4 | "submit": "Update group" 5 | }, 6 | "show": { 7 | "name": "Group name" 8 | }, 9 | "new": { 10 | "submit": "Create group" 11 | }, 12 | "flash": { 13 | "updated": "The group has been updated.", 14 | "created": "The group has been created.", 15 | "deleted": "The group has been deleted." 16 | } 17 | }, 18 | "security": { 19 | "login": { 20 | "username": "Username", 21 | "password": "Password", 22 | "remember_me": "Remember me", 23 | "submit": "Log in" 24 | } 25 | }, 26 | "profile": { 27 | "show": { 28 | "username": "Username", 29 | "email": "Email" 30 | }, 31 | "edit": { 32 | "submit": "Update" 33 | }, 34 | "flash": { 35 | "updated": "The profile has been updated." 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Nuxed/Translation/fixtures/user.en.yaml: -------------------------------------------------------------------------------- 1 | group: 2 | edit: 3 | submit: 'Update group' 4 | show: 5 | name: 'Group name' 6 | new: 7 | submit: 'Create group' 8 | flash: 9 | updated: 'The group has been updated.' 10 | created: 'The group has been created.' 11 | deleted: 'The group has been deleted.' 12 | security: 13 | login: 14 | username: Username 15 | password: Password 16 | remember_me: 'Remember me' 17 | submit: 'Log in' 18 | profile: 19 | show: 20 | username: Username 21 | email: Email 22 | edit: 23 | submit: Update 24 | flash: 25 | updated: 'The profile has been updated.' -------------------------------------------------------------------------------- /tests/Nuxed/Util/AlternativesTest.hack: -------------------------------------------------------------------------------- 1 | namespace Nuxed\Test\Util; 2 | 3 | use type Facebook\HackTest\HackTest; 4 | use type Facebook\HackTest\DataProvider; 5 | use function Facebook\FBExpect\expect; 6 | use function Nuxed\Util\alternatives; 7 | 8 | class AlternativesTest extends HackTest { 9 | <> 10 | public function testAlternatives( 11 | string $name, 12 | Container $items, 13 | Container $expected, 14 | ): void { 15 | $result = alternatives($name, $items); 16 | // the alternatives function can return 17 | // any type of container, so we need to ensure that 18 | // both the $result and $expected are the same type. 19 | $result = vec($result); 20 | $expected = vec($expected); 21 | expect($result)->toBeSame($expected); 22 | } 23 | 24 | public function data( 25 | ): Container<(string, Container, Container)> { 26 | return vec[ 27 | tuple('helo', vec['hello', 'morning'], Set {'hello'}), 28 | tuple( 29 | 'daiky', 30 | vec['daily', 'weekly', 'monthly', 'dairy', 'book'], 31 | vec['daily', 'dairy'], 32 | ), 33 | tuple('foet', vec['foot', 'feet', 'boot', 'fool'], vec['foot', 'feet']), 34 | ]; 35 | } 36 | } 37 | --------------------------------------------------------------------------------