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
10 -- \"Auto_Racing_Schedule_XML.dtd\". There's a top-level
11 -- \<message\>, containing \<Listing\>s, containing \<RaceResults\>,
12 -- containing \<RaceResultsListing\>s.
14 module TSN.XML.AutoRacingSchedule (
18 auto_racing_schedule_tests,
19 -- * WARNING: these are private but exported to silence warnings
20 AutoRacingScheduleConstructor(..),
21 AutoRacingScheduleListingConstructor(..),
22 AutoRacingScheduleListingRaceResultRaceResultListingConstructor(..) )
26 import Control.Monad ( forM_ )
27 import Data.Time ( UTCTime(..) )
28 import Data.Tuple.Curry ( uncurryN )
29 import Database.Groundhog (
34 silentMigrationLogger )
35 import Database.Groundhog.Core ( DefaultKey )
36 import Database.Groundhog.Generic ( runDbConn )
37 import Database.Groundhog.Sqlite ( withSqliteConn )
38 import Database.Groundhog.TH (
41 import Test.Tasty ( TestTree, testGroup )
42 import Test.Tasty.HUnit ( (@?=), testCase )
43 import Text.XML.HXT.Core (
58 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
59 import TSN.Picklers ( xp_date_padded, xp_tba_time, xp_time_stamp )
60 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
197 instance Child AutoRacingScheduleListingXml where
198 -- | Each 'AutoRacingScheduleListingXml' is contained in (i.e. has a
199 -- foreign key to) a 'AutoRacingSchedule'.
201 type Parent AutoRacingScheduleListingXml = AutoRacingSchedule
204 instance FromXmlFk AutoRacingScheduleListingXml where
205 -- | To convert an 'AutoRacingScheduleListingXml' to an
206 -- 'AutoRacingScheduleListing', we add the foreign key and drop
207 -- the 'xml_race_results'. We also mash the date/time together
210 from_xml_fk fk AutoRacingScheduleListingXml{..} =
211 AutoRacingScheduleListing {
212 db_auto_racing_schedules_id = fk,
213 db_race_id = xml_race_id,
214 db_race_time = make_race_time xml_race_date xml_race_time,
215 db_race_name = xml_race_name,
216 db_track_name = xml_track_name,
217 db_location = xml_location,
218 db_tv_listing = xml_tv_listing,
220 db_track_length = xml_track_length }
222 -- | Make the database \"race time\" from the XML
223 -- date/time. Simply take the day part from one and the time
226 make_race_time d Nothing = d
227 make_race_time d (Just t) = UTCTime (utctDay d) (utctDayTime t)
230 -- | This allows us to insert the XML representation
231 -- 'AutoRacingScheduleListingXml' directly.
233 instance XmlImportFk AutoRacingScheduleListingXml
237 -- * AutoRacingScheduleListingRaceResult
239 -- | The XML representation of \<message\> -> \<Listing\> ->
240 -- \<RaceResults\>. This element serves only to contain
241 -- \<RaceResultsListing\>s, so we don't store the intermediate table
244 newtype AutoRacingScheduleListingRaceResult =
245 AutoRacingScheduleListingRaceResult {
246 xml_race_result_listing ::
247 [AutoRacingScheduleListingRaceResultRaceResultListingXml] }
251 -- * AutoRacingScheduleListingRaceResultRaceResultListing / AutoRacingScheduleListingRaceResultRaceResultListingXml
253 -- Sorry about the names yo.
256 -- | Database representation of \<RaceResultListing\> within
257 -- \<RaceResults\> within \<Listing\> within... \<message\>!
259 data AutoRacingScheduleListingRaceResultRaceResultListing =
260 AutoRacingScheduleListingRaceResultRaceResultListing {
261 db_auto_racing_schedules_listings_id ::
262 DefaultKey AutoRacingScheduleListing,
263 db_finish_position :: Int,
266 db_leading_laps :: Int,
267 db_listing_laps :: Int, -- ^ Avoid clash with race's \"laps\" field.
268 db_earnings :: String, -- ^ This should be an Int, but can have commas.
269 db_status :: Maybe String -- ^ They can be empty
273 -- | XML Representation of an
274 -- 'AutoRacingScheduleListingRaceResultRaceResultListing'.
276 data AutoRacingScheduleListingRaceResultRaceResultListingXml =
277 AutoRacingScheduleListingRaceResultRaceResultListingXml {
278 xml_finish_position :: Int,
279 xml_driver_id :: Int,
281 xml_leading_laps :: Int,
282 xml_listing_laps :: Int, -- ^ Avoid clash with race's \"laps\" field.
283 xml_earnings :: String, -- ^ Should be an 'Int', but can have commas.
284 xml_status :: Maybe String -- ^ They can be empty
289 instance ToDb AutoRacingScheduleListingRaceResultRaceResultListingXml where
290 -- | The database representation of an
291 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' is an
292 -- 'AutoRacingScheduleListingRaceResultRaceResultListing'.
294 type Db AutoRacingScheduleListingRaceResultRaceResultListingXml =
295 AutoRacingScheduleListingRaceResultRaceResultListing
298 instance Child AutoRacingScheduleListingRaceResultRaceResultListingXml where
299 -- | Each 'AutoRacingScheduleListingRaceResultRaceResultListingXml'
300 -- is contained in (i.e. has a foreign key to) an
301 -- 'AutoRacingScheduleListing'. We skip the intermediate
304 type Parent AutoRacingScheduleListingRaceResultRaceResultListingXml =
305 AutoRacingScheduleListing
308 instance FromXmlFk AutoRacingScheduleListingRaceResultRaceResultListingXml where
310 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' to an
311 -- 'AutoRacingScheduleListingRaceResultRaceResultListing', we just
312 -- add the foreign key to the parent 'AutoRacingScheduleListing'.
314 from_xml_fk fk AutoRacingScheduleListingRaceResultRaceResultListingXml{..} =
315 AutoRacingScheduleListingRaceResultRaceResultListing {
316 db_auto_racing_schedules_listings_id = fk,
317 db_finish_position = xml_finish_position,
318 db_driver_id = xml_driver_id,
320 db_leading_laps = xml_leading_laps,
321 db_listing_laps = xml_listing_laps,
322 db_earnings = xml_earnings,
323 db_status = xml_status }
326 -- | This allows us to insert the XML representation
327 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml'
330 instance XmlImportFk AutoRacingScheduleListingRaceResultRaceResultListingXml
337 instance DbImport Message where
340 migrate (undefined :: AutoRacingSchedule)
341 migrate (undefined :: AutoRacingScheduleListing)
343 :: AutoRacingScheduleListingRaceResultRaceResultListing)
346 -- | We insert the message, then use its ID to insert the listings,
347 -- using their IDs to insert the race result listings.
350 msg_id <- insert_xml m
352 forM_ (xml_listings m) $ \listing -> do
353 listing_id <- insert_xml_fk msg_id listing
355 mapM_ (insert_xml_fk_ listing_id) (result_listings listing)
357 return ImportSucceeded
360 mkPersist tsn_codegen_config [groundhog|
361 - entity: AutoRacingSchedule
362 dbName: auto_racing_schedules
364 - name: AutoRacingSchedule
366 - name: unique_auto_racing_schedules
368 # Prevent multiple imports of the same message.
369 fields: [db_xml_file_id]
371 - entity: AutoRacingScheduleListing
372 dbName: auto_racing_schedules_listings
374 - name: AutoRacingScheduleListing
376 - name: db_auto_racing_schedules_id
380 - entity: AutoRacingScheduleListingRaceResultRaceResultListing
381 dbName: auto_racing_schedules_listings_race_result_listings
383 - name: AutoRacingScheduleListingRaceResultRaceResultListing
385 - name: db_auto_racing_schedules_listings_id
396 -- | Pickler for the top-level 'Message'.
398 pickle_message :: PU Message
401 xpWrap (from_tuple, to_tuple) $
402 xp8Tuple (xpElem "XML_File_ID" xpInt)
403 (xpElem "heading" xpText)
404 (xpElem "category" xpText)
405 (xpElem "sport" xpText)
406 (xpElem "Title" xpText)
407 (xpElem "Complete_Through" xpText)
408 (xpList pickle_listing)
409 (xpElem "time_stamp" xp_time_stamp)
411 from_tuple = uncurryN Message
412 to_tuple m = (xml_xml_file_id m,
417 xml_complete_through m,
422 -- | Convert an 'AutoRacingScheduleListingXml' to/from XML.
424 pickle_listing :: PU AutoRacingScheduleListingXml
427 xpWrap (from_tuple, to_tuple) $
428 xp10Tuple (xpElem "RaceID" xpInt)
429 (xpElem "Race_Date" xp_date_padded)
430 (xpElem "Race_Time" xp_tba_time)
431 (xpElem "RaceName" xpText)
432 (xpElem "TrackName" xpText)
433 (xpElem "Location" xpText)
434 (xpElem "TV_Listing" $ xpOption xpText)
435 (xpElem "Laps" xpInt)
436 (xpElem "TrackLength" xpText)
437 (xpList pickle_race_results)
439 from_tuple = uncurryN AutoRacingScheduleListingXml
440 to_tuple m = (xml_race_id m,
452 -- | Convert an 'AutoRacingScheduleListingRaceResult' to/from XML.
454 pickle_race_results :: PU AutoRacingScheduleListingRaceResult
455 pickle_race_results =
456 xpElem "RaceResults" $
457 xpWrap (to_result, from_result) $
458 xpList pickle_race_results_listing
460 to_result = AutoRacingScheduleListingRaceResult
461 from_result = xml_race_result_listing
465 -- 'AutoRacingScheduleListingRaceResultRaceResultListingXml' to/from
468 pickle_race_results_listing ::
469 PU AutoRacingScheduleListingRaceResultRaceResultListingXml
470 pickle_race_results_listing =
471 xpElem "RaceResultsListing" $
472 xpWrap (from_tuple, to_tuple) $
473 xp7Tuple (xpElem "FinishPosition" xpInt)
474 (xpElem "DriverID" xpInt)
475 (xpElem "Name" xpText)
476 (xpElem "LeadingLaps" xpInt)
477 (xpElem "Laps" xpInt)
478 (xpElem "Earnings" xpText)
479 (xpElem "Status" (xpOption xpText))
482 uncurryN AutoRacingScheduleListingRaceResultRaceResultListingXml
484 to_tuple m = (xml_finish_position m,
497 -- | A list of all tests for this module.
499 auto_racing_schedule_tests :: TestTree
500 auto_racing_schedule_tests =
502 "AutoRacingSchedule tests"
503 [ test_on_delete_cascade,
504 test_pickle_of_unpickle_is_identity,
505 test_unpickle_succeeds ]
507 -- | If we unpickle something and then pickle it, we should wind up
508 -- with the same thing we started with. WARNING: success of this
509 -- test does not mean that unpickling succeeded.
511 test_pickle_of_unpickle_is_identity :: TestTree
512 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
513 [ check "pickle composed with unpickle is the identity"
514 "test/xml/Auto_Racing_Schedule_XML.xml",
516 check "pickle composed with unpickle is the identity (miles track length)"
517 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
519 check desc path = testCase desc $ do
520 (expected, actual) <- pickle_unpickle pickle_message path
524 -- | Make sure we can actually unpickle these things.
526 test_unpickle_succeeds :: TestTree
527 test_unpickle_succeeds = testGroup "unpickle tests"
528 [ check "unpickling succeeds"
529 "test/xml/Auto_Racing_Schedule_XML.xml",
531 check "unpickling succeeds (non-int team_id)"
532 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
534 check desc path = testCase desc $ do
535 actual <- unpickleable path pickle_message
540 -- | Make sure everything gets deleted when we delete the top-level
543 test_on_delete_cascade :: TestTree
544 test_on_delete_cascade = testGroup "cascading delete tests"
545 [ check "deleting auto_racing_schedules deletes its children"
546 "test/xml/Auto_Racing_Schedule_XML.xml" ,
548 check ("deleting auto_racing_schedules deletes its children " ++
549 "(miles track length)")
550 "test/xml/Auto_Racing_Schedule_XML-miles-track-length.xml" ]
552 check desc path = testCase desc $ do
553 sched <- unsafe_unpickle path pickle_message
554 let a = undefined :: AutoRacingSchedule
555 let b = undefined :: AutoRacingScheduleListing
556 let c = undefined :: AutoRacingScheduleListingRaceResultRaceResultListing
558 actual <- withSqliteConn ":memory:" $ runDbConn $ do
559 runMigration silentMigrationLogger $ do
565 count_a <- countAll a
566 count_b <- countAll b
567 count_c <- countAll c
568 return $ sum [count_a, count_b, count_c]