1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE QuasiQuotes #-}
5 {-# LANGUAGE RecordWildCards #-}
6 {-# LANGUAGE TemplateHaskell #-}
7 {-# LANGUAGE TypeFamilies #-}
9 -- | Parse TSN XML for the DTD \"AutoRacingResultsXML.dtd\". Each
10 -- \<message\> element contains a \<Race_Information\> and a bunch of
13 module TSN.XML.AutoRacingResults (
17 auto_racing_results_tests,
18 -- * WARNING: these are private but exported to silence warnings
19 AutoRacingResultsConstructor(..),
20 AutoRacingResultsListingConstructor(..),
21 AutoRacingResultsRaceInformationConstructor(..) )
25 import Control.Monad ( forM_ )
26 import Data.Data ( Data )
27 import Data.Time ( UTCTime(..) )
28 import Data.Tuple.Curry ( uncurryN )
29 import Data.Typeable ( Typeable )
30 import Database.Groundhog (
35 silentMigrationLogger )
36 import Database.Groundhog.Core ( DefaultKey )
37 import Database.Groundhog.Generic ( runDbConn )
38 import Database.Groundhog.Sqlite ( withSqliteConn )
39 import Database.Groundhog.TH (
42 import Test.Tasty ( TestTree, testGroup )
43 import Test.Tasty.HUnit ( (@?=), testCase )
44 import Text.XML.HXT.Core (
61 import TSN.Codegen ( tsn_codegen_config )
62 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
65 xp_fracpart_only_double,
68 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
79 -- | The DTD to which this module corresponds. Used to invoke dbimport.
82 dtd = "AutoRacingResultsXML.dtd"
88 -- * AutoRacingResults/Message
90 -- | Database representation of a 'Message'. Comparatively, it lacks
91 -- the listings and race information since they are linked via a
94 data AutoRacingResults =
96 db_xml_file_id :: Int,
98 db_category :: String,
101 db_race_date :: UTCTime,
103 db_track_location :: String,
104 db_laps_remaining :: Int,
105 db_checkered_flag :: Bool,
106 db_time_stamp :: UTCTime }
111 -- | XML Representation of an 'AutoRacingResults'. It has the same
112 -- fields, but in addition contains the 'xml_listings' and
113 -- 'xml_race_information'.
117 xml_xml_file_id :: Int,
118 xml_heading :: String,
119 xml_category :: String,
122 xml_race_date :: UTCTime,
124 xml_track_location :: String,
125 xml_laps_remaining :: Int,
126 xml_checkered_flag :: Bool,
127 xml_listings :: [AutoRacingResultsListingXml],
128 xml_race_information :: AutoRacingResultsRaceInformationXml,
129 xml_time_stamp :: UTCTime }
133 instance ToDb Message where
134 -- | The database analogue of a 'Message' is a 'AutoRacingResults'.
136 type Db Message = AutoRacingResults
139 -- | The 'FromXml' instance for 'Message' is required for the
140 -- 'XmlImport' instance.
142 instance FromXml Message where
143 -- | To convert a 'Message' to an 'AutoRacingResults', we just drop
144 -- the 'xml_listings' and 'xml_race_information'.
146 from_xml Message{..} =
148 db_xml_file_id = xml_xml_file_id,
149 db_heading = xml_heading,
150 db_category = xml_category,
151 db_sport = xml_sport,
152 db_race_id = xml_race_id,
153 db_race_date = xml_race_date,
154 db_title = xml_title,
155 db_track_location = xml_track_location,
156 db_laps_remaining = xml_laps_remaining,
157 db_checkered_flag = xml_checkered_flag,
158 db_time_stamp = xml_time_stamp }
161 -- | This allows us to insert the XML representation 'Message'
164 instance XmlImport Message
167 -- * AutoRacingResultsListing/AutoRacingResultsListingXml
169 -- | Database representation of a \<Listing\> contained within a
172 data AutoRacingResultsListing =
173 AutoRacingResultsListing {
174 db_auto_racing_results_id :: DefaultKey AutoRacingResults,
175 db_finish_position :: Int,
176 db_starting_position :: Int,
177 db_car_number :: Int,
180 db_car_make :: String,
182 db_laps_completed :: Int,
183 db_laps_leading :: Int,
184 db_status :: Maybe String,
185 db_dnf :: Maybe Bool,
187 db_earnings :: Maybe Int }
190 -- | XML representation of a \<Listing\> contained within a
193 data AutoRacingResultsListingXml =
194 AutoRacingResultsListingXml {
195 xml_finish_position :: Int,
196 xml_starting_position :: Int,
197 xml_car_number :: Int,
198 xml_driver_id :: Int,
199 xml_driver :: String,
200 xml_car_make :: String,
202 xml_laps_completed :: Int,
203 xml_laps_leading :: Int,
204 xml_status :: Maybe String,
205 xml_dnf :: Maybe Bool,
206 xml_nc :: Maybe Bool,
207 xml_earnings :: Maybe Int }
211 instance ToDb AutoRacingResultsListingXml where
212 -- | The database analogue of an 'AutoRacingResultsListingXml' is
213 -- an 'AutoRacingResultsListing'.
215 type Db AutoRacingResultsListingXml = AutoRacingResultsListing
218 instance Child AutoRacingResultsListingXml where
219 -- | Each 'AutoRacingResultsListingXml' is contained in (i.e. has a
220 -- foreign key to) a 'AutoRacingResults'.
222 type Parent AutoRacingResultsListingXml = AutoRacingResults
225 instance FromXmlFk AutoRacingResultsListingXml where
226 -- | To convert an 'AutoRacingResultsListingXml' to an
227 -- 'AutoRacingResultsListing', we add the foreign key and copy
228 -- everything else verbatim.
230 from_xml_fk fk AutoRacingResultsListingXml{..} =
231 AutoRacingResultsListing {
232 db_auto_racing_results_id = fk,
233 db_finish_position = xml_finish_position,
234 db_starting_position = xml_starting_position,
235 db_car_number = xml_car_number,
236 db_driver_id = xml_driver_id,
237 db_driver = xml_driver,
238 db_car_make = xml_car_make,
239 db_points = xml_points,
240 db_laps_completed = xml_laps_completed,
241 db_laps_leading = xml_laps_leading,
242 db_status = xml_status,
245 db_earnings = xml_earnings }
248 -- | This allows us to insert the XML representation
249 -- 'AutoRacingResultsListingXml' directly.
251 instance XmlImportFk AutoRacingResultsListingXml
255 -- * AutoRacingResultsRaceInformation / AutoRacingResultsRaceInformationXml
257 -- | The \<Most_Laps_Leading\> child of \<Race_Information\> always
258 -- contains exactly three fields, so we just embed those three into
259 -- the 'AutoRacingResultsRaceInformation' type. We (pointlessly) use
260 -- the \"db_\" prefix since our field namer is going to strip of
261 -- everything before the first underscore.
263 -- We make the three fields optional because the entire
264 -- \<Most_Laps_Leading\> is apparently optional (although it is
265 -- usually present). A 'Nothing' in the XML should get turned into
266 -- three 'Nothing's in the DB.
268 data MostLapsLeading =
270 db_most_laps_leading_driver_id :: Maybe Int,
271 db_most_laps_leading_driver :: Maybe String,
272 db_most_laps_leading_number_of_laps :: Maybe Int }
273 deriving (Data, Eq, Show, Typeable)
276 -- | Database representation of a \<Race_Information\> contained
277 -- within a \<message\>.
279 -- The 'db_most_laps_leading' field is not optional because when we
280 -- convert from our XML representation, a missing 'MostLapsLeading'
281 -- will be replaced with a 'MostLapsLeading' with three missing
284 data AutoRacingResultsRaceInformation =
285 AutoRacingResultsRaceInformation {
286 -- Note the apostrophe to disambiguate it from the
287 -- AutoRacingResultsListing field.
288 db_auto_racing_results_id' :: DefaultKey AutoRacingResults,
289 db_track_length :: String, -- ^ Usually a Double, but sometimes a String,
290 -- like \"1.25 miles\".
291 db_track_length_kph :: Double,
293 db_average_speed_mph :: Maybe Double,
294 db_average_speed_kph :: Maybe Double,
295 db_average_speed :: Maybe Double,
296 db_time_of_race :: Maybe String,
297 db_margin_of_victory :: Maybe String,
298 db_cautions :: Maybe String,
299 db_lead_changes :: Maybe String,
300 db_lap_leaders :: Maybe String,
301 db_most_laps_leading :: MostLapsLeading }
304 -- | XML representation of a \<Listing\> contained within a
307 data AutoRacingResultsRaceInformationXml =
308 AutoRacingResultsRaceInformationXml {
309 xml_track_length :: String,
310 xml_track_length_kph :: Double,
312 xml_average_speed_mph :: Maybe Double,
313 xml_average_speed_kph :: Maybe Double,
314 xml_average_speed :: Maybe Double,
315 xml_time_of_race :: Maybe String,
316 xml_margin_of_victory :: Maybe String,
317 xml_cautions :: Maybe String,
318 xml_lead_changes :: Maybe String,
319 xml_lap_leaders :: Maybe String,
320 xml_most_laps_leading :: Maybe MostLapsLeading }
324 instance ToDb AutoRacingResultsRaceInformationXml where
325 -- | The database analogue of an
326 -- 'AutoRacingResultsRaceInformationXml' is an
327 -- 'AutoRacingResultsRaceInformation'.
329 type Db AutoRacingResultsRaceInformationXml =
330 AutoRacingResultsRaceInformation
333 instance Child AutoRacingResultsRaceInformationXml where
334 -- | Each 'AutoRacingResultsRaceInformationXml' is contained in
335 -- (i.e. has a foreign key to) a 'AutoRacingResults'.
337 type Parent AutoRacingResultsRaceInformationXml = AutoRacingResults
340 instance FromXmlFk AutoRacingResultsRaceInformationXml where
341 -- | To convert an 'AutoRacingResultsRaceInformationXml' to an
342 -- 'AutoRacingResultsRaceInformartion', we add the foreign key and
343 -- copy everything else verbatim.
345 from_xml_fk fk AutoRacingResultsRaceInformationXml{..} =
346 AutoRacingResultsRaceInformation {
347 db_auto_racing_results_id' = fk,
348 db_track_length = xml_track_length,
349 db_track_length_kph = xml_track_length_kph,
351 db_average_speed_mph = xml_average_speed_mph,
352 db_average_speed_kph = xml_average_speed_kph,
353 db_average_speed = xml_average_speed,
354 db_time_of_race = xml_time_of_race,
355 db_margin_of_victory = xml_margin_of_victory,
356 db_cautions = xml_cautions,
357 db_lead_changes = xml_lead_changes,
358 db_lap_leaders = xml_lap_leaders,
359 db_most_laps_leading = most_laps_leading }
361 -- If we didn't get a \<Most_Laps_Leading\>, indicate that in
362 -- the database with an (embedded) 'MostLapsLeading' with three
365 case xml_most_laps_leading of
367 Nothing -> MostLapsLeading Nothing Nothing Nothing
369 -- | This allows us to insert the XML representation
370 -- 'AutoRacingResultsRaceInformationXml' directly.
372 instance XmlImportFk AutoRacingResultsRaceInformationXml
380 instance DbImport Message where
383 migrate (undefined :: AutoRacingResults)
384 migrate (undefined :: AutoRacingResultsListing)
385 migrate (undefined :: AutoRacingResultsRaceInformation)
387 -- | We insert the message, then use its ID to insert the listings
388 -- and race information.
390 msg_id <- insert_xml m
392 insert_xml_fk_ msg_id (xml_race_information m)
394 forM_ (xml_listings m) $ insert_xml_fk_ msg_id
396 return ImportSucceeded
400 mkPersist tsn_codegen_config [groundhog|
401 - entity: AutoRacingResults
402 dbName: auto_racing_results
404 - name: AutoRacingResults
406 - name: unique_auto_racing_results
408 # Prevent multiple imports of the same message.
409 fields: [db_xml_file_id]
412 - entity: AutoRacingResultsListing
413 dbName: auto_racing_results_listings
415 - name: AutoRacingResultsListing
417 - name: db_auto_racing_results_id
421 # Note the apostrophe in the foreign key. This is to disambiguate
422 # it from the AutoRacingResultsListing foreign key of the same name.
423 # We strip it out of the dbName.
424 - entity: AutoRacingResultsRaceInformation
425 dbName: auto_racing_results_race_information
427 - name: AutoRacingResultsRaceInformation
429 - name: db_auto_racing_results_id'
430 dbName: auto_racing_results_id
433 - name: db_most_laps_leading
435 - {name: most_laps_leading_driver_id,
436 dbName: most_laps_leading_driver_id}
437 - {name: most_laps_leading_driver,
438 dbName: most_laps_leading_driver}
440 - embedded: MostLapsLeading
442 - name: db_most_laps_leading_driver_id
443 dbName: most_laps_leading_driver_id
444 - name: db_most_laps_leading_driver
445 dbName: most_laps_leading_driver
446 - name: db_most_laps_leading_number_of_laps
447 dbName: most_laps_leading_number_of_laps
455 -- | Pickler for the \<Listing\>s contained within \<message\>s.
457 pickle_listing :: PU AutoRacingResultsListingXml
460 xpWrap (from_tuple, to_tuple) $
461 xp13Tuple (xpElem "FinishPosition" xpInt)
462 (xpElem "StartingPosition" xpInt)
463 (xpElem "CarNumber" xpInt)
464 (xpElem "DriverID" xpInt)
465 (xpElem "Driver" xpText)
466 (xpElem "CarMake" xpText)
467 (xpElem "Points" xpInt)
468 (xpElem "Laps_Completed" xpInt)
469 (xpElem "Laps_Leading" xpInt)
470 (xpElem "Status" $ xpOption xpText)
471 (xpOption $ xpElem "DNF" xpPrim)
472 (xpOption $ xpElem "NC" xpPrim)
473 (xpElem "Earnings" xp_earnings)
475 from_tuple = uncurryN AutoRacingResultsListingXml
476 to_tuple m = (xml_finish_position m,
477 xml_starting_position m,
483 xml_laps_completed m,
491 -- | Pickler for the top-level 'Message'.
493 pickle_message :: PU Message
496 xpWrap (from_tuple, to_tuple) $
497 xp13Tuple (xpElem "XML_File_ID" xpInt)
498 (xpElem "heading" xpText)
499 (xpElem "category" xpText)
500 (xpElem "sport" xpText)
501 (xpElem "RaceID" xpInt)
502 (xpElem "RaceDate" xp_datetime)
503 (xpElem "Title" xpText)
504 (xpElem "Track_Location" xpText)
505 (xpElem "Laps_Remaining" xpInt)
506 (xpElem "Checkered_Flag" xpPrim)
507 (xpList pickle_listing)
508 pickle_race_information
509 (xpElem "time_stamp" xp_time_stamp)
511 from_tuple = uncurryN Message
512 to_tuple m = (xml_xml_file_id m,
519 xml_track_location m,
520 xml_laps_remaining m,
521 xml_checkered_flag m,
523 xml_race_information m,
527 -- | Pickler for the \<Most_Laps_Leading\> child of a
528 -- \<Race_Information\>. This is complicated by the fact that the
529 -- three fields we're trying to parse are not actually optional;
530 -- only the entire \<Most_Laps_Leading\> is. So we always wrap what
531 -- we parse in a 'Just', and when converting from the DB to XML,
532 -- we'll drop the entire element if any of its fields are missing
533 -- (which they never should be).
535 pickle_most_laps_leading :: PU (Maybe MostLapsLeading)
536 pickle_most_laps_leading =
537 xpElem "Most_Laps_Leading" $
538 xpWrap (from_tuple, to_tuple) $
539 xpTriple (xpOption $ xpElem "DriverID" xpInt)
540 (xpOption $ xpElem "Driver" xpText)
541 (xpOption $ xpElem "NumberOfLaps" xpInt)
543 from_tuple :: (Maybe Int, Maybe String, Maybe Int) -> Maybe MostLapsLeading
544 from_tuple (Just x, Just y, Just z) =
545 Just $ MostLapsLeading (Just x) (Just y) (Just z)
546 from_tuple _ = Nothing
548 -- Sure had to go out of my way to avoid the warnings about unused
549 -- db_most_laps_foo fields here.
550 to_tuple :: Maybe MostLapsLeading -> (Maybe Int, Maybe String, Maybe Int)
551 to_tuple Nothing = (Nothing, Nothing, Nothing)
552 to_tuple (Just (MostLapsLeading Nothing _ _)) = (Nothing, Nothing, Nothing)
553 to_tuple (Just (MostLapsLeading _ Nothing _)) = (Nothing, Nothing, Nothing)
554 to_tuple (Just (MostLapsLeading _ _ Nothing)) = (Nothing, Nothing, Nothing)
555 to_tuple (Just m) = (db_most_laps_leading_driver_id m,
556 db_most_laps_leading_driver m,
557 db_most_laps_leading_number_of_laps m)
560 -- | Pickler for the \<Race_Information\> child of \<message\>.
562 -- There's so much voodoo going on here. We have a double-layered
563 -- Maybe on top of the MostLapsLeading. When unpickling, we return a
564 -- Nothing (i.e. a Maybe MostLapsLeading) if any of its fields are
565 -- missing. But if the entire element is missing, unpickling
566 -- fails. 'xpOption' doesn't fix this because it would give us a
567 -- Maybe (Maybe MostLapsLeading). But we can use 'xpDefault' with a
568 -- default of (Nothing :: Maybe MostLapsLeading) to stick one in
569 -- there if unpicking a (Maybe MostLapsLeading) fails because
570 -- \<Most_Laps_Leading\> is missing.
572 -- Clear as mud, I know.
574 pickle_race_information :: PU AutoRacingResultsRaceInformationXml
575 pickle_race_information =
576 xpElem "Race_Information" $
577 xpWrap (from_tuple, to_tuple) $
578 xp11Tuple (-- I can't think of another way to get both the
579 -- TrackLength and its KPH attribute. So we shove them
580 -- both in a 2-tuple. This should probably be an embedded type!
581 xpElem "TrackLength" $
583 (xpAttr "KPH" xp_fracpart_only_double) )
584 (xpElem "Laps" xpInt)
585 (xpOption $ xpElem "AverageSpeedMPH" xpPrim)
586 (xpOption $ xpElem "AverageSpeedKPH" xpPrim)
587 (xpOption $ xpElem "AverageSpeed" xpPrim)
588 (xpOption $ xpElem "TimeOfRace" xpText)
589 (xpOption $ xpElem "MarginOfVictory" xpText)
590 (xpOption $ xpElem "Cautions" xpText)
591 (xpOption $ xpElem "LeadChanges" xpText)
592 (xpOption $ xpElem "LapLeaders" xpText)
593 (xpDefault Nothing pickle_most_laps_leading)
595 -- Derp. Since the first two are paired, we have to
596 -- manually unpack the bazillion arguments.
597 from_tuple ((x1,x2),x3,x4,x5,x6,x7,x8,x9,x10,x11,x12) =
598 AutoRacingResultsRaceInformationXml
599 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
601 -- And here we have to re-pair the first two.
602 to_tuple m = ((xml_track_length m, xml_track_length_kph m),
604 xml_average_speed_mph m,
605 xml_average_speed_kph m,
608 xml_margin_of_victory m,
612 xml_most_laps_leading m)
618 -- | A list of all tests for this module.
620 auto_racing_results_tests :: TestTree
621 auto_racing_results_tests =
623 "AutoRacingResults tests"
624 [ test_on_delete_cascade,
625 test_pickle_of_unpickle_is_identity,
626 test_unpickle_succeeds ]
628 -- | If we unpickle something and then pickle it, we should wind up
629 -- with the same thing we started with. WARNING: success of this
630 -- test does not mean that unpickling succeeded.
632 test_pickle_of_unpickle_is_identity :: TestTree
633 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
634 [ check "pickle composed with unpickle is the identity"
635 "test/xml/AutoRacingResultsXML.xml",
637 check "pickle composed with unpickle is the identity (fractional KPH)"
638 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
640 check "pickle composed with unpickle is the identity (No Most_Laps_Leading)"
641 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml"]
643 check desc path = testCase desc $ do
644 (expected, actual) <- pickle_unpickle pickle_message path
649 -- | Make sure we can actually unpickle these things.
651 test_unpickle_succeeds :: TestTree
652 test_unpickle_succeeds = testGroup "unpickle tests"
653 [ check "unpickling succeeds"
654 "test/xml/AutoRacingResultsXML.xml",
656 check "unpickling succeeds (fractional KPH)"
657 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
659 check "unpickling succeeds (no Most_Laps_Leading)"
660 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml" ]
662 check desc path = testCase desc $ do
663 actual <- unpickleable path pickle_message
669 -- | Make sure everything gets deleted when we delete the top-level
672 test_on_delete_cascade :: TestTree
673 test_on_delete_cascade = testGroup "cascading delete tests"
674 [ check "deleting auto_racing_results deletes its children"
675 "test/xml/AutoRacingResultsXML.xml",
677 check "deleting auto_racing_results deletes its children (fractional KPH)"
678 "test/xml/AutoRacingResultsXML-fractional-kph.xml",
680 check ("deleting auto_racing_results deletes its children " ++
681 "(No Most_Laps_Leading)")
682 "test/xml/AutoRacingResultsXML-no-most-laps-leading.xml" ]
684 check desc path = testCase desc $ do
685 results <- unsafe_unpickle path pickle_message
686 let a = undefined :: AutoRacingResults
687 let b = undefined :: AutoRacingResultsListing
688 let c = undefined :: AutoRacingResultsRaceInformation
690 actual <- withSqliteConn ":memory:" $ runDbConn $ do
691 runMigration silentMigrationLogger $ do
695 _ <- dbimport results
697 count_a <- countAll a
698 count_b <- countAll b
699 count_c <- countAll c
700 return $ sum [count_a, count_b, count_c]