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 (
60 import TSN.Codegen ( tsn_codegen_config )
61 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
64 xp_fracpart_only_double,
67 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
78 -- | The DTD to which this module corresponds. Used to invoke dbimport.
81 dtd = "AutoRacingResultsXML.dtd"
87 -- * AutoRacingResults/Message
89 -- | Database representation of a 'Message'. Comparatively, it lacks
90 -- the listings and race information since they are linked via a
93 data AutoRacingResults =
95 db_xml_file_id :: Int,
97 db_category :: String,
100 db_race_date :: UTCTime,
102 db_track_location :: String,
103 db_laps_remaining :: Int,
104 db_checkered_flag :: Bool,
105 db_time_stamp :: UTCTime }
110 -- | XML Representation of an 'AutoRacingResults'. It has the same
111 -- fields, but in addition contains the 'xml_listings' and
112 -- 'xml_race_information'.
116 xml_xml_file_id :: Int,
117 xml_heading :: String,
118 xml_category :: String,
121 xml_race_date :: UTCTime,
123 xml_track_location :: String,
124 xml_laps_remaining :: Int,
125 xml_checkered_flag :: Bool,
126 xml_listings :: [AutoRacingResultsListingXml],
127 xml_race_information :: AutoRacingResultsRaceInformationXml,
128 xml_time_stamp :: UTCTime }
132 instance ToDb Message where
133 -- | The database analogue of a 'Message' is a 'AutoRacingResults'.
135 type Db Message = AutoRacingResults
138 -- | The 'FromXml' instance for 'Message' is required for the
139 -- 'XmlImport' instance.
141 instance FromXml Message where
142 -- | To convert a 'Message' to an 'AutoRacingResults', we just drop
143 -- the 'xml_listings' and 'xml_race_information'.
145 from_xml Message{..} =
147 db_xml_file_id = xml_xml_file_id,
148 db_heading = xml_heading,
149 db_category = xml_category,
150 db_sport = xml_sport,
151 db_race_id = xml_race_id,
152 db_race_date = xml_race_date,
153 db_title = xml_title,
154 db_track_location = xml_track_location,
155 db_laps_remaining = xml_laps_remaining,
156 db_checkered_flag = xml_checkered_flag,
157 db_time_stamp = xml_time_stamp }
160 -- | This allows us to insert the XML representation 'Message'
163 instance XmlImport Message
166 -- * AutoRacingResultsListing/AutoRacingResultsListingXml
168 -- | Database representation of a \<Listing\> contained within a
171 data AutoRacingResultsListing =
172 AutoRacingResultsListing {
173 db_auto_racing_results_id :: DefaultKey AutoRacingResults,
174 db_finish_position :: Int,
175 db_starting_position :: Int,
176 db_car_number :: Int,
179 db_car_make :: String,
181 db_laps_completed :: Int,
182 db_laps_leading :: Int,
183 db_status :: Maybe String,
184 db_dnf :: Maybe Bool,
186 db_earnings :: Maybe Int }
189 -- | XML representation of a \<Listing\> contained within a
192 data AutoRacingResultsListingXml =
193 AutoRacingResultsListingXml {
194 xml_finish_position :: Int,
195 xml_starting_position :: Int,
196 xml_car_number :: Int,
197 xml_driver_id :: Int,
198 xml_driver :: String,
199 xml_car_make :: String,
201 xml_laps_completed :: Int,
202 xml_laps_leading :: Int,
203 xml_status :: Maybe String,
204 xml_dnf :: Maybe Bool,
205 xml_nc :: Maybe Bool,
206 xml_earnings :: Maybe Int }
210 instance ToDb AutoRacingResultsListingXml where
211 -- | The database analogue of an 'AutoRacingResultsListingXml' is
212 -- an 'AutoRacingResultsListing'.
214 type Db AutoRacingResultsListingXml = AutoRacingResultsListing
217 instance Child AutoRacingResultsListingXml where
218 -- | Each 'AutoRacingResultsListingXml' is contained in (i.e. has a
219 -- foreign key to) a 'AutoRacingResults'.
221 type Parent AutoRacingResultsListingXml = AutoRacingResults
224 instance FromXmlFk AutoRacingResultsListingXml where
225 -- | To convert an 'AutoRacingResultsListingXml' to an
226 -- 'AutoRacingResultsListing', we add the foreign key and copy
227 -- everything else verbatim.
229 from_xml_fk fk AutoRacingResultsListingXml{..} =
230 AutoRacingResultsListing {
231 db_auto_racing_results_id = fk,
232 db_finish_position = xml_finish_position,
233 db_starting_position = xml_starting_position,
234 db_car_number = xml_car_number,
235 db_driver_id = xml_driver_id,
236 db_driver = xml_driver,
237 db_car_make = xml_car_make,
238 db_points = xml_points,
239 db_laps_completed = xml_laps_completed,
240 db_laps_leading = xml_laps_leading,
241 db_status = xml_status,
244 db_earnings = xml_earnings }
247 -- | This allows us to insert the XML representation
248 -- 'AutoRacingResultsListingXml' directly.
250 instance XmlImportFk AutoRacingResultsListingXml
254 -- * AutoRacingResultsRaceInformation / AutoRacingResultsRaceInformationXml
256 -- | The \<Most_Laps_Leading\> child of \<Race_Information\> always
257 -- contains exactly three fields, so we just embed those three into
258 -- the 'AutoRacingResultsRaceInformation' type. We (pointlessly) use
259 -- the \"db_\" prefix since our field namer is going to strip of
260 -- everything before the first underscore.
262 data MostLapsLeading =
264 db_most_laps_leading_driver_id :: Int,
265 db_most_laps_leading_driver :: String,
266 db_most_laps_leading_number_of_laps :: Int }
267 deriving (Data, Eq, Show, Typeable)
270 -- | Database representation of a \<Race_Information\> contained
271 -- within a \<message\>.
273 data AutoRacingResultsRaceInformation =
274 AutoRacingResultsRaceInformation {
275 -- Note the apostrophe to disambiguate it from the
276 -- AutoRacingResultsListing field.
277 db_auto_racing_results_id' :: DefaultKey AutoRacingResults,
278 db_track_length :: String, -- ^ Usually a Double, but sometimes a String,
279 -- like \"1.25 miles\".
280 db_track_length_kph :: Double,
282 db_average_speed_mph :: Maybe Double,
283 db_average_speed_kph :: Maybe Double,
284 db_average_speed :: Maybe Double,
285 db_time_of_race :: Maybe String,
286 db_margin_of_victory :: Maybe String,
287 db_cautions :: Maybe String,
288 db_lead_changes :: Maybe String,
289 db_lap_leaders :: Maybe String,
290 db_most_laps_leading :: MostLapsLeading }
293 -- | XML representation of a \<Listing\> contained within a
296 data AutoRacingResultsRaceInformationXml =
297 AutoRacingResultsRaceInformationXml {
298 xml_track_length :: String,
299 xml_track_length_kph :: Double,
301 xml_average_speed_mph :: Maybe Double,
302 xml_average_speed_kph :: Maybe Double,
303 xml_average_speed :: Maybe Double,
304 xml_time_of_race :: Maybe String,
305 xml_margin_of_victory :: Maybe String,
306 xml_cautions :: Maybe String,
307 xml_lead_changes :: Maybe String,
308 xml_lap_leaders :: Maybe String,
309 xml_most_laps_leading :: MostLapsLeading }
313 instance ToDb AutoRacingResultsRaceInformationXml where
314 -- | The database analogue of an
315 -- 'AutoRacingResultsRaceInformationXml' is an
316 -- 'AutoRacingResultsRaceInformation'.
318 type Db AutoRacingResultsRaceInformationXml =
319 AutoRacingResultsRaceInformation
322 instance Child AutoRacingResultsRaceInformationXml where
323 -- | Each 'AutoRacingResultsRaceInformationXml' is contained in
324 -- (i.e. has a foreign key to) a 'AutoRacingResults'.
326 type Parent AutoRacingResultsRaceInformationXml = AutoRacingResults
329 instance FromXmlFk AutoRacingResultsRaceInformationXml where
330 -- | To convert an 'AutoRacingResultsRaceInformationXml' to an
331 -- 'AutoRacingResultsRaceInformartion', we add the foreign key and
332 -- copy everything else verbatim.
334 from_xml_fk fk AutoRacingResultsRaceInformationXml{..} =
335 AutoRacingResultsRaceInformation {
336 db_auto_racing_results_id' = fk,
337 db_track_length = xml_track_length,
338 db_track_length_kph = xml_track_length_kph,
340 db_average_speed_mph = xml_average_speed_mph,
341 db_average_speed_kph = xml_average_speed_kph,
342 db_average_speed = xml_average_speed,
343 db_time_of_race = xml_time_of_race,
344 db_margin_of_victory = xml_margin_of_victory,
345 db_cautions = xml_cautions,
346 db_lead_changes = xml_lead_changes,
347 db_lap_leaders = xml_lap_leaders,
348 db_most_laps_leading = xml_most_laps_leading }
351 -- | This allows us to insert the XML representation
352 -- 'AutoRacingResultsRaceInformationXml' directly.
354 instance XmlImportFk AutoRacingResultsRaceInformationXml
362 instance DbImport Message where
365 migrate (undefined :: AutoRacingResults)
366 migrate (undefined :: AutoRacingResultsListing)
367 migrate (undefined :: AutoRacingResultsRaceInformation)
369 -- | We insert the message, then use its ID to insert the listings
370 -- and race information.
372 msg_id <- insert_xml m
374 insert_xml_fk_ msg_id (xml_race_information m)
376 forM_ (xml_listings m) $ insert_xml_fk_ msg_id
378 return ImportSucceeded
382 mkPersist tsn_codegen_config [groundhog|
383 - entity: AutoRacingResults
384 dbName: auto_racing_results
386 - name: AutoRacingResults
388 - name: unique_auto_racing_results
390 # Prevent multiple imports of the same message.
391 fields: [db_xml_file_id]
394 - entity: AutoRacingResultsListing
395 dbName: auto_racing_results_listings
397 - name: AutoRacingResultsListing
399 - name: db_auto_racing_results_id
403 # Note the apostrophe in the foreign key. This is to disambiguate
404 # it from the AutoRacingResultsListing foreign key of the same name.
405 # We strip it out of the dbName.
406 - entity: AutoRacingResultsRaceInformation
407 dbName: auto_racing_results_race_information
409 - name: AutoRacingResultsRaceInformation
411 - name: db_auto_racing_results_id'
412 dbName: auto_racing_results_id
415 - name: db_most_laps_leading
417 - {name: most_laps_leading_driver_id,
418 dbName: most_laps_leading_driver_id}
419 - {name: most_laps_leading_driver,
420 dbName: most_laps_leading_driver}
422 - embedded: MostLapsLeading
424 - name: db_most_laps_leading_driver_id
425 dbName: most_laps_leading_driver_id
426 - name: db_most_laps_leading_driver
427 dbName: most_laps_leading_driver
428 - name: db_most_laps_leading_number_of_laps
429 dbName: most_laps_leading_number_of_laps
437 -- | Pickler for the \<Listing\>s contained within \<message\>s.
439 pickle_listing :: PU AutoRacingResultsListingXml
442 xpWrap (from_tuple, to_tuple) $
443 xp13Tuple (xpElem "FinishPosition" xpInt)
444 (xpElem "StartingPosition" xpInt)
445 (xpElem "CarNumber" xpInt)
446 (xpElem "DriverID" xpInt)
447 (xpElem "Driver" xpText)
448 (xpElem "CarMake" xpText)
449 (xpElem "Points" xpInt)
450 (xpElem "Laps_Completed" xpInt)
451 (xpElem "Laps_Leading" xpInt)
452 (xpElem "Status" $ xpOption xpText)
453 (xpOption $ xpElem "DNF" xpPrim)
454 (xpOption $ xpElem "NC" xpPrim)
455 (xpElem "Earnings" xp_earnings)
457 from_tuple = uncurryN AutoRacingResultsListingXml
458 to_tuple m = (xml_finish_position m,
459 xml_starting_position m,
465 xml_laps_completed m,
473 -- | Pickler for the top-level 'Message'.
475 pickle_message :: PU Message
478 xpWrap (from_tuple, to_tuple) $
479 xp13Tuple (xpElem "XML_File_ID" xpInt)
480 (xpElem "heading" xpText)
481 (xpElem "category" xpText)
482 (xpElem "sport" xpText)
483 (xpElem "RaceID" xpInt)
484 (xpElem "RaceDate" xp_datetime)
485 (xpElem "Title" xpText)
486 (xpElem "Track_Location" xpText)
487 (xpElem "Laps_Remaining" xpInt)
488 (xpElem "Checkered_Flag" xpPrim)
489 (xpList pickle_listing)
490 pickle_race_information
491 (xpElem "time_stamp" xp_time_stamp)
493 from_tuple = uncurryN Message
494 to_tuple m = (xml_xml_file_id m,
501 xml_track_location m,
502 xml_laps_remaining m,
503 xml_checkered_flag m,
505 xml_race_information m,
509 -- | Pickler for the \<Most_Laps_Leading\> child of a
510 -- \<Race_Information\>.
512 pickle_most_laps_leading :: PU MostLapsLeading
513 pickle_most_laps_leading =
514 xpElem "Most_Laps_Leading" $
515 xpWrap (from_tuple, to_tuple) $
516 xpTriple (xpElem "DriverID" xpInt)
517 (xpElem "Driver" xpText)
518 (xpElem "NumberOfLaps" xpInt)
520 from_tuple = uncurryN MostLapsLeading
521 to_tuple m = (db_most_laps_leading_driver_id m,
522 db_most_laps_leading_driver m,
523 db_most_laps_leading_number_of_laps m)
526 -- | Pickler for the \<Race_Information\> child of \<message\>.
528 pickle_race_information :: PU AutoRacingResultsRaceInformationXml
529 pickle_race_information =
530 xpElem "Race_Information" $
531 xpWrap (from_tuple, to_tuple) $
532 xp11Tuple (-- I can't think of another way to get both the
533 -- TrackLength and its KPH attribute. So we shove them
534 -- both in a 2-tuple. This should probably be an embedded type!
535 xpElem "TrackLength" $
537 (xpAttr "KPH" xp_fracpart_only_double) )
538 (xpElem "Laps" xpInt)
539 (xpOption $ xpElem "AverageSpeedMPH" xpPrim)
540 (xpOption $ xpElem "AverageSpeedKPH" xpPrim)
541 (xpOption $ xpElem "AverageSpeed" xpPrim)
542 (xpOption $ xpElem "TimeOfRace" xpText)
543 (xpOption $ xpElem "MarginOfVictory" xpText)
544 (xpOption $ xpElem "Cautions" xpText)
545 (xpOption $ xpElem "LeadChanges" xpText)
546 (xpOption $ xpElem "LapLeaders" xpText)
547 pickle_most_laps_leading
549 -- Derp. Since the first two are paired, we have to
550 -- manually unpack the bazillion arguments.
551 from_tuple ((x1,x2),x3,x4,x5,x6,x7,x8,x9,x10,x11,x12) =
552 AutoRacingResultsRaceInformationXml
553 x1 x2 x3 x4 x5 x6 x7 x8 x9 x10 x11 x12
555 -- And here we have to re-pair the first two.
556 to_tuple m = ((xml_track_length m, xml_track_length_kph m),
558 xml_average_speed_mph m,
559 xml_average_speed_kph m,
562 xml_margin_of_victory m,
566 xml_most_laps_leading m)
572 -- | A list of all tests for this module.
574 auto_racing_results_tests :: TestTree
575 auto_racing_results_tests =
577 "AutoRacingResults tests"
578 [ test_on_delete_cascade,
579 test_pickle_of_unpickle_is_identity,
580 test_unpickle_succeeds ]
582 -- | If we unpickle something and then pickle it, we should wind up
583 -- with the same thing we started with. WARNING: success of this
584 -- test does not mean that unpickling succeeded.
586 test_pickle_of_unpickle_is_identity :: TestTree
587 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
588 [ check "pickle composed with unpickle is the identity"
589 "test/xml/AutoRacingResultsXML.xml",
591 check "pickle composed with unpickle is the identity (fractional KPH)"
592 "test/xml/AutoRacingResultsXML-fractional-kph.xml" ]
594 check desc path = testCase desc $ do
595 (expected, actual) <- pickle_unpickle pickle_message path
600 -- | Make sure we can actually unpickle these things.
602 test_unpickle_succeeds :: TestTree
603 test_unpickle_succeeds = testGroup "unpickle tests"
604 [ check "unpickling succeeds"
605 "test/xml/AutoRacingResultsXML.xml",
607 check "unpickling succeeds (fractional KPH)"
608 "test/xml/AutoRacingResultsXML-fractional-kph.xml" ]
610 check desc path = testCase desc $ do
611 actual <- unpickleable path pickle_message
617 -- | Make sure everything gets deleted when we delete the top-level
620 test_on_delete_cascade :: TestTree
621 test_on_delete_cascade = testGroup "cascading delete tests"
622 [ check "deleting auto_racing_results deletes its children"
623 "test/xml/AutoRacingResultsXML.xml",
625 check "deleting auto_racing_results deletes its children (fractional KPH)"
626 "test/xml/AutoRacingResultsXML-fractional-kph.xml" ]
628 check desc path = testCase desc $ do
629 results <- unsafe_unpickle path pickle_message
630 let a = undefined :: AutoRacingResults
631 let b = undefined :: AutoRacingResultsListing
632 let c = undefined :: AutoRacingResultsRaceInformation
634 actual <- withSqliteConn ":memory:" $ runDbConn $ do
635 runMigration silentMigrationLogger $ do
639 _ <- dbimport results
641 count_a <- countAll a
642 count_b <- countAll b
643 count_c <- countAll c
644 return $ sum [count_a, count_b, count_c]