1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE QuasiQuotes #-}
5 {-# LANGUAGE RecordWildCards #-}
6 {-# LANGUAGE ScopedTypeVariables #-}
7 {-# LANGUAGE StandaloneDeriving #-}
8 {-# LANGUAGE TemplateHaskell #-}
9 {-# LANGUAGE TypeFamilies #-}
18 -- | Parse TSN XML for the DTD "Odds_XML.dtd". Each document contains
19 -- a root element \<message\> that contains a bunch of other
23 import Control.Monad ( forM_ )
24 import Data.Tuple.Curry ( uncurryN )
25 import Database.Groundhog (
32 import Database.Groundhog.Core ( DefaultKey )
33 import Database.Groundhog.TH (
36 import Test.Tasty ( TestTree, testGroup )
37 import Test.Tasty.HUnit ( (@?=), testCase )
38 import Text.XML.HXT.Core (
57 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
58 import TSN.XmlImport ( XmlImport(..) )
59 import Xml ( FromXml(..), pickle_unpickle, unpickleable )
63 data OddsGameCasinoXml =
65 xml_casino_client_id :: Int,
66 xml_casino_name :: String,
67 xml_casino_line :: Maybe Double }
71 -- | The casinos should have their own table, but the lines don't
72 -- belong in that table. (There should be another table joining the
73 -- casinos and the thing the lines are for together.)
75 -- We drop the 'Game' prefix because the Casinos really aren't
76 -- children of the games; the XML just makes it seem that way.
80 casino_client_id :: Int,
81 casino_name :: String }
84 instance FromXml OddsGameCasinoXml where
85 type Db OddsGameCasinoXml = OddsCasino
87 -- We don't need the key argument (from_xml_fk) since the XML type
88 -- contains more information in this case.
89 from_xml OddsGameCasinoXml{..} = OddsCasino
93 instance XmlImport OddsGameCasinoXml
96 data OddsGameHomeTeamXml =
98 xml_home_team_id :: Int,
99 xml_home_rotation_number :: Int,
100 xml_home_abbr :: String,
101 xml_home_team_name :: String,
102 xml_home_casinos :: [OddsGameCasinoXml] }
105 instance FromXml OddsGameHomeTeamXml where
106 type Db OddsGameHomeTeamXml = OddsGameTeam
107 from_xml OddsGameHomeTeamXml{..} = OddsGameTeam
112 instance XmlImport OddsGameHomeTeamXml where
119 db_team_name :: String }
123 -- | Database mapping between games and their home/away teams.
124 data OddsGame_OddsGameTeam =
125 OddsGame_OddsGameTeam {
126 ogogt_odds_games_id :: DefaultKey OddsGame,
127 ogogt_away_team_id :: DefaultKey OddsGameTeam,
128 ogogt_home_team_id :: DefaultKey OddsGameTeam }
130 data OddsGameAwayTeamXml =
131 OddsGameAwayTeamXml {
132 xml_away_team_id :: Int,
133 xml_away_rotation_number :: Int,
134 xml_away_abbr :: String,
135 xml_away_team_name :: String,
136 xml_away_casinos :: [OddsGameCasinoXml] }
139 instance FromXml OddsGameAwayTeamXml where
140 type Db OddsGameAwayTeamXml = OddsGameTeam
141 from_xml OddsGameAwayTeamXml{..} = OddsGameTeam
146 instance XmlImport OddsGameAwayTeamXml where
148 -- | Can't use a newtype with Groundhog.
149 newtype OddsGameOverUnderXml =
150 OddsGameOverUnderXml { xml_casinos :: [OddsGameCasinoXml] }
153 -- | This database representation of the casino lines can't be
154 -- constructed from the one in the XML. The casinos within
155 -- Game>HomeTeam, Game>AwayTeam, and Game>Over_Under are all the
156 -- same. We don't need a bajillion different tables to store that --
157 -- just one tying the casino/game pair to the three lines.
160 ogl_odds_games_id :: DefaultKey OddsGame,
161 ogl_odds_casinos_id :: DefaultKey OddsCasino,
162 ogl_over_under :: Maybe Double,
163 ogl_away_line :: Maybe Double,
164 ogl_home_line :: Maybe Double }
169 db_game_date :: String, -- TODO
170 db_game_time :: String, -- TODO
171 db_game_away_team_rotation_number :: Int,
172 db_game_home_team_rotation_number :: Int }
173 deriving instance Eq OddsGame
174 deriving instance Show OddsGame
179 xml_game_date :: String, -- TODO
180 xml_game_time :: String, -- TODO
181 xml_game_away_team :: OddsGameAwayTeamXml,
182 xml_game_home_team :: OddsGameHomeTeamXml,
183 xml_game_over_under :: OddsGameOverUnderXml }
186 -- | Pseudo-field that lets us get the 'OddsCasino's out of
187 -- xml_game_over_under.
188 xml_game_over_under_casinos :: OddsGameXml -> [OddsGameCasinoXml]
189 xml_game_over_under_casinos = xml_casinos . xml_game_over_under
192 instance FromXml OddsGameXml where
193 type Db OddsGameXml = OddsGame
194 from_xml OddsGameXml{..} = OddsGame
198 (xml_away_rotation_number xml_game_away_team)
199 (xml_home_rotation_number xml_game_home_team)
201 instance XmlImport OddsGameXml
209 db_line_time :: String }
212 -- | Map 'Odds' to their children 'OddsGame's.
215 oog_odds_id :: DefaultKey Odds,
216 oog_odds_games_id :: DefaultKey OddsGame }
218 -- | This is our best guess at what occurs in the Odds_XML
219 -- documents. It looks like each consecutive set of games can
220 -- optionally have some notes appear before it. Each "note" comes as
221 -- its own <Notes>...</Notes> element.
223 -- The notes are ignored completely in the database; we only bother
224 -- with them to ensure that we're (un)pickling correctly.
226 -- We can't group the notes with a "set" of 'OddsGame's, because that
227 -- leads to ambiguity in parsing. Since we're going to ignore the
228 -- notes anyway, we just stick them with an arbitrary game. C'est la
231 data OddsGameWithNotes =
234 game :: OddsGameXml }
237 -- | The XML representation of Odds.
240 xml_xml_file_id :: Int,
241 xml_heading :: String,
242 xml_category :: String,
245 xml_line_time :: String,
246 xml_games_with_notes :: [OddsGameWithNotes],
247 xml_time_stamp :: String }
250 -- | Pseudo-field that lets us get the 'OddsGame's out of
251 -- 'xml_games_with_notes'.
252 xml_games :: Message -> [OddsGameXml]
253 xml_games m = map game (xml_games_with_notes m)
256 instance FromXml Message where
257 type Db Message = Odds
259 -- We don't need the key argument (from_xml_fk) since the XML type
260 -- contains more information in this case.
261 from_xml (Message _ _ _ d e f _ _) =
264 instance XmlImport Message
268 -- * Groundhog database schema.
269 -- | This must come before the dbimport code.
271 mkPersist tsn_codegen_config [groundhog|
279 - name: unique_odds_casino
281 fields: [casino_client_id]
283 - entity: OddsGameTeam
284 dbName: odds_games_teams
288 - name: unique_odds_games_team
298 - name: unique_odds_game
302 - entity: OddsGameLine
303 dbName: odds_games_lines
305 - entity: Odds_OddsGame
306 dbName: odds__odds_games
308 - entity: OddsGame_OddsGameTeam
309 dbName: odds_games__odds_games_teams
312 instance DbImport Message where
315 migrate (undefined :: Odds)
316 migrate (undefined :: OddsCasino)
317 migrate (undefined :: OddsGameTeam)
318 migrate (undefined :: OddsGame)
319 migrate (undefined :: Odds_OddsGame)
320 migrate (undefined :: OddsGame_OddsGameTeam)
321 migrate (undefined :: OddsGameLine)
324 -- Insert the root "odds" element and acquire its primary key (id).
325 odds_id <- insert_xml m
327 -- Next, we insert the home and away teams. We do this before
328 -- inserting the game itself because the game has two foreign keys
329 -- pointing to odds_games_teams.
330 forM_ (xml_games m) $ \g -> do
331 game_id <- insert_xml_or_select g
332 -- Insert a record into odds__odds_game mapping this game
333 -- to its parent in the odds table.
334 insert_ (Odds_OddsGame odds_id game_id)
336 -- Next to insert the home and away teams.
337 away_team_id <- insert_xml_or_select (xml_game_away_team g)
338 home_team_id <- insert_xml_or_select (xml_game_home_team g)
340 -- Insert a record into odds_games__odds_games_teams
341 -- mapping the home/away teams to this game.
342 insert_ (OddsGame_OddsGameTeam game_id away_team_id home_team_id)
344 -- Finaly, we insert the lines. The over/under entries for this
345 -- game and the lines for the casinos all wind up in the same
346 -- table, odds_games_lines. We can insert the over/under entries
347 -- freely with empty away/home lines:
348 forM_ (xml_game_over_under_casinos g) $ \c -> do
349 -- Start by inderting the casino.
350 ou_casino_id <- insert_xml_or_select c
352 -- Now add the over/under entry with the casino's id.
353 let ogl = OddsGameLine
362 -- ...but then when we insert the home/away team lines, we
363 -- prefer to update the existing entry rather than overwrite it
364 -- or add a new record.
365 forM_ (xml_away_casinos $ xml_game_away_team g) $ \c ->do
366 -- insert, or more likely retrieve the existing, casino
367 a_casino_id <- insert_xml_or_select c
369 -- Unconditionally update that casino's away team line with ours.
370 update [Ogl_Away_Line =. (xml_casino_line c)] $ -- WHERE
371 Ogl_Odds_Casinos_Id ==. a_casino_id
373 -- Repeat all that for the home team.
374 forM_ (xml_home_casinos $ xml_game_home_team g) $ \c ->do
375 h_casino_id <- insert_xml_or_select c
376 update [Ogl_Home_Line =. (xml_casino_line c)] $ -- WHERE
377 Ogl_Odds_Casinos_Id ==. h_casino_id
381 return ImportSucceeded
383 pickle_game_with_notes :: PU OddsGameWithNotes
384 pickle_game_with_notes =
385 xpWrap (from_pair, to_pair) $
387 (xpList $ xpElem "Notes" xpText)
390 from_pair = uncurry OddsGameWithNotes
391 to_pair OddsGameWithNotes{..} = (notes, game)
395 pickle_casino :: PU OddsGameCasinoXml
398 xpWrap (from_tuple, to_tuple) $
400 (xpAttr "ClientID" xpInt)
401 (xpAttr "Name" xpText)
402 (xpOption xpPrim) -- Double
404 from_tuple = uncurryN OddsGameCasinoXml
405 -- Use record wildcards to avoid unused field warnings.
406 to_tuple OddsGameCasinoXml{..} = (xml_casino_client_id,
410 instance XmlPickler OddsGameCasinoXml where
411 xpickle = pickle_casino
414 pickle_home_team :: PU OddsGameHomeTeamXml
417 xpWrap (from_tuple, to_tuple) $
419 (xpElem "HomeTeamID" xpInt)
420 (xpElem "HomeRotationNumber" xpInt)
421 (xpElem "HomeAbbr" xpText)
422 (xpElem "HomeTeamName" xpText)
423 (xpList pickle_casino)
425 from_tuple = uncurryN OddsGameHomeTeamXml
426 -- Use record wildcards to avoid unused field warnings.
427 to_tuple OddsGameHomeTeamXml{..} = (xml_home_team_id,
428 xml_home_rotation_number,
433 instance XmlPickler OddsGameHomeTeamXml where
434 xpickle = pickle_home_team
437 pickle_away_team :: PU OddsGameAwayTeamXml
440 xpWrap (from_tuple, to_tuple) $
442 (xpElem "AwayTeamID" xpInt)
443 (xpElem "AwayRotationNumber" xpInt)
444 (xpElem "AwayAbbr" xpText)
445 (xpElem "AwayTeamName" xpText)
446 (xpList pickle_casino)
448 from_tuple = uncurryN OddsGameAwayTeamXml
449 -- Use record wildcards to avoid unused field warnings.
450 to_tuple OddsGameAwayTeamXml{..} = (xml_away_team_id,
451 xml_away_rotation_number,
457 instance XmlPickler OddsGameAwayTeamXml where
458 xpickle = pickle_away_team
461 pickle_over_under :: PU OddsGameOverUnderXml
463 xpElem "Over_Under" $
464 xpWrap (to_newtype, from_newtype) $
467 from_newtype (OddsGameOverUnderXml cs) = cs
468 to_newtype = OddsGameOverUnderXml
470 instance XmlPickler OddsGameOverUnderXml where
471 xpickle = pickle_over_under
474 pickle_game :: PU OddsGameXml
477 xpWrap (from_tuple, to_tuple) $
479 (xpElem "GameID" xpInt)
480 (xpElem "Game_Date" xpText)
481 (xpElem "Game_Time" xpText)
486 from_tuple = uncurryN OddsGameXml
487 -- Use record wildcards to avoid unused field warnings.
488 to_tuple OddsGameXml{..} = (xml_game_id,
495 instance XmlPickler OddsGameXml where
496 xpickle = pickle_game
499 pickle_message :: PU Message
502 xpWrap (from_tuple, to_tuple) $
503 xp8Tuple (xpElem "XML_File_ID" xpInt)
504 (xpElem "heading" xpText)
505 (xpElem "category" xpText)
506 (xpElem "sport" xpText)
507 (xpElem "Title" xpText)
508 (xpElem "Line_Time" xpText)
509 (xpList pickle_game_with_notes)
510 (xpElem "time_stamp" xpText)
512 from_tuple = uncurryN Message
513 to_tuple m = (xml_xml_file_id m,
519 xml_games_with_notes m,
523 instance XmlPickler Message where
524 xpickle = pickle_message
530 odds_tests :: TestTree
534 [ test_pickle_of_unpickle_is_identity,
535 test_unpickle_succeeds ]
538 -- | Warning, succeess of this test does not mean that unpickling
540 test_pickle_of_unpickle_is_identity :: TestTree
541 test_pickle_of_unpickle_is_identity =
542 testCase "pickle composed with unpickle is the identity" $ do
543 let path = "test/xml/Odds_XML.xml"
544 (expected :: [Message], actual) <- pickle_unpickle "message" path
548 test_unpickle_succeeds :: TestTree
549 test_unpickle_succeeds =
550 testCase "unpickling succeeds" $ do
551 let path = "test/xml/Odds_XML.xml"
552 actual <- unpickleable path pickle_message