Coverage for nexios\converters.py: 57%

1"""

2 implemented from starlatte,

3"""

4

5 from __future__ import annotations

6

7 import math

8 import typing

9 import uuid

10 import re

11 from nexios.types import Scope

12

13 T = typing.TypeVar("T")

14

15

16 class Convertor(typing.Generic[T]):

17 regex: typing.ClassVar[str] = ""

18

19 def convert(self, value: str) -> T:

20 raise NotImplementedError() # pragma: no cover

21

22 def to_string(self, value: T) -> str:

23 raise NotImplementedError() # pragma: no cover

24

25

26 class StringConvertor(Convertor[str]):

27 regex = "[^/]+"

28

29 def convert(self, value: str) -> str:

30 return value

31

32 def to_string(self, value: str) -> str:

33 value = str(value)

34 assert "/" not in value, "May not contain path separators"

35 assert value, "Must not be empty"

36 return value

37

38

39 class PathConvertor(Convertor[str]):

40 regex = ".*"

41

42 def convert(self, value: str) -> str:

43 return value

44

45 def to_string(self, value: str) -> str:

46 return value

47

48

49 class IntegerConvertor(Convertor[int]):

50 regex = "[0-9]+"

51

52 def convert(self, value: str) -> int:

53 return int(value)

54

55 def to_string(self, value: int) -> str:

56 value = int(value)

57 assert value >= 0, "Negative integers are not supported"

58 return str(value)

59

60

61 class FloatConvertor(Convertor[float]):

62 regex = r"[0-9]+(\.[0-9]+)?"

63

64 def convert(self, value: str) -> float:

65 return float(value)

66

67 def to_string(self, value: float) -> str:

68 value = float(value)

69 assert value >= 0.0, "Negative floats are not supported"

70 assert not math.isnan(value), "NaN values are not supported"

71 assert not math.isinf(value), "Infinite values are not supported"

72 return ("%0.20f" % value).rstrip("0").rstrip(".")

73

74

75 class UUIDConvertor(Convertor[uuid.UUID]):

76 regex = "[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}"

77

78 def convert(self, value: str) -> uuid.UUID:

79 return uuid.UUID(value)

80

81 def to_string(self, value: uuid.UUID) -> str:

82 return str(value)

83

84

85 class SlugConvertor(Convertor[str]):

86 """Converter for slugs (URL-friendly strings)."""

87

88 regex = r"[a-z0-9]+(?:-[a-z0-9]+)*"

89

90 def convert(self, value: str) -> str:

91 if not re.fullmatch(self.regex, value):

92 raise ValueError(f"Invalid slug format: {value}")

93 return value

94

95 def to_string(self, value: str) -> str:

96 if not re.fullmatch(self.regex, value):

97 raise ValueError(f"Invalid slug format: {value}")

98 return value

99

100

101 CONVERTOR_TYPES: dict[str, Convertor[typing.Any]] = {

102 "str": StringConvertor(),

103 "path": PathConvertor(),

104 "int": IntegerConvertor(),

105 "float": FloatConvertor(),

106 "uuid": UUIDConvertor(),

107 "slug": SlugConvertor(),

108}

109

110

111 def register_url_convertor(key: str, convertor: Convertor[typing.Any]) -> None:

112 CONVERTOR_TYPES[key] = convertor

113

114

115 def get_route_path(scope: Scope) -> str:

116 path: str = scope["path"]

117 root_path = scope.get("root_path", "")

118 if not root_path:

119 return path

120

121 if not path.startswith(root_path):

122 return path

123

124 if path == root_path:

125 return ""

126

127 if path[len(root_path)] == "/":

128 return path[len(root_path) :]

129

130 return path