Een complete Mastodon API client bouwen

Gepost in Mastodon, PHP, 9 maanden geleden Leestijd: 6 minuten
image

Je hebt soms van die dingen die heel klein beginnen maar dan heel snel uitlopen op een enorm project. Ik had een simpel idee voor een Mastodon-app. Alleen bleken de bestaande PHP implementaties incompleet, verouderd, of anderszins kwalitatief ondermaats. Dus besloot ik er zelf een te maken.

Ik schreef hier al eerder over in Slimme generics in PHP. Ik had daarmee een goede basis opgezet, en een paar eerste API methodes waren geïmplementeerd. So far, so good.

Maar in de Mastodon documentatie staan ongeveer 200 verschillende API methodes gedocumenteerd, en rond de 65 verschillende entities. Als ik die allemaal met de hand zou moeten implementeren... aintnobodygottimeforthat.gif!

Ik wilde echt een complete implementatie maken, dus moest ik iets anders verzinnen.

Ik heb er naar gezocht, maar helaas is er van de Mastodon API geen complete openapi specificatie beschikbaar. Zo'n machine readable bestand beschrijft een complete API, en daarmee is het mogelijk om (grote delen van) de code automatisch te genereren, voor zowel client als server, en het is ook meteen je gebruikers-documentatie.

Bij het bouwen van een API is daarom altijd handig om te beginnen met die specificatie, en die als basis voor alles te houden. Als je dat achteraf wil doen, en die ook up to date wil houden, dat kost uiteindelijk veel meer tijd en moeite. Vandaar ook dat die er voor Mastodon nog niet is, waarschijnlijk.

Tijdens mijn zoektocht vond ik een interessante alternatieve aanpak: AppMap. Dit is een tool om allerlei run time data te verzamelen terwijl de code uitgevoerd wordt. Deze data kan later geanalyseerd en gevisualiseerd worden, of omgezet naar andere formaten. Het idee is dat je door het draaien van de Mastodon test suite met hulp van AppMap automatisch een openapi spec zou kunnen genereren.

Helaas liep ik tijdens het draaien van de Mastodon test suite met AppMap erbij tegen tests aan die opeens faalden, terwijl deze zonder AppMap wel slaagden. Ik heb contact gezocht met de mensen achter AppMap via hun Slack-kanaal, en zij bevestigden dat het om een bug in AppMap blijkt te gaan, die helaas nog niet is opgelost.

Het lukte wel om een soort van API spec te genereren op basis van de wel slagende tests, maar die was natuurlijk verre van compleet en dus onbruikbaar voor mijn doel. Het was sowieso de vraag hoe bruikbaar deze aanpak zou kunnen zijn, ook zonder de bug in AppMap. Want zelfs als je 100% test coverage hebt, zou er nog allerlei meta-data ontbreken (zoals bijvoorbeeld documentatie) om een echt goede en complete specificatie te kunnen genereren.

Ik was dus weer terug bij af. De enige mogelijkheid die ik nu nog zag was het parsen van de documentatie markdown. Die zag er op eerste gezicht best "parseable" uit: de structuur leek uniform genoeg, en alle benodigde data was aanwezig.

Voor een eerste PoC van de parser viel ik terug op een oude bekende: Perl. Niet een taal die ik tegenwoordig nog vaak gebruik, maar Perl (in combinatie met wat Regexp wizardry) is wel uitermate geschikt voor het snel parsen en manipuleren van tekstuele data, beter dan PHP zou ik zeggen. In korte tijd had ik een paar quick & dirty scripts in elkaar gehackt die alle relevante markdown bestanden parste en er een nette JSON structuur van entities en methods van genereerde.

Nu moest ik hier nog PHP code van maken. Ik heb dit eerst geprobeerd met hulp van de Symfony Maker bundle maar die bleek wat beperkt voor mijn toepassing. Dus uiteindelijk heb ik ook maar mijn eigen code generator gemaakt.

Ik had dat nog nooit gedaan. Tenminste, verder dan generatie van lege basis-classes die je daarna handmatig aanvulde was ik nooit gekomen. Code generation is op zich niet zo verschillend van html generatie, maar je hebt wel met een aantal domein-specifieke uitdagingen te maken. De syntax van PHP code is natuurlijk stricter en complexer dan die van html, zeker als je code ook nog door de PHPStan check moet. Zo zit je bijvoorbeeld met imports die uniek moeten zijn - als je bijvoorbeeld zowel een \Foo\Request als een \Bar\Request wil importeren moet je een alias maken en daar in de rest van je code naar refereren:

use Foo\Request;
use Bar\Request as BarRequest;

// ...
$request = new BarRequest();

Dat moet natuurlijk goed gaan als de lijst van imports dynamisch (en in sommige gevallen behoorlijk lang) is. Voor code generation moet je op een hoger abstractie-niveau over je code gaan nadenken. Dat was best een leerzame ervaring.

Als je dan toch aan het genereren bent, kun ik ook makkelijk wat extra gemak genereren, met een verzameling method proxies die je in de client verwerkt, zodat je niet zelf op zoek hoeft te gaan naar de juiste request classes, maar je IDE je hiermee kan helpen. Het is dan op zich wel een vorm van code duplicatie, maar deze heeft daadwerkelijk toegevoegde waarde.

Inmiddels was ik een aantal weken verder, maar nu begon het saaie werk: testen. De Mastodon documentatie is geschreven voor en door mensen, dus is niet alles op exact dezelfde generieke manier beschreven. Er bleken best wat inconsistenties, edge cases, custom parameters etc in te zitten, waardoor mijn json en dus mijn code niet altijd kloppend of compleet was. Met de eerste poging had ik misschien 80% goed geparset, dat is best veel, maar, je weet hoe het gaat: aan die overgebleven 20% was ik uiteindelijk wel 80% van de tijd kwijt.

Want ik moest alsnog zo'n beetje elke methode en entity nalopen, checken op compleetheid en correctheid, en elke keer mijn parser en/of code generatie aanpassen op de issues die ik tegenkwam.

Voor de entities kon ik dat deels automatiseren. Voor vrijwel alle entities staat een stukje voorbeeld-json in de documentatie en daar kon ik met mijn code generator ook unit tests van bakken. Ik heb daarmee best een aantal issues kunnen spotten en oplossen, en mijn test code coverage is er ook flink van omhoog gegaan.

Maar veel was echt gewoon handwerk. Dat was best een flinke grind die veel tijd kostte. Mijn parser werd beter en beter, maar ik vond een paar inconsistenties in de documentatie waar ik niet omheen kon werken in mijn code, dus heb ik uiteindelijk een fork van de documentatie gemaakt, alles zelf aangepast en er een PR voor aangemaakt.

Toen ik na een aantal pre-releases eindelijk bij versie 1.0 uitkwam, heb ik wel even een flesje opengetrokken om het te vieren. Het was alles bij elkaar echt een uitdagend en intensief project geworden. Dit had ik niet helemaal verwacht toen ik besloot om "even" een API client te bouwen. Maar ik ben trots op het eindresultaat, en ik hoop dat het een nuttige library is die met plezier door anderen gebruikt gaat worden! Download hem op https://github.com/vazaha-nl/mastodon-api-client

Gerelateerde posts

image
Pass, the standard Unix password manager

Bijna iedereen gebruikt tegenwoordig een password manager. LastPass, 1Password, Keepass, etc. Maar slechts weinig mensen kennen "pass", de unix password manager. In deze post leg ik uit waarom pass een goede optie kan zijn voor de handige linux/unix nerd.

Lees meer →

image
Een nieuw blogplatform

Ik vond het wel eens tijd om wat meer over mijn vak te schrijven. Dus heb ik een blog opgezet. Hiervoor heb mijn eigen custom blogplatform gebouwd. En daar schrijf ik dus meteen maar een eerste post over.

Lees meer →

image
Een transparante proxy met ssh en sshuttle

Een van de meest krachtige tools die op elk unix of linux systeem beschikbaar zijn is ssh, de "secure shell". In dit artikel laat ik zien hoe je met ssh een transparant, systeembreed en secure vpn opzet.

Lees meer →

image
PHP: Frankenstein arrays

PHP is inmiddels best een mooie taal geworden, maar kent nog wel wat nare erfenissen uit het verleden. Zoals de verraderlijke Frankenstein abominatie ook wel bekend als de "array".

Lees meer →