Repository: kpn/combadge (language: python)

Issue:
Non-dict responses break in HTTPX backend
If a service is defined to return a list response of some kind. The httpx backend breaks because it will always try to parse a dictionary.

```python
    @classmethod
    def _parse_response(cls, from_response: Response, to_type: type[ResponseT]) -> ResponseT:
        try:
            json_fields = from_response.json()
        except ValueError:
            json_fields = {}
        return to_type.model_validate(
            {
                STATUS_CODE_ALIAS: from_response.status_code,
                REASON_ALIAS: from_response.reason_phrase,
                CONTENT_ALIAS: from_response.content,
                **json_fields,  # <- if json_fields is a list, this will break
            },
        )
```

sample service code that breaks:

```python
...

class BaseResponse(BaseModel):
    title: str
    name: str

class ListResponse(RootModel, SuccessfulResponse):
    root: list[BaseResponse]


class MyService(SupportsService, Protocol):
       @path("{environment}/form")
    @http_method("GET")
    async def get_xxx(
        self,
        *,
        environment: str,
    ) -> ListResponse:
        ...
```


Error:
```
    File "..../combadge/support/httpx/backends/base.py", line 42, in _parse_response
    TypeError: 'list' object is not a mapping
``` 


Discussion / hints:

Fix patch:
diff --git a/Makefile b/Makefile
index 3fae2c5..9570637 100644
--- a/Makefile
+++ b/Makefile
@@ -39,6 +39,10 @@ format/ruff:
 test:
 	poetry run pytest tests
 
+.PHONY: test/record
+test/record:
+	poetry run pytest --record-mode=once tests
+
 .PHONY: build
 build:
 	poetry build


Test patch:
diff --git a/tests/integration/cassettes/test_httpbin/test_non_dict_json.yaml b/tests/integration/cassettes/test_httpbin/test_non_dict_json.yaml
new file mode 100644
index 0000000..0c37bbd
--- /dev/null
+++ b/tests/integration/cassettes/test_httpbin/test_non_dict_json.yaml
@@ -0,0 +1,36 @@
+interactions:
+- request:
+    body: ''
+    headers:
+      accept:
+      - '*/*'
+      accept-encoding:
+      - gzip, deflate
+      connection:
+      - keep-alive
+      host:
+      - httpbin.org
+      user-agent:
+      - python-httpx/0.25.1
+    method: GET
+    uri: https://httpbin.org/get
+  response:
+    content: "[42, 43]"
+    headers:
+      Access-Control-Allow-Credentials:
+      - 'true'
+      Access-Control-Allow-Origin:
+      - '*'
+      Connection:
+      - keep-alive
+      Content-Length:
+      - '304'
+      Content-Type:
+      - application/json
+      Date:
+      - Sat, 04 Nov 2023 16:29:09 GMT
+      Server:
+      - gunicorn/19.9.0
+    http_version: HTTP/1.1
+    status_code: 200
+version: 1
diff --git a/tests/integration/test_httpbin.py b/tests/integration/test_httpbin.py
index f0e34e7..2353fd7 100644
--- a/tests/integration/test_httpbin.py
+++ b/tests/integration/test_httpbin.py
@@ -1,5 +1,5 @@
 from abc import abstractmethod
-from typing import Any, Callable, Dict, Protocol, Union
+from typing import Any, Callable, Dict, List, Protocol, Union
 
 from httpx import AsyncClient, Client
 from pydantic import BaseModel
@@ -106,3 +106,18 @@ async def get_headers(
     assert response.headers["X-Foo"] == "fooval"
     assert response.headers["X-Bar"] == "barval"
     assert response.headers["X-Baz"] == "bazval"
+
+
+@mark.vcr
+def test_non_dict_json() -> None:
+    class SupportsHttpbin(SupportsService, Protocol):
+        @http_method("GET")
+        @path("/get")
+        @abstractmethod
+        def get_non_dict(self) -> List[int]:
+            ...
+
+    # Since httpbin.org is not capable of returning a non-dict JSON,
+    # I manually patched the recorded VCR.py response.
+    service = SupportsHttpbin.bind(SyncHttpxBackend(Client(base_url="https://httpbin.org")))
+    assert service.get_non_dict() == [42, 43]