From 259699b1bfffbe4ea79c55150f59c24ae8ab5670 Mon Sep 17 00:00:00 2001 From: John Ajera <37360952+jajera@users.noreply.github.com> Date: Sat, 28 Sep 2024 13:54:45 +1200 Subject: [PATCH] feat: initial release first release --- .devcontainer/Dockerfile | 37 + .devcontainer/devcontainer.json | 27 + .devcontainer/scripts/common-debian.sh | 454 +++ .../scripts/docker-in-docker-debian.sh | 405 +++ .devcontainer/scripts/postCreate.sh | 14 + LICENSE | 356 +++ README.md | 9 + fwtype/doc.go | 5 + fwtype/missing_underlying_type_validation.go | 81 + ...missing_underlying_type_validation_test.go | 2828 +++++++++++++++++ fwtype/static_collection_validation.go | 142 + fwtype/static_collection_validation_test.go | 947 ++++++ go.mod | 21 + go.sum | 42 + 14 files changed, 5368 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/scripts/common-debian.sh create mode 100755 .devcontainer/scripts/docker-in-docker-debian.sh create mode 100755 .devcontainer/scripts/postCreate.sh create mode 100644 LICENSE create mode 100644 fwtype/doc.go create mode 100644 fwtype/missing_underlying_type_validation.go create mode 100644 fwtype/missing_underlying_type_validation_test.go create mode 100644 fwtype/static_collection_validation.go create mode 100644 fwtype/static_collection_validation_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..375690a --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,37 @@ +# Note: You can use any Debian/Ubuntu based image you want. +FROM docker.io/golang:1.22.7-bullseye + +# [Option] Install zsh +ARG INSTALL_ZSH="true" +# [Option] Upgrade OS packages to their latest versions +ARG UPGRADE_PACKAGES="false" +# [Option] Enable non-root Docker access in container +ARG ENABLE_NONROOT_DOCKER="true" +# [Option] Use the OSS Moby Engine instead of the licensed Docker Engine +ARG USE_MOBY="true" +# [Option] Engine/CLI Version +ARG DOCKER_VERSION="latest" + +# Enable new "BUILDKIT" mode for Docker CLI +ENV DOCKER_BUILDKIT=1 + +# Install needed packages and setup non-root user. Use a separate RUN statement to add your +# own dependencies. A user of "automatic" attempts to reuse an user ID if one already exists. +ARG USERNAME=automatic +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +COPY scripts/*.sh /tmp/scripts/ +RUN apt-get update -y \ + && /bin/bash /tmp/scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ + # Use Docker script from script library to set things up + && /bin/bash /tmp/scripts/docker-in-docker-debian.sh "${ENABLE_NONROOT_DOCKER}" "${USERNAME}" "${USE_MOBY}" "${DOCKER_VERSION}" \ + # Clean up + && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/scripts/ + +VOLUME [ "/var/lib/docker" ] + +# Setting the ENTRYPOINT to docker-init.sh will start up the Docker Engine +# inside the container "overrideCommand": false is set in devcontainer.json. +# The script will also execute CMD if you need to alter startup behaviors. +ENTRYPOINT [ "/usr/local/share/docker-init.sh" ] +CMD [ "sleep", "infinity" ] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..fdc6297 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,27 @@ +{ + "name": "Docker in Docker Development Container Template", + "dockerFile": "Dockerfile", + "runArgs": ["--init", "--privileged"], + "mounts": ["source=dind-var-lib-docker,target=/var/lib/docker,type=volume"], + "overrideCommand": false, + + "customizations": { + "vscode": { + "extensions": [ + "ms-azuretools.vscode-docker", + "esbenp.prettier-vscode", + "golang.go", + "hashicorp.terraform" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": ".devcontainer/scripts/postCreate.sh", + + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} diff --git a/.devcontainer/scripts/common-debian.sh b/.devcontainer/scripts/common-debian.sh new file mode 100755 index 0000000..bf1f9e2 --- /dev/null +++ b/.devcontainer/scripts/common-debian.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust[0-9] \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" \ No newline at end of file diff --git a/.devcontainer/scripts/docker-in-docker-debian.sh b/.devcontainer/scripts/docker-in-docker-debian.sh new file mode 100755 index 0000000..88603a9 --- /dev/null +++ b/.devcontainer/scripts/docker-in-docker-debian.sh @@ -0,0 +1,405 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./docker-in-docker-debian.sh [enable non-root docker access flag] [non-root user] [use moby] [Engine/CLI Version] [Major version for docker-compose] [azure DNS auto detection flag] + +ENABLE_NONROOT_DOCKER=${1:-"true"} +USERNAME=${2:-"automatic"} +USE_MOBY=${3:-"true"} +DOCKER_VERSION=${4:-"latest"} # The Docker/Moby Engine + CLI should match in version +DOCKER_DASH_COMPOSE_VERSION=${5:-"v1"} # v1 or v2 +AZURE_DNS_AUTO_DETECTION=${6:-"true"} +MICROSOFT_GPG_KEYS_URI="https://packages.microsoft.com/keys/microsoft.asc" +DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal jammy" +DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES="buster bullseye bionic focal hirsute impish jammy" + +# Default: Exit on any failure. +set -e + +# Setup STDERR. +err() { + echo "(!) $*" >&2 +} + +if [ "$(id -u)" -ne 0 ]; then + err 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +################### +# Helper Functions +# See: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/shared/utils.sh +################### + +# Determine the appropriate non-root user +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=root + fi +elif [ "${USERNAME}" = "none" ] || ! id -u ${USERNAME} > /dev/null 2>&1; then + USERNAME=root +fi + +# Get central common setting +get_common_setting() { + if [ "${common_settings_file_loaded}" != "true" ]; then + curl -sfL "https://aka.ms/vscode-dev-containers/script-library/settings.env" 2>/dev/null -o /tmp/vsdc-settings.env || echo "Could not download settings file. Skipping." + common_settings_file_loaded=true + fi + if [ -f "/tmp/vsdc-settings.env" ]; then + local multi_line="" + if [ "$2" = "true" ]; then multi_line="-z"; fi + local result="$(grep ${multi_line} -oP "$1=\"?\K[^\"]+" /tmp/vsdc-settings.env | tr -d '\0')" + if [ ! -z "${result}" ]; then declare -g $1="${result}"; fi + fi + echo "$1=${!1}" +} + +# Function to run apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Checks if packages are installed and installs them if not +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install --no-install-recommends "$@" + fi +} + +# Figure out correct version of a three part version number is not passed +find_version_from_git_tags() { + local variable_name=$1 + local requested_version=${!variable_name} + if [ "${requested_version}" = "none" ]; then return; fi + local repository=$2 + local prefix=${3:-"tags/v"} + local separator=${4:-"."} + local last_part_optional=${5:-"false"} + if [ "$(echo "${requested_version}" | grep -o "." | wc -l)" != "2" ]; then + local escaped_separator=${separator//./\\.} + local last_part + if [ "${last_part_optional}" = "true" ]; then + last_part="(${escaped_separator}[0-9]+)?" + else + last_part="${escaped_separator}[0-9]+" + fi + local regex="${prefix}\\K[0-9]+${escaped_separator}[0-9]+${last_part}$" + local version_list="$(git ls-remote --tags ${repository} | grep -oP "${regex}" | tr -d ' ' | tr "${separator}" "." | sort -rV)" + if [ "${requested_version}" = "latest" ] || [ "${requested_version}" = "current" ] || [ "${requested_version}" = "lts" ]; then + declare -g ${variable_name}="$(echo "${version_list}" | head -n 1)" + else + set +e + declare -g ${variable_name}="$(echo "${version_list}" | grep -E -m 1 "^${requested_version//./\\.}([\\.\\s]|$)")" + set -e + fi + fi + if [ -z "${!variable_name}" ] || ! echo "${version_list}" | grep "^${!variable_name//./\\.}$" > /dev/null 2>&1; then + err "Invalid ${variable_name} value: ${requested_version}\nValid values:\n${version_list}" >&2 + exit 1 + fi + echo "${variable_name}=${!variable_name}" +} + +########################################### +# Start docker-in-docker installation +########################################### + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + + +# Source /etc/os-release to get OS info +. /etc/os-release +# Fetch host/container arch. +architecture="$(dpkg --print-architecture)" + +# Check if distro is suppported +if [ "${USE_MOBY}" = "true" ]; then + # 'get_common_setting' allows attribute to be updated remotely + get_common_setting DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_MOBY_ARCHIVE_VERSION_CODENAMES}'" +else + get_common_setting DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES + if [[ "${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" != *"${VERSION_CODENAME}"* ]]; then + err "Unsupported distribution version '${VERSION_CODENAME}'. To resolve, please choose a compatible OS distribution" + err "Support distributions include: ${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}" + exit 1 + fi + echo "Distro codename '${VERSION_CODENAME}' matched filter '${DOCKER_LICENSED_ARCHIVE_VERSION_CODENAMES}'" +fi + +# Install dependencies +check_packages apt-transport-https curl ca-certificates pigz iptables gnupg2 dirmngr +if ! type git > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install git +fi + +# Swap to legacy iptables for compatibility +if type iptables-legacy > /dev/null 2>&1; then + update-alternatives --set iptables /usr/sbin/iptables-legacy + update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy +fi + + + +# Set up the necessary apt repos (either Microsoft's or Docker's) +if [ "${USE_MOBY}" = "true" ]; then + + # Name of open source engine/cli + engine_package_name="moby-engine" + cli_package_name="moby-cli" + + # Import key safely and import Microsoft apt repo + get_common_setting MICROSOFT_GPG_KEYS_URI + curl -sSL ${MICROSOFT_GPG_KEYS_URI} | gpg --dearmor > /usr/share/keyrings/microsoft-archive-keyring.gpg + echo "deb [arch=${architecture} signed-by=/usr/share/keyrings/microsoft-archive-keyring.gpg] https://packages.microsoft.com/repos/microsoft-${ID}-${VERSION_CODENAME}-prod ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/microsoft.list +else + # Name of licensed engine/cli + engine_package_name="docker-ce" + cli_package_name="docker-ce-cli" + + # Import key safely and import Docker apt repo + curl -fsSL https://download.docker.com/linux/${ID}/gpg | gpg --dearmor > /usr/share/keyrings/docker-archive-keyring.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/${ID} ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list +fi + +# Refresh apt lists +apt-get update + +# Soft version matching +if [ "${DOCKER_VERSION}" = "latest" ] || [ "${DOCKER_VERSION}" = "lts" ] || [ "${DOCKER_VERSION}" = "stable" ]; then + # Empty, meaning grab whatever "latest" is in apt repo + engine_version_suffix="" + cli_version_suffix="" +else + # Fetch a valid version from the apt-cache (eg: the Microsoft repo appends +azure, breakfix, etc...) + docker_version_dot_escaped="${DOCKER_VERSION//./\\.}" + docker_version_dot_plus_escaped="${docker_version_dot_escaped//+/\\+}" + # Regex needs to handle debian package version number format: https://www.systutorials.com/docs/linux/man/5-deb-version/ + docker_version_regex="^(.+:)?${docker_version_dot_plus_escaped}([\\.\\+ ~:-]|$)" + set +e # Don't exit if finding version fails - will handle gracefully + cli_version_suffix="=$(apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + engine_version_suffix="=$(apt-cache madison ${engine_package_name} | awk -F"|" '{print $2}' | sed -e 's/^[ \t]*//' | grep -E -m 1 "${docker_version_regex}")" + set -e + if [ -z "${engine_version_suffix}" ] || [ "${engine_version_suffix}" = "=" ] || [ -z "${cli_version_suffix}" ] || [ "${cli_version_suffix}" = "=" ] ; then + err "No full or partial Docker / Moby version match found for \"${DOCKER_VERSION}\" on OS ${ID} ${VERSION_CODENAME} (${architecture}). Available versions:" + apt-cache madison ${cli_package_name} | awk -F"|" '{print $2}' | grep -oP '^(.+:)?\K.+' + exit 1 + fi + echo "engine_version_suffix ${engine_version_suffix}" + echo "cli_version_suffix ${cli_version_suffix}" +fi + +# Install Docker / Moby CLI if not already installed +if type docker > /dev/null 2>&1 && type dockerd > /dev/null 2>&1; then + echo "Docker / Moby CLI and Engine already installed." +else + if [ "${USE_MOBY}" = "true" ]; then + # Install engine + set +e # Handle error gracefully + apt-get -y install --no-install-recommends moby-cli${cli_version_suffix} moby-buildx moby-engine${engine_version_suffix} + if [ $? -ne 0 ]; then + err "Packages for moby not available in OS ${ID} ${VERSION_CODENAME} (${architecture}). To resolve, either: (1) set feature option '\"moby\": false' , or (2) choose a compatible OS version (eg: 'ubuntu-20.04')." + exit 1 + fi + set -e + + # Install compose + apt-get -y install --no-install-recommends moby-compose || err "Package moby-compose (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + else + apt-get -y install --no-install-recommends docker-ce-cli${cli_version_suffix} docker-ce${engine_version_suffix} + # Install compose + apt-get -y install --no-install-recommends docker-compose-plugin || echo "(*) Package docker-compose-plugin (Docker Compose v2) not available for OS ${ID} ${VERSION_CODENAME} (${architecture}). Skipping." + fi +fi + +echo "Finished installing docker / moby!" + +# Install Docker Compose if not already installed and is on a supported architecture +if type docker-compose > /dev/null 2>&1; then + echo "Docker Compose v1 already installed." +else + target_compose_arch="${architecture}" + if [ "${target_compose_arch}" = "amd64" ]; then + target_compose_arch="x86_64" + fi + if [ "${target_compose_arch}" != "x86_64" ]; then + # Use pip to get a version that runs on this architecture + if ! dpkg -s python3-minimal python3-pip libffi-dev python3-venv > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get -y install python3-minimal python3-pip libffi-dev python3-venv + fi + export PIPX_HOME=/usr/local/pipx + mkdir -p ${PIPX_HOME} + export PIPX_BIN_DIR=/usr/local/bin + export PYTHONUSERBASE=/tmp/pip-tmp + export PIP_CACHE_DIR=/tmp/pip-tmp/cache + pipx_bin=pipx + if ! type pipx > /dev/null 2>&1; then + pip3 install --disable-pip-version-check --no-cache-dir --user pipx + pipx_bin=/tmp/pip-tmp/bin/pipx + fi + ${pipx_bin} install --pip-args '--no-cache-dir --force-reinstall' docker-compose + rm -rf /tmp/pip-tmp + else + compose_v1_version="1" + find_version_from_git_tags compose_v1_version "https://github.com/docker/compose" "tags/" + echo "(*) Installing docker-compose ${compose_v1_version}..." + curl -fsSL "https://github.com/docker/compose/releases/download/$%7Bcompose_v1_version%7D/docker-compose-Linux-x86_64" -o /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose + fi +fi + +# Install docker-compose switch if not already installed - https://github.com/docker/compose-switch#manual-installation +current_v1_compose_path="$(which docker-compose)" +target_v1_compose_path="$(dirname "${current_v1_compose_path}")/docker-compose-v1" +if ! type compose-switch > /dev/null 2>&1; then + echo "(*) Installing compose-switch..." + compose_switch_version="latest" + find_version_from_git_tags compose_switch_version "https://github.com/docker/compose-switch" + curl -fsSL "https://github.com/docker/compose-switch/releases/download/v$%7Bcompose_switch_version%7D/docker-compose-linux-$%7Barchitecture%7D" -o /usr/local/bin/compose-switch + chmod +x /usr/local/bin/compose-switch + # TODO: Verify checksum once available: https://github.com/docker/compose-switch/issues/11 + + # Setup v1 CLI as alternative in addition to compose-switch (which maps to v2) + mv "${current_v1_compose_path}" "${target_v1_compose_path}" + update-alternatives --install /usr/local/bin/docker-compose docker-compose /usr/local/bin/compose-switch 99 + update-alternatives --install /usr/local/bin/docker-compose docker-compose "${target_v1_compose_path}" 1 +fi +if [ "${DOCKER_DASH_COMPOSE_VERSION}" = "v1" ]; then + update-alternatives --set docker-compose "${target_v1_compose_path}" +else + update-alternatives --set docker-compose /usr/local/bin/compose-switch +fi + +# If init file already exists, exit +if [ -f "/usr/local/share/docker-init.sh" ]; then + echo "/usr/local/share/docker-init.sh already exists, so exiting." + exit 0 +fi +echo "docker-init doesnt exist, adding..." + +# Add user to the docker group +if [ "${ENABLE_NONROOT_DOCKER}" = "true" ]; then + if ! getent group docker > /dev/null 2>&1; then + groupadd docker + fi + + usermod -aG docker ${USERNAME} +fi + +tee /usr/local/share/docker-init.sh > /dev/null \ +<< EOF +#!/bin/sh +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +set -e + +AZURE_DNS_AUTO_DETECTION=$AZURE_DNS_AUTO_DETECTION +EOF + +tee -a /usr/local/share/docker-init.sh > /dev/null \ +<< 'EOF' +dockerd_start="$(cat << 'INNEREOF' + # explicitly remove dockerd and containerd PID file to ensure that it can start properly if it was stopped uncleanly + # ie: docker kill + find /run /var/run -iname 'docker*.pid' -delete || : + find /run /var/run -iname 'container*.pid' -delete || : + + ## Dind wrapper script from docker team, adapted to a function + # Maintained: https://github.com/moby/moby/blob/master/hack/dind + + export container=docker + + if [ -d /sys/kernel/security ] && ! mountpoint -q /sys/kernel/security; then + mount -t securityfs none /sys/kernel/security || { + echo >&2 'Could not mount /sys/kernel/security.' + echo >&2 'AppArmor detection and --privileged mode might break.' + } + fi + + # Mount /tmp (conditionally) + if ! mountpoint -q /tmp; then + mount -t tmpfs none /tmp + fi + + # cgroup v2: enable nesting + if [ -f /sys/fs/cgroup/cgroup.controllers ]; then + # move the processes from the root group to the /init group, + # otherwise writing subtree_control fails with EBUSY. + # An error during moving non-existent process (i.e., "cat") is ignored. + mkdir -p /sys/fs/cgroup/init + xargs -rn1 < /sys/fs/cgroup/cgroup.procs > /sys/fs/cgroup/init/cgroup.procs || : + # enable controllers + sed -e 's/ / +/g' -e 's/^/+/' < /sys/fs/cgroup/cgroup.controllers \ + > /sys/fs/cgroup/cgroup.subtree_control + fi + ## Dind wrapper over. + + # Handle DNS + set +e + cat /etc/resolv.conf | grep -i 'internal.cloudapp.net' + if [ $? -eq 0 ] && [ ${AZURE_DNS_AUTO_DETECTION} = "true" ] + then + echo "Setting dockerd Azure DNS." + CUSTOMDNS="--dns 168.63.129.16" + else + echo "Not setting dockerd DNS manually." + CUSTOMDNS="" + fi + set -e + + # Start docker/moby engine + ( dockerd $CUSTOMDNS > /tmp/dockerd.log 2>&1 ) & +INNEREOF +)" + +# Start using sudo if not invoked as root +if [ "$(id -u)" -ne 0 ]; then + sudo /bin/sh -c "${dockerd_start}" +else + eval "${dockerd_start}" +fi + +set +e + +# Execute whatever commands were passed in (if any). This allows us +# to set this script to ENTRYPOINT while still executing the default CMD. +exec "$@" +EOF + +chmod +x /usr/local/share/docker-init.sh +chown ${USERNAME}:root /usr/local/share/docker-init.sh + +echo 'docker-in-docker-debian script has completed!' \ No newline at end of file diff --git a/.devcontainer/scripts/postCreate.sh b/.devcontainer/scripts/postCreate.sh new file mode 100755 index 0000000..afdc9c4 --- /dev/null +++ b/.devcontainer/scripts/postCreate.sh @@ -0,0 +1,14 @@ +# Include commands that you would like to execute after the container is created +uname -a + +apt-get update -y +export DEBIAN_FRONTEND=noninteractive + +# terraform +apt-get install -y apt-utils gnupg software-properties-common +curl -s https://apt.releases.hashicorp.com/gpg | gpg --dearmor > hashicorp.gpg +install -o root -g root -m 644 hashicorp.gpg /etc/apt/trusted.gpg.d/ +apt-add-repository -y "deb [arch=amd64] https://apt.releases.hashicorp.com $(lsb_release -cs) main" +apt-get update -y +apt-get install -y terraform +rm hashicorp.gpg diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..84cd064 --- /dev/null +++ b/LICENSE @@ -0,0 +1,356 @@ +Copyright (c) 2021 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. + diff --git a/README.md b/README.md index cc42eda..405cdaa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # terraform-plugin-framework-internal + Internal libraries for the next-gen framework used to build Terraform providers. + +```bash +go mod init terraform-plugin-framework-internal +``` + +```bash +go mod tidy +``` \ No newline at end of file diff --git a/fwtype/doc.go b/fwtype/doc.go new file mode 100644 index 0000000..3cd75b7 --- /dev/null +++ b/fwtype/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package fwtype implements shared logic for interacting with the framework type system. +package fwtype diff --git a/fwtype/missing_underlying_type_validation.go b/fwtype/missing_underlying_type_validation.go new file mode 100644 index 0000000..9d04811 --- /dev/null +++ b/fwtype/missing_underlying_type_validation.go @@ -0,0 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ContainsMissingUnderlyingType will return true if an attr.Type is +// a complex type that either is or contains any collection types with missing +// element or attribute types. Primitives will return false. Nil will return +// true. +func ContainsMissingUnderlyingType(typ attr.Type) bool { + // The below logic must use AttrTypes/ElemType/ElemTypes directly, or the + // types package will return the unexported missingType type, which cannot + // be caught here. + switch attrType := typ.(type) { + case nil: + return true + case basetypes.ListType: + return ContainsMissingUnderlyingType(attrType.ElemType) + case basetypes.MapType: + return ContainsMissingUnderlyingType(attrType.ElemType) + case basetypes.ObjectType: + for _, objAttrType := range attrType.AttrTypes { + if ContainsMissingUnderlyingType(objAttrType) { + return true + } + } + + return false + case basetypes.SetType: + return ContainsMissingUnderlyingType(attrType.ElemType) + case basetypes.TupleType: + for _, elemType := range attrType.ElemTypes { + if ContainsMissingUnderlyingType(elemType) { + return true + } + } + + return false + // Everything else (primitives, custom types, etc.) + default: + return false + } +} + +func ParameterMissingUnderlyingTypeDiag(name string, position *int64) diag.Diagnostic { + if position == nil { + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Variadic parameter is missing underlying type.\n\n"+ + "Collection element and object attribute types are always required in Terraform.", + ) + } + + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Parameter %q at position %d is missing underlying type.\n\n", name, *position)+ + "Collection element and object attribute types are always required in Terraform.", + ) +} + +func ReturnMissingUnderlyingTypeDiag() diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Return is missing underlying type.\n\n"+ + "Collection element and object attribute types are always required in Terraform.", + ) +} diff --git a/fwtype/missing_underlying_type_validation_test.go b/fwtype/missing_underlying_type_validation_test.go new file mode 100644 index 0000000..bbed13e --- /dev/null +++ b/fwtype/missing_underlying_type_validation_test.go @@ -0,0 +1,2828 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype_test + +import ( + "terraform-plugin-framework-internal/fwtype" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/attr" + // "github.com/hashicorp/terraform-plugin-framework/internal/fwtype" + // "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestContainsMissingUnderlyingType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attrTyp attr.Type + expected bool + }{ + "nil": { + attrTyp: nil, + expected: true, + }, + "bool": { + attrTyp: types.BoolType, + expected: false, + }, + "custom-bool": { + attrTyp: testtypes.BoolType{}, + expected: false, + }, + "custom-dynamic": { + attrTyp: testtypes.DynamicType{}, + expected: false, + }, + "custom-float32": { + attrTyp: testtypes.Float32Type{}, + expected: false, + }, + "custom-float64": { + attrTyp: testtypes.Float64Type{}, + expected: false, + }, + "custom-int32": { + attrTyp: testtypes.Int32Type{}, + expected: false, + }, + "custom-int64": { + attrTyp: testtypes.Int64Type{}, + expected: false, + }, + "custom-list-nil": { + attrTyp: testtypes.ListType{}, + // While testtypes.ListType embeds basetypes.ListType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "custom-map-nil": { + attrTyp: testtypes.MapType{}, + // While testtypes.MapType embeds basetypes.MapType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "custom-object-nil": { + attrTyp: testtypes.ObjectType{}, + expected: false, // expected as objects can be empty + }, + "custom-number": { + attrTyp: testtypes.NumberType{}, + expected: false, + }, + "custom-set-nil": { + attrTyp: testtypes.SetType{}, + // While testtypes.SetType embeds basetypes.SetType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "custom-string": { + attrTyp: testtypes.StringType{}, + expected: false, + }, + "dynamic": { + attrTyp: types.DynamicType, + expected: false, + }, + "float32": { + attrTyp: types.Float32Type, + expected: false, + }, + "float64": { + attrTyp: types.Float64Type, + expected: false, + }, + "int32": { + attrTyp: types.Int32Type, + expected: false, + }, + "int64": { + attrTyp: types.Int64Type, + expected: false, + }, + "list-nil": { + attrTyp: types.ListType{}, + expected: true, + }, + "list-bool": { + attrTyp: types.ListType{ + ElemType: types.BoolType, + }, + expected: false, + }, + "list-custom-bool": { + attrTyp: types.ListType{ + ElemType: testtypes.BoolType{}, + }, + expected: false, + }, + "list-custom-dynamic": { + attrTyp: types.ListType{ + ElemType: testtypes.DynamicType{}, + }, + expected: false, + }, + "list-custom-float32": { + attrTyp: types.ListType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, + "list-custom-float64": { + attrTyp: types.ListType{ + ElemType: testtypes.Float64Type{}, + }, + expected: false, + }, + "list-custom-int32": { + attrTyp: types.ListType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, + "list-custom-int64": { + attrTyp: types.ListType{ + ElemType: testtypes.Int64Type{}, + }, + expected: false, + }, + "list-custom-list-nil": { + attrTyp: types.ListType{ + ElemType: testtypes.ListType{}, + }, + // While testtypes.ListType embeds basetypes.ListType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "list-custom-list-string": { + attrTyp: types.ListType{ + ElemType: testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "list-custom-map-nil": { + attrTyp: types.ListType{ + ElemType: testtypes.MapType{}, + }, + // While testtypes.MapType embeds basetypes.MapType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "list-custom-map-string": { + attrTyp: types.ListType{ + ElemType: testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "list-custom-number": { + attrTyp: types.ListType{ + ElemType: testtypes.NumberType{}, + }, + expected: false, + }, + "list-custom-object-nil": { + attrTyp: types.ListType{ + ElemType: testtypes.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "list-custom-object-attrtypes": { + attrTyp: types.ListType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + expected: false, + }, + "list-custom-object-attrtypes-nil": { + attrTyp: types.ListType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": nil, + }, + }, + }, + }, + // While testtypes.ObjectType embeds basetypes.ObjectType and this + // test case specifies a nil AttrTypes value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // AttributeTypes() method which would be used for custom types. + expected: false, + }, + "list-custom-set-nil": { + attrTyp: types.ListType{ + ElemType: testtypes.SetType{}, + }, + // While testtypes.SetType embeds basetypes.SetType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "list-custom-set-string": { + attrTyp: types.ListType{ + ElemType: testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "list-custom-string": { + attrTyp: types.ListType{ + ElemType: testtypes.StringType{}, + }, + expected: false, + }, + "list-dynamic": { + attrTyp: types.ListType{ + ElemType: types.DynamicType, + }, + expected: false, + }, + "list-float32": { + attrTyp: types.ListType{ + ElemType: types.Float32Type, + }, + expected: false, + }, + "list-float64": { + attrTyp: types.ListType{ + ElemType: types.Float64Type, + }, + expected: false, + }, + "list-int32": { + attrTyp: types.ListType{ + ElemType: types.Int32Type, + }, + expected: false, + }, + "list-int64": { + attrTyp: types.ListType{ + ElemType: types.Int64Type, + }, + expected: false, + }, + "list-list-nil": { + attrTyp: types.ListType{ + ElemType: types.ListType{}, + }, + expected: true, + }, + "list-list-string": { + attrTyp: types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "list-map-nil": { + attrTyp: types.ListType{ + ElemType: types.MapType{}, + }, + expected: true, + }, + "list-map-string": { + attrTyp: types.ListType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "list-object-nil": { + attrTyp: types.ListType{ + ElemType: types.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "list-object-attrtypes": { + attrTyp: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "list-object-attrtypes-nil": { + attrTyp: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + expected: true, + }, + "list-number": { + attrTyp: types.ListType{ + ElemType: types.NumberType, + }, + expected: false, + }, + "list-set-nil": { + attrTyp: types.ListType{ + ElemType: types.SetType{}, + }, + expected: true, + }, + "list-set-string": { + attrTyp: types.ListType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "list-string": { + attrTyp: types.ListType{ + ElemType: types.StringType, + }, + expected: false, + }, + "list-tuple-nil": { + attrTyp: types.ListType{ + ElemType: types.TupleType{}, + }, + expected: false, // expected as tuples can be empty + }, + "list-tuple-elemtypes": { + attrTyp: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + }, + }, + }, + expected: false, + }, + "list-tuple-elemtypes-nil": { + attrTyp: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + nil, + }, + }, + }, + expected: true, + }, + "map-nil": { + attrTyp: types.MapType{}, + expected: true, + }, + "map-bool": { + attrTyp: types.MapType{ + ElemType: types.BoolType, + }, + expected: false, + }, + "map-custom-bool": { + attrTyp: types.MapType{ + ElemType: testtypes.BoolType{}, + }, + expected: false, + }, + "map-custom-dynamic": { + attrTyp: types.MapType{ + ElemType: testtypes.DynamicType{}, + }, + expected: false, + }, + "map-custom-float32": { + attrTyp: types.MapType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, + "map-custom-float64": { + attrTyp: types.MapType{ + ElemType: testtypes.Float64Type{}, + }, + expected: false, + }, + "map-custom-int32": { + attrTyp: types.MapType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, + "map-custom-int64": { + attrTyp: types.MapType{ + ElemType: testtypes.Int64Type{}, + }, + expected: false, + }, + "map-custom-list-nil": { + attrTyp: types.MapType{ + ElemType: testtypes.ListType{}, + }, + // While testtypes.ListType embeds basetypes.ListType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "map-custom-list-string": { + attrTyp: types.MapType{ + ElemType: testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "map-custom-map-nil": { + attrTyp: types.MapType{ + ElemType: testtypes.MapType{}, + }, + // While testtypes.MapType embeds basetypes.MapType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "map-custom-map-string": { + attrTyp: types.MapType{ + ElemType: testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "map-custom-number": { + attrTyp: types.MapType{ + ElemType: testtypes.NumberType{}, + }, + expected: false, + }, + "map-custom-object-nil": { + attrTyp: types.MapType{ + ElemType: testtypes.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "map-custom-object-attrtypes": { + attrTyp: types.MapType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + expected: false, + }, + "map-custom-object-attrtypes-nil": { + attrTyp: types.MapType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": nil, + }, + }, + }, + }, + // While testtypes.ObjectType embeds basetypes.ObjectType and this + // test case specifies a nil AttrTypes value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // AttributeTypes() method which would be used for custom types. + expected: false, + }, + "map-custom-set-nil": { + attrTyp: types.MapType{ + ElemType: testtypes.SetType{}, + }, + // While testtypes.SetType embeds basetypes.SetType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "map-custom-set-string": { + attrTyp: types.MapType{ + ElemType: testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "map-custom-string": { + attrTyp: types.MapType{ + ElemType: testtypes.StringType{}, + }, + expected: false, + }, + "map-dynamic": { + attrTyp: types.MapType{ + ElemType: types.DynamicType, + }, + expected: false, + }, + "map-float32": { + attrTyp: types.MapType{ + ElemType: types.Float32Type, + }, + expected: false, + }, + "map-float64": { + attrTyp: types.MapType{ + ElemType: types.Float64Type, + }, + expected: false, + }, + "map-int32": { + attrTyp: types.MapType{ + ElemType: types.Int32Type, + }, + expected: false, + }, + "map-int64": { + attrTyp: types.MapType{ + ElemType: types.Int64Type, + }, + expected: false, + }, + "map-list-nil": { + attrTyp: types.MapType{ + ElemType: types.ListType{}, + }, + expected: true, + }, + "map-list-string": { + attrTyp: types.MapType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "map-map-nil": { + attrTyp: types.MapType{ + ElemType: types.MapType{}, + }, + expected: true, + }, + "map-map-string": { + attrTyp: types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "map-object-nil": { + attrTyp: types.MapType{ + ElemType: types.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "map-object-attrtypes": { + attrTyp: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "map-object-attrtypes-nil": { + attrTyp: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + expected: true, + }, + "map-number": { + attrTyp: types.MapType{ + ElemType: types.NumberType, + }, + expected: false, + }, + "map-set-nil": { + attrTyp: types.MapType{ + ElemType: types.SetType{}, + }, + expected: true, + }, + "map-set-string": { + attrTyp: types.MapType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "map-string": { + attrTyp: types.MapType{ + ElemType: types.StringType, + }, + expected: false, + }, + "map-tuple-nil": { + attrTyp: types.MapType{ + ElemType: types.TupleType{}, + }, + expected: false, // expected as tuples can be empty + }, + "map-tuple-elemtypes": { + attrTyp: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + }, + }, + }, + expected: false, + }, + "map-tuple-elemtypes-nil": { + attrTyp: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + nil, + }, + }, + }, + expected: true, + }, + "number": { + attrTyp: types.NumberType, + expected: false, + }, + "object-nil": { + attrTyp: types.ObjectType{}, + expected: false, // expected as objects can be empty + }, + "object-custom-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{}, + }, + }, + // While testtypes.ListType embeds basetypes.ListType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "object-custom-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.ListType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-list-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.MapType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-list-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.ObjectType{}, + }, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-custom-list-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-list-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.SetType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-list-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.TupleType{}, + }, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-custom-list-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-list-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{}, + }, + }, + // While testtypes.MapType embeds basetypes.MapType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "object-custom-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.ListType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-map-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.MapType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-map-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.ObjectType{}, + }, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-custom-map-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-map-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.SetType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-map-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.TupleType{}, + }, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-custom-map-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-map-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": testtypes.ObjectType{}, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-custom-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{}, + }, + }, + // While testtypes.SetType embeds basetypes.SetType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "object-custom-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.ListType{}, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.MapType{}, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-set-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.ObjectType{}, + }, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-custom-set-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-custom-set-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.SetType{}, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.TupleType{}, + }, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-custom-set-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-custom-set-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + }, + // Similar to other custom type test cases, the custom type will + // prevent the further checking of the element type. + expected: false, + }, + "object-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{}, + }, + }, + expected: true, + }, + "object-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "object-list-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ListType{}, + }, + }, + }, + expected: true, + }, + "object-list-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-list-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.MapType{}, + }, + }, + }, + expected: true, + }, + "object-list-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-list-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-list-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-list-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-list-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.SetType{}, + }, + }, + }, + expected: true, + }, + "object-list-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-list-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-list-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-list-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{}, + }, + }, + expected: true, + }, + "object-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "object-map-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ListType{}, + }, + }, + }, + expected: true, + }, + "object-map-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-map-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.MapType{}, + }, + }, + }, + expected: true, + }, + "object-map-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-map-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-map-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-map-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-map-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.SetType{}, + }, + }, + }, + expected: true, + }, + "object-map-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-map-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-map-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-map-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": types.ObjectType{}, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + expected: false, + }, + "object-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "object": types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + expected: true, + }, + "object-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{}, + }, + }, + expected: true, + }, + "object-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "object-set-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ListType{}, + }, + }, + }, + expected: true, + }, + "object-set-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-set-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.MapType{}, + }, + }, + }, + expected: true, + }, + "object-set-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-set-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-set-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-set-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-set-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.SetType{}, + }, + }, + }, + expected: true, + }, + "object-set-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "object-set-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-set-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "object-set-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + "object-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{}, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-tuple-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.StringType}, + }, + }, + }, + expected: false, + }, + "object-tuple-list-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.ListType{}}, + }, + }, + }, + expected: true, + }, + "object-tuple-list-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-tuple-map-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.MapType{}}, + }, + }, + }, + expected: true, + }, + "object-tuple-map-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-tuple-object-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.ObjectType{}}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "object-tuple-object-attrtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-tuple-object-attrtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "object-tuple-set-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.SetType{}}, + }, + }, + }, + expected: true, + }, + "object-tuple-set-string": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + }, + expected: false, + }, + "object-tuple-tuple-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{types.TupleType{}}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "object-tuple-tuple-elemtypes": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + }, + expected: false, + }, + "object-tuple-tuple-elemtypes-nil": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "tuple": types.TupleType{ + ElemTypes: []attr.Type{ + types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + }, + expected: true, + }, + "set-nil": { + attrTyp: types.SetType{}, + expected: true, + }, + "set-bool": { + attrTyp: types.SetType{ + ElemType: types.BoolType, + }, + expected: false, + }, + "set-custom-bool": { + attrTyp: types.SetType{ + ElemType: testtypes.BoolType{}, + }, + expected: false, + }, + "set-custom-dynamic": { + attrTyp: types.SetType{ + ElemType: testtypes.DynamicType{}, + }, + expected: false, + }, + "set-custom-float32": { + attrTyp: types.SetType{ + ElemType: testtypes.Float32Type{}, + }, + expected: false, + }, + "set-custom-float64": { + attrTyp: types.SetType{ + ElemType: testtypes.Float64Type{}, + }, + expected: false, + }, + "set-custom-int32": { + attrTyp: types.SetType{ + ElemType: testtypes.Int32Type{}, + }, + expected: false, + }, + "set-custom-int64": { + attrTyp: types.SetType{ + ElemType: testtypes.Int64Type{}, + }, + expected: false, + }, + "set-custom-list-nil": { + attrTyp: types.SetType{ + ElemType: testtypes.ListType{}, + }, + // While testtypes.ListType embeds basetypes.ListType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "set-custom-list-string": { + attrTyp: types.SetType{ + ElemType: testtypes.ListType{ + ListType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "set-custom-map-nil": { + attrTyp: types.SetType{ + ElemType: testtypes.MapType{}, + }, + // While testtypes.MapType embeds basetypes.MapType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "set-custom-map-string": { + attrTyp: types.SetType{ + ElemType: testtypes.MapType{ + MapType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "set-custom-number": { + attrTyp: types.SetType{ + ElemType: testtypes.NumberType{}, + }, + expected: false, + }, + "set-custom-object-nil": { + attrTyp: types.SetType{ + ElemType: testtypes.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "set-custom-object-attrtypes": { + attrTyp: types.SetType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + expected: false, + }, + "set-custom-object-attrtypes-nil": { + attrTyp: types.SetType{ + ElemType: testtypes.ObjectType{ + ObjectType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": nil, + }, + }, + }, + }, + // While testtypes.ObjectType embeds basetypes.ObjectType and this + // test case specifies a nil AttrTypes value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // AttributeTypes() method which would be used for custom types. + expected: false, + }, + "set-custom-set-nil": { + attrTyp: types.SetType{ + ElemType: testtypes.SetType{}, + }, + // While testtypes.SetType embeds basetypes.SetType and this test + // case does not specify an ElemType value, the function logic is + // coded to only handle basetypes implementations due to the + // unexported missingType that would be returned from the + // ElementType() method which would be used for custom types. + expected: false, + }, + "set-custom-set-string": { + attrTyp: types.SetType{ + ElemType: testtypes.SetType{ + SetType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "set-custom-string": { + attrTyp: types.SetType{ + ElemType: testtypes.StringType{}, + }, + expected: false, + }, + "set-dynamic": { + attrTyp: types.SetType{ + ElemType: types.DynamicType, + }, + expected: false, + }, + "set-float32": { + attrTyp: types.SetType{ + ElemType: types.Float32Type, + }, + expected: false, + }, + "set-float64": { + attrTyp: types.SetType{ + ElemType: types.Float64Type, + }, + expected: false, + }, + "set-int32": { + attrTyp: types.SetType{ + ElemType: types.Int32Type, + }, + expected: false, + }, + "set-int64": { + attrTyp: types.SetType{ + ElemType: types.Int64Type, + }, + expected: false, + }, + "set-list-nil": { + attrTyp: types.SetType{ + ElemType: types.ListType{}, + }, + expected: true, + }, + "set-list-string": { + attrTyp: types.SetType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "set-map-nil": { + attrTyp: types.SetType{ + ElemType: types.MapType{}, + }, + expected: true, + }, + "set-map-string": { + attrTyp: types.SetType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "set-object-nil": { + attrTyp: types.SetType{ + ElemType: types.ObjectType{}, + }, + expected: false, // expected as objects can be empty + }, + "set-object-attrtypes": { + attrTyp: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "set-object-attrtypes-nil": { + attrTyp: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + expected: true, + }, + "set-number": { + attrTyp: types.SetType{ + ElemType: types.NumberType, + }, + expected: false, + }, + "set-set-nil": { + attrTyp: types.SetType{ + ElemType: types.SetType{}, + }, + expected: true, + }, + "set-set-string": { + attrTyp: types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "set-string": { + attrTyp: types.SetType{ + ElemType: types.StringType, + }, + expected: false, + }, + "set-tuple-nil": { + attrTyp: types.SetType{ + ElemType: types.TupleType{}, + }, + expected: false, // expected as tuples can be empty + }, + "set-tuple-elemtypes": { + attrTyp: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + }, + }, + }, + expected: false, + }, + "set-tuple-elemtypes-nil": { + attrTyp: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.StringType, + nil, + }, + }, + }, + expected: true, + }, + "string": { + attrTyp: types.StringType, + expected: false, + }, + "tuple-nil": { + attrTyp: types.TupleType{}, + expected: false, // expected as tuples can be empty + }, + "tuple-bool": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.BoolType}, + }, + expected: false, + }, + "tuple-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.DynamicType}, + }, + expected: false, + }, + "tuple-float32": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Float32Type}, + }, + expected: false, + }, + "tuple-float64": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Float64Type}, + }, + expected: false, + }, + "tuple-int32": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Int32Type}, + }, + expected: false, + }, + "tuple-int64": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.Int64Type}, + }, + expected: false, + }, + "tuple-list-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{}, + }, + }, + expected: true, + }, + "tuple-list-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-list-list-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ListType{}, + }, + }, + }, + expected: true, + }, + "tuple-list-list-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-object-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "tuple-list-object-attrtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-object-attrtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-list-tuple-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "tuple-list-tuple-elemtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-tuple-elemtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-map-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{}, + }, + }, + expected: true, + }, + "tuple-map-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-map-map-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.MapType{}, + }, + }, + }, + expected: true, + }, + "tuple-map-map-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-object-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "tuple-map-object-attrtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-object-attrtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-map-tuple-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "tuple-map-tuple-elemtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-tuple-elemtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-number": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{types.NumberType}, + }, + expected: false, + }, + "tuple-object-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ObjectType{}, + }, + }, + expected: false, // expected as objects can be empty + }, + "tuple-object-attrtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + expected: false, + }, + "tuple-object-attrtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + expected: true, + }, + "tuple-set-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{}, + }, + }, + expected: true, + }, + "tuple-set-object-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.ObjectType{}, + }, + }, + }, + expected: false, // expected as objects can be empty + }, + "tuple-set-object-attrtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-object-attrtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "nil": nil, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-set-set-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.SetType{}, + }, + }, + }, + expected: true, + }, + "tuple-set-set-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-string": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-set-tuple-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.TupleType{}, + }, + }, + }, + expected: false, // expected as tuples can be empty + }, + "tuple-set-tuple-elemtypes": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-tuple-elemtypes-nil": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + nil, + }, + }, + }, + }, + }, + expected: true, + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fwtype.ContainsMissingUnderlyingType(testCase.attrTyp) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/fwtype/static_collection_validation.go b/fwtype/static_collection_validation.go new file mode 100644 index 0000000..2a91e2f --- /dev/null +++ b/fwtype/static_collection_validation.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// ContainsCollectionWithDynamic will return true if an attr.Type is a complex type that either is or contains any +// collection types with dynamic types, which are not supported by the framework type system. Primitives, invalid +// types (missingType), or nil will return false. +// +// Unsupported collection types include: +// - Lists that contain a dynamic type +// - Maps that contain a dynamic type +// - Sets that contain a dynamic type +func ContainsCollectionWithDynamic(typ attr.Type) bool { + switch attrType := typ.(type) { + // We haven't run into a collection type yet, so it's valid for this to be a dynamic type + case basetypes.DynamicTypable: + return false + // Lists, maps, sets + case attr.TypeWithElementType: + // We found a collection, need to ensure there are no dynamics from this point on. + return containsDynamic(attrType.ElementType()) + // Tuples + case attr.TypeWithElementTypes: + for _, elemType := range attrType.ElementTypes() { + hasDynamic := ContainsCollectionWithDynamic(elemType) + if hasDynamic { + return true + } + } + return false + // Objects + case attr.TypeWithAttributeTypes: + for _, objAttrType := range attrType.AttributeTypes() { + hasDynamic := ContainsCollectionWithDynamic(objAttrType) + if hasDynamic { + return true + } + } + return false + // Primitives, missing types, etc. + default: + return false + } +} + +// containsDynamic will return true if `typ` is a dynamic type or has any nested types that contain a dynamic type. +func containsDynamic(typ attr.Type) bool { + switch attrType := typ.(type) { + // Found a dynamic! + case basetypes.DynamicTypable: + return true + // Lists, maps, sets + case attr.TypeWithElementType: + return containsDynamic(attrType.ElementType()) + // Tuples + case attr.TypeWithElementTypes: + for _, elemType := range attrType.ElementTypes() { + hasDynamic := containsDynamic(elemType) + if hasDynamic { + return true + } + } + return false + // Objects + case attr.TypeWithAttributeTypes: + for _, objAttrType := range attrType.AttributeTypes() { + hasDynamic := containsDynamic(objAttrType) + if hasDynamic { + return true + } + } + return false + // Primitives, missing types, etc. + default: + return false + } +} + +func AttributeCollectionWithDynamicTypeDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is an attribute that contains a collection type with a nested dynamic type.\n\n", attributePath)+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + fmt.Sprintf("If underlying dynamic values are required, replace the %q attribute definition with DynamicAttribute instead.", attributePath), + ) +} + +func BlockCollectionWithDynamicTypeDiag(attributePath path.Path) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Schema Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("%q is a block that contains a collection type with a nested dynamic type.\n\n", attributePath)+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + fmt.Sprintf("If underlying dynamic values are required, replace the %q block definition with a DynamicAttribute.", attributePath), + ) +} + +func ParameterCollectionWithDynamicTypeDiag(argument int64, name string) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Parameter %q at position %d contains a collection type with a nested dynamic type.\n\n", name, argument)+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + fmt.Sprintf("If underlying dynamic values are required, replace the %q parameter definition with DynamicParameter instead.", name), + ) +} + +func VariadicParameterCollectionWithDynamicTypeDiag(name string) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + fmt.Sprintf("Variadic parameter %q contains a collection type with a nested dynamic type.\n\n", name)+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the variadic parameter definition with DynamicParameter instead.", + ) +} + +func ReturnCollectionWithDynamicTypeDiag() diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Invalid Function Definition", + "When validating the function definition, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "Return contains a collection type with a nested dynamic type.\n\n"+ + "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + "If underlying dynamic values are required, replace the return definition with DynamicReturn instead.", + ) +} diff --git a/fwtype/static_collection_validation_test.go b/fwtype/static_collection_validation_test.go new file mode 100644 index 0000000..b8bbd62 --- /dev/null +++ b/fwtype/static_collection_validation_test.go @@ -0,0 +1,947 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwtype_test + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework-internal/fwtype" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestTypeContainsCollectionWithDynamic(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attrTyp attr.Type + expected bool + }{ + "nil": { + attrTyp: nil, + expected: false, + }, + "dynamic": { + attrTyp: types.DynamicType, + expected: false, + }, + "primitive": { + attrTyp: types.StringType, + expected: false, + }, + "list-missing": { + attrTyp: types.ListType{}, + expected: false, + }, + "list-static": { + attrTyp: types.ListType{ + ElemType: types.StringType, + }, + expected: false, + }, + "list-list-static": { + attrTyp: types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "list-obj-static": { + attrTyp: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "list-tuple-static": { + attrTyp: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + expected: false, + }, + "list-dynamic": { + attrTyp: types.ListType{ + ElemType: types.DynamicType, + }, + expected: true, + }, + "list-list-dynamic": { + attrTyp: types.ListType{ + ElemType: types.ListType{ + ElemType: types.DynamicType, + }, + }, + expected: true, + }, + "list-obj-dynamic": { + attrTyp: types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + expected: true, + }, + "list-tuple-dynamic": { + attrTyp: types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + expected: true, + }, + "map-missing": { + attrTyp: types.MapType{}, + expected: false, + }, + "map-static": { + attrTyp: types.MapType{ + ElemType: types.StringType, + }, + expected: false, + }, + "map-map-static": { + attrTyp: types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "map-obj-static": { + attrTyp: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "map-tuple-static": { + attrTyp: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + expected: false, + }, + "map-dynamic": { + attrTyp: types.MapType{ + ElemType: types.DynamicType, + }, + expected: true, + }, + "map-map-dynamic": { + attrTyp: types.MapType{ + ElemType: types.MapType{ + ElemType: types.DynamicType, + }, + }, + expected: true, + }, + "map-obj-dynamic": { + attrTyp: types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + expected: true, + }, + "map-tuple-dynamic": { + attrTyp: types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + expected: true, + }, + "obj-list-missing": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{}, + }, + }, + expected: false, + }, + "obj-list-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "obj-list-list-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "obj-list-obj-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-list-tuple-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-list-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "obj-list-list-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ListType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "obj-list-obj-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "obj-list-tuple-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "list": types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "obj-map-missing": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{}, + }, + }, + expected: false, + }, + "obj-map-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "obj-map-map-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "obj-map-obj-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-map-tuple-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-map-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "obj-map-map-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.MapType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "obj-map-obj-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "obj-map-tuple-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "map": types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "obj-set-missing": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{}, + }, + }, + expected: false, + }, + "obj-set-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "obj-set-set-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "obj-set-obj-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-set-tuple-static": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "obj-set-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "obj-set-set-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.SetType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "obj-set-obj-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "obj-set-tuple-dynamic": { + attrTyp: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "set": types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-list-missing": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{}, + }, + }, + expected: false, + }, + "tuple-list-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-list-list-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-obj-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-tuple-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-list-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "tuple-list-list-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ListType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "tuple-list-obj-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-list-tuple-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.ListType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-map-missing": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{}, + }, + }, + expected: false, + }, + "tuple-map-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-map-map-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.MapType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-obj-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-tuple-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-map-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "tuple-map-map-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.MapType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "tuple-map-obj-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-map-tuple-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.MapType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-set-missing": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{}, + }, + }, + expected: false, + }, + "tuple-set-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.StringType, + }, + }, + }, + expected: false, + }, + "tuple-set-set-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-obj-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-tuple-static": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + }, + }, + expected: false, + }, + "tuple-set-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.DynamicType, + }, + }, + }, + expected: true, + }, + "tuple-set-set-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.SetType{ + ElemType: types.DynamicType, + }, + }, + }, + }, + expected: true, + }, + "tuple-set-obj-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "tuple-set-tuple-dynamic": { + attrTyp: types.TupleType{ + ElemTypes: []attr.Type{ + types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + }, + }, + expected: true, + }, + "set-missing": { + attrTyp: types.SetType{}, + expected: false, + }, + "set-static": { + attrTyp: types.SetType{ + ElemType: types.StringType, + }, + expected: false, + }, + "set-set-static": { + attrTyp: types.SetType{ + ElemType: types.SetType{ + ElemType: types.StringType, + }, + }, + expected: false, + }, + "set-obj-static": { + attrTyp: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "float64": types.Float64Type, + }, + }, + }, + expected: false, + }, + "set-tuple-static": { + attrTyp: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.Float64Type, + }, + }, + }, + expected: false, + }, + "set-dynamic": { + attrTyp: types.SetType{ + ElemType: types.DynamicType, + }, + expected: true, + }, + "set-set-dynamic": { + attrTyp: types.SetType{ + ElemType: types.SetType{ + ElemType: types.DynamicType, + }, + }, + expected: true, + }, + "set-obj-dynamic": { + attrTyp: types.SetType{ + ElemType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "dynamic": types.DynamicType, + }, + }, + }, + expected: true, + }, + "set-tuple-dynamic": { + attrTyp: types.SetType{ + ElemType: types.TupleType{ + ElemTypes: []attr.Type{ + types.BoolType, + types.DynamicType, + }, + }, + }, + expected: true, + }, + } + for name, testCase := range testCases { + name, testCase := name, testCase + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fwtype.ContainsCollectionWithDynamic(testCase.attrTyp) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..67159b9 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module terraform-plugin-framework-internal + +go 1.22.7 + +require ( + github.com/google/go-cmp v0.6.0 + github.com/hashicorp/terraform-plugin-framework v1.12.0 +) + +require ( + github.com/fatih/color v1.13.0 // indirect + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.24.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.21.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3521a4c --- /dev/null +++ b/go.sum @@ -0,0 +1,42 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ= +github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE= +github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U= +github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=