diff options
author | Alexander Foremny <aforemny@posteo.de> | 2024-06-05 22:47:49 +0200 |
---|---|---|
committer | Alexander Foremny <aforemny@posteo.de> | 2024-06-05 23:36:34 +0200 |
commit | bfb98d7675515394e1b9a0417bfafc83d775611c (patch) | |
tree | dec841dc2ca6b79f8eaa777b90b3b1473f369c9d | |
parent | 2064b4e7767dca2858d8093597503a594dcd74ef (diff) |
add schema version
-rw-r--r-- | backend/app/Main.hs | 108 | ||||
-rw-r--r-- | backend/backend.cabal | 4 | ||||
-rw-r--r-- | common/CHANGELOG.md | 5 | ||||
-rw-r--r-- | common/LICENSE | 30 | ||||
-rw-r--r-- | common/common.cabal | 21 | ||||
-rw-r--r-- | common/src/Version.hs | 36 | ||||
-rw-r--r-- | default.nix | 3 | ||||
-rw-r--r-- | frontend/app/Api.hs | 6 | ||||
-rw-r--r-- | frontend/app/Main.hs | 86 | ||||
-rw-r--r-- | frontend/frontend.cabal | 2 | ||||
-rw-r--r-- | nix/sources.json | 2 |
11 files changed, 252 insertions, 51 deletions
diff --git a/backend/app/Main.hs b/backend/app/Main.hs index 6773916..6742ad2 100644 --- a/backend/app/Main.hs +++ b/backend/app/Main.hs @@ -15,6 +15,8 @@ import Data.ByteString.Lazy.UTF8 qualified as LB import Data.ByteString.UTF8 qualified as B import Data.List import Data.Map qualified as M +import Data.Map.Merge.Strict qualified as M +import Data.Maybe import Data.String (IsString (fromString)) import Data.Tagged (Tagged (..), untag) import Debug.Trace @@ -25,11 +27,13 @@ import Network.HTTP.Types.Status qualified as W import Network.Wai qualified as W import Network.Wai.Handler.Warp qualified as W import Options.Applicative qualified as A +import Safe import Store qualified as Q import System.Directory (setCurrentDirectory) import System.FilePath import System.INotify import Text.Printf (printf) +import Version data Args = Args { cmd :: Cmd @@ -57,7 +61,8 @@ data Repo = Repo data Commit = Commit { id :: G.CommitOid GB.LgRepo, - collections :: [Collection] + collections :: [Collection], + schemaVersion :: Version } deriving (Show) @@ -110,22 +115,78 @@ initRepo root ref = do Just cid <- fmap Tagged <$> G.resolveReference ref c <- G.lookupCommit cid cs <- mapM G.lookupCommit =<< G.listCommits Nothing cid - fmap Repo . forM cs $ \c -> do - let cid = G.commitOid c - fs <- - fmap (filter ((== ".json") . takeExtension)) . liftIO $ - Q.withStore root ref do - Q.withCommit cid Q.listAllFiles - let cls = - M.toList . M.unionsWith (++) $ - map (\f -> M.singleton (takeDirectory f) [f]) fs - colls <- forM cls $ \(makeRelative "/" -> path, (file : files)) -> do - (value : values) <- do - liftIO $ Q.withStore root ref do - mapM (Q.withCommit cid . Q.readFile) (file : files) - let schema = fromAutoTypes path $ U.autoTypes' value values - pure $ Collection path files schema - pure (Commit cid colls) + fmap (Repo . reverse) $ + foldM + ( \cs c -> do + let cid = G.commitOid c + fs <- + fmap (filter ((== ".json") . takeExtension)) . liftIO $ + Q.withStore root ref do + Q.withCommit cid (Q.listFiles "/") + let cls = + M.toList . M.unionsWith (++) $ + map (\f -> M.singleton (takeDirectory f) [f]) fs + colls <- forM cls $ \(path, (file : files)) -> do + (value : values) <- do + liftIO $ Q.withStore root ref do + mapM (Q.withCommit cid . Q.readFile) (file : files) + let schema = fromAutoTypes path $ U.autoTypes' value values + pure $ Collection path files schema + let schemaVersion = + case lastMay cs of + Nothing -> Version 1 0 0 + Just c' -> + let Version major' minor' patch' = c'.schemaVersion + schemas' = + M.fromList + ( (\coll -> (coll.path, coll.schema)) + <$> c'.collections + ) + schemas = + M.fromList + ( (\coll -> (coll.path, coll.schema)) + <$> c.collections + ) + in case compareSchemas schemas' schemas of + Just Major -> Version (major' + 1) 0 0 + Just Minor -> Version major' (minor' + 1) 0 + Just Patch -> Version major' minor' (patch' + 1) + Nothing -> Version major' minor' patch' + c = Commit cid colls schemaVersion + pure (c : cs) + ) + [] + cs + +compareSchemas :: + M.Map String Schema -> + M.Map String Schema -> + Maybe SchemaDifference +compareSchemas schemas' schemas = + maximumMay + . catMaybes + . M.elems + . M.map (uncurry compareSchemas') + $ M.merge + (M.mapMissing (\_ schema' -> (Just schema', Nothing))) + (M.mapMissing (\_ schema -> (Nothing, Just schema))) + (M.zipWithMatched (\_ schema' schema -> (Just schema', Just schema))) + schemas' + schemas + where + compareSchemas' Nothing (Just _) = Just Patch + compareSchemas' (Just _) Nothing = Just Patch + compareSchemas' (Just schema') (Just schema) = compareSchema schema' schema + +-- TODO +compareSchema :: Schema -> Schema -> Maybe SchemaDifference +compareSchema schema' schema = Nothing + +data SchemaDifference + = Major + | Minor + | Patch + deriving (Eq, Ord) main :: IO () main = do @@ -146,21 +207,28 @@ main = do q <- fromString @Q.Query . LB.toString <$> W.lazyRequestBody req - r <- liftIO $ Q.withStore root ref (Q.query q) + r <- liftIO $ Q.withStore root ref do Q.query q respond . W.responseLBS W.status200 [] $ J.encode r - (Debug.Trace.traceShowId -> !_) -> + Right SchemaVersion -> do + repo <- atomically (readTMVar repoT) + respond $ + W.responseLBS W.status200 [] $ + J.encode (last repo.commits).schemaVersion + (traceShowId -> !_) -> respond $ W.responseLBS W.status200 [] "not implemented" data Route = SchemaJson String | Query + | SchemaVersion deriving (Show) routeP :: P.Parser Route routeP = ( P.choice [ SchemaJson <$> (P.string "/" *> P.manyTill P.anyChar (P.string ".schema.json")), - pure Query <* (P.string "/") + pure SchemaVersion <* P.string "/schemaVersion", + pure Query <* P.string "/" ] ) <* P.endOfInput diff --git a/backend/backend.cabal b/backend/backend.cabal index be7099a..058efc7 100644 --- a/backend/backend.cabal +++ b/backend/backend.cabal @@ -23,6 +23,7 @@ executable backend autotypes, base, bytestring, + common, containers, directory, filepath, @@ -33,8 +34,11 @@ executable backend http-types, mtl, optparse-applicative, + safe, + split, stm, tagged, + text, utf8-string, wai, warp diff --git a/common/CHANGELOG.md b/common/CHANGELOG.md new file mode 100644 index 0000000..47b7089 --- /dev/null +++ b/common/CHANGELOG.md @@ -0,0 +1,5 @@ +# Revision history for common + +## 0.1.0.0 -- YYYY-mm-dd + +* First version. Released on an unsuspecting world. diff --git a/common/LICENSE b/common/LICENSE new file mode 100644 index 0000000..c90516a --- /dev/null +++ b/common/LICENSE @@ -0,0 +1,30 @@ +Copyright (c) 2024, Alexander Foremny + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Alexander Foremny nor the names of other + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/common/common.cabal b/common/common.cabal new file mode 100644 index 0000000..738d8be --- /dev/null +++ b/common/common.cabal @@ -0,0 +1,21 @@ +cabal-version: 3.4 +name: common +version: 0.1.0.0 +license: BSD-3-Clause +license-file: LICENSE +maintainer: aforemny@posteo.de +author: Alexander Foremny +build-type: Simple +extra-doc-files: CHANGELOG.md + +library + exposed-modules: Version + hs-source-dirs: src + default-language: GHC2021 + default-extensions: ViewPatterns + ghc-options: -Wall + build-depends: + aeson, + base, + split, + text diff --git a/common/src/Version.hs b/common/src/Version.hs new file mode 100644 index 0000000..cb568e6 --- /dev/null +++ b/common/src/Version.hs @@ -0,0 +1,36 @@ +module Version + ( Version (..), + versionToString, + versionFromText, + versionFromString, + ) +where + +import Data.Aeson qualified as A +import Data.Aeson.Types qualified as A +import Data.List +import Data.List.Split +import Data.Text qualified as T + +data Version = Version Int Int Int + deriving (Show, Eq) + +instance A.ToJSON Version where + toJSON = + A.toJSON . versionToString + +instance A.FromJSON Version where + parseJSON (A.String (versionFromText -> Just version)) = pure version + parseJSON v = A.typeMismatch "version" v + +versionToString :: Version -> String +versionToString (Version major minor patch) = + intercalate "." (map show [major, minor, patch]) + +versionFromString :: String -> Maybe Version +versionFromString (map read . splitOn "." -> [major, minor, patch]) = + Just (Version major minor patch) +versionFromString _ = Nothing + +versionFromText :: T.Text -> Maybe Version +versionFromText = versionFromString . T.unpack diff --git a/default.nix b/default.nix index 32e9ff8..afea1a3 100644 --- a/default.nix +++ b/default.nix @@ -7,6 +7,7 @@ let astore = self.callCabal2nix "astore" sources.json2sql { }; autotypes = self.callCabal2nix "autotypes" ./autotypes { }; backend = self.callCabal2nix "backend" ./backend { }; + common = self.callCabal2nix "common" ./common { }; frontend = self.callCabal2nix "frontend" ./frontend { }; websockets = pkgs.haskell.lib.doJailbreak super.websockets; }; @@ -14,6 +15,7 @@ let jsHaskellPackages = pkgs.pkgsCross.ghcjs.haskell.packages.ghc98.override { overrides = self: super: { + common = self.callCabal2nix "common" ./common { }; frontend = self.callCabal2nix "frontend" ./frontend { }; }; }; @@ -25,6 +27,7 @@ rec { packages = _: [ haskellPackages.autotypes haskellPackages.backend + haskellPackages.common haskellPackages.frontend ]; buildInputs = [ diff --git a/frontend/app/Api.hs b/frontend/app/Api.hs index c16e269..f4e4599 100644 --- a/frontend/app/Api.hs +++ b/frontend/app/Api.hs @@ -2,6 +2,7 @@ module Api ( fetchSchema, + fetchSchemaVersion, fetchPosts, fetchPost, updatePost, @@ -25,11 +26,16 @@ import Data.Function import Miso import Safe import Schema +import Version fetchSchema :: JSM (Either String Schema) fetchSchema = A.eitherDecode <$> fetch (fromString "http://localhost:8081/posts.schema.json") +fetchSchemaVersion :: JSM (Either String Version) +fetchSchemaVersion = + A.eitherDecode <$> fetch (fromString "http://localhost:8081/schemaVersion") + fetchPosts :: JSM (Either String [A.Value]) fetchPosts = A.eitherDecode diff --git a/frontend/app/Main.hs b/frontend/app/Main.hs index 4ef4def..37230ad 100644 --- a/frontend/app/Main.hs +++ b/frontend/app/Main.hs @@ -4,24 +4,38 @@ module Main where import Language.Javascript.JSaddle.Warp as JSaddle #endif +import Api import Data.Bifunctor import Data.Default import Data.Function -import GHC.Generics (Generic) import Miso import Miso.String (toMisoString) import NeatInterpolation qualified as Q import Page (Page, initialPage, updatePage, viewPage) import Page qualified as Page import Route (parseURI) +import Version -data Model = Model - { page :: Maybe (Either String Page) +data Model + = Loading + | Failed String + | Loaded LoadedState + deriving (Show, Eq) + +data LoadedState = LoadedState + { page :: Maybe (Either String Page), + schemaVersion :: Version } - deriving (Show, Eq, Generic, Default) + deriving (Show, Eq) + +instance Default Model where + def = Loading data Action - = NoOp + = -- Loading + SetLoaded (Either String LoadedState) + | -- Loaded + NoOp | Init URI | HandleURI URI | HandlePage Page.Action @@ -50,34 +64,43 @@ main = runApp $ do logLevel = Off updateModel :: Action -> Model -> Effect Action Model -updateModel NoOp m = noEff m -updateModel (Init uri) m = - m <# do - SetPage <$> initialPage (parseURI uri) -updateModel (HandleURI uri) m = - m <# do +updateModel _ (Failed err) = noEff (Failed err) +updateModel (Init uri) Loading = + Loading <# do + page <- Just <$> initialPage (parseURI uri) + schemaVersion' <- fetchSchemaVersion + pure $ SetLoaded do + schemaVersion <- schemaVersion' + pure LoadedState {..} +updateModel (Init _) m = noEff m +updateModel (SetLoaded (Left err)) Loading = noEff (Failed err) +updateModel (SetLoaded (Right state)) Loading = noEff (Loaded state) +updateModel (SetLoaded _) m = noEff m +updateModel _ Loading = noEff Loading +updateModel NoOp (Loaded s) = noEff (Loaded s) +updateModel (HandleURI uri) (Loaded s) = + Loaded s <# do let route = parseURI uri SetPage <$> initialPage route -updateModel (SetPage page) m = noEff m {page = Just page} -updateModel (HandlePage action) m = - case m.page of +updateModel (SetPage page) (Loaded s) = noEff (Loaded s {page = Just page}) +updateModel (HandlePage action) (Loaded s) = + case s.page of Just (Right page) -> - updatePage action page - & bimap HandlePage (\page -> m {page = Just (Right page)}) - _ -> noEff m + fmap Loaded $ + updatePage action page + & bimap HandlePage (\page -> s {page = Just (Right page)}) + _ -> noEff (Loaded s) viewModel :: Model -> View Action -viewModel model = +viewModel Loading = text ".." +viewModel (Failed s) = err s +viewModel (Loaded s) = div_ [] $ [ viewCss, - viewHeader, + viewHeader s, nav_ [] [viewCollections], main_ [] $ - [ HandlePage - <$> maybe - (text "..") - (either err viewPage) - model.page + [ HandlePage <$> maybe (text "..") (either err viewPage) s.page ] ] @@ -171,16 +194,19 @@ th, td { err :: String -> View action err = text . toMisoString . ("err! " <>) -viewHeader :: View Action -viewHeader = +viewHeader :: LoadedState -> View Action +viewHeader s = header_ [] $ [ section_ [] [h1_ [] [a_ [href_ "#"] [text "acms"]]], - section_ [] [viewBranch] + section_ [] (viewBranch s) ] -viewBranch :: View Action -viewBranch = - select_ [] [option_ [] [text "main"]] +viewBranch :: LoadedState -> [View Action] +viewBranch s = + [ text (toMisoString (versionToString s.schemaVersion)), + text " ", + select_ [] [option_ [] [text "main"]] + ] viewCollections :: View Action viewCollections = diff --git a/frontend/frontend.cabal b/frontend/frontend.cabal index 3260c51..71c1afb 100644 --- a/frontend/frontend.cabal +++ b/frontend/frontend.cabal @@ -37,11 +37,13 @@ executable frontend attoparsec, base, bytestring, + common, containers, data-default, miso, neat-interpolation, safe, + split, text, utf8-string diff --git a/nix/sources.json b/nix/sources.json index cd609dd..fca5e04 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -2,7 +2,7 @@ "json2sql": { "branch": "main", "repo": "git@code.nomath.org:~/json2sql", - "rev": "d8b2af98f594e4fc5cc4919c1efe95e1d8d9aafe", + "rev": "906d9ebba1ae08ea73acb55b536ff2f49e1b55c0", "type": "git" }, "nixpkgs": { |