packages feed

htsn-import-0.2.1: src/TSN/XML/GameInfo.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

-- | GameInfo represents a collection of DTDs that we don't really
--   handle but want to make available. The raw XML gets stored in the
--   database along with the XML_File_ID, but we don't parse any of it.
--
--   See also: TSN.XML.SportInfo
--
module TSN.XML.GameInfo (
  dtds,
  game_info_tests,
  parse_xml,
  -- * WARNING: these are private but exported to silence warnings
  GameInfoConstructor(..) )
where

-- System imports.
import Data.Either ( rights )
import Data.String.Utils ( replace )
import Data.Time.Clock ( UTCTime )
import Database.Groundhog (
  countAll,
  insert_,
  migrate )
import Database.Groundhog.Generic ( runDbConn, runMigrationSilent )
import Database.Groundhog.Sqlite ( withSqliteConn )
import Database.Groundhog.TH (
  defaultCodegenConfig,
  groundhog,
  mkPersist )
import Test.Tasty ( TestTree, testGroup )
import Test.Tasty.HUnit ( (@?=), testCase )
import Text.XML.HXT.Core ( XmlTree )
import Text.XML.HXT.DOM.ShowXml ( xshow )

-- Local imports.
import TSN.DbImport (
  DbImport(..),
  ImportResult(..),
  run_dbmigrate )
import TSN.Parse (
  ParseError,
  parse_game_id,
  parse_message,
  parse_schedule_id,
  parse_xmlfid,
  parse_xml_time_stamp )
import Xml ( unsafe_read_document )


-- | The DTDs for everything that we consider \"Game Info.\"
--
--   TODO: This is the list from the old implementation. We need to
--   make sure that we are really receiving XML for these DTDs
--   (i.e. the names are correct).
--
dtds :: [String]
dtds =
  [ "CBASK_Lineup_XML.dtd",
    "cbaskpreviewxml.dtd",
    "cflpreviewxml.dtd",
    "Matchup_NBA_NHL_XML.dtd",
    "mlbpreviewxml.dtd",
    "MLB_Gaming_Matchup_XML.dtd",
    "MLB_Lineup_XML.dtd",
    "MLB_Matchup_XML.dtd",
    "MLS_Preview_XML.dtd",
    "NBA_Gaming_Matchup_XML.dtd",
    "NBA_Playoff_Matchup_XML.dtd",
    "NBALineupXML.dtd",
    "nbapreviewxml.dtd",
    "NCAA_FB_Preview_XML.dtd",
    "nflpreviewxml.dtd",
    "NFL_NCAA_FB_Matchup_XML.dtd",
    "nhlpreviewxml.dtd",
    "recapxml.dtd",
    "WorldBaseballPreviewXML.dtd" ]


-- | This serves as both the database and XML representation of a
--   GameInfo \<message\>.
--
--   The 'game_id' and 'schedule_id' fields are foreign keys, but they
--   key into multiple tables and key on records which may not exist
--   when we import the GameInfo document. We therefore don't declare
--   them as foreign keys; i.e. we don't require them to point
--   anywhere in particular. But if they do, that's nice.
--
data GameInfo =
  GameInfo {
    dtd :: String,
    xml_file_id :: Int,
    game_id :: Maybe Int, -- ^ These are optional because they are missing
                          --   from at least the MLB_Matchup_XML.dtd documents.
                          --   They provide foreign keys into any tables storing
                          --   games with their IDs.

    schedule_id :: Maybe Int, -- ^ Optional key into any table storing a
                              --   schedule along with its ID. We've noticed
                              --   them missing in e.g. recapxml.dtd documents.
    time_stamp :: UTCTime,
    xml :: String }
  deriving (Eq, Show)


-- | Attempt to parse a 'GameInfo' from an 'XmlTree'. If we cannot,
--   we fail with an error message.
--
parse_xml :: String -> XmlTree -> Either ParseError GameInfo
parse_xml dtdname xmltree = do
  xmlfid <- parse_xmlfid xmltree
  game_id <- parse_game_id xmltree
  schedule_id <- parse_schedule_id xmltree
  timestamp <- parse_xml_time_stamp xmltree
  message <- parse_message xmltree
  return $ GameInfo
             dtdname
             xmlfid
             game_id
             schedule_id
             timestamp
             (xshow [message])

--
-- * Database code
--

instance DbImport GameInfo where
  dbmigrate _ =
    run_dbmigrate $ migrate (undefined :: GameInfo)

  -- | We import a 'GameInfo' by inserting the whole thing at
  --   once. Nothing fancy going on here.
  dbimport msg = do
    insert_ msg
    return ImportSucceeded


-- | The database schema for GameInfo is trivial; all we need is for
--   the XML_File_ID to be unique.
--
mkPersist defaultCodegenConfig [groundhog|
- entity: GameInfo
  dbName: game_info
  constructors:
    - name: GameInfo
      uniques:
        - name: unique_game_info
          type: constraint
          # Prevent multiple imports of the same message.
          fields: [xml_file_id]
|]


--
-- Tasty Tests
--

-- | A list of all tests for this module.
--
game_info_tests :: TestTree
game_info_tests =
  testGroup
    "GameInfo tests"
    [ test_accessors,
      test_parse_xml_succeeds,
      test_dbimport_succeeds ]


-- | Make sure the accessors work and that we can parse one file. Ok,
--   so the real point of this is to make the unused fields (dtd, xml,
--   ...) warning go away without having to mangle the groundhog code.
--
test_accessors :: TestTree
test_accessors = testCase "we can access a parsed game_info" $ do
  xmltree <- unsafe_read_document "test/xml/gameinfo/recapxml.xml"
  let Right t = parse_xml "recapxml.dtd" xmltree
  let a1  = dtd t
  let ex1 = "recapxml.dtd"
  let a2  = xml_file_id t
  let ex2 = 21201550
  let a3  = show $ time_stamp t
  let ex3 = "2014-05-31 15:13:00 UTC"
  let a4  = game_id t
  let ex4  = Just 39978
  let a5  = schedule_id t
  let ex5  = Just 39978
  let a6  = take 9 (xml t)
  let ex6 = "<message>"
  let actual = (a1,a2,a3,a4,a5,a6)
  let expected = (ex1,ex2,ex3,ex4,ex5,ex6)
  actual @?= expected


-- | Sample XML documents for GameInfo types.
--
game_info_test_files :: [FilePath]
game_info_test_files =
  map (change_suffix . add_path) dtds
  where
     add_path = ("test/xml/gameinfo/" ++ )
     change_suffix = replace ".dtd" ".xml"

-- | Make sure we can parse every element of 'game_info_test_files'.
--
test_parse_xml_succeeds :: TestTree
test_parse_xml_succeeds =
  testGroup "parse_xml" $ map check game_info_test_files
  where
    check t = testCase t $ do
      x <- unsafe_read_document t
      let result = parse_xml "dummy" x
      let actual = case result of -- isRight appears in base-4.7
                     Left _  -> False
                     Right _ -> True
      let expected = True
      actual @?= expected


-- | Ensure that each element of 'game_info_test_files' can be imported
--   by counting the total number of database records (after
--   importing) and comparing it against the length of
--   'game_info_test_files'.
--
test_dbimport_succeeds :: TestTree
test_dbimport_succeeds = testCase "dbimport succeeds" $ do
  xmltrees <- mapM unsafe_read_document game_info_test_files
  let msgs = rights $ map (parse_xml "dummy") xmltrees
  actual <- withSqliteConn ":memory:" $ runDbConn $ do
                runMigrationSilent $
                  migrate (undefined :: GameInfo)
                mapM_ dbimport msgs
                countAll (undefined :: GameInfo)

  actual @?= expected
  where
    expected = length game_info_test_files