L’un des obstacles les plus courants lorsqu’on programme des logiciels exposés à internet en Go est le parsing du JSON. En tant que langage typé, Go exige un destinataire correctement défini pour le JSON désérialisé. Un contournement courant est un arbre imbriqué à l’infini de map[string]interface{} et un tas de type assertions récursives.
En développant atlassian-connect-go, je me suis heurté au même mur : beaucoup des types d’Atlassian n’étaient définis que sous forme de JSON. J’ai fini par le régler en créant LazyApiCoder, qui peut créer un ensemble complet de types go à partir soit d’un schéma swagger 3, soit d’un ensemble d’exemples JSON.
Mon approche :
J’ai développé une grande partie du plugin en utilisant le fantastique outil JSON to Go de @mholt6. J’avais un mélange de types issus d’exemples proposés à différents endroits de la documentation d’Atlassian. J’ai décidé que je voulais avoir tous les types possibles. J’ai écrit ce script python qui me récupérait tous leurs exemples (du moins les visibles). Là-dessus, je me suis lancé dans la première itération de LAC : automatiser le contournement habituel.
Ma première tentative est ce qui peut maintenant s’invoquer comme LAC --target schema.go --package apackagename --source "generators/jira/jira/*.json". Cette approche grossière désérialise tout le json dans un type très générique (map[string]interface{}) et fait des type assert de manière récursive. Si l’exemple est assez bon, la struct résultante est raisonnable. Le nommage est un peu discutable et elle finit par désambiguïser des types différents mais portant le même nom avec une logique bon marché. Elle résoudra néanmoins votre problème si tout ce que vous avez est un échantillon.
Après avoir utilisé ce tremplin pour construire une grande partie du plugin, je me suis heurté au deuxième mur. En procédant à un réglage très fin du plugin, j’ai trouvé que certains types manquaient de champs. En y regardant d’un peu plus près, j’ai découvert que c’étaient les exemples qui étaient en cause : ils n’étaient tout simplement pas assez exhaustifs, ou je n’étais pas tombé sur un exemple de mon cas d’usage.
Version améliorée
En relisant mon script python, j’ai réalisé qu’il y avait un bien meilleur endroit d’où tirer cette information. Atlassian (et beaucoup d’autres fournisseurs) dispose d’une spécification swagger 3. Une spec swagger contient assez d’informations pour élaborer des types Go très corrects. Les nouveaux types pouvaient avoir un Type très précis, des Comments et être assurés d’être aussi exhaustifs que l’API en aurait jamais besoin.
La nouvelle version s’invoque comme LAC --target schema.go --package apackagename --swaggerfile generators/jira/jira/swagger-v3.v3.json et ne prend qu’un seul fichier à la fois, étant donné la nature des descriptions swagger.
Comme Swagger est une spec très documentée, tout ce que j’ai eu à faire a été de la lire et, pour les cas où elle n’était pas si claire, de consulter des exemples connus. J’avais codé ma première version en utilisant un format intermédiaire, un ensemble de dictionnaires go qui servait à produire le code final. Ma seule tâche a été de créer un convertisseur de Swagger vers le format intermédiaire. Le résultat est une paire de courtes fonctions qui interprètent swagger et construisent une représentation de types en mémoire. Le renderer a dû apprendre quelques astuces, en particulier pour l’embedding de types. Les plus grands défis ont été :
- les clauses
anyOf,allOfetoneOfqui se traduisaient par plusieurs types pointeur embarqués dans un champ/type - les types uniquement
$refqui finissaient eux aussi par être des types embarqués - le référencement récursif, qui se faisait à l’aide de pointeurs vers soi-même.
- le
additionalPropertiesutilisé dans une propriété de type object uniquement avec$ref, qui finissait par devoir être interprété commemap[string]ReferencedType - les propriétés sans nom/vides qui finissaient par être une variante de
interface{}oumap[string]interface{}ou[]interface, qui se désérialisent correctement mais présentent davantage de défis pour l’utilisateur final. - s’assurer que les descriptions parviennent aux commentaires pour tous les bons types
Vous pouvez trouver un bel échantillon des résultats dans ce schéma JIRA.
Un effet secondaire sympa de cela est que vous pouvez l’utiliser comme une invocation de go generate : si votre code contient une spec swagger, vous obtenez des types gratuitement.
Conclusion
Il existe d’autres solutions qui tentent de travailler avec swagger, certaines abandonnées, certaines en développement et au moins une en usage. Mon problème avec toutes les librairies existantes pour cela est qu’elles veulent supporter tout Swagger. Je n’avais pas d’intérêt à intégrer des endpoints ni à générer des serveurs go, et encore moins des clients. Mon objectif était simple : créer des types pour travailler avec des APIs décrites en Swagger, et je pense que c’est très réussi dans son objectif. Je suis sûr qu’il y a des bugs et quelques erreurs d’interprétation. J’ajouterai davantage de flags au fil du temps pour modifier le comportement et permettre des interprétations libres des parties peu strictes.
N’hésitez pas à envoyer des bugs, des commentaires et des PRs, ils seront bien reçus.