Repository: marshmallow-code/apispec

Issue:
Make APISpec and Components methods chainable
See discussion in https://github.com/marshmallow-code/apispec/pull/305.

Just let `path`, `tag`, etc. return `self`.

Discussion:

Patch:
diff --git a/apispec/core.py b/apispec/core.py
index d7d2a00..9d8ca93 100644
--- a/apispec/core.py
+++ b/apispec/core.py
@@ -139,6 +139,7 @@ class Components(object):
         if extra_fields:
             ret.update(extra_fields)
         self._schemas[name] = ret
+        return self
 
     def parameter(self, param_id, location, **kwargs):
         """ Add a parameter which can be referenced.
@@ -157,6 +158,7 @@ class Components(object):
             except PluginMethodNotImplementedError:
                 continue
         self._parameters[param_id] = ret
+        return self
 
     def response(self, ref_id, **kwargs):
         """Add a response which can be referenced.
@@ -172,6 +174,7 @@ class Components(object):
             except PluginMethodNotImplementedError:
                 continue
         self._responses[ref_id] = ret
+        return self
 
 
 class APISpec(object):
@@ -233,6 +236,7 @@ class APISpec(object):
         :param dict tag: the dictionary storing information about the tag.
         """
         self._tags.append(tag)
+        return self
 
     def path(self, path=None, operations=None, **kwargs):
         """Add a new path object to the spec.
@@ -273,3 +277,4 @@ class APISpec(object):
         clean_operations(operations, self.openapi_version.major)
 
         self._paths.setdefault(path, operations).update(operations)
+        return self
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index c3816a5..1c4c885 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -47,6 +47,19 @@ Add paths to your spec using `path <apispec.APISpec.path>`.
         )
     )
 
+
+The API is chainable, allowing you to combine multiple method calls in
+one statement:
+
+.. code-block:: python
+
+    spec.path(...)\
+        .path(...)\
+        .tag(...)
+
+    spec.components.schema(...)\
+                   .parameter(...)
+
 To output your OpenAPI spec, invoke the `to_dict <apispec.APISpec.to_dict>` method.
 
 .. code-block:: python


Test patch:
diff --git a/tests/test_core.py b/tests/test_core.py
index 99ec31d..3c2067f 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -110,6 +110,10 @@ class TestTags:
         tags_json = spec.to_dict()['tags']
         assert self.tag in tags_json
 
+    def test_tag_is_chainable(self, spec):
+        spec.tag({'name': 'tag1'}).tag({'name': 'tag2'})
+        assert spec.to_dict()['tags'] == [{'name': 'tag1'}, {'name': 'tag2'}]
+
 
 class TestDefinitions:
 
@@ -127,6 +131,15 @@ class TestDefinitions:
         assert 'Pet' in defs
         assert defs['Pet']['properties'] == self.properties
 
+    def test_definition_is_chainable(self, spec):
+        spec.components.schema('Pet', properties={}).schema('Plant', properties={})
+        if spec.openapi_version.major < 3:
+            defs = spec.to_dict()['definitions']
+        else:
+            defs = spec.to_dict()['components']['schemas']
+        assert 'Pet' in defs
+        assert 'Plant' in defs
+
     def test_definition_description(self, spec):
         model_description = 'An animal which lives with humans.'
         spec.components.schema('Pet', properties=self.properties, description=model_description)
@@ -240,6 +253,10 @@ class TestPath:
         spec.path(path='/path4')
         assert list(spec.to_dict()['paths'].keys()) == ['/path1', '/path2', '/path3', '/path4']
 
+    def test_paths_is_chainable(self, spec):
+        spec.path(path='/path1').path('/path2')
+        assert list(spec.to_dict()['paths'].keys()) == ['/path1', '/path2']
+
     def test_methods_maintain_order(self, spec):
         methods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options']
         for method in methods:
@@ -323,6 +340,15 @@ class TestPath:
             assert p['parameters'][0] == {'$ref': '#/components/parameters/test_parameter'}
             assert route_spec['parameters'][0] == metadata['components']['parameters']['test_parameter']
 
+    def test_parameter_is_chainable(self, spec):
+        spec.components.parameter('param1', 'path').parameter('param2', 'path')
+        if spec.openapi_version.major < 3:
+            params = spec.to_dict()['parameters']
+        else:
+            params = spec.to_dict()['components']['parameters']
+        assert 'param1' in params
+        assert 'param2' in params
+
     def test_response(self, spec):
         route_spec = self.paths['/pet/{petId}']['get']
 
@@ -343,6 +369,15 @@ class TestPath:
             assert p['responses']['200'] == {'$ref': '#/components/responses/test_response'}
             assert route_spec['responses']['200'] == metadata['components']['responses']['test_response']
 
+    def test_response_is_chainable(self, spec):
+        spec.components.response('resp1').response('resp2')
+        if spec.openapi_version.major < 3:
+            responses = spec.to_dict()['responses']
+        else:
+            responses = spec.to_dict()['components']['responses']
+        assert 'resp1' in responses
+        assert 'resp2' in responses
+
     def test_path_check_invalid_http_method(self, spec):
         spec.path('/pet/{petId}', operations={'get': {}})
         spec.path('/pet/{petId}', operations={'x-dummy': {}})