import re from typing import Any, Collection, MutableMapping from django.utils.module_loading import import_string from drf_orjson_renderer.renderers import ORJSONRenderer from evibes.settings.base import MIDDLEWARE from evibes.settings.drf import JSON_UNDERSCOREIZE camelize_re = re.compile(r"[a-z0-9]?_[a-z0-9]") def underscore_to_camel(match): group = match.group() if len(group) == 3: return group[0] + group[2].upper() else: return group[1].upper() def _camelize_key(key: str) -> str: if not isinstance(key, str) or not key: return key if "_" not in key: return key parts = key.split("_") first = parts[0] rest = [p.capitalize() if p else "" for p in parts[1:]] return first + "".join(rest) def camelize(obj: Any) -> Any: if isinstance(obj, dict): return { (_camelize_key(k) if isinstance(k, str) else k): camelize(v) for k, v in obj.items() } if isinstance(obj, list): return [camelize(v) for v in obj] if isinstance(obj, tuple): return tuple(camelize(v) for v in obj) return obj def camelize_serializer_fields(result, generator, request, public): ignore_fields: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_fields", ()) ignore_keys: Collection[Any] = JSON_UNDERSCOREIZE.get("ignore_keys", ()) def has_middleware_installed(): try: from evibes.middleware import CamelCaseMiddleWare except ImportError: return False return any( isinstance(m, type) and issubclass(m, CamelCaseMiddleWare) for m in map(import_string, MIDDLEWARE) ) def camelize_str(key: str) -> str: new_key = re.sub(camelize_re, underscore_to_camel, key) if "_" in key else key if key in ignore_keys or new_key in ignore_keys: return key return new_key def camelize_component( schema: MutableMapping, name: str | None = None ) -> MutableMapping: if name is not None and ( name in ignore_fields or camelize_str(name) in ignore_fields ): return schema elif schema.get("type") == "object": if "properties" in schema: schema["properties"] = { camelize_str(field_name): camelize_component( field_schema, field_name ) for field_name, field_schema in schema["properties"].items() } if "required" in schema: schema["required"] = [ camelize_str(field) for field in schema["required"] ] elif schema.get("type") == "array" and isinstance( schema["items"], MutableMapping ): camelize_component(schema["items"]) return schema for (_, component_type), component in generator.registry._components.items(): if component_type == "schemas": camelize_component(component.schema) if has_middleware_installed(): for url_schema in result["paths"].values(): for method_schema in url_schema.values(): for parameter in method_schema.get("parameters", []): parameter["name"] = camelize_str(parameter["name"]) return result class CamelCaseRenderer(ORJSONRenderer): def render( self, data: Any, media_type: str | None = None, renderer_context: Any = None ) -> bytes: # ty:ignore[invalid-method-override] if data is None: return b"" ctx = renderer_context or {} camelize_disabled = ctx.get("camelize", True) is False payload = data if camelize_disabled else camelize(data) return super().render(payload, media_type=media_type, renderer_context=ctx) __all__ = ["CamelCaseRenderer", "camelize"]