1- {-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings #-}
1+ {-# LANGUAGE DeriveDataTypeable, DeriveGeneric, LambdaCase, ScopedTypeVariables, OverloadedStrings #-}
22-- | This module provides a type for interacting with a
33-- filesystem-backed 'MigrationStore'.
44module Database.Schema.Migrations.Filesystem
@@ -20,26 +20,21 @@ import Data.String.Conversions ( cs, (<>) )
2020
2121import Data.Typeable ( Typeable )
2222import Data.Time.Clock ( UTCTime )
23- import Data.Time () -- for UTCTime Show instance
23+ import Data.Time ( defaultTimeLocale , formatTime , parseTimeM )
2424import qualified Data.Map as Map
2525
26- import Control.Applicative ( (<$>) )
2726import Control.Monad ( filterM )
2827import Control.Exception ( Exception (.. ), throw , catch )
2928
30- import Data.Aeson as J (Object , Value (String , Null ))
31- import Data.HashMap.Strict as M (toList )
32- import Data.Yaml
29+ import Data.Aeson
30+ import Data.Aeson.Types (typeMismatch )
31+ import qualified Data.Yaml as Yaml
32+ import GHC.Generics (Generic )
3333
34- import Database.Schema.Migrations.Migration
35- ( Migration (.. )
36- , emptyMigration
37- )
34+ import Database.Schema.Migrations.Migration (Migration (.. ))
3835import Database.Schema.Migrations.Filesystem.Serialize
3936import Database.Schema.Migrations.Store
4037
41- type FieldProcessor = Text -> Migration -> Maybe Migration
42-
4338data FilesystemStoreSettings = FSStore { storePath :: FilePath }
4439
4540data FilesystemStoreError = FilesystemStoreError String
@@ -106,79 +101,77 @@ migrationFromPath path = do
106101 readMigrationFile = do
107102 ymlExists <- doesFileExist (addNewMigrationExtension path)
108103 if ymlExists
109- then decodeFileThrow (addNewMigrationExtension path) `catch` (\ (e:: ParseException ) -> throwFS $ show e)
110- else decodeFileThrow (addMigrationExtension path filenameExtensionTxt) `catch` (\ (e:: ParseException ) -> throwFS $ show e)
111-
112- process name = do
113- yaml <- readMigrationFile
114-
115- -- Convert yaml structure into basic key/value map
116- let fields = getFields yaml
117- missing = missingFields fields
118-
119- case length missing of
120- 0 -> do
121- let newM = emptyMigration name
122- case migrationFromFields newM fields of
123- Nothing -> throwFS $ " Error in " ++ (show path) ++ " : unrecognized field found"
124- Just m -> return m
125- _ -> throwFS $ " Error in " ++ (show path) ++ " : missing required field(s): " ++ (show missing)
126-
127- getFields :: J. Object -> [(Text , Text )]
128- getFields mp = map toPair $ M. toList mp
129- where
130- toPair :: (Text , Value ) -> (Text , Text )
131- toPair (k, J. String v) = (cs k, cs v)
132- toPair (k, J. Null ) = (cs k, cs (" " :: String ))
133- toPair (k, v) = throwFS $ " Error in YAML input; expected string key and string value, got " ++ (show (k, v))
134- getFields _ = throwFS " Error in YAML input; expected mapping"
135-
136- missingFields :: [(Text , Text )] -> [Text ]
137- missingFields fs =
138- [ k | k <- requiredFields, not (k `elem` inputStrings) ]
139- where
140- inputStrings = map fst fs
141-
142- -- | Given a migration and a list of parsed migration fields, update
143- -- the migration from the field values for recognized fields.
144- migrationFromFields :: Migration -> [(Text , Text )] -> Maybe Migration
145- migrationFromFields m [] = Just m
146- migrationFromFields m ((name, value): rest) = do
147- processor <- lookup name fieldProcessors
148- newM <- processor value m
149- migrationFromFields newM rest
150-
151- requiredFields :: [Text ]
152- requiredFields = [ " Apply"
153- , " Depends"
154- ]
155-
156- fieldProcessors :: [(Text , FieldProcessor )]
157- fieldProcessors = [ (" Created" , setTimestamp )
158- , (" Description" , setDescription )
159- , (" Apply" , setApply )
160- , (" Revert" , setRevert )
161- , (" Depends" , setDepends )
162- ]
163-
164- setTimestamp :: FieldProcessor
165- setTimestamp value m = do
166- ts <- case readTimestamp value of
167- [(t, _)] -> return t
168- _ -> fail " expected one valid parse"
169- return $ m { mTimestamp = Just ts }
170-
171- readTimestamp :: Text -> [(UTCTime , String )]
172- readTimestamp = reads . cs
173-
174- setDescription :: FieldProcessor
175- setDescription desc m = Just $ m { mDesc = Just desc }
176-
177- setApply :: FieldProcessor
178- setApply apply m = Just $ m { mApply = apply }
179-
180- setRevert :: FieldProcessor
181- setRevert revert m = Just $ m { mRevert = Just revert }
182-
183- setDepends :: FieldProcessor
184- setDepends depString m = Just $ m { mDeps = T. words depString }
104+ then Yaml. decodeFileThrow (addNewMigrationExtension path) `catch` (\ (e:: Yaml. ParseException ) -> throwFS $ show e)
105+ else Yaml. decodeFileThrow (addMigrationExtension path filenameExtensionTxt) `catch` (\ (e:: Yaml. ParseException ) -> throwFS $ show e)
106+
107+ process name = migrationYamlToMigration name <$> readMigrationFile
108+
109+ -- | TODO: re-use this for the generation side too
110+ data MigrationYaml = MigrationYaml
111+ { myCreated :: Maybe UTCTimeYaml
112+ , myDescription :: Maybe Text
113+ , myApply :: Text
114+ , myRevert :: Maybe Text
115+ , myDepends :: DependsYaml
116+ }
117+ deriving Generic
118+
119+ instance FromJSON MigrationYaml where
120+ parseJSON = genericParseJSON jsonOptions
121+
122+ instance ToJSON MigrationYaml where
123+ toJSON = genericToJSON jsonOptions
124+ toEncoding = genericToEncoding jsonOptions
125+
126+ jsonOptions :: Options
127+ jsonOptions = defaultOptions
128+ { fieldLabelModifier = drop 2 -- remove "my" prefix
129+ , omitNothingFields = True
130+ , rejectUnknownFields = True
131+ }
132+
133+ migrationYamlToMigration :: Text -> MigrationYaml -> Migration
134+ migrationYamlToMigration theId my = Migration
135+ { mTimestamp = unUTCTimeYaml <$> myCreated my
136+ , mId = theId
137+ , mDesc = myDescription my
138+ , mApply = myApply my
139+ , mRevert = myRevert my
140+ , mDeps = unDependsYaml $ myDepends my
141+ }
142+
143+ newtype UTCTimeYaml = UTCTimeYaml
144+ { unUTCTimeYaml :: UTCTime
145+ }
146+
147+ instance FromJSON UTCTimeYaml where
148+ parseJSON = withText " UTCTime"
149+ $ maybe (fail " Unable to parse UTCTime" ) (pure . UTCTimeYaml )
150+ . parseTimeM True defaultTimeLocale utcTimeYamlFormat
151+ . cs
152+
153+ instance ToJSON UTCTimeYaml where
154+ toJSON = toJSON . formatTime defaultTimeLocale utcTimeYamlFormat . unUTCTimeYaml
155+ toEncoding = toEncoding . formatTime defaultTimeLocale utcTimeYamlFormat . unUTCTimeYaml
156+
157+ -- Keeps things as the old Show/Read-based format, e.g "2009-04-15 10:02:06 UTC"
158+ utcTimeYamlFormat :: String
159+ utcTimeYamlFormat = " %F %T UTC"
160+
161+ newtype DependsYaml = DependsYaml
162+ { unDependsYaml :: [Text ]
163+ }
164+
165+ instance FromJSON DependsYaml where
166+ parseJSON = \ case
167+ Null -> pure $ DependsYaml []
168+ String t -> pure $ DependsYaml $ T. words t
169+ x -> typeMismatch " Null or whitespace-separated String" x
170+
171+ instance ToJSON DependsYaml where
172+ toJSON (DependsYaml ts) = case ts of
173+ [] -> toJSON Null
174+ _ -> toJSON $ T. unwords ts
175+ toEncoding (DependsYaml ts) = case ts of
176+ [] -> toEncoding Null
177+ _ -> toEncoding $ T. unwords ts
0 commit comments