API: Add urls to response, configure required fields

This commit is contained in:
Igor Scheller 2023-07-27 14:34:21 +02:00 committed by Michael Weimann
parent a5cebc8535
commit ea93e27a9d
13 changed files with 121 additions and 20 deletions

View File

@ -63,6 +63,17 @@ components:
name: name:
type: string type: string
example: Angel example: Angel
description:
type: string
example: Meta-Group of all registered Angels
url:
type: string
example: https://example.com/news/42
required:
- id
- name
- description
- url
Error: Error:
type: object type: object
properties: properties:
@ -105,6 +116,19 @@ components:
format: date-time format: date-time
description: DateTime in ISO-8601 format description: DateTime in ISO-8601 format
example: 2023-05-13T23:00:00.000000Z example: 2023-05-13T23:00:00.000000Z
url:
type: string
example: https://example.com/news/42
required:
- id
- title
- text
- is_meeting
- is_pinned
- is_highlighted
- created_at
- updated_at
- url
Room: Room:
type: object type: object
properties: properties:
@ -114,6 +138,13 @@ components:
name: name:
type: string type: string
example: Heaven example: Heaven
url:
type: string
example: https://example.com/rooms/42
required:
- id
- name
- url
Shift: Shift:
type: object type: object
properties: properties:
@ -156,6 +187,21 @@ components:
format: date-time format: date-time
description: DateTime in ISO-8601 format description: DateTime in ISO-8601 format
example: 2023-05-13T23:00:00.000000Z example: 2023-05-13T23:00:00.000000Z
url:
type: string
example: https://example.com/shifts/42
required:
- id
- title
- description
- start
- end
- entries
- room
- shift_type
- created_at
- updated_at
- url
ShiftEntry: ShiftEntry:
type: object type: object
properties: properties:
@ -163,6 +209,9 @@ components:
$ref: '#/components/schemas/User' $ref: '#/components/schemas/User'
type: type:
$ref: '#/components/schemas/AngelType' $ref: '#/components/schemas/AngelType'
required:
- user
- type
ShiftType: ShiftType:
type: object type: object
properties: properties:
@ -175,6 +224,10 @@ components:
description: description:
type: string type: string
example: This is a generic build-up shift, mostly involving heavy lifting. example: This is a generic build-up shift, mostly involving heavy lifting.
required:
- id
- name
- description
User: User:
type: object type: object
properties: properties:
@ -207,6 +260,17 @@ components:
type: string type: string
nullable: true nullable: true
example: 1234567890 example: 1234567890
url:
type: string
example: https://example.com/users/42
required:
- id
- name
- first_name
- last_name
- pronoun
- contact
- url
security: security:
- bearerAuth: [ ] - bearerAuth: [ ]

View File

@ -11,11 +11,15 @@ class AngelTypeController extends ApiController
{ {
public function index(): Response public function index(): Response
{ {
$news = AngelType::query() $models = AngelType::query()
->orderBy('name') ->orderBy('name')
->get(['id', 'name']); ->get(['id', 'name', 'description']);
$data = ['data' => $news]; $models->map(function (AngelType $model): void {
$model->url = $this->url->to('/angeltypes', ['action' => 'view', 'angeltype_id' => $model->id]);
});
$data = ['data' => $models];
return $this->response return $this->response
->withContent(json_encode($data)); ->withContent(json_encode($data));
} }

View File

@ -6,6 +6,7 @@ namespace Engelsystem\Controllers\Api;
use Engelsystem\Controllers\BaseController; use Engelsystem\Controllers\BaseController;
use Engelsystem\Http\Response; use Engelsystem\Http\Response;
use Engelsystem\Http\UrlGeneratorInterface;
abstract class ApiController extends BaseController abstract class ApiController extends BaseController
{ {
@ -13,7 +14,7 @@ abstract class ApiController extends BaseController
'api', 'api',
]; ];
public function __construct(protected Response $response) public function __construct(protected Response $response, protected UrlGeneratorInterface $url)
{ {
$this->response = $this->response $this->response = $this->response
->withHeader('content-type', 'application/json') ->withHeader('content-type', 'application/json')

View File

@ -11,12 +11,16 @@ class NewsController extends ApiController
{ {
public function index(): Response public function index(): Response
{ {
$news = News::query() $models = News::query()
->orderByDesc('updated_at') ->orderByDesc('updated_at')
->orderByDesc('created_at') ->orderByDesc('created_at')
->get(['id', 'title', 'text', 'is_meeting', 'is_pinned', 'is_highlighted', 'created_at', 'updated_at']); ->get(['id', 'title', 'text', 'is_meeting', 'is_pinned', 'is_highlighted', 'created_at', 'updated_at']);
$data = ['data' => $news]; $models->map(function (News $model): void {
$model->url = $this->url->to('/news/' . $model->id);
});
$data = ['data' => $models];
return $this->response return $this->response
->withContent(json_encode($data)); ->withContent(json_encode($data));
} }

View File

@ -11,11 +11,15 @@ class RoomsController extends ApiController
{ {
public function index(): Response public function index(): Response
{ {
$news = Room::query() $models = Room::query()
->orderBy('name') ->orderBy('name')
->get(['id', 'name']); ->get(['id', 'name']);
$data = ['data' => $news]; $models->map(function (Room $model): void {
$model->url = $this->url->to('/rooms', ['action' => 'view', 'room_id' => $model->id]);
});
$data = ['data' => $models];
return $this->response return $this->response
->withContent(json_encode($data)); ->withContent(json_encode($data));
} }

View File

@ -37,14 +37,24 @@ class ShiftsController extends ApiController
'last_name' => $user->personalData->last_name, 'last_name' => $user->personalData->last_name,
'pronoun' => $user->personalData->pronoun, 'pronoun' => $user->personalData->pronoun,
'contact' => $user->contact->only(['dect', 'mobile']), 'contact' => $user->contact->only(['dect', 'mobile']),
'url' => $this->url->to('/users', ['action' => 'view', 'user_id' => $user->id]),
]; ];
$angelTypeData = $entry->angelType->only(['id', 'name']);
$angelTypeData['url'] = $this->url->to(
'/angeltypes',
['action' => 'view', 'angeltype_id' => $entry->angelType->id]
);
$entries[] = [ $entries[] = [
'user' => $userData, 'user' => $userData,
'type' => $entry->angelType->only(['id', 'name']), 'type' => $angelTypeData,
]; ];
} }
$roomData = $room->only(['id', 'name']);
$roomData['url'] = $this->url->to('/rooms', ['action' => 'view', 'room_id' => $room->id]);
$shiftEntries[] = [ $shiftEntries[] = [
'id' => $shift->id, 'id' => $shift->id,
'title' => $shift->title, 'title' => $shift->title,
@ -52,10 +62,11 @@ class ShiftsController extends ApiController
'start' => $shift->start, 'start' => $shift->start,
'end' => $shift->end, 'end' => $shift->end,
'entries' => $entries, 'entries' => $entries,
'room' => $room->only(['id', 'name']), 'room' => $roomData,
'shift_type' => $shift->shiftType->only(['id', 'name', 'description']), 'shift_type' => $shift->shiftType->only(['id', 'name', 'description']),
'created_at' => $shift->created_at, 'created_at' => $shift->created_at,
'updated_at' => $shift->updated_at, 'updated_at' => $shift->updated_at,
'url' => $this->url->to('/shifts', ['action' => 'view', 'shift_id' => $shift->id]),
]; ];
} }

View File

@ -18,7 +18,7 @@ class AngelTypeControllerTest extends ApiBaseControllerTest
$this->initDatabase(); $this->initDatabase();
AngelType::factory(3)->create(); AngelType::factory(3)->create();
$controller = new AngelTypeController(new Response()); $controller = new AngelTypeController(new Response(), $this->url);
$response = $controller->index(); $response = $controller->index();
$this->validateApiResponse('/angeltypes', 'get', $response); $this->validateApiResponse('/angeltypes', 'get', $response);

View File

@ -4,15 +4,18 @@ declare(strict_types=1);
namespace Engelsystem\Test\Unit\Controllers\Api; namespace Engelsystem\Test\Unit\Controllers\Api;
use Engelsystem\Http\UrlGeneratorInterface;
use Engelsystem\Test\Unit\Controllers\ControllerTest as TestCase; use Engelsystem\Test\Unit\Controllers\ControllerTest as TestCase;
use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress; use League\OpenAPIValidation\PSR7\OperationAddress as OpenApiAddress;
use League\OpenAPIValidation\PSR7\ResponseValidator as OpenApiResponseValidator; use League\OpenAPIValidation\PSR7\ResponseValidator as OpenApiResponseValidator;
use League\OpenAPIValidation\PSR7\ValidatorBuilder as OpenApiValidatorBuilder; use League\OpenAPIValidation\PSR7\ValidatorBuilder as OpenApiValidatorBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
abstract class ApiBaseControllerTest extends TestCase abstract class ApiBaseControllerTest extends TestCase
{ {
protected OpenApiResponseValidator $validator; protected OpenApiResponseValidator $validator;
protected UrlGeneratorInterface $url;
protected function validateApiResponse(string $path, string $method, ResponseInterface $response): void protected function validateApiResponse(string $path, string $method, ResponseInterface $response): void
{ {
@ -29,5 +32,15 @@ abstract class ApiBaseControllerTest extends TestCase
$this->validator = (new OpenApiValidatorBuilder()) $this->validator = (new OpenApiValidatorBuilder())
->fromYamlFile($openApiDefinition) ->fromYamlFile($openApiDefinition)
->getResponseValidator(); ->getResponseValidator();
/** @var UrlGeneratorInterface|MockObject $url */
$url = $this->getMockForAbstractClass(UrlGeneratorInterface::class);
$url->expects($this->any())
->method('to')
->willReturnCallback(function (string $path, array $params): string {
$query = http_build_query($params);
return $path . ($query ? '?' . $query : '');
});
$this->url = $url;
} }
} }

View File

@ -14,7 +14,7 @@ class ApiControllerTest extends ApiBaseControllerTest
*/ */
public function testConstruct(): void public function testConstruct(): void
{ {
$controller = new class (new Response('{"some":"json"}')) extends ApiController { $controller = new class (new Response('{"some":"json"}'), $this->url) extends ApiController {
public function getResponse(): Response public function getResponse(): Response
{ {
return $this->response; return $this->response;

View File

@ -15,7 +15,7 @@ class IndexControllerTest extends ApiBaseControllerTest
*/ */
public function testIndex(): void public function testIndex(): void
{ {
$controller = new IndexController(new Response()); $controller = new IndexController(new Response(), $this->url);
$response = $controller->index(); $response = $controller->index();
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
@ -32,7 +32,7 @@ class IndexControllerTest extends ApiBaseControllerTest
*/ */
public function testIndexV0(): void public function testIndexV0(): void
{ {
$controller = new IndexController(new Response()); $controller = new IndexController(new Response(), $this->url);
$response = $controller->indexV0(); $response = $controller->indexV0();
$this->assertEquals(200, $response->getStatusCode()); $this->assertEquals(200, $response->getStatusCode());
@ -48,7 +48,7 @@ class IndexControllerTest extends ApiBaseControllerTest
*/ */
public function testOptions(): void public function testOptions(): void
{ {
$controller = new IndexController(new Response()); $controller = new IndexController(new Response(), $this->url);
$response = $controller->options(); $response = $controller->options();
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
@ -61,7 +61,7 @@ class IndexControllerTest extends ApiBaseControllerTest
*/ */
public function testNotImplemented(): void public function testNotImplemented(): void
{ {
$controller = new IndexController(new Response()); $controller = new IndexController(new Response(), $this->url);
$response = $controller->notImplemented(); $response = $controller->notImplemented();
$this->assertEquals(501, $response->getStatusCode()); $this->assertEquals(501, $response->getStatusCode());

View File

@ -17,7 +17,7 @@ class NewsControllerTest extends ApiBaseControllerTest
{ {
News::factory(3)->create(); News::factory(3)->create();
$controller = new NewsController(new Response()); $controller = new NewsController(new Response(), $this->url);
$response = $controller->index(); $response = $controller->index();
$this->validateApiResponse('/news', 'get', $response); $this->validateApiResponse('/news', 'get', $response);

View File

@ -18,7 +18,7 @@ class RoomsControllerTest extends ApiBaseControllerTest
$this->initDatabase(); $this->initDatabase();
Room::factory(3)->create(); Room::factory(3)->create();
$controller = new RoomsController(new Response()); $controller = new RoomsController(new Response(), $this->url);
$response = $controller->index(); $response = $controller->index();
$this->validateApiResponse('/rooms', 'get', $response); $this->validateApiResponse('/rooms', 'get', $response);

View File

@ -40,10 +40,10 @@ class ShiftsControllerTest extends ApiBaseControllerTest
$request = new Request(); $request = new Request();
$request = $request->withAttribute('room_id', $room->id); $request = $request->withAttribute('room_id', $room->id);
$controller = new ShiftsController(new Response()); $controller = new ShiftsController(new Response(), $this->url);
$response = $controller->entriesByRoom($request); $response = $controller->entriesByRoom($request);
$this->validateApiResponse('/rooms', 'get', $response); $this->validateApiResponse('/rooms/{id}/shifts', 'get', $response);
$this->assertEquals(['application/json'], $response->getHeader('content-type')); $this->assertEquals(['application/json'], $response->getHeader('content-type'));
$this->assertJson($response->getContent()); $this->assertJson($response->getContent());