packages feed

ziptastic-core (empty) → 0.1.0.0

raw patch · 6 files changed

+335/−0 lines, 6 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, here, hspec, http-api-data, iso3166-country-codes, servant, text, tz, ziptastic-core

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017 Elliot Cameron and Ziptastic and Grafted-In LLC++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Elliot Cameron nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,10 @@+# ziptastic-core++[![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)](http://www.haskell.org)+[![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)](https://tldrlegal.com/license/bsd-3-clause-license-%28revised%29)++Core [Servant](http://haskell-servant.readthedocs.io/en/stable/) specification for the [Ziptastic](https://www.getziptastic.com/) API providing forward and reverse geocoding via country, zip code (postal code), latitude, and longitude.++This package is maintained by [Grafted-In](https://www.graftedin.io/).++If you want to use the Ziptastic API in your application, try the [ziptastic-client](http://hackage.haskell.org/package/ziptastic-client) package.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Ziptastic/Core.hs view
@@ -0,0 +1,138 @@+{-# LANGUAGE+    DataKinds+  , DeriveGeneric+  , FlexibleInstances+  , GeneralizedNewtypeDeriving+  , OverloadedStrings+  , TypeOperators+#-}++-- | This module provides a complete and type-safe API specification for+-- Ziptastic's forward and reverse geocoding API using Servant+-- (<https://www.getziptastic.com/>).+--+-- To use this specification in your application, try the @ziptastic-client@ package.+module Ziptastic.Core+  ( Api+  , ApiKey(..)+  , ForApi(..)+  , LocaleCoords(..)+  , LocaleInfo(..)+  , baseUrlHost+  , baseUrlIsHttps+  , baseUrlPath+  , baseUrlPort+  ) where++import           Control.Monad (when)+import           Data.ISO3166_CountryCodes (CountryCode)+import qualified Data.Aeson as Json+import qualified Data.Aeson.Types as Json+import           Data.Aeson ((.:), (.:?))+import           Data.String (IsString)+import           Data.Text (Text)+import qualified Data.Text as T+import           Data.Text.Encoding (encodeUtf8)+import           Data.Time.Zones.All (TZLabel, fromTZName)+import           GHC.Generics (Generic)+import           Servant.API (Capture, Get, Header, JSON, (:>), (:<|>))+import           Text.Read (readMaybe)+import           Web.HttpApiData (ToHttpApiData, toUrlPiece)+++data LocaleCoords = LocaleCoords+  { coordsLatitude  :: Double+  , coordsLongitude :: Double+  , coordsGeohash   :: Text    -- ^ Will never be empty.+  } deriving (Eq, Generic, Show)++data LocaleInfo = LocaleInfo+  { localeCity         :: Maybe Text  -- ^ If given, text will never be empty.+  , localeCoords       :: Maybe LocaleCoords+  , localeCountry      :: CountryCode+  , localeCounty       :: Maybe Text -- ^ If given, text will never be empty.+  , localeRegionFull   :: Maybe Text -- ^ If given, text will never be empty.+  , localeRegionAbbrev :: Maybe Text -- ^ If given, text will never be empty.+  , localePostalCode   :: Text       -- ^ Will never be empty.+  , localeTimeZone     :: Maybe TZLabel+  } deriving (Eq, Generic, Show)++instance Json.FromJSON LocaleInfo where+  parseJSON (Json.Object v) = do+    maybeLat     <- v .:? "latitude"+    maybeLong    <- v .:? "longitude"+    maybeGeohash <- v `optionalStr` "geohash"++    countryCodeStr <- v .: "country"+    maybeTzText <- v `optionalStr` "timezone"++    postalCode <- v .: "postal_code"+    when (T.null postalCode) $ fail "Invalid postal code"++    let+      coords = case (maybeLat, maybeLong, maybeGeohash) of+        (Just lat, Just long, Just geohash) -> Just (LocaleCoords lat long geohash)+        _ -> Nothing++      tzParser :: Text -> Json.Parser TZLabel+      tzParser tz = maybe (fail $ "Unrecognized time zone: " ++ T.unpack tz) pure (fromTZName $ encodeUtf8 tz)++    LocaleInfo+      <$> v `optionalStr` "city"+      <*> pure coords+      <*> maybe (fail $ "Unrecognized country code: " ++ countryCodeStr) pure (readMaybe countryCodeStr)+      <*> v `optionalStr` "county"+      <*> v `optionalStr` "state"+      <*> v `optionalStr` "state_short"+      <*> pure postalCode+      <*> maybe (pure Nothing) (fmap Just . tzParser) maybeTzText++    where+      optionalStr v' key = do+        val <- v' .:? key+        pure $ case val of+          Just x -> if T.null x then Nothing else Just x+          Nothing -> Nothing++  parseJSON x = Json.typeMismatch "LocaleInfo" x++newtype ApiKey = ApiKey { getApiKey :: Text } deriving (Eq, Generic, IsString, Show, ToHttpApiData)++-- | A generic wrapper for giving external data types instances for our uses.+newtype ForApi a = ForApi a++instance ToHttpApiData (ForApi CountryCode) where+  toUrlPiece (ForApi countryCode) = T.pack (show countryCode)++type Api = "v3" :> Header "x-key" ApiKey :> ApiEndpoints+type ApiEndpoints = ForwardGeocodingApi :<|> ReverseGeocodingApi++type LocaleInfoResponse = Get '[JSON] [LocaleInfo]++type ForwardGeocodingApi+  =  Capture "country-code" (ForApi CountryCode)+  :> Capture "postal-code" Text+  :> LocaleInfoResponse++type ReverseGeocodingApi+  =  "reverse"+  :> Capture "latitude"  Double+  :> Capture "longitude" Double+  :>+    (    Capture "radius-in-meters" Int :> LocaleInfoResponse+    :<|> LocaleInfoResponse  -- use default radius+    )+++-- API URL components+baseUrlIsHttps :: Bool+baseUrlIsHttps = True++baseUrlHost :: String+baseUrlHost = "zip.getziptastic.com"++baseUrlPort :: Int+baseUrlPort = 443++baseUrlPath :: String+baseUrlPath = ""
+ test/Spec.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE+    OverloadedStrings+  , QuasiQuotes+#-}++module Main (main) where++import           Control.Monad+import           Data.Aeson (decodeStrict')+import qualified Data.ISO3166_CountryCodes as CC+import           Data.Monoid+import           Data.Text (Text)+import           Data.Text.Encoding (encodeUtf8)+import           Test.Hspec+import           Data.String.Here (here, iTrim)+import           Data.Time.Zones.All as TZ++import           Ziptastic.Core+++main :: IO ()+main = hspec spec++spec :: Spec+spec = describe "JSON decoding LocaleInfo" $ do+  let parseTemplate :: Maybe (Text, Text) -> Maybe LocaleInfo+      parseTemplate = decodeStrict' . encodeUtf8 . exampleJsonTemplate++  it "accepts the standard example" $+     parseTemplate Nothing `shouldBe` Just exampleLocaleInfo++  it "nullifies coordinates if any related fields are null" $+    forM_ [ ("geohash", quoted ""), ("geohash", "null")+          , ("latitude", "null"), ("longitude", "null") ] $ \(field, val) ->+      parseTemplate (Just (field, val)) `shouldBe` Just exampleLocaleInfo{localeCoords = Nothing}++  it "accepts empty time zone" $+    forM_ [quoted "", "null"] $ \val ->+      parseTemplate (Just ("timezone", val))+        `shouldBe` Just exampleLocaleInfo{localeTimeZone = Nothing}++  it "does not accept invalid time zone" $+    parseTemplate (Just ("timezone", "blah")) `shouldBe` Nothing++  it "does not accept invalid country code" $+    parseTemplate (Just ("country", "zz")) `shouldBe` Nothing++  it "does not accept empty postal code" $+    parseTemplate (Just ("postal_code", quoted "")) `shouldBe` Nothing+++exampleJsonTemplate :: Maybe (Text, Text) -> Text+exampleJsonTemplate fieldOverride = [iTrim|+    {+      ${f "city" $ quoted "Owosso"},+      ${f "geohash" $ quoted "dpshsfsytw8k"},+      ${f "country" $ quoted "US"},+      ${f "county" $ quoted "Shiawassee"},+      ${f "state" $ quoted "Michigan"},+      ${f "state_short" $ quoted "MI"},+      ${f "postal_code" $ quoted "48867"},+      ${f "latitude" "42.9934"},+      ${f "longitude" "-84.1595"},+      ${f "timezone" $ quoted "America/Detroit"}+    }+  |] :: Text+  where+    f :: Text -> Text -> Text+    f field defaultVal = quoted field <> ": " <> case fieldOverride of+      Nothing -> defaultVal+      Just (key, val) -> if field == key then val else defaultVal++quoted :: Text -> Text+quoted x = "\"" <> x <> "\""++exampleLocaleInfo :: LocaleInfo+exampleLocaleInfo = LocaleInfo+  { localeCity         = Just "Owosso"+  , localeCountry      = CC.US+  , localeCounty       = Just "Shiawassee"+  , localeRegionFull   = Just "Michigan"+  , localeRegionAbbrev = Just "MI"+  , localePostalCode   = "48867"+  , localeCoords       = Just LocaleCoords+      { coordsLatitude  = 42.9934+      , coordsLongitude = (-84.1595)+      , coordsGeohash   = "dpshsfsytw8k"+      }+  , localeTimeZone   = Just TZ.America__Detroit+  }
+ ziptastic-core.cabal view
@@ -0,0 +1,65 @@+name:                ziptastic-core+version:             0.1.0.0+synopsis:+  Core Servant specification for the Ziptastic API (https://www.getziptastic.com) for doing forward and reverse geocoding.+description:+  This package provides a type-safe Servant specification for the Ziptastic+  (https://www.getziptastic.com) API for doing forward and reverse geocoding+  via zip/postal code, latitude, and longitude.+  .+  If you want to use the Ziptastic API in your application, try the ziptastic-client package.+  .+  This package is maintained by Grafted-In (https://www.graftedin.io/).+homepage:            https://github.com/Ziptastic/ziptastic-haskell#readme+license:             BSD3+license-file:        LICENSE+author:              Elliot Cameron+maintainer:          elliot@graftedin.io+copyright:           2017 Elliot Cameron and Ziptastic and Grafted-In LLC+category:            Web+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10+tested-with:         GHC==8.0.2++library+  hs-source-dirs:  src+  exposed-modules: Ziptastic.Core+  build-depends:+      aeson+    , base >= 4.7 && < 5+    , bytestring+    , http-api-data+    , iso3166-country-codes+    , servant+    , text+    , tz+  default-language: Haskell2010+  other-extensions:+    DataKinds+    DeriveGeneric+    FlexibleInstances+    GeneralizedNewtypeDeriving+    OverloadedStrings+    TypeOperators+  ghc-options: -Wall++test-suite test-core+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:+      aeson+    , base+    , here+    , hspec+    , iso3166-country-codes+    , text+    , tz+    , ziptastic-core+  ghc-options:      -threaded -rtsopts -with-rtsopts=-N+  default-language: Haskell2010++source-repository head+  type:     git+  location: https://github.com/Ziptastic/ziptastic-haskell