Skip to content

Commit 2cfdf43

Browse files
authored
Merge pull request #43 from pbrisbin/pb/from-json
Rewrite Migration parser using conventional FromJSON
2 parents 2c8b153 + 5dd8aed commit 2cfdf43

2 files changed

Lines changed: 82 additions & 89 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: 81 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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'.
44
module Database.Schema.Migrations.Filesystem
@@ -20,26 +20,21 @@ import Data.String.Conversions ( cs, (<>) )
2020

2121
import Data.Typeable ( Typeable )
2222
import Data.Time.Clock ( UTCTime )
23-
import Data.Time () -- for UTCTime Show instance
23+
import Data.Time ( defaultTimeLocale, formatTime, parseTimeM )
2424
import qualified Data.Map as Map
2525

26-
import Control.Applicative ( (<$>) )
2726
import Control.Monad ( filterM )
2827
import 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(..))
3835
import Database.Schema.Migrations.Filesystem.Serialize
3936
import Database.Schema.Migrations.Store
4037

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

4540
data 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

Comments
 (0)