Skip to content

Commit 72b1f3a

Browse files
committed
Decode directly to Migration type
This patch defines a `MigrationYaml` record that represents the shape of a migration's Yaml file directly, then decodes to it. This obviates the manual "parsing" from a decoded `Object`. It also produces better error messages. The tests don't pass because we have actually been using Show/Read for the `UTCTime` fields instead of a JSON representation. I'll have to introduce a new type to account for that without breaking compatibility.
1 parent 7d052b8 commit 72b1f3a

2 files changed

Lines changed: 45 additions & 86 deletions

File tree

dbmigrations.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Library
8686
configurator >= 0.2,
8787
split >= 0.2.2,
8888
HUnit >= 1.2,
89-
aeson < 2,
89+
aeson,
9090
unordered-containers
9191

9292
Hs-Source-Dirs: src
Lines changed: 44 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings #-}
1+
{-# LANGUAGE DeriveDataTypeable, DeriveGeneric, ScopedTypeVariables, OverloadedStrings #-}
22
-- |This module provides a type for interacting with a
33
-- filesystem-backed 'MigrationStore'.
44
module Database.Schema.Migrations.Filesystem
@@ -27,19 +27,15 @@ import Control.Applicative ( (<$>) )
2727
import Control.Monad ( filterM )
2828
import Control.Exception ( Exception(..), throw, catch )
2929

30-
import Data.Aeson as J (Object, Value(String, Null))
30+
import Data.Aeson
3131
import Data.HashMap.Strict as M (toList)
32-
import Data.Yaml
32+
import qualified Data.Yaml as Yaml
33+
import GHC.Generics (Generic)
3334

34-
import Database.Schema.Migrations.Migration
35-
( Migration(..)
36-
, emptyMigration
37-
)
35+
import Database.Schema.Migrations.Migration (Migration(..))
3836
import Database.Schema.Migrations.Filesystem.Serialize
3937
import Database.Schema.Migrations.Store
4038

41-
type FieldProcessor = Text -> Migration -> Maybe Migration
42-
4339
data FilesystemStoreSettings = FSStore { storePath :: FilePath }
4440

4541
data FilesystemStoreError = FilesystemStoreError String
@@ -106,79 +102,42 @@ migrationFromPath path = do
106102
readMigrationFile = do
107103
ymlExists <- doesFileExist (addNewMigrationExtension path)
108104
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 }
105+
then Yaml.decodeFileThrow (addNewMigrationExtension path) `catch` (\(e::Yaml.ParseException) -> throwFS $ show e)
106+
else Yaml.decodeFileThrow (addMigrationExtension path filenameExtensionTxt) `catch` (\(e::Yaml.ParseException) -> throwFS $ show e)
107+
108+
process name = migrationYamlToMigration name <$> readMigrationFile
109+
110+
-- | TODO: re-use this for the generation side too
111+
data MigrationYaml = MigrationYaml
112+
{ myCreated :: Maybe UTCTime
113+
, myDescription :: Maybe Text
114+
, myApply :: Text
115+
, myRevert :: Maybe Text
116+
, myDepends :: Text
117+
-- ^ White-space separated names
118+
}
119+
deriving Generic
120+
121+
instance FromJSON MigrationYaml where
122+
parseJSON = genericParseJSON jsonOptions
123+
124+
instance ToJSON MigrationYaml where
125+
toJSON = genericToJSON jsonOptions
126+
toEncoding = genericToEncoding jsonOptions
127+
128+
jsonOptions :: Options
129+
jsonOptions = defaultOptions
130+
{ fieldLabelModifier = drop 2 -- remove "my" prefix
131+
, omitNothingFields = True
132+
, rejectUnknownFields = True
133+
}
134+
135+
migrationYamlToMigration :: Text -> MigrationYaml -> Migration
136+
migrationYamlToMigration theId my = Migration
137+
{ mTimestamp = myCreated my
138+
, mId = theId
139+
, mDesc = myDescription my
140+
, mApply = myApply my
141+
, mRevert = myRevert my
142+
, mDeps = T.words $ myDepends my
143+
}

0 commit comments

Comments
 (0)