1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE DeriveGeneric #-}
3 {-# LANGUAGE FlexibleInstances #-}
5 {-# LANGUAGE QuasiQuotes #-}
6 {-# LANGUAGE RecordWildCards #-}
7 {-# LANGUAGE TemplateHaskell #-}
8 {-# LANGUAGE TypeFamilies #-}
10 -- | Parse TSN XML for the DTD \"AutoRacingResultsXML.dtd\". Each
11 -- \<message\> element contains a \<Race_Information\> and a bunch of
14 module TSN.XML.AutoRacingResults (
18 auto_racing_results_tests,
19 -- * WARNING: these are private but exported to silence warnings
20 AutoRacingResultsConstructor(..),
21 AutoRacingResultsListingConstructor(..),
22 AutoRacingResultsRaceInformationConstructor(..) )
26 import Control.Monad ( forM_ )
27 import Data.Data ( Data )
28 import Data.Maybe ( fromMaybe )
29 import Data.Time ( UTCTime(..) )
30 import Data.Tuple.Curry ( uncurryN )
31 import Data.Typeable ( Typeable )
32 import qualified Data.Vector.HFixed as H ( HVector, cons, convert )
33 import Database.Groundhog (
38 silentMigrationLogger )
39 import Database.Groundhog.Core ( DefaultKey )
40 import Database.Groundhog.Generic ( runDbConn )
41 import Database.Groundhog.Sqlite ( withSqliteConn )
42 import Database.Groundhog.TH (
45 import qualified GHC.Generics as GHC ( Generic )
46 import Test.Tasty ( TestTree, testGroup )
47 import Test.Tasty.HUnit ( (@?=), testCase )
48 import Text.XML.HXT.Core (
65 import TSN.Codegen ( tsn_codegen_config )
66 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
69 xp_fracpart_only_double,
72 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
83 -- | The DTD to which this module corresponds. Used to invoke dbimport.
86 dtd = "AutoRacingResultsXML.dtd"
92 -- * AutoRacingResults/Message
94 -- | Database representation of a 'Message'. Comparatively, it lacks
95 -- the listings and race information since they are linked via a
98 data AutoRacingResults =
100 db_xml_file_id :: Int,
101 db_heading :: String,
102 db_category :: String,
105 db_race_date :: UTCTime,
107 db_track_location :: String,
108 db_laps_remaining :: Int,
109 db_checkered_flag :: Bool,
110 db_time_stamp :: UTCTime }
115 -- | XML Representation of an 'AutoRacingResults'. It has the same
116 -- fields, but in addition contains the 'xml_listings' and
117 -- 'xml_race_information'.
121 xml_xml_file_id :: Int,
122 xml_heading :: String,
123 xml_category :: String,
126 xml_race_date :: UTCTime,
128 xml_track_location :: String,
129 xml_laps_remaining :: Int,
130 xml_checkered_flag :: Bool,
131 xml_listings :: [AutoRacingResultsListingXml],
132 xml_race_information :: AutoRacingResultsRaceInformationXml,
133 xml_time_stamp :: UTCTime }
134 deriving (Eq, GHC.Generic, Show)
136 -- | For 'H.convert'.
138 instance H.HVector Message
141 instance ToDb Message where
142 -- | The database analogue of a 'Message' is a 'AutoRacingResults'.
144 type Db Message = AutoRacingResults
147 -- | The 'FromXml' instance for 'Message' is required for the
148 -- 'XmlImport' instance.
150 instance FromXml Message where
151 -- | To convert a 'Message' to an 'AutoRacingResults', we just drop
152 -- the 'xml_listings' and 'xml_race_information'.
154 from_xml Message{..} =
156 db_xml_file_id = xml_xml_file_id,
157 db_heading = xml_heading,
158 db_category = xml_category,
159 db_sport = xml_sport,
160 db_race_id = xml_race_id,
161 db_race_date = xml_race_date,
162 db_title = xml_title,
163 db_track_location = xml_track_location,
164 db_laps_remaining = xml_laps_remaining,
165 db_checkered_flag = xml_checkered_flag,
166 db_time_stamp = xml_time_stamp }
169 -- | This allows us to insert the XML representation 'Message'
172 instance XmlImport Message
175 -- * AutoRacingResultsListing/AutoRacingResultsListingXml
177 -- | Database representation of a \<Listing\> contained within a
178 -- \<message\>. The leading underscores prevent unused field
181 data AutoRacingResultsListing =
182 AutoRacingResultsListing {
183 _db_auto_racing_results_id :: DefaultKey AutoRacingResults,
184 _db_finish_position :: Int,
185 _db_starting_position :: Int,
186 _db_car_number :: Int,
187 _db_driver_id :: Int,
188 _db_driver :: String,
189 _db_car_make :: String,
191 _db_laps_completed :: Int,
192 _db_laps_leading :: Int,
193 _db_status :: Maybe String,
194 _db_dnf :: Maybe Bool,
195 _db_nc :: Maybe Bool,
196 _db_earnings :: Maybe Int }
197 deriving ( GHC.Generic )
199 -- | For 'H.convert' and 'H.cons'.
201 instance H.HVector AutoRacingResultsListing
203 -- | XML representation of a \<Listing\> contained within a
204 -- \<message\>. The leading underscores prevent unused field
207 data AutoRacingResultsListingXml =
208 AutoRacingResultsListingXml {
209 _xml_finish_position :: Int,
210 _xml_starting_position :: Int,
211 _xml_car_number :: Int,
212 _xml_driver_id :: Int,
213 _xml_driver :: String,
214 _xml_car_make :: String,
216 _xml_laps_completed :: Int,
217 _xml_laps_leading :: Int,
218 _xml_status :: Maybe String,
219 _xml_dnf :: Maybe Bool,
220 _xml_nc :: Maybe Bool,
221 _xml_earnings :: Maybe Int }
222 deriving (Eq, GHC.Generic, Show)
224 -- | For 'H.convert'.
226 instance H.HVector AutoRacingResultsListingXml
228 instance ToDb AutoRacingResultsListingXml where
229 -- | The database analogue of an 'AutoRacingResultsListingXml' is
230 -- an 'AutoRacingResultsListing'.
232 type Db AutoRacingResultsListingXml = AutoRacingResultsListing
235 instance Child AutoRacingResultsListingXml where
236 -- | Each 'AutoRacingResultsListingXml' is contained in (i.e. has a
237 -- foreign key to) a 'AutoRacingResults'.
239 type Parent AutoRacingResultsListingXml = AutoRacingResults
242 instance FromXmlFk AutoRacingResultsListingXml where
243 -- | To convert an 'AutoRacingResultsListingXml' to an
244 -- 'AutoRacingResultsListing', we add the foreign key and copy
245 -- everything else verbatim.
250 -- | This allows us to insert the XML representation
251 -- 'AutoRacingResultsListingXml' directly.
253 instance XmlImportFk AutoRacingResultsListingXml
257 -- * AutoRacingResultsRaceInformation / AutoRacingResultsRaceInformationXml
259 -- | The \<Most_Laps_Leading\> child of \<Race_Information\> always
260 -- contains exactly three fields, so we just embed those three into
261 -- the 'AutoRacingResultsRaceInformation' type. We (pointlessly) use
262 -- the \"db_\" prefix since our field namer is going to strip of
263 -- everything before the first underscore.
265 -- We make the three fields optional because the entire
266 -- \<Most_Laps_Leading\> is apparently optional (although it is
267 -- usually present). A 'Nothing' in the XML should get turned into
268 -- three 'Nothing's in the DB.
270 data MostLapsLeading =
272 db_most_laps_leading_driver_id :: Maybe Int,
273 db_most_laps_leading_driver :: Maybe String,
274 db_most_laps_leading_number_of_laps :: Maybe Int }
275 deriving (Data, Eq, Show, Typeable)
278 -- | Database representation of a \<Race_Information\> contained
279 -- within a \<message\>.
281 -- The 'db_most_laps_leading' field is not optional because when we
282 -- convert from our XML representation, a missing 'MostLapsLeading'
283 -- will be replaced with a 'MostLapsLeading' with three missing
286 data AutoRacingResultsRaceInformation =
287 AutoRacingResultsRaceInformation {
288 -- Note the apostrophe to disambiguate it from the
289 -- AutoRacingResultsListing field.
290 db_auto_racing_results_id' :: DefaultKey AutoRacingResults,
291 db_track_length :: String, -- ^ Usually a Double, but sometimes a String,
292 -- like \"1.25 miles\".
293 db_track_length_kph :: Double,
295 db_average_speed_mph :: Maybe Double,
296 db_average_speed_kph :: Maybe Double,
297 db_average_speed :: Maybe Double,
298 db_time_of_race :: Maybe String,
299 db_margin_of_victory :: Maybe String,
300 db_cautions :: Maybe String,
301 db_lead_changes :: Maybe String,
302 db_lap_leaders :: Maybe String,
303 db_most_laps_leading :: MostLapsLeading }
306 -- | XML representation of a \<Listing\> contained within a
309 data AutoRacingResultsRaceInformationXml =
310 AutoRacingResultsRaceInformationXml {
311 xml_track_length :: String,
312 xml_track_length_kph :: Double,
314 xml_average_speed_mph :: Maybe Double,
315 xml_average_speed_kph :: Maybe Double,
316 xml_average_speed :: Maybe Double,
317 xml_time_of_race :: Maybe String,
318 xml_margin_of_victory :: Maybe String,
319 xml_cautions :: Maybe String,
320 xml_lead_changes :: Maybe String,
321 xml_lap_leaders :: Maybe String,
322 xml_most_laps_leading :: Maybe MostLapsLeading }
326 instance ToDb AutoRacingResultsRaceInformationXml where
327 -- | The database analogue of an
328 -- 'AutoRacingResultsRaceInformationXml' is an
329 -- 'AutoRacingResultsRaceInformation'.
331 type Db AutoRacingResultsRaceInformationXml =
332 AutoRacingResultsRaceInformation
335 instance Child AutoRacingResultsRaceInformationXml where
336 -- | Each 'AutoRacingResultsRaceInformationXml' is contained in
337 -- (i.e. has a foreign key to) a 'AutoRacingResults'.
339 type Parent AutoRacingResultsRaceInformationXml = AutoRacingResults
342 instance FromXmlFk AutoRacingResultsRaceInformationXml where
343 -- | To convert an 'AutoRacingResultsRaceInformationXml' to an
344 -- 'AutoRacingResultsRaceInformartion', we add the foreign key and
345 -- massage the 'MostLapsLeading' embedded type,
347 from_xml_fk fk AutoRacingResultsRaceInformationXml{..} =
348 AutoRacingResultsRaceInformation {
349 db_auto_racing_results_id' = fk,
350 db_track_length = xml_track_length,
351 db_track_length_kph = xml_track_length_kph,
353 db_average_speed_mph = xml_average_speed_mph,
354 db_average_speed_kph = xml_average_speed_kph,
355 db_average_speed = xml_average_speed,
356 db_time_of_race = xml_time_of_race,
357 db_margin_of_victory = xml_margin_of_victory,
358 db_cautions = xml_cautions,
359 db_lead_changes = xml_lead_changes,
360 db_lap_leaders = xml_lap_leaders,
361 db_most_laps_leading = most_laps_leading }
363 -- If we didn't get a \<Most_Laps_Leading\>, indicate that in
364 -- the database with an (embedded) 'MostLapsLeading' with three
367 fromMaybe (MostLapsLeading Nothing Nothing Nothing)
368 xml_most_laps_leading
371 -- | This allows us to insert the XML representation
372 -- 'AutoRacingResultsRaceInformationXml' directly.
374 instance XmlImportFk AutoRacingResultsRaceInformationXml
382 instance DbImport Message where
385 migrate (undefined :: AutoRacingResults)
386 migrate (undefined :: AutoRacingResultsListing)
387 migrate (undefined :: AutoRacingResultsRaceInformation)
389 -- | We insert the message, then use its ID to insert the listings
390 -- and race information.
392 msg_id <- insert_xml m
394 insert_xml_fk_ msg_id (xml_race_information m)
396 forM_ (xml_listings m) $ insert_xml_fk_ msg_id
398 return ImportSucceeded
402 mkPersist tsn_codegen_config [groundhog|
403 - entity: AutoRacingResults
404 dbName: auto_racing_results
406 - name: AutoRacingResults
408 - name: unique_auto_racing_results
410 # Prevent multiple imports of the same message.
411 fields: [db_xml_file_id]
414 - entity: AutoRacingResultsListing
415 dbName: auto_racing_results_listings
417 - name: AutoRacingResultsListing
419 - name: _db_auto_racing_results_id
423 # Note the apostrophe in the foreign key. This is to disambiguate
424 # it from the AutoRacingResultsListing foreign key of the same name.
425 # We strip it out of the dbName.
426 - entity: AutoRacingResultsRaceInformation
427 dbName: auto_racing_results_race_information
429 - name: AutoRacingResultsRaceInformation
431 - name: db_auto_racing_results_id'
432 dbName: auto_racing_results_id
435 - name: db_most_laps_leading
437 - {name: most_laps_leading_driver_id,
438 dbName: most_laps_leading_driver_id}
439 - {name: most_laps_leading_driver,
440 dbName: most_laps_leading_driver}
442 - embedded: MostLapsLeading
444 - name: db_most_laps_leading_driver_id
445 dbName: most_laps_leading_driver_id
446 - name: db_most_laps_leading_driver
447 dbName: most_laps_leading_driver
448 - name: db_most_laps_leading_number_of_laps
449 dbName: most_laps_leading_number_of_laps
457 -- | Pickler for the \<Listing\>s contained within \<message\>s.
459 pickle_listing :: PU AutoRacingResultsListingXml
462 xpWrap (from_tuple, H.convert) $
463 xp13Tuple (xpElem "FinishPosition" xpInt)
464 (xpElem "StartingPosition" xpInt)
465 (xpElem "CarNumber" xpInt)
466 (xpElem "DriverID" xpInt)
467 (xpElem "Driver" xpText)
468 (xpElem "CarMake" xpText)
469 (xpElem "Points" xpInt)
470 (xpElem "Laps_Completed" xpInt)
471 (xpElem "Laps_Leading" xpInt)
472 (xpElem "Status" $ xpOption xpText)
473 (xpOption $ xpElem "DNF" xpPrim)
474 (xpOption $ xpElem "NC" xpPrim)
475 (xpElem "Earnings" xp_earnings)
477 from_tuple = uncurryN AutoRacingResultsListingXml
480 -- | Pickler for the top-level 'Message'.
482 pickle_message :: PU Message
485 xpWrap (from_tuple, H.convert) $
486 xp13Tuple (xpElem "XML_File_ID" xpInt)
487 (xpElem "heading" xpText)
488 (xpElem "category" xpText)
489 (xpElem "sport" xpText)
490 (xpElem "RaceID" xpInt)
491 (xpElem "RaceDate" xp_datetime)
492 (xpElem "Title" xpText)
493 (xpElem "Track_Location" xpText)
494 (xpElem "Laps_Remaining" xpInt)
495 (xpElem "Checkered_Flag" xpPrim)
496 (xpList pickle_listing)
497 pickle_race_information
498 (xpElem "time_stamp" xp_time_stamp)
500 from_tuple = uncurryN Message
503 -- | Pickler for the \<Most_Laps_Leading\> child of a
504 -- \<Race_Information\>. This is complicated by the fact that the
505 -- three fields we're trying to parse are not actually optional;
506 -- only the entire \<Most_Laps_Leading\> is. So we always wrap what
507 -- we parse in a 'Just', and when converting from the DB to XML,
508 -- we'll drop the entire element if any of its fields are missing
509 -- (which they never should be).
511 pickle_most_laps_leading :: PU (Maybe MostLapsLeading)
512 pickle_most_laps_leading =
513 xpElem "Most_Laps_Leading" $
514 xpWrap (from_tuple, to_tuple') $
515 xpTriple (xpOption $ xpElem "DriverID" xpInt)
516 (xpOption $ xpElem "Driver" xpText)
517 (xpOption $ xpElem "NumberOfLaps" xpInt)
519 from_tuple :: (Maybe Int, Maybe String, Maybe Int) -> Maybe MostLapsLeading
520 from_tuple (Just x, Just y, Just z) =
521 Just $ MostLapsLeading (Just x) (Just y) (Just z)
522 from_tuple _ = Nothing
524 -- Sure had to go out of my way to avoid the warnings about unused
525 -- db_most_laps_foo fields here.
526 to_tuple' :: Maybe MostLapsLeading -> (Maybe Int, Maybe String, Maybe Int)
527 to_tuple' Nothing = (Nothing, Nothing, Nothing)
528 to_tuple' (Just (MostLapsLeading Nothing _ _)) = (Nothing, Nothing, Nothing)
529 to_tuple' (Just (MostLapsLeading _ Nothing _)) = (Nothing, Nothing, Nothing)
530 to_tuple' (Just (MostLapsLeading _ _ Nothing)) = (Nothing, Nothing, Nothing)
531 to_tuple' (Just m) = (db_most_laps_leading_driver_id m,
532 db_most_laps_leading_driver m,
533 db_most_laps_leading_number_of_laps m)
536 -- | Pickler for the \<Race_Information\> child of \<message\>.
538 -- There's so much voodoo going on here. We have a double-layered
539 -- Maybe on top of the MostLapsLeading. When unpickling, we return a
540 -- Nothing (i.e. a Maybe MostLapsLeading) if any of its fields are
541 -- missing. But if the entire element is missing, unpickling
542 -- fails. 'xpOption' doesn't fix this because it would give us a
543 -- Maybe (Maybe MostLapsLeading). But we can use 'xpDefault' with a
544 -- default of (Nothing :: Maybe MostLapsLeading) to stick one in
545 -- there if unpicking a (Maybe MostLapsLeading) fails because
546 -- \<Most_Laps_Leading\> is missing.
548 -- Clear as mud, I know.
550 pickle_race_information :: PU AutoRacingResultsRaceInformationXml
551 pickle_race_information =
552 xpElem "Race_Information" $
553 xpWrap (from_tuple, to_tuple') $
554 xp11Tuple (-- I can't think of another way to get both the
555 -- TrackLength and its KPH attribute. So we shove them
556 -- both in a 2-tuple. This should probably be an embedded type!
557 xpElem "TrackLength" $
559 (xpAttr "KPH" xp_fracpart_only_double) )
560 (xpElem "Laps" xpInt)
561 (xpOption $ xpElem "AverageSpeedMPH" xpPrim)
562 (xpOption $ xpElem "AverageSpeedKPH" xpPrim)
563 (xpOption $ xpElem "AverageSpeed" xpPrim)
564 (xpOption $ xpElem "TimeOfRace" xpText)
565 (xpOption $ xpElem "MarginOfVictory" xpText)
566 (xpOption $ xpElem "Cautions" xpText)
567 (xpOption $ xpElem "LeadChanges" xpText)
568 (xpOption $ xpElem "LapLeaders" xpText)
569 (xpDefault Nothing pickle_most_laps_leading)
571 -- Derp. Since the first two are paired, we have to
572 -- manually unpack the bazillion arguments.
573 from_tuple ((x1,x2),x3,x4,x5,x6,x7,x8,x9,x10,x11,x12) =
574 AutoRacingResultsRaceInformationXml
575 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
577 -- And here we have to re-pair the first two.
578 to_tuple' m = ((xml_track_length m, xml_track_length_kph m),
580 xml_average_speed_mph m,
581 xml_average_speed_kph m,
584 xml_margin_of_victory m,
588 xml_most_laps_leading m)
594 -- | A list of all tests for this module.
596 auto_racing_results_tests :: TestTree
597 auto_racing_results_tests =
599 "AutoRacingResults tests"
600 [ test_on_delete_cascade,
601 test_pickle_of_unpickle_is_identity,
602 test_unpickle_succeeds ]
604 -- | If we unpickle something and then pickle it, we should wind up
605 -- with the same thing we started with. WARNING: success of this
606 -- test does not mean that unpickling succeeded.
608 test_pickle_of_unpickle_is_identity :: TestTree
609 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
610 [ check "pickle composed with unpickle is the identity"
611 "test/xml/AutoRacingResultsXML.xml",
613 check "pickle composed with unpickle is the identity (fractional KPH)"
614 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
616 check "pickle composed with unpickle is the identity (No Most_Laps_Leading)"
617 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml"]
619 check desc path = testCase desc $ do
620 (expected, actual) <- pickle_unpickle pickle_message path
625 -- | Make sure we can actually unpickle these things.
627 test_unpickle_succeeds :: TestTree
628 test_unpickle_succeeds = testGroup "unpickle tests"
629 [ check "unpickling succeeds"
630 "test/xml/AutoRacingResultsXML.xml",
632 check "unpickling succeeds (fractional KPH)"
633 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
635 check "unpickling succeeds (no Most_Laps_Leading)"
636 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml" ]
638 check desc path = testCase desc $ do
639 actual <- unpickleable path pickle_message
645 -- | Make sure everything gets deleted when we delete the top-level
648 test_on_delete_cascade :: TestTree
649 test_on_delete_cascade = testGroup "cascading delete tests"
650 [ check "deleting auto_racing_results deletes its children"
651 "test/xml/AutoRacingResultsXML.xml",
653 check "deleting auto_racing_results deletes its children (fractional KPH)"
654 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
656 check ("deleting auto_racing_results deletes its children " ++
657 "(No Most_Laps_Leading)")
658 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml" ]
660 check desc path = testCase desc $ do
661 results <- unsafe_unpickle path pickle_message
662 let a = undefined :: AutoRacingResults
663 let b = undefined :: AutoRacingResultsListing
664 let c = undefined :: AutoRacingResultsRaceInformation
666 actual <- withSqliteConn ":memory:" $ runDbConn $ do
667 runMigration silentMigrationLogger $ do
671 _ <- dbimport results
673 count_a <- countAll a
674 count_b <- countAll b
675 count_c <- countAll c
676 return $ sum [count_a, count_b, count_c]