diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb6e436a9..0fa6f5ce4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,8 @@ Next Release (TBD) (`#172 `__) * Add support for ``DELETE`` and ``PATCH`` in ``chalice local`` (`#167 `__) +* Add ``chalice generate-sdk`` command + (`#178 `__) 0.4.0 diff --git a/chalice/awsclient.py b/chalice/awsclient.py index 054487779..7fb0d6f04 100644 --- a/chalice/awsclient.py +++ b/chalice/awsclient.py @@ -263,6 +263,18 @@ def get_function_policy(self, function_name): policy = client.get_policy(FunctionName=function_name) return json.loads(policy['Policy']) + def get_sdk(self, rest_api_id, stage='dev', sdk_type='javascript'): + # type: (str, str, str) -> file + """Generate an SDK for a given SDK. + + Returns a file like object that streams a zip contents for the + generated SDK. + + """ + response = self._client('apigateway').get_sdk( + restApiId=rest_api_id, stageName=stage, sdkType=sdk_type) + return response['body'] + def add_permission_for_apigateway(self, function_name, region_name, account_id, rest_api_id, random_id): # type: (str, str, str, str, str) -> None diff --git a/chalice/cli/__init__.py b/chalice/cli/__init__.py index 67f3d61c8..c6c4c57cd 100644 --- a/chalice/cli/__init__.py +++ b/chalice/cli/__init__.py @@ -7,7 +7,10 @@ import json import sys import logging +import zipfile +import tempfile import importlib +import shutil import click import botocore.exceptions @@ -20,6 +23,7 @@ from chalice.logs import LogRetriever from chalice import prompts from chalice.config import Config +from chalice.awsclient import TypedAWSClient TEMPLATE_APP = """\ @@ -277,7 +281,6 @@ def url(ctx): config = create_config_obj(ctx) session = create_botocore_session(profile=config.profile, debug=ctx.obj['debug']) - from chalice.awsclient import TypedAWSClient c = TypedAWSClient(session) rest_api_id = c.get_rest_api_id(config.app_name) stage_name = config.stage @@ -288,6 +291,47 @@ def url(ctx): ) +@cli.command('generate-sdk') +@click.option('--sdk-type', default='javascript', + type=click.Choice(['javascript'])) +@click.argument('outdir') +@click.pass_context +def generate_sdk(ctx, sdk_type, outdir): + # type: (click.Context, str, str) -> None + config = create_config_obj(ctx) + session = create_botocore_session(profile=config.profile, + debug=ctx.obj['debug']) + client = TypedAWSClient(session) + rest_api_id = client.get_rest_api_id(config.app_name) + stage_name = config.stage + if rest_api_id is None: + click.echo("Could not find API ID, has this application " + "been deployed?") + raise click.Abort() + zip_stream = client.get_sdk(rest_api_id, stage=stage_name, + sdk_type=sdk_type) + tmpdir = tempfile.mkdtemp() + with open(os.path.join(tmpdir, 'sdk.zip'), 'wb') as f: + f.write(zip_stream.read()) + tmp_extract = os.path.join(tmpdir, 'extracted') + with zipfile.ZipFile(os.path.join(tmpdir, 'sdk.zip')) as z: + z.extractall(tmp_extract) + # The extract zip dir will have a single directory: + # ['apiGateway-js-sdk'] + dirnames = os.listdir(tmp_extract) + if len(dirnames) == 1: + full_dirname = os.path.join(tmp_extract, dirnames[0]) + if os.path.isdir(full_dirname): + final_dirname = '%s-js-sdk' % config.app_name + full_renamed_name = os.path.join(tmp_extract, final_dirname) + os.rename(full_dirname, full_renamed_name) + shutil.move(full_renamed_name, outdir) + return + click.echo("The downloaded SDK had an unexpected directory structure: %s" + % (', '.join(dirnames))) + raise click.Abort() + + def run_local_server(app_obj): # type: (Chalice) -> None from chalice import local diff --git a/docs/source/index.rst b/docs/source/index.rst index 1fcefd2c8..852cd8b1c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -58,6 +58,7 @@ Topics topics/configfile topics/multifile topics/logging + topics/sdks API Reference diff --git a/docs/source/topics/sdks.rst b/docs/source/topics/sdks.rst new file mode 100644 index 000000000..87f7964a5 --- /dev/null +++ b/docs/source/topics/sdks.rst @@ -0,0 +1,164 @@ +SDK Generation +============== + +The ``@app.route(...)`` information you provide chalice allows +it to create corresponding routes in API Gateway. One of the benefits of this +approach is that we can leverage API Gateway's SDK generation process. +Chalice offers a ``chalice generate-sdk`` command that will automatically +generate an SDK based on your declared routes. + +.. note:: + The only supported language at this time is javascript. + +Keep in mind that chalice itself does not have any logic for generating +SDKs. The SDK generation happens service side in `API Gateway`_, the +``chalice generate-sdk`` is just a high level wrapper around that +functionality. + +To generate an SDK for a chalice app, run this command from the project +directory:: + + $ chalice generate-sdk /tmp/sdk + +You should now have a generated javascript sdk in ``/tmp/sdk``. +API Gateway includes a ``README.md`` as part of its SDK generation +which contains details on how to use the javascript SDK. + +Example +------- + +Suppose we have the following chalice app: + +.. code-block:: python + + from chalice import Chalice + + app = Chalice(app_name='sdktest') + + @app.route('/', cors=True) + def index(): + return {'hello': 'world'} + + @app.route('/foo', cors=True) + def foo(): + return {'foo': True} + + @app.route('/hello/{name}', cors=True) + def hello_name(name): + return {'hello': name} + + @app.route('/users/{user_id}', methods=['PUT'], cors=True) + def update_user(user_id): + return {"msg": "fake updated user", "userId": user_id} + + +Let's generate a javascript SDK and test it out in the browser. +Run the following command from the project dir:: + + $ chalice generate-sdk /tmp/sdkdemo + $ cd /tmp/sdkdemo + $ ls -la + -rw-r--r-- 1 jamessar r 3227 Nov 21 17:06 README.md + -rw-r--r-- 1 jamessar r 9243 Nov 21 17:06 apigClient.js + drwxr-xr-x 6 jamessar r 204 Nov 21 17:06 lib + +You should now be able to follow the instructions from API Gateway in the +``README.md`` file. Below is a snippet that shows how the generated +javascript SDK methods correspond to the ``@app.route()`` calls in chalice. + +.. code-block:: html + + + + + + + +Example HTML File +~~~~~~~~~~~~~~~~~ + +If you want to try out the example above, you can use the following index.html +page to test: + +.. code-block:: html + + + + + SDK Test + + + + + + + + + + + + + + + + + + + +
result of rootGet()
+
result of fooGet()
+
result of helloNameGet({name: 'jimmy'})
+
result of usersUserIdPut({user_id: '123'})
+ + + + +.. _API Gateway: http://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-generate-sdk.html diff --git a/tests/functional/test_awsclient.py b/tests/functional/test_awsclient.py index 27ffd2f7c..280193acb 100644 --- a/tests/functional/test_awsclient.py +++ b/tests/functional/test_awsclient.py @@ -412,3 +412,15 @@ def test_can_add_permission_when_policy_does_not_exist(self, stubbed_session): TypedAWSClient(stubbed_session).add_permission_for_apigateway_if_needed( 'name', 'us-west-2', '123', 'rest-api-id', 'random-id') stubbed_session.verify_stubs() + + def test_get_sdk(self, stubbed_session): + apig = stubbed_session.stub('apigateway') + apig.get_sdk( + restApiId='rest-api-id', + stageName='dev', + sdkType='javascript').returns({'body': 'foo'}) + stubbed_session.activate_stubs() + awsclient = TypedAWSClient(stubbed_session) + response = awsclient.get_sdk('rest-api-id', 'dev', 'javascript') + stubbed_session.verify_stubs() + assert response == 'foo'