├── .devcontainer ├── Dockerfile ├── devcontainer.json └── library-scripts │ ├── README.md │ └── common-debian.sh ├── .github ├── FUNDING.yml └── workflows │ └── dart.yml ├── .gitignore ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── deep_pick_example.dart ├── lib ├── deep_pick.dart └── src │ ├── pick.dart │ ├── pick_bool.dart │ ├── pick_datetime.dart │ ├── pick_double.dart │ ├── pick_int.dart │ ├── pick_let.dart │ ├── pick_list.dart │ ├── pick_map.dart │ └── pick_string.dart ├── pubspec.yaml ├── test ├── all_tests.dart └── src │ ├── pick_bool_test.dart │ ├── pick_datetime_test.dart │ ├── pick_double_test.dart │ ├── pick_int_test.dart │ ├── pick_let_test.dart │ ├── pick_list_test.dart │ ├── pick_map_test.dart │ ├── pick_string_test.dart │ ├── pick_test.dart │ └── required_pick_test.dart └── tool ├── reformat.dart └── run_coverage_locally.sh /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Update VARIANT in devcontainer.json to pick a Dart version 2 | ARG VARIANT=2 3 | FROM google/dart:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="false" 9 | 10 | # Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=vscode 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | COPY library-scripts/*.sh /tmp/library-scripts/ 15 | RUN apt-get update \ 16 | && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \ 17 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts 18 | 19 | # Add bin location to path 20 | ENV PUB_CACHE="/usr/local/share/pub-cache" 21 | ENV PATH="${PATH}:${PUB_CACHE}/bin" 22 | RUN mkdir -p ${PUB_CACHE} \ 23 | && chown ${USERNAME}:root ${PUB_CACHE} \ 24 | && echo "if [ \"\$(stat -c '%U' ${PUB_CACHE})\" != \"${USERNAME}\" ]; then sudo chown -R ${USER_UID}:root ${PUB_CACHE}; fi" \ 25 | | tee -a /root/.bashrc /root/.zshrc /home/${USERNAME}/.bashrc >> /home/${USERNAME}/.zshrc 26 | 27 | # [Optional] Uncomment this section to install additional OS packages. 28 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 29 | # && apt-get -y install --no-install-recommends -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dart", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | // Update VARIANT to pick a Dart version 6 | "args": { "VARIANT": "2.9" } 7 | }, 8 | 9 | // Set *default* container specific settings.json values on container create. 10 | "settings": { 11 | "terminal.integrated.shell.linux": "/bin/bash" 12 | }, 13 | 14 | // Add the IDs of extensions you want installed when the container is created. 15 | "extensions": [ 16 | "dart-code.dart-code" 17 | ], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | "postCreateCommand": "pub get", 24 | 25 | // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. 26 | // "remoteUser": "vscode" 27 | } 28 | -------------------------------------------------------------------------------- /.devcontainer/library-scripts/README.md: -------------------------------------------------------------------------------- 1 | # Warning: Folder contents may be replaced 2 | 3 | The contents of this folder will be automatically replaced with a file of the same name in the [vscode-dev-containers](https://github.com/microsoft/vscode-dev-containers) repository's [script-library folder](https://github.com/microsoft/vscode-dev-containers/tree/master/script-library) whenever the repository is packaged. 4 | 5 | To retain your edits, move the file to a different location. You may also delete the files if they are not needed. -------------------------------------------------------------------------------- /.devcontainer/library-scripts/common-debian.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #------------------------------------------------------------------------------------------------------------- 3 | # Copyright (c) Microsoft Corporation. All rights reserved. 4 | # Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. 5 | #------------------------------------------------------------------------------------------------------------- 6 | # 7 | # Docs: https://github.com/microsoft/vscode-dev-containers/blob/master/script-library/docs/common.md 8 | # 9 | # Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My *! flag] 10 | 11 | INSTALL_ZSH=${1:-"true"} 12 | USERNAME=${2:-"automatic"} 13 | USER_UID=${3:-"automatic"} 14 | USER_GID=${4:-"automatic"} 15 | UPGRADE_PACKAGES=${5:-"true"} 16 | INSTALL_OH_MYS=${6:-"true"} 17 | 18 | set -e 19 | 20 | if [ "$(id -u)" -ne 0 ]; then 21 | echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' 22 | exit 1 23 | fi 24 | 25 | # If in automatic mode, determine if a user already exists, if not use vscode 26 | if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then 27 | USERNAME="" 28 | POSSIBLE_USERS=("vscode", "node", "codespace", "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") 29 | for CURRENT_USER in ${POSSIBLE_USERS[@]}; do 30 | if id -u ${CURRENT_USER} > /dev/null 2>&1; then 31 | USERNAME=${CURRENT_USER} 32 | break 33 | fi 34 | done 35 | if [ "${USERNAME}" = "" ]; then 36 | USERNAME=vscode 37 | fi 38 | elif [ "${USERNAME}" = "none" ]; then 39 | USERNAME=root 40 | USER_UID=0 41 | USER_GID=0 42 | fi 43 | 44 | # Load markers to see which steps have already run 45 | MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" 46 | if [ -f "${MARKER_FILE}" ]; then 47 | echo "Marker file found:" 48 | cat "${MARKER_FILE}" 49 | source "${MARKER_FILE}" 50 | fi 51 | 52 | # Ensure apt is in non-interactive to avoid prompts 53 | export DEBIAN_FRONTEND=noninteractive 54 | 55 | # Function to call apt-get if needed 56 | apt-get-update-if-needed() 57 | { 58 | if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then 59 | echo "Running apt-get update..." 60 | apt-get update 61 | else 62 | echo "Skipping apt-get update." 63 | fi 64 | } 65 | 66 | # Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies 67 | if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then 68 | apt-get-update-if-needed 69 | 70 | PACKAGE_LIST="apt-utils \ 71 | git \ 72 | openssh-client \ 73 | gnupg2 \ 74 | iproute2 \ 75 | procps \ 76 | lsof \ 77 | htop \ 78 | net-tools \ 79 | psmisc \ 80 | curl \ 81 | wget \ 82 | rsync \ 83 | ca-certificates \ 84 | unzip \ 85 | zip \ 86 | nano \ 87 | vim-tiny \ 88 | less \ 89 | jq \ 90 | lsb-release \ 91 | apt-transport-https \ 92 | dialog \ 93 | libc6 \ 94 | libgcc1 \ 95 | libgssapi-krb5-2 \ 96 | libicu[0-9][0-9] \ 97 | liblttng-ust0 \ 98 | libstdc++6 \ 99 | zlib1g \ 100 | locales \ 101 | sudo \ 102 | ncdu \ 103 | man-db" 104 | 105 | # Install libssl1.1 if available 106 | if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then 107 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.1" 108 | fi 109 | 110 | # Install appropriate version of libssl1.0.x if available 111 | LIBSSL=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') 112 | if [ "$(echo "$LIBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then 113 | if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then 114 | # Debian 9 115 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.2" 116 | elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then 117 | # Ubuntu 18.04, 16.04, earlier 118 | PACKAGE_LIST="${PACKAGE_LIST} libssl1.0.0" 119 | fi 120 | fi 121 | 122 | echo "Packages to verify are installed: ${PACKAGE_LIST}" 123 | apt-get -y install --no-install-recommends ${PACKAGE_LIST} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) 124 | 125 | PACKAGES_ALREADY_INSTALLED="true" 126 | fi 127 | 128 | # Get to latest versions of all packages 129 | if [ "${UPGRADE_PACKAGES}" = "true" ]; then 130 | apt-get-update-if-needed 131 | apt-get -y upgrade --no-install-recommends 132 | apt-get autoremove -y 133 | fi 134 | 135 | # Ensure at least the en_US.UTF-8 UTF-8 locale is available. 136 | # Common need for both applications and things like the agnoster ZSH theme. 137 | if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then 138 | echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen 139 | locale-gen 140 | LOCALE_ALREADY_SET="true" 141 | fi 142 | 143 | # Create or update a non-root user to match UID/GID. 144 | if id -u ${USERNAME} > /dev/null 2>&1; then 145 | # User exists, update if needed 146 | if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then 147 | groupmod --gid $USER_GID $USERNAME 148 | usermod --gid $USER_GID $USERNAME 149 | fi 150 | if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then 151 | usermod --uid $USER_UID $USERNAME 152 | fi 153 | else 154 | # Create user 155 | if [ "${USER_GID}" = "automatic" ]; then 156 | groupadd $USERNAME 157 | else 158 | groupadd --gid $USER_GID $USERNAME 159 | fi 160 | if [ "${USER_UID}" = "automatic" ]; then 161 | useradd -s /bin/bash --gid $USERNAME -m $USERNAME 162 | else 163 | useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME 164 | fi 165 | fi 166 | 167 | # Add add sudo support for non-root user 168 | if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then 169 | echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME 170 | chmod 0440 /etc/sudoers.d/$USERNAME 171 | EXISTING_NON_ROOT_USER="${USERNAME}" 172 | fi 173 | 174 | # ** Shell customization section ** 175 | if [ "${USERNAME}" = "root" ]; then 176 | USER_RC_PATH="/root" 177 | else 178 | USER_RC_PATH="/home/${USERNAME}" 179 | fi 180 | 181 | # .bashrc/.zshrc snippet 182 | RC_SNIPPET="$(cat << EOF 183 | export USER=\$(whoami) 184 | 185 | export PATH=\$PATH:\$HOME/.local/bin 186 | 187 | if type code-insiders > /dev/null 2>&1 && ! type code > /dev/null 2>&1; then 188 | alias code=code-insiders 189 | fi 190 | EOF 191 | )" 192 | 193 | # Codespaces themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme 194 | CODESPACES_BASH="$(cat \ 195 | <&1 261 | echo -e "$(cat "${TEMPLATE}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${USER_RC_FILE} 262 | if [ "${OH_MY}" = "bash" ]; then 263 | sed -i -e 's/OSH_THEME=.*/OSH_THEME="codespaces"/g' ${USER_RC_FILE} 264 | mkdir -p ${OH_MY_INSTALL_DIR}/custom/themes/codespaces 265 | echo "${CODESPACES_BASH}" > ${OH_MY_INSTALL_DIR}/custom/themes/codespaces/codespaces.theme.sh 266 | else 267 | sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${USER_RC_FILE} 268 | mkdir -p ${OH_MY_INSTALL_DIR}/custom/themes 269 | echo "${CODESPACES_ZSH}" > ${OH_MY_INSTALL_DIR}/custom/themes/codespaces.zsh-theme 270 | fi 271 | # Shrink git while still enabling updates 272 | cd ${OH_MY_INSTALL_DIR} 273 | git repack -a -d -f --depth=1 --window=1 274 | 275 | if [ "${USERNAME}" != "root" ]; then 276 | cp -rf ${USER_RC_FILE} ${OH_MY_INSTALL_DIR} /root 277 | chown -R ${USERNAME}:${USERNAME} ${USER_RC_PATH} 278 | fi 279 | } 280 | 281 | if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then 282 | echo "${RC_SNIPPET}" >> /etc/bash.bashrc 283 | RC_SNIPPET_ALREADY_ADDED="true" 284 | fi 285 | install-oh-my bash bashrc.osh-template https://github.com/ohmybash/oh-my-bash 286 | 287 | # Optionally install and configure zsh and Oh My Zsh! 288 | if [ "${INSTALL_ZSH}" = "true" ]; then 289 | if ! type zsh > /dev/null 2>&1; then 290 | apt-get-update-if-needed 291 | apt-get install -y zsh 292 | fi 293 | if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then 294 | echo "${RC_SNIPPET}" >> /etc/zsh/zshrc 295 | ZSH_ALREADY_INSTALLED="true" 296 | fi 297 | install-oh-my zsh zshrc.zsh-template https://github.com/ohmyzsh/ohmyzsh 298 | fi 299 | 300 | # Write marker file 301 | mkdir -p "$(dirname "${MARKER_FILE}")" 302 | echo -e "\ 303 | PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ 304 | LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ 305 | EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ 306 | RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ 307 | ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" 308 | 309 | echo "Done!" 310 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [passsy] 2 | -------------------------------------------------------------------------------- /.github/workflows/dart.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | dartversion: [ 14 | "google/dart:2.12", 15 | "dart:3.0", 16 | "dart:stable" 17 | ] 18 | 19 | container: 20 | image: ${{ matrix.dartversion }} 21 | 22 | steps: 23 | - uses: actions/checkout@v1 24 | - name: Install dependencies 25 | run: dart pub get || pub get 26 | - name: Run tests 27 | run: dart pub run test || pub run test 28 | 29 | formatting: 30 | runs-on: ubuntu-latest 31 | 32 | container: 33 | image: dart:3.5 34 | 35 | steps: 36 | - uses: actions/checkout@v1 37 | - name: Install dependencies 38 | run: dart pub get 39 | - name: check formatting 40 | run: dart format --fix --set-exit-if-changed . 41 | 42 | lint: 43 | runs-on: ubuntu-latest 44 | 45 | container: 46 | image: dart:3.5 47 | 48 | steps: 49 | - uses: actions/checkout@v1 50 | - name: Install dependencies 51 | run: dart pub get 52 | - name: lint 53 | run: dart analyze --fatal-infos 54 | - name: docs 55 | run: dart doc . 56 | - name: Verify package completeness 57 | run: dart pub publish -n 58 | 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files and directories created by pub 2 | .dart_tool/ 3 | .packages 4 | # Remove the following pattern if you wish to check in your lock file 5 | pubspec.lock 6 | 7 | # Conventional directory for build outputs 8 | build/ 9 | 10 | # Directory created by dartdoc 11 | doc/api/ 12 | .idea 13 | coverage 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Unit Tests", 9 | "type": "dart", 10 | "request": "launch", 11 | "program": "test", 12 | }, 13 | { 14 | "name": "All Unit Tests (fast)", 15 | "type": "dart", 16 | "request": "launch", 17 | "program": "test/all_tests.dart", 18 | "internalConsoleOptions": "openOnSessionStart", 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 (`30.08.24`) 4 | 5 | - Allow `.letOrNull((pick) => null)` to return `null` without manually setting a nullable type [#61](https://github.com/passsy/deep_pick/pull/61) 6 | - CI 2024 update 7 | 8 | ## 1.0.0 (`19.03.23`) 9 | 10 | - Remove long deprecated methods: `addContext`, `asBool`, `asDateTime`, `asDouble`, `asInt`, `asList`, `asMap`, `asString`. Use their `as*OrThrow` replacements. 11 | - Add support for timezones in `asDateTime*` methods [#47](https://github.com/passsy/deep_pick/pull/47), [#51](https://github.com/passsy/deep_pick/pull/51) 12 | - Add official support for date formats `RFC 3339`, `RFC 2822` and `RFC 1036` 13 | - Push test coverage to 100% 🤘 14 | 15 | 16 | ## 0.10.0 (`01.10.21`) 17 | 18 | - New: Support for more date formats. `asDateTime*` received an optional `format` parameter. By default, all possible formats will be parsed. To the existing `ISO 8601` format, `RFC 1123`, `RFC 850` and `asctime` have been added which are typically used for the HTTP header or cookies. 19 | - Documentation added for `asDouble` and `asMap` 20 | 21 | ## 0.9.0 (`02.08.21`) 22 | 23 | - New: `pickFromJson(json, args...)` allows parsing of a json String, without manually calling `jsonDecode` [#41](https://github.com/passsy/deep_pick/pull/41) 24 | - New: `pickDeep(json, 'some.key.inside.the.object'.split('.'))` allows picking with a dynamic depth [#40](https://github.com/passsy/deep_pick/pull/40) 25 | - Add `Pick.index` to get the element index for list items [#38](https://github.com/passsy/deep_pick/pull/38) 26 | 27 | ```dart 28 | pick(["John", "Paul", "George", "Ringo"]).asListOrThrow((pick) { 29 | final index = pick.index!; 30 | return Artist(id: index, name: pick.asStringOrThrow()); 31 | ); 32 | ``` 33 | 34 | - `Pick.asIntOrThrow()` now allows parsing of doubles when one of the new `roundDouble` or `truncateDouble` parameters is `true` [#37](https://github.com/passsy/deep_pick/pull/37). Thx @stevendz 35 | - Add dartdoc to `asList*()` extensions 36 | 37 | ## 0.8.0 (and 0.6.10 for Dart <2.12) (`12.02.21`) 38 | 39 | - Deprecated parsing extensions of `RequiredPick` to acknowledge that all parsers eventually causes errors. 40 | From now on, always use `.asIntOrThrow()` instead of `.required().asInt()`. Only exception is `.required().toString()`. 41 | Read more in [#34](https://github.com/passsy/deep_pick/pull/34) 42 | - Replace `dynamic` with `Object` where possible 43 | - Rename `Pick.location()` to `Pick.debugParsingExit` 44 | - Removal of `PickLocaiton` and `PickContext` mixins. They are now part of `Pick` 45 | - `RequiredPick` now extends `Pick` making it easier to write parsers for custom types 46 | 47 | ## 0.7.0 48 | 49 | - Enable nullsafety (requires Dart >=2.12) 50 | 51 | ## 0.6.10 52 | 53 | Backports 0.8.0 to pre-nullsafety 54 | 55 | ## 0.6.0 56 | 57 | ### API changes 58 | 59 | - Remove long deprecated `parseJsonTo*` methods. Use the `pick(json, args*)` api 60 | - New `asXyzOrThrow()` methods as shorthand for `.required().asXyz()` featuring better error messages 61 | - `asBoolOrThrow()` 62 | - `asDateTimeOrThrow()` 63 | - `asDoubleOrThrow()` 64 | - `asIntOrThrow()` 65 | - `letOrThrow()` 66 | - `asListOrThrow()` 67 | - `asMapOrThrow()` 68 | - `asStringOrThrow()` 69 | - New `Pick.isAbsent` getter to check if a value is absent or `null` [#24](https://github.com/passsy/deep_pick/pull/24). Absent could mean 70 | 1. Accessing a key which doesn't exist in a `Map` 71 | 2. Reading the value from `List` when the index is greater than the length 72 | 3. Trying to access a key in a `Map` but the found data a `Object` which isn't a Map 73 | - New `RequiredPick.nullable()` converting a `RequiredPick` back to a `Pick` with potential `null` value 74 | - New `PickLocation.followablePath`. While `PickLocation.path` contains the full path to the value, `followablePath` contains only the part which could be followed with a non-nullable value 75 | 76 | - **Breaking** `asList*()` now *requires* the mapping function. 77 | - **Breaking** `asList*()` now ignores `null` values when parsing. The map function now receives a `RequiredPick` as fist parameter instead of a `Pick` with a potential null value, making parsing easier. 78 | 79 | Therefore `pick().required().asList((RequiredPick pick) => /*...*/)` only maps non-nullable values. When your lists contain `null` it will be ignored. 80 | This is fine in most cases and simplifies the map function. 81 | 82 | In rare cases, where your lists contain `null` values with meaning, use the second parameter `whenNull` to map those null values `.asList((pick) => Person.fromPick(pick), whenNull: (Pick it) => null)`. The function still receives a `Pick` which gives access to the `context` api or the `PickLocation`. But the `Pick` never holds any value. 83 | 84 | ### Parsing changes 85 | 86 | - The String `"true"` and `"false"` are now parsed as boolean 87 | - **Breaking** Don't parse doubles as int because the is no rounding method which satisfies all [#31](https://github.com/passsy/deep_pick/pull/31) 88 | - **Breaking** Allow parsing of "german" doubles with `,` as decimal separator [#30](https://github.com/passsy/deep_pick/pull/30) 89 | 90 | - Improve error messages with more details where parsing stopped 91 | 92 | ## 0.6.0-nullsafety.2 93 | 94 | - **Breaking** `asList*()` methods now ignore `null` values. The map function now receives a `RequiredPick` as fist parameter instead of a `Pick` making parsing easier. 95 | 96 | Therefore `pick().required().asList((RequiredPick pick) => /*...*/)` only maps non-nullable values. When your lists contain `null` it will be ignored. 97 | This is fine in most cases and simplifies the map function. 98 | 99 | In rare cases, where your lists contain `null` values with meaning, use the second parameter `whenNull` to map those null values `.asList((pick) => Person.fromPick(pick), whenNull: (Pick it) => null)`. The function still receives a `Pick` which gives access to the `context` api or the `PickLocation`. But the `Pick` never holds any value. 100 | 101 | - **Breaking** Don't parse doubles as int because the is no rounding method which satisfies all [#31](https://github.com/passsy/deep_pick/pull/31) 102 | - **Breaking** Allow parsing of "german" doubles with `,` as decimal separator [#30](https://github.com/passsy/deep_pick/pull/30) 103 | - Improve error messages with more details where parsing stopped 104 | - New `RequiredPick.nullable()` converting a `RequiredPick` back to a `Pick` with potential `null` value 105 | - New `PickLocation.followablePath`. While `PickLocation.path` contains the full path to the value, `followablePath` contains only the part which could be followed with a non-nullable value 106 | 107 | ## 0.6.0-nullsafety.1 108 | 109 | - New `asXyzOrThrow()` methods as shorthand for `.required().asXyz()` featuring better error messages 110 | - `asBoolOrThrow()` 111 | - `asDateTimeOrThrow()` 112 | - `asDoubleOrThrow()` 113 | - `asIntOrThrow()` 114 | - `letOrThrow()` 115 | - `asListOrThrow()` 116 | - `asMapOrThrow()` 117 | - `asStringOrThrow()` 118 | - New `Pick.isAbsent` getter to check if a value is absent or `null` [#24](https://github.com/passsy/deep_pick/pull/24). Absent could mean 119 | 1. Accessing a key which doesn't exist in a `Map` 120 | 2. Reading the value from `List` when the index is greater than the length 121 | 3. Trying to access a key in a `Map` but the found data a `Object` which isn't a Map 122 | - The String `"true"` and `"false"` are now parsed as boolean 123 | - More nnbd refactoring 124 | 125 | ## 0.6.0-nullsafety.0 126 | 127 | - Migrate to nullsafety (required Dart >=2.12) 128 | - Remove long deprecated `parseJsonTo*` methods. Use the `pick(json, args*)` api 129 | - Improve dartdoc 130 | 131 | ## 0.5.1 132 | 133 | - Rename `Pick.addContext` to `Pick.withContext` using deprecation 134 | - `Pick.fromContext` now accepts 10 arguments for nested structures 135 | - Fix `Pick.fromContext` always returning `context` not the value for `key` in context `Map` 136 | 137 | ## 0.5.0 138 | 139 | - New context API. You can now attach relevant additional information for parsing directly to the `Pick` object. This allows passing information into `fromPick` constructors without adding new parameters to all constructors in between. 140 | 141 | ```dart 142 | // Add context 143 | final shoes = pick(json, 'shoes') 144 | .addContext('apiVersion', "2.3.0") 145 | .addContext('lang', "en-US") 146 | .asListOrEmpty((p) => Shoe.fromPick(p.required())); 147 | ``` 148 | 149 | ```dart 150 | import 'package:version/version.dart'; 151 | 152 | // Read context 153 | factory Shoe.fromPick(RequiredPick pick) { 154 | // read context API 155 | final version = pick.fromContext('newApi').required().let((pick) => Version(pick.asString())); 156 | return Shoe( 157 | id: pick('id').required().asString(), 158 | name: pick('name').required().asString(), 159 | // manufacturer is a required field in the new API 160 | manufacturer: version >= Version(2, 3, 0) 161 | ? pick('manufacturer').required().asString() 162 | : pick('manufacturer').asStringOrNull(), 163 | tags: pick('tags').asListOrEmpty(), 164 | ); 165 | } 166 | ``` 167 | 168 | - Breaking: `Pick` and `RequiredPick` have chained their constructor signature. `path` is now a named argument and `context` has been added. 169 | 170 | ```diff 171 | - RequiredPick(this.value, [this.path = const []]) 172 | + RequiredPick(this.value, {this.path = const [], Map context}) 173 | ``` 174 | 175 | - The `path` is now correctly forwarded after `Pick#call` or `Pick#asListOrEmpty` and always shows the full path since origin 176 | 177 | ## 0.4.3 178 | 179 | - Fix error reporting for `asMapOr[Empty|Null]` and don't swallow parsing errors 180 | - Throw Map cast errors when parsing, not lazily when accessing the data 181 | 182 | ## 0.4.2 183 | 184 | - Fix error reporting of `asListOrNull(mapToUser)` and `asListOrEmpty(mapToUser)`. Both now return errors during mapping and don't swallow them 185 | 186 | ## 0.4.1 187 | 188 | - Print correct path in error message when json is `null` 189 | - `asDateTime()` now skips parsing when the value is already a `DateTime` 190 | 191 | ## 0.4.0 192 | 193 | ### Map objects 194 | 195 | New APIs to map picks to objects and to map list elements to objects. 196 | 197 | ```dart 198 | RequiredPick.let(R Function(RequiredPick pick) block): R 199 | Pick.letOrNull(R Function(RequiredPick pick) block): R 200 | 201 | RequiredPick.asList([T Function(Pick) map]): List 202 | Pick.asListOrNull([T Function(Pick) map]): List 203 | Pick.asListOrEmpty([T Function(Pick) map]): List 204 | ``` 205 | 206 | Here are two example how to actually use them. 207 | 208 | ```dart 209 | // easily pick and map objects to dart objects 210 | final Shoe oneShoe = pick(json, 'shoes', 0).letOrNull((p) => Shoe.fromPick(p)); 211 | 212 | // map list of picks to dart objects 213 | final List shoes = 214 | pick(json, 'shoes').asListOrEmpty((p) => Shoe.fromPick(p.required())); 215 | ``` 216 | 217 | ### Required picks 218 | 219 | `Pick` now offers a new `required()` method returning a `RequiredPick`. It makes sure the picked value exists or crashes if it is `null`. Because it can't be `null`, `RequiredPick` doesn't offer fallback methods like `.asIntOrNull()` but only `.asInt()`. This makes the API a bit easier to use for values you can't live without. 220 | 221 | ```dart 222 | // use required() to crash if a object doesn't exist 223 | final name = pick(json, 'shoes', 0, 'name').required().asString(); 224 | print(name); // Nike Zoom Fly 3 225 | ``` 226 | 227 | Note: Calling `.asString()` directly on `Pick` has been deprecated. You now have to call `required()` first to convert the `Pick` to a `RequiredPick` or use a mapping method with fallbacks. 228 | 229 | ### Pick deeper 230 | 231 | Ever got a `Pick`/`RequiredPick` and you wanted to pick even further. This is now possible with the `call` method. Very useful in constructors when parsing methods. 232 | 233 | ```dart 234 | factory Shoe.fromPick(RequiredPick pick) { 235 | return Shoe( 236 | id: pick('id').required().asString(), 237 | name: pick('name').required().asString(), 238 | manufacturer: pick('manufacturer').asStringOrNull(), 239 | tags: pick('tags').asListOrEmpty(), 240 | ); 241 | } 242 | ``` 243 | 244 | ### Bugfixes 245 | 246 | - Don't crash when selecting a out of range index from a `List` 247 | - `.asMap()`, `.asMapOrNull()` and `.asMapOrEmpty()` now consistently return `Map` (was `Map`) 248 | 249 | --- 250 | 251 | Also the lib has been converted to use static extension methods which were introduced in Dart 2.6 252 | 253 | ## 0.3.0 254 | 255 | `asMap` now expects the key type, defaults to `dynamic` instead of `String` 256 | 257 | ```diff 258 | -Pick.asMap(): Map 259 | +Pick.asMap(): Map 260 | ``` 261 | 262 | ## 0.2.0 263 | 264 | New API! 265 | The old `parse*` methods are now deprecated, but still work. 266 | Replace them with the new `pick(json, arg0-9...)` method. 267 | 268 | ```diff 269 | - final name = parseJsonToString(json, 'shoes', 0, 'name'); 270 | + final name = pick(json, 'shoes', 0, 'name').asString(); 271 | ``` 272 | 273 | `pick` returns a `Pick` which offers a rich API to parse values. 274 | 275 | ```text 276 | .asString() 277 | .asStringOrNull() 278 | .asMap() 279 | .asMapOrEmpty() 280 | .asMapOrNull() 281 | .asList() 282 | .asListOrEmpty() 283 | .asListOrNull() 284 | .asBool() 285 | .asBoolOrNull() 286 | .asBoolOrTrue() 287 | .asBoolOrFalse() 288 | .asInt() 289 | .asIntOrNull() 290 | .asDouble() 291 | .asDoubleOrNull() 292 | .asDateTime() 293 | .asDateTimeOrNull() 294 | ``` 295 | 296 | ## 0.1.1 297 | 298 | - pubspec description updated 299 | 300 | ## 0.1.0 301 | 302 | - Initial version 303 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2019 Pascal Welsch 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deep_pick 2 | 3 | [![Pub](https://img.shields.io/pub/v/deep_pick)](https://pub.dartlang.org/packages/deep_pick) 4 | [![Pub Likes](https://img.shields.io/pub/likes/deep_pick)](https://pub.dev/packages/deep_pick/score) 5 | ![Build](https://img.shields.io/github/actions/workflow/status/passsy/deep_pick/dart.yml?branch=master) 6 | ![License](https://img.shields.io/github/license/passsy/deep_pick) 7 | [![style: lint](https://img.shields.io/badge/style-lint-4BC0F5.svg)](https://pub.dev/packages/lint) 8 | 9 | Simplifies manual JSON parsing with a type-safe API. 10 | - No `dynamic`, no manual casting 11 | - Flexible inputs types, fixed output types 12 | - Useful parsing error messages 13 | 14 | ```dart 15 | import 'package:deep_pick/deep_pick.dart'; 16 | 17 | pick(json, 'parsing', 'is', 'fun').asBool(); // true 18 | ``` 19 | 20 | ```bash 21 | $ dart pub add deep_pick 22 | ``` 23 | 24 | ```yaml 25 | dependencies: 26 | deep_pick: ^1.0.0 27 | ``` 28 | 29 | ### Example 30 | 31 | This example demonstrates parsing of an HTTP response using `deep_pick`. You can either use it to parse individual values of a json response or parse whole objects using the `fromPick` constructor. 32 | 33 | 34 | 35 | 64 | 92 | 93 |
36 | 37 | ```dart 38 | 39 | final response = await http.get(Uri.parse('https://api.countapi.xyz/stats')); 40 | final json = jsonDecode(response.body); 41 | 42 | // Parse individual fields (nullable) 43 | final int? requests = pick(json, 'requests').asIntOrNull(); 44 | 45 | // Require values to be non-null or throw a useful error message 46 | final int keys_created = pick(json, 'keys_created').asIntOrThrow(); 47 | 48 | // Pick deep nested values without parsing all objects in between 49 | final String? version = pick(json, 'meta', 'version', 'commit').asStringOrNull(); 50 | 51 | 52 | // Parse a full object using a fromPick factory constructor 53 | final CounterApiStats stats = CounterApiStats.fromPick(pick(json).required()); 54 | 55 | 56 | // Parse lists with a fromPick constructor 57 | final List multipleStats = pick(json, 'items') 58 | .asListOrEmpty((pick) => CounterApiStats.fromPick(pick)); 59 | 60 | 61 | ``` 62 | 63 | 65 | 66 | ```dart 67 | // Http response model 68 | class CounterApiStats { 69 | const CounterApiStats({ 70 | required this.requests, 71 | required this.keys_created, 72 | required this.keys_updated, 73 | this.version, 74 | }); 75 | 76 | final int requests; 77 | final int keys_created; 78 | final int keys_updated; 79 | final String? version; 80 | 81 | factory CounterApiStats.fromPick(RequiredPick pick) { 82 | return CounterApiStats( 83 | requests: pick('requests').asIntOrThrow(), 84 | keys_created: pick('keys_created').asIntOrThrow(), 85 | keys_updated: pick('keys_updated').asIntOrThrow(), 86 | version: pick('version').asStringOrNull(), 87 | ); 88 | } 89 | } 90 | ``` 91 |
94 | 95 | 96 | 97 | 98 | ## Supported types 99 | 100 | ### `String` 101 | Returns the picked `Object` as String representation. 102 | It doesn't matter if the value is actually a `int`, `double`, `bool` or any other Object. 103 | `pick` calls the objects `toString` method. 104 | 105 | ```dart 106 | pick('a').asStringOrThrow(); // "a" 107 | pick(1).asStringOrNull(); // "1" 108 | pick(1.0).asStringOrNull(); // "1.0" 109 | pick(true).asStringOrNull(); // "true" 110 | pick(User(name: "Jason")).asStringOrNull(); // User{name: Jason} 111 | ``` 112 | 113 | ### `int` & `double` 114 | `pick` tries to parse Strings with `int.tryParse` and `double.tryParse`. 115 | A `int` can be parsed as `double` (no precision loss) but not vice versa because it could lead to mistakes. 116 | 117 | ```dart 118 | pick(3).asIntOrThrow(); // 3 119 | pick("3").asIntOrNull(); // 3 120 | pick(1).asDoubleOrThrow(); // 1.0 121 | pick("2.7").asDoubleOrNull(); // 2.7 122 | ``` 123 | 124 | ### `bool` 125 | 126 | Parsing a bool couldn't be easier with those self-explaining methods 127 | ```dart 128 | pick(true).asBoolOrThrow(); // true 129 | pick(false).asBoolOrThrow(); // true 130 | pick(null).asBoolOrTrue(); // true 131 | pick(null).asBoolOrFalse(); // false 132 | pick(null).asBoolOrNull(); // null 133 | pick('true').asBoolOrNull(); // true; 134 | pick('false').asBoolOrNull(); // false; 135 | ``` 136 | 137 | `deep_pick` does not treat the `int` values `0` and `1` as `bool` as some other languages do. 138 | Write your own logic using `.let` instead. 139 | 140 | ```dart 141 | pick(1).asBoolOrNull(); // null 142 | pick(1).letOrNull((pick) => pick.value == 1 ? true : pick.value == 0 ? false : null); // true 143 | ``` 144 | 145 | ### `DateTime` 146 | 147 | Accepts most common date formats such as 148 | - [`ISO 8601`](https://www.w3.org/TR/NOTE-datetime) including [`RFC-3339`](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6), 149 | - [`RFC 1123`](https://www.rfc-editor.org/rfc/rfc1123#page-55) including [HTTP `date`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date) header, [RSS2](https://validator.w3.org/feed/docs/rss2.html) `pubDate`, [`RFC 822`](https://www.rfc-editor.org/rfc/rfc822#section-5), [`RFC 1036`](https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.2) and [`RFC 2822`](https://datatracker.ietf.org/doc/html/rfc2822#page-14), 150 | - [`RFC 850`](https://www.rfc-editor.org/rfc/rfc850#section-2.1.4) including [`RFC 1036`](https://www.rfc-editor.org/rfc/rfc1036#section-2.1.2), `COOKIE` 151 | - `ANSI C asctime()` 152 | 153 | ```dart 154 | pick('2021-11-01T11:53:15Z').asDateTimeOrNull(); // UTC 155 | pick('2021-11-01T11:53:15+0000').asDateTimeOrNull(); // ISO 8601 156 | pick('Monday, 01-Nov-21 11:53:15 UTC').asDateTimeOrThrow(); // RFC 850 157 | pick('Wed, 21 Oct 2015 07:28:00 GMT').asDateTimeOrThrow(); // RFC 1123 158 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(); // asctime() 159 | ``` 160 | 161 | ### `List` 162 | 163 | When the JSON object contains a List of items that List can be mapped to a `List` of objects (`T`). 164 | 165 | ```dart 166 | pick([]).asListOrNull(SomeObject.fromPick); 167 | pick([]).asListOrThrow(SomeObject.fromPick); 168 | pick([]).asListOrEmpty(SomeObject.fromPick); 169 | ``` 170 | 171 | ```dart 172 | final users = [ 173 | {'name': 'John Snow'}, 174 | {'name': 'Daenerys Targaryen'}, 175 | ]; 176 | List persons = pick(users).asListOrEmpty((pick) { 177 | return Person( 178 | name: pick('name').required().asString(), 179 | ); 180 | }); 181 | 182 | class Person { 183 | final String name; 184 | 185 | Person({required this.name}); 186 | } 187 | ``` 188 | 189 | #### Note 1 190 | Extract the mapper function and use it as a reference allows to write it in a single line again :smile: 191 | 192 | ```dart 193 | List persons = pick(users).asListOrEmpty(Person.fromPick); 194 | ``` 195 | 196 | Replacing the static function with a factory constructor doesn't work. 197 | Constructors cannot be referenced as functions, yet ([dart-lang/language/issues/216](https://github.com/dart-lang/language/issues/216)). 198 | Meanwhile, use `.asListOrEmpty((it) => Person.fromPick(it))` when using a factory constructor. 199 | 200 | #### Note 2 201 | `pick` called in the `fromPick` function uses the parameter `pick`, not the top-level function. 202 | This is possible because `Pick` implements the `.call()` method. 203 | This allows chaining indefinitely on the same `Pick` object while maintaining internal references for useful error messages. 204 | 205 | Both versions produce the same result and shows you're not limited to 10 arguments. 206 | ```dart 207 | pick(json, 'shoes', 1, 'tags', 0).asStringOrThrow(); 208 | pick(json)('shoes')(1)('tags')(0).asStringOrThrow(); 209 | ``` 210 | 211 | #### whenNull 212 | 213 | To simplify the `asList` API, the functions ignores `null` values in the `List`. 214 | This allows the usage of `RequiredPick` over `Pick` in the `map` function. 215 | 216 | When `null` is important for your logic you can process the `null` value by providing an optional `whenNull` mapper function. 217 | 218 | ```dart 219 | pick([1, null, 3]).asListOrNull( 220 | (it) => it.asInt(), 221 | whenNull: (Pick pick) => 25; 222 | ); 223 | // [1, 25, 3] 224 | ``` 225 | 226 | ### `Map` 227 | 228 | Picking the `Map` is rarely used, because `Pick` itself grants further picking using the `.call(args)` method. 229 | Converting back to a `Map` is usually only used for existing `fromMap` mapper functions. 230 | 231 | ```dart 232 | pick(json).asMapOrNull(); 233 | pick(json).asMapOrThrow(); 234 | pick(json).asMapOrEmpty(); 235 | ``` 236 | 237 | 238 | ## Custom parsers 239 | 240 | Parsers in `deep_pick` are based on extension functions on the classes `Pick`. 241 | This makes it flexible and easy for 3rd-party types to add custom parsers. 242 | 243 | This example parses a `int` as Firestore `Timestamp`. 244 | ```dart 245 | import 'package:cloud_firestore/cloud_firestore.dart'; 246 | import 'package:deep_pick/deep_pick.dart'; 247 | 248 | extension TimestampPick on Pick { 249 | Timestamp asFirestoreTimeStampOrThrow() { 250 | final value = required().value; 251 | if (value is Timestamp) { 252 | return value; 253 | } 254 | if (value is int) { 255 | return Timestamp.fromMillisecondsSinceEpoch(value); 256 | } 257 | throw PickException("value $value at $debugParsingExit can't be casted to Timestamp"); 258 | } 259 | 260 | Timestamp? asFirestoreTimeStampOrNull() { 261 | if (value == null) return null; 262 | try { 263 | return asFirestoreTimeStampOrThrow(); 264 | } catch (_) { 265 | return null; 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | ### let 272 | 273 | When using a custom type in only a few places, it might be overkill to create all the extensions. 274 | For those cases use the [let function](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html) borrowed from Kotlin to creating neat one-liners. 275 | 276 | ```dart 277 | final UserId id = pick(json, 'id').letOrNull((it) => UserId(it.asString())); 278 | final Timestamp timestamp = pick(json, 'time') 279 | .letOrThrow((it) => Timestamp.fromMillisecondsSinceEpoch(it.asInt())); 280 | ``` 281 | 282 | ## Examples 283 | 284 | ### Reading documents from Firestore 285 | Picking values from a Firebase `DocumentSnapshot` is usually very selective. 286 | Only a fraction of the properties have to be parsed. 287 | In this scenario it would be overkill to map the whole document to a Dart object. 288 | Instead, parse the values in place while staying type-safe. 289 | 290 | Use `.asStringOrThrow()` when confident that the value is never `null` and **always** exists. 291 | The return type then becomes non-nullable (`String` instead of `String?`). 292 | When the `data` doesn't contain the `full_name` field (against your assumption) it would crash throwing a `PickException`. 293 | 294 | ```dart 295 | final DocumentSnapshot userDoc = 296 | await FirebaseFirestore.instance.collection('users').doc(userId).get(); 297 | final data = userDoc.data(); 298 | final String fullName = pick(data, 'full_name').asStringOrThrow(); 299 | final String? level = pick(data, 'level').asIntOrNull(); 300 | ``` 301 | 302 | `deep_pick` offers an alternative `required()` API with the same result. 303 | This is useful to make sure a value exists before parsing it. 304 | In case it is `null` or absent a useful error message is printed. 305 | 306 | ```dart 307 | final String fullName = pick(data, 'full_name').required().asString(); 308 | ``` 309 | 310 | ## Background & Justification 311 | 312 | Before Dart 2.12 and the new `?[]` operator one had to write a lot of code to prevent crashes when a value isn't set! 313 | Reducing this boilerplate was the origin of `deep_pick`. 314 | ```dart 315 | String milestoneCreator; 316 | final milestone = json['milestore']; 317 | if (milestone != null) { 318 | final creator = json['creator']; 319 | if (creator != null) { 320 | final login = creator['login']; 321 | if (login is String) { 322 | milestoneCreator = login; 323 | } 324 | } 325 | } 326 | print(milestoneCreator); // octocat 327 | ``` 328 | This example of parses an `issue` object of the [GitHub v3 API](https://developer.github.com/v3/issues/#get-an-issue). 329 | 330 | Today with Dart 2.12+ parsing Dart data structures has become way easier with the introduction of the `?[]` operator. 331 | ```dart 332 | final json = jsonDecode(response.data); 333 | final milestoneCreator = json?['milestone']?['creator']?['login'] as String?; 334 | print(milestoneCreator); // octocat 335 | ``` 336 | 337 | `deep_pick` backports this short syntax to previous Dart versions (`<2.12`). 338 | 339 | ```dart 340 | final milestoneCreator = pick(json, 'milestone', 'creator', 'login').asStringOrNull(); 341 | print(milestoneCreator); // octocat 342 | ``` 343 | 344 | ## Still better than vanilla 345 | 346 | But even with the latest Dart version, `deep_pick` offers fantastic features over vanilla parsing using the `?[]` operators: 347 | 348 | ### 1. Flexible input types 349 | 350 | Different languages and their JSON libraries generate different JSON. 351 | Sometimes `id`s are `String`, sometimes `int`. Booleans are provided as `true` or with quotes as String `"true"`. 352 | The meaning is the same but from a type perspective they are not. 353 | 354 | `deep_pick` does the basic conversions automatically. 355 | By requesting a specific return type, apps won't break when a "price" usually returns `double` (`0.99`) but for whole numbers `int` (`1` instead of `1.0`). 356 | 357 | ```dart 358 | pick(2).asIntOrNull(); // 2 359 | pick('2').asIntOrNull(); // 2 (Sting -> int) 360 | 361 | pick(42.0).asDoubleOrNull(); // 42.0 362 | pick(42).asDoubleOrNull(); // 42.0 (double -> int) 363 | 364 | pick(true).asBoolOrFalse(); // true 365 | pick('true').asBoolOrFalse(); // true (String -> bool) 366 | ``` 367 | 368 | ### 2. No RangeError for Lists 369 | Using the `?[]` operator can crash for Lists. 370 | Accessing a list item by `index` outside of the available range causes a `RangeError`. 371 | You can't access index 23 when the `List` has only 10 items. 372 | 373 | ```dart 374 | json['shoes']?[23]?['id'] as String?; 375 | 376 | // Unhandled exception: 377 | // RangeError (index): Invalid value: Not in inclusive range 0..10: 23 378 | ``` 379 | 380 | `pick` automatically catches the `RangeError` and returns `null`. 381 | 382 | ```dart 383 | pick(json, 'shoes', 23, 'id').asStringOrNull(); // null 384 | ``` 385 | 386 | ### 3. Useful error message 387 | 388 | Vanilla Dart returns a type error because `null` is not a `String`. 389 | There is no information available which part is `null` or missing. 390 | 391 | ```dart 392 | final milestoneCreator = json?['milestone']?['creator']?['login'] as String; 393 | 394 | // Unhandled exception: 395 | // type 'Null' is not a subtype of type 'String' in type cast 396 | ``` 397 | 398 | `deep_pick` shows the exact location where parsing failed, making it easy to report errors to the API team. 399 | 400 | ```dart 401 | final milestoneCreator = pick(json, 'milestone', 'creator', 'login').asStringOrThrow(); 402 | 403 | // Unhandled exception: 404 | // PickException( 405 | // Expected a non-null value but location "milestone" in pick(json, "milestone" (absent), "creator", "login") is absent. 406 | // Use asStringOrNull() when the value may be null at some point (String?). 407 | // ) 408 | ``` 409 | 410 | Notice the distinction between "absent" and "null" when you see such errors. 411 | - `"absent"` means the key isn't found in a Map or a List has no item at the requested index 412 | - `"null"` means the value at that position is actually `null` 413 | 414 | ### 4. Null is default, crashes intentional 415 | 416 | Parsing objects from external systems isn't type-safe. 417 | API changes happen, and it is up to the consumer to decide how to handle them. 418 | Consumer always have to assume the worst, such as missing values. 419 | 420 | It's so easy to accidentally cast a value to `String` in the happy path, instead of `String?` accounting for all possible cases. 421 | Easy to write, easy to miss in code reviews. 422 | 423 | Forgetting that `null` could be a valid return type results in a type error: 424 | 425 | ```dart 426 | json?['milestone']?['creator']?['login'] as String; 427 | // ----^ 428 | // Unhandled exception: 429 | // type 'Null' is not a subtype of type 'String' in type cast 430 | ``` 431 | 432 | With `deep_pick`, all casting methods (`.as*()`) have `null` in mind. 433 | For each type you have to choose between at least two ways to deal with `null`. 434 | 435 | ```dart 436 | pick(json, ...).asStringOrNull(); 437 | pick(json, ...).asStringOrThrow(); 438 | 439 | pick(json, ...).asBoolOrNull(); 440 | pick(json, ...).asBoolOrFalse(); 441 | 442 | pick(json, ...).asListOrNull(SomeClass.fromPick); 443 | pick(json, ...).asListOrEmpty(SomeClass.fromPick); 444 | ``` 445 | 446 | Having "Throw" and "Null" in the method name, clearly indicates the possible outcome in case the values couldn't be picked. 447 | Throwing is not a bad habit, some properties are essential for the business logic and throwing an error the correct handling. 448 | But throwing should be done intentional, not accidental. 449 | 450 | ### 5. Map Objects with let 451 | 452 | Even with the new `?[]` operator, mapping a value to a new Object (i.e. when wrapping it in a domain Object) can't be done in a single line. 453 | 454 | ```dart 455 | final value = json?['id'] as String?; 456 | final UserId id = value == null ? null : UserId(value); 457 | ``` 458 | 459 | `deep_pick` borrows the [let function](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/let.html) from Kotlin creating a neat one-liner 460 | 461 | ```dart 462 | final UserId id = pick(json, 'id').letOrNull((it) => UserId(it.asString())); 463 | ``` 464 | 465 | ## License 466 | 467 | ``` 468 | Copyright 2019 Pascal Welsch 469 | 470 | Licensed under the Apache License, Version 2.0 (the "License"); 471 | you may not use this file except in compliance with the License. 472 | You may obtain a copy of the License at 473 | 474 | http://www.apache.org/licenses/LICENSE-2.0 475 | 476 | Unless required by applicable law or agreed to in writing, software 477 | distributed under the License is distributed on an "AS IS" BASIS, 478 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 479 | See the License for the specific language governing permissions and 480 | limitations under the License. 481 | ``` 482 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lint/analysis_options_package.yaml 2 | 3 | analyzer: 4 | strong-mode: 5 | implicit-casts: false 6 | 7 | linter: 8 | rules: 9 | prefer_single_quotes: true -------------------------------------------------------------------------------- /example/deep_pick_example.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print, always_require_non_null_named_parameters, non_constant_identifier_names, omit_local_variable_types, avoid_dynamic_calls, unreachable_from_main 2 | import 'dart:convert'; 3 | 4 | import 'package:deep_pick/deep_pick.dart'; 5 | import 'package:http/http.dart' as http; 6 | 7 | const String rawJson = ''' 8 | { 9 | "shoes": [ 10 | { 11 | "id": "421", 12 | "name": "Nike Zoom Fly 3", 13 | "manufacturer": "nike", 14 | "tags": ["nike", "JustDoIt"] 15 | }, 16 | { 17 | "id": "532", 18 | "name": "adidas Ultraboost", 19 | "manufacturer": "adidas", 20 | "tags": ["adidas", "ImpossibleIsNothing"], 21 | "price": null 22 | } 23 | ] 24 | } 25 | '''; 26 | 27 | Future main() async { 28 | final firstTag3 = 29 | pickFromJson(rawJson, 'shoes', 1, 'tags', 0).asStringOrThrow(); 30 | print(firstTag3); 31 | 32 | final json = jsonDecode(rawJson); 33 | // pick a value deep down the json structure or crash 34 | final firstTag = pick(json, 'shoes', 1, 'tags', 0).asStringOrThrow(); 35 | print(firstTag); // adidas 36 | 37 | // The unsafe vanilla way 38 | final firstTag2 = json['shoes']?[1]?['tags'][0] as String?; 39 | print(firstTag2); // adidas 40 | 41 | // fallback to null if it couldn't be found 42 | final manufacturer = pick(json, 'shoes', 0, 'manufacturer').asStringOrNull(); 43 | print(manufacturer); // null 44 | 45 | // you decide which type you want 46 | final id = pick(json, 'shoes', 0, 'id'); 47 | print(id.asIntOrNull()); // 421 48 | print(id.asDoubleOrNull()); // 421.0 49 | print(id.asStringOrNull()); // "421" 50 | 51 | // pick lists 52 | final tags = pick(json, 'shoes', 0, 'tags') 53 | .asListOrEmpty((it) => it.asStringOrThrow()); 54 | print(tags); // [nike, JustDoIt] 55 | 56 | // pick maps 57 | final shoe = pick(json, 'shoes', 0).required().asMapOrThrow(); 58 | print(shoe); // {id: 421, name: Nike Zoom Fly 3, tags: [nike, JustDoIt]} 59 | 60 | // easily pick and map objects to dart objects 61 | final firstShoe = pick(json, 'shoes', 0).letOrNull((p) => Shoe.fromPick(p)); 62 | print(firstShoe); 63 | // Shoe{id: 421, name: Nike Zoom Fly 3, tags: [nike, JustDoIt]} 64 | 65 | // falls back to null when the value couldn't be picked 66 | final thirdShoe = pick(json, 'shoes', 2).letOrNull((p) => Shoe.fromPick(p)); 67 | print(thirdShoe); // null 68 | 69 | // map list of picks to dart objects 70 | final shoes = pick(json, 'shoes').asListOrEmpty((p) => Shoe.fromPick(p)); 71 | print(shoes); 72 | // [ 73 | // Shoe{id: 421, name: Nike Zoom Fly 3, tags: [nike, JustDoIt]}, 74 | // Shoe{id: 532, name: adidas Ultraboost, tags: [adidas, ImpossibleIsNothing]} 75 | // ] 76 | 77 | // Use the Context API to pass contextual information down to parsing 78 | // without adding new arguments 79 | final newShoes = pick(json, 'shoes') 80 | .withContext('newApi', true) 81 | .asListOrEmpty((p) => Shoe.fromPick(p)); 82 | print(newShoes); 83 | 84 | // access value out of range 85 | final puma = pick(json, 'shoes', 1); 86 | print(puma.isAbsent); // true; 87 | print(puma.value); // null 88 | 89 | // Load data from an API 90 | final stats = await getStats(); 91 | print(stats.requests); 92 | 93 | // pick values with a dynamic selector 94 | pickDeep(json, 'some.key.inside.the.object'.split('.')).asStringOrNull(); 95 | } 96 | 97 | /// A data class representing a shoe model 98 | /// 99 | /// PODO - plain old dart object 100 | class Shoe { 101 | const Shoe({ 102 | required this.id, 103 | required this.name, 104 | this.manufacturer, 105 | required this.tags, 106 | required this.price, 107 | }); 108 | 109 | factory Shoe.fromPick(RequiredPick pick) { 110 | // read context API 111 | final newApi = pick.fromContext('newApi').asBoolOrFalse(); 112 | final pricePick = pick('price'); 113 | return Shoe( 114 | id: pick('id').asStringOrThrow(), 115 | name: pick('name').asStringOrThrow(), 116 | // manufacturer is a required field in the new API 117 | manufacturer: newApi 118 | ? pick('manufacturer').asStringOrThrow() 119 | : pick('manufacturer').asStringOrNull(), 120 | tags: pick('tags').asListOrEmpty((it) => it.asStringOrThrow()), 121 | price: () { 122 | // when server doesn't send the price field the shoe is not available 123 | if (pricePick.isAbsent) return 'Not for sale'; 124 | return pricePick.asStringOrNull() ?? 'Price available soon'; 125 | }(), 126 | ); 127 | } 128 | 129 | /// never null 130 | final String id; 131 | 132 | /// never null 133 | final String name; 134 | 135 | /// optional 136 | final String? manufacturer; 137 | 138 | /// never null, falls back to empty list 139 | final List tags; 140 | 141 | /// what to display as price 142 | final String price; 143 | 144 | @override 145 | String toString() { 146 | return 'Shoe{id: $id, name: "$name", price: "$price", tags: $tags}'; 147 | } 148 | 149 | @override 150 | bool operator ==(Object other) => 151 | identical(this, other) || 152 | other is Shoe && 153 | runtimeType == other.runtimeType && 154 | id == other.id && 155 | name == other.name && 156 | tags == other.tags; 157 | 158 | @override 159 | int get hashCode => id.hashCode ^ name.hashCode ^ tags.hashCode; 160 | } 161 | 162 | Future getStats() async { 163 | final response = await http.get(Uri.parse('https://api.countapi.xyz/stats')); 164 | final json = jsonDecode(response.body); 165 | 166 | // Parse individual fields 167 | final int? requests = pick(json, 'requests').asIntOrNull(); 168 | final int keys_created = pick(json, 'keys_created').asIntOrThrow(); 169 | final int? keys_updated = pick(json, 'keys_updated').asIntOrNull(); 170 | final String? version = pick(json, 'version').asStringOrNull(); 171 | print( 172 | 'requests $requests, keys_created $keys_created, ' 173 | 'keys_updated: $keys_updated, version: "$version"', 174 | ); 175 | 176 | // Parse the full object 177 | final CounterApiStats stats = CounterApiStats.fromPick(pick(json).required()); 178 | 179 | // Parse lists 180 | final List multipleStats = pick(json, 'items') 181 | .asListOrEmpty((pick) => CounterApiStats.fromPick(pick)); 182 | print(multipleStats); // always empty [], the countapi doesn't have items 183 | 184 | return stats; 185 | } 186 | 187 | class CounterApiStats { 188 | const CounterApiStats({ 189 | required this.requests, 190 | required this.keys_created, 191 | required this.keys_updated, 192 | this.version, 193 | }); 194 | 195 | final int requests; 196 | final int keys_created; 197 | final int keys_updated; 198 | final String? version; 199 | 200 | factory CounterApiStats.fromPick(RequiredPick pick) { 201 | return CounterApiStats( 202 | requests: pick('requests').asIntOrThrow(), 203 | keys_created: pick('keys_created').asIntOrThrow(), 204 | keys_updated: pick('keys_updated').asIntOrThrow(), 205 | version: pick('version').asStringOrNull(), 206 | ); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /lib/deep_pick.dart: -------------------------------------------------------------------------------- 1 | export 'package:deep_pick/src/pick.dart' hide requiredPickErrorHintKey; 2 | export 'package:deep_pick/src/pick_bool.dart'; 3 | export 'package:deep_pick/src/pick_datetime.dart'; 4 | export 'package:deep_pick/src/pick_double.dart'; 5 | export 'package:deep_pick/src/pick_int.dart'; 6 | export 'package:deep_pick/src/pick_let.dart'; 7 | export 'package:deep_pick/src/pick_list.dart'; 8 | export 'package:deep_pick/src/pick_map.dart'; 9 | export 'package:deep_pick/src/pick_string.dart'; 10 | -------------------------------------------------------------------------------- /lib/src/pick.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | /// Picks a values from a [json] String at location arg0, arg1... 4 | /// 5 | /// args may be 6 | /// - a [String] to pick values from a [Map] 7 | /// - or [int] when you want to pick a value at index from a [List] 8 | /// 9 | /// 10 | /// It's quite common that pick is used when parsing json from a String, such 11 | /// as a http response body. To easy this process [pickFromJson] parses a json 12 | /// String directly. 13 | /// 14 | /// ```dart 15 | /// pickFromJson(rawJson, arg0, arg1) 16 | /// ``` 17 | /// 18 | /// is a shorthand for 19 | /// 20 | /// ```dart 21 | /// final json = jsonDecode(rawJson); 22 | /// pick(json, arg0, arg1); 23 | /// ``` 24 | /// 25 | /// If objects are deeper than 10, use [pickDeep], which requires a manual call 26 | /// to [jsonDecode]. 27 | Pick pickFromJson( 28 | String json, [ 29 | Object? arg0, 30 | Object? arg1, 31 | Object? arg2, 32 | Object? arg3, 33 | Object? arg4, 34 | Object? arg5, 35 | Object? arg6, 36 | Object? arg7, 37 | Object? arg8, 38 | Object? arg9, 39 | ]) { 40 | final parsed = jsonDecode(json); 41 | return pick( 42 | parsed, 43 | arg0, 44 | arg1, 45 | arg2, 46 | arg3, 47 | arg4, 48 | arg5, 49 | arg6, 50 | arg7, 51 | arg8, 52 | arg9, 53 | ); 54 | } 55 | 56 | /// Picks the value of a [json]-like dart data structure consisting of Maps, 57 | /// Lists and objects at location arg0, arg1 ... arg9 58 | /// 59 | /// args may be 60 | /// - a [String] to pick values from a [Map] 61 | /// - or [int] when you want to pick a value at index from a [List] 62 | /// 63 | /// If objects are deeper than 10, use [pickDeep] 64 | Pick pick( 65 | /*Map|List|null*/ dynamic json, [ 66 | /*String|int|null*/ Object? arg0, 67 | /*String|int|null*/ Object? arg1, 68 | /*String|int|null*/ Object? arg2, 69 | /*String|int|null*/ Object? arg3, 70 | /*String|int|null*/ Object? arg4, 71 | /*String|int|null*/ Object? arg5, 72 | /*String|int|null*/ Object? arg6, 73 | /*String|int|null*/ Object? arg7, 74 | /*String|int|null*/ Object? arg8, 75 | /*String|int|null*/ Object? arg9, 76 | ]) { 77 | final selectors = [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9] 78 | // null is a sign for unused 'varargs' 79 | .where((dynamic it) => it != null) 80 | .cast() 81 | .toList(growable: false); 82 | return _drillDown(json, selectors); 83 | } 84 | 85 | /// Picks the value of [json] by traversing the object along the values in 86 | /// [selector] one by one 87 | /// 88 | /// Valid values for the items in selector are 89 | /// - a [String] to pick values from a [Map] 90 | /// - or [int] when you want to pick a value at index from a [List] 91 | Pick pickDeep( 92 | /*Map|List|null*/ dynamic json, 93 | List< /*String|int*/ Object> selector, 94 | ) { 95 | return _drillDown(json, selector); 96 | } 97 | 98 | /// Traverses the object along [selectors] 99 | Pick _drillDown( 100 | /*Map|List|null*/ dynamic json, 101 | List< /*String|int*/ Object> selectors, { 102 | List< /*String|int*/ Object> parentPath = const [], 103 | Map? context, 104 | }) { 105 | final fullPath = [...parentPath, ...selectors]; 106 | final path = []; 107 | /*Map|List|null*/ dynamic data = json; 108 | for (final selector in selectors) { 109 | path.add(selector); 110 | if (data is List) { 111 | if (selector is int) { 112 | try { 113 | data = data[selector]; 114 | if (data == null) { 115 | return Pick(null, path: fullPath, context: context); 116 | } 117 | // found a value, continue drill down 118 | continue; 119 | // ignore: avoid_catching_errors 120 | } on RangeError catch (_) { 121 | // out of range, value not found at index selector 122 | return Pick.absent(path.length - 1, path: fullPath, context: context); 123 | } 124 | } 125 | } 126 | if (data is Map) { 127 | if (!data.containsKey(selector)) { 128 | return Pick.absent(path.length - 1, path: fullPath, context: context); 129 | } 130 | final dynamic picked = data[selector]; 131 | if (picked == null) { 132 | // no value mapped to selector 133 | return Pick(null, path: fullPath, context: context); 134 | } 135 | data = picked; 136 | continue; 137 | } 138 | if (data is Set && selector is int) { 139 | throw PickException( 140 | 'Value at location ${path.sublist(0, path.length - 1)} is a Set, which is a unordered data structure. ' 141 | "It's not possible to pick a value by using a index ($selector)", 142 | ); 143 | } 144 | // can't drill down any more to find the exact location. 145 | return Pick.absent(path.length - 1, path: fullPath, context: context); 146 | } 147 | return Pick(data, path: fullPath, context: context); 148 | } 149 | 150 | /// A picked object holding the [value] (may be null) and giving access to useful parsing functions 151 | class Pick { 152 | /// Pick constructor when being able to drill down [path] all the way to reach 153 | /// the value. 154 | /// [value] may still be `null` but the structure was correct, therefore 155 | /// [isAbsent] will always return `false`. 156 | Pick( 157 | this.value, { 158 | this.path = const [], 159 | Map? context, 160 | }) : context = context != null ? Map.of(context) : {}; 161 | 162 | /// Pick of an absent value. While drilling down [path] the structure of the 163 | /// data did not match the [path] and the value wasn't found. 164 | /// 165 | /// [value] will always return `null` and [isAbsent] always `true`. 166 | Pick.absent( 167 | int missingValueAtIndex, { 168 | this.path = const [], 169 | Map? context, 170 | }) : value = null, 171 | _missingValueAtIndex = missingValueAtIndex, 172 | context = context != null ? Map.of(context) : {}; 173 | 174 | /// The picked value, might be `null` 175 | final Object? value; 176 | 177 | /// Allows the distinction between the actual [value] `null` and the value not 178 | /// being available 179 | /// 180 | /// Usually, it doesn't matter, but for rare cases, it does this method can be 181 | /// used to check if a [Map] contains `null` for a key or the key being absent 182 | /// 183 | /// Not available could mean: 184 | /// - Accessing a key which doesn't exist in a [Map] 185 | /// - Reading the value from [List] when the index is greater than the length 186 | /// - Trying to access a key in a [Map] but the found data structure is a [List] 187 | /// 188 | /// ``` 189 | /// pick({"a": null}, "a").isAbsent; // false 190 | /// pick({"a": null}, "b").isAbsent; // true 191 | /// 192 | /// pick([null], 0).isAbsent; // false 193 | /// pick([], 2).isAbsent; // true 194 | /// 195 | /// pick([], "a").isAbsent; // true 196 | /// ``` 197 | bool get isAbsent => missingValueAtIndex != null; 198 | 199 | /// Attaches additional information which can be used during parsing. 200 | /// i.e the HTTP request/response including headers 201 | final Map context; 202 | 203 | /// The index of the object when it is an element in a `List` 204 | /// 205 | /// Usage: 206 | /// 207 | /// ```dart 208 | /// pick(["John", "Paul", "George", "Ringo"]).asListOrThrow((pick) { 209 | /// final index = pick.index!; 210 | /// return Artist(id: index, name: pick.asStringOrThrow()); 211 | /// ); 212 | /// ``` 213 | int? get index { 214 | final lastPathSegment = path.isNotEmpty ? path.last : null; 215 | if (lastPathSegment == null) { 216 | return null; 217 | } 218 | if (lastPathSegment is int) { 219 | // within a List 220 | return lastPathSegment; 221 | } 222 | return null; 223 | } 224 | 225 | /// When the picked value is unavailable ([Pick.isAbsent]) the index in 226 | /// [path] which couldn't be found 227 | int? get missingValueAtIndex => _missingValueAtIndex; 228 | int? _missingValueAtIndex; 229 | 230 | /// The full path to [value] inside of the object 231 | /// 232 | /// I.e. `['shoes', 0, 'name']` 233 | final List path; 234 | 235 | /// The path segments containing non-null values parsing could follow along 236 | /// 237 | /// I.e. `['shoes']` for an empty shoes list 238 | List get followablePath => 239 | path.take(_missingValueAtIndex ?? path.length).toList(); 240 | 241 | // Pick even further 242 | Pick call([ 243 | Object? arg0, 244 | Object? arg1, 245 | Object? arg2, 246 | Object? arg3, 247 | Object? arg4, 248 | Object? arg5, 249 | Object? arg6, 250 | Object? arg7, 251 | Object? arg8, 252 | Object? arg9, 253 | ]) { 254 | final selectors = 255 | [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9] 256 | // null is a sign for unused 'varargs' 257 | .where((Object? it) => it != null) 258 | .cast() 259 | .toList(growable: false); 260 | 261 | return _drillDown(value, selectors, parentPath: path, context: context); 262 | } 263 | 264 | /// Enter a "required" context which requires the picked value to be non-null 265 | /// or a [PickException] is thrown. 266 | /// 267 | /// Crashes when the the value is `null`. 268 | RequiredPick required() { 269 | final value = this.value; 270 | if (value == null) { 271 | final more = fromContext(requiredPickErrorHintKey).value as String?; 272 | final moreSegment = more == null ? '' : ' $more'; 273 | throw PickException( 274 | 'Expected a non-null value but location $debugParsingExit ' 275 | 'is ${isAbsent ? 'absent' : 'null'}.$moreSegment', 276 | ); 277 | } 278 | return RequiredPick(value, path: path, context: context); 279 | } 280 | 281 | @override 282 | @Deprecated('Use asStringOrNull() to pick a String value') 283 | String toString() => 'Pick(value=$value, path=$path)'; 284 | 285 | /// Attaches additional information which can be used during parsing. 286 | /// i.e the HTTP request/response including headers 287 | /// 288 | /// Use this method to chain methods. It's pure syntax sugar. 289 | /// The alternative cascade operator often requires additional parenthesis 290 | /// 291 | /// Add context at the top 292 | /// ``` 293 | /// pick(json) 294 | /// .withContext('apiVersion', response.getApiVersion()) 295 | /// .let((pick) => Response.fromPick(pick)); 296 | /// ``` 297 | /// 298 | /// Read it where required 299 | /// ``` 300 | /// factory Item.fromPick(RequiredPick pick) { 301 | /// final Version apiVersion = pick.fromContext('apiVersion').asVersion(); 302 | /// if (apiVersion >= Version(0, 2, 0)) { 303 | /// return Item( 304 | /// color: pick("detail", "color").required().asString(), 305 | /// ); 306 | /// } else { 307 | /// return Item( 308 | /// color: pick("meta-data", "variant", 0, "color").required().asString(), 309 | /// ); 310 | /// } 311 | /// } 312 | /// ``` 313 | Pick withContext(String key, Object? value) { 314 | context[key] = value; 315 | return this; 316 | } 317 | 318 | /// Pick values from the context using the [Pick] API 319 | /// 320 | /// ``` 321 | /// pick.fromContext('apiVersion').asIntOrNull(); 322 | /// ``` 323 | Pick fromContext( 324 | String key, [ 325 | Object? arg0, 326 | Object? arg1, 327 | Object? arg2, 328 | Object? arg3, 329 | Object? arg4, 330 | Object? arg5, 331 | Object? arg6, 332 | Object? arg7, 333 | Object? arg8, 334 | ]) { 335 | return pick( 336 | context, 337 | key, 338 | arg0, 339 | arg1, 340 | arg2, 341 | arg3, 342 | arg4, 343 | arg5, 344 | arg6, 345 | arg7, 346 | arg8, 347 | ); 348 | } 349 | 350 | /// Returns a human readable String of the requested [path] and the actual 351 | /// parsed value following the path along ([followablePath]). 352 | /// 353 | /// Examples: 354 | /// picked value "b" using pick(json, "a"(b)) 355 | /// picked value "null" using pick(json, "a" (null)) 356 | /// picked value "Instance of \'Object\'" using pick() 357 | /// "unknownKey" in pick(json, "unknownKey" (absent)) 358 | String get debugParsingExit { 359 | final access = []; 360 | 361 | // The full path to [value] inside of the object 362 | // I.e. ['shoes', 0, 'name'] 363 | final fullPath = path; 364 | 365 | // The path segments containing non-null values parsing could follow along 366 | // I.e. ['shoes'] for an empty shoes list 367 | final followable = followablePath; 368 | 369 | final foundValue = followable.length == fullPath.length; 370 | var foundNullPart = false; 371 | for (var i = 0; i < fullPath.length; i++) { 372 | final full = fullPath[i]; 373 | final part = followable.length > i ? followable[i] : null; 374 | final nullPart = () { 375 | if (foundNullPart) return ''; 376 | if (foundValue && i + 1 == fullPath.length) { 377 | if (value == null) { 378 | foundNullPart = true; 379 | return ' (null)'; 380 | } else { 381 | return '($value)'; 382 | } 383 | } 384 | if (part == null) { 385 | foundNullPart = true; 386 | return ' (absent)'; 387 | } 388 | return ''; 389 | }(); 390 | 391 | if (full is int) { 392 | access.add('$full$nullPart'); 393 | } else { 394 | access.add('"$full"$nullPart'); 395 | } 396 | } 397 | 398 | var valueOrExit = ''; 399 | if (foundValue) { 400 | valueOrExit = 'picked value "$value" using'; 401 | } else { 402 | final firstMissing = fullPath.isEmpty 403 | ? '' 404 | : fullPath[followable.isEmpty ? 0 : followable.length]; 405 | final formattedMissing = 406 | firstMissing is int ? 'list index $firstMissing' : '"$firstMissing"'; 407 | valueOrExit = '$formattedMissing in'; 408 | } 409 | 410 | final params = access.isNotEmpty ? ', ${access.join(', ')}' : ''; 411 | final root = access.isEmpty ? '' : 'json'; 412 | return '$valueOrExit pick($root$params)'; 413 | } 414 | } 415 | 416 | /// A picked object holding the [value] (never null) and giving access to useful parsing functions 417 | class RequiredPick extends Pick { 418 | RequiredPick( 419 | // using dynamic here to match the return type of jsonDecode 420 | dynamic value, { 421 | List path = const [], 422 | Map? context, 423 | }) : value = value as Object, 424 | super(value, path: path, context: context); 425 | 426 | @override 427 | // ignore: overridden_fields 428 | covariant Object value; 429 | 430 | @override 431 | @Deprecated('Use asStringOrNull() to pick a String value') 432 | String toString() => 'RequiredPick(value=$value, path=$path)'; 433 | 434 | Pick nullable() => Pick(value, path: path, context: context); 435 | 436 | @override 437 | RequiredPick withContext(String key, Object? value) { 438 | super.withContext(key, value); 439 | return this; 440 | } 441 | } 442 | 443 | /// Used internally with [PickContext.withContext] to add additional information 444 | /// to the error message 445 | const requiredPickErrorHintKey = '_required_pick_error_hint'; 446 | 447 | class PickException implements Exception { 448 | PickException(this.message); 449 | 450 | final String message; 451 | 452 | @override 453 | String toString() { 454 | return 'PickException($message)'; 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /lib/src/pick_bool.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension BoolPick on Pick { 4 | /// Returns the picked [value] as [bool] 5 | /// 6 | /// {@template Pick.asBool} 7 | /// Only the exact Strings "true" and "false" are valid boolean 8 | /// representations. Other concepts of booleans such as `1` and `0`, 9 | /// or "YES" and "NO" are not supported. 10 | /// 11 | /// Use `.let()` to parse those custom representations 12 | /// ```dart 13 | /// pick(1).letOrNull((pick) { 14 | /// if (pick.value == 1) { 15 | /// return true; 16 | /// } 17 | /// if (pick.value == 0) { 18 | /// return false; 19 | /// } 20 | /// return null; 21 | /// }); 22 | /// ``` 23 | /// {@endtemplate} 24 | bool _parse() { 25 | final value = required().value; 26 | if (value is bool) { 27 | return value; 28 | } 29 | if (value is String) { 30 | if (value == 'true') return true; 31 | if (value == 'false') return false; 32 | } 33 | throw PickException( 34 | 'Type ${value.runtimeType} of $debugParsingExit can not be casted to bool', 35 | ); 36 | } 37 | 38 | /// Returns the picked [value] as [bool] or throws a [PickException] 39 | /// 40 | /// {@macro Pick.asBool} 41 | bool asBoolOrThrow() { 42 | withContext( 43 | requiredPickErrorHintKey, 44 | 'Use asBoolOrNull() when the value may be null/absent at some point (bool?).', 45 | ); 46 | return _parse(); 47 | } 48 | 49 | /// Returns the picked [value] as [bool] or `null` if it can't be interpreted 50 | /// as [bool]. 51 | /// 52 | /// {@macro Pick.asBool} 53 | bool? asBoolOrNull() { 54 | if (value == null) return null; 55 | try { 56 | return _parse(); 57 | } catch (_) { 58 | return null; 59 | } 60 | } 61 | 62 | /// Returns the picked [value] as [bool] or defaults to `true` when the 63 | /// [value] is `null` or can't be interpreted as [bool]. 64 | /// 65 | /// {@macro Pick.asBool} 66 | bool asBoolOrTrue() { 67 | if (value == null) return true; 68 | try { 69 | return _parse(); 70 | } catch (_) { 71 | return true; 72 | } 73 | } 74 | 75 | /// Returns the picked [value] as [bool] or defaults to `false` when the 76 | /// [value] is `null` or can't be interpreted as [bool]. 77 | /// 78 | /// {@macro Pick.asBool} 79 | bool asBoolOrFalse() { 80 | if (value == null) return false; 81 | try { 82 | return _parse(); 83 | } catch (_) { 84 | return false; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib/src/pick_datetime.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: constant_identifier_names 2 | import 'package:deep_pick/src/pick.dart'; 3 | 4 | /// The format of the to-be-parsed String that will be converted to [DateTime] 5 | enum PickDateFormat { 6 | /// ISO 8601 is the most common data time representation 7 | /// 8 | /// https://www.w3.org/TR/NOTE-datetime 9 | /// 10 | /// Also covers [RFC-3339](https://datatracker.ietf.org/doc/html/rfc3339#section-5.6) 11 | /// 12 | /// Example: 13 | /// - `2005-08-15T15:52:01+0000` 14 | ISO_8601, 15 | 16 | /// A typical format used in the web that's not ISO8601 17 | /// 18 | /// [RFC-1123](http://tools.ietf.org/html/rfc1123) (specifically 19 | /// [RFC-5322 Section 3.3](https://datatracker.ietf.org/doc/html/rfc5322#section-3.3)) 20 | /// based on [RFC-822](https://datatracker.ietf.org/doc/html/rfc822) 21 | /// 22 | /// Used as 23 | /// - HTTP date header https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date, https://www.rfc-editor.org/rfc/rfc2616#section-3.3 24 | /// - RSS2 pubDate, lastBuildDate https://validator.w3.org/feed/docs/rss2.html 25 | /// 26 | /// Also matches [RFC 1036](https://datatracker.ietf.org/doc/html/rfc1036#section-2.1.2), which is just a specific version of RFC 822. 27 | /// 28 | /// Example: 29 | /// - `Date: Wed, 21 Oct 2015 07:28:00 GMT` 30 | RFC_1123, 31 | 32 | /// The C language `asctime()` date format, used as legacy format by HTTP date 33 | /// headers 34 | /// 35 | /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date 36 | /// See https://www.rfc-editor.org/rfc/rfc2616#section-3.3 37 | /// 38 | /// Example: 39 | /// - `Sun Nov 6 08:49:37 1994` 40 | /// - `Fri Feb 15 14:45:01 2013` 41 | ANSI_C_asctime, 42 | 43 | /// A valid but rarely used format for HTTP date headers, and cookies 44 | /// 45 | /// https://datatracker.ietf.org/doc/html/rfc850, obsolete by 46 | /// [RFC 1036](https://datatracker.ietf.org/doc/html/rfc1036) 47 | /// 48 | /// Note in particular that ctime format is not acceptable 49 | /// 50 | /// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date 51 | /// See https://www.rfc-editor.org/rfc/rfc2616#section-3.3 52 | /// 53 | /// Format: 54 | /// `Weekday, DD-Mon-YY HH:MM:SS TIMEZONE` 55 | /// 56 | /// Example: 57 | /// - `Thu Jan 1 00:00:00 1970` 58 | /// - `Sun, 6-Nov-94 08:49:37 GMT` 59 | /// - `Monday, 15-Aug-05 15:52:01 UTC` 60 | RFC_850, 61 | } 62 | 63 | extension NullableDateTimePick on Pick { 64 | /// Parses the picked non-null [value] as [DateTime] or throws 65 | /// 66 | /// {@template Pick.asDateTime} 67 | /// Tries to parse the most common date formats such as ISO 8601, RFC 3339, 68 | /// RFC 1123, RFC 5322 and ANSI C's asctime() 69 | /// 70 | /// Optionally accepts a [format] defining the exact to be parsed format. 71 | /// By default, all formats will be attempted 72 | /// 73 | /// Examples of parsable date formats: 74 | /// 75 | /// - `'2012-02-27 13:27:00'` 76 | /// - `'2012-02-27 13:27:00.123456z'` 77 | /// - `'2012-02-27 13:27:00,123456z'` 78 | /// - `'20120227 13:27:00'` 79 | /// - `'20120227T132700'` 80 | /// - `'20120227'` 81 | /// - `'+20120227'` 82 | /// - `'2012-02-27T14Z'` 83 | /// - `'2012-02-27T14+00:00'` 84 | /// - `'-123450101 00:00:00 Z'`: in the year -12345. 85 | /// - `'2002-02-27T14:00:00-0500'`: Same as `'2002-02-27T19:00:00Z'` 86 | /// - `'Thu, 1 Jan 1970 00:00:00 GMT'` 87 | /// - `'Thursday, 1-Jan-1970 00:00:00 GMT'` 88 | /// - `'Thu Jan 1 00:00:00 1970'` 89 | /// {@endtemplate} 90 | DateTime _parse({PickDateFormat? format}) { 91 | final value = required().value; 92 | if (value is DateTime) { 93 | return value; 94 | } 95 | 96 | final Map formats = { 97 | PickDateFormat.ISO_8601: _parseIso8601, 98 | PickDateFormat.RFC_1123: _parseRfc1123, 99 | PickDateFormat.RFC_850: _parseRfc850, 100 | PickDateFormat.ANSI_C_asctime: _parseAnsiCAsctime, 101 | }; 102 | 103 | if (format != null) { 104 | // Use one specific format 105 | final dateTime = formats[format]!(); 106 | if (dateTime != null) { 107 | return dateTime; 108 | } 109 | 110 | throw PickException( 111 | 'Type ${value.runtimeType} of $debugParsingExit can not be parsed as DateTime using $format', 112 | ); 113 | } 114 | 115 | // Try all available formats 116 | final errorsByFormat = {}; 117 | for (final entry in formats.entries) { 118 | try { 119 | final dateTime = entry.value(); 120 | if (dateTime != null) { 121 | return dateTime; 122 | } 123 | } catch (e) { 124 | errorsByFormat[entry.key] = e; 125 | } 126 | } 127 | 128 | throw PickException( 129 | 'Type ${value.runtimeType} of $debugParsingExit can not be parsed as DateTime. ' 130 | 'The different parsers produced the following errors: $errorsByFormat', 131 | ); 132 | } 133 | 134 | /// Parses the picked [value] as ISO 8601 String to [DateTime] or throws 135 | /// 136 | /// Shorthand for `.required().asDateTime()` 137 | /// 138 | /// {@macro Pick.asDateTime} 139 | DateTime asDateTimeOrThrow({PickDateFormat? format}) { 140 | withContext( 141 | requiredPickErrorHintKey, 142 | 'Use asDateTimeOrNull() when the value may be null/absent at some point (DateTime?).', 143 | ); 144 | return _parse(format: format); 145 | } 146 | 147 | /// Parses the picked [value] as ISO 8601 String to [DateTime] or returns `null` 148 | /// 149 | /// {@macro Pick.asDateTime} 150 | DateTime? asDateTimeOrNull({PickDateFormat? format}) { 151 | if (value == null) return null; 152 | try { 153 | return _parse(format: format); 154 | } catch (_) { 155 | return null; 156 | } 157 | } 158 | 159 | /// [PickDateFormat.ISO_8601] 160 | DateTime? _parseIso8601() { 161 | final value = required().value; 162 | if (value is! String) return null; 163 | 164 | // DartTime.tryParse() does not support timezones like EST, PDT, etc. 165 | // deep_pick takes care of the time zone and DartTime.parse() of the rest 166 | final trimmedValue = value.trim(); 167 | final timeZoneComponent = 168 | RegExp(r'(?<=[\d\W])[a-zA-Z]+$').firstMatch(trimmedValue)?.group(0); 169 | 170 | if (timeZoneComponent == null) { 171 | // no timeZoneComponent, DartTime can 100% parse it 172 | return DateTime.tryParse(trimmedValue); 173 | } 174 | 175 | final timeZoneOffset = _parseTimeZoneOffset(timeZoneComponent); 176 | // Remove the timezone from the string and add Z, so that it's parsed as UTC 177 | final withoutTimezone = 178 | '${trimmedValue.substring(0, trimmedValue.length - timeZoneComponent.length)}Z'; 179 | // combine both again 180 | return DateTime.tryParse(withoutTimezone)?.add(timeZoneOffset); 181 | } 182 | 183 | /// [PickDateFormat.RFC_1123] 184 | DateTime? _parseRfc1123() { 185 | final value = required().value; 186 | if (value is! String) return null; 187 | // not using HttpDate.parse because it is not available in the browsers 188 | try { 189 | final rfc1123Regex = RegExp( 190 | r'^\s*(\S{3}),\s*(\d+)\s*(\S{3})\s*(\d+)\s+(\d+):(\d+):(\d+)\s*([\w+-]+)\s*', 191 | ); 192 | final match = rfc1123Regex.firstMatch(value)!; 193 | final day = int.parse(match.group(2)!); 194 | final month = _months[match.group(3)!]!; 195 | final year = _normalizeYear(int.parse(match.group(4)!)); 196 | final hour = int.parse(match.group(5)!); 197 | final minute = int.parse(match.group(6)!); 198 | final seconds = int.parse(match.group(7)!); 199 | final timezone = match.group(8); 200 | final timeZoneOffset = _parseTimeZoneOffset(timezone); 201 | return DateTime.utc(year, month, day, hour, minute, seconds) 202 | .add(timeZoneOffset); 203 | } catch (_) { 204 | return null; 205 | } 206 | } 207 | 208 | /// [PickDateFormat.ANSI_C_asctime] 209 | DateTime? _parseAnsiCAsctime() { 210 | final value = required().value; 211 | if (value is! String) return null; 212 | try { 213 | final asctimeRegex = 214 | RegExp(r'^\s*(\S{3})\s+(\S{3})\s*(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)'); 215 | final match = asctimeRegex.firstMatch(value)!; 216 | final month = _months[match.group(2)!]!; 217 | final day = int.parse(match.group(3)!); 218 | final hour = int.parse(match.group(4)!); 219 | final minute = int.parse(match.group(5)!); 220 | final seconds = int.parse(match.group(6)!); 221 | final year = int.parse(match.group(7)!); 222 | return DateTime.utc(year, month, day, hour, minute, seconds); 223 | } catch (_) { 224 | return null; 225 | } 226 | } 227 | 228 | /// [PickDateFormat.RFC_850] 229 | DateTime? _parseRfc850() { 230 | final value = required().value; 231 | if (value is! String) return null; 232 | try { 233 | final rfc850Regex = RegExp( 234 | r'^\s*(\S+),\s*(\d+)-(\S{3})-(\d+)\s+(\d+):(\d+):(\d+)\s*([\w+-]+)\s*', 235 | ); 236 | final match = rfc850Regex.firstMatch(value)!; 237 | final day = int.parse(match.group(2)!); 238 | final month = _months[match.group(3)!]!; 239 | final year = _normalizeYear(int.parse(match.group(4)!)); 240 | final hour = int.parse(match.group(5)!); 241 | final minute = int.parse(match.group(6)!); 242 | final seconds = int.parse(match.group(7)!); 243 | final timezone = match.group(8); 244 | final timeZoneOffset = _parseTimeZoneOffset(timezone); 245 | return DateTime.utc(year, month, day, hour, minute, seconds) 246 | .add(timeZoneOffset); 247 | } catch (_) { 248 | return null; 249 | } 250 | } 251 | } 252 | 253 | const _months = { 254 | 'Jan': 1, 255 | 'Feb': 2, 256 | 'Mar': 3, 257 | 'Apr': 4, 258 | 'May': 5, 259 | 'Jun': 6, 260 | 'Jul': 7, 261 | 'Aug': 8, 262 | 'Sep': 9, 263 | 'Oct': 10, 264 | 'Nov': 11, 265 | 'Dec': 12, 266 | }; 267 | 268 | /// This returns a 4-digit year from 2-digit input 269 | /// 270 | /// For years 0-49 it returns 2000-2049 271 | /// For years 50-99 it returns 1950-1999 272 | /// 273 | /// Logic taken from: 274 | /// https://www.ietf.org/rfc/rfc2822.txt 275 | int _normalizeYear(int year) { 276 | if (year < 100) { 277 | if (year < 50) { 278 | return 2000 + year; 279 | } else { 280 | return 1900 + year; 281 | } 282 | } 283 | return year; 284 | } 285 | 286 | /// The Duration to add to a DateTime to get the correct time in UTC 287 | /// 288 | /// Handles timezone abbreviations (GMT, EST, ...) and offsets (+0400, -0130) 289 | Duration _parseTimeZoneOffset(String? timeZone) { 290 | if (timeZone == null) { 291 | return Duration.zero; 292 | } 293 | if (RegExp(r'^[+-]\d{4}$').hasMatch(timeZone)) { 294 | // matches format +0000 or -0000 295 | final sign = timeZone[0] == '-' ? 1 : -1; 296 | final hours = timeZone.substring(1, 3); 297 | final minutes = timeZone.substring(3, 5); 298 | return Duration( 299 | hours: int.parse(hours) * sign, 300 | minutes: int.parse(minutes) * sign, 301 | ); 302 | } 303 | // do a simple lookup 304 | final timeZoneOffset = _timeZoneOffsets[timeZone.toUpperCase()]; 305 | if (timeZoneOffset == null) { 306 | throw PickException('Unknown time zone abbrevation $timeZone'); 307 | } 308 | return timeZoneOffset; 309 | } 310 | 311 | /// Incomplete list of time zone abbreviations and their offsets towards UTC 312 | /// 313 | /// Those are the most common used. Please open a PR if you need more. 314 | const Map _timeZoneOffsets = { 315 | 'M': Duration(hours: -12), 316 | 'A': Duration(hours: -1), 317 | 'UT': Duration.zero, 318 | 'GMT': Duration.zero, 319 | 'Z': Duration.zero, 320 | 'N': Duration(hours: 1), 321 | 'EST': Duration(hours: 5), 322 | 'EDT': Duration(hours: 5), 323 | 'CST': Duration(hours: 6), 324 | 'CDT': Duration(hours: 6), 325 | 'MST': Duration(hours: 7), 326 | 'MDT': Duration(hours: 7), 327 | 'PST': Duration(hours: 8), 328 | 'PDT': Duration(hours: 8), 329 | 'Y': Duration(hours: 12), 330 | }; 331 | -------------------------------------------------------------------------------- /lib/src/pick_double.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension NullableDoublePick on Pick { 4 | /// Returns the picked [value] as [double] 5 | /// 6 | /// {@template Pick.asDouble} 7 | /// Parses the picked value as [double]. Also tries to parse [String] as [double] 8 | /// via [double.tryParse] 9 | /// {@endtemplate} 10 | double _parse() { 11 | final value = required().value; 12 | if (value is double) { 13 | return value; 14 | } 15 | if (value is num) { 16 | return value.toDouble(); 17 | } 18 | if (value is String) { 19 | var parsed = double.tryParse(value); 20 | if (parsed != null) { 21 | return parsed; 22 | } 23 | // remove all spaces 24 | final prepared = value.replaceAll(' ', ''); 25 | 26 | if (prepared.contains(',') && !prepared.contains('.')) { 27 | // Germans use , instead of . as decimal separator 28 | // 12,56 -> 12.56 29 | parsed = double.tryParse(prepared.replaceAll(',', '.')); 30 | if (parsed != null) { 31 | return parsed; 32 | } 33 | } 34 | 35 | // handle digit group separators 36 | final firstDot = prepared.indexOf('.'); 37 | final firstComma = prepared.indexOf(','); 38 | 39 | if (firstDot <= firstComma) { 40 | // the germans again 41 | // 10.000,00 42 | parsed = 43 | double.tryParse(prepared.replaceAll('.', '').replaceAll(',', '.')); 44 | if (parsed != null) { 45 | return parsed; 46 | } 47 | } else { 48 | // 10,000.00 49 | parsed = double.tryParse(prepared.replaceAll(',', '')); 50 | if (parsed != null) { 51 | return parsed; 52 | } 53 | } 54 | } 55 | throw PickException( 56 | 'Type ${value.runtimeType} of $debugParsingExit can not be parsed as double', 57 | ); 58 | } 59 | 60 | /// Returns the picked [value] as [double] or throws 61 | /// 62 | /// {@macro Pick.asDouble} 63 | double asDoubleOrThrow() { 64 | withContext( 65 | requiredPickErrorHintKey, 66 | 'Use asDoubleOrNull() when the value may be null/absent at some point (double?).', 67 | ); 68 | return _parse(); 69 | } 70 | 71 | /// Returns the picked [value] as [double?] or returns `null` when the picked 72 | /// value is absent 73 | /// 74 | /// {@macro Pick.asDouble} 75 | double? asDoubleOrNull() { 76 | if (value == null) return null; 77 | try { 78 | return _parse(); 79 | } catch (_) { 80 | return null; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/src/pick_int.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension NullableIntPick on Pick { 4 | /// Returns the picked [value] as [int] 5 | /// 6 | /// {@template Pick.asInt} 7 | /// Parses the picked value as [int]. Also tries to parse [String] as [int] 8 | /// Set [roundDouble] to round [double] to [int] 9 | /// Set [truncateDouble] to cut off decimals 10 | /// [roundDouble] and [truncateDouble] can not be true at the same time 11 | /// via [int.tryParse] 12 | /// {@endtemplate} 13 | int _parse(bool roundDouble, bool truncateDouble) { 14 | final value = required().value; 15 | if (roundDouble && truncateDouble) { 16 | throw PickException( 17 | '[roundDouble] and [truncateDouble] can not be true at the same time', 18 | ); 19 | } 20 | if (value is int) { 21 | return value; 22 | } 23 | if (value is num && roundDouble) { 24 | return value.round(); 25 | } 26 | if (value is num && truncateDouble) { 27 | return value.toInt(); 28 | } 29 | if (value is String) { 30 | final parsed = int.tryParse(value); 31 | if (parsed != null) { 32 | return parsed; 33 | } 34 | } 35 | 36 | throw PickException( 37 | 'Type ${value.runtimeType} of $debugParsingExit can not be parsed as int, set [roundDouble] or [truncateDouble] to parse from double', 38 | ); 39 | } 40 | 41 | /// Returns the picked [value] as [int] or throws 42 | /// 43 | /// {@macro Pick.asInt} 44 | int asIntOrThrow({bool roundDouble = false, bool truncateDouble = false}) { 45 | withContext( 46 | requiredPickErrorHintKey, 47 | 'Use asIntOrNull() when the value may be null/absent at some point (int?).', 48 | ); 49 | return _parse(roundDouble, truncateDouble); 50 | } 51 | 52 | /// Returns the picked [value] as [int?] or returns `null` when the picked 53 | /// value is absent 54 | /// 55 | /// {@macro Pick.asInt} 56 | int? asIntOrNull({bool roundDouble = false, bool truncateDouble = false}) { 57 | if (value == null) return null; 58 | try { 59 | return _parse(roundDouble, truncateDouble); 60 | } catch (_) { 61 | return null; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/src/pick_let.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension Let on RequiredPick { 4 | /// Maps the pick and returns the result 5 | /// 6 | /// This allows writing parsing logic from left to right without nesting 7 | /// 8 | /// Example: 9 | /// 10 | /// ``` 11 | /// // with .let 12 | /// User user = pick(json, 'users', 0).required().let((pick) => User.fromJson(pick.asMap())); 13 | /// 14 | /// // without .let 15 | /// User user = User.fromJson(pick(json, 'users', 0).required().asMap()); 16 | /// 17 | /// ``` 18 | R let(R Function(RequiredPick pick) block) { 19 | return block(this); 20 | } 21 | } 22 | 23 | extension NullableLet on Pick { 24 | /// Maps the non-null [value] and returns the result. Throws when [value] == null 25 | /// 26 | /// Shorthand for `.required().let(mapFn)` 27 | /// 28 | /// This methods allows mapping values in a single line 29 | /// 30 | /// Example: 31 | /// 32 | /// ``` 33 | /// // with letOrThrow 34 | /// User user = 35 | /// pick(json, 'users', 0).letOrThrow((pick) => User.fromJson(pick.asMap())); 36 | /// ``` 37 | R letOrThrow(R Function(RequiredPick pick) block) { 38 | withContext( 39 | requiredPickErrorHintKey, 40 | 'Use letOrNull() when the value may be null/absent at some point.', 41 | ); 42 | return block(required()); 43 | } 44 | 45 | /// Maps the pick if [value] != null and returns the result. 46 | /// 47 | /// This methods allows mapping of optional values in a single line 48 | /// 49 | /// Example: 50 | /// 51 | /// ``` 52 | /// // with letOrNull 53 | /// User? user = pick(json, 'users', 0).letOrNull((pick) => User.fromJson(pick.asMap())); 54 | /// 55 | /// // traditionally 56 | /// Pick pick = pick(json, 'users', 0); 57 | /// User? user; 58 | /// if (pick.value != null) { 59 | /// user = User.fromJson(pick.asMap()); 60 | /// } 61 | /// ``` 62 | R? letOrNull(R? Function(RequiredPick pick) block) { 63 | if (value == null) return null; 64 | return block(required()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/src/pick_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension NullableListPick on Pick { 4 | /// Returns the items of the [List] as list, mapped each item with [map] 5 | /// 6 | /// {@template Pick.asList} 7 | /// Each item in the list gets mapped, even when the list contains `null` 8 | /// values. To simplify the api, only non-null values get mapped with the 9 | /// [map] function. By default, `null` values are ignored. To explicitly 10 | /// map `null` values, use the [whenNull] mapping function. 11 | /// 12 | /// ```dart 13 | /// final persons = pick([ 14 | /// {'name': 'John Snow'}, 15 | /// {'name': 'Daenerys Targaryen'}, 16 | /// null, // <-- valid value 17 | /// ]).asListOrThrow( 18 | /// (pick) => Person.fromPick(pick), 19 | /// whenNull: (it) => null, 20 | /// ) 21 | /// 22 | /// // persons 23 | /// [ 24 | /// Person(name: 'John Snow'), 25 | /// Person(name: 'Daenerys Targaryen'), 26 | /// null, 27 | /// ] 28 | /// ``` 29 | /// 30 | /// For some apis it is important to get the access to the [index] of an 31 | /// element in the list. Access it via [index] which is only available for 32 | /// list elements, otherwise `null`. 33 | /// 34 | /// Usage: 35 | /// 36 | /// ```dart 37 | /// pick(["John", "Paul", "George", "Ringo"]).asListOrThrow((pick) { 38 | /// final index = pick.index!; 39 | /// return Artist(id: index, name: pick.asStringOrThrow()); 40 | /// ); 41 | /// ``` 42 | /// {@endtemplate} 43 | List _parse( 44 | T Function(RequiredPick) map, { 45 | T Function(Pick pick)? whenNull, 46 | }) { 47 | final value = required().value; 48 | if (value is List) { 49 | final result = []; 50 | var index = -1; 51 | for (final item in value) { 52 | index++; 53 | if (item != null) { 54 | final picked = 55 | RequiredPick(item, path: [...path, index], context: context); 56 | result.add(map(picked)); 57 | continue; 58 | } 59 | if (whenNull == null) { 60 | // skip null items when whenNull isn't provided 61 | continue; 62 | } 63 | try { 64 | final pick = Pick(null, path: [...path, index], context: context); 65 | result.add(whenNull(pick)); 66 | continue; 67 | } catch (e) { 68 | // ignore: avoid_print 69 | print( 70 | 'whenNull at location $debugParsingExit index: $index crashed instead of returning a $T', 71 | ); 72 | rethrow; 73 | } 74 | } 75 | return result; 76 | } 77 | throw PickException( 78 | 'Type ${value.runtimeType} of $debugParsingExit can not be casted to List', 79 | ); 80 | } 81 | 82 | /// Returns the picked [value] as [List]. This method throws when [value] is 83 | /// not a `List` or [isAbsent] 84 | /// 85 | /// {@macro Pick.asList} 86 | List asListOrThrow( 87 | T Function(RequiredPick) map, { 88 | T Function(Pick pick)? whenNull, 89 | }) { 90 | withContext( 91 | requiredPickErrorHintKey, 92 | 'Use asListOrEmpty()/asListOrNull() when the value may be null/absent at some point (List<$T>?).', 93 | ); 94 | return _parse(map, whenNull: whenNull); 95 | } 96 | 97 | /// Returns the picked [value] as [List] or an empty list when the `value` 98 | /// isn't a [List] or [isAbsent]. 99 | /// 100 | /// {@macro Pick.asList} 101 | List asListOrEmpty( 102 | T Function(RequiredPick) map, { 103 | T Function(Pick pick)? whenNull, 104 | }) { 105 | if (value == null) return []; 106 | if (value is! List) return []; 107 | return _parse(map, whenNull: whenNull); 108 | } 109 | 110 | /// Returns the picked [value] as [List] or null when the `value` 111 | /// isn't a [List] or [isAbsent]. 112 | /// 113 | /// {@macro Pick.asList} 114 | List? asListOrNull( 115 | T Function(RequiredPick) map, { 116 | T Function(Pick pick)? whenNull, 117 | }) { 118 | if (value == null) return null; 119 | if (value is! List) return null; 120 | return _parse(map, whenNull: whenNull); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/pick_map.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension NullableMapPick on Pick { 4 | /// Returns the picked [value] as [Map] 5 | /// 6 | /// {@template Pick.asMap} 7 | /// Provides a view of this map as having [RK] keys and [RV] instances, 8 | /// if necessary. 9 | /// 10 | /// If this map is already a `Map`, it is returned unchanged. 11 | /// 12 | /// If any operation exposes a non-[RK] key or non-[RV] value, 13 | /// the operation will throw instead. 14 | /// 15 | /// If any operation exposes a duplicate [RK] key [RV] value is replaced 16 | /// with new one, 17 | /// 18 | /// Entries added to the map must be valid for both a `Map` and a 19 | /// `Map`. 20 | /// via [Map] cast function 21 | /// 22 | /// ```dart 23 | /// final map = pick({ 24 | /// 'a': 'John Snow', 25 | /// 'b': 1, 26 | /// 'c': null, 27 | /// 'a': 'John Snow updated', 28 | /// }).asMapOrThrow() 29 | /// 30 | /// // map 31 | /// { 32 | /// 'a': 'John Snow updated', 33 | /// 'b': 1, 34 | /// 'c': null, 35 | /// } 36 | /// ``` 37 | /// {@endtemplate} 38 | Map _parse() { 39 | final value = required().value; 40 | if (value is Map) { 41 | final view = value.cast(); 42 | // create copy of casted view so all items are type checked here 43 | // and not lazily type checked when accessing them 44 | return Map.of(view); 45 | } 46 | throw PickException( 47 | 'Type ${value.runtimeType} of $debugParsingExit can not be casted to Map', 48 | ); 49 | } 50 | 51 | /// Returns the picked [value] as [Map]. This method throws when [value] is 52 | /// not a [Map] or [isAbsent] 53 | /// 54 | /// {@macro Pick.asMap} 55 | Map asMapOrThrow() { 56 | withContext( 57 | requiredPickErrorHintKey, 58 | 'Use asMapOrEmpty()/asMapOrNull() when the value may be null/absent at some point (Map<$RK, $RV>?).', 59 | ); 60 | return _parse(); 61 | } 62 | 63 | /// Returns the picked [value] as [Map] or an empty map when the `value` 64 | /// isn't a [Map] or [isAbsent]. 65 | /// 66 | /// {@macro Pick.asMap} 67 | Map asMapOrEmpty() { 68 | if (value == null) return {}; 69 | if (value is! Map) return {}; 70 | return _parse(); 71 | } 72 | 73 | /// Returns the picked [value] as [Map] or null when the `value` 74 | /// isn't a [Map] or [isAbsent]. 75 | /// 76 | /// {@macro Pick.asMap} 77 | Map? asMapOrNull() { 78 | if (value == null) return null; 79 | if (value is! Map) return null; 80 | return _parse(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/pick_string.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/src/pick.dart'; 2 | 3 | extension RequiredStringPick on RequiredPick { 4 | /// Returns the picked [value] as [String] representation 5 | /// 6 | /// {@macro Pick.asString} 7 | String asString() => _parse(); 8 | } 9 | 10 | extension NullableStringPick on Pick { 11 | /// Returns the picked [value] as [String] representation 12 | /// 13 | /// {@template Pick.asString} 14 | /// Parses the picked [value] as String. If the value is not already a [String] 15 | /// its [Object.toString()] will be called. This means that this method works 16 | /// for [int], [double] and any other [Object]. 17 | /// {@endtemplate} 18 | String _parse() { 19 | final value = required().value; 20 | if (value is String) { 21 | return value; 22 | } 23 | return value.toString(); 24 | } 25 | 26 | /// Returns the picked [value] as String representation; only throws a 27 | /// [PickException] when the value is `null` or [isAbsent]. 28 | /// 29 | /// {@macro Pick.asString} 30 | String asStringOrThrow() { 31 | withContext( 32 | requiredPickErrorHintKey, 33 | 'Use asStringOrNull() when the value may be null/absent at some point (String?).', 34 | ); 35 | return _parse(); 36 | } 37 | 38 | /// Returns the picked [value] as [String] or returns `null` when the picked value isn't available 39 | /// 40 | /// {@macro Pick.asString} 41 | String? asStringOrNull() { 42 | if (value == null) return null; 43 | try { 44 | return _parse(); 45 | } catch (_) { 46 | return null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: deep_pick 2 | description: Simplifies manual JSON parsing with a type-safe API. No dynamic, no manual casting. Flexible inputs types, fixed output types. Useful parsing error messages 3 | version: 1.1.0 4 | repository: https://github.com/passsy/deep_pick 5 | issue_tracker: https://github.com/passsy/deep_pick/issues 6 | 7 | environment: 8 | sdk: '>=2.12.0 <4.0.0' 9 | 10 | dev_dependencies: 11 | http: '>=0.13.0 <2.0.0' 12 | lint: '>=1.5.1 <3.0.0' 13 | test: ^1.16.0 14 | -------------------------------------------------------------------------------- /test/all_tests.dart: -------------------------------------------------------------------------------- 1 | import 'src/pick_bool_test.dart' as pick_bool_test; 2 | import 'src/pick_datetime_test.dart' as pick_datetime_test; 3 | import 'src/pick_double_test.dart' as pick_double_test; 4 | import 'src/pick_int_test.dart' as pick_int_test; 5 | import 'src/pick_let_test.dart' as pick_let_test; 6 | import 'src/pick_list_test.dart' as pick_list_test; 7 | import 'src/pick_map_test.dart' as pick_map_test; 8 | import 'src/pick_string_test.dart' as pick_string_test; 9 | import 'src/pick_test.dart' as pick_test; 10 | import 'src/required_pick_test.dart' as required_pick_test; 11 | 12 | void main() { 13 | pick_bool_test.main(); 14 | pick_datetime_test.main(); 15 | pick_double_test.main(); 16 | pick_int_test.main(); 17 | pick_let_test.main(); 18 | pick_list_test.main(); 19 | pick_map_test.main(); 20 | pick_string_test.main(); 21 | pick_test.main(); 22 | required_pick_test.main(); 23 | } 24 | -------------------------------------------------------------------------------- /test/src/pick_bool_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asBool*', () { 8 | test('asBoolOrNull()', () { 9 | expect(pick(true).asBoolOrNull(), isTrue); 10 | expect(pick('a').asBoolOrNull(), isNull); 11 | expect(nullPick().asBoolOrNull(), isNull); 12 | }); 13 | 14 | test('asBoolOrTrue()', () { 15 | expect(pick(true).asBoolOrTrue(), isTrue); 16 | expect(pick(false).asBoolOrTrue(), isFalse); 17 | expect(pick('a').asBoolOrTrue(), isTrue); 18 | expect(nullPick().asBoolOrTrue(), isTrue); 19 | }); 20 | 21 | test('asBoolOrFalse()', () { 22 | expect(pick(true).asBoolOrFalse(), isTrue); 23 | expect(pick(false).asBoolOrFalse(), isFalse); 24 | expect(pick('a').asBoolOrFalse(), isFalse); 25 | expect(nullPick().asBoolOrFalse(), isFalse); 26 | }); 27 | 28 | test('asBoolOrThrow()', () { 29 | expect(pick(true).asBoolOrThrow(), isTrue); 30 | expect(pick('true').asBoolOrThrow(), isTrue); 31 | expect(pick('false').asBoolOrThrow(), isFalse); 32 | expect( 33 | () => pick('Bubblegum').asBoolOrThrow(), 34 | throwsA( 35 | pickException(containing: ['String', 'Bubblegum', '', 'bool']), 36 | ), 37 | ); 38 | expect( 39 | () => nullPick().asBoolOrThrow(), 40 | throwsA( 41 | pickException( 42 | containing: ['unknownKey', 'asBoolOrNull', 'null', 'bool?'], 43 | ), 44 | ), 45 | ); 46 | }); 47 | }); 48 | 49 | group('pick().required().asBool*', () { 50 | test('asBoolOrNull()', () { 51 | expect(pick(true).required().asBoolOrNull(), isTrue); 52 | expect(pick('a').required().asBoolOrNull(), isNull); 53 | }); 54 | 55 | test('asBoolOrTrue()', () { 56 | expect(pick(true).required().asBoolOrTrue(), isTrue); 57 | expect(pick(false).required().asBoolOrTrue(), isFalse); 58 | expect(pick('a').required().asBoolOrTrue(), isTrue); 59 | }); 60 | 61 | test('asBoolOrFalse()', () { 62 | expect(pick(true).required().asBoolOrFalse(), isTrue); 63 | expect(pick(false).required().asBoolOrFalse(), isFalse); 64 | expect(pick('a').required().asBoolOrFalse(), isFalse); 65 | }); 66 | 67 | test('asBoolOrThrow()', () { 68 | expect(pick(true).required().asBoolOrThrow(), isTrue); 69 | expect(pick('true').required().asBoolOrThrow(), isTrue); 70 | expect(pick('false').required().asBoolOrThrow(), isFalse); 71 | expect( 72 | () => pick('Bubblegum').required().asBoolOrThrow(), 73 | throwsA( 74 | pickException(containing: ['String', 'Bubblegum', '', 'bool']), 75 | ), 76 | ); 77 | expect( 78 | () => nullPick().required().asBoolOrThrow(), 79 | throwsA( 80 | pickException(containing: ['unknownKey', 'absent']), 81 | ), 82 | ); 83 | }); 84 | }); 85 | } 86 | -------------------------------------------------------------------------------- /test/src/pick_datetime_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asDateTime*', () { 8 | group('asDateTimeOrThrow', () { 9 | test('parse String', () { 10 | expect( 11 | pick('2012-02-27 13:27:00').asDateTimeOrThrow(), 12 | DateTime(2012, 2, 27, 13, 27), 13 | ); 14 | expect( 15 | pick('2012-02-27 13:27:00.123456z').asDateTimeOrThrow(), 16 | DateTime.utc(2012, 2, 27, 13, 27, 0, 123, 456), 17 | ); 18 | }); 19 | 20 | test('parse DateTime', () { 21 | final time = DateTime.utc(2012, 2, 27, 13, 27, 0, 123, 456); 22 | expect(pick(time.toString()).asDateTimeOrThrow(), time); 23 | expect(pick(time).asDateTimeOrThrow(), time); 24 | }); 25 | 26 | test('null throws', () { 27 | expect( 28 | () => nullPick().asDateTimeOrThrow(), 29 | throwsA( 30 | pickException( 31 | containing: [ 32 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asDateTimeOrNull() when the value may be null/absent at some point (DateTime?).', 33 | ], 34 | ), 35 | ), 36 | ); 37 | }); 38 | 39 | test('wrong type throws', () { 40 | expect( 41 | () => pick(Object()).asDateTimeOrThrow(), 42 | throwsA( 43 | pickException( 44 | containing: [ 45 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as DateTime', 46 | ], 47 | ), 48 | ), 49 | ); 50 | expect( 51 | () => pick('Bubblegum').asDateTimeOrThrow(), 52 | throwsA( 53 | pickException(containing: ['String', 'Bubblegum', 'DateTime']), 54 | ), 55 | ); 56 | }); 57 | }); 58 | 59 | group('asDateTimeOrNull', () { 60 | test('parse String', () { 61 | expect( 62 | pick('2012-02-27 13:27:00').asDateTimeOrNull(), 63 | DateTime(2012, 2, 27, 13, 27), 64 | ); 65 | }); 66 | 67 | test('null returns null', () { 68 | expect(nullPick().asDateTimeOrNull(), isNull); 69 | }); 70 | 71 | test('wrong type returns null', () { 72 | expect(pick(Object()).asDateTimeOrNull(), isNull); 73 | }); 74 | }); 75 | }); 76 | 77 | group('pick().required().asDateTime*', () { 78 | group('asDateTimeOrThrow', () { 79 | test('parse String', () { 80 | expect( 81 | pick('2012-02-27 13:27:00').required().asDateTimeOrThrow(), 82 | DateTime(2012, 2, 27, 13, 27), 83 | ); 84 | expect( 85 | pick('2012-02-27 13:27:00.123456z').required().asDateTimeOrThrow(), 86 | DateTime.utc(2012, 2, 27, 13, 27, 0, 123, 456), 87 | ); 88 | }); 89 | 90 | test('parse DateTime', () { 91 | final time = DateTime.utc(2012, 2, 27, 13, 27, 0, 123, 456); 92 | expect(pick(time.toString()).required().asDateTimeOrThrow(), time); 93 | expect(pick(time).required().asDateTimeOrThrow(), time); 94 | }); 95 | 96 | test('null throws', () { 97 | expect( 98 | () => nullPick().required().asDateTimeOrThrow(), 99 | throwsA( 100 | pickException( 101 | containing: [ 102 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent.', 103 | ], 104 | ), 105 | ), 106 | ); 107 | }); 108 | 109 | test('wrong type throws', () { 110 | expect( 111 | () => pick(Object()).required().asDateTimeOrThrow(), 112 | throwsA( 113 | pickException( 114 | containing: [ 115 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as DateTime', 116 | ], 117 | ), 118 | ), 119 | ); 120 | expect( 121 | () => pick('Bubblegum').required().asDateTimeOrThrow(), 122 | throwsA( 123 | pickException(containing: ['String', 'Bubblegum', 'DateTime']), 124 | ), 125 | ); 126 | }); 127 | }); 128 | 129 | group('asDateTimeOrNull', () { 130 | test('parse String', () { 131 | expect( 132 | pick('2012-02-27 13:27:00').required().asDateTimeOrNull(), 133 | DateTime(2012, 2, 27, 13, 27), 134 | ); 135 | }); 136 | 137 | test('wrong type returns null', () { 138 | expect(pick(Object()).required().asDateTimeOrNull(), isNull); 139 | }); 140 | }); 141 | 142 | group('ISO 8601', () { 143 | group('official dart tests', () { 144 | test('1', () { 145 | final date = DateTime.utc(1999, DateTime.june, 11, 18, 46, 53); 146 | expect( 147 | pick('Fri, 11 Jun 1999 18:46:53 GMT').asDateTimeOrThrow(), 148 | date, 149 | ); 150 | expect( 151 | pick('Friday, 11-Jun-1999 18:46:53 GMT').asDateTimeOrThrow(), 152 | date, 153 | ); 154 | expect(pick('Fri Jun 11 18:46:53 1999').asDateTimeOrThrow(), date); 155 | }); 156 | 157 | test('2', () { 158 | // ignore: avoid_redundant_argument_values 159 | final date = DateTime.utc(1970, DateTime.january); 160 | expect( 161 | pick('Thu, 1 Jan 1970 00:00:00 GMT').asDateTimeOrThrow(), 162 | date, 163 | ); 164 | expect( 165 | pick('Thursday, 1-Jan-1970 00:00:00 GMT').asDateTimeOrThrow(), 166 | date, 167 | ); 168 | expect(pick('Thu Jan 1 00:00:00 1970').asDateTimeOrThrow(), date); 169 | }); 170 | 171 | test('3', () { 172 | final date = DateTime.utc(2012, DateTime.march, 5, 23, 59, 59); 173 | expect( 174 | pick('Mon, 5 Mar 2012 23:59:59 GMT').asDateTimeOrThrow(), 175 | date, 176 | ); 177 | expect( 178 | pick('Monday, 5-Mar-2012 23:59:59 GMT').asDateTimeOrThrow(), 179 | date, 180 | ); 181 | expect(pick('Mon Mar 5 23:59:59 2012').asDateTimeOrThrow(), date); 182 | }); 183 | }); 184 | 185 | test('parse DateTime with timezone +0230', () { 186 | const input = '2023-01-09T12:31:54+0230'; 187 | final time = DateTime.utc(2023, 01, 09, 10, 01, 54); 188 | expect(pick(input).asDateTimeOrThrow(), time); 189 | }); 190 | 191 | test('parse DateTime with timezone EST', () { 192 | const input = '2023-01-09T12:31:54EST'; 193 | final time = DateTime.utc(2023, 01, 09, 17, 31, 54); 194 | expect(pick(input).asDateTimeOrThrow(), time); 195 | }); 196 | 197 | test('allow starting and trailing whitespace', () { 198 | expect( 199 | pick(' 2023-01-09T12:31:54EST ').asDateTimeOrThrow(), 200 | DateTime.utc(2023, 01, 09, 17, 31, 54), 201 | ); 202 | 203 | expect( 204 | pick(' 2023-01-09T12:31:54+0230 ').asDateTimeOrThrow(), 205 | DateTime.utc(2023, 01, 09, 10, 01, 54), 206 | ); 207 | 208 | expect( 209 | pick('2023-01-09T12:31:54+0230 ').asDateTimeOrThrow(), 210 | DateTime.utc(2023, 01, 09, 10, 01, 54), 211 | ); 212 | 213 | expect( 214 | pick(' 2023-01-09T12:31:54+0230').asDateTimeOrThrow(), 215 | DateTime.utc(2023, 01, 09, 10, 01, 54), 216 | ); 217 | }); 218 | 219 | test('parse DateTime with timezone PDT', () { 220 | const input = '20230109T123154PDT'; 221 | final time = DateTime.utc(2023, 01, 09, 20, 31, 54); 222 | expect(pick(input).asDateTimeOrThrow(), time); 223 | }); 224 | 225 | group('explicit format uses only one parser', () { 226 | test( 227 | 'asDateTimeOrNull: ISO-8601 String ca not be parsed by ansi c asctime', 228 | () { 229 | const iso8601 = '2005-08-15T15:52:01+0000'; 230 | final value = pick(iso8601) 231 | .asDateTimeOrNull(format: PickDateFormat.ANSI_C_asctime); 232 | expect(value, isNull); 233 | }); 234 | test( 235 | 'asDateTimeOrThrow: ISO-8601 String can not be parsed by ansi c asctime', 236 | () { 237 | const iso8601 = '2005-08-15T15:52:01+0000'; 238 | expect( 239 | () => pick(iso8601) 240 | .asDateTimeOrThrow(format: PickDateFormat.ANSI_C_asctime), 241 | throwsA( 242 | pickException( 243 | containing: ['2005-08-15T15:52:01+0000', 'DateTime'], 244 | ), 245 | ), 246 | ); 247 | }); 248 | }); 249 | 250 | group('RFC 3339', () { 251 | test('parses the example date', () { 252 | final date = pick('2021-11-01T11:53:15+00:00') 253 | .asDateTimeOrThrow(format: PickDateFormat.ISO_8601); 254 | expect(date.day, equals(1)); 255 | expect(date.month, equals(DateTime.november)); 256 | expect(date.year, equals(2021)); 257 | expect(date.hour, equals(11)); 258 | expect(date.minute, equals(53)); 259 | expect(date.second, equals(15)); 260 | expect(date.timeZoneName, equals('UTC')); 261 | }); 262 | 263 | // examples from https://datatracker.ietf.org/doc/html/rfc3339#section-5.8 264 | test('rfc339 examples 1', () { 265 | final date = pick('1985-04-12T23:20:50.52Z').asDateTimeOrThrow(); 266 | expect(date.day, equals(12)); 267 | expect(date.month, equals(DateTime.april)); 268 | expect(date.year, equals(1985)); 269 | expect(date.hour, equals(23)); 270 | expect(date.minute, equals(20)); 271 | expect(date.second, equals(50)); 272 | expect(date.millisecond, equals(520)); 273 | expect(date.timeZoneName, equals('UTC')); 274 | }); 275 | test('rfc339 examples 2 - time zone', () { 276 | final date = pick('1996-12-19T16:39:57-08:00').asDateTimeOrThrow(); 277 | expect(date.day, equals(20)); 278 | expect(date.month, equals(DateTime.december)); 279 | expect(date.year, equals(1996)); 280 | expect(date.hour, equals(0)); 281 | expect(date.minute, equals(39)); 282 | expect(date.second, equals(57)); 283 | expect(date.millisecond, equals(0)); 284 | expect(date.timeZoneName, equals('UTC')); 285 | }); 286 | test('rfc339 examples 3 - leap second', () { 287 | final date = pick('1990-12-31T23:59:60Z').asDateTimeOrThrow(); 288 | expect(date.day, equals(1)); 289 | expect(date.month, equals(DateTime.january)); 290 | expect(date.year, equals(1991)); 291 | expect(date.hour, equals(0)); 292 | expect(date.minute, equals(0)); 293 | expect(date.second, equals(0)); 294 | expect(date.millisecond, equals(0)); 295 | expect(date.timeZoneName, equals('UTC')); 296 | 297 | // example 4 298 | // same leap second, different time zone 299 | final date2 = pick('1990-12-31T23:59:60Z').asDateTimeOrThrow(); 300 | expect(date2, date); 301 | }); 302 | }); 303 | }); 304 | 305 | group('RFC 1123', () { 306 | test('parses the example date', () { 307 | final date = pick('Sun, 06 Nov 1994 08:49:37 GMT') 308 | .asDateTimeOrThrow(format: PickDateFormat.RFC_1123); 309 | expect(date.day, equals(6)); 310 | expect(date.month, equals(DateTime.november)); 311 | expect(date.year, equals(1994)); 312 | expect(date.hour, equals(8)); 313 | expect(date.minute, equals(49)); 314 | expect(date.second, equals(37)); 315 | expect(date.timeZoneName, equals('UTC')); 316 | }); 317 | 318 | test('parse DateTime with timezone +0000', () { 319 | const input = 'Mon, 21 Nov 2021 11:53:15 +0000'; 320 | final time = DateTime.utc(2021, 11, 21, 11, 53, 15); 321 | expect(pick(input).asDateTimeOrThrow(), time); 322 | }); 323 | 324 | test('parse DateTime with timezone EST', () { 325 | const input = 'Mon, 11 Nov 24 11:58:15 EST'; 326 | final time = DateTime.utc(2024, 11, 11, 16, 58, 15); 327 | expect(pick(input).asDateTimeOrThrow(), time); 328 | }); 329 | 330 | test('parse DateTime with timezone PDT', () { 331 | const input = 'Mon, 01 Nov 99 11:53:11 PDT'; 332 | final time = DateTime.utc(1999, 11, 01, 19, 53, 11); 333 | expect(pick(input).asDateTimeOrThrow(), time); 334 | }); 335 | 336 | test('parse DateTime with timezone +0000', () { 337 | const input = 'Mon, 01 Jan 20 11:53:01 +0000'; 338 | final time = DateTime.utc(2020, 01, 01, 11, 53, 01); 339 | expect(pick(input).asDateTimeOrThrow(), time); 340 | }); 341 | 342 | test('parse DateTime with timezone +0730', () { 343 | const input = 'Mon, 01 Nov 21 11:53:15 +0730'; 344 | final time = DateTime.utc(2021, 11, 01, 04, 23, 15); 345 | expect(pick(input).asDateTimeOrThrow(), time); 346 | }); 347 | 348 | test('mozilla example', () { 349 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date 350 | expect( 351 | pick('Wed, 21 Oct 2015 07:28:00 GMT').asDateTimeOrThrow(), 352 | DateTime.utc(2015, 10, 21, 7, 28), 353 | ); 354 | }); 355 | 356 | test('be flexible on whitespace', () { 357 | expect( 358 | pick('Sun,06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 359 | DateTime.utc(1994, 11, 6, 8, 49, 37), 360 | ); 361 | 362 | expect( 363 | pick('Sun, 06Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 364 | DateTime.utc(1994, 11, 6, 8, 49, 37), 365 | ); 366 | 367 | expect( 368 | pick('Sun, 06 Nov1994 08:49:37 GMT').asDateTimeOrThrow(), 369 | DateTime.utc(1994, 11, 6, 8, 49, 37), 370 | ); 371 | 372 | expect( 373 | pick('Sun, 06 Nov 1994 08:49:37GMT').asDateTimeOrThrow(), 374 | DateTime.utc(1994, 11, 6, 8, 49, 37), 375 | ); 376 | 377 | expect( 378 | pick(' Sun,06Nov1994 08:49:37GMT ').asDateTimeOrThrow(), 379 | DateTime.utc(1994, 11, 6, 8, 49, 37), 380 | ); 381 | }); 382 | 383 | test('require whitespace', () { 384 | // year and minutes need to be separated 385 | expect( 386 | () => pick('Sun, 06 Nov 199408:49:37 GMT').asDateTimeOrThrow(), 387 | throwsA( 388 | pickException( 389 | containing: ['Sun, 06 Nov 199408:49:37 GMT', 'DateTime'], 390 | ), 391 | ), 392 | ); 393 | }); 394 | 395 | // Be flexible on input 396 | test('Do not require precise number lengths', () { 397 | // short day 398 | expect( 399 | pick('Sun, 6 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 400 | DateTime.utc(1994, 11, 6, 8, 49, 37), 401 | ); 402 | 403 | // short hour 404 | expect( 405 | pick('Sun, 06 Nov 1994 8:49:37 GMT').asDateTimeOrThrow(), 406 | DateTime.utc(1994, 11, 6, 8, 49, 37), 407 | ); 408 | 409 | // short min 410 | expect( 411 | pick('Sun, 06 Nov 1994 08:9:37 GMT').asDateTimeOrThrow(), 412 | DateTime.utc(1994, 11, 6, 8, 9, 37), 413 | ); 414 | 415 | // short seconds 416 | expect( 417 | pick('Sun, 06 Nov 1994 08:49:7 GMT').asDateTimeOrThrow(), 418 | DateTime.utc(1994, 11, 6, 8, 49, 7), 419 | ); 420 | }); 421 | 422 | test('accepts days out of month range', () { 423 | // negative days 424 | expect( 425 | pick('Sun, 00 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 426 | DateTime.utc(1994, 10, 31, 8, 49, 37), 427 | ); 428 | 429 | // day overlap 430 | expect( 431 | pick('Sun, 31 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 432 | DateTime.utc(1994, 12, 01, 8, 49, 37), 433 | ); 434 | 435 | // day overlap 436 | expect( 437 | pick('Sun, 32 Aug 1994 08:49:37 GMT').asDateTimeOrThrow(), 438 | DateTime.utc(1994, 09, 01, 8, 49, 37), 439 | ); 440 | 441 | // hours overlap 442 | expect( 443 | pick('Sun, 06 Nov 1994 24:49:37 GMT').asDateTimeOrThrow(), 444 | DateTime.utc(1994, 11, 07, 0, 49, 37), 445 | ); 446 | 447 | // minutes overlap 448 | expect( 449 | pick('Sun, 06 Nov 1994 08:60:37 GMT').asDateTimeOrThrow(), 450 | DateTime.utc(1994, 11, 06, 9, 00, 37), 451 | ); 452 | 453 | // seconds overlap 454 | expect( 455 | pick('Sun, 06 Nov 1994 08:49:60 GMT').asDateTimeOrThrow(), 456 | DateTime.utc(1994, 11, 06, 8, 50), 457 | ); 458 | }); 459 | 460 | test('only allows short weekday names', () { 461 | expect( 462 | () => pick('Sunday, 6 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 463 | throwsA( 464 | pickException( 465 | containing: ['Sunday, 6 Nov 1994 08:49:37 GMT', 'DateTime'], 466 | ), 467 | ), 468 | ); 469 | }); 470 | 471 | test('only allows short month names', () { 472 | expect( 473 | () => pick('Sun, 6 November 1994 08:49:37 GMT').asDateTimeOrThrow(), 474 | throwsA( 475 | pickException( 476 | containing: ['Sun, 6 November 1994 08:49:37 GMT', 'DateTime'], 477 | ), 478 | ), 479 | ); 480 | }); 481 | 482 | test('ignore whitespaces when possible', () { 483 | expect( 484 | pick('Sun, 6 Nov 1994 08:49:37 GMT ').asDateTimeOrThrow(), 485 | DateTime.utc(1994, 11, 6, 8, 49, 37), 486 | ); 487 | 488 | expect( 489 | pick('Sun, 06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 490 | DateTime.utc(1994, 11, 6, 8, 49, 37), 491 | ); 492 | 493 | expect( 494 | pick('Sun, 06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 495 | DateTime.utc(1994, 11, 6, 8, 49, 37), 496 | ); 497 | 498 | expect( 499 | pick('Sun, 06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 500 | DateTime.utc(1994, 11, 6, 8, 49, 37), 501 | ); 502 | 503 | expect( 504 | pick('Sun, 06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 505 | DateTime.utc(1994, 11, 6, 8, 49, 37), 506 | ); 507 | 508 | expect( 509 | pick('Sun, 06 Nov 1994 08:49:37 GMT').asDateTimeOrThrow(), 510 | DateTime.utc(1994, 11, 6, 8, 49, 37), 511 | ); 512 | }); 513 | }); 514 | 515 | group('RFC 850', () { 516 | test('parses the example date', () { 517 | final date = pick('Sunday, 06-Nov-94 08:49:37 GMT').asDateTimeOrThrow(); 518 | expect(date.day, equals(6)); 519 | expect(date.month, equals(DateTime.november)); 520 | expect(date.year, equals(1994)); 521 | expect(date.hour, equals(8)); 522 | expect(date.minute, equals(49)); 523 | expect(date.second, equals(37)); 524 | expect(date.timeZoneName, equals('UTC')); 525 | }); 526 | 527 | test('parse DateTime with timezone +0000', () { 528 | const input = 'Monday, 21-Nov-21 11:53:15 +0000'; 529 | final time = DateTime.utc(2021, 11, 21, 11, 53, 15); 530 | expect(pick(input).asDateTimeOrThrow(), time); 531 | }); 532 | 533 | test('parse DateTime with timezone EST', () { 534 | const input = 'Monday, 11-Nov-24 11:58:15 EST'; 535 | final time = DateTime.utc(2024, 11, 11, 16, 58, 15); 536 | expect(pick(input).asDateTimeOrThrow(), time); 537 | }); 538 | 539 | test('parse DateTime with timezone PDT', () { 540 | const input = 'Monday, 01-Nov-99 11:53:11 PDT'; 541 | final time = DateTime.utc(1999, 11, 01, 19, 53, 11); 542 | expect(pick(input).asDateTimeOrThrow(), time); 543 | }); 544 | 545 | test('parse DateTime with timezone +0000', () { 546 | const input = 'Monday, 01-Jan-20 11:53:01 +0000'; 547 | final time = DateTime.utc(2020, 01, 01, 11, 53, 01); 548 | expect(pick(input).asDateTimeOrThrow(), time); 549 | }); 550 | 551 | test('parse DateTime with timezone +0730', () { 552 | const input = 'Monday, 01-Nov-21 11:53:15 +0730'; 553 | final time = DateTime.utc(2021, 11, 01, 04, 23, 15); 554 | expect(pick(input).asDateTimeOrThrow(), time); 555 | }); 556 | 557 | test('require whitespace between year and hour', () { 558 | expect( 559 | () => pick('Sunday, 06-Nov-9408:49:37 GMT').asDateTimeOrThrow(), 560 | throwsA( 561 | pickException( 562 | containing: ['Sunday, 06-Nov-9408:49:37 GMT', 'DateTime'], 563 | ), 564 | ), 565 | ); 566 | }); 567 | 568 | test('be flexible on spacing', () { 569 | expect( 570 | pick('Sunday, 06-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 571 | DateTime.utc(1994, 11, 6, 8, 49, 37), 572 | ); 573 | 574 | expect( 575 | pick('Sunday, 06-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 576 | DateTime.utc(1994, 11, 6, 8, 49, 37), 577 | ); 578 | 579 | expect( 580 | pick('Sunday, 06-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 581 | DateTime.utc(1994, 11, 6, 8, 49, 37), 582 | ); 583 | 584 | expect( 585 | pick('Sunday,06-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 586 | DateTime.utc(1994, 11, 6, 8, 49, 37), 587 | ); 588 | 589 | expect( 590 | pick('Sunday, 06-Nov-94 08:49:37GMT').asDateTimeOrThrow(), 591 | DateTime.utc(1994, 11, 6, 8, 49, 37), 592 | ); 593 | }); 594 | 595 | test('Do not require precise number lengths', () { 596 | // short day 597 | expect( 598 | pick('Sunday, 6-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 599 | DateTime.utc(1994, 11, 6, 8, 49, 37), 600 | ); 601 | 602 | // short hour 603 | expect( 604 | pick('Sunday, 06-Nov-94 8:49:37 GMT').asDateTimeOrThrow(), 605 | DateTime.utc(1994, 11, 6, 8, 49, 37), 606 | ); 607 | 608 | // long year 609 | expect( 610 | pick('Sunday, 06-Nov-1994 08:49:37 GMT').asDateTimeOrThrow(), 611 | DateTime.utc(1994, 11, 6, 8, 49, 37), 612 | ); 613 | expect( 614 | pick('Sunday, 06-Nov-2018 08:49:37 GMT').asDateTimeOrThrow(), 615 | DateTime.utc(2018, 11, 6, 8, 49, 37), 616 | ); 617 | 618 | // short min 619 | expect( 620 | pick('Sunday, 06-Nov-94 08:9:37 GMT').asDateTimeOrThrow(), 621 | DateTime.utc(1994, 11, 6, 8, 9, 37), 622 | ); 623 | 624 | // short seconds 625 | expect( 626 | pick('Sunday, 06-Nov-94 08:49:7 GMT').asDateTimeOrThrow(), 627 | DateTime.utc(1994, 11, 6, 8, 49, 7), 628 | ); 629 | }); 630 | 631 | test('accepts invalid dates', () { 632 | // negative days 633 | expect( 634 | pick('Sunday, 00-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 635 | DateTime.utc(1994, 10, 31, 8, 49, 37), 636 | ); 637 | 638 | // day overlap 639 | expect( 640 | pick('Sunday, 31-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 641 | DateTime.utc(1994, 12, 01, 8, 49, 37), 642 | ); 643 | 644 | // day overlap 645 | expect( 646 | pick('Sunday, 32-Aug-94 08:49:37 GMT').asDateTimeOrThrow(), 647 | DateTime.utc(1994, 09, 01, 8, 49, 37), 648 | ); 649 | 650 | // hours overlap 651 | expect( 652 | pick('Sunday, 06-Nov-94 24:49:37 GMT').asDateTimeOrThrow(), 653 | DateTime.utc(1994, 11, 07, 0, 49, 37), 654 | ); 655 | 656 | // minutes overlap 657 | expect( 658 | pick('Sunday, 06-Nov-94 08:60:37 GMT').asDateTimeOrThrow(), 659 | DateTime.utc(1994, 11, 06, 9, 00, 37), 660 | ); 661 | 662 | // seconds overlap 663 | expect( 664 | pick('Sunday, 06-Nov-94 08:49:60 GMT').asDateTimeOrThrow(), 665 | DateTime.utc(1994, 11, 06, 8, 50), 666 | ); 667 | }); 668 | 669 | test('throws for unsupported timezones', () { 670 | expect( 671 | () => pick('2023-01-09T12:31:54ABC').asDateTimeOrThrow(), 672 | throwsA( 673 | pickException( 674 | containing: [ 675 | 'Type String of picked value "2023-01-09T12:31:54ABC"', 676 | 'Unknown time zone abbrevation ABC', 677 | ], 678 | ), 679 | ), 680 | ); 681 | 682 | expect( 683 | () => pick('Mon, 11 Nov 24 11:58:15 ESTX').asDateTimeOrThrow(), 684 | throwsA( 685 | pickException( 686 | containing: [ 687 | 'Type String of picked value "Mon, 11 Nov 24 11:58:15 ESTX"', 688 | 'Unknown time zone abbrevation ESTX', 689 | ], 690 | ), 691 | ), 692 | ); 693 | }); 694 | 695 | test('short weekday names are ok', () { 696 | expect( 697 | pick('Sun, 6-Nov-94 08:49:37 GMT').asDateTimeOrThrow(), 698 | DateTime.utc(1994, 11, 6, 8, 49, 37), 699 | ); 700 | }); 701 | 702 | test('only allows short month names', () { 703 | expect( 704 | () => pick('Sunday, 6-November-94 08:49:37 GMT').asDateTimeOrThrow(), 705 | throwsA( 706 | pickException( 707 | containing: ['Sunday, 6-November-94 08:49:37 GMT', 'DateTime'], 708 | ), 709 | ), 710 | ); 711 | }); 712 | 713 | test('allow trailing whitespace', () { 714 | expect( 715 | pick('Sunday, 6-Nov-94 08:49:37 GMT ').asDateTimeOrThrow(), 716 | DateTime.utc(1994, 11, 6, 8, 49, 37), 717 | ); 718 | }); 719 | }); 720 | 721 | group('asctime()', () { 722 | test('parses the example date', () { 723 | final date = pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(); 724 | expect(date.day, equals(6)); 725 | expect(date.month, equals(DateTime.november)); 726 | expect(date.year, equals(1994)); 727 | expect(date.hour, equals(8)); 728 | expect(date.minute, equals(49)); 729 | expect(date.second, equals(37)); 730 | expect(date.timeZoneName, equals('UTC')); 731 | }); 732 | 733 | test('parses a date with a two-digit day', () { 734 | final date = pick('Sun Nov 16 08:49:37 1994').asDateTimeOrThrow(); 735 | expect(date.day, equals(16)); 736 | expect(date.month, equals(DateTime.november)); 737 | expect(date.year, equals(1994)); 738 | expect(date.hour, equals(8)); 739 | expect(date.minute, equals(49)); 740 | expect(date.second, equals(37)); 741 | expect(date.timeZoneName, equals('UTC')); 742 | }); 743 | 744 | test('parses a date with a single-digit day', () { 745 | final date = pick('Sun Nov 1 08:49:37 1994').asDateTimeOrThrow(); 746 | expect(date.day, equals(1)); 747 | expect(date.month, equals(DateTime.november)); 748 | expect(date.year, equals(1994)); 749 | expect(date.hour, equals(8)); 750 | expect(date.minute, equals(49)); 751 | expect(date.second, equals(37)); 752 | expect(date.timeZoneName, equals('UTC')); 753 | }); 754 | 755 | test('whitespace is required', () { 756 | expect( 757 | () => pick('SunNov 6 08:49:37 1994').asDateTimeOrThrow(), 758 | throwsA( 759 | pickException(containing: ['SunNov 6 08:49:37 1994', 'DateTime']), 760 | ), 761 | ); 762 | 763 | expect( 764 | () => pick('Sun Nov 608:49:37 1994').asDateTimeOrThrow(), 765 | throwsA( 766 | pickException(containing: ['Sun Nov 608:49:37 1994', 'DateTime']), 767 | ), 768 | ); 769 | 770 | expect( 771 | () => pick('Sun Nov 6 08:49:371994').asDateTimeOrThrow(), 772 | throwsA( 773 | pickException(containing: ['Sun Nov 6 08:49:371994', 'DateTime']), 774 | ), 775 | ); 776 | }); 777 | 778 | test('be flexible on spacing', () { 779 | expect( 780 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(), 781 | DateTime.utc(1994, 11, 6, 8, 49, 37), 782 | ); 783 | 784 | expect( 785 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(), 786 | DateTime.utc(1994, 11, 6, 8, 49, 37), 787 | ); 788 | 789 | expect( 790 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(), 791 | DateTime.utc(1994, 11, 6, 8, 49, 37), 792 | ); 793 | 794 | expect( 795 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(), 796 | DateTime.utc(1994, 11, 6, 8, 49, 37), 797 | ); 798 | 799 | expect( 800 | pick('Sun Nov 6 08:49:37 1994').asDateTimeOrThrow(), 801 | DateTime.utc(1994, 11, 6, 8, 49, 37), 802 | ); 803 | 804 | expect( 805 | pick(' Sun Nov6 08:49:37 1994 ').asDateTimeOrThrow(), 806 | DateTime.utc(1994, 11, 6, 8, 49, 37), 807 | ); 808 | }); 809 | 810 | test('accepts invalid dates', () { 811 | // negative days 812 | expect( 813 | pick('Sun Nov 0 08:49:37 1994').asDateTimeOrThrow(), 814 | DateTime.utc(1994, 10, 31, 8, 49, 37), 815 | ); 816 | 817 | // day overlap 818 | expect( 819 | pick('Sun Nov 31 08:49:37 1994').asDateTimeOrThrow(), 820 | DateTime.utc(1994, 12, 01, 8, 49, 37), 821 | ); 822 | 823 | // day overlap 824 | expect( 825 | pick('Sun Aug 32 08:49:37 1994').asDateTimeOrThrow(), 826 | DateTime.utc(1994, 09, 01, 8, 49, 37), 827 | ); 828 | 829 | // hours overlap 830 | expect( 831 | pick('Sun Nov 6 24:49:37 1994').asDateTimeOrThrow(), 832 | DateTime.utc(1994, 11, 07, 0, 49, 37), 833 | ); 834 | 835 | // minutes overlap 836 | expect( 837 | pick('Sun Nov 6 08:60:37 1994').asDateTimeOrThrow(), 838 | DateTime.utc(1994, 11, 06, 9, 00, 37), 839 | ); 840 | 841 | // seconds overlap 842 | expect( 843 | pick('Sun Nov 6 08:49:60 1994').asDateTimeOrThrow(), 844 | DateTime.utc(1994, 11, 06, 8, 50), 845 | ); 846 | }); 847 | 848 | test('only allows short weekday names', () { 849 | expect( 850 | () => pick('Sunday Nov 0 08:49:37 1994').asDateTimeOrThrow(), 851 | throwsA( 852 | pickException( 853 | containing: ['Sunday Nov 0 08:49:37 1994', 'DateTime'], 854 | ), 855 | ), 856 | ); 857 | }); 858 | 859 | test('only allows short month names', () { 860 | expect( 861 | () => pick('Sun November 0 08:49:37 1994').asDateTimeOrThrow(), 862 | throwsA( 863 | pickException( 864 | containing: ['Sun November 0 08:49:37 1994', 'DateTime'], 865 | ), 866 | ), 867 | ); 868 | }); 869 | 870 | test('disallows trailing whitespace', () { 871 | expect( 872 | () => pick('Sun November 0 08:49:37 1994 ').asDateTimeOrThrow(), 873 | throwsA( 874 | pickException( 875 | containing: ['Sun November 0 08:49:37 1994 ', 'DateTime'], 876 | ), 877 | ), 878 | ); 879 | }); 880 | }); 881 | 882 | group('RFC 1026', () { 883 | test('parse DateTime with timezone +0100', () { 884 | //RFC 103 Wdy, DD Mon YY HH:MM:SS TIMEZONE 885 | const input = 'Tue, 09 Jan 23 22:14:02 +0100'; 886 | expect( 887 | pick(input).asDateTimeOrThrow(format: PickDateFormat.RFC_1123), 888 | DateTime.utc(2023, 01, 09, 21, 14, 02), 889 | ); 890 | }); 891 | }); 892 | 893 | group('RFC 2822', () { 894 | test('parse date from RFC', () { 895 | const input = 'Fri, 21 Nov 1997 09:55:06 -0600'; 896 | expect( 897 | pick(input).asDateTimeOrThrow(format: PickDateFormat.RFC_1123), 898 | DateTime.utc(1997, 11, 21, 15, 55, 06), 899 | ); 900 | }); 901 | }); 902 | }); 903 | } 904 | -------------------------------------------------------------------------------- /test/src/pick_double_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asDouble*', () { 8 | group('asDoubleOrThrow', () { 9 | test('parse double', () { 10 | expect(pick(1.0).asDoubleOrThrow(), 1.0); 11 | expect(pick(double.infinity).asDoubleOrThrow(), double.infinity); 12 | }); 13 | 14 | test('parse int', () { 15 | expect(pick(1).asDoubleOrThrow(), 1.0); 16 | }); 17 | test('parse int String', () { 18 | expect(pick('1').asDoubleOrThrow(), 1.0); 19 | }); 20 | 21 | test('parse double String', () { 22 | expect(pick('1.0').asDoubleOrThrow(), 1.0); 23 | expect(pick('25.4634').asDoubleOrThrow(), 25.4634); 24 | expect(pick('12345.01').asDoubleOrThrow(), 12345.01); 25 | }); 26 | 27 | test('parse german doubles', () { 28 | expect(pick('1,0').asDoubleOrThrow(), 1.0); 29 | expect(pick('12345,01').asDoubleOrThrow(), 12345.01); 30 | }); 31 | 32 | test('parse double with separators', () { 33 | expect(pick('12,345.01').asDoubleOrThrow(), 12345.01); 34 | expect(pick('12 345,01').asDoubleOrThrow(), 12345.01); 35 | expect(pick('12.345,01').asDoubleOrThrow(), 12345.01); 36 | }); 37 | 38 | test('null throws', () { 39 | expect( 40 | () => nullPick().asDoubleOrThrow(), 41 | throwsA( 42 | pickException( 43 | containing: [ 44 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asDoubleOrNull() when the value may be null/absent at some point (double?).', 45 | ], 46 | ), 47 | ), 48 | ); 49 | }); 50 | 51 | test('wrong type throws', () { 52 | expect( 53 | () => pick(Object()).asDoubleOrThrow(), 54 | throwsA( 55 | pickException( 56 | containing: [ 57 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as double', 58 | ], 59 | ), 60 | ), 61 | ); 62 | 63 | expect( 64 | () => pick('Bubblegum').asDoubleOrThrow(), 65 | throwsA(pickException(containing: ['String', 'Bubblegum', 'double'])), 66 | ); 67 | }); 68 | }); 69 | 70 | group('asDoubleOrNull', () { 71 | test('parse String', () { 72 | expect(pick('2012').asDoubleOrNull(), 2012); 73 | }); 74 | 75 | test('null returns null', () { 76 | expect(nullPick().asDoubleOrNull(), isNull); 77 | }); 78 | 79 | test('wrong type returns null', () { 80 | expect(pick(Object()).asDoubleOrNull(), isNull); 81 | }); 82 | }); 83 | }); 84 | 85 | group('pick().required().asDouble*', () { 86 | group('asDoubleOrThrow', () { 87 | test('parse double', () { 88 | expect(pick(1.0).required().asDoubleOrThrow(), 1.0); 89 | expect( 90 | pick(double.infinity).required().asDoubleOrThrow(), 91 | double.infinity, 92 | ); 93 | }); 94 | 95 | test('parse int', () { 96 | expect(pick(1).required().asDoubleOrThrow(), 1.0); 97 | }); 98 | test('parse int String', () { 99 | expect(pick('1').required().asDoubleOrThrow(), 1.0); 100 | }); 101 | 102 | test('parse double String', () { 103 | expect(pick('1.0').required().asDoubleOrThrow(), 1.0); 104 | expect(pick('25.4634').required().asDoubleOrThrow(), 25.4634); 105 | expect(pick('12345.01').required().asDoubleOrThrow(), 12345.01); 106 | }); 107 | 108 | test('parse german doubles', () { 109 | expect(pick('1,0').required().asDoubleOrThrow(), 1.0); 110 | expect(pick('12345,01').required().asDoubleOrThrow(), 12345.01); 111 | }); 112 | 113 | test('parse double with separators', () { 114 | expect(pick('12,345.01').required().asDoubleOrThrow(), 12345.01); 115 | expect(pick('12 345,01').required().asDoubleOrThrow(), 12345.01); 116 | expect(pick('12.345,01').required().asDoubleOrThrow(), 12345.01); 117 | }); 118 | 119 | test('null throws', () { 120 | expect( 121 | () => nullPick().required().asDoubleOrThrow(), 122 | throwsA( 123 | pickException( 124 | containing: [ 125 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent.', 126 | ], 127 | ), 128 | ), 129 | ); 130 | }); 131 | 132 | test('wrong type throws', () { 133 | expect( 134 | () => pick(Object()).required().asDoubleOrThrow(), 135 | throwsA( 136 | pickException( 137 | containing: [ 138 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as double', 139 | ], 140 | ), 141 | ), 142 | ); 143 | 144 | expect( 145 | () => pick('Bubblegum').required().asDoubleOrThrow(), 146 | throwsA(pickException(containing: ['String', 'Bubblegum', 'double'])), 147 | ); 148 | }); 149 | }); 150 | 151 | group('asDoubleOrNull', () { 152 | test('parse String', () { 153 | expect(pick('2012').required().asDoubleOrNull(), 2012); 154 | }); 155 | 156 | test('wrong type returns null', () { 157 | expect(pick(Object()).required().asDoubleOrNull(), isNull); 158 | }); 159 | }); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /test/src/pick_int_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asInt*', () { 8 | group('asIntOrThrow', () { 9 | test('parse Int', () { 10 | expect(pick(1).asIntOrThrow(), 1); 11 | expect(pick(35).asIntOrThrow(), 35); 12 | }); 13 | 14 | test('round double to int', () { 15 | expect(pick(1.234).asIntOrThrow(roundDouble: true), 1); 16 | expect(pick(12.945).asIntOrThrow(roundDouble: true), 13); 17 | }); 18 | 19 | test('truncate double to int', () { 20 | expect(pick(1.234).asIntOrThrow(truncateDouble: true), 1); 21 | expect(pick(12.945).asIntOrThrow(truncateDouble: true), 12); 22 | }); 23 | 24 | test('parse int String', () { 25 | expect(pick('1').asIntOrThrow(), 1); 26 | expect(pick('123').asIntOrThrow(), 123); 27 | }); 28 | 29 | test('round and truncate true at the same time throws', () { 30 | expect( 31 | () => pick(123).asIntOrThrow(roundDouble: true, truncateDouble: true), 32 | throwsA( 33 | pickException( 34 | containing: [ 35 | '[roundDouble] and [truncateDouble] can not be true at the same time', 36 | ], 37 | ), 38 | ), 39 | ); 40 | }); 41 | 42 | test('null throws', () { 43 | expect( 44 | () => nullPick().asIntOrThrow(), 45 | throwsA( 46 | pickException( 47 | containing: [ 48 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asIntOrNull() when the value may be null/absent at some point (int?).', 49 | ], 50 | ), 51 | ), 52 | ); 53 | }); 54 | 55 | test('wrong type throws', () { 56 | expect( 57 | () => pick(Object()).asIntOrThrow(), 58 | throwsA( 59 | pickException( 60 | containing: [ 61 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as int', 62 | ], 63 | ), 64 | ), 65 | ); 66 | 67 | expect( 68 | () => pick('Bubblegum').asIntOrThrow(), 69 | throwsA(pickException(containing: ['String', 'Bubblegum', 'int'])), 70 | ); 71 | }); 72 | }); 73 | 74 | group('asIntOrNull', () { 75 | test('parse String', () { 76 | expect(pick('2012').asIntOrNull(), 2012); 77 | }); 78 | 79 | test('null returns null', () { 80 | expect(nullPick().asIntOrNull(), isNull); 81 | }); 82 | 83 | test('wrong type returns null', () { 84 | expect(pick(Object()).asIntOrNull(), isNull); 85 | }); 86 | }); 87 | }); 88 | 89 | group('pick().required().asInt*', () { 90 | group('asIntOrThrow', () { 91 | test('parse Int', () { 92 | expect(pick(1).required().asIntOrThrow(), 1); 93 | expect(pick(35).required().asIntOrThrow(), 35); 94 | }); 95 | 96 | test('parse int String', () { 97 | expect(pick('1').required().asIntOrThrow(), 1); 98 | expect(pick('123').required().asIntOrThrow(), 123); 99 | }); 100 | 101 | test('null throws', () { 102 | expect( 103 | () => nullPick().required().asIntOrThrow(), 104 | throwsA( 105 | pickException( 106 | containing: [ 107 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent.', 108 | ], 109 | ), 110 | ), 111 | ); 112 | }); 113 | 114 | test('wrong type throws', () { 115 | expect( 116 | () => pick(Object()).required().asIntOrThrow(), 117 | throwsA( 118 | pickException( 119 | containing: [ 120 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be parsed as int', 121 | ], 122 | ), 123 | ), 124 | ); 125 | 126 | expect( 127 | () => pick('Bubblegum').required().asIntOrThrow(), 128 | throwsA(pickException(containing: ['String', 'Bubblegum', 'int'])), 129 | ); 130 | }); 131 | }); 132 | 133 | group('asIntOrNull', () { 134 | test('parse String', () { 135 | expect(pick('2012').required().asIntOrNull(), 2012); 136 | }); 137 | 138 | test('wrong type returns null', () { 139 | expect(pick(Object()).required().asIntOrNull(), isNull); 140 | }); 141 | }); 142 | }); 143 | } 144 | -------------------------------------------------------------------------------- /test/src/pick_let_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().let*', () { 8 | test('.required().let()', () { 9 | expect( 10 | pick({'name': 'John Snow'}) 11 | .required() 12 | .let((pick) => Person.fromPick(pick)), 13 | Person(name: 'John Snow'), 14 | ); 15 | expect( 16 | pick({'name': 'John Snow'}) 17 | .required() 18 | .let((pick) => Person.fromPick(pick)), 19 | Person(name: 'John Snow'), 20 | ); 21 | expect( 22 | () => nullPick().required().let((pick) => Person.fromPick(pick)), 23 | throwsA(pickException(containing: ['unknownKey', 'absent'])), 24 | ); 25 | }); 26 | 27 | test('letOrThrow()', () { 28 | expect( 29 | pick({'name': 'John Snow'}).letOrThrow((pick) => Person.fromPick(pick)), 30 | Person(name: 'John Snow'), 31 | ); 32 | expect( 33 | pick({'name': 'John Snow'}).letOrThrow((pick) => Person.fromPick(pick)), 34 | Person(name: 'John Snow'), 35 | ); 36 | expect( 37 | () => nullPick().letOrThrow((pick) => Person.fromPick(pick)), 38 | throwsA(pickException(containing: ['unknownKey', 'absent'])), 39 | ); 40 | }); 41 | 42 | test('letOrNull()', () { 43 | expect( 44 | pick({'name': 'John Snow'}).letOrNull((pick) => Person.fromPick(pick)), 45 | Person(name: 'John Snow'), 46 | ); 47 | expect(nullPick().letOrNull((pick) => Person.fromPick(pick)), isNull); 48 | // allow lambda to return null 49 | final String? a = pick('a').letOrNull((pick) => null); 50 | expect(a, isNull); 51 | expect(pick('a').letOrNull((pick) => null), isNull); 52 | expect( 53 | () => pick('a').letOrNull((pick) => Person.fromPick(pick)), 54 | throwsA( 55 | pickException( 56 | containing: [ 57 | 'Expected a non-null value but location "name" in pick(json, "name" (absent)) is absent.', 58 | ], 59 | ), 60 | ), 61 | ); 62 | expect( 63 | () => pick({'asdf': 'John Snow'}) 64 | .letOrNull((pick) => Person.fromPick(pick)), 65 | throwsA( 66 | pickException( 67 | containing: [ 68 | 'Expected a non-null value but location "name" in pick(json, "name" (absent)) is absent.', 69 | ], 70 | ), 71 | ), 72 | ); 73 | }); 74 | 75 | test('letOrThrow()', () { 76 | expect( 77 | pick({'name': 'John Snow'}).letOrThrow((pick) => Person.fromPick(pick)), 78 | Person(name: 'John Snow'), 79 | ); 80 | expect( 81 | () => nullPick().letOrThrow((pick) => Person.fromPick(pick)), 82 | throwsA( 83 | pickException( 84 | containing: [ 85 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use letOrNull() when the value may be null/absent at some point.', 86 | ], 87 | ), 88 | ), 89 | ); 90 | expect( 91 | () => pick({'asdf': 'John Snow'}) 92 | .letOrThrow((pick) => Person.fromPick(pick)), 93 | throwsA( 94 | pickException( 95 | containing: [ 96 | 'Expected a non-null value but location "name" in pick(json, "name" (absent)) is absent. Use letOrNull() when the value may be null/absent at some point', 97 | ], 98 | ), 99 | ), 100 | ); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /test/src/pick_list_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asList*', () { 8 | group('asListOrThrow', () { 9 | test('pipe through List', () { 10 | expect( 11 | pick([1, 2, 3]).asListOrThrow((it) => it.asIntOrThrow()), 12 | [1, 2, 3], 13 | ); 14 | }); 15 | 16 | test('null throws', () { 17 | expect( 18 | () => nullPick().asListOrThrow((it) => it.asStringOrThrow()), 19 | throwsA( 20 | pickException( 21 | containing: [ 22 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asListOrEmpty()/asListOrNull() when the value may be null/absent at some point (List?).', 23 | ], 24 | ), 25 | ), 26 | ); 27 | }); 28 | 29 | test('map empty list to empty list', () { 30 | expect( 31 | pick([]).asListOrThrow((pick) => Person.fromPick(pick)), 32 | [], 33 | ); 34 | }); 35 | 36 | test('map to List', () { 37 | expect( 38 | pick([ 39 | {'name': 'John Snow'}, 40 | {'name': 'Daenerys Targaryen'}, 41 | ]).asListOrThrow((pick) => Person.fromPick(pick)), 42 | [ 43 | Person(name: 'John Snow'), 44 | Person(name: 'Daenerys Targaryen'), 45 | ], 46 | ); 47 | }); 48 | 49 | test('map to List ignoring null values', () { 50 | expect( 51 | pick([ 52 | {'name': 'John Snow'}, 53 | {'name': 'Daenerys Targaryen'}, 54 | null, // <-- valid value 55 | ]).asListOrThrow((pick) => Person.fromPick(pick)), 56 | [ 57 | Person(name: 'John Snow'), 58 | Person(name: 'Daenerys Targaryen'), 59 | // not 3rd item 60 | ], 61 | ); 62 | }); 63 | 64 | test('map to List with whenNull', () { 65 | expect( 66 | pick([ 67 | {'name': 'John Snow'}, 68 | {'name': 'Daenerys Targaryen'}, 69 | null, // <-- valid value 70 | ]).asListOrThrow( 71 | (pick) => Person.fromPick(pick), 72 | whenNull: (it) => null, 73 | ), 74 | [ 75 | Person(name: 'John Snow'), 76 | Person(name: 'Daenerys Targaryen'), 77 | null, 78 | ], 79 | ); 80 | }); 81 | 82 | test('map reports item parsing errors', () { 83 | expect( 84 | () => pick([ 85 | {'name': 'John Snow'}, 86 | {'asdf': 'Daenerys Targaryen'}, // <-- missing name key 87 | ]).asListOrThrow((pick) => Person.fromPick(pick)), 88 | throwsA( 89 | pickException( 90 | containing: [ 91 | 'Expected a non-null value but location list index 1 in pick(json, 1 (absent), "name") is absent. Use asListOrEmpty()/asListOrNull() when the value may be null/absent at some point (List?).', 92 | ], 93 | ), 94 | ), 95 | ); 96 | }); 97 | 98 | test('wrong type throws', () { 99 | expect( 100 | () => pick('Bubblegum').asListOrThrow((it) => it.asStringOrThrow()), 101 | throwsA( 102 | pickException( 103 | containing: ['String', 'Bubblegum', 'List'], 104 | ), 105 | ), 106 | ); 107 | expect( 108 | () => pick(Object()).asListOrThrow((it) => it.asStringOrThrow()), 109 | throwsA( 110 | pickException( 111 | containing: [ 112 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be casted to List', 113 | ], 114 | ), 115 | ), 116 | ); 117 | }); 118 | }); 119 | 120 | group('asListOrEmpty', () { 121 | test('pick value', () { 122 | expect( 123 | pick([1, 2, 3]).asListOrEmpty((it) => it.asIntOrThrow()), 124 | [1, 2, 3], 125 | ); 126 | }); 127 | 128 | test('null returns null', () { 129 | expect(nullPick().asListOrEmpty((it) => it.asIntOrThrow()), []); 130 | }); 131 | 132 | test('wrong type returns empty', () { 133 | expect(pick(Object()).asListOrEmpty((it) => it.asIntOrThrow()), []); 134 | }); 135 | 136 | test('map empty list to empty list', () { 137 | expect( 138 | pick([]).asListOrEmpty((pick) => Person.fromPick(pick)), 139 | [], 140 | ); 141 | }); 142 | 143 | test('map null list to empty list', () { 144 | expect( 145 | nullPick().asListOrEmpty((pick) => Person.fromPick(pick)), 146 | [], 147 | ); 148 | }); 149 | 150 | test('map to List', () { 151 | expect( 152 | pick([ 153 | {'name': 'John Snow'}, 154 | {'name': 'Daenerys Targaryen'}, 155 | ]).asListOrEmpty((pick) => Person.fromPick(pick)), 156 | [ 157 | Person(name: 'John Snow'), 158 | Person(name: 'Daenerys Targaryen'), 159 | ], 160 | ); 161 | }); 162 | 163 | test('map to List ignoring null values', () { 164 | expect( 165 | pick([ 166 | {'name': 'John Snow'}, 167 | {'name': 'Daenerys Targaryen'}, 168 | null, // <-- valid value 169 | ]).asListOrEmpty((pick) => Person.fromPick(pick)), 170 | [ 171 | Person(name: 'John Snow'), 172 | Person(name: 'Daenerys Targaryen'), 173 | // not 3rd item 174 | ], 175 | ); 176 | }); 177 | 178 | test('map to List with whenNull', () { 179 | expect( 180 | pick([ 181 | {'name': 'John Snow'}, 182 | {'name': 'Daenerys Targaryen'}, 183 | null, // <-- valid value 184 | ]).asListOrEmpty( 185 | (pick) => Person.fromPick(pick), 186 | whenNull: (it) => null, 187 | ), 188 | [ 189 | Person(name: 'John Snow'), 190 | Person(name: 'Daenerys Targaryen'), 191 | null, 192 | ], 193 | ); 194 | }); 195 | 196 | test('map reports item parsing errors', () { 197 | expect( 198 | () => pick([ 199 | {'name': 'John Snow'}, 200 | {'asdf': 'Daenerys Targaryen'}, // <-- missing name key 201 | ]).asListOrEmpty((pick) => Person.fromPick(pick)), 202 | throwsA( 203 | pickException( 204 | containing: [ 205 | 'Expected a non-null value but location list index 1 in pick(json, 1 (absent), "name") is absent.', 206 | ], 207 | ), 208 | ), 209 | ); 210 | }); 211 | }); 212 | 213 | group('asListOrNull', () { 214 | test('pick value', () { 215 | expect( 216 | pick([1, 2, 3]).asListOrNull((it) => it.asIntOrThrow()), 217 | [1, 2, 3], 218 | ); 219 | }); 220 | 221 | test('null returns null', () { 222 | expect(nullPick().asListOrNull((it) => it.asIntOrThrow()), isNull); 223 | }); 224 | 225 | test('wrong type returns empty', () { 226 | expect(pick(Object()).asListOrNull((it) => it.asIntOrThrow()), isNull); 227 | }); 228 | 229 | test('map empty list to empty list', () { 230 | expect( 231 | pick([]).asListOrNull((pick) => Person.fromPick(pick)), 232 | [], 233 | ); 234 | }); 235 | 236 | test('map null list to null', () { 237 | expect( 238 | nullPick().asListOrNull((pick) => Person.fromPick(pick)), 239 | null, 240 | ); 241 | }); 242 | 243 | test('map to List', () { 244 | expect( 245 | pick([ 246 | {'name': 'John Snow'}, 247 | {'name': 'Daenerys Targaryen'}, 248 | ]).asListOrNull((pick) => Person.fromPick(pick)), 249 | [ 250 | Person(name: 'John Snow'), 251 | Person(name: 'Daenerys Targaryen'), 252 | ], 253 | ); 254 | }); 255 | 256 | test('map to List ignoring null values', () { 257 | expect( 258 | pick([ 259 | {'name': 'John Snow'}, 260 | {'name': 'Daenerys Targaryen'}, 261 | null, // <-- valid value 262 | ]).asListOrNull((pick) => Person.fromPick(pick)), 263 | [ 264 | Person(name: 'John Snow'), 265 | Person(name: 'Daenerys Targaryen'), 266 | // not 3rd item 267 | ], 268 | ); 269 | }); 270 | 271 | test('map to List with whenNull', () { 272 | expect( 273 | pick([ 274 | {'name': 'John Snow'}, 275 | {'name': 'Daenerys Targaryen'}, 276 | null, // <-- valid value 277 | ]).asListOrNull( 278 | (pick) => Person.fromPick(pick), 279 | whenNull: (it) => null, 280 | ), 281 | [ 282 | Person(name: 'John Snow'), 283 | Person(name: 'Daenerys Targaryen'), 284 | null, 285 | ], 286 | ); 287 | }); 288 | 289 | test('Crashes in whenNull are thrown directly', () { 290 | expect( 291 | () => pick([null]).asListOrNull( 292 | (pick) => Person.fromPick(pick), 293 | whenNull: (it) => throw 'oops', 294 | ), 295 | throwsA(isA().having((e) => e, 'value', 'oops')), 296 | ); 297 | }); 298 | 299 | test('map reports item parsing errors', () { 300 | final data = [ 301 | {'name': 'John Snow'}, 302 | {'asdf': 'Daenerys Targaryen'}, // <-- missing name key 303 | ]; 304 | expect( 305 | () => pick(data).asListOrNull((pick) => Person.fromPick(pick)), 306 | throwsA( 307 | pickException( 308 | containing: [ 309 | 'Expected a non-null value but location list index 1 in pick(json, 1 (absent), "name") is absent.', 310 | ], 311 | ), 312 | ), 313 | ); 314 | }); 315 | }); 316 | 317 | group('index in asList* items', () { 318 | test('index is available in lists', () { 319 | final entries = pick(['a', 'b', 'c']).asListOrThrow( 320 | (pick) => MapEntry(pick.asStringOrThrow(), pick.index), 321 | ); 322 | expect(Map.fromEntries(entries), {'a': 0, 'b': 1, 'c': 2}); 323 | }); 324 | test('index increments for null values', () { 325 | final entries = pick(['a', null, null, 'b', null, 'c']).asListOrThrow( 326 | (pick) => MapEntry(pick.asStringOrThrow(), pick.index), 327 | ); 328 | expect(Map.fromEntries(entries), {'a': 0, 'b': 3, 'c': 5}); 329 | }); 330 | test('whenNull has access to index', () { 331 | final entries = pick(['a', null, null, 'b', null, 'c']).asListOrThrow( 332 | (pick) => MapEntry(pick.asStringOrThrow(), pick.index), 333 | whenNull: (pick) => MapEntry(pick.index, pick.index! * 2), 334 | ); 335 | expect(Map.fromEntries(entries), { 336 | 'a': 0, 337 | 1: 2, 338 | 2: 4, 339 | 'b': 3, 340 | 4: 8, 341 | 'c': 5, 342 | }); 343 | }); 344 | }); 345 | }); 346 | } 347 | -------------------------------------------------------------------------------- /test/src/pick_map_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asMap*', () { 8 | group('asMapOrThrow', () { 9 | test('pipe through Map', () { 10 | expect(pick({'ab': 'cd'}).asMapOrThrow(), {'ab': 'cd'}); 11 | }); 12 | 13 | test('null throws', () { 14 | expect( 15 | () => nullPick().asMapOrThrow(), 16 | throwsA( 17 | pickException( 18 | containing: [ 19 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asMapOrEmpty()/asMapOrNull() when the value may be null/absent at some point (Map?).', 20 | ], 21 | ), 22 | ), 23 | ); 24 | }); 25 | 26 | test('throws for cast error', () { 27 | final dynamic data = { 28 | 'a': {'some': 'value'}, 29 | }; 30 | 31 | try { 32 | final parsed = pick(data).asMapOrThrow(); 33 | fail( 34 | 'casted map without verifying the types. ' 35 | 'Expected Map but was ${parsed.runtimeType}', 36 | ); 37 | // ignore: avoid_catching_errors 38 | } on TypeError catch (e) { 39 | expect( 40 | e, 41 | const TypeMatcher().having( 42 | (e) => e.toString(), 43 | 'message', 44 | stringContainsInOrder( 45 | ['', 'is not a subtype of type', 'bool'], 46 | ), 47 | ), 48 | ); 49 | } 50 | }); 51 | 52 | test('wrong type throws', () { 53 | expect( 54 | () => pick('Bubblegum').asMapOrThrow(), 55 | throwsA( 56 | pickException( 57 | containing: ['String', 'Bubblegum', 'Map'], 58 | ), 59 | ), 60 | ); 61 | expect( 62 | () => pick(Object()).asMapOrThrow(), 63 | throwsA( 64 | pickException( 65 | containing: [ 66 | 'Type Object of picked value "Instance of \'Object\'" using pick() can not be casted to Map', 67 | ], 68 | ), 69 | ), 70 | ); 71 | }); 72 | }); 73 | 74 | group('asMapOrEmpty', () { 75 | test('pick value', () { 76 | expect(pick({'ab': 'cd'}).asMapOrEmpty(), {'ab': 'cd'}); 77 | }); 78 | 79 | test('null returns null', () { 80 | expect(nullPick().asMapOrEmpty(), {}); 81 | }); 82 | 83 | test('wrong type returns empty', () { 84 | expect(pick(Object()).asMapOrEmpty(), {}); 85 | }); 86 | 87 | test('reports errors correctly', () { 88 | final dynamic data = { 89 | 'a': {'some': 'value'}, 90 | }; 91 | 92 | try { 93 | final parsed = pick(data).asMapOrEmpty(); 94 | fail( 95 | 'casted map without verifying the types. ' 96 | 'Expected Map but was ${parsed.runtimeType}', 97 | ); 98 | // ignore: avoid_catching_errors 99 | } on TypeError catch (e) { 100 | expect( 101 | e, 102 | const TypeMatcher().having( 103 | (e) => e.toString(), 104 | 'message', 105 | stringContainsInOrder( 106 | ['', 'is not a subtype of type', 'bool'], 107 | ), 108 | ), 109 | ); 110 | // ignore: avoid_catching_errors, deprecated_member_use 111 | } 112 | }); 113 | }); 114 | 115 | group('asMapOrNull', () { 116 | test('pick value', () { 117 | expect(pick({'ab': 'cd'}).asMapOrNull(), {'ab': 'cd'}); 118 | }); 119 | 120 | test('null returns null', () { 121 | expect(nullPick().asMapOrNull(), isNull); 122 | }); 123 | 124 | test('wrong type returns empty', () { 125 | expect(pick(Object()).asMapOrNull(), isNull); 126 | }); 127 | }); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /test/src/pick_string_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'pick_test.dart'; 5 | 6 | void main() { 7 | group('pick().asString*', () { 8 | group('asStringOrThrow', () { 9 | test('parse anything to String', () { 10 | expect(pick('adam').asStringOrThrow(), 'adam'); 11 | expect(pick(1).asStringOrThrow(), '1'); 12 | expect(pick(2.0).asStringOrThrow(), '2.0'); 13 | expect( 14 | pick(DateTime(2000)).asStringOrThrow(), 15 | '2000-01-01 00:00:00.000', 16 | ); 17 | }); 18 | 19 | test('asString() alsow works for Maps and Lists calling their toString', 20 | () { 21 | expect(pick(['a', 'b']).asStringOrThrow(), '[a, b]'); 22 | expect(pick({'a': 1}).asStringOrThrow(), '{a: 1}'); 23 | }); 24 | 25 | test('null throws', () { 26 | expect( 27 | () => nullPick().asStringOrThrow(), 28 | throwsA( 29 | pickException( 30 | containing: [ 31 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent. Use asStringOrNull() when the value may be null/absent at some point (String?).', 32 | ], 33 | ), 34 | ), 35 | ); 36 | }); 37 | }); 38 | 39 | group('asStringOrNull', () { 40 | test('parse String', () { 41 | expect(pick('2012').asStringOrNull(), '2012'); 42 | }); 43 | 44 | test('null returns null', () { 45 | expect(nullPick().asStringOrNull(), isNull); 46 | }); 47 | 48 | test('as long it is not null it prints toString', () { 49 | expect(pick(Object()).asStringOrNull(), "Instance of 'Object'"); 50 | }); 51 | }); 52 | }); 53 | 54 | group('pick().required().asString*', () { 55 | group('asStringOrThrow', () { 56 | test('parse anything to String', () { 57 | expect(pick('adam').required().asStringOrThrow(), 'adam'); 58 | expect(pick(1).required().asStringOrThrow(), '1'); 59 | expect(pick(2.0).required().asStringOrThrow(), '2.0'); 60 | expect( 61 | pick(DateTime(2000)).required().asStringOrThrow(), 62 | '2000-01-01 00:00:00.000', 63 | ); 64 | }); 65 | 66 | test("asString() doesn't transform Maps and Lists with toString", () { 67 | expect(pick(['a', 'b']).required().asStringOrThrow(), '[a, b]'); 68 | expect(pick({'a': 1}).required().asStringOrThrow(), '{a: 1}'); 69 | }); 70 | 71 | test('null throws', () { 72 | expect( 73 | () => nullPick().required().asStringOrThrow(), 74 | throwsA( 75 | pickException( 76 | containing: [ 77 | 'Expected a non-null value but location "unknownKey" in pick(json, "unknownKey" (absent)) is absent.', 78 | ], 79 | ), 80 | ), 81 | ); 82 | }); 83 | }); 84 | 85 | group('asStringOrNull', () { 86 | test('parse String', () { 87 | expect(pick('2012').required().asStringOrNull(), '2012'); 88 | }); 89 | 90 | test('as long it is not null it prints toString', () { 91 | expect( 92 | pick(Object()).required().asStringOrNull(), 93 | "Instance of 'Object'", 94 | ); 95 | }); 96 | }); 97 | 98 | test('required().asString()', () { 99 | expect(pick('adam').required().asString(), 'adam'); 100 | expect(pick([]).required().asString(), '[]'); 101 | }); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /test/src/pick_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unreachable_from_main 2 | 3 | import 'package:deep_pick/deep_pick.dart'; 4 | import 'package:test/test.dart'; 5 | 6 | void main() { 7 | group('pick', () { 8 | test('pick a value with one arg', () { 9 | final data = {'name': 'John Snow'}; 10 | final p = pick(data, 'name'); 11 | expect(p.value, 'John Snow'); 12 | expect(p.path, ['name']); 13 | }); 14 | 15 | test('pick a value with two args', () { 16 | final data = { 17 | 'name': {'first': 'John', 'last': 'Snow'}, 18 | }; 19 | final p = pick(data, 'name', 'first'); 20 | expect(p.value, 'John'); 21 | expect(p.path, ['name', 'first']); 22 | }); 23 | 24 | test('ignores null args', () { 25 | final data = { 26 | 'name': {'first': 'John', 'last': 'Snow'}, 27 | }; 28 | // Probably nobody is using it that way. It's a byproduct of faking varargs. 29 | // But it is the public API and shouldn't break 30 | final p = pick(data, null, 'name', null, 'first'); 31 | expect(p.value, 'John'); 32 | expect(p.path, ['name', 'first']); 33 | }); 34 | }); 35 | 36 | group('pickDeep', () { 37 | test('pickDeep a value with one arg', () { 38 | final data = {'name': 'John Snow'}; 39 | final p = pickDeep(data, ['name']); 40 | expect(p.value, 'John Snow'); 41 | expect(p.path, ['name']); 42 | }); 43 | 44 | test('pickDeep a value with two args', () { 45 | final data = { 46 | 'name': {'first': 'John', 'last': 'Snow'}, 47 | }; 48 | final p = pickDeep(data, ['name', 'first']); 49 | expect(p.value, 'John'); 50 | expect(p.path, ['name', 'first']); 51 | }); 52 | }); 53 | 54 | group('pickFromJson', () { 55 | test('pick a value with one arg', () { 56 | const json = '{"name": "John Snow"}'; 57 | final p = pickFromJson(json, 'name'); 58 | expect(p.value, 'John Snow'); 59 | expect(p.path, ['name']); 60 | }); 61 | 62 | test('pick a value with two args', () { 63 | const json = '{"name": {"first": "John", "last": "Snow"}}'; 64 | final p = pickFromJson(json, 'name', 'first'); 65 | expect(p.value, 'John'); 66 | expect(p.path, ['name', 'first']); 67 | }); 68 | 69 | test('parse empty string', () { 70 | expect( 71 | () => pickFromJson('', 'name'), 72 | throwsA( 73 | isA() 74 | .having((it) => it.message, 'message', 'Unexpected end of input'), 75 | ), 76 | ); 77 | }); 78 | 79 | test('has to start with object {} or list []', () { 80 | pickFromJson('{}'); 81 | pickFromJson('[]'); 82 | expect( 83 | () => pickFromJson('someValue'), 84 | throwsA( 85 | isA() 86 | .having((it) => it.message, 'message', 'Unexpected character'), 87 | ), 88 | ); 89 | }); 90 | 91 | test('ignores null args', () { 92 | const json = '{"name": {"first": "John", "last": "Snow"}}'; 93 | // Probably nobody is using it that way. It's a byproduct of faking varargs. 94 | // But it is the public API and shouldn't break 95 | final p = pickFromJson(json, null, 'name', null, 'first'); 96 | expect(p.value, 'John'); 97 | expect(p.path, ['name', 'first']); 98 | }); 99 | }); 100 | 101 | group('Pick', () { 102 | test('null pick carries full location', () { 103 | final p = pick(null, 'some', 'path'); 104 | expect(p.path, ['some', 'path']); 105 | expect(p.value, null); 106 | }); 107 | 108 | test('required pick from null show good error message', () { 109 | expect( 110 | () => pick(null).required(), 111 | throwsA( 112 | isA().having( 113 | (e) => e.message, 114 | 'message', 115 | contains( 116 | 'Expected a non-null value but location picked value "null" using pick() is null', 117 | ), 118 | ), 119 | ), 120 | ); 121 | }); 122 | 123 | group('location', () { 124 | test('root with value', () { 125 | expect( 126 | pick('a').debugParsingExit, 127 | 'picked value "a" using pick()', 128 | ); 129 | }); 130 | test('root with null', () { 131 | expect( 132 | pick(null).debugParsingExit, 133 | 'picked value "null" using pick()', 134 | ); 135 | }); 136 | 137 | test('absent in map', () { 138 | expect( 139 | pick({'a': 1}, 'b').debugParsingExit, 140 | '"b" in pick(json, "b" (absent))', 141 | ); 142 | }); 143 | test('null in map', () { 144 | expect( 145 | pick({'a': null}, 'a').debugParsingExit, 146 | 'picked value "null" using pick(json, "a" (null))', 147 | ); 148 | }); 149 | test('value in map', () { 150 | expect( 151 | pick({'a': 'b'}, 'a').debugParsingExit, 152 | 'picked value "b" using pick(json, "a"(b))', 153 | ); 154 | }); 155 | 156 | test('long path', () { 157 | expect( 158 | pick({'a': 'b'}, 'a', 'b', 'c', 'd').debugParsingExit, 159 | '"b" in pick(json, "a", "b" (absent), "c", "d")', 160 | ); 161 | }); 162 | }); 163 | 164 | group('required', () { 165 | test('pick null but require - show good error message', () { 166 | expect( 167 | () => pick([null], 0).required(), 168 | throwsA( 169 | isA().having( 170 | (e) => e.message, 171 | 'message', 172 | contains( 173 | 'Expected a non-null value but location picked value "null" using pick(json, 0 (null)) is null', 174 | ), 175 | ), 176 | ), 177 | ); 178 | }); 179 | 180 | test('required pick from null with args show good error message', () { 181 | expect( 182 | () => pick(null, 'some', 'path').required(), 183 | throwsA( 184 | isA().having( 185 | (e) => e.message, 186 | 'message', 187 | contains( 188 | 'Expected a non-null value but location "some" in pick(json, "some" (absent), "path") is absent', 189 | ), 190 | ), 191 | ), 192 | ); 193 | }); 194 | 195 | test('not matching required pick show good error message', () { 196 | expect( 197 | () => pick('a', 'some', 'path').required(), 198 | throwsA( 199 | isA().having( 200 | (e) => e.message, 201 | 'message', 202 | contains( 203 | 'Expected a non-null value but location "some" in pick(json, "some" (absent), "path") is absent.', 204 | ), 205 | ), 206 | ), 207 | ); 208 | }); 209 | }); 210 | 211 | test('toString() prints value and path', () { 212 | expect( 213 | // ignore: deprecated_member_use_from_same_package 214 | Pick('a', path: ['b', 0]).toString(), 215 | 'Pick(value=a, path=[b, 0])', 216 | ); 217 | }); 218 | 219 | test( 220 | 'picking from sets by index is illegal ' 221 | 'because to order is not guaranteed', () { 222 | final data = { 223 | 'set': {'a', 'b', 'c'}, 224 | }; 225 | expect( 226 | () => pick(data, 'set', 0), 227 | throwsA( 228 | isA().having( 229 | (e) => e.toString(), 230 | 'toString', 231 | allOf(contains('[set]'), contains('Set'), contains('index (0)')), 232 | ), 233 | ), 234 | ); 235 | }); 236 | 237 | test('call()', () { 238 | final data = [ 239 | {'name': 'John Snow'}, 240 | {'name': 'Daenerys Targaryen'}, 241 | ]; 242 | 243 | final first = pick(data, 0); 244 | expect(first.value, {'name': 'John Snow'}); 245 | 246 | // pick further 247 | expect(first.call('name').asStringOrThrow(), 'John Snow'); 248 | }); 249 | 250 | test('pick deeper than data structure returns null pick', () { 251 | final p = pick([], 'a', 'b'); 252 | expect(p.path, ['a', 'b']); 253 | expect(p.value, isNull); 254 | }); 255 | 256 | test('call() carries over the location for good stacktraces', () { 257 | final data = [ 258 | {'name': 'John Snow'}, 259 | {'name': 'Daenerys Targaryen'}, 260 | ]; 261 | 262 | final level1Pick = pick(data, 0); 263 | expect(level1Pick.path, [0]); 264 | 265 | final level2Pick = level1Pick.call('name'); 266 | expect(level2Pick.path, [0, 'name']); 267 | }); 268 | 269 | group('isAbsent', () { 270 | test('is not absent because value', () { 271 | final p = pick('a'); 272 | expect(p.value, isNotNull); 273 | expect(p.isAbsent, isFalse); 274 | expect(p.missingValueAtIndex, null); 275 | }); 276 | 277 | test('is not absent but null', () { 278 | final p = pick(null); 279 | expect(p.value, isNull); 280 | expect(p.isAbsent, isFalse); 281 | expect(p.missingValueAtIndex, null); 282 | }); 283 | 284 | test('is not absent but null further down', () { 285 | final p = pick({'a': null}, 'a'); 286 | expect(p.value, isNull); 287 | expect(p.isAbsent, isFalse); 288 | expect(p.missingValueAtIndex, null); 289 | }); 290 | 291 | test('is not absent, not null', () { 292 | final p = pick({'a', 1}, 'b'); 293 | expect(p.value, isNull); 294 | expect(p.isAbsent, isTrue); 295 | expect(p.missingValueAtIndex, 0); 296 | }); 297 | 298 | test('is not absent, not null, further down', () { 299 | final json = { 300 | 'a': {'b': 1}, 301 | }; 302 | final p = pick(json, 'a', 'x' /*absent*/); 303 | expect(p.value, isNull); 304 | expect(p.isAbsent, isTrue); 305 | expect(p.missingValueAtIndex, 1); 306 | }); 307 | }); 308 | }); 309 | 310 | group('isAbsent', () { 311 | test('out of range in list returns null pick', () { 312 | final data = [ 313 | {'name': 'John Snow'}, 314 | {'name': 'Daenerys Targaryen'}, 315 | ]; 316 | expect(pick(data, 10).value, isNull); 317 | expect(pick(data, 10).isAbsent, true); 318 | }); 319 | 320 | test('unknown property in map returns null', () { 321 | final data = {'name': 'John Snow'}; 322 | expect(pick(data, 'birthday').value, isNull); 323 | expect(pick(data, 'birthday').isAbsent, true); 324 | }); 325 | 326 | test('documentation example Map', () { 327 | final pa = pick({'a': null}, 'a'); 328 | expect(pa.value, isNull); 329 | expect(pa.isAbsent, false); 330 | 331 | final pb = pick({'a': null}, 'b'); 332 | expect(pb.value, isNull); 333 | expect(pb.isAbsent, true); 334 | }); 335 | 336 | test('documentation example List', () { 337 | final p0 = pick([null], 0); 338 | expect(p0.value, isNull); 339 | expect(p0.isAbsent, false); 340 | 341 | final p2 = pick([], 2); 342 | expect(p2.value, isNull); 343 | expect(p2.isAbsent, true); 344 | }); 345 | 346 | test('Map key for list', () { 347 | final p = pick([], 'a'); 348 | expect(p.value, isNull); 349 | expect(p.isAbsent, true); 350 | }); 351 | }); 352 | 353 | group('context API', () { 354 | test('add and read from context', () { 355 | final data = [ 356 | {'name': 'John Snow'}, 357 | {'name': 'Daenerys Targaryen'}, 358 | ]; 359 | final root = pick(data); 360 | root.context['lang'] = 'de'; 361 | expect(root.context, {'lang': 'de'}); 362 | }); 363 | 364 | test('read from deep nested context', () { 365 | final root = pick([]).withContext('user', {'id': '1234'}); 366 | expect(root.fromContext('user', 'id').asStringOrNull(), '1234'); 367 | }); 368 | 369 | test('copy into required()', () { 370 | final data = [ 371 | {'name': 'John Snow'}, 372 | {'name': 'Daenerys Targaryen'}, 373 | ]; 374 | final root = pick(data); 375 | root.context['lang'] = 'de'; 376 | expect(root.context, {'lang': 'de'}); 377 | 378 | final requiredPick = root.required(); 379 | expect(requiredPick.context, {'lang': 'de'}); 380 | 381 | root.context['hello'] = 'world'; 382 | expect(root.context, {'lang': 'de', 'hello': 'world'}); 383 | expect(requiredPick.context, {'lang': 'de'}); 384 | }); 385 | 386 | test('copy into asList()', () { 387 | final data = [ 388 | {'name': 'John Snow'}, 389 | {'name': 'Daenerys Targaryen'}, 390 | ]; 391 | final root = pick(data); 392 | root.context['lang'] = 'de'; 393 | expect(root.context, {'lang': 'de'}); 394 | 395 | final contexts = root.asListOrNull((pick) => pick.context); 396 | expect(contexts, [ 397 | {'lang': 'de'}, 398 | {'lang': 'de'}, 399 | ]); 400 | }); 401 | 402 | test('copy into call() pick', () { 403 | final data = [ 404 | {'name': 'John Snow'}, 405 | {'name': 'Daenerys Targaryen'}, 406 | ]; 407 | final root = pick(data); 408 | root.context['lang'] = 'de'; 409 | expect(root.context, {'lang': 'de'}); 410 | 411 | final afterCall = root.call(1, 'name'); 412 | expect(afterCall.context, {'lang': 'de'}); 413 | 414 | root.context['hello'] = 'world'; 415 | expect(root.context, {'lang': 'de', 'hello': 'world'}); 416 | expect(afterCall.context, {'lang': 'de'}); 417 | }); 418 | 419 | group('index', () { 420 | test('index is available in lists', () { 421 | final picked0 = pick(['a', 'b', 'c'], 0); 422 | expect(picked0.index, 0); 423 | expect(picked0.value, 'a'); 424 | 425 | final picked1 = pick(['a', 'b', 'c'], 1); 426 | expect(picked1.index, 1); 427 | expect(picked1.value, 'b'); 428 | 429 | final picked2 = pick(['a', 'b', 'c'], 2); 430 | expect(picked2.index, 2); 431 | expect(picked2.value, 'c'); 432 | }); 433 | test('index increments for null values', () { 434 | final picked = pick(['a', null, 'c'], 1); 435 | expect(picked.index, 1); 436 | expect(picked.value, null); 437 | }); 438 | test('no index for maps', () { 439 | expect(pick({'a': 'apple', 'b': 'beer'}, 'a').index, isNull); 440 | }); 441 | }); 442 | }); 443 | } 444 | 445 | Pick nullPick() { 446 | return pick({}, 'unknownKey'); 447 | } 448 | 449 | Matcher pickException({required List containing}) { 450 | return const TypeMatcher() 451 | .having((e) => e.message, 'message', stringContainsInOrder(containing)); 452 | } 453 | 454 | class Person { 455 | final String name; 456 | 457 | Person({required this.name}); 458 | 459 | factory Person.fromPick(RequiredPick pick) { 460 | return Person( 461 | name: pick('name').required().asStringOrThrow(), 462 | ); 463 | } 464 | 465 | @override 466 | bool operator ==(Object other) => 467 | identical(this, other) || 468 | other is Person && runtimeType == other.runtimeType && name == other.name; 469 | 470 | @override 471 | int get hashCode => name.hashCode; 472 | } 473 | -------------------------------------------------------------------------------- /test/src/required_pick_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:deep_pick/deep_pick.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('RequiredPick', () { 6 | test('toString() works as expected', () { 7 | expect( 8 | // ignore: deprecated_member_use_from_same_package 9 | RequiredPick('a', path: ['b', 0]).toString(), 10 | 'RequiredPick(value=a, path=[b, 0])', 11 | ); 12 | }); 13 | 14 | test('value is non nullable', () { 15 | final nullable = pick('a'); 16 | expect([nullable.value].runtimeType.toString(), 'List'); 17 | 18 | final nonNull = nullable.required(); 19 | expect([nonNull.value].runtimeType.toString(), 'List'); 20 | }); 21 | 22 | test('nullable() converts RequiredPick back to a Pick', () { 23 | final aMayBeOptional = pick({'a': 1}, 'a'); 24 | final aIsNowRequired = aMayBeOptional.required(); 25 | final aMayBeOptionalAgain = aIsNowRequired.nullable(); 26 | expect(aMayBeOptionalAgain.asIntOrThrow(), 1); 27 | }); 28 | }); 29 | 30 | group('.call()', () { 31 | test('.call() pick further', () { 32 | final data = [ 33 | {'name': 'John Snow'}, 34 | {'name': 'Daenerys Targaryen'}, 35 | ]; 36 | 37 | final first = pick(data, 0).required(); 38 | expect(first.value, {'name': 'John Snow'}); 39 | 40 | // pick further 41 | expect(first.call('name').asStringOrThrow(), 'John Snow'); 42 | expect(first('name').asStringOrThrow(), 'John Snow'); 43 | }); 44 | 45 | test('call() carries over the location for good stacktraces', () { 46 | final data = [ 47 | {'name': 'John Snow'}, 48 | {'name': 'Daenerys Targaryen'}, 49 | ]; 50 | 51 | final level1Pick = pick(data, 0).required(); 52 | expect(level1Pick.path, [0]); 53 | 54 | final level2Pick = level1Pick.call('name'); 55 | expect(level2Pick.path, [0, 'name']); 56 | }); 57 | }); 58 | 59 | group('context API', () { 60 | test('add and read from context', () { 61 | final data = [ 62 | {'name': 'John Snow'}, 63 | {'name': 'Daenerys Targaryen'}, 64 | ]; 65 | final root = pick(data).required(); 66 | root.context['lang'] = 'de'; 67 | expect(root.context, {'lang': 'de'}); 68 | }); 69 | 70 | test('read from deep nested context', () { 71 | final root = pick([]).required().withContext('user', {'id': '1234'}); 72 | expect(root.fromContext('user', 'id').asStringOrNull(), '1234'); 73 | }); 74 | 75 | test('copy context into elements when parsing lists', () { 76 | final data = [ 77 | {'name': 'John Snow'}, 78 | {'name': 'Daenerys Targaryen'}, 79 | ]; 80 | final root = pick(data).required(); 81 | root.context['lang'] = 'de'; 82 | expect(root.context, {'lang': 'de'}); 83 | 84 | final contexts = root.asListOrEmpty((pick) => pick.context); 85 | expect(contexts, [ 86 | {'lang': 'de'}, 87 | {'lang': 'de'}, 88 | ]); 89 | }); 90 | 91 | test('copy context into call()', () { 92 | final data = [ 93 | {'name': 'John Snow'}, 94 | {'name': 'Daenerys Targaryen'}, 95 | ]; 96 | final root = pick(data).required(); 97 | root.context['lang'] = 'de'; 98 | expect(root.context, {'lang': 'de'}); 99 | 100 | final afterCall = root.call(1, 'name').required(); 101 | expect(afterCall.context, {'lang': 'de'}); 102 | 103 | root.context['hello'] = 'world'; 104 | expect(root.context, {'lang': 'de', 'hello': 'world'}); 105 | expect(afterCall.context, {'lang': 'de'}); 106 | }); 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /tool/reformat.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | Future main(List args) async { 5 | stdout.writeln('Reformatting project with dartfmt'); 6 | final dartfmt = await Process.start('dartfmt', [ 7 | '--set-exit-if-changed', 8 | '-w', 9 | '--fix', 10 | 'example', 11 | 'lib', 12 | 'test', 13 | 'tool', 14 | ]); 15 | 16 | // ignore: unawaited_futures 17 | stderr.addStream(dartfmt.stderr); 18 | // ignore: unawaited_futures 19 | stdout.addStream( 20 | dartfmt.stdout 21 | // help dartfmt formatting 22 | .map((it) => String.fromCharCodes(it)) 23 | .where((it) => !it.contains('Unchanged')) 24 | .map(reformatDartfmtStdout) 25 | .map((it) => it.codeUnits), 26 | ); 27 | 28 | final reformatExit = await dartfmt.exitCode; 29 | 30 | stdout.writeln(); 31 | if (reformatExit == 0) { 32 | stdout.writeln('All files are correctly formatted'); 33 | } else { 34 | stdout.writeln('Error: Some files require reformatting with dartfmt'); 35 | stdout.writeln('run: ./flutterw packages pub run tool/reformat.dart'); 36 | } 37 | exit(reformatExit); 38 | } 39 | 40 | String reformatDartfmtStdout(String line) { 41 | if (line.startsWith('Formatting directory')) { 42 | return line.replaceFirst('Formatting directory ', ''); 43 | } 44 | return '- $line'; 45 | } 46 | -------------------------------------------------------------------------------- /tool/run_coverage_locally.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | (dart pub global list | grep coverage) || { 5 | # install coverage when not found 6 | dart pub global activate coverage 7 | } 8 | dart pub global run coverage:test_with_coverage 9 | dart pub global run coverage:format_coverage \ 10 | --packages=.dart_tool/package_config.json \ 11 | --lcov \ 12 | -i coverage/coverage.json \ 13 | -o coverage/lcov.info 14 | 15 | if type genhtml >/dev/null 2>&1; then 16 | genhtml -o coverage/html coverage/lcov.info 17 | echo "open coverage report $PWD/out/coverage/html/index.html" 18 | else 19 | echo "genhtml not installed, can't generate html coverage output" 20 | fi 21 | --------------------------------------------------------------------------------