├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.ja.md ├── README.md ├── ReservationReporter-Page-1.drawio.png ├── ReservationReporter-Page-2.drawio.png ├── __init__.py ├── events └── event.json ├── hexagonal_architecture.png ├── setup └── add_ddb_data.sh ├── src ├── __init__.py ├── app.py ├── ddb_recipient_adapter.py ├── ddb_slot_adapter.py ├── i_recipient_adapter.py ├── i_recipient_input_port.py ├── i_recipient_output_port.py ├── i_slot_adapter.py ├── i_slot_output_port.py ├── main.py ├── recipient.py ├── recipient_input_port.py ├── recipient_output_port.py ├── requirements.txt ├── slot.py ├── slot_output_port.py └── status.py ├── template.yaml └── tests ├── __init__.py ├── requirements.txt └── unit ├── __init__.py ├── test_recipient.py ├── test_recipient_input_port.py ├── test_recipient_output_port.py ├── test_slot.py ├── test_slot_output_port.py └── test_status.py /.gitignore: -------------------------------------------------------------------------------- 1 | .aws-sam/ 2 | samconfig.toml 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # ヘキサゴナルアーキテクチャを利用した AWS Lambda のドメインモデルオブジェクトサンプル 2 | 3 | [-[Readme in English](README.md)-] 4 | 5 | ## このプロジェクトの目的 6 | 7 | このプロジェクトは、ヘキサゴナルアーキテクチャを利用した AWS Lambda 関数のドメインモデルオブジェクトの実装のサンプルです。ヘキサゴナルアーキテクチャ(別名ポートとアダプターアーキテクチャと言います)は、[Dr. Alistair Cockburn](https://en.wikipedia.org/wiki/Alistair_Cockburn)によって提唱されたソフトウェア設計におけるアーキテクチャパターンです。 8 | 9 | ![Hexaglnal Architecture](hexagonal_architecture.png) 10 | 11 | ヘキサゴナルアーキテクチャ(別名ポートとアダプターアーキテクチャと言います)を利用することで、ドメインモデルと他のレイヤーのコードを分離することができます。このサンプルアプリケーションはシンプルなワクチン予約システムのドメインモデルを実装しています。ポートとアダプタクラスを利用することで、DynamoDB のテーブルをアクセスするようなインフラストラクチャコードとドメインモデルを分離することができます。 12 | 13 | このアプリケーションはまた、ポートとアダプタのクラスを注入するために、[制御の反転 inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control)のコンセプトを利用しています。これによってテスト対象のクラスに対してダミークラスを注入することで、ユニットテストをより容易に行うことができるようになっています。詳細についてはプロジェクトに含まれるサンプルのユニットテストをご覧ください。(./tests/unit) 14 | 15 | ## クラス図 16 | 17 | ![Domain Models](ReservationReporter-Page-1.drawio.png) 18 | 19 | ## シーケンス図 20 | 21 | ![Sequence diagram](ReservationReporter-Page-2.drawio.png) 22 | 23 | ## Serverless Application Model 24 | 25 | このプロジェクトは Serverless Application Model (SAM)のためのソースとサポートコードを含んいるので、SAM CLI を利用してプロジェクトをデプロイすることができます。以下のファイルとフォルダを参照ください。 26 | 27 | - src - アプリケーションの Lambda 関数のコードです。 28 | - events - invoke function で利用可能な起動時のイベント情報です。 29 | - tests/unit - アプリケーションコードに対するユニットテストです。 30 | - template.yaml - アプリケーションで利用する AWS リソースを定義した SAM のテンプレートファイルです。 31 | 32 | ## サンプルアプリケーションのデプロイ 33 | 34 | Serverless Application Model コマンドラインインターフェイス(SAM CLI)は、Lambda 関数のビルド、テスト、デプロイの機能を提供します。SAM CLI は Docker を利用して Lambda の実行に適した Amazon Linux 環境で関数を実行します。またアプリケーションのビルド環境と API もエミュレートします。 35 | 36 | SAM CLI を使用するために以下のツールが必要です。 37 | 38 | - SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 39 | - [Python 3 installed](https://www.python.org/downloads/) 40 | - Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 41 | 42 | 初めてアプリケーションをビルドしてデプロイするには、シェル上で以下のコマンドを実行します。 43 | 44 | ```bash 45 | sam build --use-container 46 | sam deploy --guided 47 | ``` 48 | 49 | 最初のコマンドはアプリケーションのソースをビルドします。2番目のコマンドは一連の問い合わせと共にアプリケーションをパッケージ化して AWS へデプロイします。 50 | 51 | Amazon API Gateway のエンドポイント URL は、デプロイ後に表示されるアプトプットの値から取得することができます。 52 | 53 | ## Lambda 関数を実行する前に DynamoDB のデータを準備 54 | 55 | このサンプルアプリケーションを実行するにはデータ準備スクリプトを実行する必要があります。 56 | 57 | ```bash 58 | $ chmod +x setup/add_ddb_data.sh 59 | $ setup/add_ddb_data.sh 60 | 61 | ``` 62 | 63 | ## Amazon API Gateway を通じて Lambda 関数を実行する 64 | 65 | [Your api endpoint address] を SAM Deploy の output Value に置き換えてください. 66 | 67 | ```bach 68 | $ curl -X POST -H "Content-Type:application/json" -d "{\"recipient_id\":\"1\", \"slot_id\":\"1\"}" [Your api endpoint address] 69 | 70 | {"message": "The recipient's reservation is added."} 71 | ``` 72 | 73 | ## ビルドしてローカルでテストするために SAM CLI を使用する 74 | 75 | `sam build --use-container` コマンドでアプリケーションをビルドします。 76 | 77 | ```bash 78 | $ sam build --use-container 79 | ``` 80 | 81 | SAM CLI は、`src/requirements.txt`で定義された依存関係をインストールして、デプロイパッケージを作成し、`.aws-sam/build` フォルダに保存します。 82 | 83 | 関数単位のテストは、テスト用のイベント情報を伴って関数をローカル環境で直接実行します。イベントは JSON ドキュメントで関数がイベントソースから受け取る入力値を表します。テストイベントは、このプロジェクトの`events` フォルダに含まれています。 84 | 85 | 関数をローカル環境で実行するには、`sam local invoke` コマンドを実行します。 86 | 87 | ```bash 88 | $ sam local invoke ReservationFunction --event events/event.json 89 | ``` 90 | 91 | ## ユニットテストの実行 92 | 93 | テストはこのプロジェクトの`tests` フォルダに定義されてます。PIP コマンドを使ってテストに必要な依存関係をインストールし、テストを実行してください。 94 | 95 | ```bash 96 | $ pip install -r tests/requirements.txt --user 97 | # unit test 98 | $ python -m pytest tests/unit -v 99 | ``` 100 | 101 | ## クリーンアップ 102 | 103 | 作成したサンプルアプリケーションを削除するには、SAM CLI コマンドを利用します。プロジェクトのためのスタック名を stack name とすると、以下のコマンドを実行を実行します。 104 | 105 | ```bash 106 | sam delete --stack-name [Stack Name] 107 | ``` 108 | 109 | ## その他のリソース 110 | 111 | SAM の入門、仕様、SAM CLI、Servereless Application のコンセプトについては、[AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) をご覧ください。 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Domain Model objects on AWS Lambda with Hexagonal Architecture Sample 2 | 3 | [-[Readme in Japanese](README.ja.md)-] 4 | 5 | ## What is this project? 6 | 7 | This project contains a Lambda function with domain model objects. By using Hexagonal Architecture (Ports and Adapters pattern), it separates domain model from other layer code. 8 | 9 | The [Hexagonal Architecture](), or ports and adapters architecture, is an architectural pattern used in software design. The hexagonal architecture was invented by [Dr. Alistair Cockburn](https://en.wikipedia.org/wiki/Alistair_Cockburn). 10 | 11 | ![Hexaglnal Architecture](hexagonal_architecture.png) 12 | 13 | The repository shows you how to implement your classes on the function. It includes sample domain models regarding a simple vaccination reservation system. With ports and adapters classes, domain model objects are loosely coupled from infrastructure code such as accessing to DynamoDB table. 14 | 15 | This application also uses [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) concept to inject ports and adapters classes. It enables you to execute unit testing more easily because you can inject dummy instances into target classes. For more details, see sample unit testing code in this project (./tests/unit folder). 16 | 17 | ## Class Diagram 18 | 19 | ![Domain Models](ReservationReporter-Page-1.drawio.png) 20 | 21 | ## Sequence diagram 22 | 23 | ![Sequence diagram](ReservationReporter-Page-2.drawio.png) 24 | 25 | ## Serverless Application Model 26 | 27 | This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. 28 | 29 | - src - Code for the application's Lambda function. 30 | - events - Invocation events that you can use to invoke the function. 31 | - tests/unit - Unit tests for the application code. 32 | - template.yaml - A template that defines the application's AWS resources. 33 | 34 | ## Deploy the sample application 35 | 36 | The Serverless Application Model Command-Line Interface (SAM CLI) extends the AWS CLI that adds functionality to building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. 37 | 38 | To use the SAM CLI, you need the following tools. 39 | 40 | - SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 41 | - [Python 3 installed](https://www.python.org/downloads/) 42 | - Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 43 | 44 | To build and deploy your application for the first time, run the following in your shell: 45 | 46 | ```bash 47 | sam build --use-container 48 | sam deploy --guided 49 | ``` 50 | 51 | The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts: 52 | 53 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 54 | 55 | ## Prepare DynamoDB data before you execute this function 56 | 57 | When you execute this function you need to execute data prepare script. 58 | 59 | ```bash 60 | $ chmod +x setup/add_ddb_data.sh 61 | $ setup/add_ddb_data.sh 62 | 63 | ``` 64 | 65 | ## Invoke a Lambda function throw Amazon API Gateway 66 | 67 | You need to replace [Your api endpoint address] to SAM deploy output value. 68 | 69 | ```bach 70 | $ curl -X POST -H "Content-Type:application/json" -d "{\"recipient_id\":\"1\", \"slot_id\":\"1\"}" [Your api endpoint address] 71 | 72 | {"message": "The recipient's reservation is added."} 73 | ``` 74 | 75 | ## Use the SAM CLI to build and test locally 76 | 77 | Build your application with the `sam build --use-container` command. 78 | 79 | ```bash 80 | $ sam build --use-container 81 | ``` 82 | 83 | The SAM CLI installs dependencies defined in `src/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 84 | 85 | Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. 86 | 87 | Run functions locally and invoke them with the `sam local invoke` command. 88 | 89 | ```bash 90 | $ sam local invoke ReservationFunction --event events/event.json 91 | ``` 92 | 93 | ## Tests 94 | 95 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests. 96 | 97 | ```bash 98 | $ pip install -r tests/requirements.txt --user 99 | # unit test 100 | $ python -m pytest tests/unit -v 101 | ``` 102 | 103 | ## Cleanup 104 | 105 | To delete the sample application that you created, use the SAM CLI. Assuming you used your project name for the stack name, you can run the following: 106 | 107 | ```bash 108 | sam delete --stack-name [Stack Name] 109 | ``` 110 | 111 | ## Resources 112 | 113 | See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. 114 | -------------------------------------------------------------------------------- /ReservationReporter-Page-1.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/ReservationReporter-Page-1.drawio.png -------------------------------------------------------------------------------- /ReservationReporter-Page-2.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/ReservationReporter-Page-2.drawio.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/__init__.py -------------------------------------------------------------------------------- /events/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"recipient_id\": \"1\", \"slot_id\":\"1\"}", 3 | "resource": "/hello", 4 | "path": "/hello", 5 | "httpMethod": "GET", 6 | "isBase64Encoded": false, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.us-east-1.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "requestTime": "09/Apr/2015:12:34:56 +0000", 42 | "requestTimeEpoch": 1428582896000, 43 | "identity": { 44 | "cognitoIdentityPoolId": null, 45 | "accountId": null, 46 | "cognitoIdentityId": null, 47 | "caller": null, 48 | "accessKey": null, 49 | "sourceIp": "127.0.0.1", 50 | "cognitoAuthenticationType": null, 51 | "cognitoAuthenticationProvider": null, 52 | "userArn": null, 53 | "userAgent": "Custom User Agent String", 54 | "user": null 55 | }, 56 | "path": "/prod/hello", 57 | "resourcePath": "/hello", 58 | "httpMethod": "POST", 59 | "apiId": "1234567890", 60 | "protocol": "HTTP/1.1" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /hexagonal_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/hexagonal_architecture.png -------------------------------------------------------------------------------- /setup/add_ddb_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | aws dynamodb put-item --table-name "VACCINATION_RESERVATION" \ 4 | --item \ 5 | '{"pk": {"S": "slot#1"},"reservation_date":{"S": "2021-11-14 13:45:00"},"location":{"S": "Tokyo"}}' 6 | 7 | aws dynamodb put-item --table-name "VACCINATION_RESERVATION" \ 8 | --item \ 9 | '{"pk": {"S": "slot#2"},"reservation_date":{"S": "2021-11-14 14:00:00"},"location":{"S": "Tokyo"}}' 10 | 11 | aws dynamodb put-item --table-name "VACCINATION_RESERVATION" \ 12 | --item \ 13 | '{"pk": {"S": "slot#3"},"reservation_date":{"S": "2021-11-14 14:15:00"},"location":{"S": "Tokyo"}}' 14 | 15 | aws dynamodb put-item --table-name "VACCINATION_RESERVATION" \ 16 | --item \ 17 | '{"pk":{"S": "recipient#1"},"email":{"S": "fatsushi@example.com"},"first_name":{"S": "Atsushi"},"last_name":{"S": "Fukui"},"age":{"N":"20"}, "slots": {"L":[]}}' 18 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/src/__init__.py -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | from i_recipient_input_port import IRecipientInputPort 3 | from recipient_input_port import RecipientInputPort 4 | from slot_output_port import SlotOutputPort 5 | from recipient_output_port import RecipientOutputPort 6 | from ddb_recipient_adapter import DDBRecipientAdapter 7 | from ddb_slot_adapter import DDBSlotAdapter 8 | 9 | ''' 10 | app_config: injector bind the target instances to param 11 | ''' 12 | # get a RecipientInpurtPort instance 13 | def get_recipient_input_port(): 14 | return RecipientInputPort( 15 | RecipientOutputPort(DDBRecipientAdapter()), 16 | SlotOutputPort(DDBSlotAdapter())) 17 | 18 | def lambda_handler(event, context): 19 | ''' 20 | API Gateway event adapter 21 | 22 | retrieve reservation request parameters 23 | ex. '{"recipient_id": "1", "slot_id":"1"}' 24 | ''' 25 | body = json.loads(event['body']) 26 | recipient_id = body['recipient_id'] 27 | slot_id = body['slot_id'] 28 | 29 | # get an input port instance 30 | recipient_input_port = get_recipient_input_port() 31 | status = recipient_input_port.make_reservation(recipient_id, slot_id) 32 | 33 | return { 34 | "statusCode": status.status_code, 35 | "body": json.dumps({ 36 | "message": status.message 37 | }), 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/ddb_recipient_adapter.py: -------------------------------------------------------------------------------- 1 | from logging import exception 2 | import os 3 | import boto3 4 | from botocore.exceptions import ClientError 5 | from datetime import datetime 6 | from i_recipient_adapter import IRecipientAdapter 7 | from recipient import Recipient 8 | from slot import Slot 9 | 10 | table_name = os.getenv("TABLE_NAME", "VACCINATION_RESERVATION") 11 | pk_prefix = "recipient#" 12 | 13 | ''' 14 | implement of Recipient adapter for Amazon DynamoDB 15 | ''' 16 | class DDBRecipientAdapter(IRecipientAdapter): 17 | def __init__(self): 18 | ddb = boto3.resource('dynamodb') 19 | self.__table = ddb.Table(table_name) 20 | 21 | def load(self, recipient_id:str) -> Recipient: 22 | try: 23 | response = self.__table.get_item( 24 | Key={'pk': pk_prefix + recipient_id}) 25 | if 'Item' in response: 26 | item = response['Item'] 27 | email = item['email'] 28 | first_name = item['first_name'] 29 | last_name = item['last_name'] 30 | age = item['age'] 31 | recipient = Recipient(recipient_id, email, first_name, last_name, age) 32 | print("in the ddb_recipient_adapter") 33 | print(recipient) 34 | 35 | if 'slots' in item: 36 | slots = item['slots'] 37 | for slot in slots: 38 | print(slot) 39 | 40 | slot_id = slot['slot_id'] 41 | reservation_date = slot['reservation_date'] 42 | location = slot['location'] 43 | recipient.add_reserve_slot(Slot( 44 | slot_id, 45 | datetime.strptime(reservation_date, '%Y-%m-%d %H:%M:%S'), 46 | location)) 47 | 48 | return recipient 49 | 50 | print("Item not found!") 51 | return None 52 | 53 | except ClientError as e: 54 | print(e.response['Error']['Message']) 55 | return None 56 | except Exception as e: 57 | print(e) 58 | return None 59 | 60 | def save(self, recipient:Recipient) -> bool: 61 | try: 62 | item = { 63 | "pk": pk_prefix + recipient.recipient_id, 64 | "email": recipient.email, 65 | "first_name": recipient.first_name, 66 | "last_name": recipient.last_name, 67 | "age": recipient.age, 68 | "slots": [] 69 | } 70 | 71 | slots = recipient.slots 72 | for slot in slots: 73 | slot_item = { 74 | "slot_id": slot.slot_id, 75 | "reservation_date": slot.reservation_date.strftime('%Y-%m-%d %H:%M:%S'), 76 | "location": slot.location 77 | } 78 | item['slots'].append(slot_item) 79 | 80 | self.__table.put_item(Item=item) 81 | return True 82 | 83 | except ClientError as e: 84 | print(e.response['Error']['Message']) 85 | return False 86 | -------------------------------------------------------------------------------- /src/ddb_slot_adapter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from datetime import datetime 5 | from i_slot_adapter import ISlotAdapter 6 | from slot import Slot 7 | 8 | table_name = os.getenv("TABLE_NAME", "VACCINATION_RESERVATION") 9 | pk_prefix = "slot#" 10 | 11 | ''' 12 | implement of Slot adapter for Amazon DynamoDB 13 | ''' 14 | class DDBSlotAdapter(ISlotAdapter): 15 | def __init__(self): 16 | ddb = boto3.resource('dynamodb') 17 | self.__table = ddb.Table(table_name) 18 | 19 | def load(self, slot_id:str) -> Slot: 20 | try: 21 | response = self.__table.get_item( 22 | Key={'pk': pk_prefix + slot_id}) 23 | if 'Item' in response: 24 | item = response['Item'] 25 | reservation_date = item['reservation_date'] 26 | location = item['location'] 27 | slot = Slot( 28 | slot_id, 29 | datetime.strptime(reservation_date, '%Y-%m-%d %H:%M:%S'), 30 | location) 31 | 32 | return slot 33 | return None 34 | 35 | except ClientError as e: 36 | print(e.response['Error']['Message']) 37 | return None 38 | -------------------------------------------------------------------------------- /src/i_recipient_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from recipient import Recipient 3 | 4 | ''' 5 | interface of Recipient adapter 6 | ''' 7 | class IRecipientAdapter(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def load(self, recipient_id:str) -> Recipient: 11 | raise NotImplementedError() 12 | 13 | @abstractmethod 14 | def save(self, recipient:Recipient) -> bool: 15 | raise NotImplementedError() 16 | -------------------------------------------------------------------------------- /src/i_recipient_input_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from status import Status 3 | 4 | ''' 5 | interface of Recipient input port 6 | ''' 7 | class IRecipientInputPort(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def make_reservation(self, recipient_id:str, slot_id:str) -> Status: 11 | raise NotImplementedError() 12 | 13 | -------------------------------------------------------------------------------- /src/i_recipient_output_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from recipient import Recipient 3 | 4 | ''' 5 | interface of Recipient output port 6 | ''' 7 | class IRecipientOutputPort(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def get_recipient_by_id(self, recipient_id:str) -> Recipient: 11 | raise NotImplementedError() 12 | 13 | @abstractmethod 14 | def add_reservation(self, recipient:Recipient) -> bool: 15 | raise NotImplementedError() 16 | 17 | -------------------------------------------------------------------------------- /src/i_slot_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from slot import Slot 3 | 4 | ''' 5 | interface of Slot adapter 6 | ''' 7 | class ISlotAdapter(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def load(self, slot_id:str) -> Slot: 11 | raise NotImplementedError() 12 | -------------------------------------------------------------------------------- /src/i_slot_output_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from slot import Slot 3 | 4 | ''' 5 | interface of Slot output port 6 | ''' 7 | class ISlotOutputPort(metaclass=ABCMeta): 8 | 9 | @abstractmethod 10 | def get_slot_by_id(self, slot_id:str) -> Slot: 11 | raise NotImplementedError() 12 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | ''' 2 | main.py 3 | 4 | This code is only used for local testing. 5 | First, you need to deploy a DynamoDB table to your AWS account. 6 | For details, see readme.md how to deploy your DynamoDB table and to load initial data. 7 | Then you can use this code to run and test on local environment. 8 | 9 | $ python main.py 10 | 11 | ''' 12 | from ddb_recipient_adapter import DDBRecipientAdapter 13 | from ddb_slot_adapter import DDBSlotAdapter 14 | from i_recipient_input_port import IRecipientInputPort 15 | from recipient_input_port import RecipientInputPort 16 | from slot_output_port import SlotOutputPort 17 | from recipient_output_port import RecipientOutputPort 18 | 19 | 20 | def get_recipient_input_port(): 21 | return RecipientInputPort( 22 | RecipientOutputPort(DDBRecipientAdapter()), 23 | SlotOutputPort(DDBSlotAdapter())) 24 | 25 | def main(): 26 | recipient_input_port = get_recipient_input_port() 27 | status = recipient_input_port.make_reservation("1", "1") 28 | print(f"status_code: {status.status_code}, message: {status.message}") 29 | 30 | if __name__ == "__main__": 31 | main() 32 | -------------------------------------------------------------------------------- /src/recipient.py: -------------------------------------------------------------------------------- 1 | from slot import Slot 2 | 3 | ''' 4 | Recipient: Domain Model 5 | ''' 6 | class Recipient: 7 | def __init__(self, recipient_id:str, email:str, first_name:str, last_name:str, age:int): 8 | self.__recipient_id = recipient_id 9 | self.__email = email 10 | self.__first_name = first_name 11 | self.__last_name = last_name 12 | self.__age = age 13 | self.__slots = [] 14 | 15 | @property 16 | def recipient_id(self): 17 | return self.__recipient_id 18 | 19 | @property 20 | def email(self): 21 | return self.__email 22 | 23 | @property 24 | def first_name(self): 25 | return self.__first_name 26 | 27 | @property 28 | def last_name(self): 29 | return self.__last_name 30 | 31 | @property 32 | def age(self): 33 | return self.__age 34 | 35 | @property 36 | def slots(self): 37 | return self.__slots 38 | 39 | def are_slots_same_date(self, slot:Slot) -> bool: 40 | for selfslot in self.__slots: 41 | if selfslot.reservation_date == slot.reservation_date: 42 | return True 43 | return False 44 | 45 | def is_slot_counts_equal_or_over_two(self) -> bool: 46 | if len(self.__slots) >= 2: 47 | return True 48 | return False 49 | 50 | def add_reserve_slot(self, slot:Slot) -> bool: 51 | if self.are_slots_same_date(slot): 52 | return False 53 | 54 | if self.is_slot_counts_equal_or_over_two(): 55 | return False 56 | 57 | self.__slots.append(slot) 58 | slot.use_slot() 59 | return True 60 | -------------------------------------------------------------------------------- /src/recipient_input_port.py: -------------------------------------------------------------------------------- 1 | from i_recipient_input_port import IRecipientInputPort 2 | from i_recipient_output_port import IRecipientOutputPort 3 | from i_slot_output_port import ISlotOutputPort 4 | from status import Status 5 | 6 | 7 | ''' 8 | implementation of Recipient input port 9 | ''' 10 | class RecipientInputPort(IRecipientInputPort): 11 | def __init__(self, recipient_output_port: IRecipientOutputPort, slot_output_port: ISlotOutputPort): 12 | self.__recipient_output_port = recipient_output_port 13 | self.__slot_output_port = slot_output_port 14 | 15 | ''' 16 | make reservation: adapting domain model business logic 17 | ''' 18 | def make_reservation(self, recipient_id:str, slot_id:str) -> Status: 19 | status = None 20 | 21 | # --------------------------------------------------- 22 | # get an instance from output port 23 | # --------------------------------------------------- 24 | recipient = self.__recipient_output_port.get_recipient_by_id(recipient_id) 25 | slot = self.__slot_output_port.get_slot_by_id(slot_id) 26 | 27 | if recipient == None or slot == None: 28 | return Status(400, "Request instance is not found. Something wrong!") 29 | 30 | print(f"recipient: {recipient.first_name}, slot date: {slot.reservation_date}") 31 | 32 | # --------------------------------------------------- 33 | # execute domain logic 34 | # --------------------------------------------------- 35 | ret = recipient.add_reserve_slot(slot) 36 | 37 | # --------------------------------------------------- 38 | # persistent an instance throgh output port 39 | # --------------------------------------------------- 40 | if ret == True: 41 | ret = self.__recipient_output_port.add_reservation(recipient) 42 | 43 | if ret == True: 44 | status = Status(200, "The recipient's reservation is added.") 45 | else: 46 | status = Status(200, "The recipient's reservation is NOT added!") 47 | return status 48 | -------------------------------------------------------------------------------- /src/recipient_output_port.py: -------------------------------------------------------------------------------- 1 | from recipient import Recipient 2 | from i_recipient_output_port import IRecipientOutputPort 3 | from i_recipient_adapter import IRecipientAdapter 4 | 5 | ''' 6 | implementation of Recipient output port 7 | ''' 8 | class RecipientOutputPort(IRecipientOutputPort): 9 | def __init__(self, adapter:IRecipientAdapter): 10 | self.__adapter = adapter 11 | 12 | def get_recipient_by_id(self, recipient_id:str) -> Recipient: 13 | return self.__adapter.load(recipient_id) 14 | 15 | def add_reservation(self, recipient:Recipient) -> bool: 16 | return self.__adapter.save(recipient) 17 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | inject==4.3.1 2 | -------------------------------------------------------------------------------- /src/slot.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | ''' 3 | Slot: Domain Model 4 | ''' 5 | class Slot: 6 | def __init__(self, slot_id:str, reservation_date:datetime, location:str): 7 | self.__slot_id = slot_id 8 | self.__reservation_date = reservation_date 9 | self.__location = location 10 | self.__is_vacant = True 11 | 12 | @property 13 | def slot_id(self): 14 | return self.__slot_id 15 | 16 | @property 17 | def reservation_date(self): 18 | return self.__reservation_date 19 | 20 | @property 21 | def location(self): 22 | return self.__location 23 | 24 | @property 25 | def is_vacant(self): 26 | return self.__is_vacant 27 | 28 | def use_slot(self): 29 | self.__is_vacant = False 30 | -------------------------------------------------------------------------------- /src/slot_output_port.py: -------------------------------------------------------------------------------- 1 | from slot import Slot 2 | from i_slot_output_port import ISlotOutputPort 3 | from i_slot_adapter import ISlotAdapter 4 | 5 | class SlotOutputPort(ISlotOutputPort): 6 | def __init__(self, adapter:ISlotAdapter): 7 | self.__adapter = adapter 8 | 9 | def get_slot_by_id(self, slot_id:str) -> Slot: 10 | return self.__adapter.load(slot_id) 11 | -------------------------------------------------------------------------------- /src/status.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Helper class for Status Code 3 | ''' 4 | class Status: 5 | def __init__(self, status_code:int, message:str): 6 | self.__status_code = status_code 7 | self.__message = message 8 | 9 | @property 10 | def status_code(self)->int: 11 | return self.__status_code 12 | 13 | @property 14 | def message(self)->str: 15 | return self.__message 16 | -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | vaccination_reservation_demo 5 | 6 | Sample SAM Template for vaccination_reservation_demo 7 | 8 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 9 | Globals: 10 | Function: 11 | Timeout: 30 12 | 13 | Parameters: 14 | DDBTableName: 15 | Type: String 16 | Default: VACCINATION_RESERVATION 17 | 18 | Resources: 19 | ReservationFunction: 20 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 21 | Properties: 22 | CodeUri: src/ 23 | Handler: app.lambda_handler 24 | Runtime: python3.9 25 | Architectures: 26 | - x86_64 27 | Environment: 28 | Variables: 29 | TABLE_NAME: 30 | Ref: DDBTableName 31 | Policies: 32 | - DynamoDBWritePolicy: 33 | TableName: 34 | Ref: DDBTableName 35 | - DynamoDBReadPolicy: 36 | TableName: 37 | Ref: DDBTableName 38 | Events: 39 | Reservation: 40 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 41 | Properties: 42 | Path: /reservation 43 | Method: post 44 | 45 | VaccinationReservation: 46 | Type: AWS::Serverless::SimpleTable 47 | Properties: 48 | PrimaryKey: 49 | Name: pk 50 | Type: String 51 | TableName: 52 | Ref: DDBTableName 53 | 54 | Outputs: 55 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 56 | # Find out more about other implicit resources you can reference within SAM 57 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 58 | HelloWorldApi: 59 | Description: "API Gateway endpoint URL for Prod stage for Hello World function" 60 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/reservation/" 61 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | boto3 -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-lambda-domain-model-sample/63f7dd373e491c775e2ede3d3420f5b63574e2c5/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_recipient.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from datetime import datetime 4 | 5 | sys.path.append("./src/") 6 | 7 | from recipient import Recipient 8 | from slot import Slot 9 | 10 | recipient_id = "1" 11 | email = "fatsushi@example.com" 12 | first_name = "Atsushi" 13 | last_name = "Fukui" 14 | age = 30 15 | dt_slot = datetime(2021, 11, 12, 10, 0, 0) 16 | dt_slot_2 = datetime(2021, 12, 10, 10, 0, 0) 17 | dt_slot_3 = datetime(2021, 12, 31, 10, 0, 0) 18 | location = "Tokyo" 19 | 20 | 21 | @pytest.fixture() 22 | def fixture_recipient(): 23 | return Recipient(recipient_id, email, first_name, last_name, age) 24 | 25 | @pytest.fixture() 26 | def fixture_slot(): 27 | return Slot("1", dt_slot, location) 28 | 29 | @pytest.fixture() 30 | def fixture_slot_2(): 31 | return Slot("2", dt_slot_2, location) 32 | 33 | @pytest.fixture() 34 | def fixture_slot_3(): 35 | return Slot("3", dt_slot_3, location) 36 | 37 | def test_new_recipient(fixture_recipient): 38 | 39 | target = fixture_recipient 40 | assert target != None 41 | assert recipient_id == target.recipient_id 42 | assert email == target.email 43 | assert first_name == target.first_name 44 | assert last_name == target.last_name 45 | assert age == target.age 46 | assert target.slots != None 47 | assert 0 == len(target.slots) 48 | 49 | def test_add_slot_one(fixture_recipient, fixture_slot): 50 | slot = fixture_slot 51 | target = fixture_recipient 52 | target.add_reserve_slot(slot) 53 | assert slot != None 54 | assert target != None 55 | assert 1 == len(target.slots) 56 | assert slot.slot_id == target.slots[0].slot_id 57 | assert slot.reservation_date == target.slots[0].reservation_date 58 | assert slot.location == target.slots[0].location 59 | assert False == target.slots[0].is_vacant 60 | 61 | def test_add_slot_two(fixture_recipient, fixture_slot, fixture_slot_2): 62 | slot = fixture_slot 63 | slot2 = fixture_slot_2 64 | target = fixture_recipient 65 | target.add_reserve_slot(slot) 66 | target.add_reserve_slot(slot2) 67 | assert 2 == len(target.slots) 68 | 69 | def test_cannot_append_slot_more_than_two(fixture_recipient, fixture_slot, fixture_slot_2, fixture_slot_3): 70 | slot = fixture_slot 71 | slot2 = fixture_slot_2 72 | slot3 = fixture_slot_3 73 | target = fixture_recipient 74 | target.add_reserve_slot(slot) 75 | target.add_reserve_slot(slot2) 76 | ret = target.add_reserve_slot(slot3) 77 | assert False == ret 78 | assert 2 == len(target.slots) 79 | 80 | def test_cannot_append_same_date_slot(fixture_recipient, fixture_slot): 81 | slot = fixture_slot 82 | target = fixture_recipient 83 | target.add_reserve_slot(slot) 84 | ret = target.add_reserve_slot(slot) 85 | assert False == ret 86 | assert 1 == len(target.slots) 87 | -------------------------------------------------------------------------------- /tests/unit/test_recipient_input_port.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from datetime import datetime 4 | 5 | sys.path.append("./src/") 6 | 7 | from i_recipient_output_port import IRecipientOutputPort 8 | from i_slot_output_port import ISlotOutputPort 9 | from recipient_input_port import RecipientInputPort 10 | from recipient import Recipient 11 | from slot import Slot 12 | from status import Status 13 | 14 | recipient_id = "1" 15 | email = "fatsushi@example.com" 16 | first_name = "Atsushi" 17 | last_name = "Fukui" 18 | age = 61 19 | slot_id = "1" 20 | reservation_date = datetime(2021,12,20, 9, 0, 0) 21 | location = "Tokyo" 22 | 23 | 24 | class DummyRecipientOutputPort(IRecipientOutputPort): 25 | def get_recipient_by_id(self, recipient_id:str) -> Recipient: 26 | return Recipient(recipient_id, email, first_name, last_name, age) 27 | 28 | def add_reservation(self, recipient:Recipient) -> bool: 29 | return True 30 | 31 | 32 | class DummySlotOutputPort(ISlotOutputPort): 33 | def get_slot_by_id(self, slot_id:str) -> Slot: 34 | return Slot(slot_id, reservation_date, location) 35 | 36 | 37 | @pytest.fixture() 38 | def fixture_recipient_input_port(): 39 | #SetUp 40 | recipient_input_port = RecipientInputPort(DummyRecipientOutputPort(), DummySlotOutputPort()) 41 | 42 | #execute testing 43 | yield recipient_input_port 44 | 45 | #TearDown 46 | recipient_input_port = None 47 | 48 | def test_add_reservation(fixture_recipient_input_port): 49 | target = fixture_recipient_input_port 50 | 51 | recipient_id = "dummy_id" 52 | slot_id = "dummy_id" 53 | 54 | status = target.make_reservation(recipient_id, slot_id) 55 | assert 200 == status.status_code 56 | assert "The recipient's reservation is added." == status.message 57 | 58 | 59 | -------------------------------------------------------------------------------- /tests/unit/test_recipient_output_port.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | 4 | sys.path.append("./src/") 5 | 6 | from i_recipient_adapter import IRecipientAdapter 7 | from recipient_output_port import RecipientOutputPort 8 | from recipient import Recipient 9 | 10 | #value for testing 11 | recipient_id = "1" 12 | email = "fatsushi@example.com" 13 | first_name = "Atsushi" 14 | last_name = "Fukui" 15 | age = 30 16 | 17 | # Dummy class for RecipientAdapter 18 | class DummyRecipientAdapter(IRecipientAdapter): 19 | def load(self, recipient_id:str) -> Recipient: 20 | return Recipient(recipient_id, email, first_name, last_name, age) 21 | 22 | def save(self, recipient:Recipient) -> bool: 23 | return True 24 | 25 | 26 | @pytest.fixture() 27 | def fixture_recipient_output_port(): 28 | 29 | #SetUp 30 | recipient_output_port = RecipientOutputPort(DummyRecipientAdapter()) 31 | 32 | #execute testing 33 | yield recipient_output_port 34 | 35 | #TearDown 36 | recipient_output_port = None 37 | 38 | def test_recipient_port_recipient_by_id(fixture_recipient_output_port): 39 | target = fixture_recipient_output_port 40 | recipient_id = "dummy_number" 41 | recipient = target.get_recipient_by_id(recipient_id) 42 | assert recipient != None 43 | assert email == recipient.email 44 | assert first_name == recipient.first_name 45 | assert last_name == recipient.last_name 46 | assert age == recipient.age 47 | 48 | 49 | def test_recipient_port_add_reservation_must_be_true(fixture_recipient_output_port): 50 | target = fixture_recipient_output_port 51 | ret = target.add_reservation(Recipient(recipient_id, email, first_name, last_name, age)) 52 | assert True == ret 53 | -------------------------------------------------------------------------------- /tests/unit/test_slot.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from datetime import datetime 4 | 5 | sys.path.append("./src/") 6 | 7 | from slot import Slot 8 | 9 | slot_id = "1" 10 | dt_slot = datetime(2021, 11, 12, 10, 0, 0) 11 | location = "Tokyo" 12 | 13 | @pytest.fixture() 14 | def fixture_slot(): 15 | return Slot(slot_id, dt_slot, location) 16 | 17 | def test_new_slot(fixture_slot): 18 | 19 | target = fixture_slot 20 | assert target != None 21 | assert slot_id == target.slot_id 22 | assert dt_slot == target.reservation_date 23 | assert location == target.location 24 | assert True == target.is_vacant 25 | 26 | def test_use_slot(fixture_slot): 27 | target = fixture_slot 28 | assert True == target.is_vacant 29 | target.use_slot() 30 | assert False == target.is_vacant -------------------------------------------------------------------------------- /tests/unit/test_slot_output_port.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from datetime import datetime 4 | 5 | sys.path.append("./src/") 6 | 7 | from i_slot_adapter import ISlotAdapter 8 | from slot_output_port import SlotOutputPort 9 | from slot import Slot 10 | 11 | # value for testing 12 | slot_id = "1" 13 | reservation_date = datetime(2021,12,20, 9, 0, 0) 14 | location = "Tokyo" 15 | 16 | 17 | class DummySlotAdapter(ISlotAdapter): 18 | def load(self, slot_id:str) -> Slot: 19 | return Slot(slot_id, reservation_date, location) 20 | 21 | 22 | @pytest.fixture() 23 | def fixture_slot_output_port(): 24 | 25 | #SetUp 26 | slot_output_port = SlotOutputPort(DummySlotAdapter()) 27 | 28 | # execute testing 29 | yield slot_output_port 30 | 31 | #TearDown 32 | slot_output_port = None 33 | 34 | 35 | def test_slot_output_port_slot_by_id(fixture_slot_output_port): 36 | target = fixture_slot_output_port 37 | slot_id = "1" 38 | slot = target.get_slot_by_id(slot_id) 39 | assert slot != None 40 | assert reservation_date == slot.reservation_date 41 | assert location == slot.location 42 | 43 | -------------------------------------------------------------------------------- /tests/unit/test_status.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | 4 | sys.path.append("./src/") 5 | 6 | from status import Status 7 | 8 | # @pytest.fixture() 9 | # def myfixture(): 10 | # return '' 11 | 12 | def test_set_status_properties(): 13 | status_code = 200 14 | message = "hello" 15 | 16 | target = Status(status_code, message) 17 | assert status_code == target.status_code 18 | assert message == target.message 19 | --------------------------------------------------------------------------------