diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..3ca78de
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @muhamadto
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..bd3c017
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Versions**
+
+**Additional context**
+Add any other context about the problem here.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..24473de
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..1c59b0b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,24 @@
+# Licensed to Muhammad Hamadto
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.# To get started with Dependabot version updates, you'll need to specify which
+
+
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..ed8dc08
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,53 @@
+# Licensed to Muhammad Hamadto
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "Build"
+
+on:
+ push:
+ branches: [ "*" ]
+ pull_request:
+ branches: [ "main" ]
+ types: [ opened, synchronize, reopened ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ cache: 'maven'
+ - name: Cache Sonar packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.sonar/cache
+ key: ${{ runner.os }}-sonar
+ restore-keys: ${{ runner.os }}-sonar
+ - name: Cache Maven packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.m2
+ key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: ${{ runner.os }}-m2
+ - name: Build with Maven
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ run: mvn --no-transfer-progress clean verify
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..307767b
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,72 @@
+# Licensed to Muhammad Hamadto
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "*" ]
+ pull_request:
+ branches: [ "main" ]
+ types: [ opened, synchronize, reopened ]
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'java' ]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: '21'
+ cache: 'maven'
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ - name: maven-settings-xml-action
+ uses: whelk-io/maven-settings-xml-action@v18
+ with:
+ repositories: >
+ [
+ {
+ "id": "spring-releases",
+ "name": "Spring Releases",
+ "url": "https://repo.spring.io/release"
+ }
+ ]
+ plugin_repositories: >
+ [
+ {
+ "id": "spring-releases",
+ "name": "Spring Releases",
+ "url": "https://repo.spring.io/release"
+ }
+ ]
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..1056af9
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,37 @@
+# Licensed to Muhammad Hamadto
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: Publish package to Maven Central
+on:
+ release:
+ types: [ published ]
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ java-version: '21'
+ distribution: 'temurin'
+ - name: Publish package
+ run: mvn --batch-mode deploy
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b425f09
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
diff --git a/.mvn/MavenWrapperDownloader.java b/.mvn/MavenWrapperDownloader.java
new file mode 100644
index 0000000..4187c8d
--- /dev/null
+++ b/.mvn/MavenWrapperDownloader.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2007-present the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import java.net.*;
+import java.io.*;
+import java.nio.channels.*;
+import java.util.Properties;
+
+public class MavenWrapperDownloader {
+
+ private static final String WRAPPER_VERSION = "0.5.6";
+ /**
+ * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
+ */
+ private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
+
+ /**
+ * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
+ * use instead of the default one.
+ */
+ private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
+ ".mvn/wrapper/maven-wrapper.properties";
+
+ /**
+ * Path where the maven-wrapper.jar will be saved to.
+ */
+ private static final String MAVEN_WRAPPER_JAR_PATH =
+ ".mvn/wrapper/maven-wrapper.jar";
+
+ /**
+ * Name of the property which should be used to override the default download url for the wrapper.
+ */
+ private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
+
+ public static void main(String args[]) {
+ System.out.println("- Downloader started");
+ File baseDirectory = new File(args[0]);
+ System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
+
+ // If the maven-wrapper.properties exists, read it and check if it contains a custom
+ // wrapperUrl parameter.
+ File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
+ String url = DEFAULT_DOWNLOAD_URL;
+ if(mavenWrapperPropertyFile.exists()) {
+ FileInputStream mavenWrapperPropertyFileInputStream = null;
+ try {
+ mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
+ Properties mavenWrapperProperties = new Properties();
+ mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
+ url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
+ } catch (IOException e) {
+ System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
+ } finally {
+ try {
+ if(mavenWrapperPropertyFileInputStream != null) {
+ mavenWrapperPropertyFileInputStream.close();
+ }
+ } catch (IOException e) {
+ // Ignore ...
+ }
+ }
+ }
+ System.out.println("- Downloading from: " + url);
+
+ File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
+ if(!outputFile.getParentFile().exists()) {
+ if(!outputFile.getParentFile().mkdirs()) {
+ System.out.println(
+ "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
+ }
+ }
+ System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
+ try {
+ downloadFileFromURL(url, outputFile);
+ System.out.println("Done");
+ System.exit(0);
+ } catch (Throwable e) {
+ System.out.println("- Error downloading");
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ private static void downloadFileFromURL(String urlString, File destination) throws Exception {
+ if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
+ String username = System.getenv("MVNW_USERNAME");
+ char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
+ Authenticator.setDefault(new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(username, password);
+ }
+ });
+ }
+ URL website = new URL(urlString);
+ ReadableByteChannel rbc;
+ rbc = Channels.newChannel(website.openStream());
+ FileOutputStream fos = new FileOutputStream(destination);
+ fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
+ fos.close();
+ rbc.close();
+ }
+
+}
\ No newline at end of file
diff --git a/.mvn/maven-wrapper.properties b/.mvn/maven-wrapper.properties
new file mode 100644
index 0000000..a1af7c5
--- /dev/null
+++ b/.mvn/maven-wrapper.properties
@@ -0,0 +1,34 @@
+#
+# Licensed to Muhammad Hamadto
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Licensed to Muhammad Hamadto
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
+wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..53eb30c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+muhamadto@gmail.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..605698c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,15 @@
+Licensed to Muhammad Hamadto
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+https://www.apache.org/licenses/LICENSE-2.0
+
+See the NOTICE file distributed with this work for additional information regarding
+copyright ownership.
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
\ No newline at end of file
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..e69de29
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..71c43c0
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,12 @@
+## Hello, Please Review My Pull Request!
+
+
+
+#### :heavy_check_mark: Checklist
+
+
+
+- [ ] Describe what you did in the pull request description
+- [ ] Add Unit and Integration Tests - at least 80% unit tests for new code.
+- [ ] Added or updated documentation
\ No newline at end of file
diff --git a/mvnw b/mvnw
new file mode 100644
index 0000000..66df285
--- /dev/null
+++ b/mvnw
@@ -0,0 +1,308 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Apache Maven Wrapper startup batch script, version 3.2.0
+#
+# Required ENV vars:
+# ------------------
+# JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+# MAVEN_OPTS - parameters passed to the Java VM when running Maven
+# e.g. to debug Maven itself, use
+# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+ if [ -f /usr/local/etc/mavenrc ] ; then
+ . /usr/local/etc/mavenrc
+ fi
+
+ if [ -f /etc/mavenrc ] ; then
+ . /etc/mavenrc
+ fi
+
+ if [ -f "$HOME/.mavenrc" ] ; then
+ . "$HOME/.mavenrc"
+ fi
+
+fi
+
+# OS specific support. $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "$(uname)" in
+ CYGWIN*) cygwin=true ;;
+ MINGW*) mingw=true;;
+ Darwin*) darwin=true
+ # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+ # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+ if [ -z "$JAVA_HOME" ]; then
+ if [ -x "/usr/libexec/java_home" ]; then
+ JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
+ else
+ JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
+ fi
+ fi
+ ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+ if [ -r /etc/gentoo-release ] ; then
+ JAVA_HOME=$(java-config --jre-home)
+ fi
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+ [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
+ JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+ javaExecutable="$(which javac)"
+ if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
+ # readlink(1) is not available as standard on Solaris 10.
+ readLink=$(which readlink)
+ if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
+ if $darwin ; then
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
+ else
+ javaExecutable="$(readlink -f "\"$javaExecutable\"")"
+ fi
+ javaHome="$(dirname "\"$javaExecutable\"")"
+ javaHome=$(expr "$javaHome" : '\(.*\)/bin')
+ JAVA_HOME="$javaHome"
+ export JAVA_HOME
+ fi
+ fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+ if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ else
+ JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
+ fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+ echo "Error: JAVA_HOME is not defined correctly." >&2
+ echo " We cannot execute $JAVACMD" >&2
+ exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+ echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+ if [ -z "$1" ]
+ then
+ echo "Path not specified to find_maven_basedir"
+ return 1
+ fi
+
+ basedir="$1"
+ wdir="$1"
+ while [ "$wdir" != '/' ] ; do
+ if [ -d "$wdir"/.mvn ] ; then
+ basedir=$wdir
+ break
+ fi
+ # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+ if [ -d "${wdir}" ]; then
+ wdir=$(cd "$wdir/.." || exit 1; pwd)
+ fi
+ # end of workaround
+ done
+ printf '%s' "$(cd "$basedir" || exit 1; pwd)"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+ if [ -f "$1" ]; then
+ # Remove \r in case we run on Windows within Git Bash
+ # and check out the repository with auto CRLF management
+ # enabled. Otherwise, we may read lines that are delimited with
+ # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
+ # splitting rules.
+ tr -s '\r\n' ' ' < "$1"
+ fi
+}
+
+log() {
+ if [ "$MVNW_VERBOSE" = true ]; then
+ printf '%s\n' "$1"
+ fi
+}
+
+BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
+if [ -z "$BASE_DIR" ]; then
+ exit 1;
+fi
+
+MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
+log "$MAVEN_PROJECTBASEDIR"
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
+if [ -r "$wrapperJarPath" ]; then
+ log "Found $wrapperJarPath"
+else
+ log "Couldn't find $wrapperJarPath, downloading it ..."
+
+ if [ -n "$MVNW_REPOURL" ]; then
+ wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ else
+ wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
+ fi
+ while IFS="=" read -r key value; do
+ # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
+ safeValue=$(echo "$value" | tr -d '\r')
+ case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
+ esac
+ done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+ log "Downloading from: $wrapperUrl"
+
+ if $cygwin; then
+ wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
+ fi
+
+ if command -v wget > /dev/null; then
+ log "Found wget ... using wget"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ else
+ wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
+ fi
+ elif command -v curl > /dev/null; then
+ log "Found curl ... using curl"
+ [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
+ if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
+ curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ else
+ curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
+ fi
+ else
+ log "Falling back to using Java to download"
+ javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
+ javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
+ # For Cygwin, switch paths to Windows format before running javac
+ if $cygwin; then
+ javaSource=$(cygpath --path --windows "$javaSource")
+ javaClass=$(cygpath --path --windows "$javaClass")
+ fi
+ if [ -e "$javaSource" ]; then
+ if [ ! -e "$javaClass" ]; then
+ log " - Compiling MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/javac" "$javaSource")
+ fi
+ if [ -e "$javaClass" ]; then
+ log " - Running MavenWrapperDownloader.java ..."
+ ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
+ fi
+ fi
+ fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+# If specified, validate the SHA-256 sum of the Maven wrapper jar file
+wrapperSha256Sum=""
+while IFS="=" read -r key value; do
+ case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
+ esac
+done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
+if [ -n "$wrapperSha256Sum" ]; then
+ wrapperSha256Result=false
+ if command -v sha256sum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ elif command -v shasum > /dev/null; then
+ if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
+ wrapperSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
+ echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
+ exit 1
+ fi
+ if [ $wrapperSha256Result = false ]; then
+ echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
+ echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
+ echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+ exit 1
+ fi
+fi
+
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+ [ -n "$JAVA_HOME" ] &&
+ JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
+ [ -n "$CLASSPATH" ] &&
+ CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
+ [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+ MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+fi
+
+# Provide a "standardized" way to retrieve the CLI args that will
+# work with both Windows and non-Windows executions.
+MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
+export MAVEN_CMD_LINE_ARGS
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+# shellcheck disable=SC2086 # safe args
+exec "$JAVACMD" \
+ $MAVEN_OPTS \
+ $MAVEN_DEBUG_OPTS \
+ -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+ "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+ ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
new file mode 100644
index 0000000..2f8b90b
--- /dev/null
+++ b/mvnw.cmd
@@ -0,0 +1,182 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+
+FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
+ IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Found %WRAPPER_JAR%
+ )
+) else (
+ if not "%MVNW_REPOURL%" == "" (
+ SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
+ )
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Couldn't find %WRAPPER_JAR%, downloading it ...
+ echo Downloading from: %DOWNLOAD_URL%
+ )
+
+ powershell -Command "&{"^
+ "$webclient = new-object System.Net.WebClient;"^
+ "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
+ "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
+ "}"^
+ "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
+ "}"
+ if "%MVNW_VERBOSE%" == "true" (
+ echo Finished downloading %WRAPPER_JAR%
+ )
+)
+@REM End of extension
+
+@REM Provide a "standardized" way to retrieve the CLI args that will
+@REM work with both Windows and non-Windows executions.
+set MAVEN_CMD_LINE_ARGS=%*
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..22f6459
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,207 @@
+
+
+
+
+ 4.0.0
+
+ io.sandpipers
+ sandpipers-cdk
+ 1.0-SNAPSHOT
+ pom
+
+ sandpipers-cdk-constructs
+ sandpipers-cdk-assertions
+ sandpipers-cdk-examples
+ sandpipers-cdk-types
+ sandpipers-cdk-bom
+
+
+
+ 21
+ 21
+ UTF-8
+
+ 2.128.0
+ 5.10.0
+
+
+
+
+ github
+ GitHub Packages
+ https://maven.pkg.github.com/muhamadto/sandpipers-cdk
+
+
+
+
+
+
+
+ io.sandpipers
+ sandpipers-cdk-constructs
+ ${project.version}
+
+
+
+ io.sandpipers
+ sandpipers-cdk-assertions
+ ${project.version}
+
+
+
+ io.sandpipers
+ sandpipers-cdk-types
+ ${project.version}
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+ ${aws-cdk.version}
+ provided
+
+
+
+ software.amazon.awscdk
+ apprunner-alpha
+ ${aws-cdk.version}-alpha.0
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.14.0
+
+
+
+ org.apache.commons
+ commons-collections4
+ 4.4
+
+
+
+ javax.validation
+ validation-api
+ 2.0.1.Final
+
+
+
+ com.google.guava
+ guava
+ 33.0.0-jre
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
+
+ org.typefactory
+ type-factory-bom
+ 1.0.0
+ pom
+ import
+
+
+
+ com.fasterxml.jackson
+ jackson-bom
+ 2.17.0
+ pom
+ import
+
+
+
+
+
+ org.assertj
+ assertj-core
+ 3.25.3
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.2
+ test
+
+
+
+ org.mockito
+ mockito-core
+ ${mockito-core.version}
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito-core.version}
+ test
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ 1.19.0
+ pom
+ import
+
+
+
+
+
+
+
+ matto
+ Muhammad Hamadto
+ https://github.com/muhamadto
+
+
+
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ Licensed to Muhammad Hamadto
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ https://www.apache.org/licenses/LICENSE-2.0
+
+ See the NOTICE file distributed with this work for additional information regarding
+ copyright ownership.
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+
+
\ No newline at end of file
diff --git a/sandpipers-cdk-assertions/README.md b/sandpipers-cdk-assertions/README.md
new file mode 100644
index 0000000..d4e09d2
--- /dev/null
+++ b/sandpipers-cdk-assertions/README.md
@@ -0,0 +1,117 @@
+# sandpipers-cdk-assertions
+
+AssertJ-like fluent assertions for AWS CDK testing
+
+### AWS CDK testing framework
+
+AWS offers a framework to verify that your stack is well-formed and that it matches your expectations. This framework can be found [here](https://docs.aws.amazon.com/cdk/latest/guide/testing.html).
+
+Here is an example to very the stack contains a specific role:
+
+
+```java
+
+template.hasResourceProperties("AWS::IAM::Role", Match.objectEquals(
+ Collections.singletonMap("AssumeRolePolicyDocument", Map.of(
+ "Version", "2012-10-17",
+ "Statement", Collections.singletonList(Map.of(
+ "Action", "sts:AssumeRole",
+ "Effect", "Allow",
+ "Principal", Collections.singletonMap(
+ "Service", Collections.singletonMap(
+ "Fn::Join", Arrays.asList(
+ "",
+ Arrays.asList("states.", Match.anyValue(), ".amazonaws.com")
+ )
+ )
+ )
+ ))
+ ))
+));
+```
+
+### AWS CDK fluent testing library
+I noticed that the tests written using the AWS framework were verbose and presented challenges in terms of readability and maintainability. In response, I decided to create a small library to make it easier to read, write and maintain tests.
+
+To illustrate, an equivalent representation to the aforementioned example can be found below: as follows"
+```java
+CDKStackAssert.assertThat(template)
+ .containsRoleWithManagedPolicyArn(managedPolicyArn)
+ .hasAssumeRolePolicyDocument("states.amazonaws.com", null, "Allow", "2012-10-17", "sts:AssumeRole");
+```
+
+#### How to use it
+
+* The library is available on Maven Central. To use it, add the following dependency to your project:
+
+```xml
+
+ io.sandpipers
+ sandpipers-cdk-assertions
+ test
+
+```
+
+* Create a class `TemplateSupport` in your testing packages. This class will be used to load the template and create the `CDKStackAssert` object.
+
+```java
+public abstract class TemplateSupport {
+
+ public static final String ENV = "test";
+ public static final String TEST_CDK_BUCKET = "test-cdk-bucket";
+ public static final String QUALIFIER = "test";
+ static Template template;
+ private static final String STACK_NAME = "example-lambda-function-test-stack";
+
+ @TempDir
+ private static Path TEMP_DIR;
+
+ @BeforeAll
+ static void initAll() throws IOException {
+ final Path lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR);
+
+ final Map tags = createTags(ENV, TAG_VALUE_COST_CENTRE);
+ final App app = new App();
+ final ExampleLambdaStack stack = StackUtils.createStack(app, STACK_NAME, lambdaCodePath.toString(), QUALIFIER, TEST_CDK_BUCKET, ENV);
+
+ tags.entrySet().stream()
+ .filter(tag -> Objects.nonNull(tag.getValue()))
+ .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue()));
+
+ template = Template.fromStack(stack);
+ }
+
+ @AfterAll
+ static void cleanup() {
+ template = null;
+ }
+}
+```
+* Extend the `TemplateSupport` class in your test classes and use the `CDKStackAssert` object to verify your stack.
+
+```java
+class LambdaTest extends TemplateSupport {
+
+ public static final String TEST = "test";
+
+ @Test
+ void should_have_lambda_function() {
+
+ CDKStackAssert.assertThat(template)
+ .containsFunction("example-lambda-function")
+ .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest")
+ .hasCode("test-cdk-bucket", "(.*).zip")
+ .hasRole("examplelambdafunctionrole(.*)")
+ .hasDependency("examplelambdafunctionrole(.*)")
+ .hasDependency("examplelambdafunctionroleDefaultPolicy(.*)")
+ .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE)
+ .hasTag("ENV", TEST)
+ .hasEnvironmentVariable("ENV", TEST)
+ .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST)
+ .hasDescription("Lambda example")
+ .hasMemorySize(512)
+ .hasRuntime("provided.al2")
+ .hasTimeout(3);
+ }
+}
+```
\ No newline at end of file
diff --git a/sandpipers-cdk-assertions/pom.xml b/sandpipers-cdk-assertions/pom.xml
new file mode 100644
index 0000000..fadd4a0
--- /dev/null
+++ b/sandpipers-cdk-assertions/pom.xml
@@ -0,0 +1,81 @@
+
+
+
+
+ 4.0.0
+
+ io.sandpipers
+ sandpipers-cdk
+ 1.0-SNAPSHOT
+
+
+ sandpipers-cdk-assertions
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ io.sandpipers
+ sandpipers-cdk-constructs
+
+
+
+
+ software.amazon.awscdk
+ aws-cdk-lib
+
+
+
+ software.amazon.awscdk
+ apprunner-alpha
+
+
+
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+ org.assertj
+ assertj-core
+
+
+ org.apache.commons
+ commons-collections4
+
+
+
+
+
\ No newline at end of file
diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java
new file mode 100644
index 0000000..33c48ac
--- /dev/null
+++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to Muhammad Hamadto
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.sandpipers.cdk.assertion;
+
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.AbstractAssert;
+import org.assertj.core.api.Assertions;
+import org.assertj.core.api.InstanceOfAssertFactories;
+
+@SuppressWarnings("unchecked")
+public abstract class AbstractCDKResourcesAssert, ACTUAL extends Map> extends
+ AbstractAssert {
+
+ protected AbstractCDKResourcesAssert(ACTUAL actual, Class> selfType) {
+ super(actual, selfType);
+ }
+
+ public SELF hasTag(final String key, final Object value) {
+ hasTag("Tags", key, value);
+ return myself;
+ }
+
+ public SELF hasTag(final String tagsMapId,
+ final String key,
+ final Object value) {
+
+ final Map> properties =
+ (Map>) actual.get("Properties");
+
+ final List
+ *
+ * @param id the name of the lambda function
+ * @return {@link LambdaPermissionAssert} instance
+ */
+ public LambdaPermissionAssert containsLambdaPermission(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.LAMBDA_PERMISSION, id);
+
+ return LambdaPermissionAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::ApiGateway::BasePathMapping. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiDomainNameAssert} should be used
+ * instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link ApiDomainNameAssert} instance
+ */
+ public ApiDomainNameAssert containsApiDomainName(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_DOMAIN_NAME, id);
+
+ return ApiDomainNameAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::ApiGateway::BasePathMapping. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiBasePathMappingAssert} should be
+ * used instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link ApiBasePathMappingAssert} instance
+ */
+ public ApiBasePathMappingAssert containsApiPathMapping(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_BASE_PATH_MAPPING, id);
+
+ return ApiBasePathMappingAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::SQS::QueuePolicy. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link QueuePolicyAssert} should be used
+ * instead.
+ *
+ *
+ *
+ * @param id the queue policy id
+ * @return {@link QueuePolicyAssert} instance
+ */
+ public QueuePolicyAssert containsQueuePolicy(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.QUEUE_POLICY, id);
+
+ return QueuePolicyAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::SQS::Queue. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link QueueAssert} should be used instead.
+ *
+ *
+ *
+ * @param id the key of the queue
+ * @return {@link QueueAssert} instance
+ */
+ public QueueAssert containsQueue(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.QUEUE, id);
+
+ return QueueAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::SNS::Topic. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link TopicAssert} should be used instead.
+ *
+ *
+ *
+ * @param id the key of the queue
+ * @return {@link TopicAssert} instance
+ */
+ public TopicAssert containsTopic(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.TOPIC, id);
+
+ return TopicAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::SNS::Subscription. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link TopicSubscriptionAssert} should be
+ * used instead.
+ *
+ *
+ *
+ * @param id the topic subscription id
+ * @return {@link TopicSubscriptionAssert} instance
+ */
+ public TopicSubscriptionAssert containsTopicSubscription(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.TOPIC_SUBSCRIPTION, id);
+
+ return TopicSubscriptionAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::CertificateManager::Certificate. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link CertificateAssert} should be used
+ * instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link CertificateAssert} instance
+ */
+ public CertificateAssert containsCertificate(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.CERTIFICATEMANAGER_CERTIFICATE, id);
+
+ return CertificateAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::Route53::ARecord. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link RecordSetAssert} should be used
+ * instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link RecordSetAssert} instance
+ */
+ public RecordSetAssert containsRecordSet(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.ROUTE53_RECORD_SET, id);
+
+ return RecordSetAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::Route53::ARecord. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link HostedZoneAssert} should be used
+ * instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link HostedZoneAssert} instance
+ */
+ public HostedZoneAssert containsHostedZone(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.ROUTE53_HOSTED_ZONE, id);
+
+ return HostedZoneAssert.assertThat(resource.getValue());
+ }
+
+ /**
+ * Fluent assertions for AWS::DynamoDB::GlobalTable. Assertions are done directly on an object of
+ * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link DynamoDBGlobalTableAssert} should be used
+ * instead.
+ *
+ * @param id the name of the lambda function
+ * @return {@link DynamoDBGlobalTableAssert} instance
+ */
+ public DynamoDBGlobalTableAssert containsDynamoDBTable(final String id) {
+
+ final Entry> resource = containsResource(actual, CdkResourceType.DYNAMODB_GLOBAL_TABLE, id);
+
+ return DynamoDBGlobalTableAssert.assertThat(resource.getValue());
+ }
+
+ private Entry> containsResource(
+ final Template template,
+ final CdkResourceType cdkResourceType,
+ final String id) {
+
+ final Entry> resource = template.findResources(cdkResourceType.getValue())
+ .entrySet()
+ .stream()
+ .filter(entry -> stringLikeRegexp(id).test(entry.getKey()).getIsSuccess())
+ .findFirst()
+ .orElseThrow();
+
+ Assertions.assertThat(resource)
+ .isNotNull();
+
+ return resource;
+ }
+}
diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java
new file mode 100644
index 0000000..c3961bb
--- /dev/null
+++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to Muhammad Hamadto
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.sandpipers.cdk.assertion;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public enum CdkResourceType {
+ APIGATEWAY_RESTAPI("AWS::ApiGateway::RestApi"),
+ APIGATEWAY_METHOD("AWS::ApiGateway::Method"),
+ APIGATEWAY_RESOURCE("AWS::ApiGateway::Resource"),
+ APIGATEWAY_ACCOUNT("AWS::ApiGateway::Account"),
+ APIGATEWAY_DEPLOYMENT("AWS::ApiGateway::Deployment"),
+ APIGATEWAY_STAGE("AWS::ApiGateway::Stage"),
+ APIGATEWAY_DOMAIN_NAME("AWS::ApiGateway::DomainName"),
+ APIGATEWAY_BASE_PATH_MAPPING("AWS::ApiGateway::BasePathMapping"),
+ LAMBDA_FUNCTION("AWS::Lambda::Function"),
+ LAMBDA_EVENT_INVOKE_CONFIG("AWS::Lambda::EventInvokeConfig"),
+ LAMBDA_PERMISSION("AWS::Lambda::Permission"),
+ POLICY("AWS::IAM::Policy"),
+ ROLE("AWS::IAM::Role"),
+ TOPIC("AWS::SNS::Topic"),
+ TOPIC_SUBSCRIPTION("AWS::SNS::Subscription"),
+ QUEUE("AWS::SQS::Queue"),
+ QUEUE_POLICY("AWS::SQS::QueuePolicy"),
+ BUCKET("AWS::S3::Bucket"),
+ ECS_TASK_DEFINITION("AWS::ECS::TaskDefinition"),
+ DYNAMODB_TABLE("AWS::DynamoDB::Table"),
+ DYNAMODB_GLOBAL_TABLE("AWS::DynamoDB::GlobalTable"),
+ CERTIFICATEMANAGER_CERTIFICATE("AWS::CertificateManager::Certificate"),
+ ROUTE53_RECORD_SET("AWS::Route53::RecordSet"),
+ ROUTE53_HOSTED_ZONE("AWS::Route53::HostedZone"),
+ ;
+
+ private String value;
+}
diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java
new file mode 100644
index 0000000..d3702f8
--- /dev/null
+++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Muhammad Hamadto
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.sandpipers.cdk.assertion;
+
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+
+/**
+ * Fluent assertions for AWS::CertificateManager::Certificate. This should be used if the resource map is extracted from the AWS template. Otherwise, start
+ * with{@link CDKStackAssert#containsCertificate(String)}
+ */
+@SuppressWarnings("unchecked")
+public class CertificateAssert extends AbstractCDKResourcesAssert> {
+
+ private CertificateAssert(final Map actual) {
+ super(actual, CertificateAssert.class);
+ }
+
+ public static CertificateAssert assertThat(final Map actual) {
+ return new CertificateAssert(actual);
+ }
+
+ public CertificateAssert hasDomainName(final String expected) {
+
+ final Map properties = (Map) actual.get("Properties");
+
+ final String domainName = (String) properties.get("DomainName");
+
+ Assertions.assertThat(domainName)
+ .isNotEmpty()
+ .isEqualTo(expected);
+
+ return this;
+ }
+
+ public CertificateAssert hasDomainValidationOptions(final String domainName, final String validationDomain) {
+
+ final Map properties = (Map) actual.get("Properties");
+ final List< Object> domainValidationOptions = (List< Object>) properties.get("DomainValidationOptions");
+
+ Assertions.assertThat(domainValidationOptions)
+ .isNotNull()
+ .isNotEmpty()
+ .anySatisfy(dvo -> {
+ final Map domainValidationOption = (Map) dvo;
+ final String domainNameValue = (String) domainValidationOption.get("DomainName");
+ final String validationDomainValue = (String) domainValidationOption.get("ValidationDomain");
+
+ Assertions.assertThat(domainNameValue)
+ .isNotEmpty()
+ .isEqualTo(domainName);
+
+ Assertions.assertThat(validationDomainValue)
+ .isNotEmpty()
+ .isEqualTo(validationDomain);
+ });
+
+ return this;
+ }
+
+ public CertificateAssert hasValidationMethod(final String expected) {
+
+ final Map properties = (Map) actual.get("Properties");
+ final String validationMethod = (String) properties.get("ValidationMethod");
+
+ Assertions.assertThat(validationMethod)
+ .isNotEmpty()
+ .isEqualTo(expected);
+
+ return this;
+ }
+}
diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java
new file mode 100644
index 0000000..6f86ca5
--- /dev/null
+++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to Muhammad Hamadto
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * See the NOTICE file distributed with this work for additional information regarding copyright ownership.
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.sandpipers.cdk.assertion;
+
+import java.util.List;
+import java.util.Map;
+import org.assertj.core.api.Assertions;
+
+/**
+ * Fluent assertions for AWS::DynamoDB::GlobalTable. This should be used if the resource map is extracted from the AWS template. Otherwise, start with
+ * {@link CDKStackAssert#containsDynamoDBTable(String)}.
+ */
+@SuppressWarnings("unchecked")
+public class DynamoDBGlobalTableAssert extends
+ AbstractCDKResourcesAssert> {
+
+ private DynamoDBGlobalTableAssert(final Map actual) {
+ super(actual, DynamoDBGlobalTableAssert.class);
+ }
+
+ public static DynamoDBGlobalTableAssert assertThat(final Map actual) {
+ return new DynamoDBGlobalTableAssert(actual);
+ }
+
+ public DynamoDBGlobalTableAssert hasBillingMode(final String expected) {
+
+ final String billingMode = ((Map) actual.get("Properties")).get("BillingMode");
+
+ Assertions.assertThat(billingMode)
+ .isNotNull()
+ .isInstanceOf(String.class)
+ .matches(bm -> bm.matches(expected));
+
+ return this;
+ }
+
+ public DynamoDBGlobalTableAssert hasName(final String expected) {
+
+ final String billingMode = ((Map) actual.get("Properties")).get("TableName");
+
+ Assertions.assertThat(billingMode)
+ .isNotNull()
+ .isInstanceOf(String.class)
+ .matches(bm -> bm.matches(expected));
+
+ return this;
+ }
+
+ public DynamoDBGlobalTableAssert hasSSESpecification(final Boolean expected) {
+
+ final Map properties = ((Map) actual.get("Properties"));
+ final Map sseSpecification = ((Map) properties.get("SSESpecification"));
+ final Boolean sseEnabled = sseSpecification.get("SSEEnabled");
+
+ Assertions.assertThat(sseEnabled)
+ .isEqualTo(expected);
+
+ return this;
+ }
+
+ public DynamoDBGlobalTableAssert hasKeySchema(final String name, final String type) {
+
+ final Map properties = ((Map) actual.get("Properties"));
+ final List