Repository: marshmallow-code/apispec

Issue:
Core path does not support swagger basePath
Related to #69.

Flask allows configuring the `APPLICATION_ROOT`, which provides the base path for the routes. #70 fixed this issue so that the flask extension reports the fully qualified path.

When the specs are dumped as swagger specs using `to_dict()`, the full path is shown on every route, even though the swagger specification supports the [`basePath`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#fixed-fields) field on the root object.

I think it is important to keep the meaningful portion of the route URL separate from the base path to maintain readable documentation. #70 was the right fix for the flask extension since the `apispec` core assumes paths are absolute, but the fact that both `flask` and `swagger` support a configurable base path indicates that the `apispec` core should probably internalize the concept of base paths and expose them to extensions.

Examples of base path in similar libraries:
- [Bottle: ResourceManager (default) path](http://bottlepy.org/docs/dev/api.html#bottle.ResourceManager)
- [RAML: Base URI](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#base-uri-and-base-uri-parameters)

> Edit: Updated to refect that generating swagger specs is a core functionality of `apispec`, not an extension.


Discussion:

Patch:
diff --git a/apispec/core.py b/apispec/core.py
index 66028b1..170ca0b 100644
--- a/apispec/core.py
+++ b/apispec/core.py
@@ -154,12 +154,19 @@ class APISpec(object):
         :param dict|None operations: describes the http methods and options for `path`
         :param dict kwargs: parameters used by any path helpers see :meth:`register_path_helper`
         """
+        def normalize_path(path):
+            if path and 'basePath' in self.options:
+                pattern = '^{0}'.format(re.escape(self.options['basePath']))
+                path = re.sub(pattern, '', path)
+
+            return path
+
         p = path
         if isinstance(path, Path):
             p = path.path
-        if p and 'basePath' in self.options:
-            pattern = '^{0}'.format(re.escape(self.options['basePath']))
-            p = re.sub(pattern, '', p)
+
+        p = normalize_path(p)
+
         if isinstance(path, Path):
             path.path = p
         else:
@@ -173,6 +180,7 @@ class APISpec(object):
             except TypeError:
                 continue
             if isinstance(ret, Path):
+                ret.path = normalize_path(ret.path)
                 path.update(ret)
 
         if not path.path:


Test patch:
diff --git a/tests/test_ext_flask.py b/tests/test_ext_flask.py
index eeb4a03..5bbfcfd 100644
--- a/tests/test_ext_flask.py
+++ b/tests/test_ext_flask.py
@@ -108,7 +108,8 @@ class TestPathHelpers:
 
     def test_path_includes_app_root(self, app, spec):
 
-        app.config['APPLICATION_ROOT'] = '/app/root'
+        spec.options['basePath'] = '/v1'
+        app.config['APPLICATION_ROOT'] = '/v1/app/root'
 
         @app.route('/partial/path/pet')
         def get_pet():
@@ -119,7 +120,8 @@ class TestPathHelpers:
 
     def test_path_with_args_includes_app_root(self, app, spec):
 
-        app.config['APPLICATION_ROOT'] = '/app/root'
+        spec.options['basePath'] = '/v1'
+        app.config['APPLICATION_ROOT'] = '/v1/app/root'
 
         @app.route('/partial/path/pet/{pet_id}')
         def get_pet(pet_id):
@@ -130,7 +132,8 @@ class TestPathHelpers:
 
     def test_path_includes_app_root_with_right_slash(self, app, spec):
 
-        app.config['APPLICATION_ROOT'] = '/app/root/'
+        spec.options['basePath'] = '/v1'
+        app.config['APPLICATION_ROOT'] = '/v1/app/root/'
 
         @app.route('/partial/path/pet')
         def get_pet():