Coverage for nexios\converters.py: 57%

75 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-21 20:31 +0100

1""" 

2implemented from starlatte, 

3""" 

4 

5from __future__ import annotations 

6 

7import math 

8import typing 

9import uuid 

10import re 

11from nexios.types import Scope 

12 

13T = typing.TypeVar("T") 

14 

15 

16class 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 

26class 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 

39class 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 

49class 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 

61class 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 

75class 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 

85class 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 

101CONVERTOR_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 

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

112 CONVERTOR_TYPES[key] = convertor 

113 

114 

115def 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