Coverage for audoma/links.py: 82%

1"""

2 This module is responsible for creating x-choices links.

3 Such links may be used to generate choices enums for fields.

4

5 If some field in the serializer has this attribute, it

6 will be used to generate choices for this field.

7 In other words choices should be limited to values

8 available under passed x-choices link.

9"""

10

11 import re

12 from dataclasses import dataclass

13 from typing import (

14 Any,

15 Dict,

16 Type,

17 Union,

18)

19

20 from drf_spectacular.plumbing import force_instance

21 from rest_framework.serializers import BaseSerializer

22

23 from django.urls import (

24 NoReverseMatch,

25 URLResolver,

26)

27 from django.urls.resolvers import get_resolver

28

29

30 def get_endpoint_pattern(endpoint_name: str, urlconf=None) -> str:

31 """

32 This methods retrieves url pattern of the endpoint by given endpoint_name.

33

34 Args:

35 * endpoint_name: name of the endpoint

36 * urlconf: urlconf to use

37

38 Returns: url pattern of the endpoint

39 """

40 resolver = get_resolver(urlconf)

41 patterns = resolver.url_patterns

42 new_patterns = []

43 resolvers = []

44

45 for pattern in patterns:

46 if isinstance(pattern, URLResolver):

47 resolvers.append(pattern)

48 continue

49 new_patterns.append(pattern)

50

51 while resolvers:

52 resolver = resolvers.pop()

53 patterns = resolver.url_patterns

54 for pattern in patterns:

55 if isinstance(pattern, URLResolver):

56 resolvers.append(pattern)

57 continue

58 new_patterns.append(pattern)

59

60 possibilities = list(filter(lambda p: p.name == endpoint_name, new_patterns))

61

62 for p in possibilities:

63 if "format" not in p.pattern.regex.pattern:

64 return str(p.pattern)

65

66 raise NoReverseMatch(f"There is no pattern with name {endpoint_name}")

67

68

69@dataclass

70 class ChoicesOptionsLink:

71 """

72 Helper dataclass, which holds data abot x-choices link.

73 """

74

75 field_name: str

76 viewname: str

77 value_field: str

78 display_field: str

79 serializer_class: Type[BaseSerializer]

80

81 description: str = ""

82

83 def _format_param_field(self, field: str) -> str:

84 """

85 Helper method which formats field name to be a JSON pointer.

86 If the passed field name is already a JSON pointer, it is returned unchagned.

87

88 Args:

89 * field: field name

90

91 Returns: formatted field name

92 """

93 if "$" in field:

94 # than we presume that there has been given full pointer

95 return field

96

97 field = f"$response.body#results/*/{field}"

98

99 return field

100

101 @property

102 def formatted_value_field(self) -> str:

103 return self._format_param_field(self.value_field)

104

105 @property

106 def formatted_display_field(self) -> str:

107 return self._format_param_field(self.display_field)

108

109 def get_url_pattern(self) -> str:

110 """

111 Returns formatted url pattern of the linked view.

112 """

113 pattern = (

114 get_endpoint_pattern(self.viewname)

115 .replace("$", "")

116 .replace("^", "/")

117 .replace("/", "~1")

118 )

119

120 params = re.search(r"<.*>", pattern)

121 if params:

122 for param in params:

123 param = param.string.replace("<", "{").replace(">", "}")

124

125 regexes = re.search(r"\(.*\)", pattern)

126 for x, regex in enumerate(regexes):

127 pattern = pattern.replace(regex.string, params[x])

128

129 pattern = f"#/paths/{pattern}"

130

131 return pattern

132

133

134 class ChoicesOptionsLinkSchemaGenerator:

135 def _process_link(

136 self, link: Union[ChoicesOptionsLink, Dict[str, Any]]

137 ) -> ChoicesOptionsLink:

138 if isinstance(link, dict):

139 link = ChoicesOptionsLink(**link)

140

141 if not isinstance(link, ChoicesOptionsLink):

142 raise TypeError(

143 f"This is not possible to create ChoicesOptionsLink \

144 from object of type {type(link)}"

145 )

146

147 serializer = force_instance(link.serializer_class)

148 # serializer must own defined field

149 if not serializer.fields.get(link.field_name, None):

150 raise AttributeError(

151 f"Serializer class: {link.serializer_class} does not have field: {link.field_name}"

152 )

153 return link

154

155 def _create_link_title(self, link: Union[ChoicesOptionsLink, dict]) -> str:

156 partials = link.viewname.replace("-", " ").replace("_", " ").split(" ")

157 partials = [p.capitalize() for p in partials]

158 return " ".join(partials).title()

159

160 def generate_schema(self, link: Union[ChoicesOptionsLink, Dict[str, Any]]) -> dict:

161 """

162 Generates x-choices link schema.

163 Args:

164 * link: ChoicesOptionsLink instance or dict with link data

165

166 Returns: x-choices link schema

167 """

168

169 if not link:

170 return

171

172 link = self._process_link(link)

173

174 schema = {

175 "operationRef": link.get_url_pattern(),

176 # "parameters": "",

177 "value": link.formatted_value_field,

178 "display": link.formatted_display_field,

179 }

180 return schema