Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/phone.py: 26%

38 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-07 19:28 +0000

1import re 

2import typing as t 

3 

4from viur.core.bones.string import StringBone 

5from viur.core.bones.base import ReadFromClientError, ReadFromClientErrorSeverity 

6 

7DEFAULT_REGEX = r"^\+?(\d{1,3})[-\s]?(\d{1,4})[-\s]?(\d{1,4})[-\s]?(\d{1,9})$" 

8 

9 

10class PhoneBone(StringBone): 

11 """ 

12 The PhoneBone class is designed to store validated phone/fax numbers in configurable formats. 

13 This class provides a number validation method, ensuring that the given phone/fax number conforms to the 

14 required/configured format and structure. 

15 """ 

16 

17 type: str = "str.phone" 

18 """ 

19 A string representing the type of the bone, in this case "str.phone". 

20 """ 

21 

22 def __init__( 

23 self, 

24 *, 

25 test: t.Optional[t.Pattern[str]] = DEFAULT_REGEX, 

26 max_length: int = 15, # maximum allowed numbers according to ITU-T E.164 

27 default_country_code: t.Optional[str] = None, 

28 **kwargs: t.Any, 

29 ) -> None: 

30 """ 

31 Initializes the PhoneBone with an optional custom regex for phone number validation, a default country code, 

32 and a flag to apply the default country code if none is provided. 

33 

34 :param test: An optional custom regex pattern for phone number validation. 

35 :param max_length: The maximum length of the phone number. Passed to "StringBone". 

36 :param default_country_code: The default country code to apply (with leading +) for example "+49" 

37 If None is provided the PhoneBone will ignore auto prefixing of the country code. 

38 :param kwargs: Additional keyword arguments. Passed to "StringBone". 

39 :raises ValueError: If the default country code is not in the correct format for example "+123". 

40 """ 

41 if default_country_code and not re.match(r"^\+\d{1,3}$", default_country_code): 

42 raise ValueError(f"Invalid default country code format: {default_country_code}") 

43 

44 self.test: t.Pattern[str] = re.compile(test) if isinstance(test, str) else test 

45 self.default_country_code: t.Optional[str] = default_country_code 

46 super().__init__(max_length=max_length, **kwargs) 

47 

48 @staticmethod 

49 def _extract_digits(value: str) -> str: 

50 """ 

51 Extracts and returns only the digits from the given value. 

52 

53 :param value: The input string from which to extract digits. 

54 :return: A string containing only the digits from the input value. 

55 """ 

56 return re.sub(r"[^\d+]", "", value) 

57 

58 def isInvalid(self, value: str) -> t.Optional[str]: 

59 """ 

60 Checks if the provided phone number is valid or not. 

61 

62 :param value: The phone number to be validated. 

63 :return: An error message if the phone number is invalid or None if it is valid. 

64 

65 The method checks if the provided phone number is valid according to the following criteria: 

66 1. The phone number must not be empty. 

67 2. The phone number must match the provided or default phone number format. 

68 3. The phone number cannot exceed 15 digits, or the specified maximum length if provided (digits only). 

69 """ 

70 if not value: 

71 return "No value entered" 

72 

73 if self.test and not self.test.match(value): 

74 return "Invalid phone number entered" 

75 

76 # make sure max_length is not exceeded. 

77 if is_invalid := super().isInvalid(self._extract_digits(value)): 

78 return is_invalid 

79 

80 return None 

81 

82 def singleValueFromClient( 

83 self, value: str, skel: t.Any, bone_name: str, client_data: t.Any 

84 ) -> t.Tuple[t.Optional[str], t.Optional[t.List[ReadFromClientError]]]: 

85 """ 

86 Processes a single value from the client, applying the default country code if necessary and validating the 

87 phone number. 

88 

89 :param value: The phone number provided by the client. 

90 :param skel: Skeleton data (not used in this method). 

91 :param bone_name: The name of the bone (not used in this method). 

92 :param client_data: Additional client data (not used in this method). 

93 :return: A tuple containing the processed phone number and an optional list of errors. 

94 """ 

95 value = value.strip() 

96 

97 # Replace country code starting with 00 with + 

98 if value.startswith("00"): 

99 value = "+" + value[2:] 

100 

101 # Apply default country code if none is provided and default_country_code is set 

102 if self.default_country_code and value[0] != "+": 

103 if value.startswith("0"): 

104 value = value[1:] # Remove leading 0 from city code 

105 value = f"{self.default_country_code} {value}" 

106 

107 if err := self.isInvalid(value): 

108 return self.getEmptyValue(), [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)] 

109 

110 return value, None 

111 

112 def structure(self) -> t.Dict[str, t.Any]: 

113 """ 

114 Returns the structure of the PhoneBone, including the test regex pattern. 

115 

116 :return: A dictionary representing the structure of the PhoneBone. 

117 """ 

118 return super().structure() | { 

119 "test": self.test.pattern if self.test else "", 

120 "default_country_code": self.default_country_code, 

121 }