123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- <?php
- namespace Dingo\Blueprint;
- use ReflectionClass;
- use RuntimeException;
- use Illuminate\Support\Str;
- use Illuminate\Support\Collection;
- use Illuminate\Filesystem\Filesystem;
- use Doctrine\Common\Annotations\AnnotationRegistry;
- use Doctrine\Common\Annotations\SimpleAnnotationReader;
- class Blueprint
- {
- /**
- * Simple annotation reader instance.
- *
- * @var \Doctrine\Common\Annotations\SimpleAnnotationReader
- */
- protected $reader;
- /**
- * Filesytsem instance.
- *
- * @var \Illuminate\Filesystem\Filesystem
- */
- protected $files;
- /**
- * Include path for documentation files.
- *
- * @var string
- */
- protected $includePath;
- /**
- * Create a new generator instance.
- *
- * @param \Doctrine\Common\Annotations\SimpleAnnotationReader $reader
- * @param \Illuminate\Filesystem\Filesystem $files
- *
- * @return void
- */
- public function __construct(SimpleAnnotationReader $reader, Filesystem $files)
- {
- $this->reader = $reader;
- $this->files = $files;
- $this->registerAnnotationLoader();
- }
- /**
- * Register the annotation loader.
- *
- * @return void
- */
- protected function registerAnnotationLoader()
- {
- $this->reader->addNamespace('Dingo\\Blueprint\\Annotation');
- $this->reader->addNamespace('Dingo\\Blueprint\\Annotation\\Method');
- AnnotationRegistry::registerLoader(function ($class) {
- $path = __DIR__.'/'.str_replace(['Dingo\\Blueprint\\', '\\'], ['', DIRECTORY_SEPARATOR], $class).'.php';
- if (file_exists($path)) {
- require_once $path;
- return true;
- }
- });
- }
- /**
- * Generate documentation with the name, version and optional overview content.
- *
- * @param \Illuminate\Support\Collection $controllers
- * @param string $name
- * @param string $version
- * @param string $includePath
- * @param string $overviewFile
- *
- * @return bool
- */
- public function generate(Collection $controllers, $name, $version, $includePath = null, $overviewFile = null)
- {
- $this->includePath = $includePath;
- $resources = $controllers->map(function ($controller) use ($version) {
- $controller = $controller instanceof ReflectionClass ? $controller : new ReflectionClass($controller);
- $actions = new Collection;
- // Spin through all the methods on the controller and compare the version
- // annotation (if supplied) with the version given for the generation.
- // We'll also build up an array of actions on each resource.
- foreach ($controller->getMethods() as $method) {
- if ($versionAnnotation = $this->reader->getMethodAnnotation($method, Annotation\Versions::class)) {
- if (! in_array($version, $versionAnnotation->value)) {
- continue;
- }
- }
- if ($annotations = $this->reader->getMethodAnnotations($method)) {
- if (! $actions->contains($method)) {
- $actions->push(new Action($method, new Collection($annotations)));
- }
- }
- }
- $annotations = new Collection($this->reader->getClassAnnotations($controller));
- return new RestResource($controller->getName(), $controller, $annotations, $actions);
- });
- $contents = $this->generateContentsFromResources($resources, $name, $overviewFile);
- $this->includePath = null;
- return $contents;
- }
- /**
- * Generate the documentation contents from the resources collection.
- *
- * @param \Illuminate\Support\Collection $resources
- * @param string $name
- * @param string $overviewFile
- *
- * @return string
- */
- protected function generateContentsFromResources(Collection $resources, $name, $overviewFile = null)
- {
- $contents = '';
- $contents .= $this->getFormat();
- $contents .= $this->line(2);
- $contents .= sprintf('# %s', $name);
- $contents .= $this->line(2);
- $contents .= $this->getOverview($overviewFile);
- $resources->each(function ($resource) use (&$contents) {
- if ($resource->getActions()->isEmpty()) {
- return;
- }
- $contents .= $resource->getDefinition();
- if ($description = $resource->getDescription()) {
- $contents .= $this->line();
- $contents .= $description;
- }
- if (($parameters = $resource->getParameters()) && ! $parameters->isEmpty()) {
- $this->appendParameters($contents, $parameters);
- }
- $resource->getActions()->each(function ($action) use (&$contents, $resource) {
- $contents .= $this->line(2);
- $contents .= $action->getDefinition();
- if ($description = $action->getDescription()) {
- $contents .= $this->line();
- $contents .= $description;
- }
- if (($attributes = $action->getAttributes()) && ! $attributes->isEmpty()) {
- $this->appendAttributes($contents, $attributes);
- }
- if (($parameters = $action->getParameters()) && ! $parameters->isEmpty()) {
- $this->appendParameters($contents, $parameters);
- }
- if ($request = $action->getRequest()) {
- $this->appendRequest($contents, $request, $resource);
- }
- if ($response = $action->getResponse()) {
- $this->appendResponse($contents, $response, $resource);
- }
- if ($transaction = $action->getTransaction()) {
- foreach ($transaction->value as $value) {
- if ($value instanceof Annotation\Request) {
- $this->appendRequest($contents, $value, $resource);
- } elseif ($value instanceof Annotation\Response) {
- $this->appendResponse($contents, $value, $resource);
- } else {
- throw new RuntimeException('Unsupported annotation type given in transaction.');
- }
- }
- }
- });
- $contents .= $this->line(2);
- });
- return stripslashes(trim($contents));
- }
- /**
- * Append the attributes subsection to a resource or action.
- *
- * @param string $contents
- * @param \Illuminate\Support\Collection $attributes
- * @param int $indent
- *
- * @return void
- */
- protected function appendAttributes(&$contents, Collection $attributes, $indent = 0)
- {
- $this->appendSection($contents, 'Attributes', $indent);
- $attributes->each(function ($attribute) use (&$contents, $indent) {
- $contents .= $this->line();
- $contents .= $this->tab(1 + $indent);
- $contents .= sprintf('+ %s', $attribute->identifier);
- if ($attribute->sample) {
- $contents .= sprintf(': %s', $attribute->sample);
- }
- $contents .= sprintf(
- ' (%s, %s) - %s',
- $attribute->type,
- $attribute->required ? 'required' : 'optional',
- $attribute->description
- );
- });
- }
- /**
- * Append the parameters subsection to a resource or action.
- *
- * @param string $contents
- * @param \Illuminate\Support\Collection $parameters
- *
- * @return void
- */
- protected function appendParameters(&$contents, Collection $parameters)
- {
- $this->appendSection($contents, 'Parameters');
- $parameters->each(function ($parameter) use (&$contents) {
- $contents .= $this->line();
- $contents .= $this->tab();
- $contents .= sprintf(
- '+ %s:%s (%s, %s) - %s',
- $parameter->identifier,
- $parameter->example ? " `{$parameter->example}`" : '',
- $parameter->members ? sprintf('enum[%s]', $parameter->type) : $parameter->type,
- $parameter->required ? 'required' : 'optional',
- $parameter->description
- );
- if (isset($parameter->default)) {
- $this->appendSection($contents, sprintf('Default: %s', $parameter->default), 2, 1);
- }
- if (isset($parameter->members)) {
- $this->appendSection($contents, 'Members', 2, 1);
- foreach ($parameter->members as $member) {
- $this->appendSection($contents, sprintf('`%s` - %s', $member->identifier, $member->description), 3, 1);
- }
- }
- });
- }
- /**
- * Append a response subsection to an action.
- *
- * @param string $contents
- * @param \Dingo\Blueprint\Annotation\Response $response
- * @param \Dingo\Blueprint\RestResource $resource
- *
- * @return void
- */
- protected function appendResponse(&$contents, Annotation\Response $response, RestResource $resource)
- {
- $this->appendSection($contents, sprintf('Response %s', $response->statusCode));
- if (isset($response->contentType)) {
- $contents .= ' ('.$response->contentType.')';
- }
- if (! empty($response->headers) || $resource->hasResponseHeaders()) {
- $this->appendHeaders($contents, array_merge($resource->getResponseHeaders(), $response->headers));
- }
- if (isset($response->attributes)) {
- $this->appendAttributes($contents, collect($response->attributes), 1);
- }
- if (isset($response->body)) {
- $this->appendBody($contents, $this->prepareBody($response->body, $response->contentType));
- }
- }
- /**
- * Append a request subsection to an action.
- *
- * @param string $contents
- * @param \Dingo\Blueprint\Annotation\Request $request
- * @param \Dingo\Blueprint\RestResource $resource
- *
- * @return void
- */
- protected function appendRequest(&$contents, $request, RestResource $resource)
- {
- $this->appendSection($contents, 'Request');
- if (isset($request->identifier)) {
- $contents .= ' '.$request->identifier;
- }
- $contents .= ' ('.$request->contentType.')';
- if (! empty($request->headers) || $resource->hasRequestHeaders()) {
- $this->appendHeaders($contents, array_merge($resource->getRequestHeaders(), $request->headers));
- }
- if (isset($request->attributes)) {
- $this->appendAttributes($contents, collect($request->attributes), 1);
- }
- if (isset($request->body)) {
- $this->appendBody($contents, $this->prepareBody($request->body, $request->contentType));
- }
- }
- /**
- * Append a body subsection to an action.
- *
- * @param string $contents
- * @param string $body
- *
- * @return void
- */
- protected function appendBody(&$contents, $body)
- {
- $this->appendSection($contents, 'Body', 1, 1);
- $contents .= $this->line(2);
- $line = strtok($body, "\r\n");
- while ($line !== false) {
- $contents .= $this->tab(3).$line;
- $line = strtok("\r\n");
- if ($line !== false) {
- $contents .= $this->line();
- }
- }
- }
- /**
- * Append a headers subsection to an action.
- *
- * @param string $contents
- * @param array $headers
- *
- * @return void
- */
- protected function appendHeaders(&$contents, array $headers)
- {
- $this->appendSection($contents, 'Headers', 1, 1);
- $contents .= $this->line();
- foreach ($headers as $header => $value) {
- $contents .= $this->line().$this->tab(3).sprintf('%s: %s', $header, $value);
- }
- }
- /**
- * Append a subsection to an action.
- *
- * @param string $contents
- * @param string $name
- * @param int $indent
- * @param int $lines
- *
- * @return void
- */
- protected function appendSection(&$contents, $name, $indent = 0, $lines = 2)
- {
- $contents .= $this->line($lines);
- $contents .= $this->tab($indent);
- $contents .= '+ '.$name;
- }
- /**
- * Prepare a body.
- *
- * @param string $body
- * @param string $contentType
- *
- * @return string
- */
- protected function prepareBody($body, $contentType)
- {
- if (is_string($body) && Str::startsWith($body, ['json', 'file'])) {
- list($type, $path) = explode(':', $body);
- if (! Str::endsWith($path, '.json') && $type == 'json') {
- $path .= '.json';
- }
- $body = $this->files->get($this->includePath.'/'.$path);
- json_decode($body);
- if (json_last_error() == JSON_ERROR_NONE) {
- return $body;
- }
- }
- if (strpos($contentType, 'application/json') === 0) {
- return json_encode($body, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
- }
- return $body;
- }
- /**
- * Create a new line character.
- *
- * @param int $repeat
- *
- * @return string
- */
- protected function line($repeat = 1)
- {
- return str_repeat("\n", $repeat);
- }
- /**
- * Create a tab character.
- *
- * @param int $repeat
- *
- * @return string
- */
- protected function tab($repeat = 1)
- {
- return str_repeat(' ', $repeat);
- }
- /**
- * Get the API Blueprint format.
- *
- * @return string
- */
- protected function getFormat()
- {
- return 'FORMAT: 1A';
- }
- /**
- * Get the overview file content to append.
- *
- * @param null $file
- * @return null|string
- */
- protected function getOverview($file = null)
- {
- if (null !== $file) {
- if (!file_exists($file)) {
- throw new RuntimeException('Overview file could not be found.');
- }
- $content = file_get_contents($file);
- if ($content === false) {
- throw new RuntimeException('Failed to read overview file contents.');
- }
- return $content.$this->line(2);
- }
- return null;
- }
- }
|