Last active
December 15, 2022 18:07
-
-
Save wizpig64/419c78739c40c552c81881b59983d511 to your computer and use it in GitHub Desktop.
django management command to dump API metadata
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import json | |
from pathlib import Path | |
from django.core.management.base import BaseCommand | |
from django.utils.module_loading import import_string | |
from rest_framework.fields import Field | |
from rest_framework.metadata import SimpleMetadata | |
from rest_framework.routers import BaseRouter as Router | |
from rest_framework.schemas.openapi import AutoSchema | |
class ExtendedMetadata(SimpleMetadata): | |
"""Adds additional field metadata that can be useful for building front ends.""" | |
extra_validators = ["format", "pattern"] | |
def get_field_info(self, field: Field): | |
field_info = super().get_field_info(field) | |
# add extra validators from the OpenAPI schema generator. | |
schema = {} | |
AutoSchema()._map_field_validators(field, schema) | |
field_info.update( | |
(validator, schema[validator]) | |
for validator in self.extra_validators | |
if validator in schema | |
) | |
# add additional data from serializer. | |
field_info["initial"] = field.initial | |
field_info["field_name"] = field.field_name | |
field_info["write_only"] = field.write_only | |
return field_info | |
class Command(BaseCommand): | |
""" | |
Dumps a DRF Router's field options into JSON files. | |
This command was inspired by John Franey of NepFin Engineering: | |
https://medium.com/nepfin-engineering/d103cf416e23 | |
Requires you to have a DRF router instance defined somewhere in your project. | |
""" | |
help = "Dump API metadata options into json files." | |
metadata_generator = ExtendedMetadata() | |
def add_arguments(self, parser): | |
parser.add_argument( | |
"router", | |
nargs="?", | |
default="common.api.router", | |
type=import_string, | |
help="Dotted import path to a DRF router instance.", | |
) | |
parser.add_argument( | |
"dump_dir", | |
nargs="?", | |
default="common/js/options/", | |
type=Path, | |
help="The directory into which options shall be dumped.", | |
) | |
def handle(self, router: Router, dump_dir: Path, **options): | |
# iterate through the router's registry, looking for serializers. | |
known_dump_files = [] | |
for prefix, viewset, basename in router.registry: | |
serializer_class = self.get_serializer_class(viewset) | |
# if no serializer found, skip this viewset. | |
if serializer_class is None: | |
self.stdout.write(f"Skipping {viewset.__name__}, no serializer found.") | |
continue | |
# ensure dump_dir exists, just before the first dump. | |
if not known_dump_files: | |
dump_dir.mkdir(parents=True, exist_ok=True) | |
# dump extended viewset options into a json file. | |
dump_file = dump_dir / (prefix + ".json") | |
known_dump_files.append(dump_file) | |
self.stdout.write(f"Dumping options for {viewset.__name__} to {dump_file}") | |
metadata = self.metadata_generator.get_serializer_info(serializer_class()) | |
dump_file.write_text(json.dumps(metadata, indent=2, sort_keys=True)) | |
# avoid printing *nothing* to the console. | |
if not known_dump_files: | |
self.stdout.write(f"No serializers found, no options dumped.") | |
# alert user if any .json files exist in dump_dir that we don't know about. | |
if dump_dir.exists(): | |
for file in dump_dir.iterdir(): | |
if file.suffix == ".json" and file not in known_dump_files: | |
self.stdout.write(f"Warning: Unknown options {file} exists!") | |
@staticmethod | |
def get_serializer_class(viewset): | |
"""Get the viewset's serializer in a robust way.""" | |
view = viewset() | |
# this code was originally from rest_framework/filters.py, lines 209-217: | |
if hasattr(view, "get_serializer_class"): | |
try: | |
return view.get_serializer_class() | |
except AssertionError: | |
# Raised by the default implementation if | |
# no serializer_class was found | |
return None | |
else: | |
return getattr(view, "serializer_class", None) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment