Skip to content

Instantly share code, notes, and snippets.

@askuri
Forked from martianatwork/README.md
Last active January 4, 2023 08:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save askuri/44464024498096f5c3316ad72429a292 to your computer and use it in GitHub Desktop.
Save askuri/44464024498096f5c3316ad72429a292 to your computer and use it in GitHub Desktop.

I'm sorry for the code quality, got this working in 1 night work.

anyways, this is currently working fine ignore error related to should be object or Could not resolve reference it will be fixed in future

requires cebe/php-openapi package composer req cebe/php-openapi

<?php
use Illuminate\Support\Facades\Route;
Route::get('v1/public/swagger', [\App\Http\Controllers\SwaggerController::class, 'index']);
<?php
namespace App\Http\Controllers;
use cebe\openapi\spec\Components;
use cebe\openapi\spec\Example;
use cebe\openapi\spec\MediaType;
use cebe\openapi\spec\Operation;
use cebe\openapi\spec\Parameter;
use cebe\openapi\spec\PathItem;
use cebe\openapi\spec\Reference;
use cebe\openapi\spec\RequestBody;
use cebe\openapi\spec\Response;
use cebe\openapi\spec\Responses;
use cebe\openapi\spec\Schema;
use cebe\openapi\spec\Server;
use cebe\openapi\spec\ServerVariable;
use cebe\openapi\spec\Type;
use Illuminate\Support\Facades\Route;
use LaravelJsonApi\Eloquent\Fields\Boolean;
use LaravelJsonApi\Eloquent\Fields\DateTime;
use LaravelJsonApi\Eloquent\Fields\ID;
use LaravelJsonApi\Eloquent\Fields\Number;
use LaravelJsonApi\Eloquent\Fields\Relations\Relation;
use LaravelJsonApi\Eloquent\Fields\Relations\ToOne;
use LaravelJsonApi\Eloquent\Fields\Str;
class SwaggerController extends Controller
{
public function index() {
$servers = config('jsonapi.servers');
// DEFAULT SCHEMA
$default_schema = [
'unauthorized' => new Schema([
'title' => "unauthorized_error",
"type" => Type::OBJECT,
"properties" => [
"errors" => new Schema([
'title' => "errors",
"type" => Type::OBJECT,
"properties" => [
"detail" => new Schema([
'title' => "detail",
"type" => Type::STRING,
"example" => 'Unauthenticated.'
]),
"status" => new Schema([
'title' => "status",
"type" => Type::STRING,
"example" => '401'
]),
"title" => new Schema([
'title' => "title",
"type" => Type::STRING,
"example" => 'Unauthorized'
]),
]
])
]
]),
'forbidden' => new Schema([
'title' => "unauthorized_error",
"type" => Type::OBJECT,
"properties" => [
"errors" => new Schema([
'title' => "errors",
"type" => Type::OBJECT,
"properties" => [
"detail" => new Schema([
'title' => "detail",
"type" => Type::STRING,
"example" => 'Forbidden.'
]),
"status" => new Schema([
'title' => "status",
"type" => Type::STRING,
"example" => '403'
]),
"title" => new Schema([
'title' => "title",
"type" => Type::STRING,
"example" => 'Forbidden'
]),
]
])
]
]),
'not_found' => new Schema([
'title' => "404 Not Found",
"type" => Type::OBJECT,
"properties" => [
"errors" => new Schema([
'title' => "errors",
"type" => Type::OBJECT,
"properties" => [
"status" => new Schema([
'title' => "status",
"type" => Type::STRING,
"example" => '404'
]),
"title" => new Schema([
'title' => "title",
"type" => Type::STRING,
"example" => 'Not Found'
]),
]
])
]
]),
];
// END DEFAULT SCHEMA
// FOREACH OVER ALL SERVERS
foreach ($servers as $key => $server) {
$openapi = new \cebe\openapi\spec\OpenApi([
'openapi' => '3.0.2',
'info' => [
'title' => 'Test API',
'version' => '1.0.0',
],
'paths' => [],
"components" => new Components([
'parameters' => [
'sort' => new Parameter([
"name" => "sort",
"in" => "query",
"description" => '[fields to sort by](https://jsonapi.org/format/#fetching-sorting)',
"required" => false,
"allowEmptyValue" => true,
"style" => "form",
"schema" => ["type" => "string"]
]),
'pageSize' => new Parameter([
"name" => "page[size]",
"in" => "query",
"description" => 'size of page for paginated results',
"required" => false,
"allowEmptyValue" => true,
"schema" => ["type" => "integer"]
]),
'pageNumber' => new Parameter([
"name" => "page[number]",
"in" => "query",
"description" => 'size of page for paginated results',
"required" => false,
"allowEmptyValue" => true,
"schema" => ["type" => "integer"]
]),
'pageLimit' => new Parameter([
"name" => "page[limit]",
"in" => "query",
"description" => 'size of page for paginated results',
"required" => false,
"allowEmptyValue" => true,
"schema" => ["type" => "integer"]
]),
'pageOffset' => new Parameter([
"name" => "page[offset]",
"in" => "query",
"description" => 'size of page for paginated results',
"required" => false,
"allowEmptyValue" => true,
"schema" => ["type" => "integer"]
]),
]
])
]);
/** @var \LaravelJsonApi\Core\Server\Server $server */
$server = new $server(app(), $key);
$openapi->__set('servers', [new Server([
'url' => "{serverURL}",
"description" => "provide your server URL",
"variables" => [
"serverURL" => new ServerVariable([
"default" => $server->url(""),
"description" => "path for server",
])
]
])]);
$all_schemas = [];
$all_requests = [];
$all_parameters = [];
$routes = collect(Route::getRoutes()->getRoutes())->filter(function (\Illuminate\Routing\Route $route) use (
$servers, $key
) {
return \Str::contains($route->getName(), $key);
});
$route_methods = [];
// FOREACH OVER ROUTES
/** @var \Illuminate\Routing\Route $route */
foreach ($routes as $route) {
$uri = $route->uri;
/** @var string $route_uri route uri without api/serverkey prefix */
$route_uri = str_replace("api/$key", '', $uri);
// $route_uri now looks like /users for example.
// $uri now e.g. api/v1/users/{user}/owns-machines
$uri = \Str::replaceFirst($route->getPrefix(), '', $uri);
// $uri now e.g. /owns-machines
/** @var array $methods List of HTTP methods this route responds to */
$methods = $route->methods();
$requires_path = \Str::contains($uri, '{');
$schema_name = null; // default in case we can't find the schema?
// What's the purpose? In my test data, I don't have a case where {} occur. It takes /users
// and the output of between() is still /users.
$schema_name = \Str::between($uri, '{', '}');
// TODO throw away contains condition and just replace? looks useless
// remove slashes from schema name
if (\Str::contains($schema_name, '/')) {
$schema_name = str_replace('/', '', $uri);
}
$schema_name_plural = (string)\Str::of($schema_name)->plural()->replace('_', '-');
// from my perspective, this should always be true because between() returns the subject whether there is
// parentheses or not.
if ($schema_name) {
$sh = $server->schemas()->schemaFor($schema_name_plural);
/** @var \LaravelJsonApi\Eloquent\Schema $schema */
$schema = new $sh($server);
//$schema->withSchemas($server->schemas()); // method doesn't exist anymore. can't find out what it did
}
// FOREACH OVER ROUTE'S METHODS
/** @var string $method */
foreach ($methods as $method) {
$parameters = [];
$responses = new Responses([]);
if ($method === 'HEAD') {
continue;
}
if ($method === 'GET') {
if (!$requires_path) {
foreach ($schema->filters() as $filter) {
array_push($parameters, new Parameter([
'name' => "filter[{$filter->key()}]",
'in' => 'query',
'required' => false,
'allowEmptyValue' => true,
'examples' => $schema::model()::all()->pluck($filter->key())->mapWithKeys(function ($f) {
return [$f => new \cebe\openapi\spec\Example([
'value' => $f,
])];
})->toArray(),
'schema' => new Schema([
"type" => Type::STRING
]),
]));
}
foreach ([
"sort",
"pageSize",
"pageNumber",
"pageLimit",
"pageOffset",
] as $parameter) {
array_push($parameters, ['$ref' => "#/components/parameters/" . $parameter]);
}
}
}
// Why in_array? isn't $method always a string?
if (!in_array($method, ['DELETE'])) {
$responses->addResponse(200, new Response([
'description' => "$method $schema_name",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/" . $schema_name_plural
])]
])
])
],
]));
} else {
$responses->addResponse(200, new Response([
'description' => "$method $schema_name",
]));
}
if(in_array($method, ['POST'])) {
$responses->addResponse(201, new Response([
'description' => "$method $schema_name",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/" . $schema_name_plural
])]
])
])
],
]));
}
if(in_array($method, ['POST','PATCH'])) {
$responses->addResponse(202, new Response([
'description' => "$method $schema_name",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/" . $schema_name_plural
])]
])
])
],
]));
}
$responses->addResponse(401, new Response([
'description' => "Unauthorized Action",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/unauthorized"
])]
])
])
],
]));
$responses->addResponse(403, new Response([
'description' => "Forbidden Action",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/forbidden"
])]
])
])
],
]));
$responses->addResponse(404, new Response([
'description' => "Content Not Found",
"content" => [
"application/vnd.api+json" => new MediaType([
"schema" => new Schema([
"oneOf" => [new Reference([
'$ref' => "#/components/schemas/not_found"
])]
])
])
],
]));
if ($requires_path) {
$models = ($schema::model())::all();
array_push($parameters, new Parameter([
'name' => $schema_name,
'in' => 'path',
'required' => true,
'allowEmptyValue' => false,
"examples" => optional($models)->mapWithKeys(function ($model) use($schema) {return [
$model->{$schema->id()->column() ?? $model->getRouteKeyName()} => new Example([
"value" => $model->{$schema->id()->column() ?? $model->getRouteKeyName()}
])
];})->toArray(),
'schema' => new Schema([
'title' => $schema_name,
]),
]));
}
if (in_array($method, ['POST', 'PATCH'])) {
$requestBody = ['$ref' => "#/components/requestBodies/" . $schema_name_plural . "_" . strtolower($method)];
$route_methods[$route_uri] = array_merge($route_methods[$route_uri], [
strtolower($method) => new Operation([
"parameters" => $parameters,
"responses" => $responses,
"requestBody" => $requestBody,
])
]);
} else {
$route_methods = array_merge($route_methods, [
$route_uri => [
strtolower($method) => new Operation([
"parameters" => $parameters,
"responses" => $responses,
])
]]);
}
unset($parameters, $responses, $field_schemas);
}
// END FOREACH OVER ROUTE'S METHODS
}
// END FOREACH OVER ROUTES
// FOREACH OVER SCHEMAS
foreach ($server->schemas()->types() as $schema_name) {
/** @var \LaravelJsonApi\Eloquent\Schema $schema */
$schema = $server->schemas()->schemaFor($schema_name);
$schema_name_plural = (string)\Str::of($schema_name)->plural()->replace('_', '-');
$methods = ['GET', 'PATCH', 'POST', 'DELETE'];
foreach ($methods as $method) {
$field_schemas = [];
$included_schemas = [];
$parameters = [];
if ($method === 'GET') {
foreach ($schema->fields() as $field) {
if ($field instanceof Relation) {
try {
$relation_plural = \Str::plural($field->name());
$included_schemas = array_merge($included_schemas, [
new Reference([
'$ref' => "#/components/schemas/" . $relation_plural . "_data"
])
]);
} catch (\Throwable $exception) {
continue;
}
continue;
}
}
$schema_data = $this->getSwaggerSchema($server, $schema, $schema_name, $schema_name_plural, $method, $all_schemas);
$all_schemas = array_merge($all_schemas, [$schema_name => new Schema([
'title' => $schema_name,
'properties' => [
"jsonapi" => new Schema([
'title' => 'jsonapi',
'properties' => [
"version" => new Schema([
"title" => "version",
'type' => Type::STRING,
"example" => "1.0"
])
]
]),
"data" => new Schema([
"oneOf" => [
new Reference([
'$ref' => "#/components/schemas/" . $schema_name_plural . "_data"
])
]
]),
"included" => new Schema([
"type" => Type::OBJECT,
"title" => "included",
"properties" => $included_schemas
])
],
])]);
$all_schemas = array_merge($all_schemas, [$schema_name . "_data" => new Schema([
'title' => $schema_name . "_data",
'properties' => $schema_data->__get('properties'),
])]);
}
unset($included_schemas);
if (!empty($schema->fields()) && $method !== 'GET') {
$contents = [];
foreach ($schema->fields() as $field) {
$contents = array_merge($contents, [
$field->name() => new Schema([
'title' => $field->name(),
'type' => 'string',
])
]);
}
$all_requests = array_merge($all_requests, [$schema_name . "_" . strtolower($method) => new RequestBody([
'description' => $schema_name . "_" . strtolower($method),
'content' => [
'application/vnd.api+json' => new MediaType([
"schema" => new Schema([
"properties" => [
"data" => new Schema([
"title" => 'data',
"type" => Type::OBJECT,
"oneOf" => [
new Reference([
'$ref' => "#/components/schemas/" . $schema_name_plural . "_data"
])
]
])
]
])
])
]
])]);
}
unset($field_schemas, $parameters);
}
}
// END FOREACH OVER SCHEMAS
// Add paths to OpenApi spec
foreach ($route_methods as $key => $method) {
$openapi->paths["{$key}"] = new PathItem(array_merge([
"description" => $schema_name,
], $method));
}
$openapi->components->__set('schemas', array_merge($default_schema, $all_schemas));
$openapi->components->__set('requestBodies', $all_requests);
$openapi->components->__set('parameters', array_merge($openapi->components->parameters, $all_parameters));
}
// END FOREACH OVER ALL SERVERS
return \response()->json($openapi->validate() ? $openapi->getSerializableData() : $openapi->getErrors());
}
private function getSwaggerSchema($server, $schema, string $schema_name, string $schema_name_plural, string $method,
$all_schemas, $for_includes = false) {
if (isset($all_schemas[$schema_name_plural])) return $all_schemas[$schema_name_plural];
$field_schemas = [];
$relation_schemas = [];
$model = $schema::model();
$models = $model::all();
$model = $model::first();
foreach ($schema->fields() as $field) {
if (in_array($method, ['DELETE'])) {
continue;
}
$field_schema = new Schema([
'title' => $field->name(),
"type" => Type::OBJECT,
]);
if ($field instanceof ID) continue;
if (
$field instanceof Str ||
$field instanceof ID ||
$field instanceof DateTime
) {
$field_schema->__set('type', Type::STRING);
}
if ($field instanceof Boolean) {
$field_schema->__set('type', Type::BOOLEAN);
}
if ($field instanceof Number) {
$field_schema->__set('type', Type::NUMBER);
}
if (!($field instanceof Relation)) {
try {
$field_schema->__set("example", optional($model)->{$field->column()});
} catch (\Throwable $exception) {
// TODO: Figure out if the field is readonly
}
}
if ($field instanceof Relation) {
$relation_schema = new Schema([
'title' => $field->name(),
]);
$relation_link_schema = new Schema([
'title' => $field->name(),
]);
$relation_data_schema = new Schema([
'title' => $field->name(),
]);
$field_name = \LaravelJsonApi\Core\Support\Str::dasherize(
\LaravelJsonApi\Core\Support\Str::plural($field->relationName())
);
$relation_link_schema->__set('properties', [
'related' => new Schema([
'title' => 'related',
"type" => Type::STRING,
]),
'self' => new Schema([
'title' => 'self',
"type" => Type::STRING,
]),
]);
$relation_data_schema->__set('properties', [
'type' => new Schema([
'title' => 'type',
"type" => Type::STRING,
"example" => $field_name,
]),
'id' => new Schema([
'title' => 'id',
"type" => Type::STRING,
"example" => optional($model)->{$schema->id()->column() ?? optional($model)->getRouteKeyName()},
]),
]);
$relation_schema->__set('properties', [
'links' => new Schema([
'title' => 'links',
'type' => Type::OBJECT,
"allOf" => [$relation_link_schema],
"example" => $server->url([$field_name,optional($model)->{$schema->id()->column() ?? optional($model)->getRouteKeyName()}]),
]),
'data' => new Schema([
'title' => 'data',
"allOf" => [$relation_data_schema],
]),
]);
if ($field instanceof ToOne && in_array($field_name, $server->schemas()->types())) {
$field_schema->__set('oneOf', [
$relation_schema
]);
}
$relation_schemas = array_merge($relation_schemas, [$field->name() => $relation_schema]);
continue;
}
$field_schemas = array_merge($field_schemas, [$field->name() => $field_schema]);
unset($field_schema);
}
return new Schema([
"type" => Type::OBJECT,
"title" => "data",
"properties" => [
"type" => new Schema([
'title' => $schema_name,
'type' => Type::STRING,
'example' => $schema_name_plural
]),
"id" => new Schema([
'title' => 'id',
'type' => Type::STRING,
"example" => optional($model)->id
]),
"attributes" => new Schema([
'title' => 'attributes',
'properties' => $field_schemas
]),
"relationships" => new Schema([
'title' => 'relationships',
'properties' => !empty($relation_schemas) ? $relation_schemas : []
]),
"links" => new Schema([
'title' => 'links',
"nullable" => true,
'properties' => [
"self" => new Schema([
"title" => "self",
'type' => Type::STRING,
"example" => $server->url([$schema_name_plural,optional($model)->{$schema->id()->column() ?? optional($model)->getRouteKeyName()}]),
])
]
]),
]
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment