Convert built-in types when passed as strings

If on a service exposed with some arguments with a built-in type among
int, long, bool or float a request is made passing a JSON with string
values instead of the intended type, the called function would have
strings as parameters instead of the expected types. This also means
that invalid strings would still be passed without error, leading to
unexpected failures. This patch tries to convert the string to the
intended type before failing with an InvalidInput exception if the
string can't be converted. This is to try and be as nice as possible
with whatever input is thrown at wsme.

Closes-Bug: 1450544
Change-Id: I705c183bb68457d539074b78ce81339b9464e1e0
This commit is contained in:
Stéphane Bisinger 2015-05-04 18:41:32 +02:00
parent eb37037f54
commit 9a0d3c1461
5 changed files with 154 additions and 3 deletions

View File

@ -27,6 +27,8 @@ accept_content_types = [
'text/javascript',
'application/javascript'
]
ENUM_TRUE = ('true', 't', 'yes', 'y', 'on', '1')
ENUM_FALSE = ('false', 'f', 'no', 'n', 'off', '0')
@generic
@ -182,6 +184,28 @@ def text_fromjson(datatype, value):
return value
@fromjson.when_object(*six.integer_types + (float,))
def numeric_fromjson(datatype, value):
"""Convert string object to built-in types int, long or float."""
if value is None:
return None
return datatype(value)
@fromjson.when_object(bool)
def bool_fromjson(datatype, value):
"""Convert to bool, restricting strings to just unambiguous values."""
if value is None:
return None
if isinstance(value, six.integer_types + (bool,)):
return bool(value)
if value in ENUM_TRUE:
return True
if value in ENUM_FALSE:
return False
raise ValueError("Value not an unambiguous boolean: %s" % value)
@fromjson.when_object(decimal.Decimal)
def decimal_fromjson(datatype, value):
if value is None:

View File

@ -65,6 +65,11 @@ class NamedAttrsObject(object):
attr_2 = wsme.types.wsattr(int, name='attr.2')
class CustomObject(object):
aint = int
name = wsme.types.text
class NestedInnerApi(object):
@expose(bool)
def deepfunction(self):
@ -166,6 +171,10 @@ class ArgTypes(object):
if not (a == b):
raise AssertionError('%s != %s' % (a, b))
def assertIsInstance(self, value, v_type):
assert isinstance(value, v_type), ("%s is not instance of type %s" %
(value, v_type))
@expose(wsme.types.bytes)
@validate(wsme.types.bytes)
def setbytes(self, value):
@ -307,6 +316,14 @@ class ArgTypes(object):
self.assertEquals(value.attr_2, 20)
return value
@expose(CustomObject)
@validate(CustomObject)
def setcustomobject(self, value):
self.assertIsInstance(value, CustomObject)
self.assertIsInstance(value.name, wsme.types.text)
self.assertIsInstance(value.aint, int)
return value
class BodyTypes(object):
def assertEquals(self, a, b):

View File

@ -13,7 +13,7 @@ from wsme.rest.json import fromjson, tojson, parse
from wsme.utils import parse_isodatetime, parse_isotime, parse_isodate
from wsme.types import isarray, isdict, isusertype, register_type
from wsme.rest import expose, validate
from wsme.exc import InvalidInput
from wsme.exc import ClientSideError, InvalidInput
import six
@ -270,6 +270,15 @@ class TestRestJson(wsme.tests.protocol.RestOnlyProtocolTestCase):
"Unknown argument:"
)
def test_set_custom_object(self):
r = self.app.post(
'/argtypes/setcustomobject',
'{"value": {"aint": 2, "name": "test"}}',
headers={"Content-Type": "application/json"}
)
self.assertEqual(r.status_int, 200)
self.assertEqual(r.json, {'aint': 2, 'name': 'test'})
def test_unset_attrs(self):
class AType(object):
attr = int
@ -328,6 +337,107 @@ class TestRestJson(wsme.tests.protocol.RestOnlyProtocolTestCase):
assert e.value == jdate
assert e.msg == "'%s' is not a legal date value" % jdate
def test_valid_str_to_builtin_fromjson(self):
types = six.integer_types + (bool, float)
value = '2'
for t in types:
for ba in True, False:
jd = '%s' if ba else '{"a": %s}'
i = parse(jd % value, {'a': t}, ba)
self.assertEqual(
i, {'a': t(value)},
"Parsed value does not correspond for %s: "
"%s != {'a': %s}" % (
t, repr(i), repr(t(value))
)
)
self.assertIsInstance(i['a'], t)
def test_valid_int_fromjson(self):
value = 2
for ba in True, False:
jd = '%d' if ba else '{"a": %d}'
i = parse(jd % value, {'a': int}, ba)
self.assertEqual(i, {'a': 2})
self.assertIsInstance(i['a'], int)
def test_valid_num_to_float_fromjson(self):
values = 2, 2.3
for v in values:
for ba in True, False:
jd = '%f' if ba else '{"a": %f}'
i = parse(jd % v, {'a': float}, ba)
self.assertEqual(i, {'a': float(v)})
self.assertIsInstance(i['a'], float)
def test_invalid_str_to_buitin_fromjson(self):
types = six.integer_types + (float, bool)
value = '2a'
for t in types:
for ba in True, False:
jd = '"%s"' if ba else '{"a": "%s"}'
try:
parse(jd % value, {'a': t}, ba)
assert False, (
"Value '%s' should not parse correctly for %s." %
(value, t)
)
except ClientSideError as e:
self.assertIsInstance(e, InvalidInput)
self.assertEqual(e.fieldname, 'a')
self.assertEqual(e.value, value)
def test_ambiguous_to_bool(self):
amb_values = ('', 'randomstring', '2', '-32', 'not true')
for value in amb_values:
for ba in True, False:
jd = '"%s"' if ba else '{"a": "%s"}'
try:
parse(jd % value, {'a': bool}, ba)
assert False, (
"Value '%s' should not parse correctly for %s." %
(value, bool)
)
except ClientSideError as e:
self.assertIsInstance(e, InvalidInput)
self.assertEqual(e.fieldname, 'a')
self.assertEqual(e.value, value)
def test_true_strings_to_bool(self):
true_values = ('true', 't', 'yes', 'y', 'on', '1')
for value in true_values:
for ba in True, False:
jd = '"%s"' if ba else '{"a": "%s"}'
i = parse(jd % value, {'a': bool}, ba)
self.assertIsInstance(i['a'], bool)
self.assertTrue(i['a'])
def test_false_strings_to_bool(self):
false_values = ('false', 'f', 'no', 'n', 'off', '0')
for value in false_values:
for ba in True, False:
jd = '"%s"' if ba else '{"a": "%s"}'
i = parse(jd % value, {'a': bool}, ba)
self.assertIsInstance(i['a'], bool)
self.assertFalse(i['a'])
def test_true_ints_to_bool(self):
true_values = (1, 5, -3)
for value in true_values:
for ba in True, False:
jd = '%d' if ba else '{"a": %d}'
i = parse(jd % value, {'a': bool}, ba)
self.assertIsInstance(i['a'], bool)
self.assertTrue(i['a'])
def test_false_ints_to_bool(self):
value = 0
for ba in True, False:
jd = '%d' if ba else '{"a": %d}'
i = parse(jd % value, {'a': bool}, ba)
self.assertIsInstance(i['a'], bool)
self.assertFalse(i['a'])
def test_nest_result(self):
self.root.protocols[0].nest_result = True
r = self.app.get('/returntypes/getint.json')

View File

@ -18,7 +18,7 @@ class TestSpore(unittest.TestCase):
spore = json.loads(spore)
assert len(spore['methods']) == 49, str(len(spore['methods']))
assert len(spore['methods']) == 50, str(len(spore['methods']))
m = spore['methods']['argtypes_setbytesarray']
assert m['path'] == 'argtypes/setbytesarray', m['path']

View File

@ -397,7 +397,7 @@ class TestSOAP(wsme.tests.protocol.ProtocolTestCase):
assert len(sd.ports) == 1
port, methods = sd.ports[0]
self.assertEquals(len(methods), 49)
self.assertEquals(len(methods), 50)
methods = dict(methods)