|
1 | | -{-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables, OverloadedStrings #-} |
| 1 | +{-# LANGUAGE DeriveDataTypeable, DeriveGeneric, ScopedTypeVariables, OverloadedStrings #-} |
2 | 2 | -- |This module provides a type for interacting with a |
3 | 3 | -- filesystem-backed 'MigrationStore'. |
4 | 4 | module Database.Schema.Migrations.Filesystem |
@@ -27,19 +27,15 @@ import Control.Applicative ( (<$>) ) |
27 | 27 | import Control.Monad ( filterM ) |
28 | 28 | import Control.Exception ( Exception(..), throw, catch ) |
29 | 29 |
|
30 | | -import Data.Aeson as J (Object, Value(String, Null)) |
| 30 | +import Data.Aeson |
31 | 31 | import Data.HashMap.Strict as M (toList) |
32 | | -import Data.Yaml |
| 32 | +import qualified Data.Yaml as Yaml |
| 33 | +import GHC.Generics (Generic) |
33 | 34 |
|
34 | | -import Database.Schema.Migrations.Migration |
35 | | - ( Migration(..) |
36 | | - , emptyMigration |
37 | | - ) |
| 35 | +import Database.Schema.Migrations.Migration (Migration(..)) |
38 | 36 | import Database.Schema.Migrations.Filesystem.Serialize |
39 | 37 | import Database.Schema.Migrations.Store |
40 | 38 |
|
41 | | -type FieldProcessor = Text -> Migration -> Maybe Migration |
42 | | - |
43 | 39 | data FilesystemStoreSettings = FSStore { storePath :: FilePath } |
44 | 40 |
|
45 | 41 | data FilesystemStoreError = FilesystemStoreError String |
@@ -106,79 +102,42 @@ migrationFromPath path = do |
106 | 102 | readMigrationFile = do |
107 | 103 | ymlExists <- doesFileExist (addNewMigrationExtension path) |
108 | 104 | 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