Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Adding examples to the OAS3 Media Type Object (Request / Response) #855

Open
wants to merge 2 commits into
base: v4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions camel/Extraction/Example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Knuckles\Camel\Extraction;


use Knuckles\Camel\BaseDTO;

class Example extends BaseDTO
{
public ?string $type;

public array $meta;

public ?string $content;

public ?string $description;

public function __construct(array $parameters = [])
{
if (is_array($parameters['type'] ?? null)) {
$parameters['type'] = $parameters['type'];
}

$parameters['meta'] = $parameters['meta'] ?? [];

if (is_array($parameters['content'] ?? null)) {
$parameters['content'] = json_encode($parameters['content'], JSON_UNESCAPED_SLASHES);
}

parent::__construct($parameters);
}

public function fullDescription()
{
return $this->description;
}
}
13 changes: 13 additions & 0 deletions camel/Extraction/ExampleCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Knuckles\Camel\Extraction;

use Knuckles\Camel\BaseDTOCollection;

/**
* @extends BaseDTOCollection<Response>
*/
class ExampleCollection extends BaseDTOCollection
{
public static string $base = Example::class;
}
3 changes: 3 additions & 0 deletions camel/Extraction/ExtractedEndpointData.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ class ExtractedEndpointData extends BaseDTO
*/
public array $responseFields = [];

public ExampleCollection $examples;

/**
* Authentication info for this endpoint. In the form [{where}, {name}, {sample}]
* Example: ["queryParameters", "api_key", "njiuyiw97865rfyvgfvb1"]
Expand All @@ -84,6 +86,7 @@ public function __construct(array $parameters = [])
{
$parameters['metadata'] = $parameters['metadata'] ?? new Metadata([]);
$parameters['responses'] = $parameters['responses'] ?? new ResponseCollection([]);
$parameters['examples'] = $parameters['examples'] ?? new ExampleCollection([]);

parent::__construct($parameters);

Expand Down
4 changes: 4 additions & 0 deletions camel/Output/OutputEndpointData.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Knuckles\Camel\BaseDTO;
use Knuckles\Camel\Extraction\ExampleCollection;
use Knuckles\Camel\Extraction\Metadata;
use Knuckles\Camel\Extraction\ResponseCollection;
use Knuckles\Camel\Extraction\ResponseField;
Expand Down Expand Up @@ -77,6 +78,8 @@ class OutputEndpointData extends BaseDTO
*/
public array $responseFields = [];

public ExampleCollection $examples;

/**
* The same as bodyParameters, but organized in a hierarchy.
* So, top-level items first, with a __fields property containing their children, and so on.
Expand All @@ -100,6 +103,7 @@ public function __construct(array $parameters = [])
$parameters['queryParameters'] = array_map(fn($param) => new Parameter($param), $parameters['queryParameters'] ?? []);
$parameters['urlParameters'] = array_map(fn($param) => new Parameter($param), $parameters['urlParameters'] ?? []);
$parameters['responseFields'] = array_map(fn($param) => new ResponseField($param), $parameters['responseFields'] ?? []);
$parameters['examples'] = new ExampleCollection($parameters['examples'] ?? []);

parent::__construct($parameters);

Expand Down
3 changes: 3 additions & 0 deletions config/scribe.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,9 @@
Strategies\ResponseFields\GetFromResponseFieldAttribute::class,
Strategies\ResponseFields\GetFromResponseFieldTag::class,
],
'examples' => [
Strategies\Examples\UseExampleTag::class,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I feel like attributes are a more convenient way to specify multiple examples, so let's add support for that too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shalvah let me know how you would like me to update the docs, should it be a new section under Eg: Documenting your API

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say you can probably add it under the existing docs for requests and responses.

],
],

// For response calls, API resource responses and transformer responses,
Expand Down
6 changes: 6 additions & 0 deletions src/Config/Defaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,10 @@ public static function responseFieldsStrategies(): StrategyListWrapper
]);
}

public static function examplesStrategies(): StrategyListWrapper
{
return new StrategyListWrapper([
Strategies\Examples\UseExampleTag::class,
]);
}
}
17 changes: 17 additions & 0 deletions src/Exceptions/ExampleResponseStatusCodeNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Knuckles\Scribe\Exceptions;

class ExampleResponseStatusCodeNotFound extends \RuntimeException implements ScribeException
{
public static function forTag(string $tag)
{
return new self(
<<<MESSAGE
You specified the response example "$tag" field in one of your custom endpoints, but we couldn't find the required status code.
Did you forgot to define the response status code?
MESSAGE

);
}
}
17 changes: 17 additions & 0 deletions src/Exceptions/ExampleTypeNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Knuckles\Scribe\Exceptions;

class ExampleTypeNotFound extends \RuntimeException implements ScribeException
{
public static function forTag(string $tag)
{
return new self(
<<<MESSAGE
You specified the example "$tag" field in one of your custom endpoints, but we couldn't find the required type.
Did you forgot to define the response type?
MESSAGE

);
}
}
14 changes: 14 additions & 0 deletions src/Extracting/Extractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use Illuminate\Routing\Route;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Knuckles\Camel\Extraction\ExampleCollection;
use Knuckles\Camel\Extraction\RequestCollection;
use Knuckles\Camel\Extraction\ResponseCollection;
use Knuckles\Camel\Extraction\ResponseField;
use Knuckles\Camel\Output\OutputEndpointData;
Expand Down Expand Up @@ -98,6 +100,9 @@ public function processRoute(Route $route, array $routeRules = []): ExtractedEnd
$this->fetchResponseFields($endpointData, $routeRules);
$this->mergeInheritedMethodsData('responseFields', $endpointData, $inheritedDocsOverrides);

$this->fetchExampleFields($endpointData, $routeRules);
$this->mergeInheritedMethodsData('examples', $endpointData, $inheritedDocsOverrides);

self::$routeBeingProcessed = null;

return $endpointData;
Expand Down Expand Up @@ -188,6 +193,15 @@ protected function fetchRequestHeaders(ExtractedEndpointData $endpointData, arra
});
}

protected function fetchExampleFields(ExtractedEndpointData $endpointData, array $rulesToApply): void
{
$this->iterateThroughStrategies('examples', $endpointData, $rulesToApply, function ($results) use ($endpointData) {
$endpointData->examples->concat($results);
});

$endpointData->examples = new ExampleCollection($endpointData->examples->values());
}

/**
* Iterate through all defined strategies for this stage.
* A strategy may return an array of attributes
Expand Down
81 changes: 81 additions & 0 deletions src/Extracting/Strategies/Examples/UseExampleTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Knuckles\Scribe\Extracting\Strategies\Examples;

use Knuckles\Camel\Extraction\ExtractedEndpointData;
use Knuckles\Scribe\Exceptions\ExampleResponseStatusCodeNotFound;
use Knuckles\Scribe\Exceptions\ExampleTypeNotFound;
use Knuckles\Scribe\Extracting\RouteDocBlocker;
use Knuckles\Scribe\Extracting\Shared\ResponseFileTools;
use Knuckles\Scribe\Extracting\Strategies\Strategy;
use Knuckles\Scribe\Tools\AnnotationParser as a;
use Knuckles\Scribe\Tools\Utils;
use Mpociot\Reflection\DocBlock\Tag;

/**
* Get an example from the docblock ( @example ).
*/
class UseExampleTag extends Strategy
{
const TYPES = [
'request',
'response',
];

public function __invoke(ExtractedEndpointData $endpointData, array $routeRules = []): ?array
{
$docBlocks = RouteDocBlocker::getDocBlocksFromRoute($endpointData->route);
return $this->getDocBlockExamples($docBlocks['method']->getTags());
}

/**
* @param Tag[] $tags
*/
public function getDocBlockExamples(array $tags): ?array
{
$exampleTags = Utils::filterDocBlockTags($tags, 'example');

if (empty($exampleTags)) return null;

$examples = array_map(function (Tag $exampleTag) {
$content = $exampleTag->getContent();

['fields' => $fields, 'content' => $content] = a::parseIntoContentAndFields($content, ['type', 'scenario', 'file']);

$description = $fields['scenario'] ?: "";
$type = $fields['type'] ?: "";
$file = $fields['file'] ?: "";
$meta = [];

if (empty($type) || ! in_array($type, self::TYPES)) {
// Type is required
throw new ExampleTypeNotFound();
}

if ($type === 'response') {
// Extract the status code
preg_match('/status=(\d+)/', $content, $statusMatches);

if (isset($statusMatches[1])) {
$meta['status'] = (int) $statusMatches[1];
} else {
// Status code is required for type response
throw new ExampleResponseStatusCodeNotFound();
}

// Remove the status code
$content = preg_replace('/status=(\d+)/', '', $content);
}

if (! empty($file)) {
$json = json_decode($content, true) ?? null;

$content = ResponseFileTools::getResponseContents($file, $json);
}

return ['type' => $type, 'meta' => $meta, 'content' => $content, 'description' => $description];
}, $exampleTags);

return $examples;
}
}
Loading
Loading