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 one league, and
10 -- that league contains a bunch of listings.
12 module TSN.XML.Weather (
17 -- * WARNING: these are private but exported to silence warnings
18 WeatherConstructor(..),
19 WeatherForecastConstructor(..),
20 WeatherForecastListingConstructor(..) )
24 import Control.Monad ( forM_ )
25 import Data.Time ( UTCTime )
26 import Data.Tuple.Curry ( uncurryN )
27 import Database.Groundhog (
32 silentMigrationLogger )
33 import Database.Groundhog.Core ( DefaultKey )
34 import Database.Groundhog.Generic ( runDbConn )
35 import Database.Groundhog.Sqlite ( withSqliteConn )
36 import Database.Groundhog.TH (
39 import Test.Tasty ( TestTree, testGroup )
40 import Test.Tasty.HUnit ( (@?=), testCase )
41 import Text.XML.HXT.Core (
56 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
57 import TSN.Picklers ( xp_gamedate, xp_time_stamp )
58 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
69 -- | The DTD to which this module corresponds. Used to invoke dbimport.
72 dtd = "weatherxml.dtd"
79 -- * WeatherForecastListing/WeatherForecastListingXml
81 -- | XML representation of a weather forecast listing.
83 data WeatherForecastListingXml =
84 WeatherForecastListingXml {
86 xml_weather :: String }
89 -- | Database representation of a weather forecast listing.
91 data WeatherForecastListing =
92 WeatherForecastListing {
93 db_weather_forecasts_id :: DefaultKey WeatherForecast,
95 db_weather :: String }
98 -- | The database analogue of a 'WeatherForecastListingXml' is
99 -- 'WeatherForecastListing'.
101 instance ToDb WeatherForecastListingXml where
102 type Db WeatherForecastListingXml = WeatherForecastListing
104 -- | This is needed to define the 'XmlImportFk' instance for
105 -- 'WeatherForecastListing'.
107 instance FromXmlFk WeatherForecastListingXml where
108 -- | Each 'WeatherForecastListingXml' is contained in a
109 -- 'WeatherForecast'.
111 type Parent WeatherForecastListingXml = WeatherForecast
113 from_xml_fk fk WeatherForecastListingXml{..} =
114 WeatherForecastListing {
115 db_weather_forecasts_id = fk,
116 db_teams = xml_teams,
117 db_weather = xml_weather }
119 -- | This allows us to insert the XML representation
120 -- 'WeatherForecastListingXml' directly.
122 instance XmlImportFk WeatherForecastListingXml
127 -- | XML representation of a league, as they appear in the weather
128 -- documents. There is no associated database representation because
129 -- the league element really adds no information besides its own
130 -- (usually empty) name. Since there's exactly one league per
131 -- forecast, we just store the league_name in the database
132 -- representation of a forecast.
136 league_name :: Maybe String,
137 listings :: [WeatherForecastListingXml] }
141 -- * WeatherForecast/WeatherForecastXml
143 -- | Database representation of a weather forecast.
145 data WeatherForecast =
147 db_weather_id :: DefaultKey Weather,
148 db_game_date :: UTCTime,
149 db_league_name :: Maybe String }
151 -- | XML representation of a weather forecast. It would have been
152 -- cleaner to omit the 'WeatherLeague' middleman, but having it
153 -- simplifies parsing.
155 data WeatherForecastXml =
157 xml_game_date :: UTCTime,
158 xml_league :: WeatherLeague }
161 instance ToDb WeatherForecastXml where
162 -- | The database representation of a 'WeatherForecastXml' is a
163 -- 'WeatherForecast'.
165 type Db WeatherForecastXml = WeatherForecast
167 instance FromXmlFk WeatherForecastXml where
168 type Parent WeatherForecastXml = Weather
170 -- | To convert a 'WeatherForecastXml' into a 'WeatherForecast', we
171 -- replace the 'WeatherLeague' with its name.
173 from_xml_fk fk WeatherForecastXml{..} =
176 db_game_date = xml_game_date,
177 db_league_name = (league_name xml_league) }
180 -- | This allows us to call 'insert_xml' on an 'WeatherForecastXml'
181 -- without first converting it to the database representation.
183 instance XmlImportFk WeatherForecastXml
188 -- | The database representation of a weather message.
192 db_xml_file_id :: Int,
195 db_time_stamp :: UTCTime }
198 -- | The XML representation of a weather message.
202 xml_xml_file_id :: Int,
203 xml_heading :: String,
204 xml_category :: String,
207 xml_forecasts :: [WeatherForecastXml],
208 xml_time_stamp :: UTCTime }
211 instance ToDb Message where
212 -- | The database representation of 'Message' is 'Weather'.
214 type Db Message = Weather
216 instance FromXml Message where
217 -- | To get a 'Weather' from a 'Message', we drop a bunch of
220 from_xml Message{..} =
222 db_xml_file_id = xml_xml_file_id,
223 db_sport = xml_sport,
224 db_title = xml_title,
225 db_time_stamp = xml_time_stamp }
227 -- | This allows us to insert the XML representation 'Message'
230 instance XmlImport Message
237 mkPersist tsn_codegen_config [groundhog|
242 - name: unique_weather
244 # Prevent multiple imports of the same message.
245 fields: [db_xml_file_id]
247 - entity: WeatherForecast
248 dbName: weather_forecasts
250 - name: WeatherForecast
252 - name: db_weather_id
256 - entity: WeatherForecastListing
257 dbName: weather_forecast_listings
259 - name: WeatherForecastListing
261 - name: db_weather_forecasts_id
268 instance DbImport Message where
271 migrate (undefined :: Weather)
272 migrate (undefined :: WeatherForecast)
273 migrate (undefined :: WeatherForecastListing)
276 -- The weather database schema has a nice linear structure. First
277 -- we insert the top-level weather record.
278 weather_id <- insert_xml m
280 -- Next insert all of the forecasts, one at a time.
281 forM_ (xml_forecasts m) $ \forecast -> do
282 forecast_id <- insert_xml_fk weather_id forecast
284 -- Insert all of this forecast's listings.
285 mapM_ (insert_xml_fk_ forecast_id) (listings $ xml_league forecast)
287 return ImportSucceeded
294 -- | Pickler to convert a 'WeatherForecastListingXml' to/from XML.
296 pickle_listing :: PU WeatherForecastListingXml
299 xpWrap (from_pair, to_pair) $
301 (xpElem "teams" xpText)
302 (xpElem "weather" xpText)
304 from_pair = uncurry WeatherForecastListingXml
305 to_pair WeatherForecastListingXml{..} = (xml_teams, xml_weather)
308 -- | Pickler to convert a 'WeatherLeague' to/from XML.
310 pickle_league :: PU WeatherLeague
313 xpWrap (from_pair, to_pair) $
315 (xpAttr "name" $ xpOption xpText)
316 (xpList pickle_listing)
318 from_pair = uncurry WeatherLeague
319 to_pair WeatherLeague{..} = (league_name, listings)
322 -- | Pickler to convert a 'WeatherForecastXml' to/from XML.
324 pickle_forecast :: PU WeatherForecastXml
327 xpWrap (from_pair, to_pair) $
329 (xpAttr "gamedate" xp_gamedate)
332 from_pair = uncurry WeatherForecastXml
333 to_pair WeatherForecastXml{..} = (xml_game_date,
338 -- | Pickler to convert a 'Message' to/from XML.
340 pickle_message :: PU Message
343 xpWrap (from_tuple, to_tuple) $
345 (xpElem "XML_File_ID" xpInt)
346 (xpElem "heading" xpText)
347 (xpElem "category" xpText)
348 (xpElem "sport" xpText)
349 (xpElem "title" xpText)
350 (xpList pickle_forecast)
351 (xpElem "time_stamp" xp_time_stamp)
353 from_tuple = uncurryN Message
354 to_tuple Message{..} = (xml_xml_file_id,
366 weather_tests :: TestTree
370 [ test_on_delete_cascade,
371 test_pickle_of_unpickle_is_identity,
372 test_unpickle_succeeds ]
375 -- | If we unpickle something and then pickle it, we should wind up
376 -- with the same thing we started with. WARNING: success of this
377 -- test does not mean that unpickling succeeded.
379 test_pickle_of_unpickle_is_identity :: TestTree
380 test_pickle_of_unpickle_is_identity =
381 testCase "pickle composed with unpickle is the identity" $ do
382 let path = "test/xml/weatherxml.xml"
383 (expected, actual) <- pickle_unpickle pickle_message path
387 -- | Make sure we can actually unpickle these things.
389 test_unpickle_succeeds :: TestTree
390 test_unpickle_succeeds =
391 testCase "unpickling succeeds" $ do
392 let path = "test/xml/weatherxml.xml"
393 actual <- unpickleable path pickle_message
398 -- | Make sure everything gets deleted when we delete the top-level
401 test_on_delete_cascade :: TestTree
402 test_on_delete_cascade =
403 testCase "deleting weather deletes its children" $ do
404 let path = "test/xml/weatherxml.xml"
405 weather <- unsafe_unpickle path pickle_message
406 let a = undefined :: Weather
407 let b = undefined :: WeatherForecast
408 let c = undefined :: WeatherForecastListing
409 actual <- withSqliteConn ":memory:" $ runDbConn $ do
410 runMigration silentMigrationLogger $ do
414 _ <- dbimport weather
416 count_a <- countAll a
417 count_b <- countAll b
418 count_c <- countAll c
419 return $ count_a + count_b + count_c