Repository: apiflask/apiflask (python)

Issue:
Allow path parameters overriding
APIFlask currently doesn't allow overriding path parameters specification. The proposal is to add such feature.

Consider following example:
I'm writing an endpoint which takes (among other things) one or several path parameters, and let's say, that one of them is of enum type which I want to validate using `@input` decorator and providing a schema with `location="path"`, and moreover, I would like to generate a proper OpenAPI spec based on this schema.
But when it comes to actually implementing this snippet, I run into an exception from APISpec which states that I'm trying to insert a duplicated path parameter; this is because APIFlask generates path parameter spec on its own under the hood, so I'm proposing to add a boolean flag argument which can be passed to an individual view's `@input` decorator and which will allow to skip default path parameter spec generation or overriding the default spec with the one generated from a schema.

A sample usage of such feature would look like this:
```py
@blueprint.input(SampleSchema(), location="path", drop_path_parameters=True)
```
Or, alternatively, this could be a default behaviour for schemas with `location="path"`.

Pull request:
Allow path parameters overriding
<!--
For features and bug fixes, before opening a PR, please open an issue describing
the bug or feature the PR will address. You can skip this step if it's a typo fix.

Replace this comment with a description of the change. Describe how it
addresses the linked issue.
-->

<!--
Link to relevant issues or previous PRs, one per line. Use "fixes" to
automatically close an issue.
-->

- fixes #350 

<!--
If needed, ensure each step in the checklist below is complete. If only docs were changed, these aren't relevant and can be removed.
-->

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the `docs` folder and in code docstring.
- [x] Add an entry in `CHANGES.md` summarizing the change and linking to the issue and your username.
- [ ] Add `*Version changed*` or `*Version added*` note in any relevant docs and docstring.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index 639fc3f..49d11cc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -6,6 +6,9 @@
 Released: -
 
 - Add `Enum` field from marshmallow 3.18.
+- Fix OpenAPI spec generating for path parameters when path schema is provided ([issue #350][issue_350]).
+
+[issue_350]: https://github.com/apiflask/apiflask/issues/350
 
 
 ## Version 1.1.3
diff --git a/src/apiflask/app.py b/src/apiflask/app.py
index 9472fe4..6a6458a 100644
--- a/src/apiflask/app.py
+++ b/src/apiflask/app.py
@@ -1172,7 +1172,13 @@ class APIFlask(APIScaffold, Flask):
 
             # parameters
             path_arguments: t.Iterable = re.findall(r'<(([^<:]+:)?([^>]+))>', rule.rule)
-            if path_arguments:
+            if (
+                path_arguments
+                and not (
+                    hasattr(view_func, '_spec')
+                    and view_func._spec.get('omit_default_path_parameters', False)
+                )
+            ):
                 arguments: t.List[t.Dict[str, str]] = []
                 for _, argument_type, argument_name in path_arguments:
                     argument = get_argument(argument_type, argument_name)
diff --git a/src/apiflask/scaffold.py b/src/apiflask/scaffold.py
index 27ac754..9049c1e 100644
--- a/src/apiflask/scaffold.py
+++ b/src/apiflask/scaffold.py
@@ -326,6 +326,8 @@ class APIScaffold:
             else:
                 if not hasattr(f, '_spec') or f._spec.get('args') is None:
                     _annotate(f, args=[])
+                if location == 'path':
+                    _annotate(f, omit_default_path_parameters=True)
                 # TODO: Support set example for request parameters
                 f._spec['args'].append((schema, location))
             return use_args(schema, location=location, **kwargs)(f)


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 3490f31..720d9a1 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -46,7 +46,7 @@ jobs:
           pip install -U setuptools
           python -m pip install -U pip
       - name: cache mypy
-        uses: actions/cache@v3.0.8
+        uses: actions/cache@v3.0.9
         with:
           path: ./.mypy_cache
           key: mypy|${{ matrix.python }}|${{ hashFiles('setup.cfg') }}
diff --git a/tests/schemas.py b/tests/schemas.py
index 045120d..dfe1ddb 100644
--- a/tests/schemas.py
+++ b/tests/schemas.py
@@ -5,6 +5,7 @@ from apiflask.fields import File
 from apiflask.fields import Integer
 from apiflask.fields import List
 from apiflask.fields import String
+from apiflask.validators import OneOf
 
 
 class Foo(Schema):
@@ -52,6 +53,10 @@ class FormAndFiles(Schema):
     image = File()
 
 
+class EnumPathParameter(Schema):
+    image_type = String(validate=OneOf(['jpg', 'png', 'tiff', 'webp']))
+
+
 class Pagination(Schema):
     class Meta:
         unknown = EXCLUDE
diff --git a/tests/test_decorator_input.py b/tests/test_decorator_input.py
index 64cbb3e..8341df5 100644
--- a/tests/test_decorator_input.py
+++ b/tests/test_decorator_input.py
@@ -6,6 +6,7 @@ from openapi_spec_validator import validate_spec
 from werkzeug.datastructures import FileStorage
 
 from .schemas import Bar
+from .schemas import EnumPathParameter
 from .schemas import Files
 from .schemas import Foo
 from .schemas import Form
@@ -181,6 +182,38 @@ def test_input_with_form_and_files_location(app, client):
     assert rv.json == {'name': True, 'image': True}
 
 
+def test_input_with_path_location(app, client):
+    @app.get('/<image_type>')
+    @app.input(EnumPathParameter, location='path')
+    def index(image_type, _):
+        return {'image_type': image_type}
+
+    rv = client.get('/openapi.json')
+    assert rv.status_code == 200
+    # TODO: Failed validating 'oneOf' in schema
+    # https://github.com/p1c2u/openapi-spec-validator/issues/113
+    # validate_spec(rv.json)
+    assert '/{image_type}' in rv.json['paths']
+    assert len(rv.json['paths']['/{image_type}']['get']['parameters']) == 1
+    assert rv.json['paths']['/{image_type}']['get']['parameters'][0]['in'] == 'path'
+    assert rv.json['paths']['/{image_type}']['get']['parameters'][0]['name'] == 'image_type'
+    assert rv.json['paths']['/{image_type}']['get']['parameters'][0]['schema'] == {
+        'type': 'string',
+        'enum': ['jpg', 'png', 'tiff', 'webp'],
+    }
+
+    rv = client.get('/png')
+    assert rv.status_code == 200
+    assert rv.json == {'image_type': 'png'}
+
+    rv = client.get('/gif')
+    assert rv.status_code == 400
+    assert rv.json['message'] == 'Validation error'
+    assert 'path' in rv.json['detail']
+    assert 'image_type' in rv.json['detail']['path']
+    assert rv.json['detail']['path']['image_type'] == ['Must be one of: jpg, png, tiff, webp.']
+
+
 @pytest.mark.parametrize('locations', [
     ['files', 'form'],
     ['files', 'json'],