git.iko.soy/purestack-dev/project-m36-rest
on master help sign in
.
commitc083a49f34acbb72e27e1c7656cd7c6ba64b2a1a authoriko <mail@iko.soy> dateSat 31 May 2025, 19:39 (1 year ago) parent4d1d7cb
9 files changed+450
.gitignore+3 .helix/languages.toml+25 exe/Main.hs+6 flake.lock+59 flake.nix+24 package.yaml+83 src/ProjectM36/Rest.hs+163 stack.yaml+19 stack.yaml.lock+68
.gitignorenew file+3
@@ -0,0 +1,3 @@
1+.stack-work
2+project-m36-rest.cabal
3+m36
.helix/languages.tomlnew file+25
@@ -0,0 +1,25 @@
1+[language-server.dhall-lsp]
2+name = "Dhall LSP"
3+command = "dhall-lsp-server"
4+
5+
6+[[language]]
7+name = "haskell"
8+auto-format = true
9+
10+[[language]]
11+name = "nix"
12+auto-format = true
13+formatter = { command = "nixfmt" }
14+
15+[[language]]
16+auto-format = true
17+name = "dhall"
18+scope = "source.dhall"
19+injection-regex = "dhall"
20+file-types = ["dhall"]
21+roots = []
22+comment-token = "--"
23+indent = { tab-width = 2, unit = " " }
24+language-servers = ["dhall-lsp"]
25+formatter = { command = "dhall" , args = ["format"] }
exe/Main.hsnew file+6
@@ -0,0 +1,6 @@
1+module Main (main) where
2+
3+import ProjectM36.Rest (run)
4+
5+main :: IO ()
6+main = run
flake.locknew file+59
@@ -0,0 +1,59 @@
1+{
2+ "nodes": {
3+ "flake-utils": {
4+ "inputs": {
5+ "systems": "systems"
6+ },
7+ "locked": {
8+ "lastModified": 1731533236,
9+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10+ "owner": "numtide",
11+ "repo": "flake-utils",
12+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13+ "type": "github"
14+ },
15+ "original": {
16+ "id": "flake-utils",
17+ "type": "indirect"
18+ }
19+ },
20+ "nixpkgs": {
21+ "locked": {
22+ "lastModified": 1748026106,
23+ "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=",
24+ "owner": "NixOS",
25+ "repo": "nixpkgs",
26+ "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c",
27+ "type": "github"
28+ },
29+ "original": {
30+ "id": "nixpkgs",
31+ "ref": "nixos-unstable",
32+ "type": "indirect"
33+ }
34+ },
35+ "root": {
36+ "inputs": {
37+ "flake-utils": "flake-utils",
38+ "nixpkgs": "nixpkgs"
39+ }
40+ },
41+ "systems": {
42+ "locked": {
43+ "lastModified": 1681028828,
44+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
45+ "owner": "nix-systems",
46+ "repo": "default",
47+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
48+ "type": "github"
49+ },
50+ "original": {
51+ "owner": "nix-systems",
52+ "repo": "default",
53+ "type": "github"
54+ }
55+ }
56+ },
57+ "root": "root",
58+ "version": 7
59+}
flake.nixnew file+24
@@ -0,0 +1,24 @@
1+{
2+ inputs = {
3+ nixpkgs.url = "nixpkgs/nixos-unstable";
4+ flake-utils.url = "flake-utils";
5+ };
6+
7+ outputs = { self, flake-utils, nixpkgs }:
8+ let
9+ supportedSystems =
10+ [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" "aarch64-linux" ];
11+ compilerVersion = "966";
12+ in flake-utils.lib.eachSystem supportedSystems (system:
13+ let pkgs = import nixpkgs { inherit system; };
14+ in {
15+ devShells.default = pkgs.mkShell {
16+ buildInputs = with pkgs; [
17+ haskell.compiler."ghc${compilerVersion}"
18+ (pkgs.haskell-language-server.override {
19+ supportedGhcVersions = [ compilerVersion ];
20+ })
21+ ];
22+ };
23+ });
24+}
package.yamlnew file+83
@@ -0,0 +1,83 @@
1+name: project-m36-rest
2+
3+library:
4+ source-dirs: src
5+ dependencies:
6+ - base
7+ - project-m36
8+ - servant
9+ - servant-server
10+ - wai
11+ - warp
12+ - aeson
13+ - optparse-applicative
14+ - vector
15+ - base64
16+ - text
17+
18+executable:
19+ main: Main.hs
20+ source-dirs: exe
21+ dependencies:
22+ - base
23+ - project-m36-rest
24+
25+
26+default-extensions:
27+ - DefaultSignatures
28+ - DataKinds
29+ - MultiParamTypeClasses
30+ - OverloadedRecordDot
31+ - DerivingStrategies
32+ - DerivingVia
33+ - DeriveAnyClass
34+ - DuplicateRecordFields
35+ - OverloadedStrings
36+ - ApplicativeDo
37+ - LambdaCase
38+ - UndecidableInstances
39+ - TypeFamilies
40+ - FunctionalDependencies
41+ - ScopedTypeVariables
42+ - QuantifiedConstraints
43+ - OverloadedLabels
44+ - PartialTypeSignatures
45+ - ViewPatterns
46+ - NoFieldSelectors
47+ - NoFieldSelectors
48+ - AllowAmbiguousTypes
49+ - ImpredicativeTypes
50+ - MultiWayIf
51+ - PackageImports
52+ - RoleAnnotations
53+ - BangPatterns
54+ - MagicHash
55+ - UnboxedTuples
56+ - TypeOperators
57+ - StandaloneDeriving
58+ - TypeApplications
59+ - PolyKinds
60+ - TupleSections
61+ - ExistentialQuantification
62+ - FlexibleInstances
63+ - ConstraintKinds
64+ - FlexibleContexts
65+ - GADTs
66+ - DeriveGeneric
67+ - OverloadedLabels
68+ - ImportQualifiedPost
69+
70+ghc-options: >
71+ -Weverything
72+ -Wno-unsafe
73+ -Wno-missing-safe-haskell-mode
74+ -Wno-missing-import-lists
75+ -Wno-missing-kind-signatures
76+ -Wno-all-missed-specialisations
77+ -Wno-missed-specialisations
78+ -Wno-partial-type-signatures
79+ -Wno-partial-fields
80+ -Wno-operator-whitespace
81+ -Wno-missed-extra-shared-lib
82+ -Wno-implicit-prelude
83+ -threaded
src/ProjectM36/Rest.hsnew file+163
@@ -0,0 +1,163 @@
1+module ProjectM36.Rest (run) where
2+
3+import Control.Exception (bracket)
4+import Control.Monad.IO.Class
5+import Data.Aeson hiding (Options)
6+import Data.Aeson qualified as A
7+import Data.Aeson.Key qualified as Key
8+import Data.Aeson.KeyMap qualified as KeyMap
9+import Data.Aeson.Types qualified as A
10+import Data.ByteString.Base64 (decodeBase64)
11+import Data.Function
12+import Data.Functor
13+import Data.Text (Text)
14+import Data.Text qualified as T
15+import Data.Text.Encoding qualified as T
16+import Data.Traversable
17+import Data.Vector qualified as V
18+import Debug.Trace
19+import GHC.Generics (Generic)
20+import Network.Wai.Handler.Warp qualified as Warp
21+import Options.Applicative
22+import ProjectM36.Base
23+import ProjectM36.Client
24+import Servant
25+import Servant.Server.Generic
26+
27+data Options = Options
28+ { dir :: FilePath,
29+ port :: Warp.Port
30+ }
31+
32+options :: Parser Options
33+options =
34+ Options
35+ <$> strOption (short 'd' <> long "dir" <> help "Path to where project-m36 should store data." <> value "m36")
36+ <*> option auto (short 'p' <> long "port" <> help "Port on which the HTTP server should run" <> value 6543)
37+
38+data Query
39+ = DefineQuery RelVarName [AttributeExpr]
40+ | InsertQuery RelVarName RelationalExpr
41+
42+instance FromJSON Query where
43+ parseJSON = withObject "Query" $ \o -> case KeyMap.toList o of
44+ [("define", def)] -> flip (withObject "Definition") def $ \d -> do
45+ name <- d .: "name"
46+ attributes <- (d .: "attrs") >>= decodeRelationType
47+ pure $ DefineQuery name (NakedAttributeExpr <$> V.toList attributes)
48+ [("insert", ins)] -> flip (withObject "Insert") ins $ \i -> do
49+ name <- i .: "name"
50+ rel <- i .: "rel" >>= decodeExpr
51+ pure $ InsertQuery name rel
52+ _ -> fail "Poorly formed query key"
53+
54+decodeRelationType :: A.Array -> A.Parser (V.Vector Attribute)
55+decodeRelationType o =
56+ traverse parseJSON o
57+ >>= traverse
58+ ( \(key, v) ->
59+ Attribute (Key.toText key) <$> case v of
60+ A.String "int" -> pure IntAtomType
61+ A.String "integer" -> pure IntegerAtomType
62+ A.String "scientific" -> pure ScientificAtomType
63+ A.String "double" -> pure DoubleAtomType
64+ A.String "text" -> pure TextAtomType
65+ A.String "day" -> pure DayAtomType
66+ A.String "datetime" -> pure DateTimeAtomType
67+ A.String "bytestring" -> pure ByteStringAtomType
68+ A.String "bool" -> pure BoolAtomType
69+ A.String "uuid" -> pure UUIDAtomType
70+ A.Array x -> RelationAtomType . Attributes <$> decodeRelationType x
71+ _ -> fail $ "unknown type: " <> show v
72+ )
73+
74+decodeExpr :: A.Object -> A.Parser RelationalExpr
75+decodeExpr o = case KeyMap.toList (traceShowId o) of
76+ [("lit", A.Object lit)] -> do
77+ (traceShowId -> !attrs) <- lit .: "attrs" >>= decodeRelationType
78+ (traceShowId -> !vals) :: [V.Vector Value] <- lit .: "vals"
79+ tuples <- for vals $ \rel ->
80+ traverse
81+ (uncurry decodeAtom)
82+ (V.zip ((\(Attribute _ atomType) -> atomType) <$> attrs) rel)
83+ pure $
84+ MakeStaticRelation
85+ (Attributes attrs)
86+ (RelationTupleSet $ RelationTuple (Attributes attrs) <$> tuples)
87+ _ -> fail $ "unknown expression: " <> show o
88+
89+decodeAtom :: AtomType -> A.Value -> A.Parser Atom
90+decodeAtom (traceShowId -> t) (traceShowId -> v) = case t of
91+ IntAtomType -> IntAtom <$> parseJSON v
92+ IntegerAtomType -> IntegerAtom <$> parseJSON v
93+ ScientificAtomType -> ScientificAtom <$> parseJSON v
94+ DoubleAtomType -> DoubleAtom <$> parseJSON v
95+ TextAtomType -> TextAtom <$> parseJSON v
96+ DayAtomType -> DayAtom <$> parseJSON v
97+ DateTimeAtomType -> DateTimeAtom <$> parseJSON v
98+ ByteStringAtomType ->
99+ ByteStringAtom <$> do
100+ b64 <- T.encodeUtf8 <$> parseJSON @Text v
101+ case decodeBase64 b64 of
102+ Right x -> pure x
103+ Left err -> fail $ T.unpack err
104+ pure undefined
105+ BoolAtomType -> BoolAtom <$> parseJSON v
106+ UUIDAtomType -> UUIDAtom <$> parseJSON v
107+ RelationAtomType (Attributes attrs) -> do
108+ rels <- parseJSON @[(V.Vector Value)] v
109+ decodedRels <- for rels $ \rel ->
110+ for (V.zip attrs rel) $ \(Attribute _ t', a) ->
111+ decodeAtom t' a
112+ pure $
113+ RelationAtom $
114+ Relation (Attributes attrs) $
115+ RelationTupleSet $
116+ decodedRels <&> \rel -> RelationTuple (Attributes attrs) rel
117+ _ -> fail $ "Unsupported atom type: " <> show t
118+
119+data API r = API
120+ { startTransaction :: r :- "transaction" :> Post '[JSON] SessionId,
121+ commit :: r :- "transaction" :> Capture "SessionId" SessionId :> PutNoContent,
122+ abort :: r :- "transaction" :> Capture "SessionId" SessionId :> DeleteNoContent,
123+ query :: r :- "transaction" :> Capture "SessionId" SessionId :> ReqBody '[JSON] Query :> Post '[JSON] [A.Value]
124+ }
125+ deriving stock (Generic)
126+
127+server :: Connection -> API AsServer
128+server conn =
129+ API
130+ { startTransaction =
131+ liftIO (createSessionAtHead conn defaultHeadName) >>= rethrow,
132+ commit = \sess -> (liftIO (autoMergeToHead sess conn UnionMergeStrategy defaultHeadName) >>= rethrow) $> NoContent,
133+ abort = \sess -> liftIO (closeSession sess conn) $> NoContent,
134+ query = \sess -> \case
135+ DefineQuery varName attrs -> do
136+ liftIO (executeDatabaseContextExpr sess conn (Define varName attrs)) >>= rethrow
137+ pure []
138+ InsertQuery varName rel -> do
139+ liftIO (executeDatabaseContextExpr sess conn (Insert varName rel)) >>= rethrow
140+ pure []
141+ }
142+
143+rethrow :: (Show err) => Either err x -> Handler x
144+rethrow = \case
145+ Left err -> throwError err400 {errReasonPhrase = show err}
146+ Right x -> pure x
147+
148+run :: IO ()
149+run = do
150+ opts <- execParser $ info (helper <*> options) fullDesc
151+ bracket
152+ ( connectProjectM36
153+ (InProcessConnectionInfo (CrashSafePersistence opts.dir) emptyNotificationCallback [] basicDatabaseContext)
154+ >>= \case
155+ Left err -> error $ show err
156+ Right x -> pure x
157+ )
158+ close
159+ ( \conn ->
160+ Warp.runSettings
161+ (Warp.defaultSettings & Warp.setServerName "project-m36-rest" & Warp.setPort opts.port)
162+ (genericServe (server conn))
163+ )
stack.yamlnew file+19
@@ -0,0 +1,19 @@
1+resolver: lts-22.43
2+
3+nix:
4+ enable: true
5+ packages:
6+ - zlib
7+
8+packages:
9+ - .
10+
11+extra-deps:
12+ - project-m36-1.1.1
13+ - curryer-rpc-0.4.0@sha256:a270cbb8f7b29fb580e4a798350bf6d4c42b5b553b27b182589b42be74e9c6d3,2906
14+ - fast-builder-0.1.5.0@sha256:09e06fc66a7b664bb1e8fbfa56d067874ce6a447612fe08f5cf48243bdbecfae,2807
15+ - rset-1.0.0@sha256:5a66770d08ddf10f7ab3aeddd03bafe5859daa403df939f1332ae30267e74939,1643
16+ - scotty-0.22@sha256:b3c799b3c4896176342062c1140c290ffb9a8d81e6da2ea3e12f7a83cbda78d4,6013
17+ - winery-1.5@sha256:811e149601920f4b3fd356c9b854e33e78691b17d18bb74551cbab5c1bf2a002,2928
18+ - barbies-th-0.1.11@sha256:df9648ae23b2a97f7766fae2ca923b7554b53775925ba15fbcb062cc837e1040,1660
19+ - streamly-bytestring-0.2.2@sha256:dd498a4e416c60ba4271e7e72269ec5123d895dd0dd6fd93dbabc7a7e6103d98,2576
stack.yaml.locknew file+68
@@ -0,0 +1,68 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages:
7+- completed:
8+ hackage: project-m36-1.1.1@sha256:133af41c8aa1af55aa7a768da3b34f98cfed813247e5d8ca6a0596d87ac699b3,36405
9+ pantry-tree:
10+ sha256: 6f55e1d011abf0d925b99384f296c4317c8206c537954bd9399a5cc24de2aea5
11+ size: 14135
12+ original:
13+ hackage: project-m36-1.1.1
14+- completed:
15+ hackage: curryer-rpc-0.4.0@sha256:a270cbb8f7b29fb580e4a798350bf6d4c42b5b553b27b182589b42be74e9c6d3,2906
16+ pantry-tree:
17+ sha256: b2586f799f73084bcbe6588eda47afb76a9782e5e3279e963bf7349f743eedbb
18+ size: 734
19+ original:
20+ hackage: curryer-rpc-0.4.0@sha256:a270cbb8f7b29fb580e4a798350bf6d4c42b5b553b27b182589b42be74e9c6d3,2906
21+- completed:
22+ hackage: fast-builder-0.1.5.0@sha256:09e06fc66a7b664bb1e8fbfa56d067874ce6a447612fe08f5cf48243bdbecfae,2807
23+ pantry-tree:
24+ sha256: e1d55a68a91fd7afd8fadcb1c28ef518e47feab25bdb3f9a9c8eddc12c1a3639
25+ size: 986
26+ original:
27+ hackage: fast-builder-0.1.5.0@sha256:09e06fc66a7b664bb1e8fbfa56d067874ce6a447612fe08f5cf48243bdbecfae,2807
28+- completed:
29+ hackage: rset-1.0.0@sha256:5a66770d08ddf10f7ab3aeddd03bafe5859daa403df939f1332ae30267e74939,1643
30+ pantry-tree:
31+ sha256: 3bb4cf0b5df0c591934ea0f426479749cab8da61db7d6b3c8cceb9d075b83038
32+ size: 741
33+ original:
34+ hackage: rset-1.0.0@sha256:5a66770d08ddf10f7ab3aeddd03bafe5859daa403df939f1332ae30267e74939,1643
35+- completed:
36+ hackage: scotty-0.22@sha256:b3c799b3c4896176342062c1140c290ffb9a8d81e6da2ea3e12f7a83cbda78d4,6013
37+ pantry-tree:
38+ sha256: ec781405d9d771ad8e0b0d6f350b4c4296104665668fc7263ba36839f674d835
39+ size: 2138
40+ original:
41+ hackage: scotty-0.22@sha256:b3c799b3c4896176342062c1140c290ffb9a8d81e6da2ea3e12f7a83cbda78d4,6013
42+- completed:
43+ hackage: winery-1.5@sha256:811e149601920f4b3fd356c9b854e33e78691b17d18bb74551cbab5c1bf2a002,2928
44+ pantry-tree:
45+ sha256: 096460bf61a8f08d936a3a0c0c12261524010dae0082ad8cc0c61d7beb508f69
46+ size: 1047
47+ original:
48+ hackage: winery-1.5@sha256:811e149601920f4b3fd356c9b854e33e78691b17d18bb74551cbab5c1bf2a002,2928
49+- completed:
50+ hackage: barbies-th-0.1.11@sha256:df9648ae23b2a97f7766fae2ca923b7554b53775925ba15fbcb062cc837e1040,1660
51+ pantry-tree:
52+ sha256: 34c01c9fb405d5e10849a73c82d2a8706156d5ba5120d9f28747b8b92a2e825b
53+ size: 657
54+ original:
55+ hackage: barbies-th-0.1.11@sha256:df9648ae23b2a97f7766fae2ca923b7554b53775925ba15fbcb062cc837e1040,1660
56+- completed:
57+ hackage: streamly-bytestring-0.2.2@sha256:dd498a4e416c60ba4271e7e72269ec5123d895dd0dd6fd93dbabc7a7e6103d98,2576
58+ pantry-tree:
59+ sha256: cec87f3988525238e3b681614e9ba7a4ac2f834bb34959a0a001c758bc653c12
60+ size: 532
61+ original:
62+ hackage: streamly-bytestring-0.2.2@sha256:dd498a4e416c60ba4271e7e72269ec5123d895dd0dd6fd93dbabc7a7e6103d98,2576
63+snapshots:
64+- completed:
65+ sha256: 08bd13ce621b41a8f5e51456b38d5b46d7783ce114a50ab604d6bbab0d002146
66+ size: 720271
67+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/43.yaml
68+ original: lts-22.43