Pular para o conteúdo principal

OpenAPI v3

Decisão: OpenAPI v3 como contrato da API REST; Mojolicious::Plugin::OpenAPI para validação automática de requisições e respostas. ADR-015 — Contrato de API OpenAPI v3


Por que OpenAPI

O contrato OpenAPI v3 é a fonte única de verdade da API da Stega: define endpoints, parâmetros, schemas de corpo e códigos de resposta. O plugin Mojolicious::Plugin::OpenAPI valida automaticamente toda requisição recebida contra o schema — retornando 400 Bad Request com mensagem de erro estruturada antes que o controller seja chamado. Não é necessário escrever validação manual.

O arquivo api/stega.yaml também serve como documentação interativa (Swagger UI / Redoc) e como base para geração de clientes em outras linguagens.


Estrutura do arquivo de contrato

# api/stega.yaml
openapi: "3.0.3"
info:
title: Stega API
version: "1.0.0"
description: |
API REST do sistema de tickets de suporte Stega.
Autenticação: JWT Bearer (obter token no Keycloak).

servers:
- url: /api/v1
description: API versionada

components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT

schemas:
Ticket:
type: object
required: [id, title, status, priority, created_at]
properties:
id:
type: integer
title:
type: string
body:
type: string
status:
type: string
enum: [open, in_progress, waiting, resolved, closed]
priority:
type: string
enum: [low, medium, high, critical]
custom_fields:
type: object
additionalProperties: true
created_at:
type: string
format: date-time

TicketCreate:
type: object
required: [product_id, title, body]
properties:
product_id:
type: integer
title:
type: string
minLength: 5
maxLength: 200
body:
type: string
minLength: 10
priority:
type: string
enum: [low, medium, high, critical]
default: medium
custom_fields:
type: object
additionalProperties: true

Error:
type: object
required: [error]
properties:
error:
type: string

security:
- bearerAuth: []

paths:
/tickets:
get:
operationId: listTickets
summary: Listar tickets
parameters:
- name: status
in: query
schema:
type: string
enum: [open, in_progress, waiting, resolved, closed]
- name: q
in: query
description: Busca em texto completo
schema:
type: string
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
"200":
description: Lista de tickets
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Ticket'
"401":
description: Não autenticado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

post:
operationId: createTicket
summary: Criar ticket
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TicketCreate'
responses:
"201":
description: Ticket criado
content:
application/json:
schema:
type: object
properties:
id:
type: integer
"400":
description: Dados inválidos (validação automática pelo plugin)
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

Carregando o plugin no Mojolicious

# lib/Stega.pm
sub startup {
my $self = shift;

# Plugin OpenAPI — carrega o contrato e ativa validação automática
$self->plugin('OpenAPI', {
url => $self->home->child('api/stega.yaml'),
schema => 'v3',
});

# As rotas OpenAPI já estão registradas pelo plugin usando operationId
# Mapeamento: operationId => 'controller#ação'
# listTickets => Stega::Controller::Ticket::list
# createTicket => Stega::Controller::Ticket::create
}

Mapeamento operationId → controller

O plugin converte automaticamente o operationId em controller#ação:

operationIdMóduloMétodo
listTicketsStega::Controller::Ticketlist
createTicketStega::Controller::Ticketcreate
getTicketStega::Controller::Ticketget
updateTicketStega::Controller::Ticketupdate
listCommentsStega::Controller::Commentlist

A convenção: camelCase no operationId é convertido para snake_case no nome do módulo e método. Ou configure explicitamente com x-mojo-to.


Validação automática — o que o plugin faz

Com o plugin carregado, toda requisição é validada antes do controller:

# Requisição sem campo obrigatório 'title'
curl -X POST http://localhost:3000/api/v1/tickets \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"product_id": 1, "body": "Sem título"}'

# Resposta automática do plugin (400)
{
"errors": [
{
"message": "Missing property.",
"path": "/title"
}
]
}
# Valor fora do enum
curl -X POST http://localhost:3000/api/v1/tickets \
-d '{"product_id":1,"title":"Bug","body":"Detalhes","priority":"urgente"}'

# Resposta automática
{
"errors": [
{
"message": "Not in enum list: urgente.",
"path": "/priority"
}
]
}

Testes com validação OpenAPI

# t/api/tickets.t
use Test::More;
use Test::Mojo;

my $t = Test::Mojo->new('Stega');

subtest 'body inválido retorna 400 com mensagem estruturada' => sub {
$t->post_ok('/api/v1/tickets',
{ Authorization => 'Bearer test' },
json => { product_id => 1 } # falta title e body (obrigatórios)
)->status_is(400)
->json_like('/errors/0/message', qr/Missing property/);
};

subtest 'priority inválida retorna 400' => sub {
$t->post_ok('/api/v1/tickets',
{ Authorization => 'Bearer test' },
json => {
product_id => 1,
title => 'Bug crítico',
body => 'Descrição detalhada do problema',
priority => 'urgente', # não está no enum
}
)->status_is(400);
};

done_testing;

Documentação interativa

O plugin expõe automaticamente a documentação OpenAPI via Swagger UI:

# Disponível em desenvolvimento
http://localhost:3000/api/v1

Para personalizar o path de documentação:

$self->plugin('OpenAPI', {
url => $self->home->child('api/stega.yaml'),
schema => 'v3',
spec_url => '/api-docs', # URL da spec JSON
});

Armadilhas comuns

ArmadilhaDescriçãoComo evitar
operationId duplicadoO plugin registra apenas um handler por operationIdNomes únicos globais no YAML
Schema sem requiredCampos não marcados como required passam como null silenciosamenteListe campos obrigatórios explicitamente
$ref com caminho errado$ref: '#/components/schemas/Foo' — typo causa erro silenciosoValide o YAML com openapi-generator validate
Resposta não documentadaO plugin emite warning para códigos de status não no YAMLDocumente todos os códigos possíveis (200, 201, 400, 401, 404)
YAML vs JSON para o schemaO plugin aceita ambos, mas YAML é mais legível para manutençãoUse YAML; JSON apenas se gerado por ferramenta