diff --git a/.github/workflows/haskell-ci.yml b/.github/workflows/haskell-ci.yml new file mode 100644 index 0000000..3ea9fcf --- /dev/null +++ b/.github/workflows/haskell-ci.yml @@ -0,0 +1,217 @@ +# This GitHub workflow config has been generated by a script via +# +# haskell-ci 'github' 'cabal.project' +# +# To regenerate the script (for example after adjusting tested-with) run +# +# haskell-ci regenerate +# +# For more information, see https://github.com/haskell-CI/haskell-ci +# +# version: 0.16.3 +# +# REGENDATA ("0.16.3",["github","cabal.project"]) +# +name: Haskell-CI +on: + - push + - pull_request +jobs: + linux: + name: Haskell-CI - Linux - ${{ matrix.compiler }} + runs-on: ubuntu-20.04 + timeout-minutes: + 60 + container: + image: buildpack-deps:bionic + continue-on-error: ${{ matrix.allow-failure }} + strategy: + matrix: + include: + - compiler: ghc-9.6.2 + compilerKind: ghc + compilerVersion: 9.6.2 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.4.5 + compilerKind: ghc + compilerVersion: 9.4.5 + setup-method: ghcup + allow-failure: false + - compiler: ghc-9.2.7 + compilerKind: ghc + compilerVersion: 9.2.7 + setup-method: ghcup + allow-failure: false + - compiler: ghc-8.10.7 + compilerKind: ghc + compilerVersion: 8.10.7 + setup-method: ghcup + allow-failure: false + fail-fast: false + steps: + - name: apt + run: | + apt-get update + apt-get install -y --no-install-recommends gnupg ca-certificates dirmngr curl git software-properties-common libtinfo5 + mkdir -p "$HOME/.ghcup/bin" + curl -sL https://downloads.haskell.org/ghcup/0.1.19.2/x86_64-linux-ghcup-0.1.19.2 > "$HOME/.ghcup/bin/ghcup" + chmod a+x "$HOME/.ghcup/bin/ghcup" + "$HOME/.ghcup/bin/ghcup" install ghc "$HCVER" || (cat "$HOME"/.ghcup/logs/*.* && false) + "$HOME/.ghcup/bin/ghcup" install cabal 3.10.1.0 || (cat "$HOME"/.ghcup/logs/*.* && false) + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: Set PATH and environment variables + run: | + echo "$HOME/.cabal/bin" >> $GITHUB_PATH + echo "LANG=C.UTF-8" >> "$GITHUB_ENV" + echo "CABAL_DIR=$HOME/.cabal" >> "$GITHUB_ENV" + echo "CABAL_CONFIG=$HOME/.cabal/config" >> "$GITHUB_ENV" + HCDIR=/opt/$HCKIND/$HCVER + HC=$HOME/.ghcup/bin/$HCKIND-$HCVER + echo "HC=$HC" >> "$GITHUB_ENV" + echo "HCPKG=$HOME/.ghcup/bin/$HCKIND-pkg-$HCVER" >> "$GITHUB_ENV" + echo "HADDOCK=$HOME/.ghcup/bin/haddock-$HCVER" >> "$GITHUB_ENV" + echo "CABAL=$HOME/.ghcup/bin/cabal-3.10.1.0 -vnormal+nowrap" >> "$GITHUB_ENV" + HCNUMVER=$(${HC} --numeric-version|perl -ne '/^(\d+)\.(\d+)\.(\d+)(\.(\d+))?$/; print(10000 * $1 + 100 * $2 + ($3 == 0 ? $5 != 1 : $3))') + echo "HCNUMVER=$HCNUMVER" >> "$GITHUB_ENV" + echo "ARG_TESTS=--enable-tests" >> "$GITHUB_ENV" + echo "ARG_BENCH=--enable-benchmarks" >> "$GITHUB_ENV" + echo "HEADHACKAGE=false" >> "$GITHUB_ENV" + echo "ARG_COMPILER=--$HCKIND --with-compiler=$HC" >> "$GITHUB_ENV" + echo "GHCJSARITH=0" >> "$GITHUB_ENV" + env: + HCKIND: ${{ matrix.compilerKind }} + HCNAME: ${{ matrix.compiler }} + HCVER: ${{ matrix.compilerVersion }} + - name: env + run: | + env + - name: write cabal config + run: | + mkdir -p $CABAL_DIR + cat >> $CABAL_CONFIG <> $CABAL_CONFIG < cabal-plan.xz + echo 'f62ccb2971567a5f638f2005ad3173dba14693a45154c1508645c52289714cb2 cabal-plan.xz' | sha256sum -c - + xz -d < cabal-plan.xz > $HOME/.cabal/bin/cabal-plan + rm -f cabal-plan.xz + chmod a+x $HOME/.cabal/bin/cabal-plan + cabal-plan --version + - name: checkout + uses: actions/checkout@v3 + with: + path: source + - name: initial cabal.project for sdist + run: | + touch cabal.project + echo "packages: $GITHUB_WORKSPACE/source/ebird-api" >> cabal.project + echo "packages: $GITHUB_WORKSPACE/source/ebird-cli" >> cabal.project + echo "packages: $GITHUB_WORKSPACE/source/ebird-client" >> cabal.project + cat cabal.project + - name: sdist + run: | + mkdir -p sdist + $CABAL sdist all --output-dir $GITHUB_WORKSPACE/sdist + - name: unpack + run: | + mkdir -p unpacked + find sdist -maxdepth 1 -type f -name '*.tar.gz' -exec tar -C $GITHUB_WORKSPACE/unpacked -xzvf {} \; + - name: generate cabal.project + run: | + PKGDIR_ebird_api="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/ebird-api-[0-9.]*')" + echo "PKGDIR_ebird_api=${PKGDIR_ebird_api}" >> "$GITHUB_ENV" + PKGDIR_ebird_cli="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/ebird-cli-[0-9.]*')" + echo "PKGDIR_ebird_cli=${PKGDIR_ebird_cli}" >> "$GITHUB_ENV" + PKGDIR_ebird_client="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/ebird-client-[0-9.]*')" + echo "PKGDIR_ebird_client=${PKGDIR_ebird_client}" >> "$GITHUB_ENV" + rm -f cabal.project cabal.project.local + touch cabal.project + touch cabal.project.local + echo "packages: ${PKGDIR_ebird_api}" >> cabal.project + echo "packages: ${PKGDIR_ebird_cli}" >> cabal.project + echo "packages: ${PKGDIR_ebird_client}" >> cabal.project + echo "package ebird-api" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project + echo "package ebird-cli" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project + echo "package ebird-client" >> cabal.project + echo " ghc-options: -Werror=missing-methods" >> cabal.project + cat >> cabal.project <> cabal.project.local + cat cabal.project + cat cabal.project.local + - name: dump install plan + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dry-run all + cabal-plan + - name: restore cache + uses: actions/cache/restore@v3 + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store + restore-keys: ${{ runner.os }}-${{ matrix.compiler }}- + - name: install dependencies + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks --dependencies-only -j2 all + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH --dependencies-only -j2 all + - name: build w/o tests + run: | + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: build + run: | + $CABAL v2-build $ARG_COMPILER $ARG_TESTS $ARG_BENCH all --write-ghc-environment-files=always + - name: cabal check + run: | + cd ${PKGDIR_ebird_api} || false + ${CABAL} -vnormal check + cd ${PKGDIR_ebird_cli} || false + ${CABAL} -vnormal check + cd ${PKGDIR_ebird_client} || false + ${CABAL} -vnormal check + - name: haddock + run: | + $CABAL v2-haddock --disable-documentation --haddock-all $ARG_COMPILER --with-haddock $HADDOCK $ARG_TESTS $ARG_BENCH all + - name: unconstrained build + run: | + rm -f cabal.project.local + $CABAL v2-build $ARG_COMPILER --disable-tests --disable-benchmarks all + - name: save cache + uses: actions/cache/save@v3 + if: always() + with: + key: ${{ runner.os }}-${{ matrix.compiler }}-${{ github.sha }} + path: ~/.cabal/store diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8ad4ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist-newstyle +cabal.project.local +.envrc \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa8adfa --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# eBird Haskell + +eBird libraries and tools written in Haskell, for Haskell. + +## What is eBird? + +[eBird](https://ebird.org/home) is a massive collection of ornithological +science projects developed by the [Cornell Lab of +Ornithology](https://www.birds.cornell.edu/home/). The [eBird +API](https://documenter.getpostman.com/view/664302/S1ENwy59) offers programmatic +access to the incredible dataset backing these projects. + +## What is included + +This repository hosts several libraries and tools, all centered around accessing +and processing eBird data from the public eBird web API. + +### [`./ebird-cli`](./ebird-cli/) + +A command-line interface for querying the official eBird API. + +### [`./ebird-client`](./ebird-client/) + +A Haskell library for querying the official eBird API. + +### [`./ebird-api`](./ebird-api/) + +A Haskell library that defines the eBird API as a [servant][servant] API type. +This library is intended for use by those who wish to write their own eBird API +clients using [servant-client][servant-client], or who wish to do custom +processing of eBird data using the types defined in the library. + +## Contribute + +Please don't hesitate to [open an +issue](https://github.com/FinleyMcIlwaine/ebird-haskell/issues) (or a [pull +request](https://github.com/FinleyMcIlwaine/ebird-haskell/pulls)!) if you have +any questions or something doesn't work as expected. + + +[servant]: https://docs.servant.dev/en/stable/ +[servant-client]: https://hackage.haskell.org/package/servant-client diff --git a/cabal.project b/cabal.project new file mode 100644 index 0000000..ce89e1b --- /dev/null +++ b/cabal.project @@ -0,0 +1,9 @@ +packages: + ebird-api + ebird-cli + ebird-client + +package * + ghc-options: + -Wall + -Wunused-packages diff --git a/ebird-api/CHANGELOG.md b/ebird-api/CHANGELOG.md new file mode 100644 index 0000000..ba63525 --- /dev/null +++ b/ebird-api/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for ebird-api + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/ebird-api/Inconsistencies.md b/ebird-api/Inconsistencies.md new file mode 100644 index 0000000..7b54f1e --- /dev/null +++ b/ebird-api/Inconsistencies.md @@ -0,0 +1,92 @@ +# eBird API Inconsistencies and Peculiarities + +The public eBird API is inconsistent and underspecified. This fact is in direct +opposition to our goal of modeling it in Haskell. In this document, we attempt +to enumerate the various pain points of the eBird API. + +This has basically turned into a substitute for an eBird API issue tracker. + +## Inconsistencies + +Things in the API that don't line up but do "work". + +* Observation detail levels + +The "notable observations" endpoints allow a "detail" query parameter to +determine the detail level of the returned observations. Although one would +expect this query parameter to be handled by any endpoint that returns +observations, this is not the case. + +We handle this by hard coding the returned observations of some endpoints to +"simple" detail level. Any endpoint that does properly handle detail levels +returns `SomeObservation` values, which are observations whose detail levels are +existentially quantified. + +* Recent Checklists Feed + +It is in the wrong spot of the eBird API documentation. Should be under the +`product` list, since that's the route. + +* Clarify difference between checklist ID and submission ID + +* Specifying mixed region types in observation endpoints + +Errors with capture variable but is fine when mixed across capture variable and +"r" query param. + +e.g. +``` +curl "https://api.ebird.org/v2/data/obs/US-WY,US-WY-029/recent?maxResults=10&key=$(cat ~/.ebird/key.txt)" +``` +errors + +``` +curl "https://api.ebird.org/v2/data/obs/US-WY/recent?r=US-WY-029&maxResults=15&key=$(cat ~/.ebird/key.txt)" +``` +works + +## Errors + +Things that appear to be errors/bugs in the API. + +* Observation endpoints do not handle "world" region + +They return an error with "Region type custom not yet supported". But since the +"region info" endpoint does handle the "world" region, I would not expect this +to result in an error. + +* Subregions of multiple regions errors + +Specifying more than one region for the sub regions endpoint causes a 500 error, +where it seems like it should be prefectly acceptable to ask for the list of +counties in both US-WY and US-CO. + +* Recent observations of species inconsistent with notable observations + +I can see an observation of a species in the "recent notable observations" +output for a region, but I cannot see that same observation when searching for +observations of that species using the "recent species observations" endpoint. + +* Historic observations endpoint does not respect region boundary + +Requesting historic observations on this day (July 5th, 2023) in Park County, +Wyoming (US-WY-029) is showing observations from other counties. + +* `includeProvisional` does not work + +Try: +``` +curl "https://api.ebird.org/v2/data/obs/US-WY-029/recent?includeProvisional=false&key=$(cat ~/.ebird/key.txt)" +``` + +Note that the response has observations with `"obsReviewed": false`. + +* The regional statistics API does not work for subnational2 regions, always + says 0 for all results + +Try: +``` +curl "https://api.ebird.org/v2/product/stats/US-WY-029/2023/7/15recent?&key=$(cat ~/.ebird/key.txt)" +``` + +Response has 0 for all fields. diff --git a/ebird-api/LICENSE b/ebird-api/LICENSE new file mode 100644 index 0000000..d397496 --- /dev/null +++ b/ebird-api/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2023 Finley McIlwaine + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ebird-api/README.md b/ebird-api/README.md new file mode 100644 index 0000000..f46eae0 --- /dev/null +++ b/ebird-api/README.md @@ -0,0 +1,36 @@ +# ebird-api + +A Haskell description of the [eBird API][api-docs]. + +## Installation + +In your cabal file: +```cabal + build-depends: + ebird-api +``` + +## Usage + +> Note: If you are interested in *querying* the eBird API, use +> [ebird-client](../ebird-client/) instead! + +This library is intended for those who want to write their own clients for the +[eBird API][api-docs], or do some custom processing of eBird data using the +types defined here. + +Definitions for all major types of values that the [eBird API][api-docs] +communicates in are provided, including +[observations](./src/EBird/API/Observations.hs) and +[checklists](./src/EBird/API/Checklists.hs). [servant] API types for all +endpoints of the [eBird API][api-docs] are also provided. + +Please don't hesitate to + +For more documentation, see the library's [Hackage documentation][ebird-api]. + + + +[api-docs]: https://documenter.getpostman.com/view/664302/S1ENwy59 +[ebird-api]: https://hackage.haskell.org/package/ebird-api +[servant]: https://hackage.haskell.org/package/servant diff --git a/ebird-api/ebird-api.cabal b/ebird-api/ebird-api.cabal new file mode 100644 index 0000000..8af77d4 --- /dev/null +++ b/ebird-api/ebird-api.cabal @@ -0,0 +1,73 @@ +cabal-version: 3.4 +name: ebird-api +version: 0.1.0.0 +synopsis: + A Haskell description of the eBird API +description: + [eBird](https://ebird.org/home) is a massive collection of ornithological + science projects developed by the + [Cornell Lab of Ornithology](https://www.birds.cornell.edu/home/). The + [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) + offers programmatic access to the incredible dataset backing these + projects. + + This library contains a description of the + eBird API as a + [servant](https://hackage.haskell.org/package/servant) API type. It is + intended for use by those who wish to write their own clients for the + eBird API using + [servant-client](https://hackage.haskell.org/package/servant-client), or do + custom processing of eBird data using the types defined here. + + If you are interested in querying the + eBird API using an existing client, check out the + [ebird-client](https://hackage.haskell.org/package/ebird-client) library. + +license: MIT +license-file: LICENSE +author: Finley McIlwaine +maintainer: finleymcilwaine@gmail.com +copyright: 2023 Finley McIlwaine +category: Web +build-type: Simple +extra-doc-files: CHANGELOG.md +bug-reports: https://github.com/FinleyMcIlwaine/ebird-haskell/issues +homepage: https://github.com/FinleyMcIlwaine/ebird-haskell + +tested-with: + GHC == 8.10.7 + , GHC == 9.2.7 + , GHC == 9.4.5 + , GHC == 9.6.2 + +common common + build-depends: + base >= 4.13.3.0 && < 4.19 + default-extensions: + ImportQualifiedPost + LambdaCase + OverloadedStrings + RecordWildCards + default-language: Haskell2010 + +library + import: common + exposed-modules: + EBird.API + , EBird.API.Checklists + , EBird.API.EBirdString + , EBird.API.Hotspots + , EBird.API.Observations + , EBird.API.Product + , EBird.API.Regions + , EBird.API.Taxonomy + , EBird.API.Util.Time + build-depends: + aeson >= 1.5.6.0 && < 2.2 + , attoparsec >= 0.14.1 && < 0.15 + , attoparsec-iso8601 >= 1.0.2.0 && < 1.2 + , servant >= 0.18.3 && < 0.21 + , text >= 1.2.4.1 && < 2.1 + , time >= 1.9.3 && < 1.13 + hs-source-dirs: + src diff --git a/ebird-api/src/EBird/API.hs b/ebird-api/src/EBird/API.hs new file mode 100644 index 0000000..28c96b7 --- /dev/null +++ b/ebird-api/src/EBird/API.hs @@ -0,0 +1,593 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeOperators #-} + +-- | +-- Module : EBird.API +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- A description of the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) +-- as a [servant](https://hackage.haskell.org/package/servant) API type. +-- +-- Intended for use by those who wish to write their own clients for the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) using +-- [servant-client](https://hackage.haskell.org/package/servant-client). +-- +-- If you are interested in querying the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) using +-- an existing client, check out the +-- [ebird-client](https://hackage.haskell.org/package/ebird-client) library. + +module EBird.API + ( -- * Servant API types + -- + -- | Note: The individual endpoint types are only exported for those who + -- wish to implement partial clients for the eBird API. Comprehensive client + -- implementations should use the 'EBirdAPI' type. + + -- ** Top-level API + EBirdAPI + , WithAPIKey + + -- ** Observations APIs + -- + -- | These endpoints can be found under the [data\/obs + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#4e020bc2-fc67-4fb6-a926-570cedefcc34) + -- of the eBird API documentation. + , RecentObservationsAPI + , RecentNotableObservationsAPI + , RecentSpeciesObservationsAPI + , RecentNearbyObservationsAPI + , RecentNearbySpeciesObservationsAPI + , RecentNearestSpeciesObservationsAPI + , RecentNearbyNotableObservationsAPI + , HistoricalObservationsAPI + + -- ** Product APIs + -- + -- | These endpoints can be found under the [product + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#af04604f-e406-4cea-991c-a9baef24cd78) + -- of the eBird API documentation. + , RecentChecklistsAPI + , Top100API + , ChecklistFeedAPI + , RegionalStatisticsAPI + , SpeciesListAPI + , ViewChecklistAPI + + -- ** Hotspot APIs + -- + -- | These endpoints can be found under the [ref\/hotspot + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#5a1e27e9-128f-4ab5-80ad-88cd6de10026) + -- of the eBird API documentation. + , RegionHotspotsAPI + , NearbyHotspotsAPI + , HotspotInfoAPI + + -- ** Taxonomy APIs + -- + -- | These endpoints can be found under the [ref\/taxonomy + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#36c95b76-e18e-4788-9c9e-e539045f9166) + -- of the eBird API documentation. + , TaxonomyAPI + , TaxonomicFormsAPI + , TaxaLocaleCodesAPI + , TaxonomyVersionsAPI + , TaxonomicGroupsAPI + + -- ** Region APIs + -- + -- | These endpoints can be found under the [ref\/region + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#e18ea3b5-e80c-479f-87db-220ce8d9f3b6) + -- of the eBird API documentation, except for the 'AdjacentRegionsAPI', + -- which would be under the [ref\/geo + -- section](https://documenter.getpostman.com/view/664302/S1ENwy59#c9947c5c-2dce-4c6d-9911-7d702235506c) + -- of the eBird API documentation. + , RegionInfoAPI + , SubRegionListAPI + , AdjacentRegionsAPI + + -- * eBird checklists + , module EBird.API.Checklists + + -- * eBird observations + , module EBird.API.Observations + + -- * eBird products + , module EBird.API.Product + + -- * eBird hotspots + , module EBird.API.Hotspots + + -- * eBird regions + , module EBird.API.Regions + + -- * eBird taxonomy + , module EBird.API.Taxonomy + + -- * Date and time utilities + , module EBird.API.Util.Time + + -- * 'EBirdString' class + , module EBird.API.EBirdString + ) where + +import Data.Text (Text) +import Servant.API + +import EBird.API.Checklists +import EBird.API.EBirdString +import EBird.API.Observations +import EBird.API.Product +import EBird.API.Hotspots +import EBird.API.Regions +import EBird.API.Taxonomy +import EBird.API.Util.Time + +{- +Note [dependently typed APIs] + +Some eBird API abservation endpoints (such as the "recent notable observations" +endpoint) allow the caller to specify a detail level ("simple" or "full") for +the observations contained in the response. "Full" detail observations contain +more fields than "simple" detail observations. This means that "simple" and +"full" observations are most accurately represented as different types. +Furthermore, this means that the result type of these endpoints is determined by +the value of the "detail" query parameter. Since the type of the API depends on +the value of an input variable, we say that the API is dependently typed. + +There are two ways we could deal with this. First, we could model this situation +at the value level. To do this, we create a tagged union of simple and full +detail observations and have all of the observation endpoints return this +combined type, regardless of the value of "detail" query parameter. The benefit +of this approach is the simplicity. It requires no fancy type-level voodoo to +model this or to use it. The drawback of this approach is the lack of type +safety. Clients of this API know whether "simple" or "full" detail observations +are being requested, yet they still must do a runtime check of the response to +ensure that the observations in the response are detailed as expected. This is +not an error condition that clients of the API should need to worry about. + +Second, we could model this situation at the type level, much like is done in +the fantastic [Well-Typed article on dependently typed +servers](https://www.well-typed.com/blog/2015/12/dependently-typed-servers/). +The drawback of this approach is the fancy type-level programming overhead it +introduces for both this library and any consumers. + +For now, we model this at the value level. +-} + +-- | The [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) as +-- a Haskell type. +type EBirdAPI = + -- Observation APIs + RecentObservationsAPI + :<|> RecentNotableObservationsAPI + :<|> RecentSpeciesObservationsAPI + :<|> RecentNearbyObservationsAPI + :<|> RecentNearbySpeciesObservationsAPI + :<|> RecentNearestSpeciesObservationsAPI + :<|> RecentNearbyNotableObservationsAPI + :<|> HistoricalObservationsAPI + + -- Product APIs + :<|> RecentChecklistsAPI + :<|> Top100API + :<|> ChecklistFeedAPI + :<|> RegionalStatisticsAPI + :<|> SpeciesListAPI + :<|> ViewChecklistAPI + + -- Hotspot APIs + :<|> RegionHotspotsAPI + :<|> NearbyHotspotsAPI + :<|> HotspotInfoAPI + + -- Taxonomy APIs + :<|> TaxonomyAPI + :<|> TaxonomicFormsAPI + :<|> TaxaLocaleCodesAPI + :<|> TaxonomyVersionsAPI + :<|> TaxonomicGroupsAPI + + -- Region APIs + :<|> RegionInfoAPI + :<|> SubRegionListAPI + :<|> AdjacentRegionsAPI + +-- | Convenient synonym for requiring an @x-ebirdapitoken@ on a route +type WithAPIKey = Header' '[Required] "x-ebirdapitoken" Text + +{------------------------------------------------------------------------------ + Observations APIs +------------------------------------------------------------------------------} + +-- | Recent observations within a region. Note that this endpoint only ever +-- returns 'Simple' detail observations. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#3d2a17c1-2129-475c-b4c8-7d362d6000cd). +type RecentObservationsAPI = + "v2" :> "data" :> "obs" + :> WithAPIKey + :> Capture "regionCode" RegionCode + :> "recent" + :> QueryParam "back" Integer + :> QueryParam "cat" TaxonomyCategories + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "r" RegionCode + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [Observation 'Simple] + +-- | Recent /notable/ observations within a region. Since this endpoint can +-- return both 'Simple' and 'Full' detail observations, depending on the value +-- provided for the "detail" query parameter, we existentially quantify the +-- detail level of the resulting observations. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#397b9b8c-4ab9-4136-baae-3ffa4e5b26e4). +type RecentNotableObservationsAPI = + "v2" :> "data" :> "obs" + :> WithAPIKey + :> Capture "regionCode" RegionCode + :> "recent" + :> "notable" + :> QueryParam "back" Integer + :> QueryParam "detail" DetailLevel + :> QueryParam "hotspot" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "r" RegionCode + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [SomeObservation] + +-- | Recent observations of a species within a region. Note that this endpoint +-- only ever returns 'Simple' detail observations. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#755ce9ab-dc27-4cfc-953f-c69fb0f282d9). +type RecentSpeciesObservationsAPI = + "v2" :> "data" :> "obs" + :> WithAPIKey + :> Capture "regionCode" RegionCode + :> "recent" + :> Capture "speciesCode" SpeciesCode + :> QueryParam "back" Integer + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "r" RegionCode + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [Observation 'Simple] + +-- | Recent observations within some radius of some latitude/longitude. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#62b5ffb3-006e-4e8a-8e50-21d90d036edc). +type RecentNearbyObservationsAPI = + "v2" :> "data" :> "obs" :> "geo" :> "recent" + :> WithAPIKey + :> QueryParam' '[Required] "lat" Double + :> QueryParam' '[Required] "lng" Double + :> QueryParam "dist" Integer + :> QueryParam "back" Integer + :> QueryParam "cat" TaxonomyCategories + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "sort" SortObservationsBy + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [Observation 'Simple] + +-- | Recent observations of a species within some radius of some +-- latitude/longitude. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#20fb2c3b-ee7f-49ae-a912-9c3f16a40397). +type RecentNearbySpeciesObservationsAPI = + "v2" :> "data" :> "obs" :> "geo" :> "recent" + :> WithAPIKey + :> Capture "species" SpeciesCode + :> QueryParam' '[Required] "lat" Double + :> QueryParam' '[Required] "lng" Double + :> QueryParam "dist" Integer + :> QueryParam "back" Integer + :> QueryParam "cat" TaxonomyCategories + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "sort" SortObservationsBy + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [Observation 'Simple] + +-- | Nearest recent observations including a species. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#6bded97f-9997-477f-ab2f-94f254954ccb). +type RecentNearestSpeciesObservationsAPI = + "v2" :> "data" :> "nearest" :> "geo" :> "recent" + :> WithAPIKey + :> Capture "species" SpeciesCode + :> QueryParam' '[Required] "lat" Double + :> QueryParam' '[Required] "lng" Double + :> QueryParam "dist" Integer + :> QueryParam "back" Integer + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [Observation 'Simple] + +-- | Recent /notable/ observations of a within some radius of some +-- latitude/longitude. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#caa348bb-71f6-471c-b203-9e1643377cbc). +type RecentNearbyNotableObservationsAPI = + "v2" :> "data" :> "obs" :> "geo" :> "recent" :> "notable" + :> WithAPIKey + :> QueryParam' '[Required] "lat" Double + :> QueryParam' '[Required] "lng" Double + :> QueryParam "dist" Integer + :> QueryParam "detail" DetailLevel + :> QueryParam "back" Integer + :> QueryParam "hotspot" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [SomeObservation] + +-- | A list of all observations for each taxa seen in some 'RegionCode' on a +-- specific date. The specific observations returned are determined by the +-- @rank@ parameter - first observation of the species +-- ('SelectFirstObservation', default) or last observation +-- ('SelectLastObservation'). +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8c6ee8-c435-4e91-9f66-6d3eeb09edd2). +type HistoricalObservationsAPI = + "v2" :> "data" :> "obs" + :> WithAPIKey + :> Capture "regionCode" RegionCode + :> "historic" + :> Capture "year" Integer + :> Capture "month" Integer + :> Capture "day" Integer + :> QueryParam "cat" TaxonomyCategories + :> QueryParam "detail" DetailLevel + :> QueryParam "hotspot" Bool + :> QueryParam "includeProvisional" Bool + :> QueryParam "maxResults" Integer + :> QueryParam "rank" SelectObservation + :> QueryParam "r" RegionCode + :> QueryParam "sppLocale" SPPLocale + :> Get '[JSON] [SomeObservation] + +{------------------------------------------------------------------------------ + Product APIs +------------------------------------------------------------------------------} + +-- | A list of recent checklists submitted in a region. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#95a206d1-a20d-44e0-8c27-acb09ccbea1a). +type RecentChecklistsAPI = + "v2" :> "product" :> "lists" + :> WithAPIKey + :> Capture "regionCode" RegionCode + :> QueryParam "maxResults" Integer + :> Get '[JSON] [ChecklistFeedEntry] + +-- | A list of the top 100 contributors on a given date, ranked by species count +-- or checklist count. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8d3f94-c4b0-42bd-9c8e-71edfa6347ba). +type Top100API = + "v2" :> "product" :> "top100" + :> WithAPIKey + :> Capture "regionCode" Region + :> Capture "year" Integer + :> Capture "month" Integer + :> Capture "day" Integer + :> QueryParam "rankedBy" RankTop100By + :> QueryParam "maxResults" Integer + :> Get '[JSON] [Top100ListEntry] + +-- | A list of checklists submitted in a region on a date. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#4416a7cc-623b-4340-ab01-80c599ede73e). +type ChecklistFeedAPI = + "v2" :> "product" :> "lists" + :> WithAPIKey + :> Capture "regionCode" Region + :> Capture "year" Integer + :> Capture "month" Integer + :> Capture "day" Integer + :> QueryParam "sortKey" SortChecklistsBy + :> QueryParam "maxResults" Integer + :> Get '[JSON] [ChecklistFeedEntry] + +-- | A list of checklists submitted on a date. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#506e63ab-abc0-4256-b74c-cd9e77968329). +type RegionalStatisticsAPI = + "v2" :> "product" :> "stats" + :> WithAPIKey + :> Capture "regionCode" Region + :> Capture "year" Integer + :> Capture "month" Integer + :> Capture "day" Integer + :> Get '[JSON] RegionalStatistics + +-- | A list of all species ever seen in a region. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#55bd1b26-6951-4a88-943a-d3a8aa1157dd). +type SpeciesListAPI = + "v2" :> "product" :> "spplist" + :> WithAPIKey + :> Capture "regionCode" Region + :> Get '[JSON] [SpeciesCode] + +-- | The details and observations for a checklist. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#2ee89672-4211-4fc1-8493-5df884fbb386). +type ViewChecklistAPI = + "v2" :> "product" :> "checklist" :> "view" + :> WithAPIKey + :> Capture "subId" Text + :> Get '[JSON] Checklist + +{------------------------------------------------------------------------------ + Hotspot APIs +------------------------------------------------------------------------------} + +-- | The hotspots within a list of one or more regions. +-- +-- NOTE: This endpoint switches the content type of the response based on a +-- query parameter, not an "Accept" header, and for some reason it chooses to +-- make the default content type CSV. Any client for this endpoint should +-- hardcode the "fmt" parameter to 'JSONFormat'. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#f4f59f90-854e-4ba6-8207-323a8cf0bfe0). +type RegionHotspotsAPI = + "v2" :> "ref" :> "hotspot" + :> Capture "regionCode" RegionCode + :> QueryParam "back" Integer + :> QueryParam "fmt" CSVOrJSONFormat + :> Get '[JSON] [Hotspot] + +-- | The hotspots within a radius of some latitude/longitude. +-- +-- NOTE: This endpoint switches the content type of the response based on a +-- query parameter, not an "Accept" header, and for some reason it chooses to +-- make the default content type CSV. Any client for this endpoint should +-- hardcode the "fmt" parameter to 'JSONFormat'. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#674e81c1-6a0c-4836-8a7e-6ea1fe8e6677). +type NearbyHotspotsAPI = + "v2" :> "ref" :> "hotspot" :> "geo" + :> QueryParam' '[Required] "lat" Double + :> QueryParam' '[Required] "lng" Double + :> QueryParam "back" Integer + :> QueryParam "dist" Integer + :> QueryParam "fmt" CSVOrJSONFormat + :> Get '[JSON] [Hotspot] + +-- | Information about a hotspot. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#e25218db-566b-4d8b-81ca-e79a8f68c599). +type HotspotInfoAPI = + "v2" :> "ref" :> "hotspot" :> "info" + :> Capture "locId" Text + :> Get '[JSON] LocationData + +{------------------------------------------------------------------------------ + Taxonomy API +------------------------------------------------------------------------------} + +-- | The eBird taxonomy, in part or in full. +-- +-- NOTE: This endpoint switches the content type of the response based on a +-- query parameter, not an "Accept" header, and for some reason it chooses to +-- make the default content type CSV. Any client for this endpoint should +-- hardcode the "fmt" parameter to 'JSONFormat'. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#952a4310-536d-4ad1-8f3e-77cfb624d1bc). +type TaxonomyAPI = + "v2" :> "ref" :> "taxonomy" :> "ebird" + :> QueryParam "cat" TaxonomyCategories + :> QueryParam "fmt" CSVOrJSONFormat + :> QueryParam "locale" SPPLocale + :> QueryParam "species" SpeciesCodes + :> QueryParam "version" Text + :> Get '[JSON] [Taxon] + +-- | The list of subspecies of a given species recognized in the taxonomy. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#e338e5a6-919d-4603-a7db-6c690fa62371). +type TaxonomicFormsAPI = + "v2" :> "ref" :> "taxon" :> "forms" + :> WithAPIKey + :> Capture "speciesCode" SpeciesCode + :> Get '[JSON] SpeciesCodes + +-- | The supported locale codes and names for species common names. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#3ea8ff71-c254-4811-9e80-b445a39302a6). +type TaxaLocaleCodesAPI = + "v2" :> "ref" :> "taxa-locales" :> "ebird" + :> WithAPIKey + :> Header "Accept-Language" SPPLocale + :> Get '[JSON] [SPPLocaleListEntry] + +-- | The complete list of taxonomy versions, with a flag indicating which is the +-- latest. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#9bba1ff5-6eb2-4f9a-91fd-e5ed34e51500). +type TaxonomyVersionsAPI = + "v2" :> "ref" :> "taxonomy" :> "versions" + :> Get '[JSON] [TaxonomyVersionListEntry] + +-- | The list of species groups, e.g. terns, finches, etc. +-- +-- See [the eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#aa9804aa-dbf9-4a53-bbf4-48e214e4677a). +type TaxonomicGroupsAPI = + "v2" :> "ref" :> "sppgroup" + :> Capture "speciesGrouping" SPPGrouping + :> QueryParam "groupNameLocale" SPPLocale + :> Get '[JSON] [TaxonomicGroupListEntry] + +{------------------------------------------------------------------------------ + Regions API +------------------------------------------------------------------------------} + +-- | Get a 'RegionInfo' for a 'Region'. +-- +-- See the [eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#07c64240-6359-4688-9c4f-ff3d678a7248). +type RegionInfoAPI = + "v2" :> "ref" :> "region" + :> "info" + :> WithAPIKey + :> Capture "regionCode" Region + :> QueryParam "regionNameFormat" RegionNameFormat + :> Get '[JSON] RegionInfo + +-- | Get a list of subregions of a certain 'RegionType' within a 'RegionCode'. +-- +-- See the [eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#382da1c8-8bff-4926-936a-a1f8b065e7d5). +type SubRegionListAPI = + "v2" :> "ref" :> "region" + :> "list" + :> WithAPIKey + :> Capture "regionType" RegionType + :> Capture "regionCode" RegionCode + :> Get '[JSON] [RegionListEntry] + + +-- | Adjacent regions to a given region. Only 'Subnational2' region codes in the +-- United States, New Zealand, or Mexico are currently supported. +-- +-- See the [eBird API documentation for this +-- route](https://documenter.getpostman.com/view/664302/S1ENwy59#3aca0519-3105-47fc-8611-a4dfd500a32f). +type AdjacentRegionsAPI = + "v2" :> "ref" :> "adjacent" + :> WithAPIKey + :> Capture "regionCode" Region + :> Get '[JSON] [RegionListEntry] diff --git a/ebird-api/src/EBird/API/Checklists.hs b/ebird-api/src/EBird/API/Checklists.hs new file mode 100644 index 0000000..09d3fd6 --- /dev/null +++ b/ebird-api/src/EBird/API/Checklists.hs @@ -0,0 +1,499 @@ +-- | +-- Module : EBird.API.Checklists +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types related to eBird checklist API values. + +module EBird.API.Checklists + ( -- * Checklist types + Checklist(..) + , ChecklistObservation(..) + , SubAux(..) + , SubAuxAI(..) + , ChecklistFeedEntry(..) + , LocationData(..) + + -- * Auxiliary eBird checklist API types + , SortChecklistsBy(..) + + -- * attoparsec parsers + , parseSortChecklistsBy + ) where + +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Function +import Data.Functor +import Data.Maybe +import Data.String +import Data.Text (Text) +import Data.Text qualified as Text +import Servant.API (ToHttpApiData(..)) + +import EBird.API.EBirdString +import EBird.API.Regions +import EBird.API.Taxonomy +import EBird.API.Util.Time + +-- | Values returned by the 'EBird.API.ViewChecklistAPI' +data Checklist = + Checklist + { -- | Project ID, e.g. \"EBIRD\" + checklistProjectId :: Text + + -- | Checklist submission ID, e.g. \"S144646447\" + , checklistSubId :: Text + + -- | Checklist protocol ID, e.g. \"P21\" + , checklistProtocolId :: Text + + -- | Checklist location ID + , checklistLocationId :: Text + + -- | Checklist group ID + , checklistGroupId :: Text + + -- | Checklist duration, only 'Just' for checklists of appropriate + -- protocols (e.g. not incidentals) + , checklistDurationHours :: Maybe Double + + -- | Was every bird observed reported? + , checklistAllObsReported :: Bool + + -- | What date and time was the checklist created (i.e. submitted)? + , checklistCreationDateTime :: EBirdDateTime + + -- | What date and time what the checklist last edited? + , checklistLastEditedDateTime :: EBirdDateTime + + -- | What date and time what the checklist started? + , checklistObsDateTime :: EBirdDateTime + + -- | TODO: Not sure what this is for + , checklistObsTimeValid :: Bool + + -- | The ID of the checklist, e.g. \"CL24936\" + , checklistChecklistId :: Text + + -- | The number of observers on this checklist + , checklistNumObservers :: Integer + + -- | Distance travelled during this checklist in kilometers, only 'Just' + -- for checklists of appropriate protocols (e.g. not incidentals) + , checklistEffortDistanceKm :: Maybe Double + + -- | The unit of distance used for the checklist submission (e.g. "mi"), + -- only 'Just' for checklists of appropriate protocols (e.g. not + -- incidentals) + , checklistEffortDistanceEnteredUnit :: Maybe Text + + -- | The subnational1 region (state) that the checklist was submitted in + , checklistSubnational1Code :: Region + + -- | Method of checklist submission + , checklistSubmissionMethodCode :: Text + + -- | Version of the method of checklist submission, e.g. "2.13.2_SDK33" + , checklistSubmissionMethodVersion :: Text + + -- | Display-ready version of the method of checklist submission, e.g. + -- "2.13.2" + , checklistSubmissionMethodVersionDisp :: Text + + -- | Display name of the user that submitted the checklist + , checklistUserDisplayName :: Text + + -- | Number of species included in observations on this checklist + , checklistNumSpecies :: Integer + + -- | Submission auxiliary entry methods + -- + -- TODO: Not sure what these are about + , checklistSubAux :: [SubAux] + + -- | Submission auxiliary entry methods that use aritificial + -- intelligence + -- + -- TODO: Not sure what these are about + , checklistSubAuxAI :: [SubAuxAI] + + -- | Observations included in the checklist + , checklistObs:: [ChecklistObservation] + } + deriving (Show, Read, Eq) + +-- | Observation values included in checklists. +data ChecklistObservation = + ChecklistObservation + { -- | Species code of the species, e.g. "norfli" + checklistObservationSpeciesCode :: SpeciesCode + + -- | The date and time of the observation. It is not clear when this + -- would not be equal to the 'checklistObsDateTime' field of the enclosing + -- checklist. + , checklistObservationObsDateTime :: EBirdDateTime + + -- | ID of the observation + , checklistObservationObsId :: Text + + -- | A string representation of the quantity of the observation. If just + -- the presence is noted, the string will be \"X\" + , checklistObservationHowManyStr :: Text + } + deriving (Show, Read, Eq) + +-- | Values included in the 'checklistSubAux' field of 'Checklist's. +data SubAux = + SubAux + { -- | Submission ID + subAuxSubId :: Text + + -- | E.g. "nocturnal" + , subAuxFieldName :: Text + + -- | E.g. "ebird_nocturnal" + , subAuxEntryMethodCode :: Text + + -- | E.g. "0" + , subAuxAuxCode :: Text + } + deriving (Show, Read, Eq) + +-- | Values included in the 'checklistSubAuxAI' field of 'Checklist's. +data SubAuxAI = + SubAuxAI + { -- | Submission ID + subAuxAISubId :: Text + + -- | E.g. "concurrent" + , subAuxAIMethod :: Text + + -- | E.g. "sound" + , subAuxAIType :: Text + + -- | E.g. "merlin" + , subAuxAISource :: Text + + -- | E.g. 0 + , subAuxEventId :: Integer + } + deriving (Show, Read, Eq) + +-- | eBird checklists. Note that we do not include some redundant fields of +-- checklist values returned by the API (e.g. @subID@, which is always the same +-- value as @subId@). +data ChecklistFeedEntry = + ChecklistFeedEntry + { -- | The location ID of the checklist + checklistFeedEntryLocationId :: Text + + -- | Checklist submission ID + , checklistFeedEntrySubId :: Text + + -- | The display name of the user that submitted this checklist + , checklistFeedEntryUserDisplayName :: Text + + -- | Number of species included on this checklist + , checklistFeedEntryNumSpecies :: Integer + + -- | Date that this checklist was started + , checklistFeedEntryDate :: EBirdDate + + -- | Time that this checklist was started + , checklistFeedEntryTime :: EBirdTime + + -- | Location data for the checklist + , checklistFeedEntryLocationData :: LocationData + } + deriving (Show, Read, Eq) + +-- | eBird checklist or hotspot location data. Note that we do not include some +-- redundant fields of location data values returned by the API (e.g. @locName@, +-- which is always the same value as @name@). +data LocationData = + LocationData + { -- | Name of the location + locationDataName :: Text + + -- | Latitude of the location + , locationDataLatitude :: Double + + -- | Longitude of the location + , locationDataLongitude :: Double + + -- | Country code of the location + , locationDataCountryCode :: Region + + -- | Country name of the location + , locationDataCountryName :: Text + + -- | Subnational1 region that this location is in + , locationDataSubnational1Code :: Region + + -- | Name of the subnational1 region that this location is in + , locationDataSubnational1Name :: Text + + -- | Subnational2 region that this location is in + , locationDataSubnational2Code :: Region + + -- | Name of the subnational2 region that this location is in + , locationDataSubnational2Name :: Text + + -- | Is this location an eBird hotspot? + , locationDataIsHotspot:: Bool + + -- | A compound name for the location consisting of the location name, + -- county name, state name, and country name. + , locationDataHeirarchicalName:: Text + } + deriving (Show, Read, Eq) + +-- | How to rank the list returned by the 'EBird.API.Top100API'. +data SortChecklistsBy + -- | Sort checklists by the date of the observations they contain + = SortChecklistsByDateCreated + + -- | Sort checklists by the date they were submitted + | SortChecklistsByDateSubmitted + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +-- | Explicit instance for compatibility with their field names +instance FromJSON Checklist where + parseJSON = withObject "Checklist" $ \v -> + Checklist + <$> v .: "projId" + <*> v .: "subId" + <*> v .: "protocolId" + <*> v .: "locId" + <*> v .: "groupId" + <*> v .:? "durationHrs" + <*> v .: "allObsReported" + <*> v .: "creationDt" + <*> v .: "lastEditedDt" + <*> v .: "obsDt" + <*> v .: "obsTimeValid" + <*> v .: "checklistId" + <*> v .: "numObservers" + <*> v .:? "effortDistanceKm" + <*> v .:? "effortDistanceEnteredUnit" + <*> v .: "subnational1Code" + <*> v .: "submissionMethodCode" + <*> v .: "submissionMethodVersion" + <*> v .: "submissionMethodVersionDisp" + <*> v .: "userDisplayName" + <*> v .: "numSpecies" + <*> v .: "subAux" + <*> v .: "subAuxAi" + <*> v .: "obs" + +-- | Explicit instance for compatibility with their field names +instance ToJSON Checklist where + toJSON Checklist{..} = + object $ + [ "projId" .= checklistProjectId + , "subId" .= checklistSubId + , "protocolId" .= checklistProtocolId + , "locId" .= checklistLocationId + , "groupId" .= checklistGroupId + , "allObsReported" .= checklistAllObsReported + , "creationDt" .= checklistCreationDateTime + , "lastEditedDt" .= checklistLastEditedDateTime + , "obsDt" .= checklistObsDateTime + , "obsTimeValid" .= checklistObsTimeValid + , "checklistId" .= checklistChecklistId + , "numObservers" .= checklistNumObservers + , "subnational1Code" .= checklistSubnational1Code + , "submissionMethodCode" .= checklistSubmissionMethodCode + , "submissionMethodVersion" .= checklistSubmissionMethodVersion + , "submissionMethodVersionDisp" .= checklistSubmissionMethodVersionDisp + , "userDisplayName" .= checklistUserDisplayName + , "numSpecies" .= checklistNumSpecies + , "subAux" .= checklistSubAux + , "subAuxAi" .= checklistSubAuxAI + , "obs" .= checklistObs + ] + -- Fields that may or may not be included, depending on the observation + -- data + <> [ "durationHrs" .= duration + | duration <- maybeToList checklistDurationHours + ] + <> [ "effortDistanceKm" .= distance + | distance <- maybeToList checklistEffortDistanceKm + ] + <> [ "effortDistanceEnteredUnit" .= unit + | unit <- maybeToList checklistEffortDistanceEnteredUnit + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON ChecklistObservation where + parseJSON = withObject "ChecklistObservation" $ \v -> + ChecklistObservation + <$> v .: "speciesCode" + <*> v .: "obsDt" + <*> v .: "obsId" + <*> v .: "howManyStr" + +-- | Explicit instance for compatibility with their field names +instance ToJSON ChecklistObservation where + toJSON ChecklistObservation{..} = + object + [ "speciesCode" .= checklistObservationSpeciesCode + , "obsDt" .= checklistObservationObsDateTime + , "obsId" .= checklistObservationObsId + , "howManyStr" .= checklistObservationHowManyStr + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON SubAux where + parseJSON = withObject "SubAux" $ \v -> + SubAux + <$> v .: "subId" + <*> v .: "fieldName" + <*> v .: "entryMethodCode" + <*> v .: "auxCode" + +-- | Explicit instance for compatibility with their field names +instance ToJSON SubAux where + toJSON SubAux{..} = + object + [ "subId" .= subAuxSubId + , "fieldName" .= subAuxFieldName + , "entryMethodCode" .= subAuxEntryMethodCode + , "auxCode" .= subAuxAuxCode + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON SubAuxAI where + parseJSON = withObject "SubAuxAI" $ \v -> + SubAuxAI + <$> v .: "subId" + <*> v .: "method" + <*> v .: "aiType" + <*> v .: "source" + <*> v .: "eventId" + +-- | Explicit instance for compatibility with their field names +instance ToJSON SubAuxAI where + toJSON SubAuxAI{..} = + object + [ "subId" .= subAuxAISubId + , "method" .= subAuxAIMethod + , "aiType" .= subAuxAIType + , "eventId" .= subAuxEventId + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON ChecklistFeedEntry where + parseJSON = withObject "ChecklistFeedEntry" $ \v -> + ChecklistFeedEntry + <$> v .: "locId" + <*> v .: "subId" + <*> v .: "userDisplayName" + <*> v .: "numSpecies" + <*> v .: "obsDt" + <*> v .: "obsTime" + <*> v .: "loc" + +-- | Explicit instance for compatibility with their field names +instance ToJSON ChecklistFeedEntry where + toJSON ChecklistFeedEntry{..} = + object + [ "locId" .= checklistFeedEntryLocationId + , "subId" .= checklistFeedEntrySubId + , "userDisplayName" .= checklistFeedEntryUserDisplayName + , "numSpecies" .= checklistFeedEntryNumSpecies + , "obsDt" .= checklistFeedEntryDate + , "obsTime" .= checklistFeedEntryTime + , "loc" .= checklistFeedEntryLocationData + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON LocationData where + parseJSON = withObject "LocationData" $ \v -> + LocationData + <$> v .: "name" + <*> v .: "latitude" + <*> v .: "longitude" + <*> v .: "countryCode" + <*> v .: "countryName" + <*> v .: "subnational1Code" + <*> v .: "subnational1Name" + <*> v .: "subnational2Code" + <*> v .: "subnational2Name" + <*> v .: "isHotspot" + <*> v .: "hierarchicalName" + +-- | Explicit instance for compatibility with their field names +instance ToJSON LocationData where + toJSON LocationData{..} = + object + [ "name" .= locationDataName + , "latitude" .= locationDataLatitude + , "longitude" .= locationDataLongitude + , "countryCode" .= locationDataCountryCode + , "countryName" .= locationDataCountryName + , "subnational1Code" .= locationDataSubnational1Code + , "subnational1Name" .= locationDataSubnational1Name + , "subnational2Code" .= locationDataSubnational2Code + , "subnational2Name" .= locationDataSubnational2Name + , "isHotspot" .= locationDataIsHotspot + , "hierarchicalName" .= locationDataHeirarchicalName + ] + +{------------------------------------------------------------------------------ + EBirdString instances +------------------------------------------------------------------------------} + +-- | The eBird string for a 'SortChecklistsBy' value is either "obs_dt" or +-- "creation_dt". +instance EBirdString SortChecklistsBy where + toEBirdString = + \case + SortChecklistsByDateCreated -> "obs_dt" + SortChecklistsByDateSubmitted -> "creation_dt" + + fromEBirdString str = + parseOnly parseSortChecklistsBy str + & left (("Failed to parse SortChecklistsBy: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString isntances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SortChecklistsBy where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse a 'SortChecklistsBy' value +parseSortChecklistsBy :: Parser SortChecklistsBy +parseSortChecklistsBy = + choice + [ "obs_dt" $> SortChecklistsByDateCreated + , "creation_dt" $> SortChecklistsByDateSubmitted + ] + where + _casesCovered :: SortChecklistsBy -> () + _casesCovered = + \case + SortChecklistsByDateCreated -> () + SortChecklistsByDateSubmitted -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData SortChecklistsBy where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/EBirdString.hs b/ebird-api/src/EBird/API/EBirdString.hs new file mode 100644 index 0000000..16c2730 --- /dev/null +++ b/ebird-api/src/EBird/API/EBirdString.hs @@ -0,0 +1,39 @@ +-- | +-- Module : EBird.API.EBirdString +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- The 'EBirdString' class contains types whose values may be represented as +-- strings compatible with the eBird API. + +module EBird.API.EBirdString + ( -- * The class + EBirdString(..) + + -- * Unsafe interface + , unsafeFromEBirdString + ) where + +import Data.Text (Text) +import GHC.Stack + +-- | A convenience class for converting the litany of eBird API types to and +-- from their respective eBird API compatible string representations. +class EBirdString a where + -- | Convert a value to an eBird string. + toEBirdString :: a -> Text + + -- | Parse a string into an eBird value. If parsing fails, this should result + -- in 'Left' with an error message. + fromEBirdString :: Text -> Either Text a + +-- | Parse a string into an eBird value unsafely. +-- +-- __Be careful!__ This can result in runtime errors if the string is +-- malformatted. +unsafeFromEBirdString :: (HasCallStack, EBirdString a) => Text -> a +unsafeFromEBirdString str = case fromEBirdString str of + Left _ -> error "Failed to parse eBird string" + Right x -> x diff --git a/ebird-api/src/EBird/API/Hotspots.hs b/ebird-api/src/EBird/API/Hotspots.hs new file mode 100644 index 0000000..e33e2f3 --- /dev/null +++ b/ebird-api/src/EBird/API/Hotspots.hs @@ -0,0 +1,162 @@ + +-- | +-- Module : EBird.API.Hotspots +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions related to eBird hotspot API values. + +module EBird.API.Hotspots + ( -- * Hotspot types + Hotspot(..) + + -- * Auxiliary eBird hotspot-related API types + , CSVOrJSONFormat(..) + + -- * attoparsec parsers + , parseCSVOrJSONFormat + ) where + +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Function +import Data.Functor +import Data.String +import Data.Text (Text) +import Data.Text qualified as Text +import Servant.API + +import EBird.API.EBirdString +import EBird.API.Regions +import EBird.API.Util.Time + +-- | eBird hotspots, as returned by the 'EBird.API.RegionHotspotsAPI' +data Hotspot = + Hotspot + { -- | Location ID of the hotspot + hotspotLocationId :: Text + + -- | Name of the hotspot + , hotspotLocationName :: Text + + -- | The country the hotspot is in + , hotspotCountryCode :: Region + + -- | The state the hotspot is in + , hotspotSubnational1Code :: Region + + -- | The county the hotspot is in + , hotspotSubnational2Code :: Region + + -- | The latitude of the hotspot + , hotspotLatitude :: Double + + -- | The longitude of the hotspot + , hotspotLongitude :: Double + + -- | The date and time of the latest observation at the hotspot. Could + -- be 'Nothing' if the hotspot has never been birded + , hotspotLatestObsDateTime :: Maybe EBirdDateTime + + -- | The number of species ever seen at the hotspot. Could be 'Nothing' + -- if the hotspot has never been birded + , hotspotNumSpeciesAllTime :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Used to specify what format hotspot values should be returned in from the +-- hotspots APIs. +data CSVOrJSONFormat = CSVFormat | JSONFormat + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +-- | Explicit instance for compatibility with their field names +instance FromJSON Hotspot where + parseJSON = withObject "Hotspot" $ \v -> + Hotspot + <$> v .: "locId" + <*> v .: "locName" + <*> v .: "countryCode" + <*> v .: "subnational1Code" + <*> v .: "subnational2Code" + <*> v .: "lat" + <*> v .: "lng" + <*> v .:? "latestObsDt" + <*> v .:? "numSpeciesAllTime" + +-- | Explicit instance for compatibility with their field names +instance ToJSON Hotspot where + toJSON Hotspot{..} = + object $ + [ "locId" .= hotspotLocationId + , "locName" .= hotspotLocationName + , "countryCode" .= hotspotCountryCode + , "subnational1Code" .= hotspotSubnational1Code + , "subnational2Code" .= hotspotSubnational2Code + , "lat" .= hotspotLatitude + , "lng" .= hotspotLongitude + ] + -- Fields that may or may not be present depending on the data + <> [ "latestObsDt" .= latestObsDt + | Just latestObsDt <- [hotspotLatestObsDateTime] + ] + <> [ "numSpeciesAllTime" .= numSpecies + | Just numSpecies <- [hotspotNumSpeciesAllTime] + ] + +{------------------------------------------------------------------------------ + EBirdString instances +------------------------------------------------------------------------------} + +-- | The eBird string of a 'CSVOrJSONFormat' value is either "csv" or "json". +instance EBirdString CSVOrJSONFormat where + toEBirdString = + \case + CSVFormat -> "csv" + JSONFormat -> "json" + + fromEBirdString str = + parseOnly parseCSVOrJSONFormat str + & left (("Failed to parse CSVOrJSONFormat: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString CSVOrJSONFormat where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse a list of eBird API taxononomy categories. To avoid the partial +-- behavior of converting a 'sepBy1' result into a 'Data.List.NonEmpty', we +-- manually parse the first category followed by an optional tail. +parseCSVOrJSONFormat :: Parser CSVOrJSONFormat +parseCSVOrJSONFormat = + choice + [ "csv" $> CSVFormat + , "json" $> JSONFormat + ] + where + _casesCovered :: CSVOrJSONFormat -> () + _casesCovered = + \case + CSVFormat -> () + JSONFormat -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData CSVOrJSONFormat where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/Observations.hs b/ebird-api/src/EBird/API/Observations.hs new file mode 100644 index 0000000..cb4019b --- /dev/null +++ b/ebird-api/src/EBird/API/Observations.hs @@ -0,0 +1,439 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeApplications #-} + +-- | +-- Module : EBird.API.Observations +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions related to eBird observation API values. + +module EBird.API.Observations + ( -- * Observation types + Observation(..) + , ObservationDetails(..) + , SomeObservation(..) + + -- * Auxiliary eBird observation API types + , DetailLevel(..) + , SortObservationsBy(..) + , SelectObservation(..) + + -- * attoparsec parsers + , parseDetailLevel + , parseSelectObservation + , parseSortObservationsBy + ) where + +import Control.Arrow +import Data.Aeson +import Data.Aeson.KeyMap +import Data.Attoparsec.Text +import Data.Function +import Data.Functor +import Data.Maybe +import Data.String +import Data.Text as Text +import Servant.API (ToHttpApiData(..)) + +import EBird.API.EBirdString +import EBird.API.Regions +import EBird.API.Util.Time + +-- | An observation of a species submitted to eBird within a checklist. The +-- 'DetailLevel' index indicates whether the observation data includes "full" +-- details. +data Observation (detail :: DetailLevel) = + Observation + { -- | Species code, e.g. "bohwax" + obsSpeciesCode :: Text + + -- | Common name, e.g. "Bohemian Waxwing" + , obsCommonName :: Text + + -- | Scientific name, e.g. "Bombycilla garrulus" + , obsScientificName :: Text + + -- | Location ID, e.g. \"L7884500\" + , obsLocationId :: Text + + -- | Location name, e.g. "Frog Pond" + , obsLocationName :: Text + + -- | Date and time of observation + , obsDateTime :: EBirdDateTime + + -- | How many were seen? Sometimes omitted. + , obsHowMany :: Maybe Integer + + -- | Observation latitude + , obsLatitude :: Double + + -- | Observation longitude + , obsLongitude :: Double + + -- | Is this observation valid? + , obsValid :: Bool + + -- | Has this observation been reviewed? + , obsReviewed :: Bool + + -- | Is the location of this observation private? + , obsLocationPrivate :: Bool + + -- | Submission ID + , obsSubId :: Text + + , obsFullDetail :: ObservationDetails detail + } + +deriving instance Show (Observation 'Simple) +deriving instance Show (Observation 'Full) +deriving instance Eq (Observation 'Simple) +deriving instance Eq (Observation 'Full) + +-- | Extra details that may be attached to an observation. At the moment, it +-- only seems possible to get 'Full' detailed observations from the notable +-- observation endpoints (e.g. 'EBird.API.RecentNotableObservationsAPI'). +data ObservationDetails (detail :: DetailLevel) where + NoDetails :: ObservationDetails 'Simple + FullDetails :: + { -- | The subnational2 region that this observation took place in + obsSubnational2Code :: Region + + -- | The name of the subnational2 region that this observation took + -- place in + , obsSubnational2Name :: Text + + -- | The subnational1 region that this observation took place in + , obsSubnational1Code :: Region + + -- | The name of the subnational1 region that this observation took + -- place in + , obsSubnational1Name :: Text + + -- | The country region that this observation took place in + , obsCountryCode :: Region + + -- | The name of the country region that this observation took place in + , obsCountryName :: Text + + -- | The display name of the user that submitted this observation + , obsUserDisplayName :: Text + + -- | The unique ID of this observation + , obsObsId :: Text + + -- | The ID of the checklist that this observation was submitted with, + -- e.g. \"CL24936\" + , obsChecklistId :: Text + + -- | Whether the count for the observation was provided as just \"X\" + , obsPresenceNoted :: Bool + + -- | Whether this observation was submitted with comments + , obsHasComments :: Bool + + -- | The last name of the user that submitted this observation + , obsLastName :: Text + + -- | The first name of the user that submitted this observation + , obsFirstName :: Text + + -- | Whether this observation has media such as photos, videos, or + -- audio attached + , obsHasRichMedia :: Bool + } -> ObservationDetails 'Full + +deriving instance Show (ObservationDetails 'Simple) +deriving instance Show (ObservationDetails 'Full) +deriving instance Eq (ObservationDetails 'Simple) +deriving instance Eq (ObservationDetails 'Full) + +-- | 'Observation' values of existentially quantified detail. +data SomeObservation where + SomeObservation :: Observation detail -> SomeObservation + +instance Show SomeObservation where + show (SomeObservation o) = + case obsFullDetail o of + NoDetails -> show o + FullDetails{} -> show o + +-- | The promoted constructors of this type are used as type-level indices on +-- the 'Observation' type to determine whether an observation is 'Simple' detail +-- or 'Full' detail. +data DetailLevel = Simple | Full + deriving (Show, Read, Eq) + +-- | Values representing the ways that observations may be sorted in responses +-- from the API. +data SortObservationsBy + = SortObservationsByDate + | SortObservationsBySpecies + deriving (Show, Read, Eq) + +-- | Values representing how to pick which 'Observation's are returned from the +-- 'EBird.API.HistoricalObservationsAPI' in the case that there are several +-- observations of the same species on the date. +data SelectObservation + = SelectFirstObservation + | SelectLastObservation + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +-- | Explicit instance for compatibility with their field names +instance FromJSON (Observation 'Simple) where + parseJSON = withObject "Observation 'Simple" $ \v -> + Observation + <$> v .: "speciesCode" + <*> v .: "comName" + <*> v .: "sciName" + <*> v .: "locId" + <*> v .: "locName" + <*> v .: "obsDt" + <*> v .:? "howMany" + <*> v .: "lat" + <*> v .: "lng" + <*> v .: "obsValid" + <*> v .: "obsReviewed" + <*> v .: "locationPrivate" + <*> v .: "subId" + <*> pure NoDetails + +-- | Explicit instance for compatibility with their field names +instance ToJSON (Observation 'Simple) where + toJSON Observation{..} = + object $ + [ "speciesCode" .= obsSpeciesCode + , "comName" .= obsCommonName + , "sciName" .= obsScientificName + , "locId" .= obsLocationId + , "locName" .= obsLocationName + , "obsDt" .= obsDateTime + , "lat" .= obsLatitude + , "lng" .= obsLongitude + , "obsValid" .= obsValid + , "obsReviewed" .= obsReviewed + , "locationPrivate" .= obsLocationPrivate + , "subId" .= obsSubId + ] + -- Fields that may or may not be included, depending on the observation + -- data + <> ["howMany" .= howMany | Just howMany <- [obsHowMany]] + +-- | Explicit instance for compatibility with their field names +instance FromJSON (Observation 'Full) where + parseJSON = withObject "Observation 'Full" $ \v -> + Observation + <$> v .: "speciesCode" + <*> v .: "comName" + <*> v .: "sciName" + <*> v .: "locId" + <*> v .: "locName" + <*> v .: "obsDt" + <*> v .:? "howMany" + <*> v .: "lat" + <*> v .: "lng" + <*> v .: "obsValid" + <*> v .: "obsReviewed" + <*> v .: "locationPrivate" + <*> v .: "subId" + <*> ( FullDetails + <$> v .: "subnational2Code" + <*> v .: "subnational2Name" + <*> v .: "subnational1Code" + <*> v .: "subnational1Name" + <*> v .: "countryCode" + <*> v .: "countryName" + <*> v .: "userDisplayName" + <*> v .: "obsId" + <*> v .: "checklistId" + <*> v .: "presenceNoted" + <*> v .: "hasComments" + <*> v .: "lastName" + <*> v .: "firstName" + <*> v .: "hasRichMedia" + ) + +-- | Explicit instance for compatibility with their field names +instance ToJSON (Observation 'Full) where + toJSON Observation{..} = + object + [ "speciesCode" .= obsSpeciesCode + , "comName" .= obsCommonName + , "sciName" .= obsScientificName + , "locId" .= obsLocationId + , "locName" .= obsLocationName + , "obsDt" .= obsDateTime + , "howMany" .= obsHowMany + , "lat" .= obsLatitude + , "lng" .= obsLongitude + , "obsValid" .= obsValid + , "obsReviewed" .= obsReviewed + , "locationPrivate" .= obsLocationPrivate + , "subId" .= obsSubId + , "subnational2Code" .= obsSubnational2Code obsFullDetail + , "subnational2Name" .= obsSubnational2Name obsFullDetail + , "subnational1Code" .= obsSubnational1Code obsFullDetail + , "subnational1Name" .= obsSubnational1Name obsFullDetail + , "countryCode" .= obsCountryCode obsFullDetail + , "countryName" .= obsCountryName obsFullDetail + , "userDisplayName" .= obsUserDisplayName obsFullDetail + , "obsId" .= obsObsId obsFullDetail + , "checklistId" .= obsChecklistId obsFullDetail + , "presenceNoted" .= obsPresenceNoted obsFullDetail + , "hasComments" .= obsHasComments obsFullDetail + , "lastName" .= obsLastName obsFullDetail + , "firstName" .= obsFirstName obsFullDetail + , "hasRichMedia" .= obsHasRichMedia obsFullDetail + ] + +-- | Switches between parsing a 'Simple' detail 'Observation' and a 'Full' +-- detail 'Observation' depending on whether the "firstName" key is present. +instance FromJSON SomeObservation where + parseJSON obj = withObject "SomeObservation" + ( \v -> + if isJust (v !? "firstName") then + SomeObservation <$> parseJSON @(Observation 'Full) obj + else + SomeObservation <$> parseJSON @(Observation 'Simple) obj + ) obj + +-- | Switches between encoding a 'Simple' 'Observation' and a 'Full' +-- 'Observation' depending on the evidence introduced by pattern-matching on the +-- 'obsFullDetail' field. +instance ToJSON SomeObservation where + toJSON (SomeObservation obs) = + case obsFullDetail obs of + NoDetails -> toJSON @(Observation 'Simple) obs + FullDetails {} -> toJSON @(Observation 'Full) obs + +{------------------------------------------------------------------------------ + 'EBirdString' instances +------------------------------------------------------------------------------} + +-- | The eBird string for a 'DetailLevel' value is simply the lowercase +-- constructor name. +instance EBirdString DetailLevel where + toEBirdString = + \case + Simple -> "simple" + Full -> "full" + + fromEBirdString str = + parseOnly parseDetailLevel str + & left (("Failed to parse DetailLevel: " <>) . Text.pack) + +-- | The eBird string for a 'SortObservationsBy' value is either "date" or +-- "species". +instance EBirdString SortObservationsBy where + toEBirdString = + \case + SortObservationsByDate -> "date" + SortObservationsBySpecies -> "species" + + fromEBirdString str = + parseOnly parseSortObservationsBy str + & left (("Failed to parse SortObservationsBy: " <>) . Text.pack) + +-- | The eBird string for a 'SelectObservation' value is either "create" or +-- "mrec". +instance EBirdString SelectObservation where + toEBirdString = + \case + SelectFirstObservation -> "create" + SelectLastObservation -> "mrec" + + fromEBirdString str = + parseOnly parseSelectObservation str + & left (("Failed to parse SelectObservation: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString DetailLevel where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SortObservationsBy where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SelectObservation where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse a list of eBird API taxononomy categories. To avoid the partial +-- behavior of converting a 'sepBy1' result into a 'Data.List.NonEmpty', we +-- manually parse the first category followed by an optional tail. +parseDetailLevel :: Parser DetailLevel +parseDetailLevel = + choice + [ "simple" $> Simple + , "full" $> Full + ] + where + _casesCovered :: DetailLevel -> () + _casesCovered = + \case + Simple -> () + Full -> () + +-- | Parse a 'SortObservationsBy' value +parseSortObservationsBy :: Parser SortObservationsBy +parseSortObservationsBy = + choice + [ "date" $> SortObservationsByDate + , "species" $> SortObservationsBySpecies + ] + where + _casesCovered :: SortObservationsBy -> () + _casesCovered = + \case + SortObservationsByDate -> () + SortObservationsBySpecies -> () + +-- | Parse a 'SelectObservation' value +parseSelectObservation :: Parser SelectObservation +parseSelectObservation = + choice + [ "first" $> SelectFirstObservation + , "last" $> SelectLastObservation + ] + where + _casesCovered :: SelectObservation -> () + _casesCovered = + \case + SelectFirstObservation -> () + SelectLastObservation -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData DetailLevel where + toUrlPiece = toEBirdString + +instance ToHttpApiData SortObservationsBy where + toUrlPiece = toEBirdString + +instance ToHttpApiData SelectObservation where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/Product.hs b/ebird-api/src/EBird/API/Product.hs new file mode 100644 index 0000000..68cc22d --- /dev/null +++ b/ebird-api/src/EBird/API/Product.hs @@ -0,0 +1,175 @@ +-- | +-- Module : EBird.API.Product +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types related to eBird product API values. + +module EBird.API.Product + ( -- * Top 100 contributors API types + Top100ListEntry(..) + , RankTop100By(..) + + -- * Regional statistics API types + , RegionalStatistics(..) + + -- * attoparsec parsers + , parseRankTop100By + ) where + +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Function +import Data.Functor +import Data.String +import Data.Text as Text +import Servant.API (ToHttpApiData(..)) + +import EBird.API.EBirdString + +-- | Values held in the top 100 contributors list returned by the eBird API. +data Top100ListEntry = + Top100ListEntry + { -- | The profile handle of the user, whocse profile may be seen at + -- ebird.org/profile/{handle} if they have a profile + top100ListEntryProfileHandle :: Maybe Text + + -- | The display name of the user (typically their full name) + , top100ListEntryUserDisplayName :: Text + + -- | The number of species the user observed on the date + , top100ListEntryNumSpecies :: Integer + + -- | The number of complete checklists the user contributed on the date + , top100ListEntryNumCompleteChecklists :: Integer + + -- | The ranking of the user + , top100ListEntryRowNum :: Integer + + -- | The user ID od the user + , top100ListEntryUserId :: Text + } + deriving (Show, Read, Eq) + +-- | How to rank the list returned by the 'EBird.API.Top100API'. +data RankTop100By + -- | Rank the list by the number of species seen + = RankTop100BySpecies + + -- | Rank the list by number of contributed checklists + | RankTop100ByChecklists + deriving (Show, Read, Eq) + +-- | Values returned by the 'EBird.API.RegionalStatisticsAPI'. +data RegionalStatistics = + RegionalStatistics + { -- | Number of checklists submitted in the region + regionalStatisticsNumChecklists :: Integer + + -- | Number of contributors who have submitted checklists in the region + , regionalStatisticsNumContributors :: Integer + + -- | Number of species included in checklists in the region + , regionalStatisticsNumSpecies :: Integer + } + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + Aeson instances +------------------------------------------------------------------------------} + +-- | Explicit instance for compatibility with their field names +instance FromJSON Top100ListEntry where + parseJSON = withObject "Top100ListEntry" $ \v -> + Top100ListEntry + <$> v .:? "profileHandle" + <*> v .: "userDisplayName" + <*> v .: "numSpecies" + <*> v .: "numCompleteChecklists" + <*> v .: "rowNum" + <*> v .: "userId" + +-- | Explicit instance for compatibility with their field names +instance ToJSON Top100ListEntry where + toJSON Top100ListEntry{..} = + object $ + [ "userDisplayname" .= top100ListEntryUserDisplayName + , "numSpecies" .= top100ListEntryNumSpecies + , "numCompleteChecklists" .= top100ListEntryNumCompleteChecklists + , "rowNum" .= top100ListEntryRowNum + , "userId" .= top100ListEntryUserId + ] + -- Fields that may or may not be included, depending on the contributor + -- data + <> ["profileHandle" .= profileHandle + | Just profileHandle <- [top100ListEntryProfileHandle] + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON RegionalStatistics where + parseJSON = withObject "RegionalStatistics" $ \v -> + RegionalStatistics + <$> v .: "numChecklists" + <*> v .: "numContributors" + <*> v .: "numSpecies" + +-- | Explicit instance for compatibility with their field names +instance ToJSON RegionalStatistics where + toJSON RegionalStatistics{..} = + object + [ "numChecklists" .= regionalStatisticsNumChecklists + , "numContributors" .= regionalStatisticsNumContributors + , "numSpecies" .= regionalStatisticsNumSpecies + ] + +{------------------------------------------------------------------------------ + 'EBirdString' instances +------------------------------------------------------------------------------} + +-- | The eBird string for a 'RankTop100By' value is either "spp" or "cl". +instance EBirdString RankTop100By where + toEBirdString = + \case + RankTop100BySpecies -> "spp" + RankTop100ByChecklists -> "cl" + + fromEBirdString str = + parseOnly parseRankTop100By str + & left (("Failed to parse RankTop100By: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString RankTop100By where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse a 'RankTop100By' value +parseRankTop100By :: Parser RankTop100By +parseRankTop100By = + choice + [ "spp" $> RankTop100BySpecies + , "cl" $> RankTop100ByChecklists + ] + where + _casesCovered :: RankTop100By -> () + _casesCovered = + \case + RankTop100BySpecies -> () + RankTop100ByChecklists -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData RankTop100By where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/Regions.hs b/ebird-api/src/EBird/API/Regions.hs new file mode 100644 index 0000000..c02f713 --- /dev/null +++ b/ebird-api/src/EBird/API/Regions.hs @@ -0,0 +1,424 @@ +-- | +-- Module : EBird.API.Regions +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types related to eBird region API values. + +module EBird.API.Regions + ( -- * Region-related API types + Region(..) + , RegionType(..) + , RegionCode(..) + , RegionNameFormat(..) + , RegionInfo(..) + , RegionBounds(..) + , RegionListEntry(..) + + -- * attoparsec parsers + , parseRegion + , parseRegionCode + , parseRegionNameFormat + , parseRegionType + ) where + +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Function +import Data.Functor +import Data.List.NonEmpty (NonEmpty(..)) +import Data.String +import Data.Text (Text) +import Data.Text qualified as Text +import Servant.API (ToHttpApiData(..)) + +import EBird.API.EBirdString + +-- | eBird divides the world into countries, subnational1 regions (states) or +-- subnational2 regions (counties). 'Location' regions are eBird-specific +-- location identifiers. +data Region = + -- | Regions may be specified as location IDs, e.g. @L227544@ + Location Integer + + -- | The world is a region + | World + + -- | At the top level, the world is divided into countries + | Country Text + + -- | Subnational1 regions are states within countries + | Subnational1 + Text -- ^ The country + Text -- ^ The state + + -- | Subnational2 regions are counties within states + | Subnational2 + Text -- ^ The country + Text -- ^ The state + Text -- ^ The county + deriving (Show, Read, Eq) + +-- | One constructor per eBird "region type" (countries, subnational1 (states), +-- or subnational2 (counties)). +data RegionType = + CountryType + | Subnational1Type + | Subnational2Type + deriving (Show, Read, Eq) + +-- | A 'RegionCode' is a list of one or more 'Region's. +newtype RegionCode = RegionCode { regionCodeRegions :: NonEmpty Region } + deriving (Show, Read, Eq) + +-- | 'RegionNameFormat' values specify what format the API should return region +-- names in. See the constructor docs for examples. +data RegionNameFormat = + -- | 'DetailedNameFormat' region name values are fully qualified with only + -- the country abbreviated, e.g. "Madison County, New York, US" + DetailedNameFormat + + -- | 'DetailedNoQualNameFormat' region name values are like + -- 'DetailedNameFormat' but without the country qualifier and no "county" + -- annotation, e.g. "Madison, New York" + | DetailedNoQualNameFormat + + -- | 'FullNameFormat' region name values are fully qualified with no + -- abbreviated country name and no "county" annotation, e.g. "Madison, + -- New York, United States" + | FullNameFormat + + -- | 'NameQualNameFormat' region name values are just the annotated name, + -- e.g. "Madison County" + | NameQualNameFormat + + -- | 'NameOnlyNameFormat' region name values are just the name, e.g. + -- "Madison" + | NameOnlyNameFormat + + -- | 'RevDetailedNameFormat' region name values are like + -- 'DetailedNameFormat' but with reverse qualifiers, e.g. "US, New York, + -- Madison County" + | RevDetailedNameFormat + deriving (Show, Read, Eq) + +-- | 'RegionInfo' specifies the name of a region (in some 'RegionNameFormat') +-- and the bounds of that region as 'RegionBounds'. +data RegionInfo = + RegionInfo + { regionInfoName :: Text + , regionInfoBounds :: Maybe RegionBounds + } + deriving (Show, Read, Eq) + + +-- | 'RegionBounds' specify the corners of a bounding box around a region. +data RegionBounds = + RegionBounds + { regionBoundsMinX :: Double + , regionBoundsMaxX :: Double + , regionBoundsMinY :: Double + , regionBoundsMaxY :: Double + } + deriving (Show, Read, Eq) + +-- | The data structure returned by the eBird 'EBird.API.SubRegionListAPI' and +-- 'EBird.API.AdjacentRegionsAPI'. +data RegionListEntry = + RegionListEntry + { regionListEntryRegion :: Region + , regionListEntryName :: Text + } + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +instance FromJSON RegionCode where + parseJSON = withText "RegionCode" $ \t -> + case parseOnly parseRegionCode t of + Left _ -> fail "failed to parse region code" + Right c -> return c + +instance ToJSON RegionCode where + toJSON = String . toEBirdString + +instance FromJSON Region where + parseJSON = withText "RegionCode" $ \t -> + case parseOnly parseRegion t of + Left _ -> fail "failed to parse region" + Right r -> return r + +instance ToJSON Region where + toJSON = String . toEBirdString + +-- | Explicit instance for compatibility with their field names +instance FromJSON RegionInfo where + parseJSON = withObject "RegionInfo" $ \v -> + RegionInfo + <$> v .: "result" + <*> v .:? "bounds" + +-- | Explicit instance for compatibility with their field names +instance ToJSON RegionInfo where + toJSON RegionInfo{..} = + object $ + [ "result" .= regionInfoName + ] + <> ["bounds" .= bs | Just bs <- [regionInfoBounds]] + +-- | Explicit instance for compatibility with their field names +instance FromJSON RegionBounds where + parseJSON = withObject "RegionBounds" $ \v -> + RegionBounds + <$> v .: "minX" + <*> v .: "maxX" + <*> v .: "minY" + <*> v .: "maxY" + +-- | Explicit instance for compatibility with their field names +instance ToJSON RegionBounds where + toJSON RegionBounds{..} = + object + [ "minX" .= regionBoundsMinX + , "maxX" .= regionBoundsMaxX + , "minY" .= regionBoundsMinY + , "maxY" .= regionBoundsMaxY + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON RegionListEntry where + parseJSON = withObject "RegionListEntry" $ \v -> + RegionListEntry + <$> v .: "code" + <*> v .: "name" + +-- | Explicit instance for compatibility with their field names +instance ToJSON RegionListEntry where + toJSON RegionListEntry{..} = + object + [ "code" .= regionListEntryRegion + , "name" .= regionListEntryName + ] + +{------------------------------------------------------------------------------ + 'EBirdString' instances +------------------------------------------------------------------------------} + +-- | A 'Region' eBird string is either: +-- +-- * \"L227544\" for location regions, where L227544 is the location ID. +-- * "world" for 'World' regions. +-- * The country identifier (e.g. \"US\" for the United States) for 'Country' +-- regions. +-- * The country identifier and the state identifier separated by a hyphen +-- for 'Subnational1' regions (e.g. "US-WY" for Wyoming in the United +-- States). +-- * The county identifier, the state identifier, and the country identifier +-- separated by hyphens for 'Subnational2' regions (e.g. US-WY-013) +instance EBirdString Region where + toEBirdString = + \case + Location n -> "L" <> Text.pack (show n) + World -> "world" + Country cr -> cr + Subnational1 cr st -> cr <> "-" <> st + Subnational2 cr st cy -> cr <> "-" <> st <> "-" <> cy + + fromEBirdString str = + parseOnly parseRegion str + & left (("Failed to parse Region: " <>) . Text.pack) + +-- | Results in +-- [eBird region type format](https://documenter.getpostman.com/view/664302/S1ENwy59#382da1c8-8bff-4926-936a-a1f8b065e7d5) +instance EBirdString RegionType where + toEBirdString = + \case + CountryType -> "country" + Subnational1Type -> "subnational1" + Subnational2Type -> "subnational2" + + fromEBirdString str = + parseOnly parseRegionType str + & left (("Failed to parse RegionType: " <>) . Text.pack) + +-- | A 'RegionCode' eBird string is a comma-separated list of regions. +instance EBirdString RegionCode where + toEBirdString (RegionCode (r :| rs)) = + Text.intercalate "," $ map toEBirdString (r : rs) + + fromEBirdString str = + parseOnly parseRegionCode str + & left (("Failed to parse RegionCode: " <>) . Text.pack) + +-- | A 'RegionNameFormat' is shown as the constructor name without the +-- @NameFormat@ suffix, in all lower-case. +instance EBirdString RegionNameFormat where + toEBirdString = + \case + DetailedNameFormat -> "detailed" + DetailedNoQualNameFormat -> "detailednoqual" + FullNameFormat -> "full" + NameQualNameFormat -> "namequal" + NameOnlyNameFormat -> "nameonly" + RevDetailedNameFormat -> "revdetailed" + + fromEBirdString str = + parseOnly parseRegionNameFormat str + & left (("Failed to parse RegionNameFormat: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString Region where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString RegionType where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString RegionCode where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString RegionNameFormat where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse an eBird API region code, which is a comma-separated list of one or +-- more regions. To avoid the partial behavior of converting a 'sepBy1' result +-- into a 'NonEmpty', we manually parse the first region followed by an optional +-- tail. +parseRegionCode :: Parser RegionCode +parseRegionCode = do + r <- parseRegion + rs <- atEnd >>= \case + True -> return [] + False -> do + skip (==',') + parseRegion `sepBy` char ',' + return $ RegionCode (r :| rs) + +-- | Parse an eBird API region. This parser only ensures that the input is +-- somewhat well-formed, in that it is either: +-- +-- * A 'Location' region (an \'L\' followed by an integral number) +-- * The 'World' region (just the string "world") +-- * A 'Subnational2' region (formatted as "LETTERS-LETTERS-NUMBER" where +-- "LETTERS" is one or more letters in any case, and "NUMBERS" is an +-- integral number) +-- * A 'Subnational1' region (formatterd as "LETTERS-LETTERS") +-- * A 'Country' region (just \"LETTERS\") +parseRegion :: Parser Region +parseRegion = + choice + [ parseLocationId + , parseWorld + , parseSubnational2 + , parseSubnational1 + , parseCountry + ] + where + parseLocationId :: Parser Region + parseLocationId = do + "L" *> (Location <$> decimal) + + parseWorld :: Parser Region + parseWorld = do + "world" $> World + + parseCountry :: Parser Region + parseCountry = Country <$> letters + + parseSubnational1 :: Parser Region + parseSubnational1 = do + cr <- letters + skipHyphen + st <- letters + return $ Subnational1 cr st + + parseSubnational2 :: Parser Region + parseSubnational2 = do + cr <- letters + skipHyphen + st <- letters + skipHyphen + cy <- choice [letters, digits] + return $ Subnational2 cr st cy + + letters :: Parser Text + letters = Text.pack <$> many1 letter + + digits :: Parser Text + digits = Text.pack <$> many1 digit + + skipHyphen :: Parser () + skipHyphen = skip (=='-') + +-- | Parse an eBird API 'RegionNameFormat'. +parseRegionNameFormat :: Parser RegionNameFormat +parseRegionNameFormat = + choice + [ "detailednoqual" $> DetailedNoQualNameFormat + , "detailed" $> DetailedNameFormat + , "full" $> FullNameFormat + , "namequal" $> NameQualNameFormat + , "nameonly" $> NameOnlyNameFormat + , "revdetailed" $> RevDetailedNameFormat + ] + where + _casesCovered :: RegionNameFormat -> () + _casesCovered = + \case + DetailedNoQualNameFormat -> () + DetailedNameFormat -> () + FullNameFormat -> () + NameQualNameFormat -> () + NameOnlyNameFormat -> () + RevDetailedNameFormat -> () + +-- | Parse an eBird API 'RegionType'. +parseRegionType :: Parser RegionType +parseRegionType = + choice + [ "country" $> CountryType + , "subnational1" $> Subnational1Type + , "subnational2" $> Subnational2Type + ] + where + _casesCovered :: RegionType -> () + _casesCovered = + \case + CountryType -> () + Subnational1Type -> () + Subnational2Type -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData Region where + toUrlPiece = toEBirdString + +instance ToHttpApiData RegionType where + toUrlPiece = toEBirdString + +instance ToHttpApiData RegionCode where + toUrlPiece = toEBirdString + +instance ToHttpApiData RegionNameFormat where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/Taxonomy.hs b/ebird-api/src/EBird/API/Taxonomy.hs new file mode 100644 index 0000000..9faa1dc --- /dev/null +++ b/ebird-api/src/EBird/API/Taxonomy.hs @@ -0,0 +1,904 @@ +{-# LANGUAGE FlexibleInstances #-} + +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Use camelCase" #-} + +-- | +-- Module : EBird.API.Internal.Types.Taxonomy +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types related to eBird taxonomy-related API values. + +module EBird.API.Taxonomy + ( -- * Taxonomy types + Taxon(..) + , SpeciesCode(..) + , SpeciesCodes(..) + , TaxonomyCategory(..) + , TaxonomyCategories(..) + , TaxonomyVersionListEntry(..) + + -- * Auxiliary eBird taxonomy API types + , SPPLocale(..) + , SPPLocaleListEntry(..) + , SPPGrouping(..) + , TaxonomicGroupListEntry(..) + + -- * attoparsec parsers + , parseSpeciesCode + , parseSpeciesCodes + , parseTaxonomyCategory + , parseTaxonomyCategories + , parseSPPLocale + , parseSPPGrouping + ) where + +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Char +import Data.Function +import Data.Functor +import Data.List.NonEmpty (NonEmpty(..)) +import Data.String +import Data.Text (Text) +import Data.Text qualified as Text +import GHC.Exts +import Servant.API (ToHttpApiData(..)) + +import EBird.API.EBirdString + +-- | Taxa in the eBird taxonomy. +data Taxon = + Taxon + { -- | Scientific name, e.g. "Bombycilla garrulus/cedrorum" + taxonScientificName :: Text + + -- | Common name, e.g. "Bohemian/Cedar Waxwing" + , taxonCommonName :: Text + + -- | eBird species code, e.g. "waxwin" + , taxonSpeciesCode :: SpeciesCode + + -- | eBird species category, e.g. "slash" + -- + -- See the [eBird + -- documentation](https://science.ebird.org/en/use-ebird-data/the-ebird-taxonomy) + -- for more information on species categories + , taxonCategory :: TaxonomyCategory + + -- | A numeric value that determines the location of this taxon in the + -- taxonomy list, e.g. 29257.0 + , taxonTaxonOrder :: Double + + -- | Banding codes, e.g. [\"BOWA\"] for Bohemian Waxwing. + , taxonBandingCodes :: [Text] + + -- | Common name codes, e.g. [\"BOWA\",\"CEDW\",\"CEWA\"] + , taxonCommonNameCodes :: [Text] + + -- | Scientific name codes, e.g. [\"BOCE\",\"BOGA\"] + , taxonScientificNameCodes :: [Text] + + -- | Order, e.g. \"Passeriformes\" + , taxonOrder :: Text + + -- | Family code, e.g. "bombyc1" + , taxonFamilyCode :: Maybe Text + + -- | Family common name, e.g. \"Waxwings\" + , taxonFamilyCommonName :: Maybe Text + + -- | Family scientific name, e.g. \"Bombycillidae\" + , taxonFamilyScientificName :: Maybe Text + } + deriving (Show, Read, Eq) + +-- | eBird species codes, simply 'Text'; e.g. Gray Vireo is "gryvir", Field +-- Sparrow is "fiespa". +newtype SpeciesCode = SpeciesCode { speciesCode :: Text } + deriving (Eq, Show, Read) + +-- | A list of eBird 'SpeciesCode's. +newtype SpeciesCodes = SpeciesCodes { speciesCodes :: [SpeciesCode] } + deriving (Eq, Show, Read) + +-- | The taxonomy categories are explained in the +-- [eBird documentation](https://science.ebird.org/en/use-ebird-data/the-ebird-taxonomy). +-- Their examples are echoed in the documentation of the constructors of this +-- type. +data TaxonomyCategory = + -- | The 'Species' category simply identifies species, e.g. "Tundra Swan + -- /Cygnus columbianus/" + Species + + -- | Genus or broad identification, e.g. "swan sp. /Cygnus sp./" + | Spuh + + -- | Identifiable subspecies or group of subspecies, e.g. "Tundra Swan + -- (Bewick’s) /Cygnus columbianus bewickii/" or "Tundra Swan (Whistling) + -- /Cygnus columbianus columbianus/" + | ISSF + + -- | Identification to species pair, e.g. "Tundra/Trumpeter Swan + -- /Cygnus columbianus\/buccinator/" + | Slash + + -- | Hybrid between two species, e.g. "Tundra x Trumpeter Swan (hybrid)" + | Hybrid + + -- | Hybrid between two ISSF (subspecies or subspecies groups), e.g. + -- "Tundra Swan (Whistling x Bewick’s) + -- /Cygnus columbianus columbianus x bewickii/" + | Intergrade + + -- | Distinctly-plumaged domesticated varieties that may be free-flying + -- (these do not count on personal lists), e.g. "Mallard (Domestic type)" + | Domestic + + -- | Miscellaneous other taxa, including recently-described species yet to + -- be accepted or distinctive forms that are not universally accepted, + -- e.g. Red-tailed Hawk (abieticola), Upland Goose (Bar-breasted). + | Form + deriving (Show, Read, Eq) + +-- | 'TaxonomyCategories' values contain a 'NonEmpty' list of +-- 'TaxonomyCategory's. +newtype TaxonomyCategories = + TaxonomyCategories + { taxonomyCategoriesCategories :: NonEmpty TaxonomyCategory + } + deriving (Show, Read, Eq) + +-- | Values returned by the 'EBird.API.TaxonomyVersionsAPI'. +data TaxonomyVersionListEntry = + TaxonomyVersionListEntry + { taxonomyVersionAuthorityVersion :: Double + , taxonomyVersionLatest :: Bool + } + deriving (Show, Read, Eq) + +-- | eBird maintains many common name translations. See their +-- ["Bird Names in eBird"](https://support.ebird.org/en/support/solutions/articles/48000804865-bird-names-in-ebird) +-- documentation for a discussion of the languages they support. +-- +-- This type is an enumeration of those languages, and is used to support the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) +-- endpoints which allow a locale to be specified. +data SPPLocale = + Af -- ^ Afrikaans + | Sq -- ^ Albanians + | Ar -- ^ Arabic + | Hy -- ^ Armenian + | As -- ^ Assamese + | Ast -- ^ Asturian + | Az -- ^ Azerbaijani + | Eu -- ^ Basque + | Bn -- ^ Bengali + | Bg -- ^ Bulgarian + | Ca -- ^ Catalan + | Zh -- ^ Chinese, Mandarin (traditional) + | Zh_SIM -- ^ Chinese, Simple + | Ht_HT -- ^ Creole, Haiti + | Hr -- ^ Croatian + | Cs -- ^ Czech + | Da -- ^ Danish + | Nl -- ^ Dutch + | En -- ^ English + | En_AU -- ^ English, Australia + | En_BD -- ^ English, Bangladesh + | En_HAW -- ^ English, Hawaii + | En_HBW -- ^ English, HBW + | En_IN -- ^ English, India + | En_IOC -- ^ English, IOC + | En_KE -- ^ English, Kenya + | En_MY -- ^ English, Malaysia + | En_NZ -- ^ English, New Zealand + | En_PH -- ^ English, Philippines + | En_ZA -- ^ English, South Africa + | En_AE -- ^ English, UAE + | En_UK -- ^ English, United Kingdon + | En_US -- ^ English, United States + | Fo -- ^ Faroese + | Fi -- ^ Finnish + | Fr -- ^ French + | Fr_AOU -- ^ French, AOU + | Fr_FR -- ^ French, France + | Fr_CA -- ^ French, Canada + | Fr_GF -- ^ French, Guiana + | Fr_GP -- ^ French, Guadeloupe + | Fr_HT -- ^ French, Haiti + | Gl -- ^ Gallegan + | De -- ^ German + | El -- ^ Greek + | Gu -- ^ Gujarati + | He -- ^ Hebrew + | Hi -- ^ Hindi + | Hu -- ^ Hungarian + | Is -- ^ Icelandic + | In -- ^ Indonesian + | It -- ^ Italian + | Ja -- ^ Japanese + | Ko -- ^ Korean + | Lv -- ^ Latvian + | Lt -- ^ Lithuanian + | Ml -- ^ Malayalam + | Mr -- ^ Marathi + | Mn -- ^ Mongolian + | No -- ^ Norwegian + | Or -- ^ Odia + | Fa -- ^ Persian + | Pl -- ^ Polish + | Pt_AO -- ^ Portuguese, Angola + | Pt_RAA -- ^ Portuguese, Azores + | Pt_Br -- ^ Portuguese, Brazil + | Pt_RAM -- ^ Portuguese, Madeira + | Pt_PT -- ^ Portuguese, Portugal + | Ro -- ^ Romanian + | Ru -- ^ Russian + | Sr -- ^ Serbian + | Sk -- ^ Slovak + | Sl -- ^ Slovenian + | Es -- ^ Spanish + | Es_AR -- ^ Spanish, Argentina + | Es_CL -- ^ Spanish, Chile + | Es_CR -- ^ Spanish, Costa Rica + | Es_CU -- ^ Spanish, Cuba + | Es_DO -- ^ Spanish, Dominican Republic + | Es_EC -- ^ Spanish, Ecuador + | Es_HN -- ^ Spanish, Honduras + | Es_MX -- ^ Spanish, Mexico + | Es_PA -- ^ Spanish, Panama + | Es_PY -- ^ Spanish, Paraguay + | Es_PE -- ^ Spanish, Peru + | Es_PR -- ^ Spanish, Puerto Rico + | Es_ES -- ^ Spanish, Spain + | Es_UY -- ^ Spanish, Uruguay + | Es_VE -- ^ Spanish, Venezuela + | Sv -- ^ Swedish + | Te -- ^ Telugu + | Th -- ^ Thai + | Tr -- ^ Turkish + | Uk -- ^ Ukrainian + deriving (Show, Read, Eq) + +-- | Values returned from the 'EBird.API.TaxaLocaleCodesAPI'. +data SPPLocaleListEntry = + SPPLocaleListEntry + { -- | The code of the locale, e.g. 'En_US' + sppLocaleListEntryCode :: SPPLocale + + -- | The name, e.g. "English (United States)" + , sppLocaleListEntryName :: Text + + -- | The date and time of the last update for this locale + , sppLocaleListEntryLastUpdate :: Text + } + deriving (Show, Read, Eq) + +-- | Values represent the different ways that taxonomic groups may be grouped. +-- 'MerlinGrouping' puts like birds together, with falcons next to hawks. +-- 'EBirdGrouping' follows taxonomic order. +data SPPGrouping = MerlinGrouping | EBirdGrouping + deriving (Show, Read, Eq) + +-- | Values returned by the 'EBird.API.TaxonomicGroupsAPI'. +data TaxonomicGroupListEntry = + TaxonomicGroupListEntry + { -- | Name of the group, e.g. "Waterfowl" + taxonomicGroupListEntryName :: Text + + -- | Numeric value determining the location of this group in the list + , taxonomicGroupListEntryOrder :: Integer + + -- | The bounds of the ordering, depending on the grouping + , taxonomicGroupListEntryOrderBounds :: [(Integer, Integer)] + } + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +-- | Explicit instance for compatibility with their field names +instance FromJSON Taxon where + parseJSON = withObject "Taxon" $ \v -> + Taxon + <$> v .: "sciName" + <*> v .: "comName" + <*> v .: "speciesCode" + <*> v .: "category" + <*> v .: "taxonOrder" + <*> v .: "bandingCodes" + <*> v .: "comNameCodes" + <*> v .: "sciNameCodes" + <*> v .: "order" + <*> v .:? "familyCode" + <*> v .:? "familyComName" + <*> v .:? "familySciName" + + +-- | Explicit instance for compatibility with their field names +instance ToJSON Taxon where + toJSON Taxon{..} = + object $ + [ "sciName" .= taxonScientificName + , "comName" .= taxonCommonName + , "speciesCode" .= taxonSpeciesCode + , "category" .= taxonCategory + , "taxonOrder" .= taxonTaxonOrder + , "bandingCodes" .= taxonBandingCodes + , "comNameCodes" .= taxonCommonNameCodes + , "sciNameCodes" .= taxonScientificNameCodes + , "order" .= taxonOrder + , "familyComName" .= taxonFamilyCommonName + , "familySciName" .= taxonFamilyScientificName + ] + -- Fields that may or may not be included + <> [ "familyCode" .= c | Just c <- [taxonFamilyCode]] + <> [ "familyComName" .= n | Just n <- [taxonFamilyCommonName]] + <> [ "familySciName" .= n | Just n <- [taxonFamilyScientificName]] + +instance FromJSON SpeciesCode where + parseJSON = withText "SpeciesCode" (pure . SpeciesCode) + +instance ToJSON SpeciesCode where + toJSON SpeciesCode{..} = String speciesCode + +instance FromJSON SpeciesCodes where + parseJSON = withArray "SpeciesCodes" $ + fmap (SpeciesCodes . toList) . traverse parseJSON + +instance ToJSON SpeciesCodes where + toJSON = Array . fromList . map toJSON . speciesCodes + +instance FromJSON TaxonomyCategory where + parseJSON = withText "TaxonomyCategory" $ \t -> + case parseOnly parseTaxonomyCategory t of + Left _ -> fail "failed to parse taxonomy category" + Right r -> return r + +instance ToJSON TaxonomyCategory where + toJSON = String . toEBirdString + +-- | Explicit instance for compatibility with their field names +instance FromJSON TaxonomyVersionListEntry where + parseJSON = withObject "TaxonomyVersionListEntry" $ \v -> + TaxonomyVersionListEntry + <$> v .: "authorityVer" + <*> v .: "latest" + +-- | Explicit instance for compatibility with their field names +instance ToJSON TaxonomyVersionListEntry where + toJSON TaxonomyVersionListEntry{..} = + object + [ "authorityVer" .= taxonomyVersionAuthorityVersion + , "latest" .= taxonomyVersionLatest + ] + +instance FromJSON SPPLocale where + parseJSON = withText "SPPLocale" $ \t -> + case parseOnly parseSPPLocale t of + Left _ -> fail $ "failed to parse spp locale: " <> Text.unpack t + Right r -> return r + +instance ToJSON SPPLocale where + toJSON = String . toEBirdString + +-- | Explicit instance for compatibility with their field names +instance FromJSON SPPLocaleListEntry where + parseJSON = withObject "SPPLocaleListEntry" $ \v -> + SPPLocaleListEntry + <$> v .: "code" + <*> v .: "name" + <*> v .: "lastUpdate" + +-- | Explicit instance for compatibility with their field names +instance ToJSON SPPLocaleListEntry where + toJSON SPPLocaleListEntry{..} = + object + [ "code" .= sppLocaleListEntryCode + , "name" .= sppLocaleListEntryName + , "lastUpdate" .= sppLocaleListEntryLastUpdate + ] + +-- | Explicit instance for compatibility with their field names +instance FromJSON TaxonomicGroupListEntry where + parseJSON = withObject "TaxonomicGroupListEntry" $ \v -> + TaxonomicGroupListEntry + <$> v .: "groupName" + <*> v .: "groupOrder" + <*> v .: "taxonOrderBounds" + +-- | Explicit instance for compatibility with their field names +instance ToJSON TaxonomicGroupListEntry where + toJSON TaxonomicGroupListEntry{..} = + object + [ "groupName" .= taxonomicGroupListEntryName + , "groupOrder" .= taxonomicGroupListEntryOrder + , "taxonOrderBounds" .= taxonomicGroupListEntryOrderBounds + ] + +{------------------------------------------------------------------------------ + 'EBirdString' instances +------------------------------------------------------------------------------} + +-- | The eBird strings of the taxonomy categories are simply the lowercase +-- constructor names. +instance EBirdString TaxonomyCategory where + toEBirdString = + \case + Species -> "species" + ISSF -> "issf" + Spuh -> "spuh" + Slash -> "slash" + Hybrid -> "hybrid" + Intergrade -> "intergrade" + Domestic -> "domestic" + Form -> "form" + + fromEBirdString str = + parseOnly parseTaxonomyCategory str + & left (("Failed to parse TaxonomyCategory: " <>) . Text.pack) + +-- | The eBird string of a 'TaxonomyCategories' is the comma-separated list of +-- category strings. +instance EBirdString TaxonomyCategories where + toEBirdString (TaxonomyCategories (c :| cs)) = + Text.intercalate "," $ map toEBirdString (c : cs) + + fromEBirdString str = + parseOnly parseTaxonomyCategories str + & left (("Failed to parse TaxonomyCategories: " <>) . Text.pack) + +-- | The eBird string of a 'SpeciesCode' is simply the literal string +instance EBirdString SpeciesCode where + toEBirdString (SpeciesCode c) = c + + fromEBirdString str = + parseOnly parseSpeciesCode str + & left (("Failed to parse SpeciesCode: " <>) . Text.pack) + +-- | The eBird string of a 'SpeciesCodes' is simply the comma-separated +-- 'SpeciesCode's +instance EBirdString SpeciesCodes where + toEBirdString (SpeciesCodes cs) = Text.intercalate "," $ map toEBirdString cs + + fromEBirdString str = + parseOnly parseSpeciesCodes str + & left (("Failed to parse SpeciesCodes: " <>) . Text.pack) + +-- | The eBird strings of the species locales are simply the lowercase +-- constructor names. +instance EBirdString SPPLocale where + toEBirdString = + \case + Af -> "af" + Sq -> "sq" + Ar -> "ar" + Hy -> "hy" + As -> "as" + Ast -> "ast" + Az -> "az" + Eu -> "eu" + Bn -> "bn" + Bg -> "bg" + Ca -> "ca" + Zh -> "zh" + Zh_SIM -> "zh_SIM" + Ht_HT -> "ht_HT" + Hr -> "hr" + Cs -> "cs" + Da -> "da" + Nl -> "nl" + En -> "en" + En_AU -> "en_AU" + En_BD -> "en_BD" + En_HAW -> "en_HAW" + En_HBW -> "en_HBW" + En_IN -> "en_IN" + En_IOC -> "en_IOC" + En_KE -> "en_KE" + En_MY -> "en_MY" + En_NZ -> "en_NZ" + En_PH -> "en_PH" + En_ZA -> "en_ZA" + En_AE -> "en_AE" + En_UK -> "en_UK" + En_US -> "en_US" + Fo -> "fo" + Fi -> "fi" + Fr -> "fr" + Fr_AOU -> "fr_AOU" + Fr_FR -> "fr_FR" + Fr_CA -> "fr_CA" + Fr_GF -> "fr_GF" + Fr_GP -> "fr_GP" + Fr_HT -> "fr_HT" + Gl -> "gl" + De -> "de" + El -> "el" + Gu -> "gu" + He -> "he" + Hi -> "hi" + Hu -> "hu" + Is -> "is" + In -> "in" + It -> "it" + Ja -> "ja" + Ko -> "ko" + Lv -> "lv" + Lt -> "lt" + Ml -> "ml" + Mr -> "mr" + Mn -> "mn" + No -> "no" + Or -> "or" + Fa -> "fa" + Pl -> "pl" + Pt_AO -> "pt_AO" + Pt_RAA -> "pt_RAA" + Pt_Br -> "pt_BR" + Pt_RAM -> "pt_RAM" + Pt_PT -> "pt_PT" + Ro -> "ro" + Ru -> "ru" + Sr -> "sr" + Sk -> "sk" + Sl -> "sl" + Es -> "es" + Es_AR -> "es_AR" + Es_CL -> "es_CL" + Es_CR -> "es_CR" + Es_CU -> "es_CU" + Es_DO -> "es_DO" + Es_EC -> "es_EC" + Es_HN -> "es_HN" + Es_MX -> "es_MX" + Es_PA -> "es_PA" + Es_PY -> "es_PY" + Es_PE -> "es_PE" + Es_PR -> "es_PR" + Es_ES -> "es_ES" + Es_UY -> "es_UY" + Es_VE -> "es_VE" + Sv -> "sv" + Te -> "te" + Th -> "th" + Tr -> "tr" + Uk -> "uk" + + fromEBirdString str = + parseOnly parseSPPLocale str + & left (("Failed to parse SPPLocale: " <>) . Text.pack) + +-- | The eBird string of an 'SPPGrouping' is either "merlin" or "ebird" +instance EBirdString SPPGrouping where + toEBirdString = + \case + MerlinGrouping -> "merlin" + EBirdGrouping -> "ebird" + + fromEBirdString str = + parseOnly parseSPPGrouping str + & left (("Failed to parse SPPGrouping: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString TaxonomyCategory where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString TaxonomyCategories where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SpeciesCode where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SpeciesCodes where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SPPLocale where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString SPPGrouping where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse an eBird species code, which we loosely assume is a string of one or +-- more alphanumeric characters. +parseSpeciesCode :: Parser SpeciesCode +parseSpeciesCode = SpeciesCode . Text.pack <$> many1 (satisfy isAlphaNum) + +-- | Parse a comma separated list of zero or more 'SpeciesCode's +parseSpeciesCodes :: Parser SpeciesCodes +parseSpeciesCodes = SpeciesCodes <$> parseSpeciesCode `sepBy` char ',' + +-- | Parse an eBird 'TaxonomyCategory'. +parseTaxonomyCategory :: Parser TaxonomyCategory +parseTaxonomyCategory = + choice + [ "species" $> Species + , "spuh" $> Spuh + , "issf" $> ISSF + , "slash" $> Slash + , "hybrid" $> Hybrid + , "intergrade" $> Intergrade + , "domestic" $> Domestic + , "form" $> Form + ] + where + _casesCovered :: TaxonomyCategory -> () + _casesCovered = + \case + Species -> () + Spuh -> () + ISSF -> () + Slash -> () + Hybrid -> () + Intergrade -> () + Domestic -> () + Form -> () + +-- | Parse a list of eBird API taxononomy categories. To avoid the partial +-- behavior of converting a 'sepBy1' result into a 'NonEmpty', we manually parse +-- the first category followed by an optional tail. +parseTaxonomyCategories :: Parser TaxonomyCategories +parseTaxonomyCategories = do + c <- parseTaxonomyCategory + cs <- atEnd >>= \case + True -> return [] + False -> do + skip (==',') + parseTaxonomyCategory `sepBy` char ',' + return $ TaxonomyCategories (c :| cs) + +-- | Parse an eBird 'SPPLocale'. +parseSPPLocale :: Parser SPPLocale +parseSPPLocale = + choice + [ "af" $> Af + , "sq" $> Sq + , "ar" $> Ar + , "hy" $> Hy + , "as" $> As + , "ast" $> Ast + , "az" $> Az + , "eu" $> Eu + , "bn" $> Bn + , "bg" $> Bg + , "ca" $> Ca + , "zh" $> Zh + , "zh_SIM" $> Zh_SIM + , "ht_HT" $> Ht_HT + , "hr" $> Hr + , "cs" $> Cs + , "da" $> Da + , "nl" $> Nl + , "en" $> En + , "en_AU" $> En_AU + , "en_BD" $> En_BD + , "en_HAW" $> En_HAW + , "en_HBW" $> En_HBW + , "en_IN" $> En_IN + , "en_IOC" $> En_IOC + , "en_KE" $> En_KE + , "en_MY" $> En_MY + , "en_NZ" $> En_NZ + , "en_PH" $> En_PH + , "en_ZA" $> En_ZA + , "en_AE" $> En_AE + , "en_UK" $> En_UK + , "en_US" $> En_US + , "fo" $> Fo + , "fi" $> Fi + , "fr" $> Fr + , "fr_AOU" $> Fr_AOU + , "fr_FR" $> Fr_FR + , "fr_CA" $> Fr_CA + , "fr_GF" $> Fr_GF + , "fr_GP" $> Fr_GP + , "fr_HT" $> Fr_HT + , "gl" $> Gl + , "de" $> De + , "el" $> El + , "gu" $> Gu + , "he" $> He + , "hi" $> Hi + , "hu" $> Hu + , "is" $> Is + , "in" $> In + , "it" $> It + , "ja" $> Ja + , "ko" $> Ko + , "lv" $> Lv + , "lt" $> Lt + , "ml" $> Ml + , "mr" $> Mr + , "mn" $> Mn + , "no" $> No + , "or" $> Or + , "fa" $> Fa + , "pl" $> Pl + , "pt_AO" $> Pt_AO + , "pt_RAA" $> Pt_RAA + , "pt_BR" $> Pt_Br + , "pt_RAM" $> Pt_RAM + , "pt_PT" $> Pt_PT + , "ro" $> Ro + , "ru" $> Ru + , "sr" $> Sr + , "sk" $> Sk + , "sl" $> Sl + , "es" $> Es + , "es_AR" $> Es_AR + , "es_CL" $> Es_CL + , "es_CR" $> Es_CR + , "es_CU" $> Es_CU + , "es_DO" $> Es_DO + , "es_EC" $> Es_EC + , "es_HN" $> Es_HN + , "es_MX" $> Es_MX + , "es_PA" $> Es_PA + , "es_PY" $> Es_PY + , "es_PE" $> Es_PE + , "es_PR" $> Es_PR + , "es_ES" $> Es_ES + , "es_UY" $> Es_UY + , "es_VE" $> Es_VE + , "sv" $> Sv + , "te" $> Te + , "th" $> Th + , "tr" $> Tr + , "uk" $> Uk + ] + where + _casesCovered :: SPPLocale -> () + _casesCovered = + \case + Af -> () + Sq -> () + Ar -> () + Hy -> () + As -> () + Ast -> () + Az -> () + Eu -> () + Bn -> () + Bg -> () + Ca -> () + Zh -> () + Zh_SIM -> () + Ht_HT -> () + Hr -> () + Cs -> () + Da -> () + Nl -> () + En -> () + En_AU -> () + En_BD -> () + En_HAW -> () + En_HBW -> () + En_IN -> () + En_IOC -> () + En_KE -> () + En_MY -> () + En_NZ -> () + En_PH -> () + En_ZA -> () + En_AE -> () + En_UK -> () + En_US -> () + Fo -> () + Fi -> () + Fr -> () + Fr_AOU -> () + Fr_FR -> () + Fr_CA -> () + Fr_GF -> () + Fr_GP -> () + Fr_HT -> () + Gl -> () + De -> () + El -> () + Gu -> () + He -> () + Hi -> () + Hu -> () + Is -> () + In -> () + It -> () + Ja -> () + Ko -> () + Lv -> () + Lt -> () + Ml -> () + Mr -> () + Mn -> () + No -> () + Or -> () + Fa -> () + Pl -> () + Pt_AO -> () + Pt_RAA -> () + Pt_Br -> () + Pt_RAM -> () + Pt_PT -> () + Ro -> () + Ru -> () + Sr -> () + Sk -> () + Sl -> () + Es -> () + Es_AR -> () + Es_CL -> () + Es_CR -> () + Es_CU -> () + Es_DO -> () + Es_EC -> () + Es_HN -> () + Es_MX -> () + Es_PA -> () + Es_PY -> () + Es_PE -> () + Es_PR -> () + Es_ES -> () + Es_UY -> () + Es_VE -> () + Sv -> () + Te -> () + Th -> () + Tr -> () + Uk -> () + +-- | Parse an eBird 'SPPGrouping'. +parseSPPGrouping :: Parser SPPGrouping +parseSPPGrouping = + choice + [ "merlin" $> MerlinGrouping + , "ebird" $> EBirdGrouping + ] + where + _casesCovered :: SPPGrouping -> () + _casesCovered = + \case + MerlinGrouping -> () + EBirdGrouping -> () + +{------------------------------------------------------------------------------ + 'ToHttpApiData' instances +------------------------------------------------------------------------------} + +instance ToHttpApiData SpeciesCode where + toUrlPiece = toEBirdString + +instance ToHttpApiData SpeciesCodes where + toUrlPiece = Text.intercalate "," . map toEBirdString . speciesCodes + +instance ToHttpApiData TaxonomyCategories where + toUrlPiece = toEBirdString + +instance ToHttpApiData SPPLocale where + toUrlPiece = toEBirdString + +instance ToHttpApiData SPPGrouping where + toUrlPiece = toEBirdString diff --git a/ebird-api/src/EBird/API/Util/Time.hs b/ebird-api/src/EBird/API/Util/Time.hs new file mode 100644 index 0000000..a40eec3 --- /dev/null +++ b/ebird-api/src/EBird/API/Util/Time.hs @@ -0,0 +1,191 @@ +{-# LANGUAGE GeneralisedNewtypeDeriving #-} +{-# LANGUAGE DerivingStrategies #-} + +-- | +-- Module : EBird.API.Util.Time +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Utilities for parsing and printing dates and times that the eBird API +-- provides. + +module EBird.API.Util.Time ( + -- * Date and time types + EBirdDate(..) + , EBirdTime(..) + , EBirdDateTime(..) + + -- * Conversions + , eBirdDateToGregorian + + -- * attoparsec parsers + , parseEBirdDate + , parseEBirdTime + , parseEBirdDateTime + ) where + +import Control.Applicative +import Control.Arrow +import Data.Aeson +import Data.Attoparsec.Text +import Data.Attoparsec.Time +import Data.Function +import Data.String +import Data.Text qualified as Text +import Data.Time + +import EBird.API.EBirdString + +{------------------------------------------------------------------------------ + Date and time types +------------------------------------------------------------------------------} + +-- | An 'EBirdDate' is simply a 'Day'. +newtype EBirdDate = EBirdDate { eBirdDate :: Day } + deriving (Show, Read, Eq, Ord) + deriving newtype (Enum) + +-- | Since times that come from the eBird API are not provided with a time zone, +-- an 'EBirdTime' is simply a 'TimeOfDay'. Since eBird times are only provided +-- up to the minute, the 'todSec' value will always be 0. +newtype EBirdTime = EBirdTime { eBirdTime :: TimeOfDay } + deriving (Show, Read, Eq, Ord) + +-- | Dates and times that come from the eBird API are not provided with a time +-- zone. All we can do is track the 'Data.Time.Day' and 'Data.Time.TimeOfDay' +-- with a 'Data.Time.LocalTime'. Comparison of, for example, +-- 'EBird.API.Observation's that happened in different time zones must therefore +-- be done carefully. +newtype EBirdDateTime = EBirdDateTime { eBirdDateTime :: LocalTime } + deriving (Show, Read, Eq, Ord) + +{------------------------------------------------------------------------------ + Conversions +------------------------------------------------------------------------------} + +-- | Convert an 'EBirdDate' to a gregorian representation. The first element is +-- the year, the second is the month in the year (1 - 12), and the third is the +-- day in the month. +eBirdDateToGregorian :: EBirdDate -> (Integer, Integer, Integer) +eBirdDateToGregorian EBirdDate{..} = + (y, fromIntegral m, fromIntegral d) + where + (y, m, d) = toGregorian eBirdDate + +{------------------------------------------------------------------------------ + aeson instances +------------------------------------------------------------------------------} + +instance FromJSON EBirdDate where + parseJSON = withText "EBirdDate" $ \t -> + case parseOnly parseEBirdDate t of + Left _ -> fail "failed to parse eBird date" + Right r -> return r + +instance ToJSON EBirdDate where + toJSON = String . toEBirdString + +instance FromJSON EBirdTime where + parseJSON = withText "EBirdTime" $ \t -> + case parseOnly parseEBirdTime t of + Left _ -> fail "failed to parse eBird time" + Right r -> return r + +instance ToJSON EBirdTime where + toJSON = String . toEBirdString + +instance FromJSON EBirdDateTime where + parseJSON = withText "EBirdDateTime" $ \t -> + case parseOnly parseEBirdDateTime t of + Left _ -> fail "failed to parse eBird datetime" + Right r -> return r + +instance ToJSON EBirdDateTime where + toJSON = String . toEBirdString + +{------------------------------------------------------------------------------ + EBirdString instances +------------------------------------------------------------------------------} + +-- | eBird dates are formatted as YYYY-MM-DD, with 0 padding where necessary. +instance EBirdString EBirdDate where + toEBirdString = + Text.pack . formatTime defaultTimeLocale "%04Y-%02m-%02d" . eBirdDate + + fromEBirdString str = + parseOnly parseEBirdDate str + & left (("Failed to parse EBirdDate: " <>) . Text.pack) + +-- | eBird times are formatted as HH:MM, with 0 padding where necessary. +instance EBirdString EBirdTime where + toEBirdString = + Text.pack . formatTime defaultTimeLocale "%02H:%02M" . eBirdTime + + fromEBirdString str = + parseOnly parseEBirdTime str + & left (("Failed to parse EBirdTime: " <>) . Text.pack) + +-- | eBird datetimes are formatted as YYYY-MM-DD HH:MM, with 0 padding where +-- necessary. +instance EBirdString EBirdDateTime where + toEBirdString = + Text.pack + . formatTime defaultTimeLocale "%04Y-%02m-%02d %02H:%02M" + . eBirdDateTime + + fromEBirdString str = + parseOnly parseEBirdDateTime str + & left (("Failed to parse EBirdDateTime: " <>) . Text.pack) + +{------------------------------------------------------------------------------ + IsString instances +------------------------------------------------------------------------------} + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString EBirdDate where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString EBirdTime where + fromString = unsafeFromEBirdString . Text.pack + +-- | Use this instance carefully! It throws runtime exceptions if the string is +-- malformatted. +instance IsString EBirdDateTime where + fromString = unsafeFromEBirdString . Text.pack + +{------------------------------------------------------------------------------ + attoparsec parsers +------------------------------------------------------------------------------} + +-- | Parse an eBird date. Most eBird dates are formatted as YYYY-MM-DD, but the +-- 'EBird.API.ChecklistFeedAPI' gives dates in a format like "19 Jul 2023". So, +-- we try parsing the first format using 'day', and then use a custom +-- 'parseTimeM' format for the latter format if that fails. +parseEBirdDate :: Parser EBirdDate +parseEBirdDate = tryDay <|> tryParseTimeM + where + tryDay :: Parser EBirdDate + tryDay = EBirdDate <$> day + + tryParseTimeM :: Parser EBirdDate + tryParseTimeM = do + input <- takeText + d <- parseTimeM + False + defaultTimeLocale + "%e %b %Y" + (Text.unpack input) + return (EBirdDate d) + +-- | Parse an eBird time (just uses 'timeOfDay'). +parseEBirdTime :: Parser EBirdTime +parseEBirdTime = EBirdTime <$> timeOfDay + +-- | Parse an eBird datetime (just uses 'localTime'). +parseEBirdDateTime :: Parser EBirdDateTime +parseEBirdDateTime = EBirdDateTime <$> localTime diff --git a/ebird-cli/CHANGELOG.md b/ebird-cli/CHANGELOG.md new file mode 100644 index 0000000..ac8468b --- /dev/null +++ b/ebird-cli/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for ebird-cli + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/ebird-cli/LICENSE b/ebird-cli/LICENSE new file mode 100644 index 0000000..d397496 --- /dev/null +++ b/ebird-cli/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2023 Finley McIlwaine + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ebird-cli/Main.hs b/ebird-cli/Main.hs new file mode 100644 index 0000000..ebf71fc --- /dev/null +++ b/ebird-cli/Main.hs @@ -0,0 +1,16 @@ +module Main where + +-- | +-- Module : Main +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- An executable command-line utility for interacting with the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59). + +import EBird.CLI (eBirdCli) + +main :: IO () +main = eBirdCli diff --git a/ebird-cli/README.md b/ebird-cli/README.md new file mode 100644 index 0000000..4168d5f --- /dev/null +++ b/ebird-cli/README.md @@ -0,0 +1,34 @@ +# ebird-cli + +*Go birding on your command line!* + +## Installation + +Using cabal: + +``` +cabal install ebird-cli +``` + +This will install the `ebird` executable. To see the list of commands, try: + +``` +ebird --help +``` + +## Usage + +There is one command per endpoint of the official eBird API, as listed in [their +documentation][api-docs]. Some commands require an API key, which can be +obtained [here](https://ebird.org/api/keygen). If a command requires an API key, +the key can be provided via the `--key` or `-k` option. If no key is provided +via the options, the CLI will attempt to read a key from a file located at +`$HOME/.ebird/key.txt`. If no key is available there (and none was provided via +the options), the command will fail. + +Please don't hesitate to [open an +issue](https://github.com/FinleyMcIlwaine/ebird-haskell/issues) (or a [pull request](https://github.com/FinleyMcIlwaine/ebird-haskell/pulls)!) if something +doesn't work as expected. + + +[api-docs]: https://documenter.getpostman.com/view/664302/S1ENwy59 diff --git a/ebird-cli/ebird-cli.cabal b/ebird-cli/ebird-cli.cabal new file mode 100644 index 0000000..d3d3699 --- /dev/null +++ b/ebird-cli/ebird-cli.cabal @@ -0,0 +1,62 @@ +cabal-version: 3.4 +name: ebird-cli +version: 0.1.0.0 +synopsis: + A command-line utility for interacting with the + eBird API. +description: + A library containing the functions used to implement a command-line utility + for interacting with the + [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59). +license: MIT +license-file: LICENSE +author: Finley McIlwaine +maintainer: finleymcilwaine@gmail.com +copyright: 2023 Finley McIlwaine +category: Web +build-type: Simple +extra-doc-files: CHANGELOG.md +bug-reports: https://github.com/FinleyMcIlwaine/ebird-haskell/issues +homepage: https://github.com/FinleyMcIlwaine/ebird-haskell + +tested-with: + GHC == 8.10.7 + , GHC == 9.2.7 + , GHC == 9.4.5 + , GHC == 9.6.2 + +common common + build-depends: + base >= 4.13.3.0 && < 4.19 + default-extensions: + ImportQualifiedPost + LambdaCase + OverloadedStrings + RecordWildCards + default-language: Haskell2010 + +executable ebird + import: common + main-is: + Main.hs + build-depends: + ebird-cli + +library + import: common + exposed-modules: + EBird.CLI + build-depends: + ebird-api >= 0.1.0.0 && < 0.2 + , ebird-client >= 0.1.0.0 && < 0.2 + + , aeson >= 1.5.6.0 && < 2.2 + , aeson-pretty >= 0.8.8 && < 0.9 + , attoparsec >= 0.14.1 && < 0.15 + , bytestring >= 0.10.12.0 && < 0.12 + , directory >= 1.3.6.0 && < 1.4 + , filepath >= 1.4.2.1 && < 1.5 + , optparse-applicative >= 0.16.1.0 && < 0.19 + , text >= 1.2.4.1 && < 2.1 + hs-source-dirs: + src diff --git a/ebird-cli/src/EBird/CLI.hs b/ebird-cli/src/EBird/CLI.hs new file mode 100644 index 0000000..17b4fe1 --- /dev/null +++ b/ebird-cli/src/EBird/CLI.hs @@ -0,0 +1,1621 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE DataKinds #-} + +-- | +-- Module : EBird.CLI +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Functions used to implement a command-line utility for interacting with the +-- [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59). + +module EBird.CLI where + +import Control.Exception +import Data.Aeson +import Data.Aeson.Encode.Pretty +import Data.Attoparsec.Text qualified as A +#if !MIN_VERSION_bytestring(0,11,0) +-- Data.ByteString.Char8 does not export 'fromStrict' until 0.11.0.0 +import Data.ByteString.Lazy qualified as BS (fromStrict) +#endif +import Data.ByteString.Char8 qualified as BS +import Data.Char +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Text.IO qualified as Text +import Options.Applicative +import System.Directory +import System.Environment +import System.Exit +import System.FilePath +import Text.Printf + +import EBird.API +import EBird.Client + +-- | Entry point for the @ebird@ CLI. Parses the command arguments, selects an +-- API key, and executes the command. +-- +-- The API key may be provided as a command-line option. If the key option is +-- not provided, it is read from the file @~\/.ebird\/key.txt@. If that file is +-- unavailable for reading, and no key option is provided, the application +-- exits (if the command requires a key). +eBirdCli :: IO () +eBirdCli = do + (optKey, c) <- execParser opts + fileKey <- readEBirdAPIKey + let getKey = + case optKey <|> fileKey of + Just k -> pure k + Nothing -> do + eBirdFail + ( "An API key is required for this command, but no API key\n" <> + "was provided via the `-k` option and no API key file was\n" <> + "found at ~/.ebird/key.txt. Exiting." + ) + runEBirdCommand getKey c + where + opts :: ParserInfo (Maybe Text, EBirdCommand) + opts = + info + (eBirdCommand <**> helper) + ( header "ebird - Go birding on your command line!" + <> progDesc "Query the official eBird API" + ) + +-- | Read an eBird API key from @~\/.ebird\/key.txt@. If the file exists and is +-- available for reading, the result is 'Just' the contents of the file, +-- stripped of leading/trailing whitespace. Otherwise, the result is 'Nothing'. +readEBirdAPIKey :: IO (Maybe Text) +readEBirdAPIKey = do + home <- getHomeDirectory + catch + ( Just . Text.strip <$> + Text.readFile (home ".ebird" "key" <.> "txt") + ) + ( \(_ :: IOException) -> pure Nothing + ) + +-- | Run an 'EBirdCommand' with a given API key. +runEBirdCommand + :: IO Text + -- ^ Get an API key (this may fail if not found or provided) + -> EBirdCommand + -- ^ Command to execute + -> IO () +runEBirdCommand getAPIKey = \case + RecentObservationsCommand RecentObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentObservations_ apiKey + recentObservationsRegion + recentObservationsBack + recentObservationsCategories + recentObservationsHotspots + recentObservationsProvisionals + recentObservationsMaxResults + recentObservationsSubRegions + recentObservationsSPPLocale + handleResponse res + + RecentNotableObservationsCommand RecentNotableObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentNotableObservations_ apiKey + recentNotableObservationsRegion + recentNotableObservationsBack + recentNotableObservationsDetail + recentNotableObservationsHotspots + recentNotableObservationsMaxResults + recentNotableObservationsSubRegions + recentNotableObservationsSPPLocale + handleResponse res + + RecentSpeciesObservationsCommand RecentSpeciesObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentSpeciesObservations_ apiKey + recentSpeciesObservationsRegion + recentSpeciesObservationsSpecies + recentSpeciesObservationsBack + recentSpeciesObservationsHotspots + recentSpeciesObservationsProvisionals + recentSpeciesObservationsMaxResults + recentSpeciesObservationsSubRegions + recentSpeciesObservationsSPPLocale + handleResponse res + + RecentNearbyObservationsCommand RecentNearbyObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentNearbyObservations_ apiKey + recentNearbyObservationsLatitude + recentNearbyObservationsLongitude + recentNearbyObservationsDist + recentNearbyObservationsBack + recentNearbyObservationsCategories + recentNearbyObservationsHotspots + recentNearbyObservationsProvisionals + recentNearbyObservationsMaxResults + recentNearbyObservationsSortBy + recentNearbyObservationsSPPLocale + handleResponse res + + RecentNearbySpeciesObservationsCommand RecentNearbySpeciesObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentNearbySpeciesObservations_ apiKey + recentNearbySpeciesObservationsSpecies + recentNearbySpeciesObservationsLatitude + recentNearbySpeciesObservationsLongitude + recentNearbySpeciesObservationsDist + recentNearbySpeciesObservationsBack + recentNearbySpeciesObservationsCategories + recentNearbySpeciesObservationsHotspots + recentNearbySpeciesObservationsProvisionals + recentNearbySpeciesObservationsMaxResults + recentNearbySpeciesObservationsSortBy + recentNearbySpeciesObservationsSPPLocale + handleResponse res + + RecentNearestSpeciesObservationsCommand RecentNearestSpeciesObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentNearestSpeciesObservations_ apiKey + recentNearestSpeciesObservationsSpecies + recentNearestSpeciesObservationsLatitude + recentNearestSpeciesObservationsLongitude + recentNearestSpeciesObservationsDist + recentNearestSpeciesObservationsBack + recentNearestSpeciesObservationsHotspots + recentNearestSpeciesObservationsProvisionals + recentNearestSpeciesObservationsMaxResults + recentNearestSpeciesObservationsSPPLocale + handleResponse res + + RecentNearbyNotableObservationsCommand RecentNearbyNotableObservationsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentNearbyNotableObservations_ apiKey + recentNearbyNotableObservationsLatitude + recentNearbyNotableObservationsLongitude + recentNearbyNotableObservationsDist + recentNearbyNotableObservationsDetail + recentNearbyNotableObservationsBack + recentNearbyNotableObservationsHotspots + recentNearbyNotableObservationsMaxResults + recentNearbyNotableObservationsSPPLocale + handleResponse res + + HistoricalObservationsCommand HistoricalObservationsOptions{..} -> do + apiKey <- getAPIKey + let (y,m,d) = eBirdDateToGregorian historicalObservationsDate + res <- askEBird $ + historicalObservations_ apiKey + historicalObservationsRegion + y m d + historicalObservationsCategories + historicalObservationsDetail + historicalObservationsHotspots + historicalObservationsProvisionals + historicalObservationsMaxResults + historicalObservationsRank + historicalObservationsSubRegions + historicalObservationsSPPLocale + handleResponse res + + RecentChecklistsCommand RecentChecklistsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + recentChecklists_ apiKey + recentChecklistsRegion + recentChecklistsMaxResults + handleResponse res + + Top100Command Top100Options{..} -> do + apiKey <- getAPIKey + let (y,m,d) = eBirdDateToGregorian top100Date + res <- askEBird $ + top100_ apiKey + top100Region + y m d + top100RankedBy + top100MaxResults + handleResponse res + + ChecklistFeedCommand ChecklistFeedOptions{..} -> do + apiKey <- getAPIKey + let (y,m,d) = eBirdDateToGregorian checklistFeedDate + res <- askEBird $ + checklistFeed_ apiKey + checklistFeedRegion + y m d + checklistFeedSortBy + checklistFeedMaxResults + handleResponse res + + RegionalStatisticsCommand RegionalStatisticsOptions{..} -> do + apiKey <- getAPIKey + let (y,m,d) = eBirdDateToGregorian regionalStatisticsDate + res <- askEBird $ + regionalStatistics_ apiKey + regionalStatisticsRegion + y m d + handleResponse res + + SpeciesListCommand SpeciesListOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + speciesList_ apiKey speciesListRegion + handleResponse res + + ViewChecklistCommand ViewChecklistOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + viewChecklist_ apiKey viewChecklistSubId + handleResponse res + + RegionHotspotsCommand RegionHotspotsOptions{..} -> do + res <- askEBird $ + regionHotspots_ + regionHotspotsRegion + regionHotspotsBack + regionHotspotsFmt + handleResponse res + + NearbyHotspotsCommand NearbyHotspotsOptions{..} -> do + res <- askEBird $ + nearbyHotspots_ + nearbyHotspotsLatitude + nearbyHotspotsLongitude + nearbyHotspotsDist + nearbyHotspotsBack + nearbyHotspotsFmt + handleResponse res + + HotspotInfoCommand HotspotInfoOptions{..} -> do + res <- askEBird $ hotspotInfo hotspotInfoLocation + handleResponse res + + TaxonomyCommand TaxonomyOptions{..} -> do + res <- askEBird $ + taxonomy_ + taxonomyTaxonomyCategories + taxonomyFormat + taxonomySPPLocale + taxonomySpecies + taxonomyVersion + handleResponse res + + TaxonomicFormsCommand TaxonomicFormsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + taxonomicForms_ apiKey + taxonomicFormsSpecies + handleResponse res + + TaxaLocaleCodesCommand TaxaLocaleCodesOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + taxaLocaleCodes_ apiKey + taxaLocaleCodesAcceptLanguage + handleResponse res + + TaxonomyVersionsCommand -> askEBird taxonomyVersions >>= handleResponse + + TaxonomicGroupsCommand TaxonomicGroupsOptions{..} -> do + res <- askEBird $ + taxonomicGroups_ + taxonomicGroupsSPPGrouping + taxonomicGroupsSPPLocale + handleResponse res + + RegionInfoCommand RegionInfoOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + regionInfo_ apiKey + regionInfoRegion + regionInfoRegionNameFormat + handleResponse res + + SubRegionListCommand SubRegionListOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + subRegionList apiKey + subRegionListRegionType + subRegionListParentRegionCode + handleResponse res + + AdjacentRegionsCommand AdjacentRegionsOptions{..} -> do + apiKey <- getAPIKey + res <- askEBird $ + adjacentRegions apiKey + adjacentRegionsRegion + handleResponse res + where + handleResponse :: ToJSON a => Either ClientError a -> IO () + handleResponse = \case + Right v -> printResJSON v + Left err -> + eBirdFail $ + "An error occurred while executing the request:\n" <> + show err + + +-- | Simply prints a value as prettified JSON +printResJSON :: ToJSON a => a -> IO () +printResJSON = BS.putStrLn . BS.toStrict . encodePretty + +-- | Print a string to stderr, prepended with a context string, and exit with +-- failure status. +eBirdFail :: String -> IO a +eBirdFail msg = do + progName <- getProgName + die + ( progName <> ": Something went wrong!\n\n" + <> unlines (map (" " <>) (lines msg)) + ) + +{------------------------------------------------------------------------------ + Command types +------------------------------------------------------------------------------} + +-- | Each 'EBirdCommand' corresponds to an endpoint of the eBird API +data EBirdCommand + = RecentObservationsCommand RecentObservationsOptions + | RecentNotableObservationsCommand RecentNotableObservationsOptions + | RecentSpeciesObservationsCommand RecentSpeciesObservationsOptions + | RecentNearbyObservationsCommand RecentNearbyObservationsOptions + | RecentNearbySpeciesObservationsCommand RecentNearbySpeciesObservationsOptions + | RecentNearestSpeciesObservationsCommand RecentNearestSpeciesObservationsOptions + | RecentNearbyNotableObservationsCommand RecentNearbyNotableObservationsOptions + | HistoricalObservationsCommand HistoricalObservationsOptions + | RecentChecklistsCommand RecentChecklistsOptions + | Top100Command Top100Options + | ChecklistFeedCommand ChecklistFeedOptions + | RegionalStatisticsCommand RegionalStatisticsOptions + | SpeciesListCommand SpeciesListOptions + | ViewChecklistCommand ViewChecklistOptions + | RegionHotspotsCommand RegionHotspotsOptions + | NearbyHotspotsCommand NearbyHotspotsOptions + | HotspotInfoCommand HotspotInfoOptions + | TaxonomyCommand TaxonomyOptions + | TaxonomicFormsCommand TaxonomicFormsOptions + | TaxaLocaleCodesCommand TaxaLocaleCodesOptions + | TaxonomyVersionsCommand + | TaxonomicGroupsCommand TaxonomicGroupsOptions + | RegionInfoCommand RegionInfoOptions + | SubRegionListCommand SubRegionListOptions + | AdjacentRegionsCommand AdjacentRegionsOptions + deriving (Show, Eq) + +-- | Options for the @recent-observations@ command. +data RecentObservationsOptions = + RecentObservationsOptions + { recentObservationsRegion :: RegionCode + , recentObservationsBack :: Maybe Integer + , recentObservationsCategories :: Maybe TaxonomyCategories + , recentObservationsHotspots :: Maybe Bool + , recentObservationsProvisionals :: Maybe Bool + , recentObservationsMaxResults :: Maybe Integer + , recentObservationsSubRegions :: Maybe RegionCode + , recentObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-notable-observations@ command. +data RecentNotableObservationsOptions = + RecentNotableObservationsOptions + { recentNotableObservationsRegion :: RegionCode + , recentNotableObservationsBack :: Maybe Integer + , recentNotableObservationsDetail :: Maybe DetailLevel + , recentNotableObservationsHotspots :: Maybe Bool + , recentNotableObservationsMaxResults :: Maybe Integer + , recentNotableObservationsSubRegions :: Maybe RegionCode + , recentNotableObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-species-observations@ command. +data RecentSpeciesObservationsOptions = + RecentSpeciesObservationsOptions + { recentSpeciesObservationsRegion :: RegionCode + , recentSpeciesObservationsSpecies :: SpeciesCode + , recentSpeciesObservationsBack :: Maybe Integer + , recentSpeciesObservationsHotspots :: Maybe Bool + , recentSpeciesObservationsProvisionals :: Maybe Bool + , recentSpeciesObservationsMaxResults :: Maybe Integer + , recentSpeciesObservationsSubRegions :: Maybe RegionCode + , recentSpeciesObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-nearby-observations@ command. +data RecentNearbyObservationsOptions = + RecentNearbyObservationsOptions + { recentNearbyObservationsLatitude :: Double + , recentNearbyObservationsLongitude :: Double + , recentNearbyObservationsDist :: Maybe Integer + , recentNearbyObservationsBack :: Maybe Integer + , recentNearbyObservationsCategories :: Maybe TaxonomyCategories + , recentNearbyObservationsHotspots :: Maybe Bool + , recentNearbyObservationsProvisionals :: Maybe Bool + , recentNearbyObservationsMaxResults :: Maybe Integer + , recentNearbyObservationsSortBy :: Maybe SortObservationsBy + , recentNearbyObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-nearby-species-observations@ command. +data RecentNearbySpeciesObservationsOptions = + RecentNearbySpeciesObservationsOptions + { recentNearbySpeciesObservationsSpecies :: SpeciesCode + , recentNearbySpeciesObservationsLatitude :: Double + , recentNearbySpeciesObservationsLongitude :: Double + , recentNearbySpeciesObservationsDist :: Maybe Integer + , recentNearbySpeciesObservationsBack :: Maybe Integer + , recentNearbySpeciesObservationsCategories :: Maybe TaxonomyCategories + , recentNearbySpeciesObservationsHotspots :: Maybe Bool + , recentNearbySpeciesObservationsProvisionals :: Maybe Bool + , recentNearbySpeciesObservationsMaxResults :: Maybe Integer + , recentNearbySpeciesObservationsSortBy :: Maybe SortObservationsBy + , recentNearbySpeciesObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-nearest-species-observations@ command. +data RecentNearestSpeciesObservationsOptions = + RecentNearestSpeciesObservationsOptions + { recentNearestSpeciesObservationsSpecies :: SpeciesCode + , recentNearestSpeciesObservationsLatitude :: Double + , recentNearestSpeciesObservationsLongitude :: Double + , recentNearestSpeciesObservationsDist :: Maybe Integer + , recentNearestSpeciesObservationsBack :: Maybe Integer + , recentNearestSpeciesObservationsHotspots :: Maybe Bool + , recentNearestSpeciesObservationsProvisionals :: Maybe Bool + , recentNearestSpeciesObservationsMaxResults :: Maybe Integer + , recentNearestSpeciesObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-nearby-notable-observations@ command. +data RecentNearbyNotableObservationsOptions = + RecentNearbyNotableObservationsOptions + { recentNearbyNotableObservationsLatitude :: Double + , recentNearbyNotableObservationsLongitude :: Double + , recentNearbyNotableObservationsDist :: Maybe Integer + , recentNearbyNotableObservationsDetail :: Maybe DetailLevel + , recentNearbyNotableObservationsBack :: Maybe Integer + , recentNearbyNotableObservationsHotspots :: Maybe Bool + , recentNearbyNotableObservationsMaxResults :: Maybe Integer + , recentNearbyNotableObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @historical-observations@ command. +data HistoricalObservationsOptions = + HistoricalObservationsOptions + { historicalObservationsRegion :: RegionCode + , historicalObservationsDate :: EBirdDate + , historicalObservationsCategories :: Maybe TaxonomyCategories + , historicalObservationsDetail :: Maybe DetailLevel + , historicalObservationsHotspots :: Maybe Bool + , historicalObservationsProvisionals :: Maybe Bool + , historicalObservationsMaxResults :: Maybe Integer + , historicalObservationsRank :: Maybe SelectObservation + , historicalObservationsSubRegions :: Maybe RegionCode + , historicalObservationsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @recent-checklists@ command. +data RecentChecklistsOptions = + RecentChecklistsOptions + { recentChecklistsRegion :: RegionCode + , recentChecklistsMaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Options for the @top-100@ command. +data Top100Options = + Top100Options + { top100Region :: Region + , top100Date :: EBirdDate + , top100RankedBy :: Maybe RankTop100By + , top100MaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Options for the @checklist-feed@ command. +data ChecklistFeedOptions = + ChecklistFeedOptions + { checklistFeedRegion :: Region + , checklistFeedDate :: EBirdDate + , checklistFeedSortBy :: Maybe SortChecklistsBy + , checklistFeedMaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Options for the @regional-statistics@ command. +data RegionalStatisticsOptions = + RegionalStatisticsOptions + { regionalStatisticsRegion :: Region + , regionalStatisticsDate :: EBirdDate + } + deriving (Show, Read, Eq) + +-- | Options for the @species-list@ command. +newtype SpeciesListOptions = + SpeciesListOptions + { speciesListRegion :: Region + } + deriving (Show, Read, Eq) + +-- | Options for the @view-checklist@ command. +newtype ViewChecklistOptions = + ViewChecklistOptions + { viewChecklistSubId :: Text + } + deriving (Show, Read, Eq) + +-- | Options for the @region-hotspots@ command. +data RegionHotspotsOptions = + RegionHotspotsOptions + { regionHotspotsRegion :: RegionCode + , regionHotspotsBack :: Maybe Integer + , regionHotspotsFmt :: Maybe CSVOrJSONFormat + } + deriving (Show, Read, Eq) + +-- | Options for the @nearby-hotspots@ command. +data NearbyHotspotsOptions = + NearbyHotspotsOptions + { nearbyHotspotsLatitude :: Double + , nearbyHotspotsLongitude :: Double + , nearbyHotspotsBack :: Maybe Integer + , nearbyHotspotsDist :: Maybe Integer + , nearbyHotspotsFmt :: Maybe CSVOrJSONFormat + } + deriving (Show, Read, Eq) + +-- | Options for the @hotspot-info@ command. +newtype HotspotInfoOptions = + HotspotInfoOptions + { hotspotInfoLocation :: Text + } + deriving (Show, Read, Eq) + +-- | Options for the @nearby-hotspots@ command. +data TaxonomyOptions = + TaxonomyOptions + { taxonomyTaxonomyCategories :: Maybe TaxonomyCategories + , taxonomyFormat :: Maybe CSVOrJSONFormat + , taxonomySPPLocale :: Maybe SPPLocale + , taxonomySpecies :: Maybe SpeciesCodes + , taxonomyVersion :: Maybe Text + } + deriving (Show, Read, Eq) + +-- | Options for the @taxonomic-forms@ command. +newtype TaxonomicFormsOptions = + TaxonomicFormsOptions + { taxonomicFormsSpecies :: SpeciesCode + } + deriving (Show, Read, Eq) + +-- | Options for the @taxa-locale-codes@ command. +newtype TaxaLocaleCodesOptions = + TaxaLocaleCodesOptions + { taxaLocaleCodesAcceptLanguage :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @taxonomic-groups@ command. +data TaxonomicGroupsOptions = + TaxonomicGroupsOptions + { taxonomicGroupsSPPGrouping :: SPPGrouping + , taxonomicGroupsSPPLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Options for the @region-info@ command. +data RegionInfoOptions = + RegionInfoOptions + { regionInfoRegion :: Region + , regionInfoRegionNameFormat :: Maybe RegionNameFormat + } + deriving (Show, Read, Eq) + +-- | Options for the @sub-regions@ command. +data SubRegionListOptions = + SubRegionListOptions + { subRegionListParentRegionCode :: RegionCode + , subRegionListRegionType :: RegionType + } + deriving (Show, Read, Eq) + +-- | Options for the @sub-regions@ command. +newtype AdjacentRegionsOptions = + AdjacentRegionsOptions + { adjacentRegionsRegion :: Region + } + deriving (Show, Read, Eq) + +{------------------------------------------------------------------------------ + Command/option/flag parsers +------------------------------------------------------------------------------} + +-- | Parse a command provided to the @ebird@ CLI. +eBirdCommand :: Parser (Maybe Text, EBirdCommand) +eBirdCommand = + (,) + <$> optionalAPIKey + <*> + ( subparser + ( commandGroup "Observation commands:" + <> command "observations" recentObservationsInfo + <> command "notable-observations" recentNotableObservationsInfo + <> command "species-observations" recentSpeciesObservationsInfo + <> command "nearby-observations" recentNearbyObservationsInfo + <> command "nearby-species-observations" recentNearbySpeciesObservationsInfo + <> command "nearest-species-observations" recentNearestSpeciesObservationsInfo + <> command "nearby-notable-observations" recentNearbyNotableObservationsInfo + <> command "historical-observations" historicalObservationsInfo + ) + <|> subparser + ( + commandGroup "Product commands:" + <> command "recent-checklists" recentChecklistsInfo + <> command "top-100" top100Info + <> command "checklist-feed" checklistFeedInfo + <> command "regional-statistics" regionalStatisticsInfo + <> command "species-list" speciesListInfo + <> command "view-checklist" viewChecklistInfo + <> hidden + ) + <|> subparser + ( + commandGroup "Hotspot commands:" + <> command "region-hotspots" regionHotspotsInfo + <> command "nearby-hotspots" nearbyHotspotsInfo + <> command "hotspot-info" hotspotInfoInfo + <> hidden + ) + <|> subparser + ( + commandGroup "Taxonomy commands:" + <> command "taxonomy" taxonomyInfo + <> command "taxonomic-forms" taxonomicFormsInfo + <> command "taxa-locale-codes" taxaLocaleCodesInfo + <> command "taxonomy-versions" taxonomyVersionsInfo + <> command "taxonomic-groups" taxonomicGroupsInfo + <> hidden + ) + <|> subparser + ( + commandGroup "Region commands:" + <> command "region-info" regionInfoInfo + <> command "sub-regions" subRegionsInfo + <> command "adjacent-regions" adjacentRegionsInfo + <> hidden + ) + ) + where + optionalAPIKey :: Parser (Maybe Text) + optionalAPIKey = + optional + ( strOption + ( long "api-key" + <> short 'k' + <> metavar "API_KEY" + <> help "Specify an eBird API key" + ) + ) + + recentObservationsInfo :: ParserInfo EBirdCommand + recentObservationsInfo = + info + (RecentObservationsCommand <$> recentObservationsOptions) + (progDesc "Get recent observations within a region") + + recentNotableObservationsInfo :: ParserInfo EBirdCommand + recentNotableObservationsInfo = + info + (RecentNotableObservationsCommand <$> recentNotableObservationsOptions) + (progDesc "Get recent notable observations within a region") + + recentSpeciesObservationsInfo :: ParserInfo EBirdCommand + recentSpeciesObservationsInfo = + info + (RecentSpeciesObservationsCommand <$> recentSpeciesObservationsOptions) + (progDesc "Get recent observations of a species within a region") + + recentNearbyObservationsInfo :: ParserInfo EBirdCommand + recentNearbyObservationsInfo = + info + (RecentNearbyObservationsCommand <$> recentNearbyObservationsOptions) + (progDesc "Get recent observations within some radius of a latitude/longitude") + + recentNearbySpeciesObservationsInfo :: ParserInfo EBirdCommand + recentNearbySpeciesObservationsInfo = + info + (RecentNearbySpeciesObservationsCommand <$> recentNearbySpeciesObservationsOptions) + (progDesc "Get recent observations of a species within some radius of a latitude/longitude") + + recentNearestSpeciesObservationsInfo :: ParserInfo EBirdCommand + recentNearestSpeciesObservationsInfo = + info + (RecentNearestSpeciesObservationsCommand <$> recentNearestSpeciesObservationsOptions) + (progDesc "Get recent observations of a species nearest to a latitude/longitude") + + recentNearbyNotableObservationsInfo :: ParserInfo EBirdCommand + recentNearbyNotableObservationsInfo = + info + (RecentNearbyNotableObservationsCommand <$> recentNearbyNotableObservationsOptions) + (progDesc "Get recent notable observations within some radius of a latitude/longitude") + + historicalObservationsInfo :: ParserInfo EBirdCommand + historicalObservationsInfo = + info + (HistoricalObservationsCommand <$> historicalObservationsOptions) + (progDesc "Get a list of observations for each species seen in a region on a specific date") + + recentChecklistsInfo :: ParserInfo EBirdCommand + recentChecklistsInfo = + info + (RecentChecklistsCommand <$> recentChecklistsOptions) + (progDesc "Get recent checklists within a region") + + top100Info :: ParserInfo EBirdCommand + top100Info = + info + (Top100Command <$> top100Options) + (progDesc "Get the top 100 contributors in a region for a given date") + + checklistFeedInfo :: ParserInfo EBirdCommand + checklistFeedInfo = + info + (ChecklistFeedCommand <$> checklistFeedOptions) + (progDesc "Get the checklist feed in a region for a given date") + + regionalStatisticsInfo :: ParserInfo EBirdCommand + regionalStatisticsInfo = + info + (RegionalStatisticsCommand <$> regionalStatisticsOptions) + (progDesc "Get the regional statistics for a region on a given date") + + speciesListInfo :: ParserInfo EBirdCommand + speciesListInfo = + info + (SpeciesListCommand <$> speciesListOptions) + (progDesc "Get the list of all species ever observed in a region") + + viewChecklistInfo :: ParserInfo EBirdCommand + viewChecklistInfo = + info + (ViewChecklistCommand <$> viewChecklistOptions) + (progDesc "Get information about a particular checklist") + + regionHotspotsInfo :: ParserInfo EBirdCommand + regionHotspotsInfo = + info + (RegionHotspotsCommand <$> regionHotspotsOptions) + (progDesc "Get a list of hotspots in one or more regions") + + nearbyHotspotsInfo :: ParserInfo EBirdCommand + nearbyHotspotsInfo = + info + (NearbyHotspotsCommand <$> nearbyHotspotsOptions) + (progDesc "Get a list of hotspots within some radius of a latitude/longitude") + + hotspotInfoInfo :: ParserInfo EBirdCommand + hotspotInfoInfo = + info + (HotspotInfoCommand <$> hotspotInfoOptions) + (progDesc "Get information about a hotspot") + + taxonomyInfo :: ParserInfo EBirdCommand + taxonomyInfo = + info + (TaxonomyCommand <$> taxonomyOptions) + (progDesc "Get any version of the eBird taxonomy") + + taxonomicFormsInfo :: ParserInfo EBirdCommand + taxonomicFormsInfo = + info + (TaxonomicFormsCommand <$> taxonomicFormsOptions) + (progDesc "Get the subspecies of a given species recognized by the taxonomy") + + taxaLocaleCodesInfo :: ParserInfo EBirdCommand + taxaLocaleCodesInfo = + info + (TaxaLocaleCodesCommand <$> taxaLocaleCodesOptions) + (progDesc "Get the supported locale codes and names for species common names") + + taxonomyVersionsInfo :: ParserInfo EBirdCommand + taxonomyVersionsInfo = + info + (pure TaxonomyVersionsCommand <**> helper) + (progDesc "Get the complete list of taxonomy versions, with a flag indicating which is the latest") + + taxonomicGroupsInfo :: ParserInfo EBirdCommand + taxonomicGroupsInfo = + info + (TaxonomicGroupsCommand <$> taxonomicGroupsOptions) + (progDesc "Get the list of species groups in either Merlin or eBird grouping") + + regionInfoInfo :: ParserInfo EBirdCommand + regionInfoInfo = + info + (RegionInfoCommand <$> regionInfoOptions) + (progDesc "Get information about a region") + + subRegionsInfo :: ParserInfo EBirdCommand + subRegionsInfo = + info + (SubRegionListCommand <$> subRegionListOptions) + (progDesc "Get the list of sub-regions within a region") + + adjacentRegionsInfo :: ParserInfo EBirdCommand + adjacentRegionsInfo = + info + (AdjacentRegionsCommand <$> adjacentRegionsOptions) + (progDesc "Get the list of regions that are adjacent to a region") + +{------------------------------------------------------------------------------ + Parsers for observation command options +------------------------------------------------------------------------------} + +-- | Parse the options for the @recent-observations@ command. +recentObservationsOptions :: Parser RecentObservationsOptions +recentObservationsOptions = + RecentObservationsOptions + <$> regionCodeOpt "observations" + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional (taxonomyCategoriesOpt "observations of") + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional extraRegionsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-notable-observations@ command. +recentNotableObservationsOptions :: Parser RecentNotableObservationsOptions +recentNotableObservationsOptions = + RecentNotableObservationsOptions + <$> regionCodeOpt "observations" + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional observationDetailLevelOpt + <*> optional observationOnlyHotspotsOpt + <*> optional observationMaxResultsOpt + <*> optional extraRegionsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-species-observations@ command. +recentSpeciesObservationsOptions :: Parser RecentSpeciesObservationsOptions +recentSpeciesObservationsOptions = + RecentSpeciesObservationsOptions + <$> regionCodeOpt "observations" + <*> speciesCodeOpt "observations" + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional extraRegionsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-nearby-observations@ command. +recentNearbyObservationsOptions :: Parser RecentNearbyObservationsOptions +recentNearbyObservationsOptions = + RecentNearbyObservationsOptions + <$> latLngOpt "latitude" "observations" + <*> latLngOpt "longitude" "observations" + <*> optional (searchRadiusOpt "observations" 50 (Just 25)) + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional (taxonomyCategoriesOpt "observations of") + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional observationSortByOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-nearby-species-observations@ command. +recentNearbySpeciesObservationsOptions :: Parser RecentNearbySpeciesObservationsOptions +recentNearbySpeciesObservationsOptions = + RecentNearbySpeciesObservationsOptions + <$> speciesCodeOpt "observations" + <*> latLngOpt "latitude" "observations" + <*> latLngOpt "longitude" "observations" + <*> optional (searchRadiusOpt "observations" 50 (Just 25)) + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional (taxonomyCategoriesOpt "observations of") + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional observationSortByOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-nearest-species-observations@ command. +recentNearestSpeciesObservationsOptions :: Parser RecentNearestSpeciesObservationsOptions +recentNearestSpeciesObservationsOptions = + RecentNearestSpeciesObservationsOptions + <$> speciesCodeOpt "observations" + <*> latLngOpt "latitude" "observations" + <*> latLngOpt "longitude" "observations" + <*> optional (searchRadiusOpt "observations" 50 Nothing) + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @recent-nearest-species-observations@ command. +recentNearbyNotableObservationsOptions :: Parser RecentNearbyNotableObservationsOptions +recentNearbyNotableObservationsOptions = + RecentNearbyNotableObservationsOptions + <$> latLngOpt "latitude" "observations" + <*> latLngOpt "longitude" "observations" + <*> optional (searchRadiusOpt "observations" 50 (Just 25)) + <*> optional observationDetailLevelOpt + <*> optional (backOpt "observations" "submitted" (Just 14)) + <*> optional observationOnlyHotspotsOpt + <*> optional observationMaxResultsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + +-- | Parse the options for a @historical-observations@ command. +historicalObservationsOptions :: Parser HistoricalObservationsOptions +historicalObservationsOptions = + HistoricalObservationsOptions + <$> regionCodeOpt "observations" + <*> dateOpt + <*> optional (taxonomyCategoriesOpt "observations of") + <*> optional observationDetailLevelOpt + <*> optional observationOnlyHotspotsOpt + <*> optional observationIncludeProvisionalOpt + <*> optional observationMaxResultsOpt + <*> optional rankOpt + <*> optional extraRegionsOpt + <*> optional (sppLocaleOpt "common") + <**> helper + where + dateOpt :: Parser EBirdDate + dateOpt = + option (attoReadM parseEBirdDate) + ( long "date" + <> metavar "YYYY-MM-DD" + <> help ( "Specify the date to fetch observations from (year " ++ + "1800 to present)" + ) + ) + + rankOpt :: Parser SelectObservation + rankOpt = + option (attoReadM parseSelectObservation) + ( long "select" + <> metavar "SELECT" + <> help ( "Specify whether to select the first or last " ++ + "observation of a species if there are multiple " ++ + "(\"first\" or \"last\") (default: last)" + ) + ) + +-- | Parse a 'GHC.Types.Bool', intended to be used as an option determining whether to +-- include observations from hotspots in the response. +observationOnlyHotspotsOpt :: Parser Bool +observationOnlyHotspotsOpt = + switch + ( long "only-hotspots" + <> help "Only include observations from hotspots" + ) + +-- | Parse a 'GHC.Types.Bool', intended to be used as an option determining whether to +-- include unreviewed observations in the response. +observationIncludeProvisionalOpt :: Parser Bool +observationIncludeProvisionalOpt = + switch + ( long "include-provisional" + <> help "Include observations which have not yet been reviewed" + ) + +-- | Parse a 'Integer' option indicating the number of results to include in the +-- response. +observationMaxResultsOpt :: Parser Integer +observationMaxResultsOpt = + option (attoReadM A.decimal) + ( long "max-results" + <> metavar "N" + <> help ( "Specify the max number of observations to include " ++ + "(1 to 10000, default: all)" + ) + ) + +-- | Parse a 'RegionCode' as a generic command option, intended for use with the +-- observation commands. +regionCodeOpt + :: String + -- ^ What are we fetching? (e.g. "observations") + -> Parser RegionCode +regionCodeOpt thing = + option (attoReadM parseRegionCode) + ( long "region" + <> metavar "REGION_CODE" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + ( "Specify the regions to fetch %s from " ++ + "(e.g. \"US-WY,US-CO,US-ID\" or \"US-CA-037\")" + ) + thing + +-- | Configurable 'TaxonomyCategories' parser +taxonomyCategoriesOpt + :: String + -- ^ String to include after "one or more taxonomy categories to include ..." + -> Parser TaxonomyCategories +taxonomyCategoriesOpt desc = + option (attoReadM parseTaxonomyCategories) + ( long "taxonomy-categories" + <> metavar "CATEGORIES" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + ( "Specify a list of one or more taxonomy categories to include " ++ + "%s (e.g. \"issf\" or \"hybrid\") (default: all categories)" + ) + desc + +-- | Parse a 'SortObservationsBy' as an option determining how returned +-- observations will be sorted. +observationSortByOpt :: Parser SortObservationsBy +observationSortByOpt = + option (attoReadM parseSortObservationsBy) + ( long "sort-by" + <> metavar "SORT_BY" + <> help ( "Specify the ordering to use for the resulting " ++ + "observations (\"date\" or \"species\") (default: date, " ++ + "earliest first)" + ) + ) + +-- | Parse a 'DetailLevel' as an option determining the detail level of +-- observations in the result. +observationDetailLevelOpt :: Parser DetailLevel +observationDetailLevelOpt = + option (attoReadM parseDetailLevel) + ( long "detail" + <> metavar "DETAIL_LEVEL" + <> help ( "Specify the detail level of the returned observations " ++ + "(\"simple\" or \"full\") (default: simple)" + ) + ) + +-- | Parse a 'RegionCode' as extra regions to fetch observations from +extraRegionsOpt :: Parser RegionCode +extraRegionsOpt = + option (attoReadM parseRegionCode) + ( long "extra-regions" + <> metavar "REGION_CODE" + <> help "Up to 10 extra regions to fetch observations from" + ) + +{------------------------------------------------------------------------------ + Parsers for product command options +------------------------------------------------------------------------------} + +-- | Parse the options for a @recent-checklists@ command. +recentChecklistsOptions :: Parser RecentChecklistsOptions +recentChecklistsOptions = + RecentChecklistsOptions + <$> checklistRegionCodeOpt + <*> optional checklistMaxResultsOpt + <**> helper + +-- | Parse the options for a @top-100@ command. +top100Options :: Parser Top100Options +top100Options = + Top100Options + <$> regionOpt + <*> dateOpt + <*> optional rankedByOpt + <*> optional maxResultsOpt + <**> helper + where + regionOpt :: Parser Region + regionOpt = + option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help ( "Specify the region to fetch the top 100 contributors " ++ + "for (e.g. \"US-WY\" or \"US-CA-037\")" + ) + ) + + dateOpt :: Parser EBirdDate + dateOpt = + option (attoReadM parseEBirdDate) + ( long "date" + <> metavar "YYYY-MM-DD" + <> help ( "Specify the date to fetch the top contributors on " ++ + "(year 1800 to present)" + ) + ) + + rankedByOpt :: Parser RankTop100By + rankedByOpt = + option (attoReadM parseRankTop100By) + ( long "rank-by" + <> metavar "RANK_BY" + <> help ( "Specify whether to rank contributors by number of " ++ + "species observed (\"spp\") or number of checklists " ++ + "completed (\"cl\") (default: spp)" + ) + ) + + maxResultsOpt :: Parser Integer + maxResultsOpt = + option (attoReadM A.decimal) + ( long "max-results" + <> metavar "N" + <> help ( "Specify the max number of contributors to include " ++ + "(1 to 100, default: 100)" + ) + ) + +-- | Parse the options for a @top-100@ command. +checklistFeedOptions :: Parser ChecklistFeedOptions +checklistFeedOptions = + ChecklistFeedOptions + <$> regionOpt + <*> dateOpt + <*> optional sortByOpt + <*> optional maxResultsOpt + <**> helper + where + regionOpt :: Parser Region + regionOpt = + option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help ( "Specify the region to fetch the checklist feed " ++ + "for (e.g. \"US-WY\" or \"US-CA-037\")" + ) + ) + + dateOpt :: Parser EBirdDate + dateOpt = + option (attoReadM parseEBirdDate) + ( long "date" + <> metavar "YYYY-MM-DD" + <> help ( "Specify the date to fetch the checklist feed on " ++ + "(year 1800 to present)" + ) + ) + + sortByOpt :: Parser SortChecklistsBy + sortByOpt = + option (attoReadM parseSortChecklistsBy) + ( long "sort-by" + <> metavar "SORT_BY" + <> help ( "Specify whether to sort the checklist fee by date of " ++ + "creation (\"obs_dt\") or date of submission " ++ + "(\"creation_dt\") (default: obs_dt)" + ) + ) + + maxResultsOpt :: Parser Integer + maxResultsOpt = + option (attoReadM A.decimal) + ( long "max-results" + <> metavar "N" + <> help ( "Specify the max number of checklists to include " ++ + "(1 to 200, default: 10)" + ) + ) + +-- | Parse the options for a @regional-statistics@ command. +regionalStatisticsOptions :: Parser RegionalStatisticsOptions +regionalStatisticsOptions = + RegionalStatisticsOptions + <$> regionOpt + <*> dateOpt + <**> helper + where + regionOpt :: Parser Region + regionOpt = + option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help ( "Specify the region to fetch the statistics for " ++ + "(e.g. \"US-WY\" or \"US-CA-037\")" + ) + ) + + dateOpt :: Parser EBirdDate + dateOpt = + option (attoReadM parseEBirdDate) + ( long "date" + <> metavar "YYYY-MM-DD" + <> help ( "Specify the date to fetch the statistics on (year " ++ + "1800 to present)" + ) + ) + +-- | Parse the options for a @regional-statistics@ command. +speciesListOptions :: Parser SpeciesListOptions +speciesListOptions = + SpeciesListOptions + <$> regionOpt + <**> helper + where + regionOpt :: Parser Region + regionOpt = + option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help ( "Specify the region to fetch the species list for " ++ + "(e.g. \"US-WY\" or \"US-CA-037\")" + ) + ) + +-- | Parse the options for a @regional-statistics@ command. +viewChecklistOptions :: Parser ViewChecklistOptions +viewChecklistOptions = + ViewChecklistOptions + <$> subIdOpt + <**> helper + where + subIdOpt :: Parser Text + subIdOpt = + option (attoReadM A.takeText) + ( long "submission-id" + <> metavar "SUBMISSION_ID" + <> help ( "Specify the submission ID of the checklist to view " ++ + "(e.g. \"S144646447\")" + ) + ) + +-- | Parse a 'RegionCode' as a generic command option, intended for use with the +-- checklists commands. +checklistRegionCodeOpt :: Parser RegionCode +checklistRegionCodeOpt = + option (attoReadM parseRegionCode) + ( long "region" + <> metavar "REGION_CODE" + <> help ( "Specify the regions to fetch checklists from " ++ + "(e.g. \"US-WY,US-CO\" or \"US-CA-037\")" + ) + ) + +-- | Parse a 'Integer' as a generic command option, intended for use with the +-- checklists commands for determining max results to include. +checklistMaxResultsOpt :: Parser Integer +checklistMaxResultsOpt = + option (attoReadM A.decimal) + ( long "max-results" + <> metavar "N" + <> help ( "Specify the max number of checklists to include " ++ + "(1 to 200, default: 10)" + ) + ) + +{------------------------------------------------------------------------------ + Parsers for hotspot command options +------------------------------------------------------------------------------} + +-- | Parse the options for a @region-hotspots@ command. +regionHotspotsOptions :: Parser RegionHotspotsOptions +regionHotspotsOptions = + RegionHotspotsOptions + <$> regionCodeOpt "hotspots" + <*> optional (backOpt "hotspots" "visited" Nothing) + <*> pure (Just JSONFormat) + <**> helper + +-- | Parse the options for a @nearby-hotspots@ command. +nearbyHotspotsOptions :: Parser NearbyHotspotsOptions +nearbyHotspotsOptions = + NearbyHotspotsOptions + <$> latLngOpt "latitude" "hotspots" + <*> latLngOpt "longitude" "hotspots" + <*> optional (backOpt "hotspots" "visited" Nothing) + <*> optional (searchRadiusOpt "hotspots" 50 (Just 25)) + <*> pure (Just JSONFormat) + <**> helper + +-- | Parse the options for a @hotspot-info@ command. +hotspotInfoOptions :: Parser HotspotInfoOptions +hotspotInfoOptions = + HotspotInfoOptions + <$> locationCodeOpt + <**> helper + where + locationCodeOpt :: Parser Text + locationCodeOpt = + option (attoReadM A.takeText) + ( long "location" + <> metavar "LOCATION" + <> help "Location code of the hotspot (e.g. \"L5044136\")" + ) +{------------------------------------------------------------------------------ + Parsers for taxonomy command options +------------------------------------------------------------------------------} + +-- | Parse the options for a @nearby-hotspots@ command. +taxonomyOptions :: Parser TaxonomyOptions +taxonomyOptions = + TaxonomyOptions + <$> optional (taxonomyCategoriesOpt "in the taxonomy") + <*> pure (Just JSONFormat) + <*> optional (sppLocaleOpt "common") + <*> optional speciesCodesOpt + <*> optional taxonomyVersionOpt + <**> helper + where + speciesCodesOpt :: Parser SpeciesCodes + speciesCodesOpt = + option (attoReadM parseSpeciesCodes) + ( long "species" + <> metavar "SPECIES_CODES" + <> help "Only include entries for these species (default: all)" + ) + + taxonomyVersionOpt :: Parser Text + taxonomyVersionOpt = + option (attoReadM A.takeText) + ( long "version" + <> metavar "VERSION" + <> help "Taxonomy version to fetch (default: latest)" + ) + +-- | Parse the options for a @taxonomic-forms@ command. +taxonomicFormsOptions :: Parser TaxonomicFormsOptions +taxonomicFormsOptions = + TaxonomicFormsOptions + <$> speciesCodeOpt "subspecies" + <**> helper + +-- | Parse the options for a @taxa-locale-codes@ command. +taxaLocaleCodesOptions :: Parser TaxaLocaleCodesOptions +taxaLocaleCodesOptions = + TaxaLocaleCodesOptions + <$> optional acceptLanguageOpt + <**> helper + where + acceptLanguageOpt :: Parser SPPLocale + acceptLanguageOpt = + option (attoReadM parseSPPLocale) + ( long "accept-language" + <> metavar "LOCALE" + <> help ( "Get language names translated to this locale, when " ++ + "available (default: en)" + ) + ) + +-- | Parse the options for a @taxa-locale-codes@ command. +taxonomicGroupsOptions :: Parser TaxonomicGroupsOptions +taxonomicGroupsOptions = + TaxonomicGroupsOptions + <$> sppGroupingOpt + <*> optional (sppLocaleOpt "group") + <**> helper + where + sppGroupingOpt :: Parser SPPGrouping + sppGroupingOpt = + option (attoReadM parseSPPGrouping) + ( long "spp-grouping" + <> metavar "GROUPING" + <> help ( "Group species using Merlin or eBird grouping " ++ + "(\"merlin\" or \"ebird\")" + ) + ) + +{------------------------------------------------------------------------------ + Parsers for region command options +------------------------------------------------------------------------------} + +-- | Parse the options for the @region-info@ command. +regionInfoOptions :: Parser RegionInfoOptions +regionInfoOptions = + RegionInfoOptions + <$> region + <*> optional regionNameFormat + <**> helper + +-- | Parse the options for the @sub-regions@ command. +subRegionListOptions :: Parser SubRegionListOptions +subRegionListOptions = + SubRegionListOptions + <$> regionCode + <*> regionType + <**> helper + +-- | Parse the options for the @sub-regions@ command. +adjacentRegionsOptions :: Parser AdjacentRegionsOptions +adjacentRegionsOptions = + AdjacentRegionsOptions + <$> regionOpt + <**> helper + where + regionOpt :: Parser Region + regionOpt = + option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help ( "Specify the region to get adjacent regions of " ++ + "(e.g. \"US-WY\")" + ) + ) + +{------------------------------------------------------------------------------ + Generic single option parsers +------------------------------------------------------------------------------} + +-- | Parse a 'SPPLocale' as a generic command option. +sppLocaleOpt :: String -> Parser SPPLocale +sppLocaleOpt nameType = + option (attoReadM parseSPPLocale) + ( long "spp-locale" + <> metavar "LOCALE" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + "Specify a locale to use for %s names" + nameType + +-- | Parse a 'Region' as a generic command option. +region :: Parser Region +region = option (attoReadM parseRegion) + ( long "region" + <> metavar "REGION" + <> help "Specify a region (e.g. \"world\" or \"US-WY\")" + ) + +-- | Parse a 'RegionCode' as a generic command option. +regionCode :: Parser RegionCode +regionCode = option (attoReadM parseRegionCode) + ( long "region" + <> metavar "REGION_CODE" + <> help "Specify a region code (e.g. \"world\" or \"US-MT,US-WY\")" + ) + +-- | Parse a 'RegionNameFormat' as a generic command option. +regionNameFormat :: Parser RegionNameFormat +regionNameFormat = option (attoReadM parseRegionNameFormat) + ( long "region-name-format" + <> metavar "REGION_NAME_FORMAT" + <> help ( "Specify a region name format for the result. Must be one " ++ + "of \"detailed\", \"detailednoqual\", \"full\", " ++ + "\"namequal\", \"nameonly\", or \"revdetailed\" " ++ + "(default: full)" + ) + ) + +-- | Parse a 'RegionType' as a generic command option. +regionType :: Parser RegionType +regionType = option (attoReadM parseRegionType) + ( long "region-type" + <> metavar "REGION_TYPE" + <> help ( "Specify a region type (\"country\", \"subnational1\", " ++ + "\"subnational2\")" + ) + ) + +{------------------------------------------------------------------------------ + Utility functions +------------------------------------------------------------------------------} + +-- | Configurable 'SpeciesCode' option +speciesCodeOpt :: String -> Parser SpeciesCode +speciesCodeOpt thing = + option (attoReadM parseSpeciesCode) + ( long "species-code" + <> metavar "SPECIES_CODE" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + ( "Specify a species code to fetch %s for (e.g. \"barswa\" for Barn " ++ + "Swallow)" + ) + thing + +-- | Configurable "search radius" option +searchRadiusOpt + :: String + -- ^ What are we searching for (e.g. "observations") + -> Integer + -- ^ Maximum allowed value by the API + -> Maybe Integer + -- ^ Default value of the API ('Nothing' for "no limit") + -> Parser Integer +searchRadiusOpt thing maxRadius mDefaultRadius = + option (attoReadM A.decimal) + ( long "radius" + <> metavar "KILOMETERS" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + "Specify the search radius to fetch %s within (0 to %d, default: %s)" + thing + maxRadius + defStr + + defStr = maybe "no limit" show mDefaultRadius + +-- | Configurable "back" option +backOpt + :: String + -- ^ What are we fetching (e.g. "hotspots") + -> String + -- ^ Verb we are filtering on (e.g. "submitted", or "visited") + -> Maybe Integer + -- ^ Default value ('Nothing' for "no limit") + -> Parser Integer +backOpt thing verb mmax = + option (attoReadM A.decimal) + ( long "back" + <> metavar "N" + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + "Only fetch %s %s within the last N days (1 - 30, default: %s)" + thing + verb + defStr + + defStr = maybe "no limit" show mmax + +-- | Configurable lat/lng option +latLngOpt + :: String + -- ^ "latitude" or "longitude" + -> String + -- ^ What are we looking for + -> Parser Double +latLngOpt latLng thing = + option (attoReadM A.double) + ( long latLng + <> metavar (map toUpper latLng) + <> help helpStr + ) + where + helpStr :: String + helpStr = + printf + "Specify the %s of the location to fetch %s near" + latLng + thing + +-- Attoparsec utilities + +-- | Convert an attoparsec parser into an optparse-applicative parser. +attoReadM :: A.Parser a -> ReadM a +attoReadM p = eitherReader (A.parseOnly p . Text.pack) diff --git a/ebird-client/CHANGELOG.md b/ebird-client/CHANGELOG.md new file mode 100644 index 0000000..3ce77e8 --- /dev/null +++ b/ebird-client/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for ebird-client + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/ebird-client/LICENSE b/ebird-client/LICENSE new file mode 100644 index 0000000..d397496 --- /dev/null +++ b/ebird-client/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2023 Finley McIlwaine + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/ebird-client/README.md b/ebird-client/README.md new file mode 100644 index 0000000..74e1409 --- /dev/null +++ b/ebird-client/README.md @@ -0,0 +1,33 @@ +# ebird-client + +Query the official eBird API from Haskell. + +## Installation + +In your cabal file: +```cabal + build-depends: + ebird-client +``` + +## Usage + +Every eBird API endpoint (as listed in [their documentation][api-docs]) is +supported. Use `askEBird` to send requests to the official eBird API. Many +requests require an API key, which can be obtained +[here](https://ebird.org/api/keygen). + +For example, to get recent observations of Peregrine Falcons in Park County, +Wyoming (using `-XOverloadedStrings`): + +```haskell +askEBird $ recentSpeciesObservations apiKey "US-WY-029" "perfal" def +``` + +For more examples and documentation, see the library's [Hackage +documentation][ebird-client]. + + + +[api-docs]: https://documenter.getpostman.com/view/664302/S1ENwy59 +[ebird-client]: https://hackage.haskell.org/package/ebird-client diff --git a/ebird-client/ebird-client.cabal b/ebird-client/ebird-client.cabal new file mode 100644 index 0000000..91e640e --- /dev/null +++ b/ebird-client/ebird-client.cabal @@ -0,0 +1,69 @@ +cabal-version: 3.4 +name: ebird-client +version: 0.1.0.0 +synopsis: + Client functions for querying the eBird API. +description: + [eBird](https://ebird.org/home) is a massive collection of ornithological + science projects developed by the + [Cornell Lab of Ornithology](https://www.birds.cornell.edu/home/). The + [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59) + offers programmatic access to the incredible dataset backing these + projects. + + This library contains functions for retrieving data from the + [eBird API](https://documenter.getpostman.com/view/664302/S1ENwy59), as + defined in the + [ebird-api](https://hackage.haskell.org/package/ebird-api) library. + + If you'd like to run the queries defined in this library directly on your + command line, checkout out the + [ebird-cli](https://hackage.haskell.org/package/ebird-cli). +license: MIT +license-file: LICENSE +author: Finley McIlwaine +maintainer: finleymcilwaine@gmail.com +copyright: 2023 Finley McIlwaine +category: Web +build-type: Simple +extra-doc-files: CHANGELOG.md +bug-reports: https://github.com/FinleyMcIlwaine/ebird-haskell/issues +homepage: https://github.com/FinleyMcIlwaine/ebird-haskell + +tested-with: + GHC == 8.10.7 + , GHC == 9.2.7 + , GHC == 9.4.5 + , GHC == 9.6.2 + +common common + build-depends: + base >= 4.13.3.0 && < 4.19 + default-extensions: + ImportQualifiedPost + LambdaCase + OverloadedStrings + RecordWildCards + default-language: Haskell2010 + +library + import: common + exposed-modules: + EBird.Client + EBird.Client.Generated + EBird.Client.Hotspots + EBird.Client.Observations + EBird.Client.Product + EBird.Client.Regions + EBird.Client.Taxonomy + build-depends: + , ebird-api >= 0.1.0.0 && < 0.2 + + , data-default >= 0.7.1.1 && < 0.8 + , http-client-tls >= 0.3.5.3 && < 0.4 + , optics >= 0.4 && < 0.5 + , servant >= 0.18.3 && < 0.21 + , servant-client >= 0.18.3 && < 0.21 + , text >= 1.2.4.1 && < 2.1 + hs-source-dirs: + src diff --git a/ebird-client/src/EBird/Client.hs b/ebird-client/src/EBird/Client.hs new file mode 100644 index 0000000..91e4f5d --- /dev/null +++ b/ebird-client/src/EBird/Client.hs @@ -0,0 +1,83 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Eta reduce" #-} + +-- | +-- Module : EBird.Client +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Functions that support querying the official [eBird +-- API](https://documenter.getpostman.com/view/664302/S1ENwy59) as defined in +-- the [ebird-api](https://hackage.haskell.org/package/ebird-api) library. + +module EBird.Client ( + -- * Execute client functions + askEBird + + -- * eBird API client functions + -- ** Observations queries + , recentObservations + , recentNotableObservations + , recentNearbyObservations + , recentNearbySpeciesObservations + , recentNearestSpeciesObservations + , recentNearbyNotableObservations + , historicalObservations + + -- ** Product queries + , recentChecklists + , top100 + , checklistFeed + , regionalStatistics + , speciesList + , viewChecklist + + -- ** Hotspot queries + , regionHotspots + , nearbyHotspots + , hotspotInfo + + -- ** Taxonomy queries + , taxonomy + , taxonomicForms + , taxaLocaleCodes + , taxonomyVersions + , taxonomicGroups + + -- ** Region queries + , regionInfo + , subRegionList + , adjacentRegions + + -- * Less convenient, generated queries + , module EBird.Client.Generated + + -- * Convenient re-exports + , module EBird.API + , ClientError + ) where + +import Network.HTTP.Client.TLS +import Servant.Client + +import EBird.API +import EBird.Client.Generated +import EBird.Client.Hotspots +import EBird.Client.Observations +import EBird.Client.Product +import EBird.Client.Regions +import EBird.Client.Taxonomy + +-- | Send a request to the official eBird API. +askEBird :: ClientM a -> IO (Either ClientError a) +askEBird question = do + manager' <- newTlsManager + runClientM question (mkClientEnv manager' ebirdHQ) + where + -- Home of the official eBird API + ebirdHQ :: BaseUrl + ebirdHQ = BaseUrl Https "api.ebird.org" 443 "" diff --git a/ebird-client/src/EBird/Client/Generated.hs b/ebird-client/src/EBird/Client/Generated.hs new file mode 100644 index 0000000..bdecc50 --- /dev/null +++ b/ebird-client/src/EBird/Client/Generated.hs @@ -0,0 +1,764 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE TypeApplications #-} + +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# HLINT ignore "Eta reduce" #-} + +-- | +-- Module : EBird.Client.Generated +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Client functions generated using +-- [servant-client](https://hackage.haskell.org/package/servant-client). The +-- queries here match exactly the schemas defined in +-- [ebird-api](https://hackage.haskell.org/package/ebird-api), and are therefore +-- potentially a bit more clunky to use. See the wrappers in 'EBird.Client' for +-- more convenient options. + +module EBird.Client.Generated + ( -- * Generated eBird API client functions + -- + -- | Generated directly from the definition of the API in + -- [ebird-api](https://hackage.haskell.org/package/ebird-api). + + -- ** Observations queries + recentObservations_ + , recentNotableObservations_ + , recentSpeciesObservations_ + , recentNearbyObservations_ + , recentNearbySpeciesObservations_ + , recentNearestSpeciesObservations_ + , recentNearbyNotableObservations_ + , historicalObservations_ + + -- ** Product queries + , recentChecklists_ + , top100_ + , checklistFeed_ + , regionalStatistics_ + , speciesList_ + , viewChecklist_ + + -- ** Hotspot queries + , regionHotspots_ + , nearbyHotspots_ + , hotspotInfo_ + + -- ** Taxonomy queries + , taxonomy_ + , taxonomicForms_ + , taxaLocaleCodes_ + , taxonomyVersions_ + , taxonomicGroups_ + + -- ** Region queries + , regionInfo_ + , subRegionList_ + , adjacentRegions_ + ) where + + +import Data.Text (Text) +import Data.Proxy +import Servant.API.Alternative +import Servant.Client + +import EBird.API + +{------------------------------------------------------------------------------ + Observation APIs +------------------------------------------------------------------------------} + +-- | Get a list of recent observations within a region. Results only include the +-- most recent observation for each species in the region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3d2a17c1-2129-475c-b4c8-7d362d6000cd). +recentObservations_ + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /1 - 30, default: 14/ + -> Maybe TaxonomyCategories + -- ^ Only include observations in these taxonomy categories + -- + -- /default: all categories/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe RegionCode + -- ^ Up to 10 extra regions to get observations from + -- + -- /default: none/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [Observation 'Simple] + +-- | Get a list of recent notable observations within a region. Results only +-- include the most recent observation for each species in the region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#397b9b8c-4ab9-4136-baae-3ffa4e5b26e4). +recentNotableObservations_ + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /1 - 30, default: 14/ + -> Maybe DetailLevel + -- ^ Detail level for the resulting observations + -- + -- /default: 'Simple'/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe RegionCode + -- ^ Up to 10 extra regions to get observations from + -- + -- /default: none/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [SomeObservation] + +-- | Get a list of recent observations of a specific species within a region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#755ce9ab-dc27-4cfc-953f-c69fb0f282d9). +recentSpeciesObservations_ + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> SpeciesCode + -- ^ Species to get observations of (e.g. "barswa" for Barn Swallow) + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /1 - 30, default: 14/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe RegionCode + -- ^ Up to 10 extra regions to get observations from + -- + -- /default: none/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [Observation 'Simple] + +-- | Get a list of recent observations within some radius of some +-- latitude/longitude. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#62b5ffb3-006e-4e8a-8e50-21d90d036edc). +recentNearbyObservations_ + :: Text + -- ^ eBird API key + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> Maybe Integer + -- ^ Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /1 - 30, default: 14/ + -> Maybe TaxonomyCategories + -- ^ Only include observations in these taxonomy categories + -- + -- /default: all/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe SortObservationsBy + -- ^ Sort observations by taxonomy ('SortObservationsBySpecies') or by date + -- ('SortObservationsByDate', most recent first) + -- + -- /default: 'SortObservationsByDate'/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [Observation 'Simple] + +-- | Get a list of recent observations of a species within some radius of some +-- latitude/longitude. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#20fb2c3b-ee7f-49ae-a912-9c3f16a40397). +recentNearbySpeciesObservations_ + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ Species to get observations of (e.g. "bohwax" for Bohemian Waxwing) + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> Maybe Integer + -- ^ Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /1 - 30, default: 14/ + -> Maybe TaxonomyCategories + -- ^ Only include observations in these taxonomy categories + -- + -- /default: all/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe SortObservationsBy + -- ^ Sort observations by taxonomy ('SortObservationsBySpecies') or by date + -- ('SortObservationsByDate', most recent first) + -- + -- /default: 'SortObservationsByDate'/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [Observation 'Simple] + +-- | Get a list of recent observations of some species nearest to some +-- latitude/longitude. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#6bded97f-9997-477f-ab2f-94f254954ccb). +recentNearestSpeciesObservations_ + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ Species to get observations of + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> Maybe Integer + -- ^ Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /0 - 30, default: 14/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [Observation 'Simple] + +-- | Get a list of recent /notable/ observations of some near some +-- latitude/longitude. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#caa348bb-71f6-471c-b203-9e1643377cbc). +recentNearbyNotableObservations_ + :: Text + -- ^ eBird API key + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> Maybe Integer + -- ^ Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + -> Maybe DetailLevel + -- ^ Detail level for the resulting observations + -- + -- /default: 'Simple'/ + -> Maybe Integer + -- ^ How many days back to look for observations + -- + -- /0 - 30, default: 14/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [SomeObservation] + +-- | Get a list of observations for each species seen on a specific date. The +-- specific observations returned are determined by the 'SelectObservation' +-- parameter - first observation of the species ('SelectFirstObservation') or +-- last observation ('SelectLastObservation', default). +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8c6ee8-c435-4e91-9f66-6d3eeb09edd2). +historicalObservations_ + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observation from + -> Integer + -- ^ Year, from 1800 to present + -> Integer + -- ^ Month (1 - 12) + -> Integer + -- ^ Day in the month + -> Maybe TaxonomyCategories + -- ^ Only include observations in these taxonomy categories + -- + -- /default: all/ + -> Maybe DetailLevel + -- ^ Detail level for the resulting observations + -- + -- /default: 'Simple'/ + -> Maybe Bool + -- ^ Only get observations from hotspots + -- + -- /default: 'False'/ + -> Maybe Bool + -- ^ Include observations which have not been reviewed + -- + -- /default: 'False'/ + -> Maybe Integer + -- ^ Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + -> Maybe SelectObservation + -- ^ Whether to display the first or last observation of a species on the + -- date, in the case that there are multiple observations of the same species + -- on the date + -- + -- /default: 'SelectLastObservation'/ + -> Maybe RegionCode + -- ^ Up to 50 extra regions to get observations from + -- + -- /default: none/ + -> Maybe SPPLocale + -- ^ Return observations with common names in this locale + -- + -- /default: 'En'/ + -> ClientM [SomeObservation] + +{------------------------------------------------------------------------------ + Product APIs +------------------------------------------------------------------------------} + +-- | Get a list recently submitted checklists within a region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#95a206d1-a20d-44e0-8c27-acb09ccbea1a). +recentChecklists_ + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get checklists from + -> Maybe Integer + -- ^ Maximum number of checklists to fetch + -- + -- /1 - 200, default: 10/ + -> ClientM [ChecklistFeedEntry] + +-- | Get a list of top contributors for a region on a specific date, ranked by +-- number of species observed or number of checklists submitted. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8d3f94-c4b0-42bd-9c8e-71edfa6347ba). +top100_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the ranking for + -> Integer + -- ^ Year, from 1800 to present + -> Integer + -- ^ Month (1 - 12) + -> Integer + -- ^ Day in the month + -> Maybe RankTop100By + -- ^ Rank the resulting list by number of species observed or by number of + -- checklists completed + -- + -- /default: 'RankTop100BySpecies'/ + -> Maybe Integer + -- ^ Maximum number of entries to fetch + -- + -- /1 - 100, default: 100/ + -> ClientM [Top100ListEntry] + +-- | Get a list of checklists submitted within a region on a specific date. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#4416a7cc-623b-4340-ab01-80c599ede73e). +checklistFeed_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the checklist feed for + -> Integer + -- ^ Year, from 1800 to present + -> Integer + -- ^ Month (1 - 12) + -> Integer + -- ^ Day in the month + -> Maybe SortChecklistsBy + -- ^ Sort the resulting list by date of checklist submission or date of + -- checklist creation + -- + -- /default: 'SortChecklistsByDateCreated'/ + -> Maybe Integer + -- ^ Maximum number of checklists to fetch + -- + -- /1 - 200, default: 10/ + -> ClientM [ChecklistFeedEntry] + +-- | Get the 'RegionalStatistics' for a region on a specific date. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#506e63ab-abc0-4256-b74c-cd9e77968329). +regionalStatistics_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the statistics for + -> Integer + -- ^ Year, from 1800 to present + -> Integer + -- ^ Month (1 - 12) + -> Integer + -- ^ Day in the month + -> ClientM RegionalStatistics + +-- | Get a list of all species ever seen in a region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#55bd1b26-6951-4a88-943a-d3a8aa1157dd). +speciesList_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the species list for + -> ClientM [SpeciesCode] + +-- | Get information about a checklist. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2ee89672-4211-4fc1-8493-5df884fbb386). +viewChecklist_ + :: Text + -- ^ eBird API key + -> Text + -- ^ Checklist submission ID, e.g. \"S144646447\" + -> ClientM Checklist + +{------------------------------------------------------------------------------ + Hotspot APIs +------------------------------------------------------------------------------} + +-- | Get all hotspots in a list of one or more regions ('RegionCode'). +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#f4f59f90-854e-4ba6-8207-323a8cf0bfe0). +-- +-- __NOTE:__ The eBird API is broken. Always hardcode the 'CSVOrJSONFormat' +-- argument to 'JSONFormat'. +regionHotspots_ + :: RegionCode + -- ^ Region(s) to get hotspots in + -> Maybe Integer + -- ^ Only fetch hotspots that have been visited within this many days ago + -- + -- /1 - 30, default: no limit/ + -> Maybe CSVOrJSONFormat + -- ^ Format results in CSV or JSON format + -- + -- __NOTE:__ This argument should /always/ be hardcoded to 'JSONFormat', even + -- though the default is 'CSVFormat'. It is only here for to be consistent + -- with the eBird API. Unfortunately, the endpoint for this query switches + -- content types based on this query parameter instead of an "Accept" header. + -- That means servant is unable to determine whether the result will be CSV or + -- JSON encoded. For now, the workaround is to always hardcode this to + -- 'JSONFormat'. + -- + -- /default: 'CSVFormat'/ + -> ClientM [Hotspot] + +-- | Get all hotspots within a radius of some latitude/longitude. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#674e81c1-6a0c-4836-8a7e-6ea1fe8e6677). +-- +-- __NOTE:__ The eBird API is broken. Always hardcode the 'CSVOrJSONFormat' +-- argument to 'JSONFormat'. +nearbyHotspots_ + :: Double + -- ^ Latitude of the location to get hotspots near + -> Double + -- ^ Longitude of the location to get hotspots near + -> Maybe Integer + -- ^ Only fetch hotspots that have been visited within this many days ago + -- + -- /1 - 30, default: no limit/ + -> Maybe Integer + -- ^ Search radius in kilometers + -- + -- /0 - 50, default: 25/ + -> Maybe CSVOrJSONFormat + -- ^ Format results in CSV or JSON format + -- + -- __NOTE:__ This argument should /always/ be hardcoded to 'JSONFormat', even + -- though the default is 'CSVFormat'. It is only here for to be consistent + -- with the eBird API. Unfortunately, the endpoint for this query switches + -- content types based on this query parameter instead of an "Accept" header. + -- That means servant is unable to determine whether the result will be CSV or + -- JSON encoded. For now, the workaround is to always hardcode this to + -- 'JSONFormat'. + -- + -- /default: 'CSVFormat'/ + -> ClientM [Hotspot] + +-- | Get information about a hotspot. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#e25218db-566b-4d8b-81ca-e79a8f68c599). +hotspotInfo_ + :: Text + -- ^ Location ID of the hotspot (e.g. \"L2373040\") + -> ClientM LocationData + +{------------------------------------------------------------------------------ + Taxonomy APIs +------------------------------------------------------------------------------} + +-- | Get any version of the eBird taxonomy, with optional filtering based on +-- taxonomy categories and species. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#952a4310-536d-4ad1-8f3e-77cfb624d1bc). +-- +-- __NOTE:__ The eBird API is broken. Always hardcode the 'CSVOrJSONFormat' +-- argument to 'JSONFormat'. +taxonomy_ + :: Maybe TaxonomyCategories + -- ^ Only include species of these 'TaxonomyCategory's in the taxonomy + -- + -- /default: all categories/ + -> Maybe CSVOrJSONFormat + -- ^ Format the taxonomy in CSV or JSON + -- + -- __NOTE:__ This argument should /always/ be hardcoded to 'JSONFormat', even + -- though the default is 'CSVFormat'. It is only here for to be consistent + -- with the eBird API. Unfortunately, the endpoint for this query switches + -- content types based on this query parameter instead of an "Accept" header. + -- That means servant is unable to determine whether the result will be CSV or + -- JSON encoded. For now, the workaround is to always hardcode this to + -- 'JSONFormat'. + -- + -- /default: 'CSVFormat'/ + -> Maybe SPPLocale + -- ^ Use this locale for common names + -- + -- /default: 'En'/ + -> Maybe SpeciesCodes + -- ^ Only fetch records for these species + -- + -- /default: all/ + -> Maybe Text + -- ^ Fetch this version of the eBird taxonomy + -- + -- /default: latest/ + -> ClientM [Taxon] + +-- | Get the list of sub species of a given species recognized in the eBird +-- taxonomy. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#e338e5a6-919d-4603-a7db-6c690fa62371). +taxonomicForms_ + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ Species to get the sub species of + -> ClientM SpeciesCodes + +-- | Get the supported locale codes and names for species common names, with the +-- last time they were updated. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3ea8ff71-c254-4811-9e80-b445a39302a6). +taxaLocaleCodes_ + :: Text + -- ^ eBird API key + -> Maybe SPPLocale + -- ^ Value for the "Accept-Language" header, for translated language names, + -- when available + -- + -- /default: 'En'/ + -> ClientM [SPPLocaleListEntry] + +-- | Get all versions of the taxonomy, with a flag indicating which is latest. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#9bba1ff5-6eb2-4f9a-91fd-e5ed34e51500). +taxonomyVersions_ :: ClientM [TaxonomyVersionListEntry] + +-- | Get the list of species groups, in either Merlin or eBird grouping. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#aa9804aa-dbf9-4a53-bbf4-48e214e4677a). +taxonomicGroups_ + :: SPPGrouping + -- ^ 'MerlinGrouping' groups like birds together, with falcons next to hawks, + -- while 'EBirdGrouping' groups in taxonomy order + -> Maybe SPPLocale + -- ^ Locale to use for species group names. 'En' is used for any locale whose + -- translations are unavailable at this endpoint + -- + -- /default: 'En'/ + -> ClientM [TaxonomicGroupListEntry] + +{------------------------------------------------------------------------------ + Region APIs +------------------------------------------------------------------------------} + +-- | Get a 'RegionInfo' for an eBird region. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#07c64240-6359-4688-9c4f-ff3d678a7248). +regionInfo_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to get information for + -> Maybe RegionNameFormat + -- ^ How to format the region name in the response + -- + -- /default: 'Full'/ + -> ClientM RegionInfo + +-- | Get a list of sub-regions of a given region type within a given region. +-- Keep in mind that many combinations of sub region and parent region are +-- invalid, e.g. 'CountryType' regions within \"US-WY\". +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#382da1c8-8bff-4926-936a-a1f8b065e7d5). +subRegionList_ + :: Text + -- ^ eBird API key + -> RegionType + -- ^ Type of subregions to fetch + -> RegionCode + -- ^ Parent 'RegionCode' + -> ClientM [RegionListEntry] + +-- | Get a list of regions adjacent to a given region. Only 'Subnational2' +-- region codes in the United States, New Zealand, or Mexico are currently +-- supported. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3aca0519-3105-47fc-8611-a4dfd500a32f). +adjacentRegions_ + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the adjacent regions of + -> ClientM [RegionListEntry] + +recentObservations_ + :<|> recentNotableObservations_ + :<|> recentSpeciesObservations_ + :<|> recentNearbyObservations_ + :<|> recentNearbySpeciesObservations_ + :<|> recentNearestSpeciesObservations_ + :<|> recentNearbyNotableObservations_ + :<|> historicalObservations_ + :<|> recentChecklists_ + :<|> top100_ + :<|> checklistFeed_ + :<|> regionalStatistics_ + :<|> speciesList_ + :<|> viewChecklist_ + :<|> regionHotspots_ + :<|> nearbyHotspots_ + :<|> hotspotInfo_ + :<|> taxonomy_ + :<|> taxonomicForms_ + :<|> taxaLocaleCodes_ + :<|> taxonomyVersions_ + :<|> taxonomicGroups_ + :<|> regionInfo_ + :<|> subRegionList_ + :<|> adjacentRegions_ = client (Proxy @EBirdAPI) diff --git a/ebird-client/src/EBird/Client/Hotspots.hs b/ebird-client/src/EBird/Client/Hotspots.hs new file mode 100644 index 0000000..d3dfcf9 --- /dev/null +++ b/ebird-client/src/EBird/Client/Hotspots.hs @@ -0,0 +1,205 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | +-- Module : EBird.Client.Hotspots +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions for hotspot-related eBird API queries. + +module EBird.Client.Hotspots where + +import Data.Default +import Data.Text +import Optics.TH +import Servant.Client + +import EBird.API +import EBird.Client.Generated + +{------------------------------------------------------------------------------ +-- * Region hotspots +------------------------------------------------------------------------------} + +-- | Get all hotspots in a list of one or more regions ('RegionCode'). +-- +-- For example, get the hotspots in Albany County, Wyoming and Park County, +-- Wyoming that have been visited in the last 5 days (using @-XOverloadedLabels@ +-- and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ regionHotspots "US-WY-001,US-WY-029" (def & #back ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#f4f59f90-854e-4ba6-8207-323a8cf0bfe0). +regionHotspots + :: RegionCode + -- ^ Region(s) to get hotspots in + -> RegionHotspotsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRegionHotspotsParams'/ + -> ClientM [Hotspot] +regionHotspots r RegionHotspotsParams{..} = + regionHotspots_ r _regionHotspotsParamsBack + -- Hard coded to JSONFormat because it makes no difference and CSVFormat + -- does not work like it should. See the note on the generated function's + -- parameter documentation. + (Just JSONFormat) + +-- | Optional parameters accepted by the 'RegionHotspotsAPI'. +-- +-- Note that 'defaultRegionHotspotsParams' (or the 'Default' instance's 'def' +-- value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_regionHotspotsParamsBack' field to 10: +-- +-- > def & regionHotspotsParamsBack ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #back ?~ 10 +newtype RegionHotspotsParams = + RegionHotspotsParams + { -- | Only fetch hotspots that have been visited within this many days + -- ago + -- + -- /1 - 30, default: no limit/ + _regionHotspotsParamsBack :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRegionHotspotsParams :: RegionHotspotsParams +defaultRegionHotspotsParams = + RegionHotspotsParams + { _regionHotspotsParamsBack = Nothing + } + +instance Default RegionHotspotsParams where + def = defaultRegionHotspotsParams + +-- ** Optics for 'RegionHotspotsParams' +makeLenses ''RegionHotspotsParams +makeFieldLabels ''RegionHotspotsParams + +{------------------------------------------------------------------------------ +-- * Nearby hotspots +------------------------------------------------------------------------------} + +-- | Get all hotspots within a radius of some latitude/longitude. +-- +-- For example, get the hotspots within 30km of Cody, Wyoming that have been +-- visited in the last 5 days (using @-XOverloadedLabels@ +-- and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- nearbyHotspots +-- 44.526340 (-109.056534) +-- (def & #radius ?~ 30 & #back ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#674e81c1-6a0c-4836-8a7e-6ea1fe8e6677). +nearbyHotspots + :: Double + -- ^ Latitude of the location to get hotspots near + -> Double + -- ^ Longitude of the location to get hotspots near + -> NearbyHotspotsParams + -- ^ Optional parameters + -- + -- /default: 'defaultNearbyHotspotsParams'/ + -> ClientM [Hotspot] +nearbyHotspots lat lng NearbyHotspotsParams{..} = + nearbyHotspots_ lat lng + _nearbyHotspotsParamsBack + _nearbyHotspotsParamsRadius + -- Hard coded to JSONFormat because it makes no difference and CSVFormat + -- does not work like it should. See the note on the generated function's + -- parameter documentation. + (Just JSONFormat) + +-- | Optional parameters accepted by the 'NearbyHotspotsAPI'. +-- +-- Note that 'defaultNearbyHotspotsParams' (or the 'Default' instance's 'def' +-- value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_nearbyHotspotsParamsBack' field to 10: +-- +-- > def & nearbyHotspotsParamsBack ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #back ?~ 10 +data NearbyHotspotsParams = + NearbyHotspotsParams + { -- | Only fetch hotspots that have been visited within this many days + -- ago + -- + -- /1 - 30, default: no limit/ + _nearbyHotspotsParamsBack :: Maybe Integer + + -- ^ Search radius in kilometers + -- + -- /0 - 50, default: 25/ + , _nearbyHotspotsParamsRadius :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultNearbyHotspotsParams :: NearbyHotspotsParams +defaultNearbyHotspotsParams = + NearbyHotspotsParams + { _nearbyHotspotsParamsBack = Nothing + , _nearbyHotspotsParamsRadius = Nothing + } + +instance Default NearbyHotspotsParams where + def = defaultNearbyHotspotsParams + +-- ** Optics for 'NearbyHotspotsParams' +makeLenses ''NearbyHotspotsParams +makeFieldLabels ''NearbyHotspotsParams + +{------------------------------------------------------------------------------ +-- * Hotspot info +------------------------------------------------------------------------------} + +-- | Get information about a hotspot. +-- +-- For example, get information for a hotspot with location ID +-- \"L2373040\" (using @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ hotspotInfo "L2373040" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'hotspotInfo' +-- is equivalent to the generated 'hotspotInfo_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#e25218db-566b-4d8b-81ca-e79a8f68c599). +hotspotInfo + :: Text + -- ^ Hotspot location ID, e.g. \"L2373040\" + -> ClientM LocationData +hotspotInfo = hotspotInfo_ diff --git a/ebird-client/src/EBird/Client/Observations.hs b/ebird-client/src/EBird/Client/Observations.hs new file mode 100644 index 0000000..afb29e6 --- /dev/null +++ b/ebird-client/src/EBird/Client/Observations.hs @@ -0,0 +1,958 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | +-- Module : EBird.Client.Observations +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions for observation-related eBird API queries. + +module EBird.Client.Observations where + +import Data.Default +import Data.Text +import Optics.TH +import Servant.Client + +import EBird.API +import EBird.Client.Generated + +{------------------------------------------------------------------------------ +-- * Recent observations +------------------------------------------------------------------------------} + +-- | Get a list of recent observations within a region. Results only include the +-- most recent observation for each species in the region. +-- +-- For example, get up to 10 recent observations from the last 5 days in Park +-- County, Wyoming (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentObservations key +-- "US-WY-029" +-- (def & #maxResults ?~ 10 & #back ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3d2a17c1-2129-475c-b4c8-7d362d6000cd). +recentObservations + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> RecentObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentObservationsParams'/ + -> ClientM [Observation 'Simple] +recentObservations k r RecentObservationsParams{..} = + recentObservations_ k r + _recentObservationsParamsBack + _recentObservationsParamsCategories + _recentObservationsParamsHotspot + _recentObservationsParamsProvisional + _recentObservationsParamsMaxResults + _recentObservationsParamsExtraRegions + _recentObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentObservationsAPI'. +-- +-- Note that 'defaultRecentObservationsParams' (or the 'Default' instance's +-- 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentObservationsParamsBack' field to 30: +-- +-- > def & recentObservationsParamsBack ?~ 30 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #back ?~ 30 +data RecentObservationsParams = + RecentObservationsParams + { -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + _recentObservationsParamsBack :: Maybe Integer + + -- | Only include observations in these taxonomy categories + -- + -- /default: all categories/ + , _recentObservationsParamsCategories :: Maybe TaxonomyCategories + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _recentObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentObservationsParamsMaxResults :: Maybe Integer + + -- | Up to 10 extra regions to get observations from + -- + -- /default: none/ + , _recentObservationsParamsExtraRegions :: Maybe RegionCode + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentObservationsParams :: RecentObservationsParams +defaultRecentObservationsParams = + RecentObservationsParams + { _recentObservationsParamsBack = Nothing + , _recentObservationsParamsCategories = Nothing + , _recentObservationsParamsHotspot = Nothing + , _recentObservationsParamsProvisional = Nothing + , _recentObservationsParamsMaxResults = Nothing + , _recentObservationsParamsExtraRegions = Nothing + , _recentObservationsParamsLocale = Nothing + } + +instance Default RecentObservationsParams where + def = defaultRecentObservationsParams + +-- ** Optics for 'RecentObservationsParams' +makeLenses ''RecentObservationsParams +makeFieldLabels ''RecentObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent notable observations +------------------------------------------------------------------------------} + +-- | Get a list of recent notable observations within a region. Results only +-- include the most recent observation for each species in the region. +-- +-- For example, get up to 10 recent notable observations from the last 30 days +-- in Park County, Wyoming (using @-XOverloadedLabels@ and +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentNotableObservations key +-- "US-WY-029" +-- (def & #maxResults ?~ 10 & #back ?~ 30) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#397b9b8c-4ab9-4136-baae-3ffa4e5b26e4). +recentNotableObservations + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> RecentNotableObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentNotableObservationsParams'/ + -> ClientM [SomeObservation] +recentNotableObservations k r RecentNotableObservationsParams{..} = + recentNotableObservations_ k r + _recentNotableObservationsParamsBack + _recentNotableObservationsParamsDetail + _recentNotableObservationsParamsHotspot + _recentNotableObservationsParamsMaxResults + _recentNotableObservationsParamsExtraRegions + _recentNotableObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentNotableObservationsAPI'. +-- +-- Note that 'defaultRecentNotableObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentNotableObservationsParamsBack' field to 30: +-- +-- > def & recentNotableObservationsParamsBack ?~ 30 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #back ?~ 30 +data RecentNotableObservationsParams = + RecentNotableObservationsParams + { -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + _recentNotableObservationsParamsBack :: Maybe Integer + + -- | Detail level for the resulting observations + -- + -- /default: 'Simple'/ + , _recentNotableObservationsParamsDetail :: Maybe DetailLevel + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentNotableObservationsParamsHotspot :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentNotableObservationsParamsMaxResults :: Maybe Integer + + -- | Up to 10 extra regions to get observations from + -- + -- /default: none/ + , _recentNotableObservationsParamsExtraRegions :: Maybe RegionCode + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentNotableObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentNotableObservationsParams :: RecentNotableObservationsParams +defaultRecentNotableObservationsParams = + RecentNotableObservationsParams + { _recentNotableObservationsParamsBack = Nothing + , _recentNotableObservationsParamsDetail = Nothing + , _recentNotableObservationsParamsHotspot = Nothing + , _recentNotableObservationsParamsMaxResults = Nothing + , _recentNotableObservationsParamsExtraRegions = Nothing + , _recentNotableObservationsParamsLocale = Nothing + } + +instance Default RecentNotableObservationsParams where + def = defaultRecentNotableObservationsParams + +-- ** Optics for 'RecentNotableObservationsParams' +makeLenses ''RecentNotableObservationsParams +makeFieldLabels ''RecentNotableObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent species observations +------------------------------------------------------------------------------} + +-- | Get a list of recent observations of a specific species within a region. +-- +-- For example, get observations of Peregrine Falcons from the last 30 days in +-- Park County, Wyoming (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentSpeciesObservations key +-- "US-WY-029" +-- "perfal" +-- (def & #back ?~ 30) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#755ce9ab-dc27-4cfc-953f-c69fb0f282d9). +recentSpeciesObservations + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> SpeciesCode + -- ^ Species to get observations of (e.g. "barswa" for Barn Swallow) + -> RecentSpeciesObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentSpeciesObservationsParams'/ + -> ClientM [Observation 'Simple] +recentSpeciesObservations k r sp RecentSpeciesObservationsParams{..} = + recentSpeciesObservations_ k r sp + _recentSpeciesObservationsParamsBack + _recentSpeciesObservationsParamsHotspot + _recentSpeciesObservationsParamsProvisional + _recentSpeciesObservationsParamsMaxResults + _recentSpeciesObservationsParamsExtraRegions + _recentSpeciesObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentSpeciesObservationsAPI'. +-- +-- Note that 'defaultRecentSpeciesObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentSpeciesObservationsParamsBack' field to 30: +-- +-- > def & recentSpeciesObservationsParamsBack ?~ 30 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #back ?~ 30 +data RecentSpeciesObservationsParams = + RecentSpeciesObservationsParams + { -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + _recentSpeciesObservationsParamsBack :: Maybe Integer + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentSpeciesObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _recentSpeciesObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentSpeciesObservationsParamsMaxResults :: Maybe Integer + + -- | Up to 10 extra regions to get observations from + -- + -- /default: none/ + , _recentSpeciesObservationsParamsExtraRegions :: Maybe RegionCode + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentSpeciesObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentSpeciesObservationsParams :: RecentSpeciesObservationsParams +defaultRecentSpeciesObservationsParams = + RecentSpeciesObservationsParams + { _recentSpeciesObservationsParamsBack = Nothing + , _recentSpeciesObservationsParamsHotspot = Nothing + , _recentSpeciesObservationsParamsProvisional = Nothing + , _recentSpeciesObservationsParamsMaxResults = Nothing + , _recentSpeciesObservationsParamsExtraRegions = Nothing + , _recentSpeciesObservationsParamsLocale = Nothing + } + +instance Default RecentSpeciesObservationsParams where + def = defaultRecentSpeciesObservationsParams + +-- ** Optics for 'RecentSpciesObservationsParams' +makeLenses ''RecentSpeciesObservationsParams +makeFieldLabels ''RecentSpeciesObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent nearby observations +------------------------------------------------------------------------------} + +-- | Get a list of recent observations within some radius of some +-- latitude/longitude. +-- +-- For example, get up to 5 nearby observations within 10km of Cody, Wyoming +-- (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentNearbyObservations key +-- 44.526340 (-109.056534) +-- (def & #maxResults ?~ 5 & #radius ?~ 10) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#397b9b8c-4ab9-4136-baae-3ffa4e5b26e4). +recentNearbyObservations + :: Text + -- ^ eBird API key + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> RecentNearbyObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentNearbyObservationsParams'/ + -> ClientM [Observation 'Simple] +recentNearbyObservations k lat lng RecentNearbyObservationsParams{..} = + recentNearbyObservations_ k lat lng + _recentNearbyObservationsParamsRadius + _recentNearbyObservationsParamsBack + _recentNearbyObservationsParamsCategories + _recentNearbyObservationsParamsHotspot + _recentNearbyObservationsParamsProvisional + _recentNearbyObservationsParamsMaxResults + _recentNearbyObservationsParamsSortBy + _recentNearbyObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentNearbyObservationsAPI'. +-- +-- Note that 'defaultRecentNearbyObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentNearbyObservationsParamsRadius' field to 10km: +-- +-- > def & recentNearbyObservationsParamsRadius ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #radius ?~ 10 +data RecentNearbyObservationsParams = + RecentNearbyObservationsParams + { -- | Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + _recentNearbyObservationsParamsRadius :: Maybe Integer + + -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + , _recentNearbyObservationsParamsBack :: Maybe Integer + + -- | Only include observations in these taxonomy categories + -- + -- /default: all/ + , _recentNearbyObservationsParamsCategories :: Maybe TaxonomyCategories + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentNearbyObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _recentNearbyObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentNearbyObservationsParamsMaxResults :: Maybe Integer + + -- | Sort observations by taxonomy ('SortObservationsBySpecies') or by + -- date ('SortObservationsByDate', most recent first) + -- + -- /default: 'SortObservationsByDate'/ + , _recentNearbyObservationsParamsSortBy :: Maybe SortObservationsBy + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentNearbyObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentNearbyObservationsParams :: RecentNearbyObservationsParams +defaultRecentNearbyObservationsParams = + RecentNearbyObservationsParams + { _recentNearbyObservationsParamsRadius = Nothing + , _recentNearbyObservationsParamsBack = Nothing + , _recentNearbyObservationsParamsCategories = Nothing + , _recentNearbyObservationsParamsHotspot = Nothing + , _recentNearbyObservationsParamsProvisional = Nothing + , _recentNearbyObservationsParamsMaxResults = Nothing + , _recentNearbyObservationsParamsSortBy = Nothing + , _recentNearbyObservationsParamsLocale = Nothing + } + +instance Default RecentNearbyObservationsParams where + def = defaultRecentNearbyObservationsParams + +-- ** Optics for 'RecentNearbyObservationsParams' +makeLenses ''RecentNearbyObservationsParams +makeFieldLabels ''RecentNearbyObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent nearby species observations +------------------------------------------------------------------------------} + +-- | Get a list of recent observations of a species within some radius of some +-- latitude/longitude. +-- +-- For example, get up to 5 nearby observations of Peregrine Falcons within 50km +-- of Cody, Wyoming (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentNearbySpeciesObservations key +-- "perfal" +-- 44.526340 (-109.056534) +-- (def & #radius ?~ 50 & #maxResults ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#20fb2c3b-ee7f-49ae-a912-9c3f16a40397). +recentNearbySpeciesObservations + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ Species to get observations of (e.g. "bohwax" for Bohemian Waxwing) + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> RecentNearbySpeciesObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentNearbySpeciesObservationsParams'/ + -> ClientM [Observation 'Simple] +recentNearbySpeciesObservations k sp lat lng RecentNearbySpeciesObservationsParams{..} = + recentNearbySpeciesObservations_ k sp lat lng + _recentNearbySpeciesObservationsParamsRadius + _recentNearbySpeciesObservationsParamsBack + _recentNearbySpeciesObservationsParamsCategories + _recentNearbySpeciesObservationsParamsHotspot + _recentNearbySpeciesObservationsParamsProvisional + _recentNearbySpeciesObservationsParamsMaxResults + _recentNearbySpeciesObservationsParamsSortBy + _recentNearbySpeciesObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentNearbySpeciesObservationsAPI'. +-- +-- Note that 'defaultRecentNearbySpeciesObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentNearbySpeciesObservationsParamsRadius' field to 10km: +-- +-- > def & recentNearbySpeciesObservationsParamsRadius ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #radius ?~ 10 +data RecentNearbySpeciesObservationsParams = + RecentNearbySpeciesObservationsParams + { -- | Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + _recentNearbySpeciesObservationsParamsRadius :: Maybe Integer + + -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + , _recentNearbySpeciesObservationsParamsBack :: Maybe Integer + + -- | Only include observations in these taxonomy categories + -- + -- /default: all/ + , _recentNearbySpeciesObservationsParamsCategories :: Maybe TaxonomyCategories + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentNearbySpeciesObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _recentNearbySpeciesObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentNearbySpeciesObservationsParamsMaxResults :: Maybe Integer + + -- | Sort observations by taxonomy ('SortObservationsBySpecies') or by + -- date ('SortObservationsByDate', most recent first) + -- + -- /default: 'SortObservationsByDate'/ + , _recentNearbySpeciesObservationsParamsSortBy :: Maybe SortObservationsBy + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentNearbySpeciesObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentNearbySpeciesObservationsParams :: RecentNearbySpeciesObservationsParams +defaultRecentNearbySpeciesObservationsParams = + RecentNearbySpeciesObservationsParams + { _recentNearbySpeciesObservationsParamsRadius = Nothing + , _recentNearbySpeciesObservationsParamsBack = Nothing + , _recentNearbySpeciesObservationsParamsCategories = Nothing + , _recentNearbySpeciesObservationsParamsHotspot = Nothing + , _recentNearbySpeciesObservationsParamsProvisional = Nothing + , _recentNearbySpeciesObservationsParamsMaxResults = Nothing + , _recentNearbySpeciesObservationsParamsSortBy = Nothing + , _recentNearbySpeciesObservationsParamsLocale = Nothing + } + +instance Default RecentNearbySpeciesObservationsParams where + def = defaultRecentNearbySpeciesObservationsParams + +-- ** Optics for 'RecentNearbySpeciesObservationsParams' +makeLenses ''RecentNearbySpeciesObservationsParams +makeFieldLabels ''RecentNearbySpeciesObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent nearest species observations +------------------------------------------------------------------------------} + +-- | Get a list of recent observations of some species nearest to some +-- latitude/longitude. +-- +-- For example, get the 5 nearest observations of Black-throated Gray Warblers +-- within 50km of Capitol Reef National Park (using @-XOverloadedLabels@ and +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentNearestSpeciesObservations key +-- "btywar" +-- 38.366970 (-111.261504) +-- (def & #radius ?~ 50 & #maxResults ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#6bded97f-9997-477f-ab2f-94f254954ccb). +recentNearestSpeciesObservations + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ Species to get observations of (e.g. "bohwax" for Bohemian Waxwing) + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> RecentNearestSpeciesObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentNearestSpeciesObservationsParams'/ + -> ClientM [Observation 'Simple] +recentNearestSpeciesObservations k sp lat lng RecentNearestSpeciesObservationsParams{..} = + recentNearestSpeciesObservations_ k sp lat lng + _recentNearestSpeciesObservationsParamsRadius + _recentNearestSpeciesObservationsParamsBack + _recentNearestSpeciesObservationsParamsHotspot + _recentNearestSpeciesObservationsParamsProvisional + _recentNearestSpeciesObservationsParamsMaxResults + _recentNearestSpeciesObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentNearestSpeciesObservationsAPI'. +-- +-- Note that 'defaultRecentNearestSpeciesObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentNearestSpeciesObservationsParamsRadius' field to 10km: +-- +-- > def & recentNearestSpeciesObservationsParamsRadius ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #radius ?~ 10 +data RecentNearestSpeciesObservationsParams = + RecentNearestSpeciesObservationsParams + { -- | Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + _recentNearestSpeciesObservationsParamsRadius :: Maybe Integer + + -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + , _recentNearestSpeciesObservationsParamsBack :: Maybe Integer + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentNearestSpeciesObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _recentNearestSpeciesObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentNearestSpeciesObservationsParamsMaxResults :: Maybe Integer + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentNearestSpeciesObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentNearestSpeciesObservationsParams :: RecentNearestSpeciesObservationsParams +defaultRecentNearestSpeciesObservationsParams = + RecentNearestSpeciesObservationsParams + { _recentNearestSpeciesObservationsParamsRadius = Nothing + , _recentNearestSpeciesObservationsParamsBack = Nothing + , _recentNearestSpeciesObservationsParamsHotspot = Nothing + , _recentNearestSpeciesObservationsParamsProvisional = Nothing + , _recentNearestSpeciesObservationsParamsMaxResults = Nothing + , _recentNearestSpeciesObservationsParamsLocale = Nothing + } + +instance Default RecentNearestSpeciesObservationsParams where + def = defaultRecentNearestSpeciesObservationsParams + +-- ** Optics for 'RecentNearestSpeciesObservationsParams' +makeLenses ''RecentNearestSpeciesObservationsParams +makeFieldLabels ''RecentNearestSpeciesObservationsParams + +{------------------------------------------------------------------------------ +-- * Recent nearby notable observations +------------------------------------------------------------------------------} + +-- | Get a list of recent /notable/ observations of some near some +-- latitude/longitude. +-- +-- For example, get 5 notable observations within 25km of Capitol Reef National +-- Park (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentNearbyNotableObservations key +-- 38.366970 (-111.261504) +-- (def & #radius ?~ 25 & #maxResults ?~ 5) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#caa348bb-71f6-471c-b203-9e1643377cbc). +recentNearbyNotableObservations + :: Text + -- ^ eBird API key + -> Double + -- ^ Latitude of the location to get observations near + -> Double + -- ^ Longitude of the location to get observations near + -> RecentNearbyNotableObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentNearbyNotableObservationsParams'/ + -> ClientM [SomeObservation] +recentNearbyNotableObservations k lat lng RecentNearbyNotableObservationsParams{..} = + recentNearbyNotableObservations_ k lat lng + _recentNearbyNotableObservationsParamsRadius + _recentNearbyNotableObservationsParamsDetail + _recentNearbyNotableObservationsParamsBack + _recentNearbyNotableObservationsParamsHotspot + _recentNearbyNotableObservationsParamsMaxResults + _recentNearbyNotableObservationsParamsLocale + +-- | Optional parameters accepted by the 'RecentNearbyNotableObservationsAPI'. +-- +-- Note that 'defaultRecentNearbyNotableObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentNearbyNotableObservationsParamsRadius' field to 10km: +-- +-- > def & recentNearbyNotableObservationsParamsRadius ?~ 10 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #radius ?~ 10 +data RecentNearbyNotableObservationsParams = + RecentNearbyNotableObservationsParams + { -- | Search radius from the given latitude/longitude in kilometers + -- + -- /0 - 50, default: 25/ + _recentNearbyNotableObservationsParamsRadius :: Maybe Integer + + -- | Detail level for the resulting observations + -- + -- /default: 'Simple'/ + , _recentNearbyNotableObservationsParamsDetail :: Maybe DetailLevel + + -- | How many days back to look for observations + -- + -- /1 - 30, default: 14/ + , _recentNearbyNotableObservationsParamsBack :: Maybe Integer + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _recentNearbyNotableObservationsParamsHotspot :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _recentNearbyNotableObservationsParamsMaxResults :: Maybe Integer + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _recentNearbyNotableObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentNearbyNotableObservationsParams :: RecentNearbyNotableObservationsParams +defaultRecentNearbyNotableObservationsParams = + RecentNearbyNotableObservationsParams + { _recentNearbyNotableObservationsParamsRadius = Nothing + , _recentNearbyNotableObservationsParamsDetail = Nothing + , _recentNearbyNotableObservationsParamsBack = Nothing + , _recentNearbyNotableObservationsParamsHotspot = Nothing + , _recentNearbyNotableObservationsParamsMaxResults = Nothing + , _recentNearbyNotableObservationsParamsLocale = Nothing + } + +instance Default RecentNearbyNotableObservationsParams where + def = defaultRecentNearbyNotableObservationsParams + +-- ** Optics for 'RecentNearbyNotableObservationsParams' +makeLenses ''RecentNearbyNotableObservationsParams +makeFieldLabels ''RecentNearbyNotableObservationsParams + +{------------------------------------------------------------------------------ +-- * Historical observations +------------------------------------------------------------------------------} + +-- | Get a list of observations for each species seen on a specific date. +-- +-- For example, get a list of 10 fully detailed observations for each species +-- seen on July 11th, 2023 in Park County, Wyoming (using @-XOverloadedLabels@ +-- and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- historicalObservations key +-- "US-WY-029" +-- "2023-07-11" +-- (def & #maxResults ?~ 10 & #detail ?~ Full) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8c6ee8-c435-4e91-9f66-6d3eeb09edd2). +historicalObservations + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get observations from + -> EBirdDate + -- ^ Date to get observations on, from year 1800 to present + -> HistoricalObservationsParams + -- ^ Optional parameters + -- + -- /default: 'defaultHistoricalObservationsParams'/ + -> ClientM [SomeObservation] +historicalObservations k r date HistoricalObservationsParams{..} = + historicalObservations_ k r y m d + _historicalObservationsParamsCategories + _historicalObservationsParamsDetail + _historicalObservationsParamsHotspot + _historicalObservationsParamsProvisional + _historicalObservationsParamsMaxResults + _historicalObservationsParamsSelect + _historicalObservationsParamsExtraRegions + _historicalObservationsParamsLocale + where + (y,m,d) = eBirdDateToGregorian date + +-- | Optional parameters accepted by the 'HistoricalObservationsAPI'. +-- +-- Note that 'defaultHistoricalObservationsParams' (or the 'Default' +-- instance's 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_historicalObservationsParamsDetail' field to 'Full': +-- +-- > def & historicalObservationsParamsDetail ?~ Full +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #detail ?~ Full +data HistoricalObservationsParams = + HistoricalObservationsParams + { -- | Only include observations in these taxonomy categories + -- + -- /default: all/ + _historicalObservationsParamsCategories :: Maybe TaxonomyCategories + + -- | Detail level for the resulting observations + -- + -- /default: 'Simple'/ + , _historicalObservationsParamsDetail :: Maybe DetailLevel + + -- | Only get observations from hotspots + -- + -- /default: 'False'/ + , _historicalObservationsParamsHotspot :: Maybe Bool + + -- | Include observations which have not been reviewed + -- + -- /default: 'False'/ + , _historicalObservationsParamsProvisional :: Maybe Bool + + -- | Maximum number of observations to get + -- + -- /1 - 10000, default: all/ + , _historicalObservationsParamsMaxResults :: Maybe Integer + + -- | Whether to display the first or last observation of a species on + -- the date, in the case that there are multiple observations of the + -- same species on the date + -- + -- /default: 'SelectLastObservation'/ + , _historicalObservationsParamsSelect :: Maybe SelectObservation + + -- | Up to 50 extra regions to get observations from + -- + -- /default: none/ + , _historicalObservationsParamsExtraRegions :: Maybe RegionCode + + -- | Return observations with common names in this locale + -- + -- /default: 'En'/ + , _historicalObservationsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultHistoricalObservationsParams :: HistoricalObservationsParams +defaultHistoricalObservationsParams = + HistoricalObservationsParams + { _historicalObservationsParamsCategories = Nothing + , _historicalObservationsParamsDetail = Nothing + , _historicalObservationsParamsHotspot = Nothing + , _historicalObservationsParamsProvisional = Nothing + , _historicalObservationsParamsMaxResults = Nothing + , _historicalObservationsParamsSelect = Nothing + , _historicalObservationsParamsExtraRegions = Nothing + , _historicalObservationsParamsLocale = Nothing + } + +instance Default HistoricalObservationsParams where + def = defaultHistoricalObservationsParams + +-- ** Optics for 'HistoricalObservationsParams' +makeLenses ''HistoricalObservationsParams +makeFieldLabels ''HistoricalObservationsParams diff --git a/ebird-client/src/EBird/Client/Product.hs b/ebird-client/src/EBird/Client/Product.hs new file mode 100644 index 0000000..3f83f26 --- /dev/null +++ b/ebird-client/src/EBird/Client/Product.hs @@ -0,0 +1,350 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | +-- Module : EBird.Client.Product +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions for product-related eBird API queries. + +module EBird.Client.Product where + +import Data.Default +import Data.Text +import Optics.TH +import Servant.Client + +import EBird.API +import EBird.Client.Generated + +{------------------------------------------------------------------------------ +-- * Recent checklists +------------------------------------------------------------------------------} + +-- | Get a list recently submitted checklists within a region. +-- +-- For example, get up to 3 recent checklists submitted in Park County, Wyoming +-- (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- recentChecklists key +-- "US-WY-029" +-- (def & #maxResults ?~ 3) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#95a206d1-a20d-44e0-8c27-acb09ccbea1a). +recentChecklists + :: Text + -- ^ eBird API key + -> RegionCode + -- ^ Region(s) to get Checklists from + -> RecentChecklistsParams + -- ^ Optional parameters + -- + -- /default: 'defaultRecentChecklistsParams'/ + -> ClientM [ChecklistFeedEntry] +recentChecklists k r RecentChecklistsParams{..} = + recentChecklists_ k r _recentChecklistsParamsMaxResults + +-- | Optional parameters accepted by the 'RecentChecklistsAPI'. +-- +-- Note that 'defaultRecentChecklistsParams' (or the 'Default' instance's +-- 'def' value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_recentChecklistsParamsMaxResults' field to 3: +-- +-- > def & recentChecklistsParamsMaxResults ?~ 3 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #maxResults ?~ 3 +newtype RecentChecklistsParams = + RecentChecklistsParams + { -- | Maximum number of checklists to get + -- + -- /1 - 200, default: 10/ + _recentChecklistsParamsMaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRecentChecklistsParams :: RecentChecklistsParams +defaultRecentChecklistsParams = + RecentChecklistsParams + { _recentChecklistsParamsMaxResults = Nothing + } + +instance Default RecentChecklistsParams where + def = defaultRecentChecklistsParams + +-- ** Optics for 'RecentChecklistsParams' +makeLenses ''RecentChecklistsParams +makeFieldLabels ''RecentChecklistsParams + +{------------------------------------------------------------------------------ +-- * Top 100 +------------------------------------------------------------------------------} + +-- | Get a list of top contributors for a region on a specific date, ranked by +-- number of species observed or number of checklists submitted. +-- +-- For example, get the top 10 contributors by number of species observed on +-- July 11th, 2023 in Wyoming (using @-XOverloadedLabels@ and +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- top100 key +-- "US-WY" +-- "2023-07-11" +-- (def & #maxResults ?~ 10) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2d8d3f94-c4b0-42bd-9c8e-71edfa6347ba). +top100 + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the ranking for + -- + -- __Note:__ Only country, subnational1, or location regions are supported for + -- this endpoint of the eBird API. + -> EBirdDate + -- ^ Date to get the top 100 on + -> Top100Params + -- ^ Optional parameters + -- + -- /default: 'defaultTop100Params'/ + -> ClientM [Top100ListEntry] +top100 k r date Top100Params{..} = + top100_ k r y m d _top100ParamsRankBy _top100ParamsMaxResults + where + (y,m,d) = eBirdDateToGregorian date + +-- | Optional parameters accepted by the 'Top100API'. +-- +-- Note that 'defaultTop100Params' (or the 'Default' instance's 'def' value) may +-- be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_top100ParamsMaxResults' field to 50: +-- +-- > def & top100ParamsMaxResults ?~ 50 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #maxResults ?~ 50 +data Top100Params = + Top100Params + { -- | Rank the resulting list by number of species observed or by number of + -- checklists completed + -- + -- /default: 'RankTop100BySpecies'/ + _top100ParamsRankBy :: Maybe RankTop100By + + -- | Maximum number of entries to fetch + -- + -- /1 - 100, default: 100/ + , _top100ParamsMaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultTop100Params :: Top100Params +defaultTop100Params = + Top100Params + { _top100ParamsRankBy = Nothing + , _top100ParamsMaxResults = Nothing + } + +instance Default Top100Params where + def = defaultTop100Params + +-- ** Optics for 'Top100Params' +makeLenses ''Top100Params +makeFieldLabels ''Top100Params + +{------------------------------------------------------------------------------ +-- * Checklist feed +------------------------------------------------------------------------------} + +-- | Get a list of checklists submitted within a region on a specific date. +-- +-- For example, get a feed of 10 checklists submitted in Park County, Wyoming on +-- July 11th, 2023 (using @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- checklistFeed key +-- "US-WY-029" +-- "2023-07-11" +-- (def & #maxResults ?~ 10) +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#4416a7cc-623b-4340-ab01-80c599ede73e). +checklistFeed + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the checklist feed for + -> EBirdDate + -- ^ Date to get the checklist feed on + -> ChecklistFeedParams + -- ^ Optional parameters + -- + -- /default: 'defaultChecklistFeedParams'/ + -> ClientM [ChecklistFeedEntry] +checklistFeed k r date ChecklistFeedParams{..} = + checklistFeed_ k r y m d + _checklistFeedParamsSortBy + _checklistFeedParamsMaxResults + where + (y,m,d) = eBirdDateToGregorian date + +-- | Optional parameters accepted by the 'ChecklistFeedAPI'. +-- +-- Note that 'defaultChecklistFeedParams' (or the 'Default' instance's 'def' value) may +-- be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_checklistFeedParamsMaxResults' field to 50: +-- +-- > def & checklistFeedParamsMaxResults ?~ 50 +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #maxResults ?~ 50 +data ChecklistFeedParams = + ChecklistFeedParams + { -- | Sort the resulting list by date of checklist submission or date of + -- checklist creation + -- + -- /default: 'SortChecklistsByDateCreated'/ + _checklistFeedParamsSortBy :: Maybe SortChecklistsBy + + -- | Maximum number of checklists to get + -- + -- /1 - 200, default: 10/ + , _checklistFeedParamsMaxResults :: Maybe Integer + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultChecklistFeedParams :: ChecklistFeedParams +defaultChecklistFeedParams = + ChecklistFeedParams + { _checklistFeedParamsSortBy = Nothing + , _checklistFeedParamsMaxResults = Nothing + } + +instance Default ChecklistFeedParams where + def = defaultChecklistFeedParams + +-- ** Optics for 'ChecklistFeedParams' +makeLenses ''ChecklistFeedParams +makeFieldLabels ''ChecklistFeedParams + +{------------------------------------------------------------------------------ +-- * Regional statistics +------------------------------------------------------------------------------} + +-- | Get the 'RegionalStatistics' for a region on a specific date. +-- +-- For example, get the statistics for Wyoming on July 11th, 2023 (using +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ +-- regionalStatistics key +-- "US-WY" +-- "2023-07-11" +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#506e63ab-abc0-4256-b74c-cd9e77968329). +regionalStatistics + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the statistics for + -> EBirdDate + -- ^ Date to get the statistics on + -> ClientM RegionalStatistics +regionalStatistics k r date = + regionalStatistics_ k r y m d + where + (y,m,d) = eBirdDateToGregorian date + +{------------------------------------------------------------------------------ +-- * Species list +------------------------------------------------------------------------------} + +-- | Get a list of all species ever seen in a 'Region'. +-- +-- For example, get all species ever seen in Park County, Wyoming (using +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ speciesList key "US-WY-029" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'speciesList' is +-- equivalent to the generated 'speciesList_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#55bd1b26-6951-4a88-943a-d3a8aa1157dd). +speciesList + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the species list for + -> ClientM [SpeciesCode] +speciesList = speciesList_ + +{------------------------------------------------------------------------------ +-- * View checklist +------------------------------------------------------------------------------} + +-- | Get information about a checklist. +-- +-- For example, get information for a checklist with submission ID +-- \"S144646447\" (using @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ viewChecklist key "S144646447" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'viewChecklist' +-- is equivalent to the generated 'viewChecklist_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#2ee89672-4211-4fc1-8493-5df884fbb386). +viewChecklist + :: Text + -- ^ eBird API key + -> Text + -- ^ Checklist submission ID, e.g. \"S144646447\" + -> ClientM Checklist +viewChecklist = viewChecklist_ diff --git a/ebird-client/src/EBird/Client/Regions.hs b/ebird-client/src/EBird/Client/Regions.hs new file mode 100644 index 0000000..28d9d35 --- /dev/null +++ b/ebird-client/src/EBird/Client/Regions.hs @@ -0,0 +1,149 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | +-- Module : EBird.Client.Regions +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions for region-related eBird API queries. + +module EBird.Client.Regions where + +import Data.Default +import Data.Text +import Optics.TH +import Servant.Client + +import EBird.API +import EBird.Client.Generated + +{------------------------------------------------------------------------------ +-- * Region info +------------------------------------------------------------------------------} + +-- | Get a 'RegionInfo' for an eBird region. +-- +-- For example, get information about the Park County, Wyoming region (using +-- @-XOverloadedLabels@ and @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ regionInfo key "US-WY-029" def +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#07c64240-6359-4688-9c4f-ff3d678a7248). +regionInfo + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to get information for + -> RegionInfoParams + -- ^ Optional parameters + -- + -- /default: 'defaultRegionInfoParams'/ + -> ClientM RegionInfo +regionInfo k r RegionInfoParams{..} = regionInfo_ k r _regionInfoParamsFormat + +-- | Optional parameters accepted by the 'RegionInfoAPI'. +-- +-- Note that 'defaultRegionInfoParams' (or the 'Default' instance's 'def' value) +-- may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_regionInfoParamsFormat' field to 'NameOnlyNameFormat' (using +-- @-XOverloadedString@): +-- +-- > def & regionInfoParamsFormat ?~ "nameonly" +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #format ?~ "nameonly" +newtype RegionInfoParams = + RegionInfoParams + { -- | How to format the region name in the response + -- + -- /default: 'Full'/ + _regionInfoParamsFormat :: Maybe RegionNameFormat + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultRegionInfoParams :: RegionInfoParams +defaultRegionInfoParams = + RegionInfoParams + { _regionInfoParamsFormat = Nothing + } + +instance Default RegionInfoParams where + def = defaultRegionInfoParams + +-- ** Optics for 'RegionInfoParams' +makeLenses ''RegionInfoParams +makeFieldLabels ''RegionInfoParams + +{------------------------------------------------------------------------------ +-- * Sub region list +------------------------------------------------------------------------------} + +-- | Get a list of sub-regions of a given region type within a given region. +-- Keep in mind that many combinations of sub-region and parent region are +-- invalid, e.g. 'CountryType' regions within \"US-WY\". +-- +-- For example, get county sub regions of Wyoming (using @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ subRegionList key Subnational2Type "US-WY" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'subRegionList' +-- is equivalent to the generated 'subRegionList_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#382da1c8-8bff-4926-936a-a1f8b065e7d5). +subRegionList + :: Text + -- ^ eBird API key + -> RegionType + -- ^ Type of subregions to fetch + -> RegionCode + -- ^ Parent 'RegionCode' + -> ClientM [RegionListEntry] +subRegionList = subRegionList_ + +{------------------------------------------------------------------------------ +-- * Adjacent regions +------------------------------------------------------------------------------} + +-- | Get a list of regions adjacent to a given region. 'Subnational2' region +-- codes are only currently supported in the United States, New Zealand, or +-- Mexico. +-- +-- For example, get regions adjacent to Wyoming (using @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ adjacentRegions key "US-WY" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'adjacentRegions' +-- is equivalent to the generated 'adjacentRegions_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3aca0519-3105-47fc-8611-a4dfd500a32f). +adjacentRegions + :: Text + -- ^ eBird API key + -> Region + -- ^ Region to fetch the adjacent regions of + -> ClientM [RegionListEntry] +adjacentRegions = adjacentRegions_ diff --git a/ebird-client/src/EBird/Client/Taxonomy.hs b/ebird-client/src/EBird/Client/Taxonomy.hs new file mode 100644 index 0000000..e6ff112 --- /dev/null +++ b/ebird-client/src/EBird/Client/Taxonomy.hs @@ -0,0 +1,295 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +{-# LANGUAGE UndecidableInstances #-} + +-- | +-- Module : EBird.Client.Taxonomy +-- Copyright : (c) 2023 Finley McIlwaine +-- License : MIT (see LICENSE) +-- +-- Maintainer : Finley McIlwaine +-- +-- Types and functions for taxonomy-related eBird API queries. + +module EBird.Client.Taxonomy where + +import Data.Default +import Data.Text +import Optics.TH +import Servant.Client + +import EBird.API +import EBird.Client.Generated + +{------------------------------------------------------------------------------ + Taxonomy +------------------------------------------------------------------------------} + +-- | Get any version of the eBird taxonomy, with optional filtering based on +-- taxonomy categories and species. +-- +-- For example, get the taxa for species in the "hybrid" category: +-- +-- @ +-- askEBird $ taxonomy (def & #categories ?~ "hybrid") +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#952a4310-536d-4ad1-8f3e-77cfb624d1bc). +taxonomy + :: TaxonomyParams + -- ^ Optional parameters + -- + -- /default: 'defaultTaxonomyParams'/ + -> ClientM [Taxon] +taxonomy TaxonomyParams{..} = + taxonomy_ + _taxonomyParamsCategories + -- Hard coded to JSONFormat because it makes no difference and CSVFormat + -- does not work like it should. See the note on the generated function's + -- parameter documentation. + (Just JSONFormat) + _taxonomyParamsLocale + _taxonomyParamsSpecies + _taxonomyParamsVersion + +-- | Optional parameters accepted by the 'TaxonomyAPI'. +-- +-- Note that 'defaultTaxonomyParams' (or the 'Default' instance's 'def' value) +-- may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_taxonomyParamsSpecies' field to "bohwax": +-- +-- > def & taxonomyParamsSpecies ?~ "bohwax" +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #species ?~ "bohwax" +data TaxonomyParams = + TaxonomyParams + { -- | Only include species of these 'TaxonomyCategory's in the taxonomy + -- + -- /default: all categories/ + _taxonomyParamsCategories :: Maybe TaxonomyCategories + + -- | Use this locale for common names + -- + -- /default: 'En'/ + , _taxonomyParamsLocale :: Maybe SPPLocale + + -- | Only fetch records for these species + -- + -- /default: all/ + , _taxonomyParamsSpecies :: Maybe SpeciesCodes + + -- | Fetch this version of the eBird taxonomy + -- + -- /default: latest/ + , _taxonomyParamsVersion :: Maybe Text + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultTaxonomyParams :: TaxonomyParams +defaultTaxonomyParams = + TaxonomyParams + { _taxonomyParamsCategories = Nothing + , _taxonomyParamsLocale = Nothing + , _taxonomyParamsSpecies = Nothing + , _taxonomyParamsVersion = Nothing + } + +instance Default TaxonomyParams where + def = defaultTaxonomyParams + +-- ** Optics for 'TaxonomyParams' +makeLenses ''TaxonomyParams +makeFieldLabels ''TaxonomyParams + +{------------------------------------------------------------------------------ + Taxonomic forms +------------------------------------------------------------------------------} + +-- | Get the list of subspecies of a given species recognized in the eBird +-- taxonomy. +-- +-- For example, get subspecies of Canada Goose (using +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ taxonomicForms key "cangoo" +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'taxonomicForms' +-- is equivalent to the generated 'taxonomicForms_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#e338e5a6-919d-4603-a7db-6c690fa62371). +taxonomicForms + :: Text + -- ^ eBird API key + -> SpeciesCode + -- ^ The species to get subspecies of + -> ClientM SpeciesCodes +taxonomicForms = taxonomicForms_ + +{------------------------------------------------------------------------------ + Taxa locale codes +------------------------------------------------------------------------------} + +-- | Get the supported locale codes and names for species common names, with the +-- last time they were updated. +-- +-- For example: +-- +-- @ +-- askEBird $ taxaLocaleCodes key def +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#3ea8ff71-c254-4811-9e80-b445a39302a6). +taxaLocaleCodes + :: Text + -- ^ eBird API key + -> TaxaLocaleCodesParams + -- ^ Optional parameters + -- + -- /default: 'defaultTaxaLocaleCodesParams'/ + -> ClientM [SPPLocaleListEntry] +taxaLocaleCodes k TaxaLocaleCodesParams{..} = + taxaLocaleCodes_ k _taxaLocaleCodesParamsLocale + +-- | Optional parameters accepted by the 'TaxaLocaleCodesAPI'. +-- +-- Note that 'defaultTaxaLocaleCodesParams' (or the 'Default' instance's 'def' +-- value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_taxaLocaleCodesParamsLocale' field to 'Es': +-- +-- > def & taxaLocaleCodesParamsLocale ?~ Es +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #locale ?~ Es +newtype TaxaLocaleCodesParams = + TaxaLocaleCodesParams + { -- | Value for the "Accept-Language" header, for translated language + -- names, when available + -- + -- /default: 'En'/ + _taxaLocaleCodesParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultTaxaLocaleCodesParams :: TaxaLocaleCodesParams +defaultTaxaLocaleCodesParams = + TaxaLocaleCodesParams + { _taxaLocaleCodesParamsLocale = Nothing + } + +instance Default TaxaLocaleCodesParams where + def = defaultTaxaLocaleCodesParams + +-- ** Optics for 'TaxaLocaleCodesParams' +makeLenses ''TaxaLocaleCodesParams +makeFieldLabels ''TaxaLocaleCodesParams + +{------------------------------------------------------------------------------ +-- * Taxonomy versions +------------------------------------------------------------------------------} + +-- | Get all versions of the taxonomy, with a flag indicating which is latest. +-- +-- For example: +-- +-- @ +-- askEBird taxonomyVersions +-- @ +-- +-- Note that the endpoint for this query is simple enough that 'taxonomyVersions' +-- is equivalent to the generated 'taxonomyVersions_'. +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#9bba1ff5-6eb2-4f9a-91fd-e5ed34e51500). +taxonomyVersions :: ClientM [TaxonomyVersionListEntry] +taxonomyVersions = taxonomyVersions_ + +{------------------------------------------------------------------------------ +-- * Taxonomic groups +------------------------------------------------------------------------------} + +-- | Get the list of species groups, in either Merlin or eBird grouping. +-- +-- For example, get the taxonomic groups using eBird grouping order (using +-- @-XOverloadedStrings@): +-- +-- @ +-- askEBird $ taxonomicGroups "ebird" def +-- @ +-- +-- See the [eBird API documentation for the corresponding +-- endpoint](https://documenter.getpostman.com/view/664302/S1ENwy59#aa9804aa-dbf9-4a53-bbf4-48e214e4677a). +taxonomicGroups + :: SPPGrouping + -- ^ 'MerlinGrouping' groups like birds together, with falcons next to hawks, + -- while 'EBirdGrouping' groups in taxonomy order + -> TaxonomicGroupsParams + -- ^ Optional parameters + -- + -- /default: 'defaultTaxonomicGroupsParams'/ + -> ClientM [TaxonomicGroupListEntry] +taxonomicGroups r TaxonomicGroupsParams{..} = + taxonomicGroups_ r _taxonomicGroupsParamsLocale + +-- | Optional parameters accepted by the 'TaxonomicGroupsAPI'. +-- +-- Note that 'defaultTaxonomicGroupsParams' (or the 'Default' instance's 'def' +-- value) may be used to accept the defaults of the eBird API. +-- +-- Additionally, note that there are optics available for manipulating this +-- type. For example, if you would like to just set the +-- '_taxonomicGroupsParamsLocale' field to 'Es': +-- +-- > def & taxonomicGroupsParamsLocale ?~ Es +-- +-- Or, using @-XOverloadedLabels@: +-- +-- > def & #locale ?~ Es +newtype TaxonomicGroupsParams = + TaxonomicGroupsParams + { -- | Locale to use for species group names. 'En' is used for any locale + -- whose translations are unavailable at this endpoint + -- + -- /default: 'En'/ + _taxonomicGroupsParamsLocale :: Maybe SPPLocale + } + deriving (Show, Read, Eq) + +-- | Note that this value does not actually use the eBird API default values. +-- It simply sets every option to 'Nothing', which means we just don't send any +-- of these parameters to the eBird API and they will use /their own/ defaults. +defaultTaxonomicGroupsParams :: TaxonomicGroupsParams +defaultTaxonomicGroupsParams = + TaxonomicGroupsParams + { _taxonomicGroupsParamsLocale = Nothing + } + +instance Default TaxonomicGroupsParams where + def = defaultTaxonomicGroupsParams + +-- ** Optics for 'TaxonomicGroupsParams' +makeLenses ''TaxonomicGroupsParams +makeFieldLabels ''TaxonomicGroupsParams diff --git a/hie.yaml b/hie.yaml new file mode 100644 index 0000000..8766cbc --- /dev/null +++ b/hie.yaml @@ -0,0 +1,13 @@ +cradle: + cabal: + - path: "ebird-api/src" + component: "lib:ebird-api" + + - path: "ebird-cli" + component: "ebird-cli:exe:ebird" + + - path: "ebird-cli/src" + component: "lib:ebird-cli" + + - path: "ebird-client/src" + component: "lib:ebird-client"