Skip to content

Commit e92842f

Browse files
committed
SimpleResolver -> Resolver, CONVERSION_TYPES -> CONVERTERS, file refactoring, and better resolver base class
1 parent 7a242e1 commit e92842f

File tree

5 files changed

+114
-101
lines changed

5 files changed

+114
-101
lines changed

src/reactpy_router/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
# the version is statically loaded by setup.py
22
__version__ = "0.1.1"
33

4-
from . import routers
4+
5+
from .converters import CONVERTERS
56
from .core import create_router, link, route, router_component, use_params, use_search_params
7+
from .resolvers import Resolver
68
from .routers import browser_router
79
from .types import Route, RouteCompiler, RouteResolver
810

911
__all__ = (
1012
"create_router",
1113
"link",
1214
"route",
13-
"route",
14-
"Route",
1515
"routers",
16+
"Route",
1617
"RouteCompiler",
1718
"router_component",
1819
"RouteResolver",
1920
"browser_router",
2021
"use_params",
2122
"use_search_params",
23+
"Resolver",
24+
"CONVERTERS",
2225
)

src/reactpy_router/converters.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import uuid
2+
3+
from reactpy_router.types import ConversionInfo
4+
5+
__all__ = ["CONVERTERS"]
6+
7+
CONVERTERS: dict[str, ConversionInfo] = {
8+
"int": {
9+
"regex": r"\d+",
10+
"func": int,
11+
},
12+
"str": {
13+
"regex": r"[^/]+",
14+
"func": str,
15+
},
16+
"uuid": {
17+
"regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
18+
"func": uuid.UUID,
19+
},
20+
"slug": {
21+
"regex": r"[-a-zA-Z0-9_]+",
22+
"func": str,
23+
},
24+
"path": {
25+
"regex": r".+",
26+
"func": str,
27+
},
28+
"float": {
29+
"regex": r"\d+(\.\d+)?",
30+
"func": float,
31+
},
32+
}
33+
"""The conversion types supported by the default Resolver. You can add more types if needed."""

src/reactpy_router/resolvers.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import re
2+
from typing import Any
3+
4+
from reactpy_router.converters import CONVERTERS
5+
from reactpy_router.types import ConversionInfo, ConverterMapping, Route
6+
7+
__all__ = ["Resolver"]
8+
9+
10+
class Resolver:
11+
"""A simple route resolver that uses regex to match paths"""
12+
13+
def __init__(
14+
self,
15+
route: Route,
16+
param_pattern=r"{(?P<name>\w+)(?P<type>:\w+)?}",
17+
converters: dict[str, ConversionInfo] | None = None,
18+
) -> None:
19+
self.element = route.element
20+
self.pattern, self.converter_mapping = self.parse_path(route.path)
21+
self.converters = converters or CONVERTERS
22+
self.key = self.pattern.pattern
23+
self.param_regex = re.compile(param_pattern)
24+
25+
def parse_path(self, path: str) -> tuple[re.Pattern[str], ConverterMapping]:
26+
pattern = "^"
27+
last_match_end = 0
28+
converter_mapping: ConverterMapping = {}
29+
for match in self.param_regex.finditer(path):
30+
param_name = match.group("name")
31+
param_type = (match.group("type") or "str").strip(":")
32+
try:
33+
param_conv = self.converter_mapping[param_type]
34+
except KeyError as e:
35+
raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}") from e
36+
pattern += re.escape(path[last_match_end : match.start()])
37+
pattern += f"(?P<{param_name}>{param_conv['regex']})"
38+
converter_mapping[param_name] = param_conv["func"]
39+
last_match_end = match.end()
40+
pattern += f"{re.escape(path[last_match_end:])}$"
41+
42+
# Replace literal `*` with "match anything" regex pattern, if it's at the end of the path
43+
if pattern.endswith(r"\*$"):
44+
pattern = f"{pattern[:-3]}.*$"
45+
46+
return re.compile(pattern), converter_mapping
47+
48+
def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
49+
match = self.pattern.match(path)
50+
if match:
51+
return (
52+
self.element,
53+
{k: self.converter_mapping[k](v) for k, v in match.groupdict().items()},
54+
)
55+
return None

src/reactpy_router/routers.py

Lines changed: 2 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -2,103 +2,13 @@
22

33
from __future__ import annotations
44

5-
import re
6-
import uuid
7-
from typing import Any, Callable
8-
9-
from typing_extensions import TypeAlias, TypedDict
10-
115
from reactpy_router.core import create_router
12-
from reactpy_router.types import Route
6+
from reactpy_router.resolvers import Resolver
137

148
__all__ = ["browser_router"]
159

16-
ConversionFunc: TypeAlias = "Callable[[str], Any]"
17-
ConverterMapping: TypeAlias = "dict[str, ConversionFunc]"
18-
19-
STAR_PATTERN = re.compile("^.*$")
20-
PARAM_PATTERN = re.compile(r"{(?P<name>\w+)(?P<type>:\w+)?}")
21-
22-
23-
class SimpleResolver:
24-
"""A simple route resolver that uses regex to match paths"""
25-
26-
def __init__(self, route: Route) -> None:
27-
self.element = route.element
28-
self.pattern, self.converters = parse_path(route.path)
29-
self.key = self.pattern.pattern
30-
31-
def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
32-
match = self.pattern.match(path)
33-
if match:
34-
return (
35-
self.element,
36-
{k: self.converters[k](v) for k, v in match.groupdict().items()},
37-
)
38-
return None
39-
40-
41-
def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]:
42-
if path == "*":
43-
return STAR_PATTERN, {}
44-
45-
pattern = "^"
46-
last_match_end = 0
47-
converters: ConverterMapping = {}
48-
for match in PARAM_PATTERN.finditer(path):
49-
param_name = match.group("name")
50-
param_type = (match.group("type") or "str").lstrip(":")
51-
try:
52-
param_conv = CONVERSION_TYPES[param_type]
53-
except KeyError:
54-
raise ValueError(f"Unknown conversion type {param_type!r} in {path!r}")
55-
pattern += re.escape(path[last_match_end : match.start()])
56-
pattern += f"(?P<{param_name}>{param_conv['regex']})"
57-
converters[param_name] = param_conv["func"]
58-
last_match_end = match.end()
59-
pattern += re.escape(path[last_match_end:]) + "$"
60-
return re.compile(pattern), converters
61-
62-
63-
class ConversionInfo(TypedDict):
64-
"""Information about a conversion type"""
65-
66-
regex: str
67-
"""The regex to match the conversion type"""
68-
func: ConversionFunc
69-
"""The function to convert the matched string to the expected type"""
70-
71-
72-
CONVERSION_TYPES: dict[str, ConversionInfo] = {
73-
"int": {
74-
"regex": r"\d+",
75-
"func": int,
76-
},
77-
"str": {
78-
"regex": r"[^/]+",
79-
"func": str,
80-
},
81-
"uuid": {
82-
"regex": r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}",
83-
"func": uuid.UUID,
84-
},
85-
"slug": {
86-
"regex": r"[-a-zA-Z0-9_]+",
87-
"func": str,
88-
},
89-
"path": {
90-
"regex": r".+",
91-
"func": str,
92-
},
93-
"float": {
94-
"regex": r"\d+(\.\d+)?",
95-
"func": float,
96-
},
97-
}
98-
"""The conversion types supported by the default Resolver. You can add more types if needed."""
99-
10010

101-
browser_router = create_router(SimpleResolver)
11+
browser_router = create_router(Resolver)
10212
"""This is the recommended router for all ReactPy Router web projects.
10313
It uses the DOM History API to update the URL and manage the history stack."""
10414
# TODO: Check if this is true. If not, make it true.

src/reactpy_router/types.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass, field
6-
from typing import Any, Literal, Sequence, TypeVar
6+
from typing import Any, Callable, Literal, Sequence, TypeAlias, TypedDict, TypeVar
77

88
from reactpy.core.vdom import is_vdom
99
from reactpy.types import ComponentType, Key
1010
from typing_extensions import Protocol, Self
1111

12+
ConversionFunc: TypeAlias = Callable[[str], Any]
13+
ConverterMapping: TypeAlias = dict[str, ConversionFunc]
14+
1215

1316
@dataclass(frozen=True)
1417
class Route:
@@ -29,20 +32,20 @@ def __hash__(self) -> int:
2932
return hash((self.path, key, self.routes))
3033

3134

32-
R = TypeVar("R", bound=Route, contravariant=True)
35+
R_contra = TypeVar("R_contra", bound=Route, contravariant=True)
3336

3437

35-
class Router(Protocol[R]):
38+
class Router(Protocol[R_contra]):
3639
"""Return a component that renders the first matching route"""
3740

38-
def __call__(self, *routes: R, select: Literal["first", "all"]) -> ComponentType:
41+
def __call__(self, *routes: R_contra, select: Literal["first", "all"]) -> ComponentType:
3942
...
4043

4144

42-
class RouteCompiler(Protocol[R]):
45+
class RouteCompiler(Protocol[R_contra]):
4346
"""Compile a route into a resolver that can be matched against a path"""
4447

45-
def __call__(self, route: R) -> RouteResolver:
48+
def __call__(self, route: R_contra) -> RouteResolver:
4649
...
4750

4851

@@ -55,3 +58,12 @@ def key(self) -> Key:
5558

5659
def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None:
5760
"""Return the path's associated element and path params or None"""
61+
62+
63+
class ConversionInfo(TypedDict):
64+
"""Information about a conversion type"""
65+
66+
regex: str
67+
"""The regex to match the conversion type"""
68+
func: ConversionFunc
69+
"""The function to convert the matched string to the expected type"""

0 commit comments

Comments
 (0)