diff --git a/tests/fixtures/LICENSE b/tests/fixtures/LICENSE new file mode 100644 index 0000000..7e79d94 --- /dev/null +++ b/tests/fixtures/LICENSE @@ -0,0 +1,14 @@ + + Copyright 2011- The Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md index ffe46cb..1fa3f4b 100644 --- a/tests/fixtures/README.md +++ b/tests/fixtures/README.md @@ -1,10 +1,9 @@ -These test are borrowed from https://github.com/uri-templates/uritemplate-test -at commit: fdd5d611a849b922c2ff40fc3997fd265dd14c02 + URI Template Tests ================== This is a set of tests for implementations of -[RFC6570](http://tools.ietf.org/html/rfc6570) - URI Template. It is designed +[RFC6570](https://datatracker.ietf.org/doc/html/rfc6570) - URI Template. It is designed to be reused by any implementation, to improve interoperability and implementation quality. diff --git a/tests/fixtures/extended-tests.json b/tests/fixtures/extended-tests.json index ad43ad6..df27d95 100644 --- a/tests/fixtures/extended-tests.json +++ b/tests/fixtures/extended-tests.json @@ -28,18 +28,8 @@ "testcases":[ [ "{/id*}" , "/person" ], - [ "{/id*}{?fields,first_name,last.name,token}" , [ - "/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345", - "/person?fields=id,picture,name&first_name=John&last.name=Doe&token=12345", - "/person?fields=picture,name,id&first_name=John&last.name=Doe&token=12345", - "/person?fields=picture,id,name&first_name=John&last.name=Doe&token=12345", - "/person?fields=name,picture,id&first_name=John&last.name=Doe&token=12345", - "/person?fields=name,id,picture&first_name=John&last.name=Doe&token=12345"] - ], - ["/search.{format}{?q,geocode,lang,locale,page,result_type}", - [ "/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5", - "/search.json?q=URI%20Templates&geocode=-122.427,37.76&lang=en&page=5"] - ], + [ "{/id*}{?fields,first_name,last.name,token}","/person?fields=id,name,picture&first_name=John&last.name=Doe&token=12345"], + ["/search.{format}{?q,geocode,lang,locale,page,result_type}","/search.json?q=URI%20Templates&geocode=37.76,-122.427&lang=en&page=5"], ["/test{/Some%20Thing}", "/test/foo" ], ["/set{?number}", "/set?number=6"], ["/loc{?long,lat}" , "/loc?long=37.76&lat=-122.427"], @@ -67,21 +57,8 @@ }, "testcases":[ - [ "{/id*}" , ["/person/albums","/albums/person"] ], - [ "{/id*}{?fields,token}" , [ - "/person/albums?fields=id,name,picture&token=12345", - "/person/albums?fields=id,picture,name&token=12345", - "/person/albums?fields=picture,name,id&token=12345", - "/person/albums?fields=picture,id,name&token=12345", - "/person/albums?fields=name,picture,id&token=12345", - "/person/albums?fields=name,id,picture&token=12345", - "/albums/person?fields=id,name,picture&token=12345", - "/albums/person?fields=id,picture,name&token=12345", - "/albums/person?fields=picture,name,id&token=12345", - "/albums/person?fields=picture,id,name&token=12345", - "/albums/person?fields=name,picture,id&token=12345", - "/albums/person?fields=name,id,picture&token=12345"] - ] + [ "{/id*}" , "/person/albums" ], + [ "{/id*}{?fields,token}" , "/person/albums?fields=id,name,picture&token=12345" ] ] }, "Additional Examples 3: Empty Variables":{ @@ -114,5 +91,59 @@ [ "{?1337*}", "?1337=leet&1337=as&1337=it&1337=can&1337=be"], [ "{?german*}", [ "?11=elf&12=zw%C3%B6lf", "?12=zw%C3%B6lf&11=elf"] ] ] + }, + "Additional Examples 5: Explode Combinations":{ + "variables" : { + "id" : "admin", + "token" : "12345", + "tab" : "overview", + "keys" : { + "key1": "val1", + "key2": "val2" + } + }, + "testcases":[ + [ "{?id,token,keys*}", [ + "?id=admin&token=12345&key1=val1&key2=val2", + "?id=admin&token=12345&key2=val2&key1=val1"] + ], + [ "{/id}{?token,keys*}", [ + "/admin?token=12345&key1=val1&key2=val2", + "/admin?token=12345&key2=val2&key1=val1"] + ], + [ "{?id,token}{&keys*}", [ + "?id=admin&token=12345&key1=val1&key2=val2", + "?id=admin&token=12345&key2=val2&key1=val1"] + ], + [ "/user{/id}{?token,tab}{&keys*}", [ + "/user/admin?token=12345&tab=overview&key1=val1&key2=val2", + "/user/admin?token=12345&tab=overview&key2=val2&key1=val1"] + ] + ] + }, + "Additional Examples 6: Reserved Expansion":{ + "variables" : { + "id" : "admin%2F", + "not_pct" : "%foo", + "list" : ["red%25", "%2Fgreen", "blue "], + "keys" : { + "key1": "val1%2F", + "key2": "val2%2F" + } + }, + "testcases": [ + ["{+id}", "admin%2F"], + ["{#id}", "#admin%2F"], + ["{id}", "admin%252F"], + ["{+not_pct}", "%25foo"], + ["{#not_pct}", "#%25foo"], + ["{not_pct}", "%25foo"], + ["{+list}", "red%25,%2Fgreen,blue%20"], + ["{#list}", "#red%25,%2Fgreen,blue%20"], + ["{list}", "red%2525,%252Fgreen,blue%20"], + ["{+keys}", "key1,val1%2F,key2,val2%2F"], + ["{#keys}", "#key1,val1%2F,key2,val2%2F"], + ["{keys}", "key1,val1%252F,key2,val2%252F"] + ] } } diff --git a/tests/fixtures/spec-examples-by-section.json b/tests/fixtures/spec-examples-by-section.json index 5aef182..01c8eca 100644 --- a/tests/fixtures/spec-examples-by-section.json +++ b/tests/fixtures/spec-examples-by-section.json @@ -1,4 +1,13 @@ { + "2.1 Literals" : + { + "variables": { + "count" : ["one", "two", "three"] + }, + "testcases" : [ + ["'{count}'", "'one,two,three'"] + ] + }, "3.2.1 Variable Expansion" : { "variables": { @@ -17,7 +26,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -50,7 +59,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -104,7 +113,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -161,7 +170,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -203,7 +212,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -255,7 +264,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -307,7 +316,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -358,7 +367,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ @@ -406,7 +415,7 @@ "x" : "1024", "y" : "768", "empty" : "", - "empty_keys" : [], + "empty_keys" : {}, "undef" : null }, "testcases" : [ diff --git a/tests/fixtures/spec-examples.json b/tests/fixtures/spec-examples.json index 2e8e942..652acfa 100644 --- a/tests/fixtures/spec-examples.json +++ b/tests/fixtures/spec-examples.json @@ -8,6 +8,7 @@ }, "testcases" : [ ["{var}", "value"], + ["'{var}'", "'value'"], ["{hello}", "Hello%20World%21"] ] }, diff --git a/tests/test_from_fixtures.py b/tests/test_from_fixtures.py index cad9ce5..bbb4645 100644 --- a/tests/test_from_fixtures.py +++ b/tests/test_from_fixtures.py @@ -44,8 +44,8 @@ def _get_test( ) -> t.Tuple[ExampleVariables, ExampleTemplatesAndResults]: test = t.cast(ExampleWithVariables, self.examples.get(section, {})) return ( - t.cast(ExampleVariables, test.get("variables", {})), - t.cast(ExampleTemplatesAndResults, test.get("testcases", [])), + test.get("variables", {}), + test.get("testcases", []), ) def _test(self, testname: str) -> None: @@ -53,7 +53,11 @@ def _test(self, testname: str) -> None: for template, expected in testcases: expected_templates = expected_set(expected) expanded = uritemplate.expand(template, variables) - assert expanded in expected_templates + assert expanded in expected_templates, ( # nosec + f"expanded {template!r} with {variables!r} " + f"and got {expanded!r} but expected one of " + f"{expected_templates!r}" + ) class TestSpecExamples(FixtureMixin): @@ -134,3 +138,11 @@ def test_additional_examples_3(self) -> None: def test_additional_examples_4(self) -> None: """Check Additional Examples 4.""" self._test("Additional Examples 4: Numeric Keys") + + def test_additional_examples_5(self) -> None: + """Check Additional Examples 5.""" + self._test("Additional Examples 5: Explode Combinations") + + def test_additional_examples_6(self) -> None: + """Check Additional Examples 6.""" + self._test("Additional Examples 6: Reserved Expansion") diff --git a/tests/test_uritemplate.py b/tests/test_uritemplate.py index ca6363d..dc185d5 100644 --- a/tests/test_uritemplate.py +++ b/tests/test_uritemplate.py @@ -562,7 +562,7 @@ def test_semi_path_expansion(self) -> None: self.assertEqual( v._semi_path_expansion("foo", None, False, False), None ) - t.variables[0].operator = "?" + t.variables[0].operator = variable.Operator.form_style_query self.assertEqual( v._semi_path_expansion("foo", ["bar", "bogus"], True, False), "foo=bar&foo=bogus", @@ -588,50 +588,6 @@ def test_no_mutate(self) -> None: self.assertEqual(args, {}) -class TestURIVariable(unittest.TestCase): - def setUp(self) -> None: - self.v = variable.URIVariable("{foo}") - - def test_post_parse(self) -> None: - v = self.v - self.assertEqual(v.join_str, ",") - self.assertEqual(v.operator, "") - self.assertEqual(v.safe, "") - self.assertEqual(v.start, "") - - def test_post_parse_plus(self) -> None: - v = self.v - v.operator = "+" - v.post_parse() - self.assertEqual(v.join_str, ",") - self.assertEqual(v.safe, variable.URIVariable.reserved) - self.assertEqual(v.start, "") - - def test_post_parse_octothorpe(self) -> None: - v = self.v - v.operator = "#" - v.post_parse() - self.assertEqual(v.join_str, ",") - self.assertEqual(v.safe, variable.URIVariable.reserved) - self.assertEqual(v.start, "#") - - def test_post_parse_question(self) -> None: - v = self.v - v.operator = "?" - v.post_parse() - self.assertEqual(v.join_str, "&") - self.assertEqual(v.safe, "") - self.assertEqual(v.start, "?") - - def test_post_parse_ampersand(self) -> None: - v = self.v - v.operator = "&" - v.post_parse() - self.assertEqual(v.join_str, "&") - self.assertEqual(v.safe, "") - self.assertEqual(v.start, "&") - - class TestVariableModule(unittest.TestCase): def test_is_list_of_tuples(self) -> None: a_list = [(1, 2), (3, 4)] diff --git a/uritemplate/variable.py b/uritemplate/variable.py index 41134a7..02e33e0 100644 --- a/uritemplate/variable.py +++ b/uritemplate/variable.py @@ -16,6 +16,8 @@ """ import collections.abc +import enum +import string import typing as t import urllib.parse @@ -30,6 +32,159 @@ VariableValueDict = t.Dict[str, VariableValue] +_UNRESERVED_CHARACTERS: t.Final[str] = ( + f"{string.ascii_letters}{string.digits}~-_." +) +_GEN_DELIMS: t.Final[str] = ":/?#[]@" +_SUB_DELIMS: t.Final[str] = "!$&'()*+,;=" +_RESERVED_CHARACTERS: t.Final[str] = f"{_GEN_DELIMS}{_SUB_DELIMS}" + + +class Operator(enum.Enum): + # Section 2.2. Expressions + # expression = "{" [ operator ] variable-list "}" + # operator = op-level2 / op-level3 / op-reserve + # op-level2 = "+" / "#" + # op-level3 = "." / "/" / ";" / "?" / "&" + # op-reserve = "=" / "," / "!" / "@" / "|" + default = "" # 3.2.2. Simple String Expansiona: {var} + # Operator Level 2 (op-level2) + reserved = "+" # 3.2.3. Reserved Expansion: {+var} + fragment = "#" # 3.2.4. Fragment Expansion: {#var} + # Operator Level 3 (op-level3) + # 3.2.5. Label Expansion with Dot-Prefix: {.var} + label_with_dot_prefix = "." + path_segment = "/" # 3.2.6. Path Segment Expansion: {/var} + path_style_parameter = ( + ";" # 3.2.7. Path-Style Parameter Expansion: {;var} + ) + form_style_query = "?" # 3.2.8. Form-Style Query Expansion: {?var} + # 3.2.9. Form-Style Query Continuation: {&var} + form_style_query_continuation = "&" + # Reserved Operators (op-reserve) + reserved_eq = "=" + reserved_comma = "," + reserved_bang = "!" + reserved_at = "@" + reserved_pipe = "|" + + def reserved_characters(self) -> str: + # TODO: Re-enable after un-commenting 3.9 + # match self: + # case Operator.reserved: + # return _RESERVED_CHARACTERS + "%" + # # case Operator.default | Operator.reserved | Operator.fragment: + # case Operator.fragment: + # return _RESERVED_CHARACTERS + # case _: + # return "" + if self == Operator.reserved: + return _RESERVED_CHARACTERS + "%" + if self == Operator.fragment: + return _RESERVED_CHARACTERS + return "" + + def expansion_separator(self) -> str: + """Identify the separator used during expansion. + + Per `Section 3.2.1. Variable Expansion`_: + + ====== =========== ========= + Type Separator + ====== =========== ========= + ``","`` (default) + ``+`` ``","`` + ``#`` ``","`` + ``.`` ``"."`` + ``/`` ``"/"`` + ``;`` ``";"`` + ``?`` ``"&"`` + ``&`` ``"&"`` + ====== =========== ========= + + .. _`Section 3.2.1. Variable Expansion`: + https://www.rfc-editor.org/rfc/rfc6570#section-3.2.1 + """ + if self == Operator.label_with_dot_prefix: + return "." + if self == Operator.path_segment: + return "/" + if self == Operator.path_style_parameter: + return ";" + if ( + self == Operator.form_style_query + or self == Operator.form_style_query_continuation + ): + return "&" + # if self == Operator.reserved or self == Operator.fragment: + # return "," + return "," + # match self: + # case Operator.label_with_dot_prefix: + # return "." + # case Operator.path_segment: + # return "/" + # case Operator.path_style_parameter: + # return ";" + # case ( + # Operator.form_style_query | + # Operator.form_style_query_continuation + # ): + # return "&" + # case Operator.reserved | Operator.fragment: + # return "," + # case _: + # return "," + + def variable_prefix(self) -> str: + if self == Operator.reserved: + return "" + return t.cast(str, self.value) + # match self: + # case Operator.reserved: + # return "" + # case _: + # return t.cast(str, self.value) + + def _always_quote(self, value: str) -> str: + return quote(value, "") + + def _only_quote_unquoted_characters(self, value: str) -> str: + if urllib.parse.unquote(value) == value: + return quote(value, _RESERVED_CHARACTERS) + return value + + def quote(self, value: t.Any) -> str: + if not isinstance(value, (str, bytes)): + value = str(value) + if isinstance(value, bytes): + value = value.decode() + + if self == Operator.reserved or self == Operator.fragment: + return self._only_quote_unquoted_characters(value) + return self._always_quote(value) + + @staticmethod + def from_string(s: str) -> "Operator": + return _operators.get(s, Operator.default) + + +_operators: t.Final[t.Dict[str, Operator]] = { + "+": Operator.reserved, + "#": Operator.fragment, + ".": Operator.label_with_dot_prefix, + "/": Operator.path_segment, + ";": Operator.path_style_parameter, + "?": Operator.form_style_query, + "&": Operator.form_style_query_continuation, + "!": Operator.reserved_bang, + "|": Operator.reserved_pipe, + "@": Operator.reserved_at, + "=": Operator.reserved_eq, + ",": Operator.reserved_comma, +} + + class URIVariable: """This object validates everything inside the URITemplate object. @@ -49,16 +204,11 @@ class URIVariable: """ - operators = ("+", "#", ".", "/", ";", "?", "&", "|", "!", "@") - reserved = ":/?#[]@!$&'()*+,;=" - def __init__(self, var: str): #: The original string that comes through with the variable self.original: str = var #: The operator for the variable - self.operator: str = "" - #: List of safe characters when quoting the string - self.safe: str = "" + self.operator: Operator = Operator.default #: List of variables in this variable self.variables: t.List[t.Tuple[str, t.MutableMapping[str, t.Any]]] = ( [] @@ -69,7 +219,6 @@ def __init__(self, var: str): self.defaults: t.MutableMapping[str, ScalarVariableValue] = {} # Parse the variable itself. self.parse() - self.post_parse() def __repr__(self) -> str: return "URIVariable(%s)" % self @@ -88,25 +237,22 @@ def parse(self) -> None: """ var_list_str = self.original - if self.original[0] in URIVariable.operators: - self.operator = self.original[0] + if (operator_str := self.original[0]) in _operators: + self.operator = Operator.from_string(operator_str) var_list_str = self.original[1:] - if self.operator in URIVariable.operators[:2]: - self.safe = URIVariable.reserved - var_list = var_list_str.split(",") for var in var_list: default_val = None name = var + # NOTE(sigmavirus24): This is from an earlier draft but is not in + # the specification if "=" in var: name, default_val = tuple(var.split("=", 1)) - explode = False - if name.endswith("*"): - explode = True - name = name[:-1] + explode = name.endswith("*") + name = name.rstrip("*") prefix: t.Optional[int] = None if ":" in name: @@ -122,27 +268,6 @@ def parse(self) -> None: self.variable_names = [varname for (varname, _) in self.variables] - def post_parse(self) -> None: - """Set ``start``, ``join_str`` and ``safe`` attributes. - - After parsing the variable, we need to set up these attributes and it - only makes sense to do it in a more easily testable way. - """ - self.safe = "" - self.start = self.join_str = self.operator - if self.operator == "+": - self.start = "" - if self.operator in ("+", "#", ""): - self.join_str = "," - if self.operator == "#": - self.start = "#" - if self.operator == "?": - self.start = "?" - self.join_str = "&" - - if self.operator in ("+", "#"): - self.safe = URIVariable.reserved - def _query_expansion( self, name: str, @@ -156,17 +281,18 @@ def _query_expansion( tuples, items = is_list_of_tuples(value) - safe = self.safe + safe = self.operator.reserved_characters() + _quote = self.operator.quote if list_test(value) and not tuples: if not value: return None value = t.cast(t.Sequence[ScalarVariableValue], value) if explode: - return self.join_str.join( - f"{name}={quote(v, safe)}" for v in value + return self.operator.expansion_separator().join( + f"{name}={_quote(v)}" for v in value ) else: - value = ",".join(quote(v, safe) for v in value) + value = ",".join(_quote(v) for v in value) return f"{name}={value}" if dict_test(value) or tuples: @@ -175,19 +301,19 @@ def _query_expansion( value = t.cast(t.Mapping[str, ScalarVariableValue], value) items = items or sorted(value.items()) if explode: - return self.join_str.join( - f"{quote(k, safe)}={quote(v, safe)}" for k, v in items + return self.operator.expansion_separator().join( + f"{quote(k, safe)}={_quote(v)}" for k, v in items ) else: value = ",".join( - f"{quote(k, safe)},{quote(v, safe)}" for k, v in items + f"{quote(k, safe)},{_quote(v)}" for k, v in items ) return f"{name}={value}" if value: value = t.cast(t.Text, value) value = value[:prefix] if prefix else value - return f"{name}={quote(value, safe)}" + return f"{name}={_quote(value)}" return name + "=" def _label_path_expansion( @@ -202,8 +328,8 @@ def _label_path_expansion( Expands for operators: '/', '.' """ - join_str = self.join_str - safe = self.safe + join_str = self.operator.expansion_separator() + safe = self.operator.reserved_characters() if value is None or ( not isinstance(value, (str, int, float, complex)) @@ -218,7 +344,9 @@ def _label_path_expansion( join_str = "," value = t.cast(t.Sequence[ScalarVariableValue], value) - fragments = [quote(v, safe) for v in value if v is not None] + fragments = [ + self.operator.quote(v) for v in value if v is not None + ] return join_str.join(fragments) if fragments else None if dict_test(value) or tuples: @@ -230,7 +358,7 @@ def _label_path_expansion( join_str = "," expanded = join_str.join( - format_str % (quote(k, safe), quote(v, safe)) + format_str % (quote(k, safe), self.operator.quote(v)) for k, v in items if v is not None ) @@ -238,7 +366,7 @@ def _label_path_expansion( value = t.cast(t.Text, value) value = value[:prefix] if prefix else value - return quote(value, safe) + return self.operator.quote(value) def _semi_path_expansion( self, @@ -248,15 +376,12 @@ def _semi_path_expansion( prefix: t.Optional[int], ) -> t.Optional[str]: """Expansion method for ';' operator.""" - join_str = self.join_str - safe = self.safe + join_str = self.operator.expansion_separator() + safe = self.operator.reserved_characters() if value is None: return None - if self.operator == "?": - join_str = "&" - tuples, items = is_list_of_tuples(value) if list_test(value) and not tuples: @@ -276,13 +401,13 @@ def _semi_path_expansion( if explode: return join_str.join( - f"{quote(k, safe)}={quote(v, safe)}" + f"{quote(k, safe)}={self.operator.quote(v)}" for k, v in items if v is not None ) else: expanded = ",".join( - f"{quote(k, safe)},{quote(v, safe)}" + f"{quote(k, safe)},{self.operator.quote(v)}" for k, v in items if v is not None ) @@ -291,7 +416,7 @@ def _semi_path_expansion( value = t.cast(t.Text, value) value = value[:prefix] if prefix else value if value: - return f"{name}={quote(value, safe)}" + return f"{name}={self.operator.quote(value)}" return name @@ -309,7 +434,7 @@ def _string_expansion( if list_test(value) and not tuples: value = t.cast(t.Sequence[ScalarVariableValue], value) - return ",".join(quote(v, self.safe) for v in value) + return ",".join(self.operator.quote(v) for v in value) if dict_test(value) or tuples: value = t.cast(t.Mapping[str, ScalarVariableValue], value) @@ -317,13 +442,13 @@ def _string_expansion( format_str = "%s=%s" if explode else "%s,%s" return ",".join( - format_str % (quote(k, self.safe), quote(v, self.safe)) + format_str % (self.operator.quote(k), self.operator.quote(v)) for k, v in items ) value = t.cast(t.Text, value) value = value[:prefix] if prefix else value - return quote(value, self.safe) + return self.operator.quote(value) def expand( self, var_dict: t.Optional[VariableValueDict] = None @@ -367,14 +492,30 @@ def expand( continue expanded = None - if self.operator in ("/", "."): + if ( + self.operator == Operator.path_segment + or self.operator == Operator.label_with_dot_prefix + ): expansion = self._label_path_expansion - elif self.operator in ("?", "&"): + elif ( + self.operator == Operator.form_style_query + or self.operator == Operator.form_style_query_continuation + ): expansion = self._query_expansion - elif self.operator == ";": + elif self.operator == Operator.path_style_parameter: expansion = self._semi_path_expansion else: expansion = self._string_expansion + # match self.operator: + # case Operator.path_segment | Operator.label_with_dot_prefix: + # expansion = self._label_path_expansion + # case (Operator.form_style_query | + # Operator.form_style_query_continuation): + # expansion = self._query_expansion + # case Operator.path_style_parameter: + # expansion = self._semi_path_expansion + # case _: + # expansion = self._string_expansion expanded = expansion(name, value, opts["explode"], opts["prefix"]) @@ -383,7 +524,10 @@ def expand( value = "" if return_values: - value = self.start + self.join_str.join(return_values) + value = ( + self.operator.variable_prefix() + + self.operator.expansion_separator().join(return_values) + ) return {self.original: value}