Managing a changelog with API Platform & OpenAPI

Benjamin Ellis
4 min readFeb 2, 2022

--

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 2
POST /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:

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

--

--

Benjamin Ellis

Symfony developer, work with VueJS, owner of www.sport-finder.com and currently employee.