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 3897a52..87cb741 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() @@ -234,7 +233,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 4ab390f..8be2b19 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 2b4c956..e4bb2ce 100644 --- a/worf/views/create.py +++ b/worf/views/create.py @@ -1,3 +1,5 @@ +from django.core.exceptions import ValidationError + from worf.assigns import AssignAttributes from worf.views.base import AbstractBaseAPI @@ -17,3 +19,20 @@ def post(self, request, *args, **kwargs): new_instance = self.create() serializer = self.get_serializer() return self.render_to_response(serializer.read(new_instance), 201) + + def create(self): + self.validate() + + return self.model.objects.create(**self.bundle) + + def validate(self): + create_fields = self.get_serializer().create() + + for key in self.bundle.keys(): + self.validate_bundle(key) + # ignore create_fields for now if it's empty + # this should be moved into validate bundle + if create_fields and key not in create_fields: + raise ValidationError( + f"{self.keymap[key]} not allowed when creating {self.name}" + )