├── .gitignore ├── 2021120_JAWS_PANKRATION_2021_DDD_Lambda.pdf ├── LICENSE ├── README.md ├── ReservationReporter-Page-1.drawio.png ├── ReservationReporter-Page-2.drawio.png ├── __init__.py ├── events └── event.json ├── setup └── add_ddb_data.sh ├── src ├── __init__.py ├── app.py ├── ddb_recipient_adapter.py ├── ddb_slot_adapter.py ├── i_recipient_adapter.py ├── i_recipient_port.py ├── i_slot_adapter.py ├── i_slot_port.py ├── main.py ├── recipient.py ├── recipient_port.py ├── request_port.py ├── requirements.txt ├── reservation_service.py ├── slot.py ├── slot_port.py └── status.py ├── template.yaml └── tests ├── __init__.py ├── requirements.txt └── unit ├── __init__.py ├── test_recipient.py ├── test_recipient_port.py ├── test_reservation_service.py ├── test_slot.py ├── test_slot_port.py └── test_status.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### OSX ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### PyCharm ### 47 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 48 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 49 | 50 | # User-specific stuff: 51 | .idea/**/workspace.xml 52 | .idea/**/tasks.xml 53 | .idea/dictionaries 54 | 55 | # Sensitive or high-churn files: 56 | .idea/**/dataSources/ 57 | .idea/**/dataSources.ids 58 | .idea/**/dataSources.xml 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | 64 | # Gradle: 65 | .idea/**/gradle.xml 66 | .idea/**/libraries 67 | 68 | # CMake 69 | cmake-build-debug/ 70 | 71 | # Mongo Explorer plugin: 72 | .idea/**/mongoSettings.xml 73 | 74 | ## File-based project format: 75 | *.iws 76 | 77 | ## Plugin-specific files: 78 | 79 | # IntelliJ 80 | /out/ 81 | 82 | # mpeltonen/sbt-idea plugin 83 | .idea_modules/ 84 | 85 | # JIRA plugin 86 | atlassian-ide-plugin.xml 87 | 88 | # Cursive Clojure plugin 89 | .idea/replstate.xml 90 | 91 | # Ruby plugin and RubyMine 92 | /.rakeTasks 93 | 94 | # Crashlytics plugin (for Android Studio and IntelliJ) 95 | com_crashlytics_export_strings.xml 96 | crashlytics.properties 97 | crashlytics-build.properties 98 | fabric.properties 99 | 100 | ### PyCharm Patch ### 101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 102 | 103 | # *.iml 104 | # modules.xml 105 | # .idea/misc.xml 106 | # *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | ### Python ### 112 | # Byte-compiled / optimized / DLL files 113 | __pycache__/ 114 | *.py[cod] 115 | *$py.class 116 | 117 | # C extensions 118 | *.so 119 | 120 | # Distribution / packaging 121 | .Python 122 | build/ 123 | develop-eggs/ 124 | dist/ 125 | downloads/ 126 | eggs/ 127 | .eggs/ 128 | lib/ 129 | lib64/ 130 | parts/ 131 | sdist/ 132 | var/ 133 | wheels/ 134 | *.egg-info/ 135 | .installed.cfg 136 | *.egg 137 | 138 | # PyInstaller 139 | # Usually these files are written by a python script from a template 140 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 141 | *.manifest 142 | *.spec 143 | 144 | # Installer logs 145 | pip-log.txt 146 | pip-delete-this-directory.txt 147 | 148 | # Unit test / coverage reports 149 | htmlcov/ 150 | .tox/ 151 | .coverage 152 | .coverage.* 153 | .cache 154 | .pytest_cache/ 155 | nosetests.xml 156 | coverage.xml 157 | *.cover 158 | .hypothesis/ 159 | 160 | # Translations 161 | *.mo 162 | *.pot 163 | 164 | # Flask stuff: 165 | instance/ 166 | .webassets-cache 167 | 168 | # Scrapy stuff: 169 | .scrapy 170 | 171 | # Sphinx documentation 172 | docs/_build/ 173 | 174 | # PyBuilder 175 | target/ 176 | 177 | # Jupyter Notebook 178 | .ipynb_checkpoints 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # celery beat schedule file 184 | celerybeat-schedule.* 185 | 186 | # SageMath parsed files 187 | *.sage.py 188 | 189 | # Environments 190 | .env 191 | .venv 192 | env/ 193 | venv/ 194 | ENV/ 195 | env.bak/ 196 | venv.bak/ 197 | 198 | # Spyder project settings 199 | .spyderproject 200 | .spyproject 201 | 202 | # Rope project settings 203 | .ropeproject 204 | 205 | # mkdocs documentation 206 | /site 207 | 208 | # mypy 209 | .mypy_cache/ 210 | 211 | ### VisualStudioCode ### 212 | .vscode/* 213 | !.vscode/settings.json 214 | !.vscode/tasks.json 215 | !.vscode/launch.json 216 | !.vscode/extensions.json 217 | .history 218 | 219 | ### Windows ### 220 | # Windows thumbnail cache files 221 | Thumbs.db 222 | ehthumbs.db 223 | ehthumbs_vista.db 224 | 225 | # Folder config file 226 | Desktop.ini 227 | 228 | # Recycle Bin used on file shares 229 | $RECYCLE.BIN/ 230 | 231 | # Windows Installer files 232 | *.cab 233 | *.msi 234 | *.msm 235 | *.msp 236 | 237 | # Windows shortcuts 238 | *.lnk 239 | 240 | # Build folder 241 | 242 | */build/* 243 | 244 | # End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode 245 | 246 | #sam 247 | samconfig.toml 248 | .aws-sam/ -------------------------------------------------------------------------------- /2021120_JAWS_PANKRATION_2021_DDD_Lambda.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/2021120_JAWS_PANKRATION_2021_DDD_Lambda.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2021 Atsushi Fukui All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in 4 | the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JAWS Pankration 2021 - DDD on AWS Lambda sample 2 | 3 | ## What is this project? 4 | 5 | This project contains sample code for AWS Lambda with domain models. I presented the session about how to implement an AWS Lambda function with domain models at [JAWS Pankration 20201](https://jawspankration2021.jaws-ug.jp/en). 6 | 7 | The repository shows you how to implement your classes on the function. It includes sample domain models regarding a vaccination reservation system, which are loosely coupled from infrastructure code such as accessing a DynamoDB table with ports and adapters classes. The application is designed by the concept of [hexagonal architecture]() or ports and adapters architecture by [Alistair Cockburn](https://en.wikipedia.org/wiki/Alistair_Cockburn). This application also uses [injector](https://github.com/alecthomas/injector) Python library 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) and [this session slide deck](https://github.com/afukui/jaws-pankration-ddd-lambda/blob/main/2021120_JAWS_PANKRATION_2021_DDD_Lambda.pdf). 8 | 9 | ## Domain Models 10 | 11 | ![Domain Models](ReservationReporter-Page-1.drawio.png) 12 | 13 | ## Sequence diagram for this application 14 | 15 | ![Sequence diagram](ReservationReporter-Page-2.drawio.png) 16 | 17 | ## Serverless Application Model 18 | 19 | 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. 20 | 21 | - src - Code for the application's Lambda function. 22 | - events - Invocation events that you can use to invoke the function. 23 | - tests/unit - Unit tests for the application code. 24 | - template.yaml - A template that defines the application's AWS resources. 25 | 26 | ## Deploy the sample application 27 | 28 | The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for 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. 29 | 30 | To use the SAM CLI, you need the following tools. 31 | 32 | - SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) 33 | - [Python 3 installed](https://www.python.org/downloads/) 34 | - Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) 35 | 36 | To build and deploy your application for the first time, run the following in your shell: 37 | 38 | ```bash 39 | sam build --use-container 40 | sam deploy --guided 41 | ``` 42 | 43 | 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: 44 | 45 | You can find your API Gateway Endpoint URL in the output values displayed after deployment. 46 | 47 | ## Prepare DynamoDB data before you execute this function 48 | 49 | When you execute this function you need to execute data prepare script. 50 | 51 | ```bash 52 | $ chmod +x setup/add_ddb_data.sh 53 | $ setup/add_ddb_data.sh 54 | 55 | ``` 56 | 57 | ## Use the SAM CLI to build and test locally 58 | 59 | Build your application with the `sam build --use-container` command. 60 | 61 | ```bash 62 | vaccination_reservation_demo$ sam build --use-container 63 | ``` 64 | 65 | The SAM CLI installs dependencies defined in `src/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder. 66 | 67 | 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. 68 | 69 | Run functions locally and invoke them with the `sam local invoke` command. 70 | 71 | ```bash 72 | vaccination_reservation_demo$ sam local invoke HelloWorldFunction --event events/event.json 73 | ``` 74 | 75 | ## Tests 76 | 77 | Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests. 78 | 79 | ```bash 80 | vaccination_reservation_demo$ pip install -r tests/requirements.txt --user 81 | # unit test 82 | vaccination_reservation_demo$ python -m pytest tests/unit -v 83 | # integration test, requiring deploying the stack first. 84 | # Create the env variable AWS_SAM_STACK_NAME with the name of the stack we are testing 85 | vaccination_reservation_demo$ AWS_SAM_STACK_NAME= python -m pytest tests/integration -v 86 | ``` 87 | 88 | ## Cleanup 89 | 90 | To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: 91 | 92 | ```bash 93 | aws cloudformation delete-stack --stack-name vaccination_reservation_demo 94 | ``` 95 | 96 | ## Resources 97 | 98 | 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. 99 | 100 | Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) 101 | -------------------------------------------------------------------------------- /ReservationReporter-Page-1.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/ReservationReporter-Page-1.drawio.png -------------------------------------------------------------------------------- /ReservationReporter-Page-2.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/ReservationReporter-Page-2.drawio.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/__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 | -------------------------------------------------------------------------------- /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/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/src/__init__.py -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | from injector import Injector, Module 3 | from ddb_recipient_adapter import DDBRecipientAdapter 4 | from ddb_slot_adapter import DDBSlotAdapter 5 | from request_port import RequestPort 6 | from slot_port import SlotPort 7 | from recipient_port import RecipientPort 8 | from reservation_service import ReservationService 9 | 10 | class RequestPortModule(Module): 11 | def configure(self, binder): 12 | binder.bind(ReservationService, to=ReservationService( 13 | RecipientPort(DDBRecipientAdapter()), SlotPort(DDBSlotAdapter()))) 14 | 15 | def lambda_handler(event, context): 16 | ''' 17 | API Gateway event adapter 18 | 19 | retrieve reservation request parameters 20 | ex: '{"recipient_id": "1", "slot_id":"1"}' 21 | ''' 22 | body = json.loads(event['body']) 23 | recipient_id = body['recipient_id'] 24 | slot_id = body['slot_id'] 25 | 26 | injector = Injector([RequestPortModule]) 27 | request_port = injector.get(RequestPort) 28 | status = request_port.make_reservation(recipient_id, slot_id) 29 | 30 | return { 31 | "statusCode": status.status_code, 32 | "body": json.dumps({ 33 | "message": status.message 34 | }), 35 | } 36 | 37 | -------------------------------------------------------------------------------- /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 | class DDBRecipientAdapter(IRecipientAdapter): 14 | def __init__(self): 15 | ddb = boto3.resource('dynamodb') 16 | self.__table = ddb.Table(table_name) 17 | 18 | def load(self, recipient_id:str) -> Recipient: 19 | try: 20 | response = self.__table.get_item( 21 | Key={'pk': pk_prefix + recipient_id}) 22 | if 'Item' in response: 23 | item = response['Item'] 24 | email = item['email'] 25 | first_name = item['first_name'] 26 | last_name = item['last_name'] 27 | age = item['age'] 28 | recipient = Recipient(recipient_id, email, first_name, last_name, age) 29 | print("in the ddb_recipient_adapter") 30 | print(recipient) 31 | 32 | if 'slots' in item: 33 | slots = item['slots'] 34 | for slot in slots: 35 | print(slot) 36 | 37 | slot_id = slot['slot_id'] 38 | reservation_date = slot['reservation_date'] 39 | location = slot['location'] 40 | recipient.add_reserve_slot(Slot( 41 | slot_id, 42 | datetime.strptime(reservation_date, '%Y-%m-%d %H:%M:%S'), 43 | location)) 44 | 45 | return recipient 46 | 47 | print("Item not found!") 48 | return None 49 | 50 | except ClientError as e: 51 | print(e.response['Error']['Message']) 52 | return None 53 | except Exception as e: 54 | print(e) 55 | return None 56 | 57 | def save(self, recipient:Recipient) -> bool: 58 | try: 59 | item = { 60 | "pk": pk_prefix + recipient.recipient_id, 61 | "email": recipient.email, 62 | "first_name": recipient.first_name, 63 | "last_name": recipient.last_name, 64 | "age": recipient.age, 65 | "slots": [] 66 | } 67 | 68 | slots = recipient.slots 69 | for slot in slots: 70 | slot_item = { 71 | "slot_id": slot.slot_id, 72 | "reservation_date": slot.reservation_date.strftime('%Y-%m-%d %H:%M:%S'), 73 | "location": slot.location 74 | } 75 | item['slots'].append(slot_item) 76 | 77 | self.__table.put_item(Item=item) 78 | return True 79 | 80 | except ClientError as e: 81 | print(e.response['Error']['Message']) 82 | return False 83 | -------------------------------------------------------------------------------- /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 | class DDBSlotAdapter(ISlotAdapter): 12 | def __init__(self): 13 | ddb = boto3.resource('dynamodb') 14 | self.__table = ddb.Table(table_name) 15 | 16 | def load(self, slot_id:str) -> Slot: 17 | try: 18 | response = self.__table.get_item( 19 | Key={'pk': pk_prefix + slot_id}) 20 | if 'Item' in response: 21 | item = response['Item'] 22 | reservation_date = item['reservation_date'] 23 | location = item['location'] 24 | slot = Slot( 25 | slot_id, 26 | datetime.strptime(reservation_date, '%Y-%m-%d %H:%M:%S'), 27 | location) 28 | 29 | return slot 30 | return None 31 | 32 | except ClientError as e: 33 | print(e.response['Error']['Message']) 34 | return None 35 | -------------------------------------------------------------------------------- /src/i_recipient_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from recipient import Recipient 3 | 4 | class IRecipientAdapter(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def load(self, recipient_id:str) -> Recipient: 8 | raise NotImplementedError() 9 | 10 | @abstractmethod 11 | def save(self, recipient:Recipient) -> bool: 12 | raise NotImplementedError() 13 | -------------------------------------------------------------------------------- /src/i_recipient_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from recipient import Recipient 3 | 4 | class IRecipientPort(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def recipient_by_id(self, recipient_id:str) -> Recipient: 8 | raise NotImplementedError() 9 | 10 | @abstractmethod 11 | def add_reservation(self, recipient:Recipient) -> bool: 12 | raise NotImplementedError() 13 | 14 | -------------------------------------------------------------------------------- /src/i_slot_adapter.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from slot import Slot 3 | 4 | class ISlotAdapter(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def load(self, slot_id:str) -> Slot: 8 | raise NotImplementedError() 9 | -------------------------------------------------------------------------------- /src/i_slot_port.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from slot import Slot 3 | 4 | class ISlotPort(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def slot_by_id(self, slot_id:str) -> Slot: 8 | raise NotImplementedError() 9 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from injector import Injector, Module 2 | from ddb_recipient_adapter import DDBRecipientAdapter 3 | from ddb_slot_adapter import DDBSlotAdapter 4 | from request_port import RequestPort 5 | from slot_port import SlotPort 6 | from recipient_port import RecipientPort 7 | from reservation_service import ReservationService 8 | 9 | class RequestPortModule(Module): 10 | def configure(self, binder): 11 | binder.bind(ReservationService, to=ReservationService( 12 | RecipientPort(DDBRecipientAdapter()), SlotPort(DDBSlotAdapter()))) 13 | 14 | def main(): 15 | injector = Injector([RequestPortModule]) 16 | request_port = injector.get(RequestPort) 17 | status = request_port.make_reservation("1", "1") 18 | print(f"status_code: {status.status_code}, message: {status.message}") 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /src/recipient.py: -------------------------------------------------------------------------------- 1 | from slot import Slot 2 | 3 | class Recipient: 4 | def __init__(self, recipient_id:str, email:str, first_name:str, last_name:str, age:int): 5 | self.__recipient_id = recipient_id 6 | self.__email = email 7 | self.__first_name = first_name 8 | self.__last_name = last_name 9 | self.__age = age 10 | self.__slots = [] 11 | 12 | @property 13 | def recipient_id(self): 14 | return self.__recipient_id 15 | 16 | @property 17 | def email(self): 18 | return self.__email 19 | 20 | @property 21 | def first_name(self): 22 | return self.__first_name 23 | 24 | @property 25 | def last_name(self): 26 | return self.__last_name 27 | 28 | @property 29 | def age(self): 30 | return self.__age 31 | 32 | @property 33 | def slots(self): 34 | return self.__slots 35 | 36 | def are_slots_same_date(self, slot:Slot) -> bool: 37 | for selfslot in self.__slots: 38 | if selfslot.reservation_date == slot.reservation_date: 39 | return True 40 | return False 41 | 42 | def is_slot_counts_equal_or_over_two(self) -> bool: 43 | if len(self.__slots) >= 2: 44 | return True 45 | return False 46 | 47 | def add_reserve_slot(self, slot:Slot) -> bool: 48 | if self.are_slots_same_date(slot): 49 | return False 50 | 51 | if self.is_slot_counts_equal_or_over_two(): 52 | return False 53 | 54 | self.__slots.append(slot) 55 | slot.use_slot() 56 | return True 57 | -------------------------------------------------------------------------------- /src/recipient_port.py: -------------------------------------------------------------------------------- 1 | from injector import inject 2 | from recipient import Recipient 3 | from i_recipient_port import IRecipientPort 4 | from i_recipient_adapter import IRecipientAdapter 5 | 6 | class RecipientPort(IRecipientPort): 7 | 8 | @inject 9 | def __init__(self, adapter:IRecipientAdapter): 10 | self.__adapter = adapter 11 | 12 | def 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/request_port.py: -------------------------------------------------------------------------------- 1 | from injector import inject 2 | from reservation_service import ReservationService 3 | from status import Status 4 | 5 | class RequestPort: 6 | @inject 7 | def __init__(self, reservation_service:ReservationService): 8 | self.__reservation_service = reservation_service 9 | 10 | def make_reservation(self, recipient_id:str, slot_id:str) -> Status: 11 | status = None 12 | # call domain object 13 | if self.__reservation_service.add_reservation(recipient_id, slot_id) == True: 14 | status = Status(200, "The recipient's reservation is added.") 15 | else: 16 | status = Status(200, "The recipient's reservation is NOT added!") 17 | return status 18 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | injector -------------------------------------------------------------------------------- /src/reservation_service.py: -------------------------------------------------------------------------------- 1 | from injector import inject 2 | from i_recipient_port import IRecipientPort 3 | from i_slot_port import ISlotPort 4 | 5 | class ReservationService: 6 | @inject 7 | def __init__(self, recipient_port:IRecipientPort, slot_port:ISlotPort): 8 | self.__recipient_port = recipient_port 9 | self.__slot_port = slot_port 10 | 11 | def add_reservation(self, recipient_id:str, slot_id:str) -> bool: 12 | recipient = self.__recipient_port.recipient_by_id(recipient_id) 13 | slot = self.__slot_port.slot_by_id(slot_id) 14 | 15 | if recipient == None or slot == None: 16 | return False 17 | 18 | print(f"recipient: {recipient.first_name}, slot date: {slot.reservation_date}") 19 | ret = recipient.add_reserve_slot(slot) 20 | if ret == True: 21 | ret = self.__recipient_port.add_reservation(recipient) 22 | return ret 23 | 24 | -------------------------------------------------------------------------------- /src/slot.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | class Slot: 4 | def __init__(self, slot_id:str, reservation_date:datetime, location:str): 5 | self.__slot_id = slot_id 6 | self.__reservation_date = reservation_date 7 | self.__location = location 8 | self.__is_vacant = True 9 | 10 | @property 11 | def slot_id(self): 12 | return self.__slot_id 13 | 14 | @property 15 | def reservation_date(self): 16 | return self.__reservation_date 17 | 18 | @property 19 | def location(self): 20 | return self.__location 21 | 22 | @property 23 | def is_vacant(self): 24 | return self.__is_vacant 25 | 26 | def use_slot(self): 27 | self.__is_vacant = False 28 | -------------------------------------------------------------------------------- /src/slot_port.py: -------------------------------------------------------------------------------- 1 | from injector import inject 2 | from slot import Slot 3 | from i_slot_port import ISlotPort 4 | from i_slot_adapter import ISlotAdapter 5 | 6 | class SlotPort(ISlotPort): 7 | @inject 8 | def __init__(self, adapter:ISlotAdapter): 9 | self.__adapter = adapter 10 | 11 | def slot_by_id(self, slot_id:str) -> Slot: 12 | return self.__adapter.load(slot_id) 13 | -------------------------------------------------------------------------------- /src/status.py: -------------------------------------------------------------------------------- 1 | class Status: 2 | def __init__(self, status_code:int, message:str): 3 | self.__status_code = status_code 4 | self.__message = message 5 | 6 | @property 7 | def status_code(self)->int: 8 | return self.__status_code 9 | 10 | @property 11 | def message(self)->str: 12 | return self.__message 13 | -------------------------------------------------------------------------------- /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/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/tests/__init__.py -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-mock 3 | boto3 4 | injector 5 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afukui/jaws-pankration-ddd-lambda/e5340a6a25de24aa6e1a93fa06094502dcddb7b7/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_port.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from injector import Injector, Module 4 | 5 | sys.path.append("./src/") 6 | 7 | from i_recipient_adapter import IRecipientAdapter 8 | from recipient_port import RecipientPort 9 | from recipient import Recipient 10 | 11 | recipient_id = "1" 12 | email = "fatsushi@example.com" 13 | first_name = "Atsushi" 14 | last_name = "Fukui" 15 | age = 30 16 | 17 | 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 | class DummyModule(Module): 27 | def configure(self, binder): 28 | binder.bind(RecipientPort, to=RecipientPort(DummyRecipientAdapter())) 29 | 30 | 31 | @pytest.fixture() 32 | def fixture_recipient_port(): 33 | injector = Injector([DummyModule]) 34 | recipient_port = injector.get(RecipientPort) 35 | return recipient_port 36 | 37 | 38 | def test_recipient_port_recipient_by_id(fixture_recipient_port): 39 | target = fixture_recipient_port 40 | recipient_id = "dummy_number" 41 | recipient = target.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_port): 50 | target = fixture_recipient_port 51 | ret = target.add_reservation(Recipient(recipient_id, email, first_name, last_name, age)) 52 | assert True == ret 53 | -------------------------------------------------------------------------------- /tests/unit/test_reservation_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from injector import Injector, Module 4 | from datetime import datetime 5 | 6 | sys.path.append("./src/") 7 | 8 | from reservation_service import ReservationService 9 | from i_recipient_adapter import IRecipientAdapter 10 | from recipient_port import RecipientPort 11 | from recipient import Recipient 12 | from i_slot_adapter import ISlotAdapter 13 | from slot_port import SlotPort 14 | from slot import Slot 15 | 16 | recipient_id = "1" 17 | email = "fatsushi@example.com" 18 | first_name = "Atsushi" 19 | last_name = "Fukui" 20 | age = 30 21 | slot_id = "1" 22 | reservation_date = datetime(2021,12,20, 9, 0, 0) 23 | location = "Tokyo" 24 | 25 | 26 | class DummyRecipientAdapter(IRecipientAdapter): 27 | def load(self, recipient_id:str) -> Recipient: 28 | return Recipient(recipient_id, email, first_name, last_name, age) 29 | 30 | def save(self, recipient:Recipient) -> bool: 31 | return True 32 | 33 | 34 | class DummySlotAdapter(ISlotAdapter): 35 | def load(self, slot_id:str) -> Slot: 36 | return Slot(slot_id, reservation_date, location) 37 | 38 | 39 | class DummyModule(Module): 40 | def configure(self, binder): 41 | binder.bind(ReservationService, to=ReservationService( 42 | RecipientPort(DummyRecipientAdapter()), SlotPort(DummySlotAdapter()))) 43 | 44 | 45 | @pytest.fixture() 46 | def fixture_reservation_service(): 47 | injector = Injector([DummyModule]) 48 | reservation_service = injector.get(ReservationService) 49 | return reservation_service 50 | 51 | 52 | def test_add_reservation(fixture_reservation_service): 53 | target = fixture_reservation_service 54 | recipient_id = "dummy_id" 55 | slot_id = "dummy_id" 56 | 57 | ret = target.add_reservation(recipient_id, slot_id) 58 | assert True == ret 59 | 60 | -------------------------------------------------------------------------------- /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_port.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sys 3 | from injector import Injector, Module 4 | from datetime import datetime 5 | 6 | sys.path.append("./src/") 7 | 8 | from i_slot_adapter import ISlotAdapter 9 | from slot_port import SlotPort 10 | from slot import Slot 11 | 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 | class DummyModule(Module): 23 | def configure(self, binder): 24 | binder.bind(SlotPort, to=SlotPort(DummySlotAdapter())) 25 | 26 | 27 | @pytest.fixture() 28 | def fixture_slot_port(): 29 | injector = Injector([DummyModule]) 30 | slot_port = injector.get(SlotPort) 31 | return slot_port 32 | 33 | 34 | def test_recipient_port_recipient_by_id(fixture_slot_port): 35 | target = fixture_slot_port 36 | slot_id = "1" 37 | slot = target.slot_by_id(slot_id) 38 | assert slot != None 39 | assert reservation_date == slot.reservation_date 40 | assert location == slot.location 41 | 42 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------