1 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE QuasiQuotes #-}
4 {-# LANGUAGE RecordWildCards #-}
5 {-# LANGUAGE TemplateHaskell #-}
6 {-# LANGUAGE TypeFamilies #-}
8 -- | Parse TSN XML for the DTD \"weatherxml.dtd\". Each document
9 -- contains a bunch of forecasts, which each contain zero or more
10 -- leagues, which in turn (each) contain a bunch of listings.
12 module TSN.XML.Weather (
18 -- * WARNING: these are private but exported to silence warnings
19 WeatherConstructor(..),
20 WeatherDetailedWeatherListingItemConstructor(..),
21 WeatherForecastConstructor(..),
22 WeatherForecastListingConstructor(..) )
26 import Control.Monad ( forM_ )
27 import Data.Time ( UTCTime )
28 import Data.Tuple.Curry ( uncurryN )
29 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 (
67 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
68 import TSN.Picklers ( xp_datetime, xp_gamedate, xp_time_stamp )
69 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
82 -- | The DTD to which this module corresponds. Used to invoke dbimport.
85 dtd = "weatherxml.dtd"
92 -- * WeatherForecastListing/WeatherForecastListingXml
94 -- | XML representation of a weather forecast listing.
96 data WeatherForecastListingXml =
97 WeatherForecastListingXml {
99 xml_weather :: String }
103 -- | Database representation of a weather forecast listing. The
104 -- 'db_league_name' field should come from the containing \<league\>
105 -- element which is not stored in the database.
107 data WeatherForecastListing =
108 WeatherForecastListing {
109 db_weather_forecasts_id :: DefaultKey WeatherForecast,
110 db_league_name :: Maybe String,
112 db_weather :: String }
115 -- | We don't make 'WeatherForecastListingXml' an instance of
116 -- 'FromXmlFk' because it needs some additional information, namely
117 -- the league name from its containing \<league\> element.
119 -- When supplied with a forecast id and a league name, this will
120 -- turn an XML listing into a database one.
122 from_xml_fk_league :: DefaultKey WeatherForecast
124 -> WeatherForecastListingXml
125 -> WeatherForecastListing
126 from_xml_fk_league fk ln WeatherForecastListingXml{..} =
127 WeatherForecastListing {
128 db_weather_forecasts_id = fk,
130 db_teams = xml_teams,
131 db_weather = xml_weather }
136 -- | XML representation of a league, as they appear in the weather
137 -- documents. There is no associated database representation because
138 -- the league element really adds no information besides its own
139 -- (usually empty) name. The leagues contain listings, so we
140 -- associate the league name with each listing instead.
144 league_name :: Maybe String,
145 listings :: [WeatherForecastListingXml] }
149 -- * WeatherForecast/WeatherForecastXml
151 -- | Database representation of a weather forecast.
153 data WeatherForecast =
155 db_weather_id :: DefaultKey Weather,
156 db_game_date :: UTCTime }
159 -- | XML representation of a weather forecast.
161 data WeatherForecastXml =
163 xml_game_date :: UTCTime,
164 xml_leagues :: [WeatherLeague] }
168 instance ToDb WeatherForecastXml where
169 -- | The database representation of a 'WeatherForecastXml' is a
170 -- 'WeatherForecast'.
172 type Db WeatherForecastXml = WeatherForecast
175 instance Child WeatherForecastXml where
176 -- | The database type containing a 'WeatherForecastXml' is
178 type Parent WeatherForecastXml = Weather
181 instance FromXmlFk WeatherForecastXml where
183 -- | To convert a 'WeatherForecastXml' into a 'WeatherForecast', we
184 -- just copy everything verbatim.
186 from_xml_fk fk WeatherForecastXml{..} =
189 db_game_date = xml_game_date }
192 -- | This allows us to call 'insert_xml' on an 'WeatherForecastXml'
193 -- without first converting it to the database representation.
195 instance XmlImportFk WeatherForecastXml
197 -- * WeatherDetailedWeatherXml
199 -- | XML Representation of a \<Detailed_Weather\>, which just contains
200 -- a bunch iof \<DW_Listing\>s. There is no associated database type
201 -- since these don't really contain any information.
203 data WeatherDetailedWeatherXml =
204 WeatherDetailedWeatherXml {
205 xml_detailed_listings :: [WeatherDetailedWeatherListingXml] }
209 -- * WeatherDetailedWeatherXml
211 -- | XML Representation of a \<DW_Listing\>. The sport and sport code
212 -- come as attributes, but then these just contain a bunch of
213 -- \<Item\>s. There is no associated database type since these don't
214 -- contain much information. The sport we already know from the
215 -- \<message\>, while the sport code is ignored since it's already
216 -- present in each \<Item\>s.
218 data WeatherDetailedWeatherListingXml =
219 WeatherDetailedWeatherListingXml {
220 xml_dtl_listing_sport :: String,
221 xml_dtl_listing_sport_code :: String,
222 xml_items :: [WeatherDetailedWeatherListingItemXml] }
225 -- * WeatherDetailedWeatherListingItem / WeatherDetailedWeatherListingItemXml
227 -- | Database representation of a detailed weather item. The away/home
228 -- teams don't use the representation in "TSN.Team" because all
229 -- we're given is a name, and a team id is required for "TSN.Team".
231 -- We also drop the sport name, because it's given in the parent
234 data WeatherDetailedWeatherListingItem =
235 WeatherDetailedWeatherListingItem {
236 db_dtl_weather_id :: DefaultKey Weather, -- ^ Avoid name collision by
237 -- using \"dtl\" prefix.
238 db_sport_code :: String,
240 db_dtl_game_date :: UTCTime, -- ^ Avoid name clash with \"dtl\" prefix
241 db_away_team :: String,
242 db_home_team :: String,
243 db_weather_type :: Int,
244 db_description :: String,
245 db_temp_adjust :: String,
246 db_temperature :: Int }
249 -- | XML representation of a detailed weather item. Same as the
250 -- database representation, only without the foreign key and the
251 -- sport name that comes from the containing listing.
252 data WeatherDetailedWeatherListingItemXml =
253 WeatherDetailedWeatherListingItemXml {
254 xml_sport_code :: String,
256 xml_dtl_game_date :: UTCTime,
257 xml_away_team :: String,
258 xml_home_team :: String,
259 xml_weather_type :: Int,
260 xml_description :: String,
261 xml_temp_adjust :: String,
262 xml_temperature :: Int }
266 instance ToDb WeatherDetailedWeatherListingItemXml where
267 -- | Our database analogue is a 'WeatherDetailedWeatherListingItem'.
268 type Db WeatherDetailedWeatherListingItemXml =
269 WeatherDetailedWeatherListingItem
271 instance Child WeatherDetailedWeatherListingItemXml where
272 -- | We skip two levels of containers and say that the items belong
273 -- to the top-level 'Weather'.
274 type Parent WeatherDetailedWeatherListingItemXml = Weather
276 instance FromXmlFk WeatherDetailedWeatherListingItemXml where
277 -- | To convert from the XML to database representation, we simply
278 -- add the foreign key (to Weather) and copy the rest of the fields.
279 from_xml_fk fk WeatherDetailedWeatherListingItemXml{..} =
280 WeatherDetailedWeatherListingItem {
281 db_dtl_weather_id = fk,
282 db_sport_code = xml_sport_code,
283 db_game_id = xml_game_id,
284 db_dtl_game_date = xml_dtl_game_date,
285 db_away_team = xml_away_team,
286 db_home_team = xml_home_team,
287 db_weather_type = xml_weather_type,
288 db_description = xml_description,
289 db_temp_adjust = xml_temp_adjust,
290 db_temperature = xml_temperature }
292 -- | This allows us to insert the XML representation directly without
293 -- having to do the manual XML -\> DB conversion.
295 instance XmlImportFk WeatherDetailedWeatherListingItemXml
299 -- | The database representation of a weather message. We don't
300 -- contain the forecasts or the detailed weather since those are
301 -- foreigned-keyed to us.
305 db_xml_file_id :: Int,
308 db_time_stamp :: UTCTime }
311 -- | The XML representation of a weather message.
315 xml_xml_file_id :: Int,
316 xml_heading :: String,
317 xml_category :: String,
320 xml_forecasts :: [WeatherForecastXml],
321 xml_detailed_weather :: Maybe WeatherDetailedWeatherXml,
322 xml_time_stamp :: UTCTime }
325 instance ToDb Message where
326 -- | The database representation of 'Message' is 'Weather'.
328 type Db Message = Weather
330 instance FromXml Message where
331 -- | To get a 'Weather' from a 'Message', we drop a bunch of
334 from_xml Message{..} =
336 db_xml_file_id = xml_xml_file_id,
337 db_sport = xml_sport,
338 db_title = xml_title,
339 db_time_stamp = xml_time_stamp }
341 -- | This allows us to insert the XML representation 'Message'
344 instance XmlImport Message
351 mkPersist tsn_codegen_config [groundhog|
356 - name: unique_weather
358 # Prevent multiple imports of the same message.
359 fields: [db_xml_file_id]
361 - entity: WeatherForecast
362 dbName: weather_forecasts
364 - name: WeatherForecast
366 - name: db_weather_id
370 - entity: WeatherForecastListing
371 dbName: weather_forecast_listings
373 - name: WeatherForecastListing
375 - name: db_weather_forecasts_id
379 # We rename the two fields that needed a "dtl" prefix to avoid a name clash.
380 - entity: WeatherDetailedWeatherListingItem
381 dbName: weather_detailed_items
383 - name: WeatherDetailedWeatherListingItem
385 - name: db_dtl_weather_id
389 - name: db_dtl_game_date
396 -- | There are two different types of documents that claim to be
397 -- \"weatherxml.dtd\". The first, more common type has listings
398 -- within forecasts. The second type has forecasts within
399 -- listings. Clearly we can't parse both of these using the same
402 -- For now we're simply punting on the issue and refusing to parse
403 -- the second type. This will check the given @xmltree@ to see if
404 -- there are any forecasts contained within listings. If there are,
405 -- then it's the second type that we don't know what to do with.
407 is_type1 :: XmlTree -> Bool
413 parse :: XmlTree -> [XmlTree]
414 parse = runLA $ hasName "/"
417 /> hasName "forecast"
419 elements = parse xmltree
422 instance DbImport Message where
425 migrate (undefined :: Weather)
426 migrate (undefined :: WeatherForecast)
427 migrate (undefined :: WeatherForecastListing)
428 migrate (undefined :: WeatherDetailedWeatherListingItem)
431 -- First we insert the top-level weather record.
432 weather_id <- insert_xml m
434 -- Next insert all of the forecasts, one at a time.
435 forM_ (xml_forecasts m) $ \forecast -> do
436 forecast_id <- insert_xml_fk weather_id forecast
438 -- With the forecast id in hand, loop through this forecast's
440 forM_ (xml_leagues forecast) $ \league -> do
441 -- Construct the function that converts an XML listing to a
443 let todb = from_xml_fk_league forecast_id (league_name league)
445 -- Now use it to convert all of the XML listings.
446 let db_listings = map todb (listings league)
448 -- And finally, insert those DB listings.
449 mapM_ insert_ db_listings
451 return ImportSucceeded
458 -- | Pickler to convert a 'WeatherForecastListingXml' to/from XML.
460 pickle_listing :: PU WeatherForecastListingXml
463 xpWrap (from_pair, to_pair) $
465 (xpElem "teams" xpText)
466 (xpElem "weather" xpText)
468 from_pair = uncurry WeatherForecastListingXml
469 to_pair WeatherForecastListingXml{..} = (xml_teams, xml_weather)
472 -- | Pickler to convert a 'WeatherLeague' to/from XML.
474 pickle_league :: PU WeatherLeague
477 xpWrap (from_pair, to_pair) $
479 (xpAttr "name" $ xpOption xpText)
480 (xpList pickle_listing)
482 from_pair = uncurry WeatherLeague
483 to_pair WeatherLeague{..} = (league_name, listings)
486 -- | Pickler to convert a 'WeatherForecastXml' to/from XML.
488 pickle_forecast :: PU WeatherForecastXml
491 xpWrap (from_pair, to_pair) $
493 (xpAttr "gamedate" xp_gamedate)
494 (xpList pickle_league)
496 from_pair = uncurry WeatherForecastXml
497 to_pair WeatherForecastXml{..} = (xml_game_date,
502 -- | (Un)pickle a 'WeatherDetailedWeatherListingItemXml'.
504 pickle_item :: PU WeatherDetailedWeatherListingItemXml
507 xpWrap (from_tuple, to_tuple) $
508 xp9Tuple (xpElem "Sportcode" xpText)
509 (xpElem "GameID" xpInt)
510 (xpElem "Gamedate" xp_datetime)
511 (xpElem "AwayTeam" xpText)
512 (xpElem "HomeTeam" xpText)
513 (xpElem "WeatherType" xpInt)
514 (xpElem "Description" xpText)
515 (xpElem "TempAdjust" xpText)
516 (xpElem "Temperature" xpInt)
518 from_tuple = uncurryN WeatherDetailedWeatherListingItemXml
519 to_tuple w = (xml_sport_code w,
530 -- | (Un)pickle a 'WeatherDetailedWeatherListingXml'.
532 pickle_dw_listing :: PU WeatherDetailedWeatherListingXml
534 xpElem "DW_Listing" $
535 xpWrap (from_tuple, to_tuple) $
536 xpTriple (xpAttr "SportCode" xpText)
537 (xpAttr "Sport" xpText)
540 from_tuple = uncurryN WeatherDetailedWeatherListingXml
541 to_tuple w = (xml_dtl_listing_sport w,
542 xml_dtl_listing_sport_code w,
546 -- | (Un)pickle a 'WeatherDetailedWeatherXml'
548 pickle_detailed_weather :: PU WeatherDetailedWeatherXml
549 pickle_detailed_weather =
550 xpElem "Detailed_Weather" $
551 xpWrap (WeatherDetailedWeatherXml, xml_detailed_listings)
552 (xpList pickle_dw_listing)
555 -- | Pickler to convert a 'Message' to/from XML.
557 pickle_message :: PU Message
560 xpWrap (from_tuple, to_tuple) $
562 (xpElem "XML_File_ID" xpInt)
563 (xpElem "heading" xpText)
564 (xpElem "category" xpText)
565 (xpElem "sport" xpText)
566 (xpElem "title" xpText)
567 (xpList pickle_forecast)
568 (xpOption pickle_detailed_weather)
569 (xpElem "time_stamp" xp_time_stamp)
571 from_tuple = uncurryN Message
572 to_tuple Message{..} = (xml_xml_file_id,
578 xml_detailed_weather,
585 weather_tests :: TestTree
589 [ test_on_delete_cascade,
590 test_pickle_of_unpickle_is_identity,
591 test_unpickle_succeeds,
592 test_types_detected_correctly ]
595 -- | If we unpickle something and then pickle it, we should wind up
596 -- with the same thing we started with. WARNING: success of this
597 -- test does not mean that unpickling succeeded.
599 test_pickle_of_unpickle_is_identity :: TestTree
600 test_pickle_of_unpickle_is_identity = testGroup "pickle-unpickle tests"
601 [ check "pickle composed with unpickle is the identity"
602 "test/xml/weatherxml.xml",
604 check "pickle composed with unpickle is the identity (detailed)"
605 "test/xml/weatherxml-detailed.xml" ]
607 check desc path = testCase desc $ do
608 (expected, actual) <- pickle_unpickle pickle_message path
612 -- | Make sure we can actually unpickle these things.
614 test_unpickle_succeeds :: TestTree
615 test_unpickle_succeeds = testGroup "unpickle tests"
616 [ check "unpickling succeeds"
617 "test/xml/weatherxml.xml",
618 check "unpickling succeeds (detailed)"
619 "test/xml/weatherxml-detailed.xml" ]
621 check desc path = testCase desc $ do
622 actual <- unpickleable path pickle_message
627 -- | Make sure everything gets deleted when we delete the top-level
630 test_on_delete_cascade :: TestTree
631 test_on_delete_cascade = testGroup "cascading delete tests"
632 [ check "deleting weather deletes its children"
633 "test/xml/weatherxml.xml",
634 check "deleting weather deletes its children (detailed)"
635 "test/xml/weatherxml-detailed.xml" ]
637 check desc path = testCase desc $ do
638 weather <- unsafe_unpickle path pickle_message
639 let a = undefined :: Weather
640 let b = undefined :: WeatherForecast
641 let c = undefined :: WeatherForecastListing
642 let d = undefined :: WeatherDetailedWeatherListingItem
643 actual <- withSqliteConn ":memory:" $ runDbConn $ do
644 runMigration silentMigrationLogger $ do
649 _ <- dbimport weather
651 count_a <- countAll a
652 count_b <- countAll b
653 count_c <- countAll c
654 count_d <- countAll d
655 return $ count_a + count_b + count_c + count_d
660 test_types_detected_correctly :: TestTree
661 test_types_detected_correctly =
662 testGroup "weatherxml types detected correctly" $
663 [ check "test/xml/weatherxml.xml"
664 "first type detected correctly"
666 check "test/xml/weatherxml-detailed.xml"
667 "first type detected correctly (detailed)"
669 check "test/xml/weatherxml-type2.xml"
670 "second type detected correctly"
673 unsafe_get_xmltree :: String -> IO XmlTree
674 unsafe_get_xmltree path =
675 fmap head $ runX $ readDocument parse_opts path
677 check path desc expected = testCase desc $ do
678 xmltree <- unsafe_get_xmltree path
679 let actual = is_type1 xmltree