Skip to content

Commit

Permalink
V1.2 (#8)
Browse files Browse the repository at this point in the history
* Add support for default arguments in route declaration
* Add support for more precise types for the typescript route functions
  • Loading branch information
BolZer authored Mar 24, 2022
1 parent 9a55dda commit 230ae1b
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 76 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,5 @@ composer.phar
vendor
node_modules/
.idea/
.phpunit.cache
.phpunit.cache
./test.ts
169 changes: 128 additions & 41 deletions Service/GeneratorService.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ public function generate(GeneratorConfig $config): array
private function getTypescriptUtilityFunctions(): array
{
return [
'const rRP = (rawRoute: string, routeParams: Record<string, string>): string => {Object.entries(routeParams).forEach(([key, value]) => rawRoute = rawRoute.replace(`{${key}}`, value)); return rawRoute;}',
'const aQP = (route: string, queryParams?: Record<string, string>): string => queryParams ? route + "?" + new URLSearchParams(queryParams).toString() : route;',
'const replaceRouteParams = (rawRoute: string, routeParams: Record<string, string|number>): string => {Object.entries(routeParams).forEach(([key, value]) => rawRoute = rawRoute.replace(`{${key}}`, value as string)); return rawRoute;}',
'const appendQueryParams = (route: string, queryParams?: Record<string, string|number>): string => queryParams ? route + "?" + new URLSearchParams(queryParams as Record<string, string>).toString() : route;',
];
}

Expand All @@ -52,7 +52,7 @@ private function buildFunctionForRoute(GeneratorConfig $config, string $routeNam
if ($config->isGenerateRelativeUrls()) {
$buffer = array_merge($buffer, [
'relative: (',
$this->createRouteParamFunctionArgument($relativeRouteVariables),
$this->createRouteParamFunctionArgument($route, $relativeRouteVariables),
$this->createQueryParamFunctionArgument(),
') => string, ',
]);
Expand All @@ -61,7 +61,7 @@ private function buildFunctionForRoute(GeneratorConfig $config, string $routeNam
if ($config->isGenerateAbsoluteUrls()) {
$buffer = array_merge($buffer, [
'absolute: (',
$this->createRouteParamFunctionArgument($absoluteRouteVariables),
$this->createRouteParamFunctionArgument($route, $absoluteRouteVariables),
$this->createQueryParamFunctionArgument(),
') => string',
]);
Expand All @@ -75,7 +75,7 @@ private function buildFunctionForRoute(GeneratorConfig $config, string $routeNam
if ($config->isGenerateRelativeUrls()) {
$buffer = array_merge($buffer, [
'relative: (',
$this->createRouteParamFunctionArgument($relativeRouteVariables),
$this->createRouteParamFunctionArgument($route, $relativeRouteVariables),
$this->createQueryParamFunctionArgument(),
'): string => ' . $this->createFunctionCallForRelativePath($route, $relativeRouteVariables) . ', ',
]);
Expand All @@ -84,7 +84,7 @@ private function buildFunctionForRoute(GeneratorConfig $config, string $routeNam
if ($config->isGenerateAbsoluteUrls()) {
$buffer = array_merge($buffer, [
'absolute: (',
$this->createRouteParamFunctionArgument($absoluteRouteVariables),
$this->createRouteParamFunctionArgument($route, $absoluteRouteVariables),
$this->createQueryParamFunctionArgument(),
'): string => ' . $this->createFunctionCallForAbsolutePath($route, $absoluteRouteVariables),
]);
Expand Down Expand Up @@ -124,7 +124,19 @@ private function retrieveVariablesFromRoutePath(Route $route): array

private function retrieveVariablesFromAbsoluteRoutePath(Route $route): array
{
$url = \sprintf('%s%s', $route->getHost(), $route->getPath());
$availableSchemes = $route->getSchemes();

$usedScheme = '{scheme}';

if (\in_array('http', $availableSchemes, true)) {
$usedScheme = 'http';
}

if (\in_array('https', $availableSchemes, true)) {
$usedScheme = 'https';
}

$url = \sprintf('%s://%s%s', $usedScheme, $route->getHost(), $route->getPath());

$matches = [];

Expand All @@ -140,12 +152,7 @@ private function retrieveVariablesFromAbsoluteRoutePath(Route $route): array
return [];
}

$buffer = [];
foreach ($matches as $match) {
$buffer[] = $match[1];
}

return $buffer;
return array_map(static fn (array $match) => $match[1], $matches);
}

private function sanitizeRouteFunctionName(string $routeName): string
Expand All @@ -159,15 +166,19 @@ private function sanitizeRouteFunctionName(string $routeName): string
return 'path_' . preg_replace('/\W/', '_', $routeName);
}

private function createRouteParamFunctionArgument(array $variables): string
private function createRouteParamFunctionArgument(Route $route, array $variables): string
{
if (!$variables) {
return '';
}

return 'routeParams: {' . \implode(', ', array_map(
static function (string $variable) {
return $variable . ': string';
function (string $variable) use ($route) {
if (array_key_exists($variable, $route->getDefaults())) {
return sprintf('%s?:%s', $variable, $this->guessTypeOfPathVariable($route, $variable));
}

return sprintf('%s:%s', $variable, $this->guessTypeOfPathVariable($route, $variable));
},
$variables
)) . '}, ';
Expand All @@ -182,17 +193,17 @@ private function createFunctionCallForRelativePath(Route $route, array $variable
{
if ($variables) {
return \implode('', [
'aQP(',
"rRP('",
'appendQueryParams(',
"replaceRouteParams('",
$route->getPath(),
"', routeParams",
sprintf("', %s", $this->createRouteParamsMergeExpressionForDefaults($route)),
'), queryParams',
')',
]);
}

return \implode('', [
"aQP('",
"appendQueryParams('",
$route->getPath(),
"', queryParams",
')',
Expand All @@ -201,43 +212,119 @@ private function createFunctionCallForRelativePath(Route $route, array $variable

private function createFunctionCallForAbsolutePath(Route $route, array $variables): string
{
$availableSchemes = $route->getSchemes();

$usedScheme = null;

if (\in_array('http', $availableSchemes, true)) {
$usedScheme = 'http';
}

if (\in_array('https', $availableSchemes, true)) {
$usedScheme = 'https';
}

if ($usedScheme === null) {
throw new \InvalidArgumentException('Route must have https or http as scheme.');
}

$absolutePath = $usedScheme . '://' . $route->getHost() . $route->getPath();
$absolutePath = sprintf('%s://%s%s', $this->retrieveSchemeFromRoute($route), $route->getHost(), $route->getPath());

if ($variables) {
return \implode('', [
'aQP(',
"rRP('",
'appendQueryParams(',
"replaceRouteParams('",
$absolutePath,
"', routeParams",
sprintf("', %s", $this->createRouteParamsMergeExpressionForDefaults($route)),
'), queryParams',
')',
]);
}

return \implode('', [
"aQP('",
"appendQueryParams('",
$absolutePath,
"', queryParams",
')',
]);
}

private function guessTypeOfPathVariable(Route $route, string $variable): string
{
$requirement = $route->getRequirement($variable);

if ($requirement === null) {
return 'string';
}

if ($this->isDigitRegex($requirement)) {
return 'number';
}

if ($this->isEitherAOrBRegex($requirement)) {
return $this->deriveEitherAOrBRegexExpressionForTypescript($requirement);
}

return 'string';
}

private function isDigitRegex(string $requirement): bool
{
return $requirement === '\d+';
}

private function isEitherAOrBRegex(string $requirement): bool
{
$matches = [];

preg_match_all($this->getEitherAOrBRegexGuessRegex(), $requirement, $matches, PREG_SET_ORDER);

return count($matches) > 0;
}

private function createRouteParamsMergeExpressionForDefaults(Route $route): string
{
if (!$route->getDefaults()) {
return 'routeParams';
}

return sprintf('{...%s, ...routeParams}', json_encode($route->getDefaults(), JSON_THROW_ON_ERROR));
}

private function deriveEitherAOrBRegexExpressionForTypescript(string $requirement): string
{
$matches = [];

preg_match_all($this->getEitherAOrBRegexGuessRegex(), $requirement, $matches, PREG_SET_ORDER);

if (!$matches) {
throw new \LogicException('At this point the either A Or B regex must have matches');
}

$matchingGroup = $matches[0] ?? [];

if (!$matchingGroup) {
throw new \InvalidArgumentException('At this point the MatchingGroup must exist');
}

$buffer = [];
foreach ($matchingGroup as $key => $match) {
if ($key === 0) {
continue;
}

$buffer[] = $match;
}

return implode('|', array_map(static fn (string $matchFromBuffer) => sprintf("'%s'", $matchFromBuffer), $buffer));
}

private function getEitherAOrBRegexGuessRegex(): string
{
return '/([a-zA-Z]+)(?>\|)([a-zA-Z]+)/m';
}

private function retrieveSchemeFromRoute(Route $route): string
{
$availableSchemes = $route->getSchemes();

$usedScheme = '{scheme}';

if (\in_array('http', $availableSchemes, true)) {
$usedScheme = 'http';
}

if (\in_array('https', $availableSchemes, true)) {
$usedScheme = 'https';
}

return $usedScheme;
}

private function assertValidConfiguration(GeneratorConfig $config): void
{
if (!$config->isGenerateAbsoluteUrls() && !$config->isGenerateRelativeUrls()) {
Expand Down
39 changes: 25 additions & 14 deletions Tests/GenerateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,36 @@ class GenerateTest extends TestCase
{
use ProphecyTrait;

private const UPDATE_OUTPUT_FILES = true;
private const UPDATE_OUTPUT_FILES = false;

public function generationServiceDataProvider(): \Generator
{
$routeCollection = new RouteCollection();
$routeCollection->add('user_route', new Route('/user/{id}/notes/{noteId}', host: 'app.development.org', schemes: 'https'));
$routeCollection->add('user_route_http', new Route('/user/{id}/notes/{noteId}', host: 'app.development.org', schemes: 'http'));
$routeCollection->add('users_route', new Route('/users', host: 'app.development.org', schemes: 'https'));
$routeCollection->add('user_route_without_scheme', new Route('/user/{id}/notes/{noteId}', host: 'app.development.org'));
$routeCollection->add('users_route_without_requirements_and_defaults', new Route('/users', host: 'app.development.org', schemes: 'https'));
$routeCollection->add('users_route_with_requirements', new Route(
path: '/users/{id}/{locale}',
requirements: [
'id' => '\d+',
'locale' => 'en|fr',
],
host: 'app.development.org',
schemes: 'https'
));
$routeCollection->add('users_route_with_requirements_and_defaults', new Route(
path: '/users/{id}/{locale}',
defaults: [
'locale' => 'en',
],
requirements: [
'id' => '\d+',
'locale' => 'en|fr',
],
host: 'app.development.org',
schemes: 'https'
));

yield ['output.ts', $routeCollection, GeneratorConfig::generateEverything()];
yield ['output_relative.ts', $routeCollection, GeneratorConfig::generateOnlyRelativeUrls()];
Expand All @@ -38,7 +60,7 @@ public function testGenerationService(string $outputFileName, RouteCollection $c
$service = new GeneratorService($this->getMockedRouter($collection));
$result = implode("\n", $service->generate($config));

if (true) {
if (self::UPDATE_OUTPUT_FILES) {
\file_put_contents($file, $result);
}

Expand All @@ -48,17 +70,6 @@ public function testGenerationService(string $outputFileName, RouteCollection $c
);
}

public function testGenerationServiceWithAInvalidRouteForAbsoluteUrlGeneration(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectDeprecationMessage('Route must have https or http as scheme.');

$routeCollection = new RouteCollection();
$routeCollection->add('user_route', new Route('/user/{id}/notes/{noteId}', host: 'app.development.org'));

(new GeneratorService($this->getMockedRouter($routeCollection)))->generate(GeneratorConfig::generateOnlyAbsoluteUrls());
}

public function testGenerationServiceWithAInvalidRouteForRelativeUrlGeneration(): void
{
$routeCollection = new RouteCollection();
Expand Down
22 changes: 20 additions & 2 deletions Tests/output.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {path_user_route} from "./output";
import {path_user_route, path_users_route_with_requirements_and_defaults} from "./output";

test('test path_user_route relative route', () => {
const result1 = path_user_route().relative({id: "exampleID", noteId: "exampleNoteID"})
Expand All @@ -14,4 +14,22 @@ test('test path_user_route absolute route', () => {

const result2 = path_user_route().absolute({id: "exampleID", noteId: "exampleNoteID"}, {count: "20", page: "3"})
expect(result2).toBe('https://app.development.org/user/exampleID/notes/exampleNoteID?count=20&page=3');
});
});

test('test path_users_route_with_requirements_and_defaults - to assure that defaults and requirements are properly understood', () => {
// Test if the default for locale takes effect
const result1 = path_users_route_with_requirements_and_defaults().relative({id: 1})
expect(result1).toBe('/users/1/en');

// Test if the default for locale does not take effect if an argument is for the param is given
const result2 = path_users_route_with_requirements_and_defaults().relative({id: 1, locale: "fr"})
expect(result2).toBe('/users/1/fr');

// Test if the default for locale takes effect
const result3 = path_users_route_with_requirements_and_defaults().absolute({id: 1})
expect(result3).toBe('https://app.development.org/users/1/en');

// Test if the default for locale does not take effect if an argument is for the param is given
const result4 = path_users_route_with_requirements_and_defaults().absolute({id: 1, locale: "fr"})
expect(result4).toBe('https://app.development.org/users/1/fr');
})
Loading

0 comments on commit 230ae1b

Please sign in to comment.