Managing a changelog with API Platform & OpenAPI
TLDR: how to generate a nice and human-readable changelog with API Platform (2.6).
The context
I’m managing a few microservices in PHP. The APIs are exposed to 2 internal consumers (website, backoffice and soon apps) and in the future many consumer apps (our B2B consutomers).
This involves many responsibilities, such as no BC break, semantic versioning, and a proper migration path if needed: the changelog.
Our stack is not yet in production. We will go live soon, and our architects reviewed all our data models and ask us to make any changes. Our front-end developers fear the incoming change and BC break.
The goal
I must generate a proper changelog for my consumers.
Where is an example of a (dummy) changelog, I’m going to provide them:
bin/openapi/generate_diff.sh backend base revision📖 Generating the OpenAPI diff for backend :
- specs/backend/base.yaml
- specs/backend/revision.yaml### New Endpoints: 6
--------------------
GET /dummies
POST /dummies
DELETE /dummies/{id}
GET /dummies/{id}
PATCH /dummies/{id}
PUT /dummies/{id}### Deleted Endpoints: None
---------------------------### Modified Endpoints: 6
-------------------------
POST /admin/login
- Responses changed
- Modified response: 200
- Content changed
- Modified media type: application/json
- Schema changed
- Properties changed
- New property: banned
- Modified property: result
- Type changed from 'string' to 'boolean'POST /blacklisted_countries
- Responses changed
- Modified response: 201
- Content changed
- Modified media type: application/json
- Schema changed
- Properties changed
- Modified property: iso
- MinLength changed from 3 to 2
- MaxLength changed from 3 to 2POST /blacklisted_countries
- Request body changed
- Content changed
- Modified media type: application/json
- Schema changed
- Properties changed
- Modified property: iso
- MinLength changed from 3 to 2
- MaxLength changed from 3 to 2
The tools
To achieve this result, you need:
- generate 2 exports (eg: one from develop, one from a feature branch)
- a tool to generate the diff : https://github.com/Tufin/oasdiff
Not many OpenAPI diff generators handle well OpenAPI documentation generated by ApiPlatform. The main limitation is the usage of $refs
. tunfin/oasdiff
is the only one I know that supports it.
Generate the 2 exports
Just use the built-in command provided by ApiPlatform :
php bin/console api:openapi:export --yaml
oasdiff
There are many ways to use the binary. I’ll use docker because I don’t have go installed on my machine.
docker run — rm -t -v $(pwd)/specs:/specs:ro tufin/oasdiff -format text -base develop.yaml -revision feature.yaml
The cautions
ApiPlatform handles many formats such as json, json+ld, HTML, etc…
In my case, the output was, at first, far too verbose for our developers. I decided to exclude all formats except json
in the diff. I’m using a decorator and a specific env variable to enable this hack (the env var is not implemented yet).
FILTER_FORMAT=application/json php bin/console api:openapi:export --yaml
or
FILTER_FORMAT=application/json,application/json+ld php bin/console api:openapi:export --yaml
This way, you end up with a smaller generate doc and at the end, a smaller (but potentially incomplete) changelog.
<?php
namespace App\Service;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\Model;
use ApiPlatform\Core\OpenApi\Model\PathItem;
use ApiPlatform\Core\OpenApi\OpenApi;
use Symfony\Component\PropertyAccess\PropertyAccess;
final class ExportOpenApiDecorator implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated,
) {
}
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->decorated)($context);
$propertyAccess = PropertyAccess::createPropertyAccessor();
/**
* @var string $path
* @var Model\PathItem $pathItem
*/
foreach ($openApi->getPaths()->getPaths() as $path => $pathItem) {
foreach (PathItem::$methods as $method) {
/** @var Model\Operation|null $operation */
$operation = $propertyAccess->getValue($pathItem, $method);
if ($operation == null) continue;
$operation = $this->cleanOperation($operation);
$set = 'with'.$method;
$pathItem = $pathItem->{$set}($operation);
}
$openApi->getPaths()->addPath(
path: $path,
pathItem: $pathItem
);
}
return $openApi;
}
private function cleanOperation(?Model\Operation $operation): Model\Operation
{
$responses = $operation->getResponses();
/** @var Model\Response $response */
foreach ($responses as $response) {
$content = $response->getContent();
/** @var \ArrayObject $content */
if ($content == null) {
continue;
}
/**
* @var string $key
* @var Model\MediaType $mediaType
*/
foreach ($content->getArrayCopy() as $key => $mediaType) {
if (!in_array($key, ['application/json'], true)) {
$content->offsetUnset($key);
}
}
}
$content = $operation->getRequestBody() ?
$operation->getRequestBody()->getContent() :
new \ArrayObject();
foreach ($content->getArrayCopy() as $key => $mediaType) {
if (!in_array($key, ['application/json'], true)) {
$content->offsetUnset($key);
}
}
return $operation;
}
}
And voila! It’s definitely a temporary solution, but it’s a good start.
🍒 on the cake : a script to automate diff between 2 branches
#!/usr/bin/env bash
# immediately exit if any command goes wrong
set -e
# error when an undefined variable is used
set -u
# return code of the whole pipeline
set -o pipefail
DIR="${BASH_SOURCE%/*}/"
SRC="${DIR}src/"
SERVICE_PATH=src/services/backend
SERVICE_NAME=backend
DOCKER_SERVICE_NAME=project-${SERVICE_NAME}
BASE=$1
REVISION=$2
mkdir -p specs/$SERVICE_NAME
printf "\n🆚 Checkout the base branch : ${BASE} \n\n"
GIT_WORK_TREE=${SERVICE_PATH} GIT_DIR=${SERVICE_PATH}/.git git checkout ${BASE}
printf "\n📗 Generating the OpenAPI yaml doc and store it in specs/${SERVICE_NAME}/${BASE}.yaml \n\n"
COMMAND="docker exec -it ${DOCKER_SERVICE_NAME} php bin/console api:openapi:export --yaml > specs/${SERVICE_NAME}/base.yaml"
printf "\t ${COMMAND} \n\n"
docker exec -it ${DOCKER_SERVICE_NAME} php bin/console api:openapi:export --yaml > specs/${SERVICE_NAME}/base.yaml
echo "🆚 Checkout the revision branch : ${REVISION}"
GIT_WORK_TREE=${SERVICE_PATH} GIT_DIR=${SERVICE_PATH}/.git git checkout ${REVISION}
echo "📗 Generating the OpenAPI yaml doc and store it in specs/${SERVICE_NAME}/${REVISION}.yaml"
COMMAND="docker exec -it ${DOCKER_SERVICE_NAME} php bin/console api:openapi:export --yaml > specs/${SERVICE_NAME}/revision.yaml"
printf "\t ${COMMAND} \n\n"
docker exec -it ${DOCKER_SERVICE_NAME} php bin/console api:openapi:export --yaml > specs/${SERVICE_NAME}/revision.yaml
docker run --rm -t -v $(pwd)/specs:/specs:ro tufin/oasdiff \
-format text \
-base /specs/backend/${BASE}.yaml \
-revision /specs/backend/next.yaml > specs/backend/diff.txt