diff --git a/python-rapidjson/docstrings.h b/python-rapidjson/docstrings.h index 5d25acf..6c904dd 100644 --- a/python-rapidjson/docstrings.h +++ b/python-rapidjson/docstrings.h @@ -6,14 +6,14 @@ PyDoc_STRVAR(rapidjson_module_docstring, PyDoc_STRVAR(rapidjson_loads_docstring, "loads(s, object_hook=None, use_decimal=False, precise_float=True," - " allow_nan=True, datetime_mode=None)\n" + " allow_nan=True, datetime_mode=None, uuid_mode=None)\n" "\n" - "Decodes a JSON string into Python object.\n"); + "Decodes a JSON string into Python object."); PyDoc_STRVAR(rapidjson_dumps_docstring, "dumps(obj, skipkeys=False, ensure_ascii=True, allow_nan=True, indent=None," " default=None, sort_keys=False, use_decimal=False, max_recursion_depth=2048," - " datetime_mode=None)\n" + " datetime_mode=None, uuid_mode=None)\n" "\n" "Encodes Python object into a JSON string."); diff --git a/python-rapidjson/rapidjson.cpp b/python-rapidjson/rapidjson.cpp index 2e45361..b58f3c1 100644 --- a/python-rapidjson/rapidjson.cpp +++ b/python-rapidjson/rapidjson.cpp @@ -21,6 +21,7 @@ using namespace rapidjson; static PyObject* rapidjson_decimal_type = NULL; static PyObject* rapidjson_timezone_type = NULL; static PyObject* rapidjson_timezone_utc = NULL; +static PyObject* rapidjson_uuid_type = NULL; struct HandlerContext { PyObject* object; @@ -49,16 +50,24 @@ days_per_month(int year, int month) { return 28; } +enum UuidMode { + UUID_MODE_NONE = 0, + UUID_MODE_CANONICAL = 1, // only 4-dashed 32 hex chars: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + UUID_MODE_HEX = 2 // canonical OR 32 hex chars +}; + struct PyHandler { int useDecimal; int allowNan; PyObject* root; PyObject* objectHook; DatetimeMode datetimeMode; + UuidMode uuidMode; std::vector stack; - PyHandler(int ud, PyObject* hook, int an, DatetimeMode dm) - : useDecimal(ud), allowNan(an), root(NULL), objectHook(hook), datetimeMode(dm) + PyHandler(int ud, PyObject* hook, int an, DatetimeMode dm, UuidMode um) + : useDecimal(ud), allowNan(an), root(NULL), objectHook(hook), datetimeMode(dm), + uuidMode(um) { stack.reserve(128); } @@ -652,13 +661,48 @@ struct PyHandler { #undef digit + bool IsUuid(const char* str, SizeType length) { + if (uuidMode == UUID_MODE_HEX && length == 32) { + for (int i = length - 1; i >= 0; --i) + if (!isxdigit(str[i])) + return false; + return true; + } else if (length == 36 + && str[8] == '-' && str[13] == '-' + && str[18] == '-' && str[23] == '-') { + for (int i = length - 1; i >= 0; --i) + if (i != 8 && i != 13 && i != 18 && i != 23 && !isxdigit(str[i])) + return false; + return true; + } + return false; + } + + bool HandleUuid(const char* str, SizeType length) { + PyObject* pystr = PyUnicode_FromStringAndSize(str, length); + if (pystr == NULL) + return false; + + PyObject* value = PyObject_CallFunctionObjArgs(rapidjson_uuid_type, pystr, NULL); + Py_DECREF(pystr); + + if (value == NULL) + return false; + else + return HandleSimpleType(value); + } + bool String(const char* str, SizeType length, bool copy) { + PyObject* value; + if (datetimeMode != DATETIME_MODE_NONE && IsIso8601(str, length)) return HandleIso8601(str, length); - else { - PyObject* value = PyUnicode_FromStringAndSize(str, length); - return HandleSimpleType(value); - } + + if (uuidMode != UUID_MODE_NONE && IsUuid(str, length)) + return HandleUuid(str, length); + + value = PyUnicode_FromStringAndSize(str, length); + return HandleSimpleType(value); } }; @@ -674,6 +718,8 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs) int allowNan = 1; PyObject* datetimeModeObj = NULL; DatetimeMode datetimeMode = DATETIME_MODE_NONE; + PyObject* uuidModeObj = NULL; + UuidMode uuidMode = UUID_MODE_NONE; static char* kwlist[] = { "s", @@ -682,17 +728,19 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs) "precise_float", "allow_nan", "datetime_mode", + "uuid_mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OpppO:rapidjson.loads", + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OpppOO:rapidjson.loads", kwlist, &jsonObject, &objectHook, &useDecimal, &preciseFloat, &allowNan, - &datetimeModeObj)) + &datetimeModeObj, + &uuidModeObj)) if (objectHook && !PyCallable_Check(objectHook)) { PyErr_SetString(PyExc_TypeError, "object_hook is not callable"); @@ -725,10 +773,18 @@ rapidjson_loads(PyObject* self, PyObject* args, PyObject* kwargs) } } + if (uuidModeObj && PyLong_Check(uuidModeObj)) { + uuidMode = (UuidMode) PyLong_AsLong(uuidModeObj); + if (uuidMode < UUID_MODE_NONE || uuidMode > UUID_MODE_HEX) { + PyErr_SetString(PyExc_ValueError, "Invalid uuid_time"); + return NULL; + } + } + char* jsonStrCopy = (char*) malloc(sizeof(char) * (jsonStrLen+1)); memcpy(jsonStrCopy, jsonStr, jsonStrLen+1); - PyHandler handler(useDecimal, objectHook, allowNan, datetimeMode); + PyHandler handler(useDecimal, objectHook, allowNan, datetimeMode, uuidMode); Reader reader; InsituStringStream ss(jsonStrCopy); @@ -807,7 +863,8 @@ rapidjson_dumps_internal( int sortKeys, int useDecimal, unsigned maxRecursionDepth, - DatetimeMode datetimeMode) + DatetimeMode datetimeMode, + UuidMode uuidMode) { int isDec; std::vector stack; @@ -1105,6 +1162,20 @@ rapidjson_dumps_internal( snprintf(isoformat, ISOFORMAT_LEN-1, "%04d-%02d-%02d", year, month, day); writer->String(isoformat); } + else if (uuidMode != UUID_MODE_NONE + && PyObject_TypeCheck(object, (PyTypeObject *) rapidjson_uuid_type)) { + PyObject* retval; + if (uuidMode == UUID_MODE_CANONICAL) + retval = PyObject_Str(object); + else + retval = PyObject_GetAttrString(object, "hex"); + if (retval == NULL) + goto error; + + // Decref the return value once it's done being dumped to a string. + stack.push_back(WriterContext(NULL, NULL, false, currentLevel, retval)); + stack.push_back(WriterContext(NULL, retval, false, currentLevel)); + } else if (defaultFn) { PyObject* retval = PyObject_CallFunctionObjArgs(defaultFn, object, NULL); if (retval == NULL) @@ -1140,7 +1211,8 @@ rapidjson_dumps_internal( sortKeys, \ useDecimal, \ maxRecursionDepth, \ - datetimeMode) + datetimeMode, \ + uuidMode) static PyObject* @@ -1159,10 +1231,12 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs) unsigned maxRecursionDepth = MAX_RECURSION_DEPTH; PyObject* datetimeModeObj = NULL; DatetimeMode datetimeMode = DATETIME_MODE_NONE; + PyObject* uuidModeObj = NULL; + UuidMode uuidMode = UUID_MODE_NONE; bool prettyPrint = false; const char indentChar = ' '; - unsigned char indentCharCount = 4; + unsigned indentCharCount = 4; static char* kwlist[] = { "obj", @@ -1175,9 +1249,10 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs) "use_decimal", "max_recursion_depth", "datetime_mode", + "uuid_mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pppOOppIO:rapidjson.dumps", + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|pppOOppIOO:rapidjson.dumps", kwlist, &value, &skipKeys, @@ -1188,7 +1263,8 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs) &sortKeys, &useDecimal, &maxRecursionDepth, - &datetimeModeObj)) + &datetimeModeObj, + &uuidModeObj)) return NULL; if (defaultFn && !PyCallable_Check(defaultFn)) { @@ -1216,6 +1292,14 @@ rapidjson_dumps(PyObject* self, PyObject* args, PyObject* kwargs) } } + if (uuidModeObj && PyLong_Check(uuidModeObj)) { + uuidMode = (UuidMode) PyLong_AsLong(uuidModeObj); + if (uuidMode < UUID_MODE_NONE || uuidMode > UUID_MODE_HEX) { + PyErr_SetString(PyExc_ValueError, "Invalid uuid_time"); + return NULL; + } + } + if (!prettyPrint) { if (ensureAscii) { GenericStringBuffer > buf; @@ -1280,24 +1364,60 @@ PyInit_rapidjson() return NULL; } + PyObject* uuidModule = PyImport_ImportModule("uuid"); + if (uuidModule == NULL) { + Py_DECREF(rapidjson_timezone_type); + Py_DECREF(rapidjson_timezone_utc); + return NULL; + } + + rapidjson_uuid_type = PyObject_GetAttrString(uuidModule, "UUID"); + Py_DECREF(uuidModule); + + if (rapidjson_uuid_type == NULL) { + Py_DECREF(rapidjson_timezone_type); + Py_DECREF(rapidjson_timezone_utc); + return NULL; + } + PyObject* decimalModule = PyImport_ImportModule("decimal"); - if (decimalModule == NULL) + if (decimalModule == NULL) { + Py_DECREF(rapidjson_timezone_type); + Py_DECREF(rapidjson_timezone_utc); + Py_DECREF(rapidjson_uuid_type); return NULL; + } rapidjson_decimal_type = PyObject_GetAttrString(decimalModule, "Decimal"); Py_DECREF(decimalModule); + if (rapidjson_decimal_type == NULL) { + Py_DECREF(rapidjson_timezone_type); + Py_DECREF(rapidjson_timezone_utc); + Py_DECREF(rapidjson_uuid_type); + return NULL; + } + PyObject* module; module = PyModule_Create(&rapidjson_module); - if (module == NULL) + if (module == NULL) { + Py_DECREF(rapidjson_timezone_type); + Py_DECREF(rapidjson_timezone_utc); + Py_DECREF(rapidjson_decimal_type); + Py_DECREF(rapidjson_uuid_type); return NULL; + } PyModule_AddIntConstant(module, "DATETIME_MODE_NONE", DATETIME_MODE_NONE); PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601", DATETIME_MODE_ISO8601); PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601_IGNORE_TZ", DATETIME_MODE_ISO8601_IGNORE_TZ); PyModule_AddIntConstant(module, "DATETIME_MODE_ISO8601_UTC", DATETIME_MODE_ISO8601_UTC); + PyModule_AddIntConstant(module, "UUID_MODE_NONE", UUID_MODE_NONE); + PyModule_AddIntConstant(module, "UUID_MODE_HEX", UUID_MODE_HEX); + PyModule_AddIntConstant(module, "UUID_MODE_CANONICAL", UUID_MODE_CANONICAL); + PyModule_AddStringConstant(module, "__version__", PYTHON_RAPIDJSON_VERSION); PyModule_AddStringConstant( module, diff --git a/tests/test_params.py b/tests/test_params.py index 717f692..be3ef10 100644 --- a/tests/test_params.py +++ b/tests/test_params.py @@ -1,5 +1,7 @@ from datetime import date, datetime, time import math +import uuid + import pytest import rapidjson @@ -244,6 +246,47 @@ def test_datetime_values(value): assert loaded == value +@pytest.mark.unit +def test_uuid_mode(): + assert rapidjson.UUID_MODE_NONE == 0 + assert rapidjson.UUID_MODE_CANONICAL == 1 + assert rapidjson.UUID_MODE_HEX == 2 + + value = uuid.uuid1() + with pytest.raises(TypeError): + rapidjson.dumps(value) + + with pytest.raises(ValueError): + rapidjson.dumps(value, uuid_mode=42) + + with pytest.raises(ValueError): + rapidjson.loads('""', uuid_mode=42) + + dumped = rapidjson.dumps(value, uuid_mode=rapidjson.UUID_MODE_CANONICAL) + loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_CANONICAL) + assert loaded == value + + # When loading, hex mode implies canonical format + loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_HEX) + assert loaded == value + + dumped = rapidjson.dumps(value, uuid_mode=rapidjson.UUID_MODE_HEX) + loaded = rapidjson.loads(dumped, uuid_mode=rapidjson.UUID_MODE_HEX) + assert loaded == value + + +@pytest.mark.unit +def test_uuid_and_datetime_mode_together(): + value = [date.today(), uuid.uuid1()] + dumped = rapidjson.dumps(value, + datetime_mode=rapidjson.DATETIME_MODE_ISO8601, + uuid_mode=rapidjson.UUID_MODE_CANONICAL) + loaded = rapidjson.loads(dumped, + datetime_mode=rapidjson.DATETIME_MODE_ISO8601, + uuid_mode=rapidjson.UUID_MODE_CANONICAL) + assert loaded == value + + @pytest.mark.unit @pytest.mark.parametrize( 'value,cls', [ @@ -293,6 +336,33 @@ def test_datetime_iso8601(value, cls): assert isinstance(result, cls) +@pytest.mark.unit +@pytest.mark.parametrize( + 'value,cls', [ + ('7a683da49aa011e5972e3085a99ccac7', str), + ('7a683da4 9aa0-11e5-972e-3085a99ccac7', str), + ('za683da4-9aa0-11e5-972e-3085a99ccac7', str), + + ('7a683da4-9aa0-11e5-972e-3085a99ccac7', uuid.UUID), + ]) +def test_uuid_canonical(value, cls): + result = rapidjson.loads('"%s"' % value, uuid_mode=rapidjson.UUID_MODE_CANONICAL) + assert isinstance(result, cls), type(result) + + +@pytest.mark.unit +@pytest.mark.parametrize( + 'value,cls', [ + ('za683da49aa011e5972e3085a99ccac7', str), + + ('7a683da49aa011e5972e3085a99ccac7', uuid.UUID), + ('7a683da4-9aa0-11e5-972e-3085a99ccac7', uuid.UUID), + ]) +def test_uuid_hex(value, cls): + result = rapidjson.loads('"%s"' % value, uuid_mode=rapidjson.UUID_MODE_HEX) + assert isinstance(result, cls), type(result) + + @pytest.mark.unit def test_precise_float(): f = "1.234567890E+34"