Repository: pallets/werkzeug (python)

Issue:
`any` converter should validate value to `url_for`
Expectation:
When using `url_for` for an endpoint with an `<any(foo, bar):arg>` argument in its route, it will raise an error for arguments that are not part of the any statement.

Actual result:
It returns an invalid URL


Example:
```
from flask import Flask, url_for, redirect

app = Flask(__name__)

@app.route("/any/<any(foo, bar):part>")
def url_with_any(part):
    return part

@app.route("/")
def index():
    return redirect(url_for("url_with_any", part="doesnotexist")) # this will 404

```

Environment:

- Python version: Python 3.10.4
- Flask version: Flask 2.1.1


Pull request description:
`any` converter validates the value to `url_for`
Add method for to_url for AnyConverter. Add protection for AnyConverter to raise a ValueError for items not specified in the rule when the build method is called by running a `value in set(items)` check.

Add test for assertions where values do match to items in the rule, and assertions for something invalid.

- fixes #2388 

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index c5247876..e9e0418d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -12,6 +12,8 @@ Version 2.2.0
     machine. :pr:`2433`
 -   Names within options headers are always converted to lowercase. This
     matches :rfc:`6266` that the case is not relevant. :issue:`2442`
+-   ``AnyConverter`` validates the value passed for it when building
+    URLs. :issue:`2388`
 
 
 Version 2.1.2
diff --git a/src/werkzeug/routing/converters.py b/src/werkzeug/routing/converters.py
index 24d62e44..bbad29d7 100644
--- a/src/werkzeug/routing/converters.py
+++ b/src/werkzeug/routing/converters.py
@@ -81,14 +81,25 @@ class AnyConverter(BaseConverter):
     :param map: the :class:`Map`.
     :param items: this function accepts the possible items as positional
                   arguments.
+
+    .. versionchanged:: 2.2
+        Value is validated when building a URL.
     """
 
     part_isolating = True
 
     def __init__(self, map: "Map", *items: str) -> None:
         super().__init__(map)
+        self.items = set(items)
         self.regex = f"(?:{'|'.join([re.escape(x) for x in items])})"
 
+    def to_url(self, value: t.Any) -> str:
+        if value in self.items:
+            return str(value)
+
+        valid_values = ", ".join(f"'{item}'" for item in sorted(self.items))
+        raise ValueError(f"'{value}' is not one of {valid_values}")
+
 
 class PathConverter(BaseConverter):
     """Like the default :class:`UnicodeConverter`, but it also matches


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
index 4a188b02..b1d2ee11 100644
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -757,6 +757,19 @@ def test_anyconverter():
     assert a.match("/a.2") == ("yes_dot", {"a": "a.2"})
 
 
+def test_any_converter_build_validates_value() -> None:
+    m = r.Map([r.Rule("/<any(patient, provider):value>", endpoint="actor")])
+    a = m.bind("localhost")
+
+    assert a.build("actor", {"value": "patient"}) == "/patient"
+    assert a.build("actor", {"value": "provider"}) == "/provider"
+
+    with pytest.raises(ValueError) as exc:
+        a.build("actor", {"value": "invalid"})
+
+    assert str(exc.value) == "'invalid' is not one of 'patient', 'provider'"
+
+
 @pytest.mark.parametrize(
     ("endpoint", "value", "expect"),
     [