Skip to content

Commit a893fb3

Browse files
committed
wrap up JSON format completely and make it compliant with the spec
Signed-off-by: Tudor Plugaru <[email protected]>
1 parent adfee8f commit a893fb3

File tree

3 files changed

+117
-20
lines changed

3 files changed

+117
-20
lines changed

src/cloudevents/core/formats/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
# under the License.
1414

1515

16-
from typing import Protocol, Union
16+
from typing import Callable, Optional, Protocol, Union
1717

1818
from cloudevents.core.base import BaseCloudEvent
1919

2020

2121
class Format(Protocol):
22-
def read(self, data: Union[str, bytes]) -> BaseCloudEvent: ...
22+
def read(self, event_factory: Callable[[dict, Optional[Union[dict, str, bytes]]], BaseCloudEvent], data: Union[str, bytes]) -> BaseCloudEvent: ...
2323

2424
def write(self, event: BaseCloudEvent) -> bytes: ...

src/cloudevents/core/formats/json.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,13 @@
1717
import re
1818
from datetime import datetime
1919
from json import JSONEncoder, dumps, loads
20-
from typing import Any, Final, Pattern, Type, TypeVar, Union
20+
from typing import Any, Callable, Final, Optional, Pattern, Union
2121

2222
from dateutil.parser import isoparse # type: ignore[import-untyped]
2323

2424
from cloudevents.core.base import BaseCloudEvent
2525
from cloudevents.core.formats.base import Format
2626

27-
T = TypeVar("T", bound=BaseCloudEvent)
28-
2927

3028
class _JSONEncoderWithDatetime(JSONEncoder):
3129
"""
@@ -47,13 +45,14 @@ def default(self, obj: Any) -> Any:
4745
class JSONFormat(Format):
4846
CONTENT_TYPE: Final[str] = "application/cloudevents+json"
4947
JSON_CONTENT_TYPE_PATTERN: Pattern[str] = re.compile(
50-
r"^(application|text)\\/([a-zA-Z]+\\+)?json(;.*)*$"
48+
r"^(application|text)/([a-zA-Z0-9\-\.]+\+)?json(;.*)?$"
5149
)
5250

53-
def read(self, event_klass: Type[T], data: Union[str, bytes]) -> T:
51+
def read(self, event_factory: Callable[[dict, Optional[Union[dict, str, bytes]]], BaseCloudEvent], data: Union[str, bytes]) -> BaseCloudEvent:
5452
"""
5553
Read a CloudEvent from a JSON formatted byte string.
5654
55+
:param event_factory: A factory function to create CloudEvent instances.
5756
:param data: The JSON formatted byte array.
5857
:return: The CloudEvent instance.
5958
"""
@@ -67,16 +66,15 @@ def read(self, event_klass: Type[T], data: Union[str, bytes]) -> T:
6766
if "time" in event_attributes:
6867
event_attributes["time"] = isoparse(event_attributes["time"])
6968

70-
event_data: Union[str, bytes] = event_attributes.pop("data", None)
69+
event_data: Union[dict, str, bytes, None] = event_attributes.pop("data", None)
7170
if event_data is None:
7271
event_data_base64 = event_attributes.pop("data_base64", None)
7372
if event_data_base64 is not None:
7473
event_data = base64.b64decode(event_data_base64)
7574

76-
# disable mypy due to https://github.com/python/mypy/issues/9003
77-
return event_klass(event_attributes, event_data) # type: ignore
75+
return event_factory(event_attributes, event_data)
7876

79-
def write(self, event: T) -> bytes:
77+
def write(self, event: BaseCloudEvent) -> bytes:
8078
"""
8179
Write a CloudEvent to a JSON formatted byte string.
8280
@@ -90,12 +88,9 @@ def write(self, event: T) -> bytes:
9088
if isinstance(event_data, (bytes, bytearray)):
9189
event_dict["data_base64"] = base64.b64encode(event_data).decode("utf-8")
9290
else:
93-
datacontenttype = event_dict.get(
94-
"datacontenttype", JSONFormat.CONTENT_TYPE
95-
)
96-
# Should we fail if we can't serialize data to JSON?
91+
datacontenttype = event_dict.get("datacontenttype", "application/json")
9792
if re.match(JSONFormat.JSON_CONTENT_TYPE_PATTERN, datacontenttype):
98-
event_dict["data"] = dumps(event_data)
93+
event_dict["data"] = event_data
9994
else:
10095
event_dict["data"] = str(event_data)
10196

tests/test_core/test_format/test_json.py

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515

1616
from datetime import datetime, timezone
1717

18-
import pytest
19-
2018
from cloudevents.core.formats.json import JSONFormat
2119
from cloudevents.core.v1.event import CloudEvent
2220

@@ -61,7 +59,7 @@ def test_write_cloud_event_to_json_with_data_as_json() -> None:
6159

6260
assert (
6361
result
64-
== '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "time": "2023-10-25T17:09:19.736166Z", "datacontenttype": "application/json", "dataschema": "http://example.com/schema", "subject": "test_subject", "data": "{\'key\': \'value\'}"}'.encode(
62+
== '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "time": "2023-10-25T17:09:19.736166Z", "datacontenttype": "application/json", "dataschema": "http://example.com/schema", "subject": "test_subject", "data": {"key": "value"}}'.encode(
6563
"utf-8"
6664
)
6765
)
@@ -151,7 +149,7 @@ def test_write_cloud_event_to_json_with_no_content_type_set_and_data_as_json() -
151149

152150
assert (
153151
result
154-
== '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "time": "2023-10-25T17:09:19.736166Z", "dataschema": "http://example.com/schema", "subject": "test_subject", "data": "{\'key\': \'value\'}"}'.encode(
152+
== '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "time": "2023-10-25T17:09:19.736166Z", "dataschema": "http://example.com/schema", "subject": "test_subject", "data": {"key": "value"}}'.encode(
155153
"utf-8"
156154
)
157155
)
@@ -215,3 +213,107 @@ def test_read_cloud_event_from_json_with_json_as_data() -> None:
215213
assert result.get_dataschema() == "http://example.com/schema"
216214
assert result.get_subject() == "test_subject"
217215
assert result.get_data() == {"key": "value"}
216+
217+
218+
def test_write_cloud_event_with_extension_attributes() -> None:
219+
attributes = {
220+
"id": "123",
221+
"source": "source",
222+
"type": "type",
223+
"specversion": "1.0",
224+
"customext1": "value1",
225+
"customext2": 123,
226+
}
227+
event = CloudEvent(attributes=attributes, data=None)
228+
formatter = JSONFormat()
229+
result = formatter.write(event)
230+
231+
assert b'"customext1": "value1"' in result
232+
assert b'"customext2": 123' in result
233+
234+
235+
def test_read_cloud_event_with_extension_attributes() -> None:
236+
data = '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "customext1": "value1", "customext2": 123}'.encode("utf-8")
237+
formatter = JSONFormat()
238+
result = formatter.read(CloudEvent, data)
239+
240+
assert result.get_extension("customext1") == "value1"
241+
assert result.get_extension("customext2") == 123
242+
243+
244+
def test_write_cloud_event_with_different_json_content_types() -> None:
245+
test_cases = [
246+
("application/vnd.api+json", {"key": "value"}),
247+
("text/json", {"key": "value"}),
248+
("application/json; charset=utf-8", {"key": "value"}),
249+
]
250+
251+
for content_type, data in test_cases:
252+
attributes = {
253+
"id": "123",
254+
"source": "source",
255+
"type": "type",
256+
"specversion": "1.0",
257+
"datacontenttype": content_type,
258+
}
259+
event = CloudEvent(attributes=attributes, data=data)
260+
formatter = JSONFormat()
261+
result = formatter.write(event)
262+
263+
assert b'"data": {"key": "value"}' in result
264+
265+
266+
def test_read_cloud_event_with_string_data() -> None:
267+
data = '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "data": "plain string data"}'.encode("utf-8")
268+
formatter = JSONFormat()
269+
result = formatter.read(CloudEvent, data)
270+
271+
assert result.get_data() == "plain string data"
272+
273+
274+
def test_write_cloud_event_with_utc_timezone_z_suffix() -> None:
275+
attributes = {
276+
"id": "123",
277+
"source": "source",
278+
"type": "type",
279+
"specversion": "1.0",
280+
"time": datetime(2023, 10, 25, 17, 9, 19, 736166, tzinfo=timezone.utc),
281+
}
282+
event = CloudEvent(attributes=attributes, data=None)
283+
formatter = JSONFormat()
284+
result = formatter.write(event)
285+
286+
assert b'"time": "2023-10-25T17:09:19.736166Z"' in result
287+
288+
289+
def test_write_cloud_event_with_unicode_data() -> None:
290+
attributes = {
291+
"id": "123",
292+
"source": "source",
293+
"type": "type",
294+
"specversion": "1.0",
295+
}
296+
event = CloudEvent(attributes=attributes, data="Hello 世界 🌍")
297+
formatter = JSONFormat()
298+
result = formatter.write(event)
299+
300+
decoded = result.decode("utf-8")
301+
assert '"data": "Hello' in decoded
302+
assert "Hello" in decoded
303+
304+
305+
def test_read_cloud_event_with_unicode_data() -> None:
306+
data = '{"id": "123", "source": "source", "type": "type", "specversion": "1.0", "data": "Hello 世界 🌍"}'.encode("utf-8")
307+
formatter = JSONFormat()
308+
result = formatter.read(CloudEvent, data)
309+
310+
assert result.get_data() == "Hello 世界 🌍"
311+
312+
313+
def test_read_cloud_event_from_string_input() -> None:
314+
data = '{"id": "123", "source": "source", "type": "type", "specversion": "1.0"}'
315+
formatter = JSONFormat()
316+
result = formatter.read(CloudEvent, data)
317+
318+
assert result.get_id() == "123"
319+
assert result.get_source() == "source"

0 commit comments

Comments
 (0)