packages feed

kawhi 0.1.0 → 0.2.0

raw patch · 7 files changed

+537/−527 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- NBA.Stats: Split :: SplitName -> [SplitColumn] -> [SplitRow] -> Split
- NBA.Stats: SplitColumnNameNotFound :: String -> StatsError
- NBA.Stats: SplitKeyNotFound :: String -> StatsError
- NBA.Stats: SplitNameNotFound :: String -> StatsError
- NBA.Stats: SplitRowCardinalityInconsistent :: String -> StatsError
- NBA.Stats: SplitRowParseFailure :: String -> StatsError
- NBA.Stats: Stats :: [Split] -> Stats
- NBA.Stats: StatsResponseDecodeFailure :: String -> StatsError
- NBA.Stats: [columns] :: Split -> [SplitColumn]
- NBA.Stats: [name] :: Split -> SplitName
- NBA.Stats: [rows] :: Split -> [SplitRow]
- NBA.Stats: [splits] :: Stats -> [Split]
- NBA.Stats: data Split
- NBA.Stats: data Stats
- NBA.Stats: data StatsError
- NBA.Stats: getSplitRow :: (Eq v, Show v, FromJSON v, FromJSON a) => StatsPath -> SplitName -> SplitColumn -> v -> StatsParameters -> IO (Either StatsError a)
- NBA.Stats: getSplitRowGeneric :: (MonadIO m, MonadHttp m, MonadError StatsError m, MonadThrow m, Eq v, Show v, FromJSON v, FromJSON a) => StatsPath -> SplitName -> SplitColumn -> v -> StatsParameters -> m a
- NBA.Stats: getSplitRows :: (FromJSON a) => StatsPath -> SplitName -> StatsParameters -> IO (Either StatsError [a])
- NBA.Stats: getSplitRowsGeneric :: (MonadIO m, MonadHttp m, MonadError StatsError m, MonadThrow m, FromJSON a) => StatsPath -> SplitName -> StatsParameters -> m [a]
- NBA.Stats: instance Data.Aeson.Types.Class.FromJSON NBA.Stats.Split
- NBA.Stats: instance Data.Aeson.Types.Class.FromJSON NBA.Stats.Stats
- NBA.Stats: instance Data.Aeson.Types.Class.ToJSON NBA.Stats.Split
- NBA.Stats: instance Data.Aeson.Types.Class.ToJSON NBA.Stats.Stats
- NBA.Stats: instance GHC.Classes.Eq NBA.Stats.Split
- NBA.Stats: instance GHC.Classes.Eq NBA.Stats.Stats
- NBA.Stats: instance GHC.Classes.Eq NBA.Stats.StatsError
- NBA.Stats: instance GHC.Show.Show NBA.Stats.Split
- NBA.Stats: instance GHC.Show.Show NBA.Stats.Stats
- NBA.Stats: instance GHC.Show.Show NBA.Stats.StatsError
- NBA.Stats: type SplitColumn = Text
- NBA.Stats: type SplitName = Text
- NBA.Stats: type SplitRow = [Value]
- NBA.Stats: type StatsParameters = [(ByteString, Maybe ByteString)]
- NBA.Stats: type StatsPath = ByteString
+ Data.NBA.Stats: Split :: SplitName -> [SplitColumn] -> [SplitRow] -> Split
+ Data.NBA.Stats: SplitColumnNameNotFound :: String -> StatsError
+ Data.NBA.Stats: SplitKeyNotFound :: String -> StatsError
+ Data.NBA.Stats: SplitNameNotFound :: String -> StatsError
+ Data.NBA.Stats: SplitRowCardinalityInconsistent :: String -> StatsError
+ Data.NBA.Stats: SplitRowParseFailure :: String -> StatsError
+ Data.NBA.Stats: Stats :: [Split] -> Stats
+ Data.NBA.Stats: StatsResponseDecodeFailure :: String -> StatsError
+ Data.NBA.Stats: [columns] :: Split -> [SplitColumn]
+ Data.NBA.Stats: [name] :: Split -> SplitName
+ Data.NBA.Stats: [rows] :: Split -> [SplitRow]
+ Data.NBA.Stats: [splits] :: Stats -> [Split]
+ Data.NBA.Stats: data Split
+ Data.NBA.Stats: data Stats
+ Data.NBA.Stats: data StatsError
+ Data.NBA.Stats: getSplitRow :: (Eq v, Show v, FromJSON v, FromJSON a) => StatsPath -> SplitName -> SplitColumn -> v -> StatsParameters -> IO (Either StatsError a)
+ Data.NBA.Stats: getSplitRowGeneric :: (MonadIO m, MonadHttp m, MonadError StatsError m, MonadThrow m, Eq v, Show v, FromJSON v, FromJSON a) => StatsPath -> SplitName -> SplitColumn -> v -> StatsParameters -> m a
+ Data.NBA.Stats: getSplitRows :: (FromJSON a) => StatsPath -> SplitName -> StatsParameters -> IO (Either StatsError [a])
+ Data.NBA.Stats: getSplitRowsGeneric :: (MonadIO m, MonadHttp m, MonadError StatsError m, MonadThrow m, FromJSON a) => StatsPath -> SplitName -> StatsParameters -> m [a]
+ Data.NBA.Stats: instance Data.Aeson.Types.Class.FromJSON Data.NBA.Stats.Split
+ Data.NBA.Stats: instance Data.Aeson.Types.Class.FromJSON Data.NBA.Stats.Stats
+ Data.NBA.Stats: instance Data.Aeson.Types.Class.ToJSON Data.NBA.Stats.Split
+ Data.NBA.Stats: instance Data.Aeson.Types.Class.ToJSON Data.NBA.Stats.Stats
+ Data.NBA.Stats: instance GHC.Classes.Eq Data.NBA.Stats.Split
+ Data.NBA.Stats: instance GHC.Classes.Eq Data.NBA.Stats.Stats
+ Data.NBA.Stats: instance GHC.Classes.Eq Data.NBA.Stats.StatsError
+ Data.NBA.Stats: instance GHC.Show.Show Data.NBA.Stats.Split
+ Data.NBA.Stats: instance GHC.Show.Show Data.NBA.Stats.Stats
+ Data.NBA.Stats: instance GHC.Show.Show Data.NBA.Stats.StatsError
+ Data.NBA.Stats: type SplitColumn = Text
+ Data.NBA.Stats: type SplitName = Text
+ Data.NBA.Stats: type SplitRow = [Value]
+ Data.NBA.Stats: type StatsParameters = [(ByteString, Maybe ByteString)]
+ Data.NBA.Stats: type StatsPath = ByteString

Files

changelog.md view
@@ -1,5 +1,15 @@ # Changelog +## 0.2.0++### Summary++Updated the top-level module name.++### API Changes++NBA.Stats -> Data.NBA.Stats+ ## 0.1.0  ### Summary
kawhi.cabal view
@@ -1,5 +1,5 @@ name: kawhi-version: 0.1.0+version: 0.2.0 synopsis: stats.NBA.com library description: Functions and types for interacting with stats.NBA.com homepage: https://github.com/hamsterdam/kawhi@@ -20,7 +20,7 @@   hs-source-dirs: library   exposed-modules:     Control.Monad.Http-    NBA.Stats+    Data.NBA.Stats   other-modules:   build-depends:     base >= 4.8 && < 5,@@ -41,7 +41,7 @@   type: exitcode-stdio-1.0   hs-source-dirs: tests   main-is: Tests.hs-  other-modules: NBA.Stats.Tests+  other-modules: Data.NBA.Stats.Tests   build-depends:     base >= 4.8 && < 5,     kawhi >= 0.0,
+ library/Data/NBA/Stats.hs view
@@ -0,0 +1,333 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TypeSynonymInstances #-}++{-|+    Module: Data.NBA.Stats+    Copyright: Aaron Taylor, 2016+    License: MIT+    Maintainer: aaron@hamsterdam.co++    Functions and types for interacting with <http://stats.NBA.com NBA Stats>.+-}+module Data.NBA.Stats (+    -- * How to use this library+    -- $use++    -- * Simple API+    getSplitRows,+    getSplitRow,++    -- * Generic API+    getSplitRowsGeneric,+    getSplitRowGeneric,++    -- * Types+    Stats(..),+    Split(..),+    SplitName,+    SplitColumn,+    SplitRow,+    StatsPath,+    StatsParameters,+    StatsError(..),+) where++import qualified Control.Monad.Catch as Catch+import qualified Control.Monad.Except as Except+import qualified Control.Monad.Trans as Trans+import qualified Control.Monad.Http as MonadHttp+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import Data.Aeson ((.:), (.=))+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString as SBS+import qualified Data.ByteString.Char8 as Char8+import qualified Data.List as List+import Data.Monoid ((<>))+import qualified Data.Text as Text+import qualified Network.HTTP.Client as HTTP+import qualified Safe++{- |+    Gets all the rows in a NBA Stats split.++    When using this function in a custom monad transformer, it may be desirable to use the generic version of this function, 'getSplitRowsGeneric', instead.+-}+getSplitRows ::+    (Aeson.FromJSON a)+    => StatsPath -- ^ The URL path for the stats web page containing the split.+    -> SplitName -- ^ The split name.+    -> StatsParameters -- ^ The parameters for customizing the stats.+    -> IO (Either StatsError [a]) -- ^ The return value: an IO action resulting in an error or split rows.+getSplitRows path splitName params = Except.runExceptT $ getSplitRowsGeneric path splitName params++{- |+    Gets a row in a NBA Stats split.++    When using this function in a custom monad transformer, it may be desirable to use the generic version of this function, 'getSplitRowGeneric', instead.+-}+getSplitRow ::+    (Eq v, Show v, Aeson.FromJSON v, Aeson.FromJSON a)+    => StatsPath -- ^ The URL path for the stats web page containing the split.+    -> SplitName -- ^ The split name.+    -> SplitColumn -- ^ The column name key for a the desired row.+    -> v -- ^ The expected row value associated with the column name key for a the desired row.+    -> StatsParameters -- ^ The parameters for customizing the stats.+    -> IO (Either StatsError a) -- ^ The return value: an IO action resulting in an error or a split row.+getSplitRow path splitName key value params = Except.runExceptT $ getSplitRowGeneric path splitName key value params++{- |+    Gets all the rows in a NBA Stats split.++    The simpler version of this function, 'getSplitRows', has a concrete 'm'.+-}+getSplitRowsGeneric ::+    (Trans.MonadIO m, MonadHttp.MonadHttp m, Except.MonadError StatsError m, Catch.MonadThrow m, Aeson.FromJSON a)+    => StatsPath -- ^ The URL path for the stats web page containing the split.+    -> SplitName -- ^ The split name.+    -> StatsParameters -- ^ The parameters for customizing the stats.+    -> m [a] -- ^ The return value: an action resulting in an error or split rows.+getSplitRowsGeneric path splitName params = do+    response <- get path params+    split <- findSplit response splitName+    traverse (parseSplitRow $ columns split) $ rows split++{- |+    Gets a row in an NBA Stats split.++    The simpler version of this function, 'getSplitRows', has a concrete 'm'.+-}+getSplitRowGeneric ::+    (Trans.MonadIO m, MonadHttp.MonadHttp m, Except.MonadError StatsError m, Catch.MonadThrow m, Eq v, Show v, Aeson.FromJSON v, Aeson.FromJSON a)+    => StatsPath -- ^ The URL path for the stats web page containing the split.+    -> SplitName -- ^ The split name.+    -> SplitColumn -- ^ The column name key for a the desired row.+    -> v -- ^ The expected row value associated with the column name key for a the desired row.+    -> StatsParameters -- ^ The parameters for customizing the stats.+    -> m a -- ^ The return value: an action resulting in an error or a split row.+getSplitRowGeneric path splitName key value params = do+    response <- get path params+    split <- findSplit response splitName+    keyIndex <- maybe+        (Except.throwError $ SplitColumnNameNotFound $ Text.unpack key)+        return+        (List.elemIndex key (columns split))+    row <- maybe+        (Except.throwError $ SplitKeyNotFound $ show value)+        return+        (List.find+            (\row -> maybe+                False+                (==value)+                (Safe.atMay row keyIndex >>= Aeson.parseMaybe Aeson.parseJSON))+            (rows split))+    parseSplitRow (columns split) row++{- |+    An NBA Stats split.++    This type represents splits available from NBA Stats.+-}+data Split =+    -- | Constructor for a split.+    Split {+        -- | The split's name.+        name :: SplitName,+        -- | The split's column names.+        columns :: [SplitColumn],+        -- | The split's rows of data.+        rows :: [SplitRow]+    }+    deriving (Show, Eq)++instance Aeson.FromJSON Split where+    parseJSON (Aeson.Object v) = do+        name <- v .: "name"+        columns <- v .: "headers"+        rows <- v .: "rowSet"+        return Split {..}+    parseJSON invalid = Aeson.typeMismatch "Split" invalid++instance Aeson.ToJSON Split where+    toJSON Split {..} = Aeson.object [+        "name" .= name,+        "headers" .= columns,+        "rowSet" .= rows]++-- | An NBA Stats split name.+type SplitName = Text.Text++-- | A column name in an NBA Stats split.+type SplitColumn = Text.Text++-- | A row of data in an NBA Stats split.+type SplitRow = [Aeson.Value]++{- |+    An NBA Stats resource.++    This type represents the top-level JSON object returned from the NBA Stats REST API.+-}+data Stats =+    -- | Constructor for stats resource.+    Stats {+        -- | The resource's splits.+        splits :: [Split]+    }+    deriving (Show, Eq)++instance Aeson.ToJSON Stats where+    toJSON Stats {..} = Aeson.object [+        "resultSets" .= splits]++instance Aeson.FromJSON Stats where+    parseJSON (Aeson.Object o) = do+        splits <- o .: "resultSets"+        return Stats {..}+    parseJSON invalid = Aeson.typeMismatch "Stats" invalid++-- | A URL path for an NBA Stats resource.+type StatsPath = SBS.ByteString++-- | A collection of parameters that customize NBA Stats resources.+type StatsParameters = [(SBS.ByteString, Maybe SBS.ByteString)]++{- |+    An error which may be generated by this library.+-}+data StatsError =+    -- | An HTTP response has invalid JSON.+    StatsResponseDecodeFailure String |+    -- | A stats resource does not have a split matching the given split name.+    SplitNameNotFound String |+    -- | A split does not have a row matching the given column key and row value.+    SplitKeyNotFound String |+    -- | A split row has a different cardinality than the associated columns.+    SplitRowCardinalityInconsistent String |+    -- | A split does not have a column name matching the given key.+    SplitColumnNameNotFound String |+    -- | A failure to parse a split row's tabular data to the destination type.+    SplitRowParseFailure String+    deriving (Eq)++instance Show StatsError where+    show statsError = "StatsError (" ++ showCase statsError ++ ")"+        where+            showCase err = case err of+                StatsResponseDecodeFailure message -> format "StatsResponseDecodeFailure" message+                SplitNameNotFound message -> format "SplitNameNotFound" message+                SplitKeyNotFound message -> format "SplitKeyNotFound" message+                SplitRowCardinalityInconsistent message -> format "SplitRowCardinalityInconsistent" message+                SplitColumnNameNotFound message -> format "SplitColumnNameNotFound" message+                SplitRowParseFailure message -> format "SplitRowParseFailure" message+            format :: String -> String -> String+            format name message = name ++ " " ++ message++parseSplitRow :: (Except.MonadError StatsError m, Aeson.FromJSON a) => [SplitColumn] -> SplitRow -> m a+parseSplitRow columns row =+    if length columns == length row+        then case Aeson.parse Aeson.parseJSON $ Aeson.object (zip columns row) of+            Aeson.Error message -> Except.throwError $ SplitRowParseFailure message+            Aeson.Success split -> return split+        else Except.throwError $ SplitRowCardinalityInconsistent $ show row++findSplit :: (Except.MonadError StatsError m) => HTTP.Response LBS.ByteString -> SplitName -> m Split+findSplit response splitName = do+    stats <- either+        (Except.throwError . StatsResponseDecodeFailure)+        return+        (Aeson.eitherDecode . HTTP.responseBody $ response)+    maybe+        (Except.throwError $ SplitNameNotFound $ Text.unpack splitName)+        return+        (List.find (\r -> name r == splitName) $ splits stats)++++get :: (MonadHttp.MonadHttp m, Catch.MonadThrow m) => StatsPath -> StatsParameters -> m (HTTP.Response LBS.ByteString)+get path params = do+    request <- HTTP.parseRequest $ Char8.unpack $ "http://stats.nba.com/stats/" <> path+    MonadHttp.performRequest $ HTTP.setQueryString params request++{- $use+    The following is a working example of getting some "advanced statistics", split by month, for the San Antonio Spurs 2015-2016 regular season.++    To learn how to find the NBA Stats values, like 'teamdashboardbygeneralsplits', for this example, read the <https://github.com/hamsterdam/kawhi/blob/master/guide.md guide>.++    @+    import qualified Data.Aeson as Aeson+    import qualified Data.Aeson.Types as Aeson+    import Data.Aeson ((.:))+    import qualified Data.NBA.Stats as Stats++    main :: IO ()+    main = do+        eitherErrorOrStats <- advancedStatsByMonth+        case eitherErrorOrStats of+            Left statsError -> print statsError+            Right stats -> mapM_ print stats++    data AdvancedStats = AdvancedStats {+        month :: String,+        offensiveRating :: Double,+        defensiveRating :: Double+    } deriving (Show, Eq)++    instance Aeson.FromJSON AdvancedStats where+        parseJSON (Aeson.Object o) = do+            month <- o .: \"SEASON_MONTH_NAME\"+            offensiveRating <- o .: \"OFF_RATING\"+            defensiveRating <- o .: \"DEF_RATING\"+            return AdvancedStats {..}+        parseJSON invalid = Aeson.typeMismatch \"AdvancedStats\" invalid++    advancedStatsByMonth :: IO (Either Stats.StatsError [AdvancedStats])+    advancedStatsByMonth = Stats.getSplitRows \"teamdashboardbygeneralsplits\" \"MonthTeamDashboard\"+        [+            (\"Conference\", Nothing),+            (\"DateFrom\", Nothing),+            (\"DateTo\", Nothing),+            (\"Division\", Nothing),+            (\"GameScope\", Nothing),+            (\"GameSegment\", Nothing),+            (\"LastNGames\", Just \"0\"),+            (\"LeagueID\", Just \"00\"),+            (\"Location\", Nothing),+            (\"MeasureType\", Just \"Advanced\"),+            (\"Month\", Just \"0\"),+            (\"OpponentTeamID\", Just \"0\"),+            (\"Outcome\", Nothing),+            (\"PaceAdjust\", Just \"N\"),+            (\"PerMode\", Just \"PerGame\"),+            (\"Period\", Just \"0\"),+            (\"PlayerExperience\", Nothing),+            (\"PlayerPosition\", Nothing),+            (\"PlusMinus\", Just \"N\"),+            (\"PORound\", Just \"0\"),+            (\"Rank\", Just \"N\"),+            (\"Season\", Just \"2015-16\"),+            (\"SeasonSegment\", Nothing),+            (\"SeasonType\", Just \"Regular Season\"),+            (\"ShotClockRange\", Nothing),+            (\"StarterBench\", Nothing),+            (\"TeamID\", Just \"1610612759\"),+            (\"VsConference\", Nothing),+            (\"VsDivision\", Nothing)+        ]+    @++    This program's output at the time of writing is:++    @+    AdvancedStats {month = \"October\", offensiveRating = 102.7, defensiveRating = 93.4}+    AdvancedStats {month = \"November\", offensiveRating = 102.5, defensiveRating = 93.4}+    AdvancedStats {month = \"December\", offensiveRating = 111.8, defensiveRating = 91.5}+    AdvancedStats {month = \"January\", offensiveRating = 114.0, defensiveRating = 100.7}+    AdvancedStats {month = \"February\", offensiveRating = 110.7, defensiveRating = 99.1}+    AdvancedStats {month = \"March\", offensiveRating = 107.8, defensiveRating = 97.2}+    AdvancedStats {month = \"April\", offensiveRating = 102.3, defensiveRating = 103.5}+    @+-}
− library/NBA/Stats.hs
@@ -1,333 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TypeSynonymInstances #-}--{-|-    Module: NBA.Stats-    Copyright: Aaron Taylor, 2016-    License: MIT-    Maintainer: aaron@hamsterdam.co--    Functions and types for interacting with <http://stats.NBA.com NBA Stats>.--}-module NBA.Stats (-    -- * How to use this library-    -- $use--    -- * Simple API-    getSplitRows,-    getSplitRow,--    -- * Generic API-    getSplitRowsGeneric,-    getSplitRowGeneric,--    -- * Types-    Stats(..),-    Split(..),-    SplitName,-    SplitColumn,-    SplitRow,-    StatsPath,-    StatsParameters,-    StatsError(..),-) where--import qualified Control.Monad.Catch as Catch-import qualified Control.Monad.Except as Except-import qualified Control.Monad.Trans as Trans-import qualified Control.Monad.Http as MonadHttp-import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Types as Aeson-import Data.Aeson ((.:), (.=))-import qualified Data.ByteString.Lazy as LBS-import qualified Data.ByteString as SBS-import qualified Data.ByteString.Char8 as Char8-import qualified Data.List as List-import Data.Monoid ((<>))-import qualified Data.Text as Text-import qualified Network.HTTP.Client as HTTP-import qualified Safe--{- |-    Gets all the rows in a NBA Stats split.--    When using this function in a custom monad transformer, it may be desirable to use the generic version of this function, 'getSplitRowsGeneric', instead.--}-getSplitRows ::-    (Aeson.FromJSON a)-    => StatsPath -- ^ The URL path for the stats web page containing the split.-    -> SplitName -- ^ The split name.-    -> StatsParameters -- ^ The parameters for customizing the stats.-    -> IO (Either StatsError [a]) -- ^ The return value: an IO action resulting in an error or split rows.-getSplitRows path splitName params = Except.runExceptT $ getSplitRowsGeneric path splitName params--{- |-    Gets a row in a NBA Stats split.--    When using this function in a custom monad transformer, it may be desirable to use the generic version of this function, 'getSplitRowGeneric', instead.--}-getSplitRow ::-    (Eq v, Show v, Aeson.FromJSON v, Aeson.FromJSON a)-    => StatsPath -- ^ The URL path for the stats web page containing the split.-    -> SplitName -- ^ The split name.-    -> SplitColumn -- ^ The column name key for a the desired row.-    -> v -- ^ The expected row value associated with the column name key for a the desired row.-    -> StatsParameters -- ^ The parameters for customizing the stats.-    -> IO (Either StatsError a) -- ^ The return value: an IO action resulting in an error or a split row.-getSplitRow path splitName key value params = Except.runExceptT $ getSplitRowGeneric path splitName key value params--{- |-    Gets all the rows in a NBA Stats split.--    The simpler version of this function, 'getSplitRows', has a concrete 'm'.--}-getSplitRowsGeneric ::-    (Trans.MonadIO m, MonadHttp.MonadHttp m, Except.MonadError StatsError m, Catch.MonadThrow m, Aeson.FromJSON a)-    => StatsPath -- ^ The URL path for the stats web page containing the split.-    -> SplitName -- ^ The split name.-    -> StatsParameters -- ^ The parameters for customizing the stats.-    -> m [a] -- ^ The return value: an action resulting in an error or split rows.-getSplitRowsGeneric path splitName params = do-    response <- get path params-    split <- findSplit response splitName-    traverse (parseSplitRow $ columns split) $ rows split--{- |-    Gets a row in an NBA Stats split.--    The simpler version of this function, 'getSplitRows', has a concrete 'm'.--}-getSplitRowGeneric ::-    (Trans.MonadIO m, MonadHttp.MonadHttp m, Except.MonadError StatsError m, Catch.MonadThrow m, Eq v, Show v, Aeson.FromJSON v, Aeson.FromJSON a)-    => StatsPath -- ^ The URL path for the stats web page containing the split.-    -> SplitName -- ^ The split name.-    -> SplitColumn -- ^ The column name key for a the desired row.-    -> v -- ^ The expected row value associated with the column name key for a the desired row.-    -> StatsParameters -- ^ The parameters for customizing the stats.-    -> m a -- ^ The return value: an action resulting in an error or a split row.-getSplitRowGeneric path splitName key value params = do-    response <- get path params-    split <- findSplit response splitName-    keyIndex <- maybe-        (Except.throwError $ SplitColumnNameNotFound $ Text.unpack key)-        return-        (List.elemIndex key (columns split))-    row <- maybe-        (Except.throwError $ SplitKeyNotFound $ show value)-        return-        (List.find-            (\row -> maybe-                False-                (==value)-                (Safe.atMay row keyIndex >>= Aeson.parseMaybe Aeson.parseJSON))-            (rows split))-    parseSplitRow (columns split) row--{- |-    An NBA Stats split.--    This type represents splits available from NBA Stats.--}-data Split =-    -- | Constructor for a split.-    Split {-        -- | The split's name.-        name :: SplitName,-        -- | The split's column names.-        columns :: [SplitColumn],-        -- | The split's rows of data.-        rows :: [SplitRow]-    }-    deriving (Show, Eq)--instance Aeson.FromJSON Split where-    parseJSON (Aeson.Object v) = do-        name <- v .: "name"-        columns <- v .: "headers"-        rows <- v .: "rowSet"-        return Split {..}-    parseJSON invalid = Aeson.typeMismatch "Split" invalid--instance Aeson.ToJSON Split where-    toJSON Split {..} = Aeson.object [-        "name" .= name,-        "headers" .= columns,-        "rowSet" .= rows]---- | An NBA Stats split name.-type SplitName = Text.Text---- | A column name in an NBA Stats split.-type SplitColumn = Text.Text---- | A row of data in an NBA Stats split.-type SplitRow = [Aeson.Value]--{- |-    An NBA Stats resource.--    This type represents the top-level JSON object returned from the NBA Stats REST API.--}-data Stats =-    -- | Constructor for stats resource.-    Stats {-        -- | The resource's splits.-        splits :: [Split]-    }-    deriving (Show, Eq)--instance Aeson.ToJSON Stats where-    toJSON Stats {..} = Aeson.object [-        "resultSets" .= splits]--instance Aeson.FromJSON Stats where-    parseJSON (Aeson.Object o) = do-        splits <- o .: "resultSets"-        return Stats {..}-    parseJSON invalid = Aeson.typeMismatch "Stats" invalid---- | A URL path for an NBA Stats resource.-type StatsPath = SBS.ByteString---- | A collection of parameters that customize NBA Stats resources.-type StatsParameters = [(SBS.ByteString, Maybe SBS.ByteString)]--{- |-    An error which may be generated by this library.--}-data StatsError =-    -- | An HTTP response has invalid JSON.-    StatsResponseDecodeFailure String |-    -- | A stats resource does not have a split matching the given split name.-    SplitNameNotFound String |-    -- | A split does not have a row matching the given column key and row value.-    SplitKeyNotFound String |-    -- | A split row has a different cardinality than the associated columns.-    SplitRowCardinalityInconsistent String |-    -- | A split does not have a column name matching the given key.-    SplitColumnNameNotFound String |-    -- | A failure to parse a split row's tabular data to the destination type.-    SplitRowParseFailure String-    deriving (Eq)--instance Show StatsError where-    show statsError = "StatsError (" ++ showCase statsError ++ ")"-        where-            showCase err = case err of-                StatsResponseDecodeFailure message -> format "StatsResponseDecodeFailure" message-                SplitNameNotFound message -> format "SplitNameNotFound" message-                SplitKeyNotFound message -> format "SplitKeyNotFound" message-                SplitRowCardinalityInconsistent message -> format "SplitRowCardinalityInconsistent" message-                SplitColumnNameNotFound message -> format "SplitColumnNameNotFound" message-                SplitRowParseFailure message -> format "SplitRowParseFailure" message-            format :: String -> String -> String-            format name message = name ++ " " ++ message--parseSplitRow :: (Except.MonadError StatsError m, Aeson.FromJSON a) => [SplitColumn] -> SplitRow -> m a-parseSplitRow columns row =-    if length columns == length row-        then case Aeson.parse Aeson.parseJSON $ Aeson.object (zip columns row) of-            Aeson.Error message -> Except.throwError $ SplitRowParseFailure message-            Aeson.Success split -> return split-        else Except.throwError $ SplitRowCardinalityInconsistent $ show row--findSplit :: (Except.MonadError StatsError m) => HTTP.Response LBS.ByteString -> SplitName -> m Split-findSplit response splitName = do-    stats <- either-        (Except.throwError . StatsResponseDecodeFailure)-        return-        (Aeson.eitherDecode . HTTP.responseBody $ response)-    maybe-        (Except.throwError $ SplitNameNotFound $ Text.unpack splitName)-        return-        (List.find (\r -> name r == splitName) $ splits stats)----get :: (MonadHttp.MonadHttp m, Catch.MonadThrow m) => StatsPath -> StatsParameters -> m (HTTP.Response LBS.ByteString)-get path params = do-    request <- HTTP.parseRequest $ Char8.unpack $ "http://stats.nba.com/stats/" <> path-    MonadHttp.performRequest $ HTTP.setQueryString params request--{- $use-    The following is a working example of getting some "advanced statistics", split by month, for the San Antonio Spurs 2015-2016 regular season.--    To learn how to find the NBA Stats values, like 'teamdashboardbygeneralsplits', for this example, read the <https://github.com/hamsterdam/kawhi/blob/master/guide.md guide>.--    @-    import qualified Data.Aeson as Aeson-    import qualified Data.Aeson.Types as Aeson-    import Data.Aeson ((.:))-    import qualified NBA.Stats as Stats--    main :: IO ()-    main = do-        eitherErrorOrStats <- advancedStatsByMonth-        case eitherErrorOrStats of-            Left statsError -> print statsError-            Right stats -> mapM_ print stats--    data AdvancedStats = AdvancedStats {-        month :: String,-        offensiveRating :: Double,-        defensiveRating :: Double-    } deriving (Show, Eq)--    instance Aeson.FromJSON AdvancedStats where-        parseJSON (Aeson.Object o) = do-            month <- o .: \"SEASON_MONTH_NAME\"-            offensiveRating <- o .: \"OFF_RATING\"-            defensiveRating <- o .: \"DEF_RATING\"-            return AdvancedStats {..}-        parseJSON invalid = Aeson.typeMismatch \"AdvancedStats\" invalid--    advancedStatsByMonth :: IO (Either Stats.StatsError [AdvancedStats])-    advancedStatsByMonth = Stats.getSplitRows \"teamdashboardbygeneralsplits\" \"MonthTeamDashboard\"-        [-            (\"Conference\", Nothing),-            (\"DateFrom\", Nothing),-            (\"DateTo\", Nothing),-            (\"Division\", Nothing),-            (\"GameScope\", Nothing),-            (\"GameSegment\", Nothing),-            (\"LastNGames\", Just \"0\"),-            (\"LeagueID\", Just \"00\"),-            (\"Location\", Nothing),-            (\"MeasureType\", Just \"Advanced\"),-            (\"Month\", Just \"0\"),-            (\"OpponentTeamID\", Just \"0\"),-            (\"Outcome\", Nothing),-            (\"PaceAdjust\", Just \"N\"),-            (\"PerMode\", Just \"PerGame\"),-            (\"Period\", Just \"0\"),-            (\"PlayerExperience\", Nothing),-            (\"PlayerPosition\", Nothing),-            (\"PlusMinus\", Just \"N\"),-            (\"PORound\", Just \"0\"),-            (\"Rank\", Just \"N\"),-            (\"Season\", Just \"2015-16\"),-            (\"SeasonSegment\", Nothing),-            (\"SeasonType\", Just \"Regular Season\"),-            (\"ShotClockRange\", Nothing),-            (\"StarterBench\", Nothing),-            (\"TeamID\", Just \"1610612759\"),-            (\"VsConference\", Nothing),-            (\"VsDivision\", Nothing)-        ]-    @--    This program's output at the time of writing is:--    @-    AdvancedStats {month = \"October\", offensiveRating = 102.7, defensiveRating = 93.4}-    AdvancedStats {month = \"November\", offensiveRating = 102.5, defensiveRating = 93.4}-    AdvancedStats {month = \"December\", offensiveRating = 111.8, defensiveRating = 91.5}-    AdvancedStats {month = \"January\", offensiveRating = 114.0, defensiveRating = 100.7}-    AdvancedStats {month = \"February\", offensiveRating = 110.7, defensiveRating = 99.1}-    AdvancedStats {month = \"March\", offensiveRating = 107.8, defensiveRating = 97.2}-    AdvancedStats {month = \"April\", offensiveRating = 102.3, defensiveRating = 103.5}-    @--}
+ tests/Data/NBA/Stats/Tests.hs view
@@ -0,0 +1,190 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Data.NBA.Stats.Tests where++import qualified Control.Monad.Except as Except+import qualified Control.Monad.Http as MonadHttp+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import Data.Aeson ((.:))+import qualified Data.ByteString.Lazy as ByteString+import Data.Monoid ((<>))+import qualified Data.Scientific as Sci+import qualified Data.Text as Text+import qualified Network.HTTP.Client.Internal as HTTP+import qualified Network.HTTP.Types as HTTP+import qualified Data.NBA.Stats as Stats+import qualified Test.Tasty as Tasty+import qualified Test.Tasty.HUnit as HUnit+import qualified Test.Tasty.SmallCheck as SC+import Test.Tasty.HUnit ((@?=))++tests :: Tasty.TestTree+tests = Tasty.testGroup "Data.NBA.Stats" [+    propertyStatsErrorShow Stats.StatsResponseDecodeFailure "StatsResponseDecodeFailure",+    propertyStatsErrorShow Stats.SplitNameNotFound "SplitNameNotFound",+    propertyStatsErrorShow Stats.SplitKeyNotFound "SplitKeyNotFound",+    propertyStatsErrorShow Stats.SplitColumnNameNotFound "SplitColumnNameNotFound",+    propertyStatsErrorShow Stats.SplitRowCardinalityInconsistent "SplitRowCardinalityInconsistent",+    propertyStatsErrorShow Stats.SplitRowParseFailure "SplitRowParseFailure",++    getSplitRowExpectSuccess+        "Success"+        (defaultResponseBody [defaultSplit])+        defaultModel,++    getSplitRowsExpectSuccess+        "Success"+        (defaultResponseBody [defaultSplit { Stats.rows = [defaultRow, defaultRow] }])+        [defaultModel, defaultModel],++    getSplitRowExpectFailure+        "SplitRowCardinalityInconsistent"+        (defaultResponseBody [defaultSplit { Stats.rows = [take 3 defaultRow] }])+        (Stats.SplitRowCardinalityInconsistent $ show $ take 3 defaultRow),++    getSplitRowExpectFailure+        "SplitKeyNotFound (no rows)"+        (defaultResponseBody [defaultSplit { Stats.rows = [] }])+        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),++    getSplitRowExpectFailure+        "SplitKeyNotFound (no key value)"+        (defaultResponseBody [defaultSplit { Stats.rows = [[]] }])+        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),++    getSplitRowExpectFailure+        "SplitKeyNotFound (JSON parse error for key value)"+        (let rowWithBadValue = Aeson.Number 99 : defaultRow in defaultResponseBody [defaultSplit { Stats.rows = [rowWithBadValue] }])+        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),++    getSplitRowExpectFailure+        "SplitNameNotFound"+        (defaultResponseBody [])+        (Stats.SplitNameNotFound $ Text.unpack defaultSplitName),++    getSplitRowExpectFailure+        "SplitColumnNameNotFound"+        (defaultResponseBody [defaultSplit { Stats.columns = [] }])+        (Stats.SplitColumnNameNotFound $ Text.unpack defaultColumnsKey),++    getSplitRowExpectFailure+        "SplitRowParseFailure (type mismatch)"+        (defaultResponseBody [defaultSplit { Stats.rows = [[Aeson.String defaultRowIdentifier, Aeson.String $ Text.pack $ show (a defaultModel), Aeson.String $ b defaultModel, Aeson.Number $ Sci.fromFloatDigits (c defaultModel)]] }])+        (Stats.SplitRowParseFailure "failed to parse field A: expected Integral, encountered String"),++    getSplitRowExpectFailure+        "SplitRowParseFailure (missing field)"+        (defaultResponseBody [defaultSplit { Stats.columns = fmap (\c -> if c == "B" then "!B" else c) defaultColumns }])+        (Stats.SplitRowParseFailure "key \"B\" not present"),++    getSplitRowExpectFailure+        "StatsResponseDecodeFailure (for Stats)"+        (Aeson.encode [defaultSplit])+        (Stats.StatsResponseDecodeFailure "Error in $: expected Stats, encountered Array"),++    HUnit.testCase+        "parseJSON invalid :: Parser Split -> Error"+        (case Aeson.fromJSON $ Aeson.String "foo" :: Aeson.Result Stats.Split of+            Aeson.Success _ -> HUnit.assertFailure "Parse should not have succeeded"+            Aeson.Error e -> e @?= "expected Split, encountered String")+    ]++propertyStatsErrorShow :: Show a => (String -> a) -> String -> Tasty.TestTree+propertyStatsErrorShow constructor exception =+    SC.testProperty testName property+    where+        testName = "show (" ++ exception ++ " message) == \"StatsError (" ++ exception ++ " message)\""+        property message = show (constructor message) == "StatsError (" ++ exception ++ " " ++ message ++ ")"++getSplitRowExpectFailure :: Tasty.TestName -> ByteString.ByteString -> Stats.StatsError -> Tasty.TestTree+getSplitRowExpectFailure testName responseBody expected = HUnit.testCase ("Get stat -> " <> testName) $ do+    eitherModel <- runStatsTest $ runAction statAction responseBody HTTP.ok200+    case eitherModel of+        Left err -> err @?= expected+        Right model -> HUnit.assertFailure $ show model++expectSuccess :: (Eq a, Show a) => MonadHttp.HttpT StatsTest a -> Tasty.TestName -> ByteString.ByteString -> a -> Tasty.TestTree+expectSuccess action testName responseBody expected =+    HUnit.testCase testName $ do+        eitherModel <- runStatsTest $ runAction action responseBody HTTP.ok200+        case eitherModel of+            Left err -> HUnit.assertFailure $ show err+            Right actual -> actual @?= expected++getSplitRowsExpectSuccess :: Tasty.TestName -> ByteString.ByteString -> [MockModel] -> Tasty.TestTree+getSplitRowsExpectSuccess name = expectSuccess (Stats.getSplitRowsGeneric "mockmodels" defaultSplitName defaultParams) $ "Get stats -> " <> name++getSplitRowExpectSuccess :: Tasty.TestName -> ByteString.ByteString -> MockModel -> Tasty.TestTree+getSplitRowExpectSuccess name = expectSuccess statAction $ "Get stat -> " <> name++statAction :: MonadHttp.HttpT StatsTest MockModel+statAction = Stats.getSplitRowGeneric "mockmodels" defaultSplitName defaultColumnsKey defaultRowIdentifier defaultParams++runAction :: MonadHttp.HttpT StatsTest a -> ByteString.ByteString -> HTTP.Status -> StatsTest a+runAction action responseBody responseStatus = MonadHttp.runHttpT+    action+    HTTP.Response {+        HTTP.responseStatus = responseStatus,+        HTTP.responseVersion = HTTP.http11,+        HTTP.responseHeaders = [],+        HTTP.responseBody = responseBody,+        HTTP.responseCookieJar = HTTP.createCookieJar [],+        HTTP.responseClose' = HTTP.ResponseClose (return () :: IO ())+    }++defaultParams :: Stats.StatsParameters+defaultParams = [("param", Just "value")]++defaultResponseBody :: [Stats.Split] -> ByteString.ByteString+defaultResponseBody splits = Aeson.encode defaultStats { Stats.splits = splits }++defaultStats :: Stats.Stats+defaultStats = Stats.Stats {+    splits = []+}++defaultSplit :: Stats.Split+defaultSplit = Stats.Split {+    name = defaultSplitName,+    columns = defaultColumns,+    rows = [defaultRow]+}++defaultSplitName :: Stats.SplitName+defaultSplitName = "name"++defaultColumnsKey :: Stats.SplitColumn+defaultColumnsKey = "key"++defaultRowIdentifier :: Text.Text+defaultRowIdentifier = "identifier"++defaultColumns :: [Stats.SplitColumn]+defaultColumns = [defaultColumnsKey, "A", "B", "C"]++defaultRow :: Stats.SplitRow+defaultRow = [Aeson.String defaultRowIdentifier, Aeson.Number $ Sci.scientific (a defaultModel) 0, Aeson.String $ b defaultModel, Aeson.Number $ Sci.fromFloatDigits (c defaultModel)]++defaultModel :: MockModel+defaultModel = MockModel { a = 1, b = "1", c = 1.1 }++type StatsTest = Except.ExceptT Stats.StatsError IO++runStatsTest :: StatsTest a -> IO (Either Stats.StatsError a)+runStatsTest = Except.runExceptT++data MockModel = MockModel {+    a :: Integer,+    b :: Text.Text,+    c :: Double+} deriving (Show, Eq)++instance Aeson.FromJSON MockModel where+    parseJSON (Aeson.Object o) = do+        a <- o .: "A"+        b <- o .: "B"+        c <- o .: "C"+        return MockModel {..}+    parseJSON invalid = Aeson.typeMismatch "MockModel" invalid
− tests/NBA/Stats/Tests.hs
@@ -1,190 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module NBA.Stats.Tests where--import qualified Control.Monad.Except as Except-import qualified Control.Monad.Http as MonadHttp-import qualified Data.Aeson as Aeson-import qualified Data.Aeson.Types as Aeson-import Data.Aeson ((.:))-import qualified Data.ByteString.Lazy as ByteString-import Data.Monoid ((<>))-import qualified Data.Scientific as Sci-import qualified Data.Text as Text-import qualified Network.HTTP.Client.Internal as HTTP-import qualified Network.HTTP.Types as HTTP-import qualified NBA.Stats as Stats-import qualified Test.Tasty as Tasty-import qualified Test.Tasty.HUnit as HUnit-import qualified Test.Tasty.SmallCheck as SC-import Test.Tasty.HUnit ((@?=))--tests :: Tasty.TestTree-tests = Tasty.testGroup "NBA.Stats" [-    propertyStatsErrorShow Stats.StatsResponseDecodeFailure "StatsResponseDecodeFailure",-    propertyStatsErrorShow Stats.SplitNameNotFound "SplitNameNotFound",-    propertyStatsErrorShow Stats.SplitKeyNotFound "SplitKeyNotFound",-    propertyStatsErrorShow Stats.SplitColumnNameNotFound "SplitColumnNameNotFound",-    propertyStatsErrorShow Stats.SplitRowCardinalityInconsistent "SplitRowCardinalityInconsistent",-    propertyStatsErrorShow Stats.SplitRowParseFailure "SplitRowParseFailure",--    getSplitRowExpectSuccess-        "Success"-        (defaultResponseBody [defaultSplit])-        defaultModel,--    getSplitRowsExpectSuccess-        "Success"-        (defaultResponseBody [defaultSplit { Stats.rows = [defaultRow, defaultRow] }])-        [defaultModel, defaultModel],--    getSplitRowExpectFailure-        "SplitRowCardinalityInconsistent"-        (defaultResponseBody [defaultSplit { Stats.rows = [take 3 defaultRow] }])-        (Stats.SplitRowCardinalityInconsistent $ show $ take 3 defaultRow),--    getSplitRowExpectFailure-        "SplitKeyNotFound (no rows)"-        (defaultResponseBody [defaultSplit { Stats.rows = [] }])-        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),--    getSplitRowExpectFailure-        "SplitKeyNotFound (no key value)"-        (defaultResponseBody [defaultSplit { Stats.rows = [[]] }])-        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),--    getSplitRowExpectFailure-        "SplitKeyNotFound (JSON parse error for key value)"-        (let rowWithBadValue = Aeson.Number 99 : defaultRow in defaultResponseBody [defaultSplit { Stats.rows = [rowWithBadValue] }])-        (Stats.SplitKeyNotFound $ show defaultRowIdentifier),--    getSplitRowExpectFailure-        "SplitNameNotFound"-        (defaultResponseBody [])-        (Stats.SplitNameNotFound $ Text.unpack defaultSplitName),--    getSplitRowExpectFailure-        "SplitColumnNameNotFound"-        (defaultResponseBody [defaultSplit { Stats.columns = [] }])-        (Stats.SplitColumnNameNotFound $ Text.unpack defaultColumnsKey),--    getSplitRowExpectFailure-        "SplitRowParseFailure (type mismatch)"-        (defaultResponseBody [defaultSplit { Stats.rows = [[Aeson.String defaultRowIdentifier, Aeson.String $ Text.pack $ show (a defaultModel), Aeson.String $ b defaultModel, Aeson.Number $ Sci.fromFloatDigits (c defaultModel)]] }])-        (Stats.SplitRowParseFailure "failed to parse field A: expected Integral, encountered String"),--    getSplitRowExpectFailure-        "SplitRowParseFailure (missing field)"-        (defaultResponseBody [defaultSplit { Stats.columns = fmap (\c -> if c == "B" then "!B" else c) defaultColumns }])-        (Stats.SplitRowParseFailure "key \"B\" not present"),--    getSplitRowExpectFailure-        "StatsResponseDecodeFailure (for Stats)"-        (Aeson.encode [defaultSplit])-        (Stats.StatsResponseDecodeFailure "Error in $: expected Stats, encountered Array"),--    HUnit.testCase-        "parseJSON invalid :: Parser Split -> Error"-        (case Aeson.fromJSON $ Aeson.String "foo" :: Aeson.Result Stats.Split of-            Aeson.Success _ -> HUnit.assertFailure "Parse should not have succeeded"-            Aeson.Error e -> e @?= "expected Split, encountered String")-    ]--propertyStatsErrorShow :: Show a => (String -> a) -> String -> Tasty.TestTree-propertyStatsErrorShow constructor exception =-    SC.testProperty testName property-    where-        testName = "show (" ++ exception ++ " message) == \"StatsError (" ++ exception ++ " message)\""-        property message = show (constructor message) == "StatsError (" ++ exception ++ " " ++ message ++ ")"--getSplitRowExpectFailure :: Tasty.TestName -> ByteString.ByteString -> Stats.StatsError -> Tasty.TestTree-getSplitRowExpectFailure testName responseBody expected = HUnit.testCase ("Get stat -> " <> testName) $ do-    eitherModel <- runStatsTest $ runAction statAction responseBody HTTP.ok200-    case eitherModel of-        Left err -> err @?= expected-        Right model -> HUnit.assertFailure $ show model--expectSuccess :: (Eq a, Show a) => MonadHttp.HttpT StatsTest a -> Tasty.TestName -> ByteString.ByteString -> a -> Tasty.TestTree-expectSuccess action testName responseBody expected =-    HUnit.testCase testName $ do-        eitherModel <- runStatsTest $ runAction action responseBody HTTP.ok200-        case eitherModel of-            Left err -> HUnit.assertFailure $ show err-            Right actual -> actual @?= expected--getSplitRowsExpectSuccess :: Tasty.TestName -> ByteString.ByteString -> [MockModel] -> Tasty.TestTree-getSplitRowsExpectSuccess name = expectSuccess (Stats.getSplitRowsGeneric "mockmodels" defaultSplitName defaultParams) $ "Get stats -> " <> name--getSplitRowExpectSuccess :: Tasty.TestName -> ByteString.ByteString -> MockModel -> Tasty.TestTree-getSplitRowExpectSuccess name = expectSuccess statAction $ "Get stat -> " <> name--statAction :: MonadHttp.HttpT StatsTest MockModel-statAction = Stats.getSplitRowGeneric "mockmodels" defaultSplitName defaultColumnsKey defaultRowIdentifier defaultParams--runAction :: MonadHttp.HttpT StatsTest a -> ByteString.ByteString -> HTTP.Status -> StatsTest a-runAction action responseBody responseStatus = MonadHttp.runHttpT-    action-    HTTP.Response {-        HTTP.responseStatus = responseStatus,-        HTTP.responseVersion = HTTP.http11,-        HTTP.responseHeaders = [],-        HTTP.responseBody = responseBody,-        HTTP.responseCookieJar = HTTP.createCookieJar [],-        HTTP.responseClose' = HTTP.ResponseClose (return () :: IO ())-    }--defaultParams :: Stats.StatsParameters-defaultParams = [("param", Just "value")]--defaultResponseBody :: [Stats.Split] -> ByteString.ByteString-defaultResponseBody splits = Aeson.encode defaultStats { Stats.splits = splits }--defaultStats :: Stats.Stats-defaultStats = Stats.Stats {-    splits = []-}--defaultSplit :: Stats.Split-defaultSplit = Stats.Split {-    name = defaultSplitName,-    columns = defaultColumns,-    rows = [defaultRow]-}--defaultSplitName :: Stats.SplitName-defaultSplitName = "name"--defaultColumnsKey :: Stats.SplitColumn-defaultColumnsKey = "key"--defaultRowIdentifier :: Text.Text-defaultRowIdentifier = "identifier"--defaultColumns :: [Stats.SplitColumn]-defaultColumns = [defaultColumnsKey, "A", "B", "C"]--defaultRow :: Stats.SplitRow-defaultRow = [Aeson.String defaultRowIdentifier, Aeson.Number $ Sci.scientific (a defaultModel) 0, Aeson.String $ b defaultModel, Aeson.Number $ Sci.fromFloatDigits (c defaultModel)]--defaultModel :: MockModel-defaultModel = MockModel { a = 1, b = "1", c = 1.1 }--type StatsTest = Except.ExceptT Stats.StatsError IO--runStatsTest :: StatsTest a -> IO (Either Stats.StatsError a)-runStatsTest = Except.runExceptT--data MockModel = MockModel {-    a :: Integer,-    b :: Text.Text,-    c :: Double-} deriving (Show, Eq)--instance Aeson.FromJSON MockModel where-    parseJSON (Aeson.Object o) = do-        a <- o .: "A"-        b <- o .: "B"-        c <- o .: "C"-        return MockModel {..}-    parseJSON invalid = Aeson.typeMismatch "MockModel" invalid
tests/Tests.hs view
@@ -1,6 +1,6 @@ import qualified Test.Tasty as Tasty -import qualified NBA.Stats.Tests as Stats+import qualified Data.NBA.Stats.Tests as Stats  main :: IO () main = Tasty.defaultMain Stats.tests