diff --git a/tests/test_casing.py b/tests/test_casing.py index fb7bd89..57ea9be 100644 --- a/tests/test_casing.py +++ b/tests/test_casing.py @@ -1,6 +1,6 @@ import pytest -from worf.casing import camel_to_snake, snake_to_camel, whitespace_to_camel +from worf.casing import camel_to_snake, snake_to_camel from worf.exceptions import NamingThingsError @@ -39,7 +39,3 @@ def test_camel_to_snake_catches_invalid_chars(): def test_snake_to_camel_catches_invalid_chars(): with pytest.raises(NamingThingsError): snake_to_camel("this_aint_no_🐍") - - -def test_whitespace_to_camel(): - assert "thisIsAVerboseName" == whitespace_to_camel("This is a verbose name") diff --git a/worf/casing.py b/worf/casing.py index 414fa1c..d428472 100644 --- a/worf/casing.py +++ b/worf/casing.py @@ -37,12 +37,3 @@ def camel_to_snake(camel): last_was_upper = value.isupper() return snake - - -def whitespace_to_camel(string): - pos = string.find(" ") - if pos == -1: - return string[:1].lower() + string[1:] - - new_string = string[:pos] + string[pos + 1 :].capitalize() - return whitespace_to_camel(new_string) diff --git a/worf/validators.py b/worf/validators.py index 6cdbaaf..1c333ab 100644 --- a/worf/validators.py +++ b/worf/validators.py @@ -8,7 +8,6 @@ from django.utils.dateparse import parse_datetime from worf.exceptions import NotImplementedInWorfYet -from worf.casing import snake_to_camel class ValidationMixin: @@ -36,7 +35,7 @@ def _validate_boolean(self, key): if not isinstance(coerced, bool): raise ValidationError( - f"Field {snake_to_camel(key)} accepts a boolean, got {value}, coerced to {coerced}" + f"Field {self.keymap[key]} accepts a boolean, got {value}, coerced to {coerced}" ) return coerced @@ -58,7 +57,7 @@ def _validate_datetime(self, key): if not isinstance(coerced, datetime): raise ValidationError( - f"Field {snake_to_camel(key)} accepts a iso datetime string, got {value}, coerced to {coerced}" + f"Field {self.keymap[key]} accepts a iso datetime string, got {value}, coerced to {coerced}" ) return coerced @@ -68,18 +67,18 @@ def _validate_many_to_many(self, key): if not isinstance(value, list): raise ValidationError( - f"Field {snake_to_camel(key)} accepts an array, got {type(value)} {value}" + f"Field {self.keymap[key]} accepts an array, got {type(value)} {value}" ) def _validate_string(self, key, max_length): value = self.bundle[key] if not isinstance(value, str): - raise ValidationError(f"Field {snake_to_camel(key)} accepts string") + raise ValidationError(f"Field {self.keymap[key]} accepts string") if max_length is not None and len(value) > max_length: raise ValidationError( - f"Field {snake_to_camel(key)} accepts a maximum of {max_length} characters" + f"Field {self.keymap[key]} accepts a maximum of {max_length} characters" ) return value @@ -93,7 +92,7 @@ def _validate_int(self, key): try: integer = int(value) except (TypeError, ValueError): - raise ValidationError(f"Field {snake_to_camel(key)} accepts an integer") + raise ValidationError(f"Field {self.keymap[key]} accepts an integer") return integer @@ -102,7 +101,7 @@ def _validate_positive_int(self, key): if integer < 0: raise ValidationError( - f"Field {snake_to_camel(key)} accepts a positive integer" + f"Field {self.keymap[key]} accepts a positive integer" ) return integer @@ -113,7 +112,7 @@ def coerce_array_of_integers(self, key): try: self.bundle[key] = [int(id) for id in self.bundle[key]] except ValueError: - message = f"Field {snake_to_camel(key)} accepts an array of integers. Got {self.bundle[key]} instead." + message = f"Field {self.keymap[key]} accepts an array of integers. Got {self.bundle[key]} instead." raise ValidationError(message + " I couldn't coerce the values.") def validate_int(self, value): @@ -167,7 +166,7 @@ def validate_bundle(self, key): serializer = self.get_serializer() if self.request.method in ("PATCH", "PUT") and key not in serializer.write(): - message = f"{snake_to_camel(key)} is not editable" + message = f"{self.keymap[key]} is not editable" if settings.DEBUG: message += f":: {serializer}" raise ValidationError(message) @@ -179,7 +178,7 @@ def validate_bundle(self, key): ) if not hasattr(self.model, key) and not annotation: - raise ValidationError(f"{snake_to_camel(key)} does not exist") + raise ValidationError(f"{self.keymap[key]} does not exist") if key not in self.secure_fields and isinstance(self.bundle[key], str): self.bundle[key] = self.bundle[key].strip() @@ -231,7 +230,7 @@ def validate_bundle(self, key): # try: # json.loads(self.bundle[key]) # except ValueError: - # raise ValidationError(f"Field {snake_to_camel(key)} requires valid JSON") + # raise ValidationError(f"Field {self.keymap[key]} requires valid JSON") else: message = f"{field.get_internal_type()} has no validation method for {key}" diff --git a/worf/views/base.py b/worf/views/base.py index 4712cee..6d63563 100644 --- a/worf/views/base.py +++ b/worf/views/base.py @@ -17,7 +17,7 @@ from django.views.decorators.cache import never_cache from django.utils.decorators import method_decorator -from worf.casing import camel_to_snake, whitespace_to_camel +from worf.casing import camel_to_snake, snake_to_camel from worf.exceptions import HTTP_EXCEPTIONS, HTTP404, HTTP422, PermissionsException from worf.serializers import LegacySerializer from worf.validators import ValidationMixin @@ -89,7 +89,8 @@ def __init__(self, *args, **kwargs): def name(self): if isinstance(self.payload_key, str): return self.payload_key - return whitespace_to_camel(self.model._meta.verbose_name_plural) + verbose_name_plural = self.model._meta.verbose_name_plural + return snake_to_camel(verbose_name_plural.replace(" ", "_").lower()) def _check_permissions(self): """Return a permissions exception when in debug mode instead of 404.""" @@ -185,31 +186,24 @@ def set_bundle_from_querystring(self): # parse_qs gives us a dictionary where all values are lists qs = parse_qs(self.request.META["QUERY_STRING"]) - # TODO: TLDR; Switch to POST for search instead of GET/querystring params. - # we want to preserve strings wherever there are not duplicate keys - # Step through the list and construct a dictionary for all fields - # that are not duplicated - - # fundamentally all urlparams are treated as arrays natively. - # we can't enforce type coersion here... - - # we can't assume everything is an array or everything is not an array - # when it's a querystring - raw_bundle = {} + for key, value in qs.items(): raw_bundle[key] = value[0] if len(value) == 1 else value self.set_bundle(raw_bundle) - def set_bundle(self, raw): + def set_bundle(self, raw_bundle): self.bundle = {} - if not raw: + self.keymap = {} + + if not raw_bundle: return # No need to loop or set self.bundle again if it's empty - for key in raw.keys(): + for key in raw_bundle.keys(): field = camel_to_snake(key) - self.bundle[field] = raw[key] + self.bundle[field] = raw_bundle[key] + self.keymap[field] = key def dispatch(self, request, *args, **kwargs): method = request.method.lower() diff --git a/worf/views/create.py b/worf/views/create.py index 61d4795..28bfabb 100644 --- a/worf/views/create.py +++ b/worf/views/create.py @@ -1,6 +1,5 @@ from django.core.exceptions import ValidationError -from worf.casing import snake_to_camel from worf.views.base import AbstractBaseAPI @@ -27,7 +26,5 @@ def validate(self): # this should be moved into validate bundle if create_fields and key not in create_fields: raise ValidationError( - "{} not allowed when creating {}".format( - snake_to_camel(key), self.name - ) + f"{self.keymap[key]} not allowed when creating {self.name}" ) diff --git a/worf/views/detail.py b/worf/views/detail.py index 57b9542..4e966c6 100644 --- a/worf/views/detail.py +++ b/worf/views/detail.py @@ -2,7 +2,6 @@ from django.db import models from django.db.utils import IntegrityError -from worf.casing import snake_to_camel from worf.views.base import AbstractBaseAPI @@ -46,14 +45,14 @@ def set_foreign_key(self, instance, key): else None ) except related_model.DoesNotExist as e: - raise ValidationError(f"Invalid {snake_to_camel(key)}") from e + raise ValidationError(f"Invalid {self.keymap[key]}") from e setattr(instance, key, related_instance) def set_many_to_many(self, instance, key): try: getattr(instance, key).set(self.bundle[key]) except (IntegrityError, ValueError) as e: - raise ValidationError(f"Invalid {snake_to_camel(key)}") from e + raise ValidationError(f"Invalid {self.keymap[key]}") from e def set_many_to_many_with_through(self, instance, key): try: @@ -83,7 +82,7 @@ def set_many_to_many_with_through(self, instance, key): ] ) except (AttributeError, IntegrityError, ValueError) as e: - raise ValidationError(f"Invalid {snake_to_camel(key)}") from e + raise ValidationError(f"Invalid {self.keymap[key]}") from e def update(self): instance = self.get_instance() @@ -119,7 +118,7 @@ def validate(self): field = self.model._meta.get_field(key) if self.bundle[key] is None and not field.null: - raise ValidationError(f"Invalid {snake_to_camel(key)}") + raise ValidationError(f"Invalid {self.keymap[key]}") class DetailUpdateAPI(DetailAPI):