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 +30/−0
- README.md +10/−0
- Setup.hs +2/−0
- src/Ziptastic/Core.hs +138/−0
- test/Spec.hs +90/−0
- ziptastic-core.cabal +65/−0
+ 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++[](http://www.haskell.org)+[](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