Skip to content

Commit 19ab827

Browse files
authored
Merge pull request #4 from secondlife/signal/llsd-json
Add support for serializing LLSD types
2 parents 4a01f3a + 558e1f0 commit 19ab827

File tree

3 files changed

+47
-5
lines changed

3 files changed

+47
-5
lines changed

llsd_asgi/middleware.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import json
2+
from base64 import b64encode
3+
from datetime import date, datetime
4+
from typing import Any
5+
from uuid import UUID
26

37
import llsd
48
from starlette.datastructures import Headers, MutableHeaders
@@ -31,6 +35,17 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
3135
}
3236

3337

38+
class JSONEncoder(json.JSONEncoder):
39+
def default(self, o: Any):
40+
if isinstance(o, UUID):
41+
return str(o)
42+
if isinstance(o, bytes):
43+
return b64encode(o).decode("utf-8")
44+
if isinstance(o, (datetime, date)):
45+
return o.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
46+
return super().default(o)
47+
48+
3449
class _LLSDResponder:
3550

3651
def __init__(self, app: ASGIApp, quirks: bool = False) -> None:
@@ -101,7 +116,7 @@ async def receive_with_llsd(self) -> Message:
101116
if message["body"] != b"": # pragma: no cover
102117
raise NotImplementedError("Streaming the request body isn't supported yet")
103118

104-
message["body"] = json.dumps(self.parse(body)).encode()
119+
message["body"] = json.dumps(self.parse(body), cls=JSONEncoder).encode()
105120

106121
return message
107122

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dev = [
3232

3333
[tool.pytest.ini_options]
3434
minversion = "6.0"
35-
addopts = "-vv --cov=llsd_asgi --cov-report=xml"
35+
addopts = "-vv --cov=llsd_asgi --cov-report=xml --cov-report term-missing"
3636
testpaths = ["tests"]
3737

3838
[build-system]

tests/test_middleware.py

+30-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import json
2+
from datetime import date, datetime
13
from typing import Any, Callable
4+
from uuid import UUID
25

36
import httpx
47
import llsd
@@ -8,6 +11,7 @@
811
from starlette.types import Receive, Scope, Send
912

1013
from llsd_asgi import LLSDMiddleware
14+
from llsd_asgi.middleware import JSONEncoder
1115
from tests.utils import mock_receive, mock_send
1216

1317
Format = Callable[[Any], bytes]
@@ -29,19 +33,22 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
2933
content_type = request.headers["content-type"]
3034
data = await request.json()
3135
message = data["message"]
32-
text = f"content_type={content_type!r} message={message!r}"
36+
text = f"content_type={content_type!r} message={message!r} id={data['id']!r}"
3337

3438
response = PlainTextResponse(text)
3539
await response(scope, receive, send)
3640

3741
app = LLSDMiddleware(app)
3842

3943
async with httpx.AsyncClient(app=app, base_url="http://testserver") as client:
40-
content = {"message": "Hello, world!"}
44+
content = {"message": "Hello, world!", "id": UUID("380cbef3-74de-411b-bf5c-9ad98b376b41")}
4145
body = format(content)
4246
r = await client.post("/", content=body, headers={"content-type": content_type})
4347
assert r.status_code == 200
44-
assert r.text == "content_type='application/json' message='Hello, world!'"
48+
assert (
49+
r.text
50+
== "content_type='application/json' message='Hello, world!' id='380cbef3-74de-411b-bf5c-9ad98b376b41'"
51+
)
4552

4653

4754
@pytest.mark.asyncio
@@ -199,3 +206,23 @@ async def test_quirks_exceptions(accept: str) -> None:
199206
assert r.status_code == 200
200207
assert r.headers["content-type"] == "application/json"
201208
assert r.json() == {"message": "Hello, world!"}
209+
210+
211+
@pytest.mark.asyncio
212+
@pytest.mark.parametrize(
213+
"input,expected",
214+
[
215+
(datetime(2024, 1, 1, 0, 0, 0), '"2024-01-01T00:00:00.000000Z"'),
216+
(date(2024, 1, 1), '"2024-01-01T00:00:00.000000Z"'),
217+
(UUID("c72736e5-e9e4-4779-b46b-b49467e425ff"), '"c72736e5-e9e4-4779-b46b-b49467e425ff"'),
218+
(b"Hello", '"SGVsbG8="'),
219+
],
220+
)
221+
async def test_json_encoder(input: Any, expected: Any):
222+
assert json.dumps(input, cls=JSONEncoder) == expected
223+
224+
225+
@pytest.mark.asyncio
226+
async def test_json_encoder_calls_default():
227+
with pytest.raises(TypeError):
228+
json.dumps(object(), cls=JSONEncoder)

0 commit comments

Comments
 (0)