Skip to content

Commit

Permalink
Merge pull request #4 from secondlife/signal/llsd-json
Browse files Browse the repository at this point in the history
Add support for serializing LLSD types
  • Loading branch information
bennettgoble authored Feb 19, 2024
2 parents 4a01f3a + 558e1f0 commit 19ab827
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 5 deletions.
17 changes: 16 additions & 1 deletion llsd_asgi/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import json
from base64 import b64encode
from datetime import date, datetime
from typing import Any
from uuid import UUID

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


class JSONEncoder(json.JSONEncoder):
def default(self, o: Any):
if isinstance(o, UUID):
return str(o)
if isinstance(o, bytes):
return b64encode(o).decode("utf-8")
if isinstance(o, (datetime, date)):
return o.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
return super().default(o)


class _LLSDResponder:

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

message["body"] = json.dumps(self.parse(body)).encode()
message["body"] = json.dumps(self.parse(body), cls=JSONEncoder).encode()

return message

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dev = [

[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-vv --cov=llsd_asgi --cov-report=xml"
addopts = "-vv --cov=llsd_asgi --cov-report=xml --cov-report term-missing"
testpaths = ["tests"]

[build-system]
Expand Down
33 changes: 30 additions & 3 deletions tests/test_middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
from datetime import date, datetime
from typing import Any, Callable
from uuid import UUID

import httpx
import llsd
Expand All @@ -8,6 +11,7 @@
from starlette.types import Receive, Scope, Send

from llsd_asgi import LLSDMiddleware
from llsd_asgi.middleware import JSONEncoder
from tests.utils import mock_receive, mock_send

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

response = PlainTextResponse(text)
await response(scope, receive, send)

app = LLSDMiddleware(app)

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


@pytest.mark.asyncio
Expand Down Expand Up @@ -199,3 +206,23 @@ async def test_quirks_exceptions(accept: str) -> None:
assert r.status_code == 200
assert r.headers["content-type"] == "application/json"
assert r.json() == {"message": "Hello, world!"}


@pytest.mark.asyncio
@pytest.mark.parametrize(
"input,expected",
[
(datetime(2024, 1, 1, 0, 0, 0), '"2024-01-01T00:00:00.000000Z"'),
(date(2024, 1, 1), '"2024-01-01T00:00:00.000000Z"'),
(UUID("c72736e5-e9e4-4779-b46b-b49467e425ff"), '"c72736e5-e9e4-4779-b46b-b49467e425ff"'),
(b"Hello", '"SGVsbG8="'),
],
)
async def test_json_encoder(input: Any, expected: Any):
assert json.dumps(input, cls=JSONEncoder) == expected


@pytest.mark.asyncio
async def test_json_encoder_calls_default():
with pytest.raises(TypeError):
json.dumps(object(), cls=JSONEncoder)

0 comments on commit 19ab827

Please sign in to comment.