Coverage for audoma/links.py: 82%
80 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-08 06:12 +0000
« prev ^ index » next coverage.py v6.4.2, created at 2022-08-08 06:12 +0000
1"""
2This module is responsible for creating x-choices links.
3Such links may be used to generate choices enums for fields.
5If some field in the serializer has this attribute, it
6will be used to generate choices for this field.
7In other words choices should be limited to values
8available under passed x-choices link.
9"""
11import re
12from dataclasses import dataclass
13from typing import (
14 Any,
15 Dict,
16 Type,
17 Union,
18)
20from drf_spectacular.plumbing import force_instance
21from rest_framework.serializers import BaseSerializer
23from django.urls import (
24 NoReverseMatch,
25 URLResolver,
26)
27from django.urls.resolvers import get_resolver
30def get_endpoint_pattern(endpoint_name: str, urlconf=None) -> str:
31 """
32 This methods retrieves url pattern of the endpoint by given endpoint_name.
34 Args:
35 * endpoint_name: name of the endpoint
36 * urlconf: urlconf to use
38 Returns: url pattern of the endpoint
39 """
40 resolver = get_resolver(urlconf)
41 patterns = resolver.url_patterns
42 new_patterns = []
43 resolvers = []
45 for pattern in patterns:
46 if isinstance(pattern, URLResolver):
47 resolvers.append(pattern)
48 continue
49 new_patterns.append(pattern)
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)
60 possibilities = list(filter(lambda p: p.name == endpoint_name, new_patterns))
62 for p in possibilities:
63 if "format" not in p.pattern.regex.pattern:
64 return str(p.pattern)
66 raise NoReverseMatch(f"There is no pattern with name {endpoint_name}")
69@dataclass
70class ChoicesOptionsLink:
71 """
72 Helper dataclass, which holds data abot x-choices link.
73 """
75 field_name: str
76 viewname: str
77 value_field: str
78 display_field: str
79 serializer_class: Type[BaseSerializer]
81 description: str = ""
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.
88 Args:
89 * field: field name
91 Returns: formatted field name
92 """
93 if "$" in field:
94 # than we presume that there has been given full pointer
95 return field
97 field = f"$response.body#results/*/{field}"
99 return field
101 @property
102 def formatted_value_field(self) -> str:
103 return self._format_param_field(self.value_field)
105 @property
106 def formatted_display_field(self) -> str:
107 return self._format_param_field(self.display_field)
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 )
120 params = re.search(r"<.*>", pattern)
121 if params:
122 for param in params:
123 param = param.string.replace("<", "{").replace(">", "}")
125 regexes = re.search(r"\(.*\)", pattern)
126 for x, regex in enumerate(regexes):
127 pattern = pattern.replace(regex.string, params[x])
129 pattern = f"#/paths/{pattern}"
131 return pattern
134class ChoicesOptionsLinkSchemaGenerator:
135 def _process_link(
136 self, link: Union[ChoicesOptionsLink, Dict[str, Any]]
137 ) -> ChoicesOptionsLink:
138 if isinstance(link, dict):
139 link = ChoicesOptionsLink(**link)
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 )
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
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()
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
166 Returns: x-choices link schema
167 """
169 if not link:
170 return
172 link = self._process_link(link)
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