Skip to content

Commit f936304

Browse files
authored
Add MongoDB Extended JSON 2.0 support (#33)
1 parent 0b1c89b commit f936304

File tree

3 files changed

+123
-27
lines changed

3 files changed

+123
-27
lines changed

src/bsonjs.c

+47-22
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ PyDoc_STRVAR(bsonjs_documentation,
2525
"native libbson functions. https://github.com/mongodb/libbson");
2626

2727
char *
28-
bson_str_to_json(const char *bson, size_t bson_len, size_t *json_len)
28+
bson_str_to_json(const char *bson, size_t bson_len, size_t *json_len, const
29+
int mode)
2930
{
3031
char *json;
3132
const bson_t *b;
@@ -39,8 +40,18 @@ bson_str_to_json(const char *bson, size_t bson_len, size_t *json_len)
3940
bson_reader_destroy(reader);
4041
return NULL;
4142
}
42-
43-
json = bson_as_json(b, json_len);
43+
if (mode == 1) {
44+
json = bson_as_relaxed_extended_json(b, json_len);
45+
} else if (mode == 2) {
46+
json = bson_as_canonical_extended_json(b, json_len);
47+
} else if (mode == 0) {
48+
json = bson_as_json(b, json_len);
49+
} else {
50+
PyErr_SetString(PyExc_ValueError, "The value of mode must be one of: "
51+
"bsonjs.RELAXED, bsonjs.LEGACY, "
52+
"or bsonjs.CANONICAL.");
53+
return NULL;
54+
}
4455

4556
bson_reader_destroy(reader);
4657

@@ -53,7 +64,7 @@ bson_str_to_json(const char *bson, size_t bson_len, size_t *json_len)
5364
}
5465

5566
static PyObject *
56-
_dumps(PyObject *bson)
67+
_dumps(PyObject *bson, int mode)
5768
{
5869
PyObject *rv;
5970
char *bson_str, *json;
@@ -63,7 +74,7 @@ _dumps(PyObject *bson)
6374
bson_str = PyBytes_AS_STRING(bson);
6475
bson_len = PyBytes_GET_SIZE(bson);
6576

66-
json = bson_str_to_json(bson_str, (size_t)bson_len, &json_len);
77+
json = bson_str_to_json(bson_str, (size_t)bson_len, &json_len, mode);
6778
if (!json) {
6879
// error is already set
6980
return NULL;
@@ -77,20 +88,27 @@ _dumps(PyObject *bson)
7788
PyDoc_STRVAR(dump__doc__,
7889
"dump(bson, fp)\n"
7990
"\n"
80-
"Decode the BSON bytes object `bson` to MongoDB Extended JSON strict mode\n"
81-
"written to `fp` (a `.write()`-supporting file-like object).\n"
82-
"This function wraps `bson_as_json` from libbson.");
91+
"Decode the BSON bytes object `bson` to MongoDB Extended JSON 2.0 relaxed\n"
92+
"mode written to `fp` (a `.write()`-supporting file-like object).\n"
93+
"\n"
94+
"Accepts a keyword argument `mode` which can be one of `bsonjs.RELAXED`\n"
95+
"`bsonjs.CANONICAL`, or `bsonjs.LEGACY`. Where `RELAXED` and `CANONICAL` \n"
96+
"correspond to the MongoDB Extended JSON 2.0 modes and `LEGACY` uses libbson's\n"
97+
"legacy JSON format");
8398

8499
static PyObject *
85-
dump(PyObject *self, PyObject *args)
100+
dump(PyObject *self, PyObject *args, PyObject *kwargs)
86101
{
87102
PyObject *bson, *file, *json;
88-
89-
if (!PyArg_ParseTuple(args, "SO", &bson, &file)) {
103+
static char *kwlist[] = {"", "", "mode", NULL};
104+
int mode = 1;
105+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "SO|i", kwlist, &bson,
106+
&file,
107+
&mode)) {
90108
return NULL;
91109
}
92110

93-
json = _dumps(bson);
111+
json = _dumps(bson, mode);
94112
if (!json) {
95113
return NULL;
96114
}
@@ -107,19 +125,23 @@ dump(PyObject *self, PyObject *args)
107125
PyDoc_STRVAR(dumps__doc__,
108126
"dumps(bson) -> str\n"
109127
"\n"
110-
"Decode the BSON bytes object `bson` to MongoDB Extended JSON strict mode.\n"
111-
"This function wraps `bson_as_json` from libbson.");
112-
128+
"Decode the BSON bytes object `bson` to MongoDB Extended JSON 2.0 relaxed\n"
129+
"mode. \n"
130+
"Accepts a keyword argument `mode` which can be one of `bsonjs.RELAXED`\n"
131+
"`bsonjs.CANONICAL`, or `bsonjs.LEGACY`. Where `RELAXED` and `CANONICAL` \n"
132+
"correspond to the MongoDB Extended JSON 2.0 modes and `LEGACY` uses libbson's\n"
133+
"legacy JSON format");
113134
static PyObject *
114-
dumps(PyObject *self, PyObject *args)
135+
dumps(PyObject *self, PyObject *args, PyObject *kwargs)
115136
{
116137
PyObject *bson;
117-
118-
if (!PyArg_ParseTuple(args, "S", &bson)) {
138+
int mode = 1;
139+
static char *kwlist[] = {"", "mode", NULL};
140+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "S|i", kwlist, &bson,
141+
&mode)) {
119142
return NULL;
120143
}
121-
122-
return _dumps(bson);
144+
return _dumps(bson, mode);
123145
}
124146

125147
static PyObject *
@@ -208,8 +230,8 @@ loads(PyObject *self, PyObject *args)
208230
}
209231

210232
static PyMethodDef BsonjsClientMethods[] = {
211-
{"dump", dump, METH_VARARGS, dump__doc__},
212-
{"dumps", dumps, METH_VARARGS, dumps__doc__},
233+
{"dump", dump, METH_VARARGS | METH_KEYWORDS, dump__doc__},
234+
{"dumps", dumps, METH_VARARGS | METH_KEYWORDS, dumps__doc__},
213235
{"load", load, METH_VARARGS, load__doc__},
214236
{"loads", loads, METH_VARARGS, loads__doc__},
215237
{NULL, NULL, 0, NULL}
@@ -244,5 +266,8 @@ PyInit_bsonjs(VOID)
244266
Py_DECREF(module);
245267
INITERROR;
246268
}
269+
PyModule_AddIntConstant(module, "LEGACY", 0);
270+
PyModule_AddIntConstant(module, "RELAXED", 1);
271+
PyModule_AddIntConstant(module, "CANONICAL", 2);
247272
return module;
248273
}

src/bsonjs.h

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
#include <Python.h>
1919

2020
static PyObject *
21-
dump(PyObject *self, PyObject *args);
21+
dump(PyObject *self, PyObject *args, PyObject *kwargs);
2222

2323
static PyObject *
24-
dumps(PyObject *self, PyObject *args);
24+
dumps(PyObject *self, PyObject *args, PyObject *kwargs);
2525

2626
static PyObject *
2727
load(PyObject *self, PyObject *args);
2828

2929
static PyObject *
30-
loads(PyObject *self, PyObject *args);
30+
loads(PyObject *self, PyObject *args);
31+

test/test_bsonjs.py

+72-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import re
1919
import sys
2020
import uuid
21+
import io
2122

2223
import bson
2324
from bson import json_util, EPOCH_AWARE
@@ -56,9 +57,13 @@ def to_bson(obj):
5657
uuid_representation=UuidRepresentation.PYTHON_LEGACY))
5758

5859

59-
def bsonjs_dumps(doc):
60+
def bsonjs_dumps(doc, mode=bsonjs.LEGACY):
6061
"""Provide same API as json_util.dumps"""
61-
return bsonjs.dumps(to_bson(doc))
62+
return bsonjs.dumps(to_bson(doc), mode=mode)
63+
64+
def bsonjs_dump(doc, file, mode=bsonjs.LEGACY):
65+
"""Provide same API as json_util.dumps"""
66+
return bsonjs.dump(to_bson(doc), file, mode=mode)
6267

6368

6469
def bsonjs_loads(json_str):
@@ -287,6 +292,71 @@ def test_load_throws_no_read_attribute(self):
287292
not_file = {}
288293
self.assertRaises(AttributeError, bsonjs.load, not_file)
289294

295+
def test_mode(self):
296+
297+
json_str = '{ "test" : "me" }'
298+
bson_bytes = bsonjs.loads(json_str)
299+
self.assertRaises(ValueError, bsonjs.dumps, bson_bytes, mode=4)
300+
301+
# Test support for passing mode as positional argument
302+
self.assertEqual(
303+
'{ "regex" : { "$regex" : ".*", "$options" : "mx" } }',
304+
bsonjs_dumps({"regex": Regex(".*", re.M | re.X)}, bsonjs.LEGACY))
305+
306+
self.assertEqual(
307+
'{ "regex" : { "$regex" : ".*", "$options" : "mx" } }',
308+
bsonjs_dumps({"regex": Regex(".*", re.M | re.X)},
309+
mode=bsonjs.LEGACY))
310+
self.assertEqual(
311+
'{ "regex" : { "$regularExpression" : { "pattern" : ".*", "options" : "mx" } } }',
312+
bsonjs_dumps({"regex": Regex(".*", re.M | re.X)},
313+
mode=bsonjs.RELAXED))
314+
self.assertEqual('{ "date" : { "$date" : "2020-12-16T00:00:00Z" } }',
315+
bsonjs_dumps({"date": datetime.datetime(2020, 12, 16)},
316+
mode=bsonjs.RELAXED))
317+
self.assertEqual('{ "date" : { "$date" : { "$numberLong" : "1608076800000" } } }',
318+
bsonjs_dumps({"date": datetime.datetime(2020, 12, 16)},
319+
mode=bsonjs.CANONICAL))
320+
321+
# Test dump
322+
with io.StringIO() as f:
323+
bson_bytes = bsonjs.loads('{ "test" : "me" }')
324+
self.assertRaises(ValueError, bsonjs.dump, bson_bytes, f, mode=4)
325+
326+
with io.StringIO() as f:
327+
bsonjs_dump({"regex": Regex(".*", re.M | re.X)},
328+
f, bsonjs.LEGACY)
329+
self.assertEqual(
330+
'{ "regex" : { "$regex" : ".*", "$options" : "mx" } }',
331+
f.getvalue())
332+
333+
with io.StringIO() as f:
334+
bsonjs_dump({"regex": Regex(".*", re.M | re.X)},
335+
f, mode=bsonjs.LEGACY)
336+
self.assertEqual(
337+
'{ "regex" : { "$regex" : ".*", "$options" : "mx" } }',
338+
f.getvalue())
339+
340+
with io.StringIO() as f:
341+
bsonjs_dump({"regex": Regex(".*", re.M | re.X)},
342+
f, mode=bsonjs.RELAXED)
343+
self.assertEqual(
344+
'{ "regex" : { "$regularExpression" : { "pattern" : ".*", "options" : "mx" } } }',
345+
f.getvalue())
346+
347+
with io.StringIO() as f:
348+
bsonjs_dump({"date": datetime.datetime(2020, 12, 16)},
349+
f, mode=bsonjs.RELAXED)
350+
self.assertEqual(
351+
'{ "date" : { "$date" : "2020-12-16T00:00:00Z" } }',
352+
f.getvalue())
353+
354+
with io.StringIO() as f:
355+
bsonjs_dump({"date": datetime.datetime(2020, 12, 16)},
356+
f, mode=bsonjs.CANONICAL)
357+
self.assertEqual(
358+
'{ "date" : { "$date" : { "$numberLong" : "1608076800000" } '
359+
'} }', f.getvalue())
290360

291361
if __name__ == "__main__":
292362
unittest.main()

0 commit comments

Comments
 (0)