-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Split Main into separate modules; refactor to separate App Monad so I…
… can supply my own MonadReader; introduce dhall for configuration; various and sundry other tweaks and cleanup
- Loading branch information
1 parent
3f49f33
commit 396a9a9
Showing
8 changed files
with
202 additions
and
150 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ positionStackAccessKey = "fill me out" | ||
, emailAddress = "fill me out" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
|
||
module Forecast.Cache where | ||
|
||
import Control.Monad | ||
import qualified Data.Map.Strict as M | ||
import Forecast.Types | ||
import qualified Text.Read as R | ||
import System.Directory | ||
import System.IO | ||
|
||
readCache' :: CacheString -> Cache | ||
readCache' cacheString = maybe M.empty id (R.readMaybe cacheString :: Maybe Cache) | ||
|
||
readCache :: FilePath -> IO Cache | ||
readCache filePath = do | ||
handle <- openFile filePath ReadWriteMode | ||
cacheString <- hGetContents handle | ||
let cache = readCache' cacheString | ||
when (length cacheString > 0) $ hClose handle | ||
pure cache | ||
|
||
writeCache :: FilePath -> Cache -> IO () | ||
writeCache filePath cache = do | ||
writeFile (filePath ++ ".new") $ show cache | ||
renameFile (filePath ++ ".new") filePath |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
|
||
module Forecast.Display where | ||
|
||
import Control.Lens | ||
import Data.Aeson | ||
import Data.Aeson.Lens | ||
import qualified Data.Text as T | ||
import Text.Tabular | ||
import Text.Tabular.AsciiArt as AsciiArt | ||
|
||
tablize :: Value -> Table String String String | ||
tablize forecastJson = table | ||
where periods = forecastJson ^.. key "properties" . key "periods" . _Array . taking 4 traversed | ||
numericRow keyName = periods ^.. traversed . key keyName . _Number . to show | ||
stringRow keyName = periods ^.. traversed . key keyName . _String . to T.unpack | ||
rows = [ numericRow "temperature" | ||
, zipWith (\s d -> s <> " " <> d) (stringRow "windSpeed") (stringRow "windDirection") | ||
, stringRow "startTime" | ||
, stringRow "endTime" | ||
, stringRow "shortForecast" | ||
] | ||
table = Table | ||
(Group SingleLine | ||
[Group SingleLine | ||
[ Header "Temp." | ||
, Header "Wind" | ||
, Header "Start" | ||
, Header "End" | ||
, Header "Forecast" | ||
]]) | ||
(Group SingleLine | ||
[Group SingleLine $ fmap Header $ stringRow "name"]) | ||
rows | ||
|
||
renderForecast :: Value -> String | ||
renderForecast = AsciiArt.render id id id . tablize |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
{-# LANGUAGE FlexibleContexts #-} | ||
|
||
module Forecast.Request where | ||
|
||
import Control.Lens | ||
import Control.Monad.IO.Class | ||
import Control.Monad.Reader.Class | ||
import Data.Aeson | ||
import Data.Aeson.Lens | ||
import qualified Data.List as L | ||
import Data.Maybe | ||
import qualified Data.Text as T | ||
import Data.Text.Encoding | ||
import Forecast.Types | ||
import Network.HTTP.Req | ||
import Text.URI | ||
|
||
userAgent :: T.Text -> Option s | ||
userAgent emailAddress = header | ||
(encodeUtf8 "User-Agent") | ||
(encodeUtf8 ("(My CLI Weather App, " <> emailAddress <> ")" :: T.Text)) | ||
|
||
queryPositionStack :: (Monad m, MonadHttp m, MonadReader Config m) => T.Text -> m (JsonResponse Value) | ||
queryPositionStack address = do | ||
config <- ask | ||
-- positionstack's free plan only allows for http ¯\_(ツ)_/¯ | ||
let (uri, options) = fromJust $ mkURI "http://api.positionstack.com/v1/forward" >>= useHttpURI | ||
accessKeyQP = "access_key" =: (positionStackAccessKey config) | ||
queryQP = "query" =: address | ||
options' = options <> accessKeyQP <> queryQP | ||
req GET uri NoReqBody jsonResponse options' | ||
|
||
getWeatherGov :: (Monad m, MonadHttp m, MonadReader Config m) => T.Text -> m (JsonResponse Value) | ||
getWeatherGov url = do | ||
config <- ask | ||
let (uri, options) = fromJust $ mkURI url >>= useHttpsURI | ||
options' = options <> (userAgent $ emailAddress config) | ||
req GET uri NoReqBody jsonResponse options' | ||
|
||
getCoordinates :: Value -> T.Text | ||
getCoordinates json = T.pack $ L.intercalate "," coordPair | ||
where coordPair = json ^.. key "data" | ||
. _Array | ||
. taking 1 traversed | ||
. (key "latitude" <> key "longitude") | ||
. _Number | ||
. to show | ||
|
||
getForecastUrl :: Value -> Maybe T.Text | ||
getForecastUrl = preview $ key "properties" . key "forecast" . _String | ||
|
||
getLatestForecast :: (Monad m, MonadIO m, MonadHttp m, MonadReader Config m) => Address -> m Value | ||
getLatestForecast address = do | ||
-- first we get lat/long coordinates we can use with weather.gov via | ||
-- positionstack.com | ||
coordsResp <- queryPositionStack address | ||
let coords = getCoordinates $ responseBody coordsResp | ||
liftIO $ putStrLn $ ("retrieved coordinate pair " ++ show coords ++ " for address " ++ T.unpack address) | ||
|
||
-- then we get the forecastUrl via the weather.gov/points endpoint | ||
pointsResp <- getWeatherGov (T.append "https://api.weather.gov/points/" coords) | ||
let forecastUrl = getForecastUrl $ responseBody pointsResp | ||
liftIO $ putStrLn $ ("retrieved forecastUrl: " ++ show forecastUrl) | ||
|
||
-- finally we can get the forecast for our location | ||
forecastResp <- getWeatherGov $ fromJust forecastUrl | ||
pure (responseBody forecastResp) |
Oops, something went wrong.