Repository: aminalaee/mongox (language: python)

Issue:
Add `Q` method `regex` for matched strings
Need a way to search for a regular expression in a string type field.

```py
movie = await Movie.query(Q.regex(Movie.name, r"\w+")).first()
```


Discussion / hints:
I could do that in the next few days.
Awesome, thank you @MAD-py 

Fix patch:
diff --git a/docs/queriying_documents.md b/docs/queriying_documents.md
index 7393a56..ec30877 100644
--- a/docs/queriying_documents.md
+++ b/docs/queriying_documents.md
@@ -365,17 +365,23 @@ movies = await Movie.query(Q.or_(Movie.name == "Forrest Gump", Movie.year > 2000
 
 * `Q.contains()` Querying inside lists nad strings.
  
- This will query for movie with tag `Drama`.
+This will query for movies with tag `Drama`.
 
- ```python
- movies = await Movie.query(Q.contains(Movie.tags, "Drama")).all()
- ```
+```python
+movies = await Movie.query(Q.contains(Movie.tags, "Drama")).all()
+```
+
+This will query for movies which in the name contains `Forrest`.
+
+```python
+movies = await Movie.query(Q.contains(Movie.name, "Forrest")).all()
+```
 
- This will query for movie which in the name contains `Forrest`.
+This will query for movies which the name matches `\w+ Two \w+`.
 
- ```python
- movies = await Movie.query(Q.contains(Movie.name, "Forrest")).all()
- ```
+```python
+movies = await Movie.query(Q.regex(Movie.name, r"\w+ The \w+")).all()
+```
 
 ### Embedded Models
 
diff --git a/mongox/exceptions.py b/mongox/exceptions.py
index 1b4bca0..cd80a92 100644
--- a/mongox/exceptions.py
+++ b/mongox/exceptions.py
@@ -16,3 +16,7 @@ class InvalidKeyException(QueryException):
 
 class InvalidObjectIdException(QueryException):
     pass
+
+
+class InvalidFieldTypeException(QueryException):
+    pass
diff --git a/mongox/models.py b/mongox/models.py
index c450953..1f4f52b 100644
--- a/mongox/models.py
+++ b/mongox/models.py
@@ -1,3 +1,4 @@
+import re
 import typing
 
 import bson
@@ -6,6 +7,7 @@
 from mongox._helpers import normalize_class_name
 from mongox.database import Collection, Database
 from mongox.exceptions import (
+    InvalidFieldTypeException,
     InvalidKeyException,
     InvalidObjectIdException,
     MultipleMatchesFound,
@@ -55,6 +57,16 @@ def contains(cls, key: typing.Any, value: typing.Any) -> QueryExpression:
             return QueryExpression(key=key, operator="$regex", value=value)
         return QueryExpression(key=key, operator="$eq", value=value)
 
+    @classmethod
+    def regex(
+        cls, key: typing.Any, value: typing.Union[str, re.Pattern]
+    ) -> QueryExpression:
+        if key._pydantic_field.outer_type_ is str:
+            expression = value.pattern if isinstance(value, re.Pattern) else value
+            return QueryExpression(key=key, operator="$regex", value=expression)
+        name = key if isinstance(key, str) else key._name
+        raise InvalidFieldTypeException(f"The {name} field is not of type str")
+
 
 class QuerySet(typing.Generic[T]):
     def __init__(


Test patch:
diff --git a/tests/test_models.py b/tests/test_models.py
index 9aa6c03..832058c 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,5 +1,6 @@
 import asyncio
 import os
+import re
 import secrets
 import typing
 
@@ -10,6 +11,7 @@
 
 from mongox.database import Client
 from mongox.exceptions import (
+    InvalidFieldTypeException,
     InvalidObjectIdException,
     MultipleMatchesFound,
     NoMatchFound,
@@ -448,6 +450,20 @@ async def test_custom_query_operators() -> None:
     movies = await Movie.query(Q.contains(Movie.name, "Two")).all()
     assert movies[0].name == "The Two Towers"
 
+    movies = await Movie.query(Q.regex(Movie.name, r"\w+ Two \w+")).all()
+    assert len(movies) == 1
+    assert movies[0].name == "The Two Towers"
+
+    movies = await Movie.query(Q.regex(Movie.name, re.compile(r"\w+ Two \w+"))).all()
+    assert len(movies) == 1
+    assert movies[0].name == "The Two Towers"
+
+    movies = await Movie.query(Q.regex(Movie.name, r"\w+ The \w+")).all()
+    assert len(movies) == 0
+
+    with pytest.raises(InvalidFieldTypeException):
+        await Movie.query(Q.regex(Movie.year, r"\w+ The \w+")).all()
+
 
 async def test_model_get_or_create() -> None:
     movie = await Movie.query({Movie.name: "Forrest Gump"}).get_or_create(