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 (
16 -- * WARNING: these are private but exported to silence warnings
17 WeatherConstructor(..),
18 WeatherForecastConstructor(..),
19 WeatherForecastListingConstructor(..) )
23 import Control.Monad ( forM_ )
24 import Data.Time ( UTCTime )
25 import Data.Tuple.Curry ( uncurryN )
26 import Database.Groundhog (
31 silentMigrationLogger )
32 import Database.Groundhog.Core ( DefaultKey )
33 import Database.Groundhog.Generic ( runDbConn )
34 import Database.Groundhog.Sqlite ( withSqliteConn )
35 import Database.Groundhog.TH (
38 import Test.Tasty ( TestTree, testGroup )
39 import Test.Tasty.HUnit ( (@?=), testCase )
40 import Text.XML.HXT.Core (
55 import TSN.DbImport ( DbImport(..), ImportResult(..), run_dbmigrate )
56 import TSN.Picklers ( xp_gamedate, xp_time_stamp )
57 import TSN.XmlImport ( XmlImport(..), XmlImportFk(..) )
71 -- * WeatherForecastListing/WeatherForecastListingXml
73 -- | XML representation of a weather forecast listing.
75 data WeatherForecastListingXml =
76 WeatherForecastListingXml {
78 xml_weather :: String }
81 -- | Database representation of a weather forecast listing.
83 data WeatherForecastListing =
84 WeatherForecastListing {
85 db_weather_forecasts_id :: DefaultKey WeatherForecast,
87 db_weather :: String }
90 -- | The database analogue of a 'WeatherForecastListingXml' is
91 -- 'WeatherForecastListing'.
93 instance ToDb WeatherForecastListingXml where
94 type Db WeatherForecastListingXml = WeatherForecastListing
96 -- | This is needed to define the 'XmlImportFk' instance for
97 -- 'WeatherForecastListing'.
99 instance FromXmlFk WeatherForecastListingXml where
100 -- | Each 'WeatherForecastListingXml' is contained in a
101 -- 'WeatherForecast'.
103 type Parent WeatherForecastListingXml = WeatherForecast
105 from_xml_fk fk WeatherForecastListingXml{..} =
106 WeatherForecastListing {
107 db_weather_forecasts_id = fk,
108 db_teams = xml_teams,
109 db_weather = xml_weather }
111 -- | This allows us to insert the XML representation
112 -- 'WeatherForecastListingXml' directly.
114 instance XmlImportFk WeatherForecastListingXml
119 -- | XML representation of a league, as they appear in the weather
120 -- documents. There is no associated database representation because
121 -- the league element really adds no information besides its own
122 -- (usually empty) name. Since there's exactly one league per
123 -- forecast, we just store the league_name in the database
124 -- representation of a forecast.
128 league_name :: Maybe String,
129 listings :: [WeatherForecastListingXml] }
133 -- * WeatherForecast/WeatherForecastXml
135 -- | Database representation of a weather forecast.
137 data WeatherForecast =
139 db_weather_id :: DefaultKey Weather,
140 db_game_date :: UTCTime,
141 db_league_name :: Maybe String }
143 -- | XML representation of a weather forecast. It would have been
144 -- cleaner to omit the 'WeatherLeague' middleman, but having it
145 -- simplifies parsing.
147 data WeatherForecastXml =
149 xml_game_date :: UTCTime,
150 xml_league :: WeatherLeague }
153 instance ToDb WeatherForecastXml where
154 -- | The database representation of a 'WeatherForecastXml' is a
155 -- 'WeatherForecast'.
157 type Db WeatherForecastXml = WeatherForecast
159 instance FromXmlFk WeatherForecastXml where
160 type Parent WeatherForecastXml = Weather
162 -- | To convert a 'WeatherForecastXml' into a 'WeatherForecast', we
163 -- replace the 'WeatherLeague' with its name.
165 from_xml_fk fk WeatherForecastXml{..} =
168 db_game_date = xml_game_date,
169 db_league_name = (league_name xml_league) }
172 -- | This allows us to call 'insert_xml' on an 'WeatherForecastXml'
173 -- without first converting it to the database representation.
175 instance XmlImportFk WeatherForecastXml
180 -- | The database representation of a weather message.
184 db_xml_file_id :: Int,
187 db_time_stamp :: UTCTime }
190 -- | The XML representation of a weather message.
194 xml_xml_file_id :: Int,
195 xml_heading :: String,
196 xml_category :: String,
199 xml_forecasts :: [WeatherForecastXml],
200 xml_time_stamp :: UTCTime }
203 instance ToDb Message where
204 -- | The database representation of 'Message' is 'Weather'.
206 type Db Message = Weather
208 instance FromXml Message where
209 -- | To get a 'Weather' from a 'Message', we drop a bunch of
212 from_xml Message{..} =
214 db_xml_file_id = xml_xml_file_id,
215 db_sport = xml_sport,
216 db_title = xml_title,
217 db_time_stamp = xml_time_stamp }
219 -- | This allows us to insert the XML representation 'Message'
222 instance XmlImport Message
229 mkPersist tsn_codegen_config [groundhog|
234 - name: unique_weather
236 # Prevent multiple imports of the same message.
237 fields: [db_xml_file_id]
239 - entity: WeatherForecast
240 dbName: weather_forecasts
242 - name: WeatherForecast
244 - name: db_weather_id
248 - entity: WeatherForecastListing
249 dbName: weather_forecast_listings
251 - name: WeatherForecastListing
253 - name: db_weather_forecasts_id
260 instance DbImport Message where
263 migrate (undefined :: Weather)
264 migrate (undefined :: WeatherForecast)
265 migrate (undefined :: WeatherForecastListing)
268 -- The weather database schema has a nice linear structure. First
269 -- we insert the top-level weather record.
270 weather_id <- insert_xml m
272 -- Next insert all of the forecasts, one at a time.
273 forM_ (xml_forecasts m) $ \forecast -> do
274 forecast_id <- insert_xml_fk weather_id forecast
276 -- Insert all of this forecast's listings.
277 mapM_ (insert_xml_fk_ forecast_id) (listings $ xml_league forecast)
279 return ImportSucceeded
286 -- | Pickler to convert a 'WeatherForecastListingXml' to/from XML.
288 pickle_listing :: PU WeatherForecastListingXml
291 xpWrap (from_pair, to_pair) $
293 (xpElem "teams" xpText)
294 (xpElem "weather" xpText)
296 from_pair = uncurry WeatherForecastListingXml
297 to_pair WeatherForecastListingXml{..} = (xml_teams, xml_weather)
300 -- | Pickler to convert a 'WeatherLeague' to/from XML.
302 pickle_league :: PU WeatherLeague
305 xpWrap (from_pair, to_pair) $
307 (xpAttr "name" $ xpOption xpText)
308 (xpList pickle_listing)
310 from_pair = uncurry WeatherLeague
311 to_pair WeatherLeague{..} = (league_name, listings)
314 -- | Pickler to convert a 'WeatherForecastXml' to/from XML.
316 pickle_forecast :: PU WeatherForecastXml
319 xpWrap (from_pair, to_pair) $
321 (xpAttr "gamedate" xp_gamedate)
324 from_pair = uncurry WeatherForecastXml
325 to_pair WeatherForecastXml{..} = (xml_game_date,
330 -- | Pickler to convert a 'Message' to/from XML.
332 pickle_message :: PU Message
335 xpWrap (from_tuple, to_tuple) $
337 (xpElem "XML_File_ID" xpInt)
338 (xpElem "heading" xpText)
339 (xpElem "category" xpText)
340 (xpElem "sport" xpText)
341 (xpElem "title" xpText)
342 (xpList pickle_forecast)
343 (xpElem "time_stamp" xp_time_stamp)
345 from_tuple = uncurryN Message
346 to_tuple Message{..} = (xml_xml_file_id,
358 weather_tests :: TestTree
362 [ test_on_delete_cascade,
363 test_pickle_of_unpickle_is_identity,
364 test_unpickle_succeeds ]
367 -- | If we unpickle something and then pickle it, we should wind up
368 -- with the same thing we started with. WARNING: success of this
369 -- test does not mean that unpickling succeeded.
371 test_pickle_of_unpickle_is_identity :: TestTree
372 test_pickle_of_unpickle_is_identity =
373 testCase "pickle composed with unpickle is the identity" $ do
374 let path = "test/xml/weatherxml.xml"
375 (expected, actual) <- pickle_unpickle pickle_message path
379 -- | Make sure we can actually unpickle these things.
381 test_unpickle_succeeds :: TestTree
382 test_unpickle_succeeds =
383 testCase "unpickling succeeds" $ do
384 let path = "test/xml/weatherxml.xml"
385 actual <- unpickleable path pickle_message
390 -- | Make sure everything gets deleted when we delete the top-level
393 test_on_delete_cascade :: TestTree
394 test_on_delete_cascade =
395 testCase "deleting weather deletes its children" $ do
396 let path = "test/xml/weatherxml.xml"
397 weather <- unsafe_unpickle path pickle_message
398 let a = undefined :: Weather
399 let b = undefined :: WeatherForecast
400 let c = undefined :: WeatherForecastListing
401 actual <- withSqliteConn ":memory:" $ runDbConn $ do
402 runMigration silentMigrationLogger $ do
406 _ <- dbimport weather
407 -- No idea how 'delete' works, so do this instead.
408 executeRaw False "DELETE FROM weather;" []
409 count_a <- countAll a
410 count_b <- countAll b
411 count_c <- countAll c
412 return $ count_a + count_b + count_c