ebird-api (empty) → 0.1.0.0
raw patch · 12 files changed
+3846/−0 lines, 12 filesdep +aesondep +attoparsecdep +attoparsec-iso8601
Dependencies added: aeson, attoparsec, attoparsec-iso8601, base, optics, servant, text, time
Files
- CHANGELOG.md +5/−0
- LICENSE +20/−0
- ebird-api.cabal +74/−0
- src/Data/EBird/API.hs +593/−0
- src/Data/EBird/API/Checklists.hs +519/−0
- src/Data/EBird/API/EBirdString.hs +39/−0
- src/Data/EBird/API/Hotspots.hs +174/−0
- src/Data/EBird/API/Observations.hs +697/−0
- src/Data/EBird/API/Product.hs +192/−0
- src/Data/EBird/API/Regions.hs +431/−0
- src/Data/EBird/API/Taxonomy.hs +911/−0
- src/Data/EBird/API/Util/Time.hs +191/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for ebird-api++## 0.1.0.0 -- 2023-07-30++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -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.
+ ebird-api.cabal view
@@ -0,0 +1,74 @@+cabal-version: 3.0+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:+ Data.EBird.API+ , Data.EBird.API.Checklists+ , Data.EBird.API.EBirdString+ , Data.EBird.API.Hotspots+ , Data.EBird.API.Observations+ , Data.EBird.API.Product+ , Data.EBird.API.Regions+ , Data.EBird.API.Taxonomy+ , Data.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+ , optics >= 0.4 && < 0.5+ , servant >= 0.18.3 && < 0.21+ , text >= 1.2.4.1 && < 2.1+ , time >= 1.9.3 && < 1.13+ hs-source-dirs:+ src
+ src/Data/EBird/API.hs view
@@ -0,0 +1,593 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeOperators #-}++-- |+-- Module : Data.EBird.API+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- 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 Data.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 Data.EBird.API.Checklists++ -- * eBird observations+ , module Data.EBird.API.Observations++ -- * eBird products+ , module Data.EBird.API.Product++ -- * eBird hotspots+ , module Data.EBird.API.Hotspots++ -- * eBird regions+ , module Data.EBird.API.Regions++ -- * eBird taxonomy+ , module Data.EBird.API.Taxonomy++ -- * Date and time utilities+ , module Data.EBird.API.Util.Time++ -- * 'EBirdString' class+ , module Data.EBird.API.EBirdString+ ) where++import Data.Text (Text)+import Servant.API++import Data.EBird.API.Checklists+import Data.EBird.API.EBirdString+import Data.EBird.API.Observations+import Data.EBird.API.Product+import Data.EBird.API.Hotspots+import Data.EBird.API.Regions+import Data.EBird.API.Taxonomy+import Data.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]
+ src/Data/EBird/API/Checklists.hs view
@@ -0,0 +1,519 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Data.EBird.API.Checklists+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types related to eBird checklist API values.++module Data.EBird.API.Checklists 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 Optics+import Servant.API (ToHttpApiData(..))++import Data.EBird.API.EBirdString+import Data.EBird.API.Regions+import Data.EBird.API.Taxonomy+import Data.EBird.API.Util.Time++-------------------------------------------------------------------------------+-- * Checklist types+-------------------------------------------------------------------------------++-- | Values returned by the 'Data.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)++-------------------------------------------------------------------------------+-- * Auxiliary eBird checklist-related API types+-------------------------------------------------------------------------------++-- | How to rank the list returned by the 'Data.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)++-------------------------------------------------------------------------------+-- * Optics for checklist types+-------------------------------------------------------------------------------++makeLenses ''Checklist+makeFieldLabels ''Checklist+makeLenses ''ChecklistObservation+makeFieldLabels ''ChecklistObservation+makeLenses ''SubAux+makeFieldLabels ''SubAux+makeLenses ''SubAuxAI+makeFieldLabels ''SubAuxAI+makeLenses ''ChecklistFeedEntry+makeFieldLabels ''ChecklistFeedEntry+makeLenses ''LocationData+makeFieldLabels ''LocationData++-------------------------------------------------------------------------------+-- 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+ | Just duration <- [_checklistDurationHours]+ ]+ <> [ "effortDistanceKm" .= distance+ | Just distance <- [_checklistEffortDistanceKm]+ ]+ <> [ "effortDistanceEnteredUnit" .= unit+ | Just unit <- [_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
+ src/Data/EBird/API/EBirdString.hs view
@@ -0,0 +1,39 @@+-- |+-- Module : Data.EBird.API.EBirdString+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- The 'EBirdString' class contains types whose values may be represented as+-- strings compatible with the eBird API.++module Data.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
+ src/Data/EBird/API/Hotspots.hs view
@@ -0,0 +1,174 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Data.EBird.API.Hotspots+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types and functions related to eBird hotspot API values.++module Data.EBird.API.Hotspots 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 Optics+import Servant.API++import Data.EBird.API.EBirdString+import Data.EBird.API.Regions+import Data.EBird.API.Util.Time++-------------------------------------------------------------------------------+-- * Hotspot type+-------------------------------------------------------------------------------++-- | eBird hotspots, as returned by the 'Data.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)++-- ** Optics for the Hotspot type++makeLenses ''Hotspot+makeFieldLabels ''Hotspot++-------------------------------------------------------------------------------+-- * Auxiliary eBird hotspot-related API types+-------------------------------------------------------------------------------++-- | 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
+ src/Data/EBird/API/Observations.hs view
@@ -0,0 +1,697 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+++-- |+-- Module : Data.EBird.API.Observations+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types and functions related to eBird observation API values.++module Data.EBird.API.Observations 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 Optics+import Servant.API (ToHttpApiData(..))++import Data.EBird.API.EBirdString+import Data.EBird.API.Regions+import Data.EBird.API.Util.Time++-------------------------------------------------------------------------------+-- * Observation types+-------------------------------------------------------------------------------++-- | 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"+ _observationSpeciesCode :: Text++ -- | Common name, e.g. "Bohemian Waxwing"+ , _observationCommonName :: Text++ -- | Scientific name, e.g. "Bombycilla garrulus"+ , _observationScientificName :: Text++ -- | Location ID, e.g. \"L7884500\"+ , _observationLocationId :: Text++ -- | Location name, e.g. "Frog Pond"+ , _observationLocationName :: Text++ -- | Date and time of observation+ , _observationDateTime :: EBirdDateTime++ -- | How many were seen? Sometimes omitted.+ , _observationHowMany :: Maybe Integer++ -- | Observation latitude+ , _observationLatitude :: Double++ -- | Observation longitude+ , _observationLongitude :: Double++ -- | Is this observation valid?+ , _observationValid :: Bool++ -- | Has this observation been reviewed?+ , _observationReviewed :: Bool++ -- | Is the location of this observation private?+ , _observationLocationPrivate :: Bool++ -- | Submission ID+ , _observationSubId :: Text++ , _observationFullDetail :: 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. 'Data.EBird.API.RecentNotableObservationsAPI').+data ObservationDetails (detail :: DetailLevel) where+ NoDetails :: ObservationDetails 'Simple+ FullDetails ::+ { -- | The subnational2 region that this observation took place in+ _observationDetailsSubnational2Code :: Region++ -- | The name of the subnational2 region that this observation took+ -- place in+ , _observationDetailsSubnational2Name :: Text++ -- | The subnational1 region that this observation took place in+ , _observationDetailsSubnational1Code :: Region++ -- | The name of the subnational1 region that this observation took+ -- place in+ , _observationDetailsSubnational1Name :: Text++ -- | The country region that this observation took place in+ , _observationDetailsCountryCode :: Region++ -- | The name of the country region that this observation took place in+ , _observationDetailsCountryName :: Text++ -- | The display name of the user that submitted this observation+ , _observationDetailsUserDisplayName :: Text++ -- | The unique ID of this observation+ , _observationDetailsObsId :: Text++ -- | The ID of the checklist that this observation was submitted with,+ -- e.g. \"CL24936\"+ , _observationDetailsChecklistId :: Text++ -- | Whether the count for the observation was provided as just \"X\"+ , _observationDetailsPresenceNoted :: Bool++ -- | Whether this observation was submitted with comments+ , _observationDetailsHasComments :: Bool++ -- | The last name of the user that submitted this observation+ , _observationDetailsLastName :: Text++ -- | The first name of the user that submitted this observation+ , _observationDetailsFirstName :: Text++ -- | Whether this observation has media such as photos, videos, or+ -- audio attached+ , _observationDetailsHasRichMedia :: 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 _observationFullDetail o of+ NoDetails -> show o+ FullDetails{} -> show o++-------------------------------------------------------------------------------+-- * Auxiliary eBird observation API types+-------------------------------------------------------------------------------++-- | 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+-- 'Data.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)++-------------------------------------------------------------------------------+-- * Optics for observation types+-------------------------------------------------------------------------------++makeLenses ''Observation+makeFieldLabels ''Observation++observationDetailsSubnational2Code :: Lens' (ObservationDetails 'Full) Region+observationDetailsSubnational2Code =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsSubnational2Code = c })+ <$> f _observationDetailsSubnational2Code++observationDetailsSubnational2Name :: Lens' (ObservationDetails 'Full) Text+observationDetailsSubnational2Name =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsSubnational2Name = c })+ <$> f _observationDetailsSubnational2Name++observationDetailsSubnational1Code :: Lens' (ObservationDetails 'Full) Region+observationDetailsSubnational1Code =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsSubnational1Code = c })+ <$> f _observationDetailsSubnational1Code++observationDetailsSubnational1Name :: Lens' (ObservationDetails 'Full) Text+observationDetailsSubnational1Name =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsSubnational1Name = c })+ <$> f _observationDetailsSubnational1Name++observationDetailsCountryCode :: Lens' (ObservationDetails 'Full) Region+observationDetailsCountryCode =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsCountryCode = c })+ <$> f _observationDetailsCountryCode++observationDetailsCountryName :: Lens' (ObservationDetails 'Full) Text+observationDetailsCountryName =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsCountryName = c })+ <$> f _observationDetailsCountryName++observationDetailsUserDisplayName :: Lens' (ObservationDetails 'Full) Text+observationDetailsUserDisplayName =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsUserDisplayName = c })+ <$> f _observationDetailsUserDisplayName++observationDetailsObsId :: Lens' (ObservationDetails 'Full) Text+observationDetailsObsId =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsObsId = c })+ <$> f _observationDetailsObsId++observationDetailsChecklistId :: Lens' (ObservationDetails 'Full) Text+observationDetailsChecklistId =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsChecklistId = c })+ <$> f _observationDetailsChecklistId++observationDetailsPresenceNoted :: Lens' (ObservationDetails 'Full) Bool+observationDetailsPresenceNoted =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsPresenceNoted = c })+ <$> f _observationDetailsPresenceNoted++observationDetailsHasComments :: Lens' (ObservationDetails 'Full) Bool+observationDetailsHasComments =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsHasComments = c })+ <$> f _observationDetailsHasComments++observationDetailsLastName :: Lens' (ObservationDetails 'Full) Text+observationDetailsLastName =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsLastName = c })+ <$> f _observationDetailsLastName++observationDetailsFirstName :: Lens' (ObservationDetails 'Full) Text+observationDetailsFirstName =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsFirstName = c })+ <$> f _observationDetailsFirstName++observationDetailsHasRichMedia :: Lens' (ObservationDetails 'Full) Bool+observationDetailsHasRichMedia =+ lensVL $ \f d@FullDetails{..} ->+ (\c -> d { _observationDetailsHasRichMedia = c })+ <$> f _observationDetailsHasRichMedia++instance+ k ~ A_Lens+ => LabelOptic+ "subnational2Code" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Region+ Region+ where+ labelOptic = observationDetailsSubnational2Code++instance+ k ~ A_Lens+ => LabelOptic+ "subnational2Name" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsSubnational2Name++instance+ k ~ A_Lens+ => LabelOptic+ "subnational1Code" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Region+ Region+ where+ labelOptic = observationDetailsSubnational1Code++instance+ k ~ A_Lens+ => LabelOptic+ "subnational1Name" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsSubnational1Name++instance+ k ~ A_Lens+ => LabelOptic+ "countryCode" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Region+ Region+ where+ labelOptic = observationDetailsCountryCode++instance+ k ~ A_Lens+ => LabelOptic+ "countryName" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsCountryName++instance+ k ~ A_Lens+ => LabelOptic+ "userDisplayName" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsUserDisplayName++instance+ k ~ A_Lens+ => LabelOptic+ "obsId" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsObsId++instance+ k ~ A_Lens+ => LabelOptic+ "checklistId" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsChecklistId++instance+ k ~ A_Lens+ => LabelOptic+ "presenceNoted" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Bool+ Bool+ where+ labelOptic = observationDetailsPresenceNoted++instance+ k ~ A_Lens+ => LabelOptic+ "hasComments" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Bool+ Bool+ where+ labelOptic = observationDetailsHasComments++instance+ k ~ A_Lens+ => LabelOptic+ "lastName" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsLastName++instance+ k ~ A_Lens+ => LabelOptic+ "firstName" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Text+ Text+ where+ labelOptic = observationDetailsFirstName++instance+ k ~ A_Lens+ => LabelOptic+ "hasRichMedia" k+ (ObservationDetails 'Full)+ (ObservationDetails 'Full)+ Bool+ Bool+ where+ labelOptic = observationDetailsHasRichMedia++-------------------------------------------------------------------------------+-- 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" .= _observationSpeciesCode+ , "comName" .= _observationCommonName+ , "sciName" .= _observationScientificName+ , "locId" .= _observationLocationId+ , "locName" .= _observationLocationName+ , "obsDt" .= _observationDateTime+ , "lat" .= _observationLatitude+ , "lng" .= _observationLongitude+ , "obsValid" .= _observationValid+ , "obsReviewed" .= _observationReviewed+ , "locationPrivate" .= _observationLocationPrivate+ , "subId" .= _observationSubId+ ]+ -- Fields that may or may not be included, depending on the observation+ -- data+ <> ["howMany" .= howMany | Just howMany <- [_observationHowMany]]++-- | 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" .= _observationSpeciesCode+ , "comName" .= _observationCommonName+ , "sciName" .= _observationScientificName+ , "locId" .= _observationLocationId+ , "locName" .= _observationLocationName+ , "obsDt" .= _observationDateTime+ , "howMany" .= _observationHowMany+ , "lat" .= _observationLatitude+ , "lng" .= _observationLongitude+ , "obsValid" .= _observationValid+ , "obsReviewed" .= _observationReviewed+ , "locationPrivate" .= _observationLocationPrivate+ , "subId" .= _observationSubId+ , "subnational2Code" .=+ _observationDetailsSubnational2Code _observationFullDetail+ , "subnational2Name" .=+ _observationDetailsSubnational2Name _observationFullDetail+ , "subnational1Code" .=+ _observationDetailsSubnational1Code _observationFullDetail+ , "subnational1Name" .=+ _observationDetailsSubnational1Name _observationFullDetail+ , "countryCode" .=+ _observationDetailsCountryCode _observationFullDetail+ , "countryName" .=+ _observationDetailsCountryName _observationFullDetail+ , "userDisplayName" .=+ _observationDetailsUserDisplayName _observationFullDetail+ , "obsId" .=+ _observationDetailsObsId _observationFullDetail+ , "checklistId" .=+ _observationDetailsChecklistId _observationFullDetail+ , "presenceNoted" .=+ _observationDetailsPresenceNoted _observationFullDetail+ , "hasComments" .=+ _observationDetailsHasComments _observationFullDetail+ , "lastName" .=+ _observationDetailsLastName _observationFullDetail+ , "firstName" .=+ _observationDetailsFirstName _observationFullDetail+ , "hasRichMedia" .=+ _observationDetailsHasRichMedia _observationFullDetail+ ]++-- | 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+-- 'observationFullDetail' field.+instance ToJSON SomeObservation where+ toJSON (SomeObservation obs) =+ case _observationFullDetail 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
+ src/Data/EBird/API/Product.hs view
@@ -0,0 +1,192 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Data.EBird.API.Product+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types related to eBird product API values.++module Data.EBird.API.Product 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 Optics+import Servant.API (ToHttpApiData(..))++import Data.EBird.API.EBirdString++-------------------------------------------------------------------------------+-- * Top 100 contributors API types+-------------------------------------------------------------------------------++-- | 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)++-- ** Optics for the Top100ListEntry type++makeLenses ''Top100ListEntry+makeFieldLabels ''Top100ListEntry++-- | How to rank the list returned by the 'Data.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)++-------------------------------------------------------------------------------+-- * Regional statistics API types+-------------------------------------------------------------------------------++-- | Values returned by the 'Data.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)++-- ** Optics for the RegionalStatistics type++makeLenses ''RegionalStatistics+makeFieldLabels ''RegionalStatistics++-------------------------------------------------------------------------------+-- 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
+ src/Data/EBird/API/Regions.hs view
@@ -0,0 +1,431 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++-- |+-- Module : Data.EBird.API.Regions+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types related to eBird region API values.++module Data.EBird.API.Regions 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 Optics+import Servant.API (ToHttpApiData(..))++import Data.EBird.API.EBirdString++-------------------------------------------------------------------------------+-- * Region-related API types+-------------------------------------------------------------------------------++-- | 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 'Data.EBird.API.SubRegionListAPI' and+-- 'Data.EBird.API.AdjacentRegionsAPI'.+data RegionListEntry =+ RegionListEntry+ { _regionListEntryRegion :: Region+ , _regionListEntryName :: Text+ }+ deriving (Show, Read, Eq)++-- ** Optics for region-related API types++makeLenses ''RegionInfo+makeFieldLabels ''RegionInfo+makeLenses ''RegionBounds+makeFieldLabels ''RegionBounds+makeLenses ''RegionListEntry+makeFieldLabels ''RegionListEntry++-------------------------------------------------------------------------------+-- 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
+ src/Data/EBird/API/Taxonomy.hs view
@@ -0,0 +1,911 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}+{-# HLINT ignore "Use camelCase" #-}++-- |+-- Module : Data.EBird.API.Internal.Types.Taxonomy+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Types related to eBird taxonomy-related API values.++module Data.EBird.API.Taxonomy 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 Optics+import GHC.Exts+import Servant.API (ToHttpApiData(..))++import Data.EBird.API.EBirdString++-------------------------------------------------------------------------------+-- * Taxonomy types+-------------------------------------------------------------------------------++-- | 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)++-- ** Optics for taxonomy types++makeLenses ''Taxon+makeFieldLabels ''Taxon++-------------------------------------------------------------------------------+-- * Auxiliary eBird taxonomy-related API types+-------------------------------------------------------------------------------++-- | 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 'Data.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 'Data.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)++-- | Values returned by the 'Data.EBird.API.TaxonomyVersionsAPI'.+data TaxonomyVersionListEntry =+ TaxonomyVersionListEntry+ { _taxonomyVersionAuthorityVersion :: Double+ , _taxonomyVersionLatest :: Bool+ }+ deriving (Show, Read, Eq)++-- ** Optics for taxonomy-related types++makeLenses ''SPPLocaleListEntry+makeFieldLabels ''SPPLocaleListEntry+makeLenses ''TaxonomicGroupListEntry+makeFieldLabels ''TaxonomicGroupListEntry+makeLenses ''TaxonomyVersionListEntry+makeFieldLabels ''TaxonomyVersionListEntry++-------------------------------------------------------------------------------+-- 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
+ src/Data/EBird/API/Util/Time.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE GeneralisedNewtypeDeriving #-}+{-# LANGUAGE DerivingStrategies #-}++-- |+-- Module : Data.EBird.API.Util.Time+-- Copyright : (c) 2023 Finley McIlwaine+-- License : MIT (see LICENSE)+--+-- Maintainer : Finley McIlwaine <finleymcilwaine@gmail.com>+--+-- Utilities for parsing and printing dates and times that the eBird API+-- provides.++module Data.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 Data.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,+-- 'Data.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+-- 'Data.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