This commit is contained in:
Steven Cummings 2014-01-02 14:25:10 -08:00
commit abfb1c0085
4 changed files with 334 additions and 1 deletions

View File

@ -297,6 +297,35 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
---
# Compound Fields
These fields represent compound datatypes, which build on other fields with some additional aspect such collecting multiple elements.
## ListField
A list representation, whose elements are described by a given item field. This means that all elements must meet the definition of
that field. The item field can be another field type (e.g., CharField) or a serializer. If `item_field` is not given, then the
list-items are passed through as-is, and can be anything. Note that in this case, any non-native list elements wouldn't be properly
prepared for data rendering.
**Signature:** `ListField(item_field=None)`
## DictField
A dictionary representation, whose values are described by a given value field. This means that all values must meet the definition of
that field. The value field can be another field type (e.g., CharField) or a serializer. If `value_field` is not given, then the `dict`
values are passed through-as-is, and can be anything. Note that in this case, any non-native `dict` values wouldn't be properly
prepared for data rendering.
Dictionary keys are presumed to be character strings or convertible to such, and so during processing are casted to `unicode`. If
necessary, options for unicode conversion (such as the encoding, or error processing) can be provided to a `DictField`. For more info,
see [py_unicode].
**Signature:** `DictField(value_field=None, unicode_options=None)`
If given, unicode_options must be a dict providing options per the [unicode](http://docs.python.org/2/library/functions.html#unicode)
function.
# Custom fields
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects.
@ -345,3 +374,4 @@ As an example, let's create a field that can be used represent the class name of
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
[iso8601]: http://www.w3.org/TR/NOTE-datetime
[py_unicode]: http://docs.python.org/2/howto/unicode.html

View File

@ -0,0 +1,108 @@
"""
Compound fields for processing values that are lists and dicts of values described by embedded
fields.
"""
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from .fields import WritableField
from rest_framework.compat import six
class ListField(WritableField):
"""
A field whose values are lists of items described by the given item field. The item field can
be another field type (e.g., CharField) or a serializer.
"""
default_error_messages = {
'invalid_type': _('%(value)s is not a list.'),
}
def __init__(self, item_field=None, *args, **kwargs):
super(ListField, self).__init__(*args, **kwargs)
self.item_field = item_field
def to_native(self, obj):
if self.item_field and obj:
return [
self.item_field.to_native(item)
for item in obj
]
return obj
def from_native(self, data):
if self.item_field and data:
return [
self.item_field.from_native(item_data)
for item_data in data
]
return data
def validate(self, value):
super(ListField, self).validate(value)
if not isinstance(value, list):
raise ValidationError(self.error_messages['invalid_type'] % {'value': value})
if self.item_field:
errors = {}
for index, item in enumerate(value):
try:
self.item_field.validate(item)
self.item_field.run_validators(item)
except ValidationError as e:
errors[index] = [e]
if errors:
raise ValidationError(errors)
class DictField(WritableField):
"""
A field whose values are dicts of values described by the given value field. The value field
can be another field type (e.g., CharField) or a serializer.
"""
default_error_messages = {
'invalid_type': _('%(value)s is not a dict.'),
}
def __init__(self, value_field=None, unicode_options=None, *args, **kwargs):
super(DictField, self).__init__(*args, **kwargs)
self.value_field = value_field
self.unicode_options = unicode_options or {}
def to_native(self, obj):
if self.value_field and obj:
return dict(
(six.text_type(key, **self.unicode_options), self.value_field.to_native(value))
for key, value in obj.items()
)
return obj
def from_native(self, data):
if self.value_field and data:
return dict(
(six.text_type(key, **self.unicode_options), self.value_field.from_native(value))
for key, value in data.items()
)
return data
def validate(self, value):
super(DictField, self).validate(value)
if not isinstance(value, dict):
raise ValidationError(self.error_messages['invalid_type'] % {'value': value})
if self.value_field:
errors = {}
for k, v in six.iteritems(value):
try:
self.value_field.validate(v)
self.value_field.run_validators(v)
except ValidationError as e:
errors[k] = [e]
if errors:
raise ValidationError(errors)

View File

@ -331,7 +331,7 @@ class BaseSerializer(WritableField):
return ret
def from_native(self, data, files):
def from_native(self, data, files=None):
"""
Deserialize primitives -> objects.
"""

View File

@ -0,0 +1,195 @@
"""
General serializer field tests.
"""
from datetime import date
import unittest
from django.core.exceptions import ValidationError
from rest_framework import ISO_8601
from rest_framework.compound_fields import DictField
from rest_framework.compound_fields import ListField
from rest_framework.fields import CharField
from rest_framework.fields import DateField
class ListFieldTests(unittest.TestCase):
"""
Tests for the ListField behavior
"""
def test_from_native_no_item_field(self):
"""
When a ListField has no item-field, from_native should return the data it was given
un-processed.
"""
field = ListField()
data = range(5)
obj = field.from_native(data)
self.assertEqual(data, obj)
def test_to_native_no_item_field(self):
"""
When a ListField has no item-field, to_native should return the data it was given
un-processed.
"""
field = ListField()
obj = range(5)
data = field.to_native(obj)
self.assertEqual(obj, data)
def test_from_native_with_item_field(self):
"""
When a ListField has an item-field, from_native should return a list of elements resulting
from the application of the item-field's from_native method to each element of the input
data list.
"""
field = ListField(DateField())
data = ["2000-01-01", "2000-01-02"]
obj = field.from_native(data)
self.assertEqual([date(2000, 1, 1), date(2000, 1, 2)], obj)
def test_to_native_with_item_field(self):
"""
When a ListField has an item-field, to_native should return a list of elements resulting
from the application of the item-field's to_native method to each element of the input
object list.
"""
field = ListField(DateField(format=ISO_8601))
obj = [date(2000, 1, 1), date(2000, 1, 2)]
data = field.to_native(obj)
self.assertEqual(["2000-01-01", "2000-01-02"], data)
def test_missing_required_list(self):
"""
When a ListField requires a value, then validate will raise a ValidationError on a missing
(None) value.
"""
field = ListField()
self.assertRaises(ValidationError, field.validate, None)
def test_validate_non_list(self):
"""
When a ListField is given a non-list value, then validate will raise a ValidationError.
"""
field = ListField()
self.assertRaises(ValidationError, field.validate, 'notAList')
def test_validate_empty_list(self):
"""
When a ListField requires a value, then validate will raise a ValidationError on an empty
value.
"""
field = ListField()
self.assertRaises(ValidationError, field.validate, [])
def test_validate_elements_valid(self):
"""
When a ListField is given a list whose elements are valid for the item-field, then validate
will not raise a ValidationError.
"""
field = ListField(CharField(max_length=5))
try:
field.validate(["a", "b", "c"])
except ValidationError:
self.fail("ValidationError was raised")
def test_validate_elements_invalid(self):
"""
When a ListField is given a list containing elements that are invalid for the item-field,
then validate will raise a ValidationError.
"""
field = ListField(CharField(max_length=5))
self.assertRaises(ValidationError, field.validate, ["012345", "012345"])
class DictFieldTests(unittest.TestCase):
"""
Tests for the DictField behavior
"""
def test_from_native_no_value_field(self):
"""
When a DictField has no value-field, from_native should return the data it was given
un-processed.
"""
field = DictField()
data = {"a": 1, "b": 2}
obj = field.from_native(data)
self.assertEqual(data, obj)
def test_to_native_no_value_field(self):
"""
When a DictField has no value-field, to_native should return the data it was given
un-processed.
"""
field = DictField()
obj = {"a": 1, "b": 2}
data = field.to_native(obj)
self.assertEqual(obj, data)
def test_from_native_with_value_field(self):
"""
When a DictField has an value-field, from_native should return a dict of elements resulting
from the application of the value-field's from_native method to each value of the input
data dict.
"""
field = DictField(DateField())
data = {"a": "2000-01-01", "b": "2000-01-02"}
obj = field.from_native(data)
self.assertEqual({"a": date(2000, 1, 1), "b": date(2000, 1, 2)}, obj)
def test_to_native_with_value_field(self):
"""
When a DictField has an value-field, to_native should return a dict of elements resulting
from the application of the value-field's to_native method to each value of the input
object dict.
"""
field = DictField(DateField(format=ISO_8601))
obj = {"a": date(2000, 1, 1), "b": date(2000, 1, 2)}
data = field.to_native(obj)
self.assertEqual({"a": "2000-01-01", "b": "2000-01-02"}, data)
def test_missing_required_dict(self):
"""
When a DictField requires a value, then validate will raise a ValidationError on a missing
(None) value.
"""
field = DictField()
self.assertRaises(ValidationError, field.validate, None)
def test_validate_non_dict(self):
"""
When a DictField is given a non-dict value, then validate will raise a ValidationError.
"""
field = DictField()
self.assertRaises(ValidationError, field.validate, 'notADict')
def test_validate_empty_dict(self):
"""
When a DictField requires a value, then validate will raise a ValidationError on an empty
value.
"""
field = DictField()
self.assertRaises(ValidationError, field.validate, {})
def test_validate_elements_valid(self):
"""
When a DictField is given a dict whose values are valid for the value-field, then validate
will not raise a ValidationError.
"""
field = DictField(CharField(max_length=5))
try:
field.validate({"a": "a", "b": "b", "c": "c"})
except ValidationError:
self.fail("ValidationError was raised")
def test_validate_elements_invalid(self):
"""
When a DictField is given a dict containing values that are invalid for the value-field,
then validate will raise a ValidationError.
"""
field = DictField(CharField(max_length=5))
self.assertRaises(ValidationError, field.validate, {"a": "012345", "b": "012345"})