1"""Process URI templates per http://tools.ietf.org/html/rfc6570."""
2
3from __future__ import annotations
4
5import collections
6from typing import Any, TYPE_CHECKING, cast
7
8from .charset import Charset
9from .variable import Variable
10
11if (TYPE_CHECKING):
12 from collections.abc import Iterable, Mapping
13
14
15class ExpansionFailedError(Exception):
16 """Exception thrown when expansions fail."""
17
18 variable: str
19
20 def __init__(self, variable: str) -> None:
21 self.variable = variable
22
23 def __str__(self) -> str:
24 """Convert to string."""
25 return 'Bad expansion: ' + self.variable
26
27
28class Expansion:
29 """
30 Base class for template expansions.
31
32 https://tools.ietf.org/html/rfc6570#section-3
33 """
34
35 def __init__(self) -> None:
36 pass
37
38 @property
39 def variables(self) -> Iterable[Variable]:
40 """Get all variables in this expansion."""
41 return []
42
43 @property
44 def variable_names(self) -> Iterable[str]:
45 """Get the names of all variables in this expansion."""
46 return []
47
48 def _encode(self, value: str, legal: str, pct_encoded: bool) -> str:
49 """Encode a string into legal values."""
50 output = ''
51 index = 0
52 while (index < len(value)):
53 codepoint = value[index]
54 if (codepoint in legal):
55 output += codepoint
56 elif (pct_encoded and ('%' == codepoint)
57 and ((index + 2) < len(value))
58 and (value[index + 1] in Charset.HEX_DIGIT)
59 and (value[index + 2] in Charset.HEX_DIGIT)):
60 output += value[index:index + 3]
61 index += 2
62 else:
63 utf8 = codepoint.encode('utf8')
64 for byte in utf8:
65 output += '%' + Charset.HEX_DIGIT[int(byte / 16)] + Charset.HEX_DIGIT[byte % 16]
66 index += 1
67 return output
68
69 def _uri_encode_value(self, value: str) -> str:
70 """Encode a value into uri encoding."""
71 return self._encode(value, Charset.UNRESERVED, False)
72
73 def _uri_encode_name(self, name: (str | int)) -> str:
74 """Encode a variable name into uri encoding."""
75 return self._encode(str(name), Charset.UNRESERVED + Charset.RESERVED, True) if (name) else ''
76
77 def _join(self, prefix: str, joiner: str, value: str) -> str:
78 """Join a prefix to a value."""
79 if (prefix):
80 return prefix + joiner + value
81 return value
82
83 def _encode_str(self, variable: Variable, name: str, value: str, prefix: str, joiner: str, first: bool) -> str:
84 """Encode a string value for a variable."""
85 if (variable.max_length):
86 if (not first):
87 raise ExpansionFailedError(str(variable))
88 return self._join(prefix, joiner, self._uri_encode_value(value[:variable.max_length]))
89 return self._join(prefix, joiner, self._uri_encode_value(value))
90
91 def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
92 delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
93 """Encode a dict item for a variable."""
94 joiner = '=' if (variable.explode) else ','
95 if (variable.array):
96 name = self._uri_encode_name(key)
97 prefix = (prefix + '[' + name + ']') if (prefix and not first) else name
98 else:
99 prefix = self._join(prefix, '.', self._uri_encode_name(key))
100 return self._encode_var(variable, str(key), item, delim, prefix, joiner, False)
101
102 def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
103 delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
104 """Encode a list item for a variable."""
105 if (variable.array):
106 prefix = prefix + '[' + str(index) + ']' if (prefix) else ''
107 return self._encode_var(variable, '', item, delim, prefix, joiner, False)
108 return self._encode_var(variable, name, item, delim, prefix, '.', False)
109
110 def _encode_var(self, variable: Variable, name: str, value: Any,
111 delim: str = ',', prefix: str = '', joiner: str = '=', first: bool = True) -> (str | None):
112 """Encode a variable."""
113 if (isinstance(value, str)):
114 return self._encode_str(variable, name, value, prefix, joiner, first)
115 elif (isinstance(value, collections.abc.Mapping)):
116 if (len(value)):
117 encoded_items = [self._encode_dict_item(variable, name, key, value[key], delim, prefix, joiner, first)
118 for key in value.keys()]
119 return delim.join([item for item in encoded_items if (item is not None)])
120 return None
121 elif (isinstance(value, collections.abc.Sequence)):
122 if (len(value)):
123 encoded_items = [self._encode_list_item(variable, name, index, item, delim, prefix, joiner, first)
124 for index, item in enumerate(value)]
125 return delim.join([item for item in encoded_items if (item is not None)])
126 return None
127 elif (isinstance(value, bool)):
128 return self._encode_str(variable, name, str(value).lower(), prefix, joiner, first)
129 else:
130 return self._encode_str(variable, name, str(value), prefix, joiner, first)
131
132 def expand(self, values: Mapping[str, Any]) -> (str | None):
133 """Expand values."""
134 return None
135
136 def partial(self, values: Mapping[str, Any]) -> str:
137 """Perform partial expansion."""
138 return ''
139
140
141class Literal(Expansion):
142 """
143 A literal expansion.
144
145 https://tools.ietf.org/html/rfc6570#section-3.1
146 """
147
148 value: str
149
150 def __init__(self, value: str) -> None:
151 super().__init__()
152 self.value = value
153
154 def expand(self, values: Mapping[str, Any]) -> (str | None):
155 """Perform exansion."""
156 return self._encode(self.value, (Charset.UNRESERVED + Charset.RESERVED), True)
157
158 def __str__(self) -> str:
159 """Convert to string."""
160 return self.value
161
162
163class ExpressionExpansion(Expansion):
164 """
165 Base class for expression expansions.
166
167 https://tools.ietf.org/html/rfc6570#section-3.2
168 """
169
170 operator = ''
171 partial_operator = ','
172 output_prefix = ''
173 var_joiner = ','
174 partial_joiner = ','
175
176 vars: list[Variable]
177 trailing_joiner: str = ''
178
179 def __init__(self, variables: str) -> None:
180 super().__init__()
181 if (variables and (variables[-1] in (',', '.', '/', ';', '&'))):
182 self.trailing_joiner = variables[-1]
183 variables = variables[:-1]
184 self.vars = [Variable(var) for var in variables.split(',')]
185
186 @property
187 def variables(self) -> Iterable[Variable]:
188 """Get all variables."""
189 return list(self.vars)
190
191 @property
192 def variable_names(self) -> Iterable[str]:
193 """Get names of all variables."""
194 return [var.name for var in self.vars]
195
196 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
197 """Expand a single variable."""
198 return self._encode_var(variable, self._uri_encode_name(variable.name), value)
199
200 def expand(self, values: Mapping[str, Any]) -> (str | None):
201 """Expand all variables, skip missing values."""
202 expanded_vars: list[str] = []
203 for var in self.vars:
204 value = values.get(var.key, var.default)
205 if (value is not None):
206 expanded_var = self._expand_var(var, value)
207 if (expanded_var is not None):
208 expanded_vars.append(expanded_var)
209 if (expanded_vars):
210 return ((self.output_prefix if (not self.trailing_joiner) else '') + self.var_joiner.join(expanded_vars)
211 + self.trailing_joiner)
212 return None
213
214 def partial(self, values: Mapping[str, Any]) -> str:
215 """Expand all variables, replace missing values with expansions."""
216 expanded_vars: list[str] = []
217 missing_vars: list[Variable] = []
218 result: list[tuple[(list[str] | None), (list[Variable] | None)]] = []
219 for var in self.vars:
220 value = values.get(var.name, var.default)
221 if (value is not None):
222 expanded_var = self._expand_var(var, value)
223 if (expanded_var is not None):
224 if (missing_vars):
225 result.append((None, missing_vars))
226 missing_vars = []
227 expanded_vars.append(expanded_var)
228 else:
229 if (expanded_vars):
230 result.append((expanded_vars, None))
231 expanded_vars = []
232 missing_vars.append(var)
233 if (expanded_vars):
234 result.append((expanded_vars, None))
235 if (missing_vars):
236 result.append((None, missing_vars))
237
238 output: str = ''
239 first = True
240 for index, (expanded, missing) in enumerate(result):
241 last = (index == (len(result) - 1))
242 if (expanded):
243 output += ((self.output_prefix if (first and (not self.trailing_joiner)) else '')
244 + self.var_joiner.join(expanded) + self.trailing_joiner)
245 else:
246 output += ((self.output_prefix if (first and not last) else (self.var_joiner if (not last) else ''))
247 + '{' + (self.operator if (first) else self.partial_operator)
248 + ','.join([str(var) for var in cast('list[Variable]', missing)])
249 + (self.partial_joiner if (not last) else '') + '}')
250 first = False
251 return output
252
253 def __str__(self) -> str:
254 """Convert to string."""
255 return ('{' + self.operator + ','.join([str(var) for var in self.vars]) + self.trailing_joiner + '}')
256
257
258class SimpleExpansion(ExpressionExpansion):
259 """
260 Simple String expansion {var}.
261
262 https://tools.ietf.org/html/rfc6570#section-3.2.2
263
264 """
265
266 def __init__(self, variables: str) -> None:
267 super().__init__(variables)
268
269
270class ReservedExpansion(ExpressionExpansion):
271 """
272 Reserved Expansion {+var}.
273
274 https://tools.ietf.org/html/rfc6570#section-3.2.3
275 """
276
277 operator = '+'
278 partial_operator = ',+'
279
280 def __init__(self, variables: str) -> None:
281 super().__init__(variables[1:])
282
283 def _uri_encode_value(self, value: str) -> str:
284 """Encode a value into uri encoding."""
285 return self._encode(value, (Charset.UNRESERVED + Charset.RESERVED), True)
286
287
288class FragmentExpansion(ReservedExpansion):
289 """
290 Fragment Expansion {#var}.
291
292 https://tools.ietf.org/html/rfc6570#section-3.2.4
293 """
294
295 operator = '#'
296 output_prefix = '#'
297
298 def __init__(self, variables: str) -> None:
299 super().__init__(variables)
300
301
302class LabelExpansion(ExpressionExpansion):
303 """
304 Label Expansion with Dot-Prefix {.var}.
305
306 https://tools.ietf.org/html/rfc6570#section-3.2.5
307 """
308
309 operator = '.'
310 partial_operator = '.'
311 output_prefix = '.'
312 var_joiner = '.'
313 partial_joiner = '.'
314
315 def __init__(self, variables: str) -> None:
316 super().__init__(variables[1:])
317
318 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
319 """Expand a single variable."""
320 return self._encode_var(variable, self._uri_encode_name(variable.name), value,
321 delim=('.' if variable.explode else ','))
322
323
324class PathExpansion(ExpressionExpansion):
325 """
326 Path Segment Expansion {/var}.
327
328 https://tools.ietf.org/html/rfc6570#section-3.2.6
329 """
330
331 operator = '/'
332 partial_operator = '/'
333 output_prefix = '/'
334 var_joiner = '/'
335 partial_joiner = '/'
336
337 def __init__(self, variables: str) -> None:
338 super().__init__(variables[1:])
339
340 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
341 """Expand a single variable."""
342 return self._encode_var(variable, self._uri_encode_name(variable.name), value,
343 delim=('/' if variable.explode else ','))
344
345
346class PathStyleExpansion(ExpressionExpansion):
347 """
348 Path-Style Parameter Expansion {;var}.
349
350 https://tools.ietf.org/html/rfc6570#section-3.2.7
351 """
352
353 operator = ';'
354 partial_operator = ';'
355 output_prefix = ';'
356 var_joiner = ';'
357 partial_joiner = ';'
358
359 def __init__(self, variables: str) -> None:
360 super().__init__(variables[1:])
361
362 def _encode_str(self, variable: Variable, name: str, value: Any, prefix: str, joiner: str, first: bool) -> str:
363 """Encode a string for a variable."""
364 if (variable.array):
365 if (name):
366 prefix = prefix + '[' + name + ']' if (prefix) else name
367 elif (variable.explode):
368 prefix = self._join(prefix, '.', name)
369 return super()._encode_str(variable, name, value, prefix, joiner, first)
370
371 def _encode_dict_item(self, variable: Variable, name: str, key: (int | str), item: Any,
372 delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
373 """Encode a dict item for a variable."""
374 if (variable.array):
375 if (name):
376 prefix = prefix + '[' + name + ']' if (prefix) else name
377 if (prefix and not first):
378 prefix = (prefix + '[' + self._uri_encode_name(key) + ']')
379 else:
380 prefix = self._uri_encode_name(key)
381 elif (variable.explode):
382 prefix = self._join(prefix, '.', name) if (not first) else ''
383 else:
384 prefix = self._join(prefix, '.', self._uri_encode_name(key))
385 joiner = ','
386 return self._encode_var(variable, self._uri_encode_name(key) if (not variable.array) else '', item,
387 delim, prefix, joiner, False)
388
389 def _encode_list_item(self, variable: Variable, name: str, index: int, item: Any,
390 delim: str, prefix: str, joiner: str, first: bool) -> (str | None):
391 """Encode a list item for a variable."""
392 if (variable.array):
393 if (name):
394 prefix = prefix + '[' + name + ']' if (prefix) else name
395 return self._encode_var(variable, str(index), item, delim, prefix, joiner, False)
396 return self._encode_var(variable, name, item, delim, prefix, '=' if (variable.explode) else '.', False)
397
398 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
399 """Expand a single variable."""
400 if (variable.explode):
401 return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=';')
402 value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
403 return (self._uri_encode_name(variable.name) + '=' + value) if (value) else variable.name
404
405
406class FormStyleQueryExpansion(PathStyleExpansion):
407 """
408 Form-Style Query Expansion {?var}.
409
410 https://tools.ietf.org/html/rfc6570#section-3.2.8
411 """
412
413 operator = '?'
414 partial_operator = '&'
415 output_prefix = '?'
416 var_joiner = '&'
417 partial_joiner = '&'
418
419 def __init__(self, variables: str) -> None:
420 super().__init__(variables)
421
422 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
423 """Expand a single variable."""
424 if (variable.explode):
425 return self._encode_var(variable, self._uri_encode_name(variable.name), value, delim='&')
426 value = self._encode_var(variable, self._uri_encode_name(variable.name), value, delim=',')
427 return (self._uri_encode_name(variable.name) + '=' + value) if (value is not None) else None
428
429
430class FormStyleQueryContinuation(FormStyleQueryExpansion):
431 """
432 Form-Style Query Continuation {&var}.
433
434 https://tools.ietf.org/html/rfc6570#section-3.2.9
435 """
436
437 operator = '&'
438 output_prefix = '&'
439
440 def __init__(self, variables: str) -> None:
441 super().__init__(variables)
442
443# non-standard extension
444
445
446class CommaExpansion(ExpressionExpansion):
447 """
448 Label Expansion with Comma-Prefix {,var}.
449
450 Non-standard extension to support partial expansions.
451 """
452
453 operator = ','
454 output_prefix = ','
455
456 def __init__(self, variables: str) -> None:
457 super().__init__(variables[1:])
458
459 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
460 """Expand a single variable."""
461 return self._encode_var(variable, self._uri_encode_name(variable.name), value,
462 delim=('.' if variable.explode else ','))
463
464
465class ReservedCommaExpansion(ReservedExpansion):
466 """
467 Reserved Expansion with comma prefix {,+var}.
468
469 Non-standard extension to support partial expansions.
470 """
471
472 operator = ',+'
473 output_prefix = ','
474
475 def __init__(self, variables: str) -> None:
476 super().__init__(variables[1:])
477
478 def _expand_var(self, variable: Variable, value: Any) -> (str | None):
479 """Expand a single variable."""
480 return self._encode_var(variable, self._uri_encode_name(variable.name), value,
481 delim=('.' if variable.explode else ','))