1 {-# LANGUAGE DeriveDataTypeable #-}
2 {-# LANGUAGE FlexibleInstances #-}
4 {-# LANGUAGE QuasiQuotes #-}
5 {-# LANGUAGE RecordWildCards #-}
6 {-# LANGUAGE StandaloneDeriving #-}
7 {-# LANGUAGE TemplateHaskell #-}
8 {-# LANGUAGE TypeFamilies #-}
10 -- | Parse TSN XML for the DTD
11 -- \"Auto_Racing_Schedule_XML.dtd\". There's a top-level
12 -- \<message\>, containing \<Listing\>s, containing \<RaceResults\>,
13 -- containing \<RaceResultsListing\>s.
15 module TSN.XML.AutoRacingSchedule (
19 auto_racing_schedule_tests,
20 -- * WARNING: these are private but exported to silence warnings
21 AutoRacingScheduleConstructor(..),
22 AutoRacingScheduleListingConstructor(..),
23 AutoRacingScheduleListingRaceResultRaceResultListingConstructor(..) )
27 import Control.Monad ( forM_ )
28 import Data.Time ( UTCTime(..) )
29 import Data.Tuple.Curry ( uncurryN )
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 (
59 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
60 import TSN.Picklers ( xp_date, xp_tba_time, xp_time_stamp )
61 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
71 -- | The DTD to which this module corresponds. Used to invoke dbimport.
74 dtd = "Auto_Racing_Schedule_XML.dtd"
80 -- * AutoRacingSchedule/Message
82 -- | Database representation of a 'Message'.
84 data AutoRacingSchedule =
86 db_xml_file_id :: Int,
88 db_category :: String,
91 db_complete_through :: String,
92 db_time_stamp :: UTCTime }
96 -- | XML Representation of an 'AutoRacingSchedule'.
100 xml_xml_file_id :: Int,
101 xml_heading :: String,
102 xml_category :: String,
105 xml_complete_through :: String,
106 xml_listings :: [AutoRacingScheduleListingXml],
107 xml_time_stamp :: UTCTime }
111 instance ToDb Message where
112 -- | The database analogue of a 'Message' is a 'AutoRacingSchedule'.
114 type Db Message = AutoRacingSchedule
117 -- | The 'FromXml' instance for 'Message' is required for the
118 -- 'XmlImport' instance.
120 instance FromXml Message where
121 -- | To convert a 'Message' to an 'AutoRacingSchedule', we just drop
122 -- the 'xml_listings'.
124 from_xml Message{..} =
126 db_xml_file_id = xml_xml_file_id,
127 db_heading = xml_heading,
128 db_category = xml_category,
129 db_sport = xml_sport,
130 db_title = xml_title,
131 db_complete_through = xml_complete_through,
132 db_time_stamp = xml_time_stamp }
135 -- | This allows us to insert the XML representation 'Message'
138 instance XmlImport Message
141 -- * AutoRacingScheduleListing/AutoRacingScheduleListingXml
143 -- | Database representation of a \<Listing\> contained within a
144 -- \<Message\>. We combine the race date/time into a single
145 -- race_time, drop the race results list, and add a foreign key to
148 data AutoRacingScheduleListing =
149 AutoRacingScheduleListing {
150 db_auto_racing_schedules_id :: DefaultKey AutoRacingSchedule,
152 db_race_time :: UTCTime,
153 db_race_name :: String,
154 db_track_name :: String,
155 db_location :: String,
156 db_tv_listing :: Maybe String,
158 db_track_length :: String -- ^ Sometimes the word "miles" shows up.
162 -- | XML representation of a \<Listing\> contained within a
165 data AutoRacingScheduleListingXml =
166 AutoRacingScheduleListingXml {
168 xml_race_date :: UTCTime,
169 xml_race_time :: Maybe UTCTime,
170 xml_race_name :: String,
171 xml_track_name :: String,
172 xml_location :: String,
173 xml_tv_listing :: Maybe String,
175 xml_track_length :: String, -- ^ Sometimes the word \"miles\" shows up,
176 -- so we can't do the right thing and use
178 xml_race_results :: [AutoRacingScheduleListingRaceResult] }
182 -- | Pseudo-accessor to get the race result listings out of a
183 -- 'AutoRacingScheduleListingXml'. A poor man's lens.
185 result_listings :: AutoRacingScheduleListingXml
186 -> [AutoRacingScheduleListingRaceResultRaceResultListingXml]
187 result_listings = (concatMap xml_race_result_listing) . xml_race_results
190 instance ToDb AutoRacingScheduleListingXml where
191 -- | The database analogue of an 'AutoRacingScheduleListingXml' is
192 -- an 'AutoRacingScheduleListing'.
194 type Db AutoRacingScheduleListingXml = AutoRacingScheduleListing
196 instance FromXmlFk AutoRacingScheduleListingXml where
197 -- | Each 'AutoRacingScheduleListingXml' is contained in (i.e. has a
198 -- foreign key to) a 'AutoRacingSchedule'.
200 type Parent AutoRacingScheduleListingXml = AutoRacingSchedule
202 -- | To convert an 'AutoRacingScheduleListingXml' to an
203 -- 'AutoRacingScheduleListing', we add the foreign key and drop
204 -- the 'xml_race_results'. We also mash the date/time together
207 from_xml_fk fk AutoRacingScheduleListingXml{..} =
208 AutoRacingScheduleListing {
209 db_auto_racing_schedules_id = fk,
210 db_race_id = xml_race_id,
211 db_race_time = make_race_time xml_race_date xml_race_time,
212 db_race_name = xml_race_name,
213 db_track_name = xml_track_name,
214 db_location = xml_location,
215 db_tv_listing = xml_tv_listing,
217 db_track_length = xml_track_length }
219 -- | Make the database \"race time\" from the XML
220 -- date/time. Simply take the day part from one and the time
223 make_race_time d Nothing = d
224 make_race_time d (Just t) = UTCTime (utctDay d) (utctDayTime t)
227 -- | This allows us to insert the XML representation
228 -- 'AutoRacingScheduleListingXml' directly.
230 instance XmlImportFk AutoRacingScheduleListingXml
234 -- * AutoRacingScheduleListingRaceResult
236 -- | The XML representation of \<message\> -> \<Listing\> ->
237 -- \<RaceResults\>. This element serves only to contain
238 -- \<RaceResultsListing\>s, so we don't store the intermediate table
241 newtype AutoRacingScheduleListingRaceResult =
242 AutoRacingScheduleListingRaceResult {
243 xml_race_result_listing ::
244 [AutoRacingScheduleListingRaceResultRaceResultListingXml] }
248 -- * AutoRacingScheduleListingRaceResultRaceResultListing /
249 -- AutoRacingScheduleListingRaceResultRaceResultListingXml
251 -- Sorry about the names yo.
254 -- | Database representation of \<RaceResultListing\> within
255 -- \<RaceResults\> within \<Listing\> within... \<message\>!
257 data AutoRacingScheduleListingRaceResultRaceResultListing =
258 AutoRacingScheduleListingRaceResultRaceResultListing {
259 db_auto_racing_schedules_listings_id ::
260 DefaultKey AutoRacingScheduleListing,
261 db_finish_position :: Int,
264 db_leading_laps :: Int,
265 db_listing_laps :: Int, -- ^ Avoid clash with race's \"laps\" field.
266 db_earnings :: String, -- ^ This should be an Int, but can have commas.
267 db_status :: String }
270 -- | XML Representation of an
271 -- 'AutoRacingScheduleListingRaceResultRaceResultListing'.
273 data AutoRacingScheduleListingRaceResultRaceResultListingXml =
274 AutoRacingScheduleListingRaceResultRaceResultListingXml {
275 xml_finish_position :: Int,
276 xml_driver_id :: Int,
278 xml_leading_laps :: Int,
279 xml_listing_laps :: Int, -- ^ Avoid clash with race's \"laps\" field.
280 xml_earnings :: String, -- ^ Should be an 'Int', but can have commas.
281 xml_status :: String }
285 instance ToDb AutoRacingScheduleListingRaceResultRaceResultListingXml where
286 -- | The database representation of an
287 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' is an
288 -- 'AutoRacingScheduleListingRaceResultRaceResultListing'.
290 type Db AutoRacingScheduleListingRaceResultRaceResultListingXml =
291 AutoRacingScheduleListingRaceResultRaceResultListing
294 instance FromXmlFk AutoRacingScheduleListingRaceResultRaceResultListingXml where
295 -- | Each 'AutoRacingScheduleListingRaceResultRaceResultListingXml'
296 -- is contained in (i.e. has a foreign key to) an
297 -- 'AutoRacingScheduleListing'. We skip the intermediate
300 type Parent AutoRacingScheduleListingRaceResultRaceResultListingXml =
301 AutoRacingScheduleListing
304 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' to an
305 -- 'AutoRacingScheduleListingRaceResultRaceResultListing', we just
306 -- add the foreign key to the parent 'AutoRacingScheduleListing'.
308 from_xml_fk fk AutoRacingScheduleListingRaceResultRaceResultListingXml{..} =
309 AutoRacingScheduleListingRaceResultRaceResultListing {
310 db_auto_racing_schedules_listings_id = fk,
311 db_finish_position = xml_finish_position,
312 db_driver_id = xml_driver_id,
314 db_leading_laps = xml_leading_laps,
315 db_listing_laps = xml_listing_laps,
316 db_earnings = xml_earnings,
317 db_status = xml_earnings }
320 -- | This allows us to insert the XML representation
321 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml'
324 instance XmlImportFk AutoRacingScheduleListingRaceResultRaceResultListingXml
331 instance DbImport Message where
334 migrate (undefined :: AutoRacingSchedule)
335 migrate (undefined :: AutoRacingScheduleListing)
337 :: AutoRacingScheduleListingRaceResultRaceResultListing)
340 -- | We insert the message, then use its ID to insert the listings,
341 -- using their IDs to insert the race result listings.
344 msg_id <- insert_xml m
346 forM_ (xml_listings m) $ \listing -> do
347 listing_id <- insert_xml_fk msg_id listing
349 mapM_ (insert_xml_fk_ listing_id) (result_listings listing)
351 return ImportSucceeded
354 mkPersist tsn_codegen_config [groundhog|
355 - entity: AutoRacingSchedule
356 dbName: auto_racing_schedules
358 - name: AutoRacingSchedule
360 - name: unique_auto_racing_schedule
362 # Prevent multiple imports of the same message.
363 fields: [db_xml_file_id]
365 - entity: AutoRacingScheduleListing
366 dbName: auto_racing_schedules_listings
368 - name: AutoRacingScheduleListing
370 - name: db_auto_racing_schedules_id
374 - entity: AutoRacingScheduleListingRaceResultRaceResultListing
375 dbName: auto_racing_schedules_listings_race_result_listings
377 - name: AutoRacingScheduleListingRaceResultRaceResultListing
379 - name: db_auto_racing_schedules_listings_id
390 -- | Pickler for the top-level 'Message'.
392 pickle_message :: PU Message
395 xpWrap (from_tuple, to_tuple) $
396 xp8Tuple (xpElem "XML_File_ID" xpInt)
397 (xpElem "heading" xpText)
398 (xpElem "category" xpText)
399 (xpElem "sport" xpText)
400 (xpElem "Title" xpText)
401 (xpElem "Complete_Through" xpText)
402 (xpList pickle_listing)
403 (xpElem "time_stamp" xp_time_stamp)
405 from_tuple = uncurryN Message
406 to_tuple m = (xml_xml_file_id m,
411 xml_complete_through m,
416 -- | Convert an 'AutoRacingScheduleListingXml' to/from XML.
418 pickle_listing :: PU AutoRacingScheduleListingXml
421 xpWrap (from_tuple, to_tuple) $
422 xp10Tuple (xpElem "RaceID" xpInt)
423 (xpElem "Race_Date" xp_date)
424 (xpElem "Race_Time" xp_tba_time)
425 (xpElem "RaceName" xpText)
426 (xpElem "TrackName" xpText)
427 (xpElem "Location" xpText)
428 (xpElem "TV_Listing" $ xpOption xpText)
429 (xpElem "Laps" xpInt)
430 (xpElem "TrackLength" xpText)
431 (xpList pickle_race_results)
433 from_tuple = uncurryN AutoRacingScheduleListingXml
434 to_tuple m = (xml_race_id m,
446 -- | Convert an 'AutoRacingScheduleListingRaceResult' to/from XML.
448 pickle_race_results :: PU AutoRacingScheduleListingRaceResult
449 pickle_race_results =
450 xpElem "RaceResults" $
451 xpWrap (to_result, from_result) $
452 xpList pickle_race_results_listing
454 to_result = AutoRacingScheduleListingRaceResult
455 from_result = xml_race_result_listing
459 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' to/from
462 pickle_race_results_listing ::
463 PU AutoRacingScheduleListingRaceResultRaceResultListingXml
464 pickle_race_results_listing =
465 xpElem "RaceResultsListing" $
466 xpWrap (from_tuple, to_tuple) $
467 xp7Tuple (xpElem "FinishPosition" xpInt)
468 (xpElem "DriverID" xpInt)
469 (xpElem "Name" xpText)
470 (xpElem "LeadingLaps" xpInt)
471 (xpElem "Laps" xpInt)
472 (xpElem "Earnings" xpText)
473 (xpElem "Status" xpText)
476 uncurryN AutoRacingScheduleListingRaceResultRaceResultListingXml
478 to_tuple m = (xml_finish_position m,
491 -- | A list of all tests for this module.
493 auto_racing_schedule_tests :: TestTree
494 auto_racing_schedule_tests =
496 "AutoRacingSchedule tests"
497 [ test_on_delete_cascade,
498 test_pickle_of_unpickle_is_identity,
499 test_unpickle_succeeds ]
501 -- | If we unpickle something and then pickle it, we should wind up
502 -- with the same thing we started with. WARNING: success of this
503 -- test does not mean that unpickling succeeded.
505 test_pickle_of_unpickle_is_identity :: TestTree
506 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
507 [ check "pickle composed with unpickle is the identity"
508 "test/xml/Auto_Racing_Schedule_XML.xml",
510 check "pickle composed with unpickle is the identity (miles track length)"
511 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
513 check desc path = testCase desc $ do
514 (expected, actual) <- pickle_unpickle pickle_message path
518 -- | Make sure we can actually unpickle these things.
520 test_unpickle_succeeds :: TestTree
521 test_unpickle_succeeds = testGroup "unpickle tests"
522 [ check "unpickling succeeds"
523 "test/xml/Auto_Racing_Schedule_XML.xml",
525 check "unpickling succeeds (non-int team_id)"
526 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
528 check desc path = testCase desc $ do
529 actual <- unpickleable path pickle_message
534 -- | Make sure everything gets deleted when we delete the top-level
537 test_on_delete_cascade :: TestTree
538 test_on_delete_cascade = testGroup "cascading delete tests"
539 [ check "deleting auto_racing_schedules deletes its children"
540 "test/xml/Auto_Racing_Schedule_XML.xml" ,
542 check ("deleting auto_racing_schedules deletes its children " ++
543 "(miles track length)")
544 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
546 check desc path = testCase desc $ do
547 sched <- unsafe_unpickle path pickle_message
548 let a = undefined :: AutoRacingSchedule
549 let b = undefined :: AutoRacingScheduleListing
550 let c = undefined :: AutoRacingScheduleListingRaceResultRaceResultListing
552 actual <- withSqliteConn ":memory:" $ runDbConn $ do
553 runMigration silentMigrationLogger $ do
559 count_a <- countAll a
560 count_b <- countAll b
561 count_c <- countAll c
562 return $ sum [count_a, count_b, count_c]