Repository: sanic-org/sanic-ext (python)

Issue:
Query parameter validation using Literal types in a dataclass fails
Query parameter validation fails to coerce valid data when using `Literal` in a dataclass:

```python
import json
from dataclasses import dataclass
from typing import Literal

from sanic import Sanic
from sanic.response import empty
from sanic_ext import validate


app = Sanic(name='name')


@dataclass
class Spec:
    name: Literal['foo', 'bar']


@app.get('/route')
@validate(query=Spec)
def route(_, body: Spec):
    print(body.name)
    return empty()


req, resp = app.test_client.get('/route', params={'name': 'baz'})
# ✔️ Validation catches invalid data:
# sanic_ext.exceptions.ValidationError: Invalid request body: Spec. Error: Value '['baz']' must be one of foo, bar

req, resp = app.test_client.get('/route', params={'name': 'bar'})
# ❌ Validation error coercing valid data:
# [ERROR] Exception occurred while handling uri: 'http://127.0.0.1:39541/route?name=bar'
# Traceback (most recent call last):
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/validators.py", line 25, in validate_body
#     return validator(model, body)
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/validators.py", line 38, in _validate_annotations
#     return check_data(model, body, schema, allow_multiple, allow_coerce)
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/check.py", line 147, in check_data
#     hydration_values[key] = hint.validate(
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/check.py", line 100, in validate
#     value = self.coerce(value)
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/check.py", line 109, in coerce
#     coerce_types = [arg for arg in args if not isinstance(None, arg)]
#   File ".../env/lib/python3.8/site-packages/sanic_ext/extras/validation/check.py", line 109, in <listcomp>
#     coerce_types = [arg for arg in args if not isinstance(None, arg)]
# TypeError: isinstance() arg 2 must be a type or tuple of types
# ...
# sanic_ext.exceptions.ValidationError: Invalid request body: Spec. Error: isinstance() arg 2 must be a type or tuple of types


```

**Expected behavior**
No error when valid query parameters are used.

**Environment (please complete the following information):**
Sanic v22.12.0, sanic-routing==22.8.0, sanic-testing==22.9.0, sanic-ext==22.12.0


Pull request description:
Do not coerce a literal type
Closes #173 


Patch:
diff --git a/sanic_ext/extras/validation/check.py b/sanic_ext/extras/validation/check.py
index d9a72ba..5a6a166 100644
--- a/sanic_ext/extras/validation/check.py
+++ b/sanic_ext/extras/validation/check.py
@@ -1,7 +1,16 @@
 from __future__ import annotations
 
 from dataclasses import _HAS_DEFAULT_FACTORY  # type: ignore
-from typing import Any, Literal, NamedTuple, Optional, Tuple, Union, get_args
+from typing import (
+    Any,
+    Literal,
+    NamedTuple,
+    Optional,
+    Tuple,
+    Union,
+    get_args,
+    get_origin,
+)
 
 from sanic_ext.utils.typing import UnionType, is_generic, is_optional
 
@@ -104,6 +113,10 @@ class Hint(NamedTuple):
     def coerce(self, value):
         if is_generic(self.coerce_type):
             args = get_args(self.coerce_type)
+            if get_origin(self.coerce_type) == Literal or (
+                all(get_origin(arg) == Literal for arg in args)
+            ):
+                return value
             if type(None) in args and value is None:
                 return None
             coerce_types = [arg for arg in args if not isinstance(None, arg)]


Test patch:
diff --git a/tests/extra/test_validation.py b/tests/extra/test_validation.py
new file mode 100644
index 0000000..b1c8eb9
--- /dev/null
+++ b/tests/extra/test_validation.py
@@ -0,0 +1,47 @@
+from dataclasses import dataclass
+from sys import version_info
+from typing import Literal, Union
+
+import pytest
+from sanic import text
+
+from sanic_ext import validate
+
+
+@pytest.mark.parametrize(
+    "annotation",
+    (
+        (
+            Literal["foo"],
+            Literal["foo", "bar"],
+            Union[Literal["foo"], Literal["bar"]],
+        )
+    ),
+)
+def test_literal(app, annotation):
+    @dataclass
+    class Spec:
+        name: annotation
+
+    @app.get("/")
+    @validate(query=Spec)
+    def route(_, query: Spec):
+        return text(query.name)
+
+    _, response = app.test_client.get("", params={"name": "foo"})
+    assert response.text == "foo"
+
+
+@pytest.mark.skipif(version_info < (3, 10), reason="Not needed on 3.10")
+def test_literal_3_10(app):
+    @dataclass
+    class Spec:
+        name: Literal["foo"] | Literal["bar"]
+
+    @app.get("/")
+    @validate(query=Spec)
+    def route(_, query: Spec):
+        return text(query.name)
+
+    _, response = app.test_client.get("", params={"name": "foo"})
+    assert response.text == "foo"