packages feed

porcupine-http-0.1.0.0: examples/example-Poke/ExamplePokeAPI.hs

{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE OverloadedLabels      #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE ScopedTypeVariables   #-}
{-# LANGUAGE TypeApplications      #-}
{-# LANGUAGE TupleSections         #-}
{-# LANGUAGE Arrows #-}

-- Don't forget to map locations to http urls in the 'exampleHTTP.yaml'
-- generated by calling 'exampleHTTP write-config-template'.

import           Control.Monad
import           Data.Aeson
import           Data.DocRecord
import qualified Data.Text                     as T
import           GHC.Generics
import           Porcupine
import           Prelude                       hiding (id, (.))
import           Graphics.Vega.VegaLite        as VL

import           Data.Locations.Accessors.HTTP


-- | The type of our raw data, read from the REST API
data Pokemon = Pokemon { pkName  :: !T.Text
                       , pkMoves :: ![T.Text]
                       , pkTypes :: ![T.Text] }
  deriving (Generic, Store)  -- Store makes them cacheable

instance FromJSON Pokemon where
  parseJSON = withObject "Pokemon" $ \o -> Pokemon
    <$> o .: "name"
    <*> (o .: "moves" >>= mapM ((.: "move") >=> (.: "name")))
    <*> (o .: "types" >>= mapM ((.: "type") >=> (.: "name")))

-- | How to load pokemons
--
-- See https://pokeapi.co/api/v2/pokemon/25 for instance
pokemonFile :: DataSource Pokemon
pokemonFile = usesCacherWithIdent 12345 $  -- This tells we want to support
                                           -- caching of the fetched data
              clockVFileAccesses $  -- This tells we want to add info about time
                                    -- taken to read data
              dataSource ["Inputs", "Pokemon"]
                         (somePureDeserial JSONSerial)

-- | One over-simple intermediary result type
newtype Analysis = Analysis { moveCount :: Int }
  deriving (Generic, ToJSON)

-- | How to write analysis
analysisFile :: DataSink Analysis
analysisFile = dataSink ["Outputs", "Analysis"]
                        (somePureSerial JSONSerial)

-- | Where to write the final summary visualization
vlSummarySink :: DataSink VegaLite
vlSummarySink = dataSink ["Outputs", "Summary"]
              (lmap VL.toHtml (somePureSerial $ PlainTextSerial $ Just "html")
               <>
               lmap VL.fromVL (somePureSerial JSONSerial))

-- | Create the vega-lite specification of the visualization we want
writeSummary :: (LogThrow m) => PTask m [Pokemon] ()
writeSummary = proc pkmn -> do
  let dat = dataFromColumns []
          . dataColumn "name" (Strings $ map pkName pkmn)
          . dataColumn "numMoves" (Numbers $ map (fromIntegral . length . pkMoves) pkmn)

      enc = encoding
          . position X [ PName "name", PmType Nominal ]
          . position Y [ PName "numMoves", PmType Quantitative ] -- , PAggregate Mean ]

      spec = toVegaLite [ dat [], mark Bar [], enc [] ]
  writeData vlSummarySink -< spec

-- | The task combining the three previous operations.
--
-- This task may look very opaque from the outside, having no parameters and no
-- return value. But we will be able to ppreuse it over different users without
-- having to change it at all.
analyzeOnePokemon :: (LogThrow m) => PTask m a Pokemon
analyzeOnePokemon =
  loadData pokemonFile >>> (arr analyzePokemon >>> writeData analysisFile) &&& id >>> arr snd
  where
    analyzePokemon = Analysis . length . pkMoves -- Just count number of moves

mainTask :: (LogThrow m) => PTask m () ()
mainTask =
  -- First we get the ids of the users that we want to analyse. We need only one
  -- field that will contain a range of values, see IndexRange. By default, this
  -- range contains just one value, zero.
  getOption ["Settings"] (docField @"pokemonIds" (oneIndex (1::Int)) "The indices of the pokemon to load")
  -- We turn the range we read into a full lazy list:
  >>> arr enumTRIndices
  -- Then we just map over these ids and call analyseOnePokemon each time:
  >>> parMapTask "pokemonId" analyzeOnePokemon
  >>> writeSummary

main :: IO ()
main = runPipelineTask (FullConfig "example-pokeapi"
                                   "porcupine-http/examples/example-Poke/example-pokeapi.yaml"
                                   "example-pokeapi_files"
                                   ())
                       (  #http <-- useHTTP
                            -- We just add #http on top of the baseContexts.
                       :& baseContexts "")
                       mainTask ()