Skip to content

Commit

Permalink
Implement basic subgroup support
Browse files Browse the repository at this point in the history
  • Loading branch information
shalvah committed Jul 8, 2022
1 parent 5ae4c14 commit 7cf0773
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 58 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Removed

# 4.0.0
### Added
- Support for specifying groups and endpoints order in config file ([29ddcfc](https://github.com/knuckleswtf/scribe/commit/29ddcfcf284a06da0ae6cb399d09ee5cf1f9ffa7)))
- Support for specifying example model sources ([39ff208](https://github.com/knuckleswtf/scribe/commit/39ff208085d68eed4c459768ac5a1120934f021a)))
- Support for subgroups ([39ff208](https://github.com/knuckleswtf/scribe/commit/39ff208085d68eed4c459768ac5a1120934f021a)))

## 3.33.0 (27 June 2022)
### Added
- Include description in Postman collection for formdata body parameters ([10faa500](https://github.com/knuckleswtf/scribe/commit/10faa500e36e02d4efcecf8ad5e1d91ba1c7728d)))
Expand Down
21 changes: 17 additions & 4 deletions camel/Camel.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,23 @@ public static function groupEndpoints(array $endpoints, array $endpointGroupInde
if (empty($endpointGroupIndexes)) {
$groupName = data_get($endpointsInGroup[0], 'metadata.groupName');
if ($defaultGroupsOrder && isset($defaultGroupsOrder[$groupName])) {
$endpointsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder[$groupName]);
$subGroupOrEndpointsOrder = Utils::getTopLevelItemsFromMixedConfigList($defaultGroupsOrder[$groupName]);
$sortedEndpoints = $endpointsInGroup->sortBy(
function (ExtractedEndpointData $e) use ($endpointsOrder) {
$index = array_search($e->httpMethods[0].' '.$e->uri, $endpointsOrder);
function (ExtractedEndpointData $e) use ($defaultGroupsOrder, $subGroupOrEndpointsOrder) {
$endpointIdentifier = $e->httpMethods[0].' /'.$e->uri;
$index = array_search($e->metadata->subgroup, $subGroupOrEndpointsOrder);

if ($index !== false) {
// This is a subgroup
$endpointsOrderInSubgroup = $defaultGroupsOrder[$e->metadata->groupName][$e->metadata->subgroup] ?? null;
if ($endpointsOrderInSubgroup) {
$indexInSubGroup = array_search($endpointIdentifier, $endpointsOrderInSubgroup);
$index = ($indexInSubGroup === false) ? $index : ($index + ($indexInSubGroup * 0.1));
}
} else {
// This is an endpoint
$index = array_search($endpointIdentifier, $subGroupOrEndpointsOrder);
}
return $index === false ? INF : $index;
},
);
Expand Down Expand Up @@ -238,4 +251,4 @@ public static function getOrderListComparator(array $order): \Closure
return strnatcmp($a, $b);
};
}
}
}
3 changes: 2 additions & 1 deletion camel/Extraction/ExtractedEndpointData.php
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,12 @@ public function normalizeResourceParamName(string $uri, Route $route): string
public function forSerialisation()
{
$copy = $this->except(
// Get rid of all duplicate data
// Get rid of all duplicate data
'cleanQueryParameters', 'cleanUrlParameters', 'fileParameters', 'cleanBodyParameters',
// and objects used only in extraction
'route', 'controller', 'method', 'auth',
);
// Remove these, since they're on the parent group object
$copy->metadata = $copy->metadata->except('groupName', 'groupDescription', 'beforeGroup', 'afterGroup');

return $copy;
Expand Down
1 change: 1 addition & 0 deletions camel/Extraction/Metadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
class Metadata extends BaseDTO
{
public ?string $groupName;
public ?string $subgroup;

/**
* Name of the group that this group should be placed just before.
Expand Down
18 changes: 12 additions & 6 deletions config/scribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,17 +314,23 @@

/*
* By default, Scribe will sort groups alphabetically, and endpoints in the order their routes are defined.
* You can customise that by listing the groups and endpoints here in the order you want them.
* You can customise that by listing the groups, subgroups and endpoints here in the order you want them.
*
* Any groups or endpoints you don't list here will be added as usual after the ones here.
* If an endpoint is listed under a group it doesn't belong in, it will be ignored.
* Note: omit the initial '/' when writing an endpoint.
* Any groups, subgroups or endpoints you don't list here will be added as usual after the ones here.
* If an endpoint/subgroups is listed under a group it doesn't belong in, it will be ignored.
* Note: you must include the initial '/' when writing an endpoint.
*/
'order' => [
// 'This group comes first',
// 'This group comes next' => [
// 'POST this-endpoint-comes-first',
// 'GET this-endpoint-comes-next',
// 'POST /this-endpoint-comes-first',
// 'GET /this-endpoint-comes-next',
// ],
// 'This group comes third' => [
// 'This subgroup comes first' => [
// 'GET /this-other-endpoint-comes-first',
// 'GET /this-other-endpoint-comes-next',
// ]
// ]
],
],
Expand Down
4 changes: 3 additions & 1 deletion resources/example_custom_endpoint.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
# you can define it in a custom.*.yaml file, like this one.
# Each custom file should contain an array of endpoints. Here's an example:
# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options

#- httpMethods:
# - POST
# uri: api/doSomething/{param}
# metadata:
# groupName: The group the endpoint belongs to. Can be a new group or an existing group.
# groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
# subgroup: You can add a subgroup, too.
# title: Do something
# description: 'This endpoint allows you to do something.'
# authenticated: false
Expand Down Expand Up @@ -48,4 +50,4 @@
# hey:
# name: hey
# description: Who knows?
# type: string # This is optional
# type: string # This is optional
7 changes: 5 additions & 2 deletions src/Commands/Upgrade.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ public function handle(): void
{
$oldConfig = config('scribe');
$upgrader = Upgrader::ofConfigFile('config/scribe.php', __DIR__ . '/../../config/scribe.php')
->dontTouch('routes', 'laravel.middleware', 'postman.overrides', 'openapi.overrides')
->move('interactive', 'try_it_out.enabled');
->dontTouch('routes', 'laravel.middleware', 'postman.overrides', 'openapi.overrides',
'example_languages', 'database_connections_to_transact', 'strategies')
->move('interactive', 'try_it_out.enabled')
->move('default_group', 'groups.default')
->move('faker_seed', 'examples.faker_seed');

$changes = $upgrader->dryRun();
if (empty($changes)) {
Expand Down
81 changes: 47 additions & 34 deletions src/Extracting/Strategies/Metadata/GetFromDocBlocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ public function __invoke(ExtractedEndpointData $endpointData, array $routeRules)

public function getMetadataFromDocBlock(DocBlock $methodDocBlock, DocBlock $classDocBlock): array
{
[$routeGroupName, $routeGroupDescription, $routeTitle] = $this->getRouteGroupDescriptionAndTitle($methodDocBlock, $classDocBlock);
[$routeGroupName, $routeGroupDescription, $routeTitle] = $this->getEndpointGroupDetails($methodDocBlock, $classDocBlock);

return [
'groupName' => $routeGroupName,
'groupDescription' => $routeGroupDescription,
'subgroup' => $this->getEndpointSubGroup($methodDocBlock, $classDocBlock),
'title' => $routeTitle ?: $methodDocBlock->getShortDescription(),
'description' => $methodDocBlock->getLongDescription()->getContents(),
'authenticated' => $this->getAuthStatusFromDocBlock($methodDocBlock, $classDocBlock),
Expand All @@ -49,46 +50,41 @@ protected function getAuthStatusFromDocBlock(DocBlock $methodDocBlock, DocBlock
}

/**
* @param DocBlock $methodDocBlock
* @param DocBlock $controllerDocBlock
*
* @return array The route group name, the group description, and the route title
*/
protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
protected function getEndpointGroupDetails(DocBlock $methodDocBlock, DocBlock $controllerDocBlock)
{
// @group tag on the method overrides that on the controller
if (!empty($methodDocBlock->getTags())) {
foreach ($methodDocBlock->getTags() as $tag) {
if ($tag->getName() === 'group') {
$routeGroupParts = explode("\n", trim($tag->getContent()));
$routeGroupName = array_shift($routeGroupParts);
$routeGroupDescription = trim(implode("\n", $routeGroupParts));

// If the route has no title (the methodDocBlock's "short description"),
// we'll assume the routeGroupDescription is actually the title
// Something like this:
// /**
// * Fetch cars. <-- This is route title.
// * @group Cars <-- This is group name.
// * APIs for cars. <-- This is group description (not required).
// **/
// VS
// /**
// * @group Cars <-- This is group name.
// * Fetch cars. <-- This is route title, NOT group description.
// **/

// BTW, this is a spaghetti way of doing this.
// It shall be refactored soon. Deus vult!💪
if (empty($methodDocBlock->getShortDescription())) {
return [$routeGroupName, '', $routeGroupDescription];
}

return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
foreach ($methodDocBlock->getTags() as $tag) {
if ($tag->getName() === 'group') {
$routeGroupParts = explode("\n", trim($tag->getContent()));
$routeGroupName = array_shift($routeGroupParts);
$routeGroupDescription = trim(implode("\n", $routeGroupParts));

// If the route has no title (the methodDocBlock's "short description"),
// we'll assume the routeGroupDescription is actually the title
// Something like this:
// /**
// * Fetch cars. <-- This is route title.
// * @group Cars <-- This is group name.
// * APIs for cars. <-- This is group description (not required).
// **/
// VS
// /**
// * @group Cars <-- This is group name.
// * Fetch cars. <-- This is route title, NOT group description.
// **/

// BTW, this is a spaghetti way of doing this.
// It shall be refactored soon. Deus vult!💪
if (empty($methodDocBlock->getShortDescription())) {
return [$routeGroupName, '', $routeGroupDescription];
}

return [$routeGroupName, $routeGroupDescription, $methodDocBlock->getShortDescription()];
}
}

// Fall back to the controller
foreach ($controllerDocBlock->getTags() as $tag) {
if ($tag->getName() === 'group') {
$routeGroupParts = explode("\n", trim($tag->getContent()));
Expand All @@ -101,4 +97,21 @@ protected function getRouteGroupDescriptionAndTitle(DocBlock $methodDocBlock, Do

return [$this->config->get('groups.default'), '', $methodDocBlock->getShortDescription()];
}

protected function getEndpointSubGroup(DocBlock $methodDocBlock, DocBlock $controllerDocBlock): ?string
{
foreach ($methodDocBlock->getTags() as $tag) {
if ($tag->getName() === 'subgroup') {
return trim($tag->getContent());
}
}

foreach ($controllerDocBlock->getTags() as $tag) {
if ($tag->getName() === 'subgroup') {
return trim($tag->getContent());
}
}

return null;
}
}
39 changes: 39 additions & 0 deletions tests/Fixtures/TestGroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,43 @@ public function action2()
public function action10()
{
}

/**
* @group 13. Group 13
* @subgroup SG B
*/
public function action13a()
{
}

/**
* @group 13. Group 13
* @subgroup SG C
*/
public function action13b()
{
}

/**
* @group 13. Group 13
*/
public function action13c()
{
}

/**
* @group 13. Group 13
* @subgroup SG B
*/
public function action13d()
{
}

/**
* @group 13. Group 13
* @subgroup SG A
*/
public function action13e()
{
}
}
46 changes: 36 additions & 10 deletions tests/GenerateDocumentation/OutputTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,31 +191,57 @@ public function sorts_group_naturally()
/** @test */
public function sorts_groups_and_endpoints_in_the_specified_order()
{
$order = [
config(['scribe.groups.order' => [
'10. Group 10',
'1. Group 1' => [
'GET api/action1b',
'GET api/action1',
'GET /api/action1b',
'GET /api/action1',
],
];
config(['scribe.groups.order' => $order]);
'13. Group 13' => [
'SG B' => [
'POST /api/action13d',
'GET /api/action13a',
],
'SG A',
'PUT /api/action13c',
'POST /api/action13b',
],
]]);

RouteFacade::get('/api/action1', TestGroupController::class . '@action1');
RouteFacade::get('/api/action1b', TestGroupController::class . '@action1b');
RouteFacade::get('/api/action2', TestGroupController::class . '@action2');
RouteFacade::get('/api/action10', TestGroupController::class . '@action10');
RouteFacade::get('/api/action1', [TestGroupController::class, 'action1']);
RouteFacade::get('/api/action1b', [TestGroupController::class, 'action1b']);
RouteFacade::get('/api/action2', [TestGroupController::class, 'action2']);
RouteFacade::get('/api/action10', [TestGroupController::class, 'action10']);
RouteFacade::get('/api/action13a', [TestGroupController::class, 'action13a']);
RouteFacade::post('/api/action13b', [TestGroupController::class, 'action13b']);
RouteFacade::put('/api/action13c', [TestGroupController::class, 'action13c']);
RouteFacade::post('/api/action13d', [TestGroupController::class, 'action13d']);
RouteFacade::get('/api/action13e', [TestGroupController::class, 'action13e']);

$this->generate();

$this->assertEquals('10. Group 10', Yaml::parseFile('.scribe/endpoints/00.yaml')['name']);
$secondGroup = Yaml::parseFile('.scribe/endpoints/01.yaml');
$this->assertEquals('1. Group 1', $secondGroup['name']);
$this->assertEquals('2. Group 2', Yaml::parseFile('.scribe/endpoints/02.yaml')['name']);
$thirdGroup = Yaml::parseFile('.scribe/endpoints/02.yaml');
$this->assertEquals('13. Group 13', $thirdGroup['name']);
$this->assertEquals('2. Group 2', Yaml::parseFile('.scribe/endpoints/03.yaml')['name']);

$this->assertEquals('api/action1b', $secondGroup['endpoints'][0]['uri']);
$this->assertEquals('GET', $secondGroup['endpoints'][0]['httpMethods'][0]);
$this->assertEquals('api/action1', $secondGroup['endpoints'][1]['uri']);
$this->assertEquals('GET', $secondGroup['endpoints'][1]['httpMethods'][0]);

$this->assertEquals('api/action13d', $thirdGroup['endpoints'][0]['uri']);
$this->assertEquals('POST', $thirdGroup['endpoints'][0]['httpMethods'][0]);
$this->assertEquals('api/action13a', $thirdGroup['endpoints'][1]['uri']);
$this->assertEquals('GET', $thirdGroup['endpoints'][1]['httpMethods'][0]);
$this->assertEquals('api/action13e', $thirdGroup['endpoints'][2]['uri']);
$this->assertEquals('GET', $thirdGroup['endpoints'][2]['httpMethods'][0]);
$this->assertEquals('api/action13c', $thirdGroup['endpoints'][3]['uri']);
$this->assertEquals('PUT', $thirdGroup['endpoints'][3]['httpMethods'][0]);
$this->assertEquals('api/action13b', $thirdGroup['endpoints'][4]['uri']);
$this->assertEquals('POST', $thirdGroup['endpoints'][4]['httpMethods'][0]);
}

/** @test */
Expand Down
Loading

0 comments on commit 7cf0773

Please sign in to comment.