|]
60 |
61 | bot =
62 | [Perl6.q|
63 |
64 |
65 |
66 | |]
67 |
68 | (|>) = (&)
69 |
70 | someFunc :: IO ()
71 | someFunc = do
72 | md <- Text.readFile "index.md"
73 | ast <- pureAST md
74 | let modifiedAst =
75 | ast
76 | |> Pandoc.walk addHeaderID
77 | findLinks (Pandoc.Header lvl (i, _, _) ils) =
78 | if lvl == 2
79 | then [(i, Pandoc.query blockText ils |> Text.intercalate " ")]
80 | else []
81 | findLinks _ = []
82 | links = Pandoc.query findLinks modifiedAst
83 |
84 | top = makeTop links
85 | html <- mdToHTML modifiedAst
86 | Text.writeFile "pages/index.html" (top <> html <> bot)
87 |
88 | addHeaderID :: Pandoc.Block -> Pandoc.Block
89 | addHeaderID block@(Pandoc.Header lvl (_, c, kv) ils) =
90 | if lvl == 2
91 | then Pandoc.Header lvl (i', c, kv) ils
92 | else block
93 | where
94 | i' =
95 | Pandoc.query blockText ils
96 | |> map Text.toLower
97 | |> Text.intercalate ""
98 | addHeaderID block = block
99 |
100 | blockText = \case
101 | Pandoc.Str str -> [str]
102 | _ -> []
103 |
104 | -- Text.writeFile "Report.hs" $ Text.pack $ show ast
--------------------------------------------------------------------------------
/old/Query/Applicative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Query.Applicative where
6 |
7 | -- import Data.ByteString qualified as BS
8 | -- import Data.List qualified as List
9 | -- import Data.Text qualified as Text
10 | -- import Data.Text.Encoding qualified as Text
11 | -- import Network.HTTP.Types qualified as HTTP
12 | -- import Web.HttpApiData qualified as Web
13 | -- import Network.Wai qualified as Wai
14 | -- import Network.Wai.Internal qualified as Wai
15 | -- import Okapi.Parser.Query.Operation qualified as Operation
16 |
17 | -- data Parser a where
18 | -- FMap :: (a -> b) -> Parser a -> Parser b
19 | -- Pure :: a -> Parser a
20 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
21 | -- Optional :: Parser a -> Parser (Maybe a)
22 | -- Option :: a -> Parser a -> Parser a
23 | -- Operation :: Operation.Parser a -> Parser a
24 |
25 | -- instance Functor Parser where
26 | -- fmap = FMap
27 |
28 | -- instance Applicative Parser where
29 | -- pure = Pure
30 | -- (<*>) = Apply
31 |
32 | -- param :: Web.FromHttpApiData a => BS.ByteString -> Parser a
33 | -- param = Operation . Operation.Param
34 |
35 | -- flag :: BS.ByteString -> Parser ()
36 | -- flag = Operation . Operation.Flag
37 |
38 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
39 | -- optional = Optional
40 |
41 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
42 | -- option = Option
43 |
44 | -- eval ::
45 | -- Parser a ->
46 | -- Wai.Request ->
47 | -- (Either Operation.Error a, Wai.Request)
48 | -- eval (FMap f opX) state = case eval opX state of
49 | -- (Left e, state') -> (Left e, state')
50 | -- (Right x, state') -> (Right $ f x, state')
51 | -- eval (Pure x) state = (Right x, state)
52 | -- eval (Apply opF opX) state = case eval opF state of
53 | -- (Right f, state') -> case eval opX state' of
54 | -- (Right x, state'') -> (Right $ f x, state'')
55 | -- (Left e, state'') -> (Left e, state'')
56 | -- (Left e, state') -> (Left e, state')
57 | -- eval (Optional op) state = case op of
58 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
59 | -- (Right result, state') -> (Right $ Just result, state')
60 | -- (_, state') -> (Right Nothing, state')
61 | -- Operation flag@(Operation.Flag _) -> case Operation.eval flag state of
62 | -- (Right result, state') -> (Right $ Just result, state')
63 | -- (_, state') -> (Right Nothing, state')
64 | -- _ -> case eval op state of
65 | -- (Right result, state') -> (Right $ Just result, state')
66 | -- (Left err, state') -> (Left err, state')
67 | -- eval (Option def op) state = case op of
68 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
69 | -- (Right result, state') -> (Right result, state')
70 | -- (_, state') -> (Right def, state')
71 | -- Operation flag@(Operation.Flag _) -> case Operation.eval flag state of
72 | -- (Right result, state') -> (Right result, state')
73 | -- (_, state') -> (Right def, state')
74 | -- _ -> eval op state
75 | -- eval (Operation op) state = Operation.eval op state
76 |
77 | -- class FromQuery a where
78 | -- parser :: Parser a
79 |
80 | -- parse :: FromQuery a => Wai.Request -> Either Operation.Error a
81 | -- parse req = fst $ eval parser req
82 |
--------------------------------------------------------------------------------
/old/Body/Applicative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Body.Applicative where
6 |
7 | -- import Data.ByteString qualified as BS
8 | -- import Data.List qualified as List
9 | -- import Data.Text qualified as Text
10 | -- import Data.Text.Encoding qualified as Text
11 | -- import Network.HTTP.Types qualified as HTTP
12 | -- import Web.HttpApiData qualified as Web
13 | -- import Network.Wai qualified as Wai
14 | -- import Network.Wai.Internal qualified as Wai
15 | -- import Okapi.Parser.Headers.Operation qualified as Operation
16 |
17 | -- data Parser a where
18 | -- FMap :: (a -> b) -> Parser a -> Parser b
19 | -- Pure :: a -> Parser a
20 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
21 | -- Optional :: Parser a -> Parser (Maybe a)
22 | -- Option :: a -> Parser a -> Parser a
23 | -- Operation :: Operation.Parser a -> Parser a
24 |
25 | -- instance Functor Parser where
26 | -- fmap = FMap
27 |
28 | -- instance Applicative Parser where
29 | -- pure = Pure
30 | -- (<*>) = Apply
31 |
32 | -- param :: Web.FromHttpApiData a => HTTP.HeaderName -> Parser a
33 | -- param = Operation . Operation.Param
34 |
35 | -- cookie :: BS.ByteString -> Parser ()
36 | -- cookie = Operation . Operation.Cookie
37 |
38 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
39 | -- optional = Optional
40 |
41 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
42 | -- option = Option
43 |
44 | -- eval ::
45 | -- Parser a ->
46 | -- Wai.Request ->
47 | -- (Either Operation.Error a, Wai.Request)
48 | -- eval (FMap f opX) state = case eval opX state of
49 | -- (Left e, state') -> (Left e, state')
50 | -- (Right x, state') -> (Right $ f x, state')
51 | -- eval (Pure x) state = (Right x, state)
52 | -- eval (Apply opF opX) state = case eval opF state of
53 | -- (Right f, state') -> case eval opX state' of
54 | -- (Right x, state'') -> (Right $ f x, state'')
55 | -- (Left e, state'') -> (Left e, state'')
56 | -- (Left e, state') -> (Left e, state')
57 | -- eval (Optional op) state = case op of
58 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
59 | -- (Right result, state') -> (Right $ Just result, state')
60 | -- (_, state') -> (Right Nothing, state')
61 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
62 | -- (Right result, state') -> (Right $ Just result, state')
63 | -- (_, state') -> (Right Nothing, state')
64 | -- _ -> case eval op state of
65 | -- (Right result, state') -> (Right $ Just result, state')
66 | -- (Left err, state') -> (Left err, state')
67 | -- eval (Option def op) state = case op of
68 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
69 | -- (Right result, state') -> (Right result, state')
70 | -- (_, state') -> (Right def, state')
71 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
72 | -- (Right result, state') -> (Right result, state')
73 | -- (_, state') -> (Right def, state')
74 | -- _ -> eval op state
75 | -- eval (Operation op) state = Operation.eval op state
76 |
77 | -- class FromQuery a where
78 | -- parser :: Parser a
79 |
80 | -- parse :: FromQuery a => Wai.Request -> Either Operation.Error a
81 | -- parse req = fst $ eval parser req
82 |
--------------------------------------------------------------------------------
/old/Headers/Applicative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Headers.Applicative where
6 |
7 | -- import Data.ByteString qualified as BS
8 | -- import Data.List qualified as List
9 | -- import Data.Text qualified as Text
10 | -- import Data.Text.Encoding qualified as Text
11 | -- import Network.HTTP.Types qualified as HTTP
12 | -- import Web.HttpApiData qualified as Web
13 | -- import Network.Wai qualified as Wai
14 | -- import Network.Wai.Internal qualified as Wai
15 | -- import Okapi.Parser.Headers.Operation qualified as Operation
16 |
17 | -- data Parser a where
18 | -- FMap :: (a -> b) -> Parser a -> Parser b
19 | -- Pure :: a -> Parser a
20 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
21 | -- Optional :: Parser a -> Parser (Maybe a)
22 | -- Option :: a -> Parser a -> Parser a
23 | -- Operation :: Operation.Parser a -> Parser a
24 |
25 | -- instance Functor Parser where
26 | -- fmap = FMap
27 |
28 | -- instance Applicative Parser where
29 | -- pure = Pure
30 | -- (<*>) = Apply
31 |
32 | -- param :: Web.FromHttpApiData a => HTTP.HeaderName -> Parser a
33 | -- param = Operation . Operation.Param
34 |
35 | -- cookie :: BS.ByteString -> Parser ()
36 | -- cookie = Operation . Operation.Cookie
37 |
38 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
39 | -- optional = Optional
40 |
41 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
42 | -- option = Option
43 |
44 | -- eval ::
45 | -- Parser a ->
46 | -- Wai.Request ->
47 | -- (Either Operation.Error a, Wai.Request)
48 | -- eval (FMap f opX) state = case eval opX state of
49 | -- (Left e, state') -> (Left e, state')
50 | -- (Right x, state') -> (Right $ f x, state')
51 | -- eval (Pure x) state = (Right x, state)
52 | -- eval (Apply opF opX) state = case eval opF state of
53 | -- (Right f, state') -> case eval opX state' of
54 | -- (Right x, state'') -> (Right $ f x, state'')
55 | -- (Left e, state'') -> (Left e, state'')
56 | -- (Left e, state') -> (Left e, state')
57 | -- eval (Optional op) state = case op of
58 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
59 | -- (Right result, state') -> (Right $ Just result, state')
60 | -- (_, state') -> (Right Nothing, state')
61 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
62 | -- (Right result, state') -> (Right $ Just result, state')
63 | -- (_, state') -> (Right Nothing, state')
64 | -- _ -> case eval op state of
65 | -- (Right result, state') -> (Right $ Just result, state')
66 | -- (Left err, state') -> (Left err, state')
67 | -- eval (Option def op) state = case op of
68 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
69 | -- (Right result, state') -> (Right result, state')
70 | -- (_, state') -> (Right def, state')
71 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
72 | -- (Right result, state') -> (Right result, state')
73 | -- (_, state') -> (Right def, state')
74 | -- _ -> eval op state
75 | -- eval (Operation op) state = Operation.eval op state
76 |
77 | -- class FromQuery a where
78 | -- parser :: Parser a
79 |
80 | -- parse :: FromQuery a => Wai.Request -> Either Operation.Error a
81 | -- parse req = fst $ eval parser req
82 |
--------------------------------------------------------------------------------
/lib/okapi.cabal:
--------------------------------------------------------------------------------
1 | cabal-version: 3.6
2 |
3 | -- This file has been generated from package.yaml by hpack version 0.34.4.
4 | --
5 | -- see: https://github.com/sol/hpack
6 |
7 | name: okapi
8 | version: 0.2.0.0
9 | synopsis: A micro web framework based on monadic parsing
10 | description: Please see the README on GitHub at
11 | category: Web
12 | homepage: https://github.com/monadicsystems/okapi#readme
13 | bug-reports: https://github.com/monadicsystems/okapi/issues
14 | author: Monadic Systems LLC
15 | maintainer: tech@monadic.systems
16 | copyright: 2022 Monadic Systems LLC
17 | license: BSD-3-Clause
18 | license-file: LICENSE
19 | build-type: Simple
20 | extra-source-files:
21 | README.md
22 | ChangeLog.md
23 |
24 | source-repository head
25 | type: git
26 | location: https://github.com/monadicsystems/okapi
27 |
28 | library
29 | exposed-modules:
30 | Okapi
31 | Okapi.Middleware
32 | Okapi.Route.Pattern
33 | Okapi.Headers
34 | Okapi.Query
35 | Okapi.Body
36 | Okapi.App
37 | Okapi.Route
38 | Okapi.Response
39 | other-modules:
40 | Paths_okapi
41 | hs-source-dirs:
42 | src
43 | build-depends:
44 | aeson
45 | , base >=4.7 && <5
46 | , base64
47 | , binary
48 | , bytestring
49 | , case-insensitive
50 | , containers
51 | , cookie
52 | , extra
53 | , http-api-data
54 | , http-types
55 | , natural-transformation
56 | , network
57 | , pretty-simple
58 | , regex-tdfa
59 | , text
60 | , vault
61 | , wai
62 | , wai-extra
63 | , wai-logger
64 | , warp
65 | default-language: Haskell2010
66 |
67 | executable hello-world
68 | main-is: Main.hs
69 | hs-source-dirs:
70 | examples/hello-world
71 | build-depends:
72 | base
73 | , okapi
74 | , warp
75 | , wai
76 | , text
77 | , http-api-data
78 | , http-types
79 | ghc-options: -threaded -rtsopts -with-rtsopts=-N
80 | default-language: Haskell2010
81 |
82 | executable calculator
83 | main-is: Main.hs
84 | hs-source-dirs:
85 | examples/calculator
86 | build-depends:
87 | base
88 | , okapi
89 | , warp
90 | , wai
91 | , text
92 | , http-api-data
93 | , http-types
94 | ghc-options: -threaded -rtsopts -with-rtsopts=-N
95 | default-language: Haskell2010
96 |
97 | executable bookstore
98 | main-is: Main.hs
99 | hs-source-dirs:
100 | examples/bookstore
101 | build-depends:
102 | base
103 | , aeson
104 | , okapi
105 | , warp
106 | , wai
107 | , text
108 | , http-api-data
109 | , http-types
110 | ghc-options: -threaded -rtsopts -with-rtsopts=-N
111 | default-language: Haskell2010
112 |
113 | executable tick
114 | main-is: Main.hs
115 | hs-source-dirs:
116 | examples/tick
117 | build-depends:
118 | base
119 | , okapi
120 | , warp
121 | , wai
122 | , text
123 | , http-api-data
124 | , http-types
125 | , wai-extra
126 | , binary
127 | ghc-options: -threaded -rtsopts -with-rtsopts=-N
128 | default-language: Haskell2010
129 |
130 | test-suite okapi-test
131 | type: exitcode-stdio-1.0
132 | main-is: Spec.hs
133 | other-modules:
134 | Paths_okapi
135 | hs-source-dirs:
136 | test
137 | ghc-options: -threaded -rtsopts -with-rtsopts=-N
138 | build-depends:
139 | base >=4.7 && <5
140 | , hspec
141 | , text
142 | default-language: Haskell2010
143 |
--------------------------------------------------------------------------------
/old/Query/Alternative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Query.Alternative where
6 |
7 | -- import Data.Bifunctor qualified as Bifunctor
8 | -- import Data.ByteString qualified as BS
9 | -- import Data.List qualified as List
10 | -- import Data.Text qualified as Text
11 | -- import Data.Text.Encoding qualified as Text
12 | -- import Network.HTTP.Types qualified as HTTP
13 | -- import Web.HttpApiData qualified as Web
14 | -- import Network.Wai qualified as Wai
15 | -- import Network.Wai.Internal qualified as Wai
16 | -- import Okapi.Parser.Query.Operation qualified as Operation
17 | -- import Control.Applicative (Alternative(..))
18 | -- import Okapi.Tree qualified as Tree
19 |
20 | -- data Parser a where
21 | -- FMap :: (a -> b) -> Parser a -> Parser b
22 | -- Pure :: a -> Parser a
23 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
24 | -- Empty :: Parser a
25 | -- Or :: Parser a -> Parser a -> Parser a
26 | -- Optional :: Parser a -> Parser (Maybe a)
27 | -- Option :: a -> Parser a -> Parser a
28 | -- Operation :: Operation.Parser a -> Parser a
29 |
30 | -- instance Functor Parser where
31 | -- fmap = FMap
32 |
33 | -- instance Applicative Parser where
34 | -- pure = Pure
35 | -- (<*>) = Apply
36 |
37 | -- instance Alternative Parser where
38 | -- empty = Empty
39 | -- (<|>) = Or
40 |
41 | -- param :: Web.FromHttpApiData a => BS.ByteString -> Parser a
42 | -- param = Operation . Operation.Param
43 |
44 | -- flag :: BS.ByteString -> Parser ()
45 | -- flag = Operation . Operation.Flag
46 |
47 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
48 | -- optional = Optional
49 |
50 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
51 | -- option = Option
52 |
53 | -- eval ::
54 | -- Parser a ->
55 | -- Wai.Request ->
56 | -- (Either (Tree.Tree Operation.Error) a, Wai.Request)
57 | -- eval (FMap f opX) state = case eval opX state of
58 | -- (Left e, state') -> (Left e, state')
59 | -- (Right x, state') -> (Right $ f x, state')
60 | -- eval (Pure x) state = (Right x, state)
61 | -- eval (Apply opF opX) state = case eval opF state of
62 | -- (Right f, state') -> case eval opX state' of
63 | -- (Right x, state'') -> (Right $ f x, state'')
64 | -- (Left e, state'') -> (Left e, state'')
65 | -- (Left e, state') -> (Left e, state')
66 | -- eval Empty state = (Left Tree.Nil, state)
67 | -- eval (Or opA opB) state = case eval opA state of
68 | -- (Right a, state') -> (Right a, state')
69 | -- (Left l, state') -> case eval opB state' of
70 | -- (Right b, state'') -> (Right b, state'')
71 | -- (Left r, state'') -> (Left (l Tree.:|: r), state'')
72 | -- eval (Optional op) state = case op of
73 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
74 | -- (Right result, state') -> (Right $ Just result, state')
75 | -- (_, state') -> (Right Nothing, state')
76 | -- Operation flag@(Operation.Flag _) -> case Operation.eval flag state of
77 | -- (Right result, state') -> (Right $ Just result, state')
78 | -- (_, state') -> (Right Nothing, state')
79 | -- _ -> case eval op state of
80 | -- (Right result, state') -> (Right $ Just result, state')
81 | -- (Left err, state') -> (Left err, state')
82 | -- eval (Option def op) state = case op of
83 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
84 | -- (Right result, state') -> (Right result, state')
85 | -- (_, state') -> (Right def, state')
86 | -- Operation flag@(Operation.Flag _) -> case Operation.eval flag state of
87 | -- (Right result, state') -> (Right result, state')
88 | -- (_, state') -> (Right def, state')
89 | -- _ -> eval op state
90 | -- eval (Operation op) state = Bifunctor.first (Bifunctor.first Tree.Leaf) $ Operation.eval op state
91 |
92 | -- class FromQuery a where
93 | -- parser :: Parser a
94 |
95 | -- parse :: FromQuery a => Wai.Request -> Either (Tree.Tree Operation.Error) a
96 | -- parse req = fst $ eval parser req
97 |
--------------------------------------------------------------------------------
/old/Body/Alternative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Body.Alternative where
6 |
7 | -- import Data.Bifunctor qualified as Bifunctor
8 | -- import Data.ByteString qualified as BS
9 | -- import Data.List qualified as List
10 | -- import Data.Text qualified as Text
11 | -- import Data.Text.Encoding qualified as Text
12 | -- import Network.HTTP.Types qualified as HTTP
13 | -- import Web.HttpApiData qualified as Web
14 | -- import Network.Wai qualified as Wai
15 | -- import Network.Wai.Internal qualified as Wai
16 | -- import Okapi.Parser.Headers.Operation qualified as Operation
17 | -- import Control.Applicative (Alternative(..))
18 | -- import Okapi.Tree qualified as Tree
19 |
20 | -- data Parser a where
21 | -- FMap :: (a -> b) -> Parser a -> Parser b
22 | -- Pure :: a -> Parser a
23 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
24 | -- Empty :: Parser a
25 | -- Or :: Parser a -> Parser a -> Parser a
26 | -- Optional :: Parser a -> Parser (Maybe a)
27 | -- Option :: a -> Parser a -> Parser a
28 | -- Operation :: Operation.Parser a -> Parser a
29 |
30 | -- instance Functor Parser where
31 | -- fmap = FMap
32 |
33 | -- instance Applicative Parser where
34 | -- pure = Pure
35 | -- (<*>) = Apply
36 |
37 | -- instance Alternative Parser where
38 | -- empty = Empty
39 | -- (<|>) = Or
40 |
41 | -- param :: Web.FromHttpApiData a => HTTP.HeaderName -> Parser a
42 | -- param = Operation . Operation.Param
43 |
44 | -- cookie :: BS.ByteString -> Parser ()
45 | -- cookie = Operation . Operation.Cookie
46 |
47 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
48 | -- optional = Optional
49 |
50 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
51 | -- option = Option
52 |
53 | -- eval ::
54 | -- Parser a ->
55 | -- Wai.Request ->
56 | -- (Either (Tree.Tree Operation.Error) a, Wai.Request)
57 | -- eval (FMap f opX) state = case eval opX state of
58 | -- (Left e, state') -> (Left e, state')
59 | -- (Right x, state') -> (Right $ f x, state')
60 | -- eval (Pure x) state = (Right x, state)
61 | -- eval (Apply opF opX) state = case eval opF state of
62 | -- (Right f, state') -> case eval opX state' of
63 | -- (Right x, state'') -> (Right $ f x, state'')
64 | -- (Left e, state'') -> (Left e, state'')
65 | -- (Left e, state') -> (Left e, state')
66 | -- eval Empty state = (Left Tree.Nil, state)
67 | -- eval (Or opA opB) state = case eval opA state of
68 | -- (Right a, state') -> (Right a, state')
69 | -- (Left l, state') -> case eval opB state' of
70 | -- (Right b, state'') -> (Right b, state'')
71 | -- (Left r, state'') -> (Left (l Tree.:|: r), state'')
72 | -- eval (Optional op) state = case op of
73 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
74 | -- (Right result, state') -> (Right $ Just result, state')
75 | -- (_, state') -> (Right Nothing, state')
76 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
77 | -- (Right result, state') -> (Right $ Just result, state')
78 | -- (_, state') -> (Right Nothing, state')
79 | -- _ -> case eval op state of
80 | -- (Right result, state') -> (Right $ Just result, state')
81 | -- (Left err, state') -> (Left err, state')
82 | -- eval (Option def op) state = case op of
83 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
84 | -- (Right result, state') -> (Right result, state')
85 | -- (_, state') -> (Right def, state')
86 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
87 | -- (Right result, state') -> (Right result, state')
88 | -- (_, state') -> (Right def, state')
89 | -- _ -> eval op state
90 | -- eval (Operation op) state = Bifunctor.first (Bifunctor.first Tree.Leaf) $ Operation.eval op state
91 |
92 | -- class FromQuery a where
93 | -- parser :: Parser a
94 |
95 | -- parse :: FromQuery a => Wai.Request -> Either (Tree.Tree Operation.Error) a
96 | -- parse req = fst $ eval parser req
97 |
--------------------------------------------------------------------------------
/old/Headers/Alternative.hs:
--------------------------------------------------------------------------------
1 | -- {-# LANGUAGE GADTs #-}
2 | -- {-# LANGUAGE ImportQualifiedPost #-}
3 | -- {-# LANGUAGE OverloadedRecordDot #-}
4 |
5 | -- module Okapi.Parser.Headers.Alternative where
6 |
7 | -- import Data.Bifunctor qualified as Bifunctor
8 | -- import Data.ByteString qualified as BS
9 | -- import Data.List qualified as List
10 | -- import Data.Text qualified as Text
11 | -- import Data.Text.Encoding qualified as Text
12 | -- import Network.HTTP.Types qualified as HTTP
13 | -- import Web.HttpApiData qualified as Web
14 | -- import Network.Wai qualified as Wai
15 | -- import Network.Wai.Internal qualified as Wai
16 | -- import Okapi.Parser.Headers.Operation qualified as Operation
17 | -- import Control.Applicative (Alternative(..))
18 | -- import Okapi.Tree qualified as Tree
19 |
20 | -- data Parser a where
21 | -- FMap :: (a -> b) -> Parser a -> Parser b
22 | -- Pure :: a -> Parser a
23 | -- Apply :: Parser (a -> b) -> Parser a -> Parser b
24 | -- Empty :: Parser a
25 | -- Or :: Parser a -> Parser a -> Parser a
26 | -- Optional :: Parser a -> Parser (Maybe a)
27 | -- Option :: a -> Parser a -> Parser a
28 | -- Operation :: Operation.Parser a -> Parser a
29 |
30 | -- instance Functor Parser where
31 | -- fmap = FMap
32 |
33 | -- instance Applicative Parser where
34 | -- pure = Pure
35 | -- (<*>) = Apply
36 |
37 | -- instance Alternative Parser where
38 | -- empty = Empty
39 | -- (<|>) = Or
40 |
41 | -- param :: Web.FromHttpApiData a => HTTP.HeaderName -> Parser a
42 | -- param = Operation . Operation.Param
43 |
44 | -- cookie :: BS.ByteString -> Parser ()
45 | -- cookie = Operation . Operation.Cookie
46 |
47 | -- optional :: Web.FromHttpApiData a => Parser a -> Parser (Maybe a)
48 | -- optional = Optional
49 |
50 | -- option :: Web.FromHttpApiData a => a -> Parser a -> Parser a
51 | -- option = Option
52 |
53 | -- eval ::
54 | -- Parser a ->
55 | -- Wai.Request ->
56 | -- (Either (Tree.Tree Operation.Error) a, Wai.Request)
57 | -- eval (FMap f opX) state = case eval opX state of
58 | -- (Left e, state') -> (Left e, state')
59 | -- (Right x, state') -> (Right $ f x, state')
60 | -- eval (Pure x) state = (Right x, state)
61 | -- eval (Apply opF opX) state = case eval opF state of
62 | -- (Right f, state') -> case eval opX state' of
63 | -- (Right x, state'') -> (Right $ f x, state'')
64 | -- (Left e, state'') -> (Left e, state'')
65 | -- (Left e, state') -> (Left e, state')
66 | -- eval Empty state = (Left Tree.Nil, state)
67 | -- eval (Or opA opB) state = case eval opA state of
68 | -- (Right a, state') -> (Right a, state')
69 | -- (Left l, state') -> case eval opB state' of
70 | -- (Right b, state'') -> (Right b, state'')
71 | -- (Left r, state'') -> (Left (l Tree.:|: r), state'')
72 | -- eval (Optional op) state = case op of
73 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
74 | -- (Right result, state') -> (Right $ Just result, state')
75 | -- (_, state') -> (Right Nothing, state')
76 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
77 | -- (Right result, state') -> (Right $ Just result, state')
78 | -- (_, state') -> (Right Nothing, state')
79 | -- _ -> case eval op state of
80 | -- (Right result, state') -> (Right $ Just result, state')
81 | -- (Left err, state') -> (Left err, state')
82 | -- eval (Option def op) state = case op of
83 | -- Operation param@(Operation.Param _) -> case Operation.eval param state of
84 | -- (Right result, state') -> (Right result, state')
85 | -- (_, state') -> (Right def, state')
86 | -- Operation cookie@(Operation.Cookie _) -> case Operation.eval cookie state of
87 | -- (Right result, state') -> (Right result, state')
88 | -- (_, state') -> (Right def, state')
89 | -- _ -> eval op state
90 | -- eval (Operation op) state = Bifunctor.first (Bifunctor.first Tree.Leaf) $ Operation.eval op state
91 |
92 | -- class FromQuery a where
93 | -- parser :: Parser a
94 |
95 | -- parse :: FromQuery a => Wai.Request -> Either (Tree.Tree Operation.Error) a
96 | -- parse req = fst $ eval parser req
97 |
--------------------------------------------------------------------------------
/lib/examples/bookstore/Main.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE DataKinds #-}
2 | {-# LANGUAGE OverloadedStrings #-}
3 | {-# LANGUAGE TypeApplications #-}
4 | {-# LANGUAGE DuplicateRecordFields #-}
5 | {-# LANGUAGE DeriveAnyClass #-}
6 | {-# LANGUAGE DeriveGeneric #-}
7 |
8 | module Main where
9 |
10 | import qualified GHC.Generics as Generics
11 | import qualified Data.Aeson as Aeson
12 | import qualified Data.Text as Text
13 | import qualified Network.HTTP.Types as HTTP
14 | import qualified Network.Wai as Wai
15 | import qualified Network.Wai.Handler.Warp as Warp
16 | import Okapi.App
17 | import Okapi.Response
18 | import qualified Web.HttpApiData as Web
19 |
20 | -- Data types representing books, authors, genres, and user preferences
21 | data Book = Book
22 | { bookId :: Int
23 | , title :: Text.Text
24 | , authorId :: Int
25 | , genreId :: Int
26 | }
27 | deriving (Generics.Generic, Aeson.ToJSON, Show)
28 | data Author = Author
29 | { authorId :: Int
30 | , authorName :: Text.Text
31 | }
32 | deriving (Generics.Generic, Aeson.ToJSON, Show)
33 | data Genre = Genre
34 | { genreId :: Int
35 | , genreName :: Text.Text
36 | }
37 | deriving (Generics.Generic, Aeson.ToJSON, Show)
38 | data UserPreference = UserPreference
39 | { userId :: Int
40 | , bookId :: Int
41 | }
42 | deriving (Generics.Generic, Aeson.ToJSON, Show)
43 |
44 | -- API for listing books, authors, and genres
45 | bookstoreApi =
46 | choice
47 | [ lit "books"
48 | $ choice
49 | [ lit "list"
50 | . responder @200 @'[] @Aeson.Value @[Book]
51 | . method HTTP.GET id
52 | $ \ok _req ->
53 | return $ ok noHeaders [Book 1 "The Hobbit" 1 1, Book 2 "1984" 2 2]
54 | , lit "details"
55 | . param @Int
56 | . responder @200 @'[] @Aeson.Value @Book
57 | . responder @500 @'[] @Aeson.Value @Text.Text
58 | . method HTTP.GET id
59 | $ \bookId ok bookNotFound _req ->
60 | return $ case findBook bookId of
61 | Just book -> ok noHeaders book
62 | Nothing -> bookNotFound noHeaders "Book not found"
63 | ]
64 | , lit "authors"
65 | . responder @200 @'[] @Aeson.Value @[Author]
66 | . method HTTP.GET id
67 | $ \ok _req ->
68 | return $ ok noHeaders [Author 1 "J.R.R. Tolkien", Author 2 "George Orwell"]
69 | , lit "genres"
70 | . responder @200 @'[] @Aeson.Value @[Genre]
71 | . method HTTP.GET id
72 | $ \ok _req ->
73 | return $ ok noHeaders [Genre 1 "Fantasy", Genre 2 "Dystopian"]
74 | ]
75 |
76 | -- API for user preferences
77 | userApi =
78 | lit "user"
79 | $ choice
80 | [ lit "preferences"
81 | -- . authenticateUser -- Middleware for user authentication
82 | . param @Int
83 | . responder @200 @'[] @Aeson.Value @[Book]
84 | . responder @500 @'[] @Text.Text @Text.Text
85 | . method HTTP.GET id
86 | $ \userId ok userNotFound _req ->
87 | return $ case getUserPreferences userId of
88 | Just preferences -> ok noHeaders preferences
89 | Nothing -> userNotFound noHeaders "User not found"
90 | ]
91 |
92 | -- Combining the Bookstore and User APIs
93 | api = choice [bookstoreApi, userApi]
94 |
95 | -- Helper function to find a book by ID (replace with database query)
96 | findBook :: Int -> Maybe Book
97 | findBook 1 = Just $ Book 1 "The Hobbit" 1 1
98 | findBook 2 = Just $ Book 2 "1984" 2 2
99 | findBook _ = Nothing
100 |
101 | -- Helper function to get user preferences (replace with database query)
102 | getUserPreferences :: Int -> Maybe [Book]
103 | getUserPreferences userId
104 | | userId == 1 = Just [Book 1 "The Hobbit" 1 1]
105 | | userId == 2 = Just [Book 2 "1984" 2 2]
106 | | otherwise = Nothing
107 |
108 | -- Run the API on port 8009
109 | main :: IO ()
110 | main = Warp.run 8009 . withDefault api $ \req resp ->
111 | resp $ Wai.responseLBS HTTP.status404 [] "Not Found..."
--------------------------------------------------------------------------------
/docs/pages/syntax.css:
--------------------------------------------------------------------------------
1 | pre>code.sourceCode {
2 | white-space: pre;
3 | position: relative;
4 | }
5 |
6 | pre>code.sourceCode>span {
7 | display: inline-block;
8 | line-height: 1.25;
9 | }
10 |
11 | pre>code.sourceCode>span:empty {
12 | height: 1.2em;
13 | }
14 |
15 | .sourceCode {
16 | overflow: visible;
17 | }
18 |
19 | code.sourceCode>span {
20 | color: inherit;
21 | text-decoration: inherit;
22 | }
23 |
24 | div.sourceCode {
25 | margin: 1em 0;
26 | padding: 1em;
27 | border: #204a87 2px solid;
28 | border-radius: 1em;
29 | }
30 |
31 | pre.sourceCode {
32 | margin: 0;
33 | }
34 |
35 | @media screen {
36 | div.sourceCode {
37 | overflow: auto;
38 | }
39 | }
40 |
41 | @media print {
42 | pre>code.sourceCode {
43 | white-space: pre-wrap;
44 | }
45 |
46 | pre>code.sourceCode>span {
47 | text-indent: -5em;
48 | padding-left: 5em;
49 | }
50 | }
51 |
52 | pre.numberSource code {
53 | counter-reset: source-line 0;
54 | }
55 |
56 | pre.numberSource code>span {
57 | position: relative;
58 | left: -4em;
59 | counter-increment: source-line;
60 | }
61 |
62 | pre.numberSource code>span>a:first-child::before {
63 | content: counter(source-line);
64 | position: relative;
65 | left: -1em;
66 | text-align: right;
67 | vertical-align: baseline;
68 | border: none;
69 | display: inline-block;
70 | -webkit-touch-callout: none;
71 | -webkit-user-select: none;
72 | -khtml-user-select: none;
73 | -moz-user-select: none;
74 | -ms-user-select: none;
75 | user-select: none;
76 | padding: 0 4px;
77 | width: 4em;
78 | color: #aaaaaa;
79 | }
80 |
81 | pre.numberSource {
82 | margin-left: 3em;
83 | border-left: 1px solid #aaaaaa;
84 | padding-left: 4px;
85 | }
86 |
87 | div.sourceCode {
88 | background-color: transparent;
89 | }
90 |
91 | @media screen {
92 | pre>code.sourceCode>span>a:first-child::before {
93 | text-decoration: underline;
94 | }
95 | }
96 |
97 | code span.al {
98 | color: #ef2929;
99 | }
100 |
101 | /* Alert */
102 | code span.an {
103 | color: #8f5902;
104 | font-weight: bold;
105 | font-style: italic;
106 | }
107 |
108 | /* Annotation */
109 | code span.at {
110 | color: #204a87;
111 | }
112 |
113 | /* Attribute */
114 | code span.bn {
115 | color: #0000cf;
116 | }
117 |
118 | /* BaseN */
119 | code span.cf {
120 | color: #204a87;
121 | font-weight: bold;
122 | }
123 |
124 | /* ControlFlow */
125 | code span.ch {
126 | color: #4e9a06;
127 | }
128 |
129 | /* Char */
130 | code span.cn {
131 | color: #8f5902;
132 | }
133 |
134 | /* Constant */
135 | code span.co {
136 | color: #8f5902;
137 | font-style: italic;
138 | }
139 |
140 | /* Comment */
141 | code span.cv {
142 | color: #8f5902;
143 | font-weight: bold;
144 | font-style: italic;
145 | }
146 |
147 | /* CommentVar */
148 | code span.do {
149 | color: #8f5902;
150 | font-weight: bold;
151 | font-style: italic;
152 | }
153 |
154 | /* Documentation */
155 | code span.dt {
156 | color: #204a87;
157 | }
158 |
159 | /* DataType */
160 | code span.dv {
161 | color: #0000cf;
162 | }
163 |
164 | /* DecVal */
165 | code span.er {
166 | color: #a40000;
167 | font-weight: bold;
168 | }
169 |
170 | /* Error */
171 | code span.ex {}
172 |
173 | /* Extension */
174 | code span.fl {
175 | color: #0000cf;
176 | }
177 |
178 | /* Float */
179 | code span.fu {
180 | color: #204a87;
181 | font-weight: bold;
182 | }
183 |
184 | /* Function */
185 | code span.im {}
186 |
187 | /* Import */
188 | code span.in {
189 | color: #8f5902;
190 | font-weight: bold;
191 | font-style: italic;
192 | }
193 |
194 | /* Information */
195 | code span.kw {
196 | color: #204a87;
197 | font-weight: bold;
198 | }
199 |
200 | /* Keyword */
201 | code span.op {
202 | color: #ce5c00;
203 | font-weight: bold;
204 | }
205 |
206 | /* Operator */
207 | code span.ot {
208 | color: #8f5902;
209 | }
210 |
211 | /* Other */
212 | code span.pp {
213 | color: #8f5902;
214 | font-style: italic;
215 | }
216 |
217 | /* Preprocessor */
218 | code span.sc {
219 | color: #ce5c00;
220 | font-weight: bold;
221 | }
222 |
223 | /* SpecialChar */
224 | code span.ss {
225 | color: #4e9a06;
226 | }
227 |
228 | /* SpecialString */
229 | code span.st {
230 | color: #4e9a06;
231 | }
232 |
233 | /* String */
234 | code span.va {
235 | color: #000000;
236 | }
237 |
238 | /* Variable */
239 | code span.vs {
240 | color: #4e9a06;
241 | }
242 |
243 | /* VerbatimString */
244 | code span.wa {
245 | color: #8f5902;
246 | font-weight: bold;
247 | font-style: italic;
248 | }
249 |
250 | /* Warning */
--------------------------------------------------------------------------------
/lib/src/Okapi/Response.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE AllowAmbiguousTypes #-}
2 | {-# LANGUAGE ApplicativeDo #-}
3 | {-# LANGUAGE BlockArguments #-}
4 | {-# LANGUAGE DataKinds #-}
5 | {-# LANGUAGE DeriveGeneric #-}
6 | {-# LANGUAGE FlexibleContexts #-}
7 | {-# LANGUAGE FlexibleInstances #-}
8 | {-# LANGUAGE FunctionalDependencies #-}
9 | {-# LANGUAGE GADTs #-}
10 | {-# LANGUAGE ImportQualifiedPost #-}
11 | {-# LANGUAGE LambdaCase #-}
12 | {-# LANGUAGE MultiParamTypeClasses #-}
13 | {-# LANGUAGE OverloadedStrings #-}
14 | {-# LANGUAGE PolyKinds #-}
15 | {-# LANGUAGE QualifiedDo #-}
16 | {-# LANGUAGE RankNTypes #-}
17 | {-# LANGUAGE RecordWildCards #-}
18 | {-# LANGUAGE ScopedTypeVariables #-}
19 | {-# LANGUAGE StandaloneKindSignatures #-}
20 | {-# LANGUAGE TypeApplications #-}
21 | {-# LANGUAGE TypeFamilies #-}
22 | {-# LANGUAGE TypeOperators #-}
23 | {-# LANGUAGE UndecidableInstances #-}
24 |
25 | module Okapi.Response where
26 |
27 | import Control.Natural qualified as Natural
28 | import Data.Aeson qualified as Aeson
29 | import Data.Binary.Builder qualified as Builder
30 | import Data.ByteString qualified as BS
31 | import Data.ByteString.Char8 qualified as Char8
32 | import Data.ByteString.Lazy qualified as LBS
33 | import Data.ByteString.Lazy.Char8 qualified as LBSChar8
34 | import Data.CaseInsensitive qualified as CI
35 | import Data.Functor.Identity qualified as Identity
36 | import Data.Kind
37 | import Data.List qualified as List
38 | import Data.List.NonEmpty qualified as NonEmpty
39 | import Data.Text qualified as Text
40 | import Data.Text.Lazy qualified as LText
41 | import Data.Text.Lazy.Encoding qualified as Text
42 | import Data.Tree qualified as Tree
43 | import Data.Type.Equality qualified as Equality
44 | import Data.Typeable qualified as Typeable
45 | import Data.Vault.Lazy qualified as Vault
46 | import GHC.Exts qualified as Exts
47 | import GHC.Generics qualified as Generics
48 | import GHC.Natural qualified as Natural
49 | import GHC.TypeLits qualified as TypeLits
50 | import GHC.TypeNats qualified as Nat
51 | import Network.HTTP.Types qualified as HTTP
52 | import Network.Wai qualified as Wai
53 | import Okapi.Headers qualified as Headers
54 | import Okapi.Route qualified as Route
55 |
56 | import Web.HttpApiData qualified as Web
57 |
58 | data Headers (headerKeys :: [Exts.Symbol]) where
59 | NoHeaders :: Headers '[]
60 | InsertHeader ::
61 | forall (headerKey :: Exts.Symbol) headerValue (headerKeys :: [Exts.Symbol]).
62 | (TypeLits.KnownSymbol headerKey, Web.ToHttpApiData headerValue) =>
63 | headerValue ->
64 | Headers headerKeys ->
65 | Headers (headerKey : headerKeys)
66 |
67 | noHeaders :: Headers '[]
68 | noHeaders = NoHeaders
69 |
70 | insertHeader ::
71 | forall (headerKey :: Exts.Symbol) headerValue (headerKeys :: [Exts.Symbol]).
72 | (TypeLits.KnownSymbol headerKey, Web.ToHttpApiData headerValue) =>
73 | headerValue ->
74 | Headers headerKeys ->
75 | Headers (headerKey : headerKeys)
76 | insertHeader = InsertHeader
77 |
78 | data HeaderKey (k :: Exts.Symbol) = HeaderKey
79 |
80 | -- | Membership test a type class (predicate)
81 | class IsMember (headerKey :: Exts.Symbol) (headerKeys :: [Exts.Symbol]) where
82 | -- | Value-level lookup of elements from a map, via type class predicate
83 | lookupHeader :: HeaderKey headerKey -> Headers headerKeys -> BS.ByteString
84 |
85 | instance {-# OVERLAPS #-} IsMember headerKey (headerKey ': rest) where
86 | lookupHeader _ (InsertHeader v _) = Web.toHeader v
87 |
88 | instance {-# OVERLAPPABLE #-} (IsMember headerKey headerKeys) => IsMember headerKey (otherHeaderKey ': headerKeys) where
89 | lookupHeader k (InsertHeader _ tail) = lookupHeader k tail
90 |
91 | class WaiResponseHeaders (headerKeys :: [Exts.Symbol]) where
92 | toWaiResponseHeaders :: Headers headerKeys -> HTTP.ResponseHeaders
93 |
94 | instance {-# OVERLAPS #-} WaiResponseHeaders '[] where
95 | toWaiResponseHeaders _ = []
96 |
97 | instance {-# OVERLAPPABLE #-} (WaiResponseHeaders headerKeys) => WaiResponseHeaders (headerKey ': headerKeys) where
98 | toWaiResponseHeaders (InsertHeader v tail) = [(CI.mk . Char8.pack $ TypeLits.symbolVal @headerKey Typeable.Proxy, Web.toHeader v)]
99 |
100 | data Body
101 | = BodyStream Wai.StreamingBody
102 | | BodyBuilder Builder.Builder
103 | | BodyBytes LBS.ByteString
104 | | BodyFile FilePath (Maybe Wai.FilePart)
105 |
106 | class ContentType a where
107 | contentTypeName :: BS.ByteString
108 | contentTypeBody :: a -> Body
109 |
110 | instance ContentType Text.Text where
111 | contentTypeName = "text/plain"
112 | contentTypeBody = BodyBytes . Text.encodeUtf8 . LText.fromStrict
113 |
114 | instance ContentType Aeson.Value where
115 | contentTypeName = "application/json"
116 | contentTypeBody = BodyBytes . Aeson.encode
117 |
118 | class (ContentType a) => ToContentType a b where
119 | toContentType :: b -> a
120 |
121 | instance ToContentType Text.Text Text.Text where
122 | toContentType = id
123 |
124 | instance ToContentType Text.Text Int where
125 | toContentType = Text.pack . show
126 |
127 | instance (Aeson.ToJSON a) => ToContentType Aeson.Value a where
128 | toContentType = Aeson.toJSON
129 |
130 | data Response where
131 | Response ::
132 | forall (status :: Natural.Natural) (headerKeys :: [Exts.Symbol]) (contentType :: Type) (resultType :: Type).
133 | (ContentType contentType, ToContentType contentType resultType, Typeable.Typeable headerKeys, Typeable.Typeable resultType) =>
134 | Response
135 |
136 | natToStatus :: Nat.Nat -> HTTP.Status
137 | natToStatus n = toEnum $ fromEnum n
138 |
139 | makeResponder ::
140 | forall (status :: Natural.Natural) (headerKeys :: [Exts.Symbol]) (contentType :: Type) (resultType :: Type).
141 | (Nat.KnownNat status, WaiResponseHeaders headerKeys, ContentType contentType, ToContentType contentType resultType, Typeable.Typeable headerKeys, Typeable.Typeable resultType) =>
142 | (Headers headerKeys -> resultType -> Wai.Response)
143 | makeResponder headerMap result =
144 | let status = natToStatus $ Nat.natVal @status Typeable.Proxy
145 | contentType = toContentType @contentType @resultType result
146 | bodyType = contentTypeBody @contentType contentType
147 | name = contentTypeName @contentType
148 | headers = ("Content-Type", name) : toWaiResponseHeaders headerMap
149 | in case bodyType of
150 | BodyBytes bytes -> Wai.responseLBS status headers bytes
151 | BodyBuilder builder -> Wai.responseBuilder status headers builder
152 | BodyStream stream -> Wai.responseStream status headers stream
153 | BodyFile path part -> Wai.responseFile status headers path part
154 |
--------------------------------------------------------------------------------
/lib/src/Okapi.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE AllowAmbiguousTypes #-}
2 | {-# LANGUAGE ApplicativeDo #-}
3 | {-# LANGUAGE BlockArguments #-}
4 | {-# LANGUAGE DataKinds #-}
5 | {-# LANGUAGE FlexibleContexts #-}
6 | {-# LANGUAGE FlexibleInstances #-}
7 | {-# LANGUAGE GADTs #-}
8 | {-# LANGUAGE ImportQualifiedPost #-}
9 | {-# LANGUAGE LambdaCase #-}
10 | {-# LANGUAGE MultiParamTypeClasses #-}
11 | {-# LANGUAGE OverloadedStrings #-}
12 | {-# LANGUAGE PolyKinds #-}
13 | {-# LANGUAGE QualifiedDo #-}
14 | {-# LANGUAGE RankNTypes #-}
15 | {-# LANGUAGE RecordWildCards #-}
16 | {-# LANGUAGE ScopedTypeVariables #-}
17 | {-# LANGUAGE TypeApplications #-}
18 | {-# LANGUAGE TypeFamilies #-}
19 | {-# LANGUAGE TypeOperators #-}
20 |
21 | module Okapi where
22 |
23 | import Control.Natural qualified as Natural
24 | import Data.ByteString.Lazy qualified as LBS
25 | import Data.ByteString.Lazy.Char8 qualified as LBSChar8
26 | import Data.Functor.Identity qualified as Identity
27 | import Data.List qualified as List
28 | import Data.List.NonEmpty qualified as NonEmpty
29 | import Data.Text qualified as Text
30 | import Data.Tree qualified as Tree
31 | import Data.Typeable qualified as Typeable
32 | import Data.Vault.Lazy qualified as Vault
33 | import Network.HTTP.Types qualified as HTTP
34 | import Network.Wai qualified as Wai
35 | import Network.Wai.Handler.Warp qualified as Warp
36 | import Network.Wai.Middleware.RequestLogger qualified as Wai
37 | import Okapi.App
38 | import Okapi.App qualified as App
39 | import Okapi.Headers qualified as Headers
40 | import Okapi.Route qualified as Route
41 |
42 | import Text.Pretty.Simple qualified as Pretty
43 | import Web.HttpApiData qualified as Web
44 |
45 | {-
46 | test1 :: IO ()
47 | test1 = do
48 | apiTreeRep <- forest testAPI
49 | putStrLn $ Tree.drawTree apiTreeRep
50 | where
51 | -- Warp.run 1234 $ (build testAPI id) backupWaiApp
52 |
53 | backupWaiApp = \req resp -> do
54 | resp $ Wai.responseLBS HTTP.status200 [] "The test app failed..."
55 | testAPI :: [App]
56 | testAPI =
57 | [ lit
58 | "" -- Won't be matched because you can't request http://localhost:1234/
59 | [ get_ id \req -> do
60 | return $ Wai.responseLBS HTTP.status200 [] "The trailing slash"
61 | ],
62 | lit
63 | "hello"
64 | [ get_ id \req -> do
65 | return $ Wai.responseLBS HTTP.status200 [] "world",
66 | lit
67 | ""
68 | [ get_ id \req -> do
69 | return $ Wai.responseLBS HTTP.status200 [] "Trailing slash after \"hello\""
70 | ],
71 | lit
72 | "world"
73 | [ get_ id \req -> do
74 | return $ Wai.responseLBS HTTP.status200 [] "!"
75 | ]
76 | ],
77 | get_ id \req -> do
78 | return $ Wai.responseLBS HTTP.status200 [] "You made a GET request to :ROOT:"
79 | ]
80 |
81 | test2 :: IO ()
82 | test2 = do
83 | apiTreeRep <- forest testAPI
84 | putStrLn $ Tree.drawTree apiTreeRep
85 | where
86 | -- Warp.run 1234 $ (build testAPI id) backupWaiApp
87 |
88 | backupWaiApp = \req resp -> do
89 | resp $ Wai.responseLBS HTTP.status200 [] "The test app failed..."
90 | testAPI :: [App]
91 | testAPI =
92 | lit
93 | "" -- Won't be matched because you can't request http://localhost:1234/
94 | [ get_ id \req -> do
95 | return $ Wai.responseLBS HTTP.status200 [] "The trailing slash"
96 | ]
97 | : lit
98 | "hello"
99 | [ get_ id \req -> do
100 | return $ Wai.responseLBS HTTP.status200 [] "world",
101 | lit
102 | ""
103 | [ get_ id \req -> do
104 | return $ Wai.responseLBS HTTP.status200 [] "Trailing slash after \"hello\""
105 | ],
106 | lit
107 | "world"
108 | [ get_ id \req -> do
109 | return $ Wai.responseLBS HTTP.status200 [] "!"
110 | ]
111 | ]
112 | : ( get_ id \req -> do
113 | return $ Wai.responseLBS HTTP.status200 [] "You made a GET request to :ROOT:"
114 | )
115 | : []
116 |
117 | test3 :: IO ()
118 | test3 = do
119 | apiTreeRep <- forest testAPI
120 | putStrLn $ Tree.drawTree apiTreeRep
121 | where
122 | -- Warp.run 1234 $ (build testAPI id) backupWaiApp
123 |
124 | backupWaiApp = \_ resp -> do
125 | resp $ Wai.responseLBS HTTP.status200 [] "The test app failed..."
126 | testAPI :: [App]
127 | testAPI =
128 | [ lit
129 | "numbers"
130 | [ lit
131 | "add"
132 | [ param @Int \xS ->
133 | [ param @Int \yS ->
134 | [ getIO_ \req -> do
135 | let magic = Secret.tell req
136 | x = magic xS
137 | y = magic yS
138 | return $ Wai.responseLBS HTTP.status200 [] $ LBSChar8.pack $ show (x + y)
139 | ]
140 | ]
141 | ],
142 | getIO_ \req -> do
143 | return $ Wai.responseLBS HTTP.status200 [] "Use /add, /sub, or /mul"
144 | ]
145 | ]
146 |
147 | data Op = Add | Sub | Mul
148 |
149 | instance Web.FromHttpApiData Op where
150 | parseUrlPiece "add" = Right Add
151 | parseUrlPiece "sub" = Right Sub
152 | parseUrlPiece "mul" = Right Mul
153 | parseUrlPiece _ = Left undefined
154 |
155 | test4 :: IO ()
156 | test4 = do
157 | apiTreeRep <- forest testAPI
158 | putStrLn $ Tree.drawTree apiTreeRep
159 | where
160 | -- Warp.run 1234 $ Wai.logStdoutDev $ build testAPI id backupWaiApp
161 |
162 | backupWaiApp = \_ resp -> do
163 | resp $ Wai.responseLBS HTTP.status404 [] "The test app failed..."
164 | testAPI :: [App]
165 | testAPI =
166 | [ lit
167 | "numbers"
168 | [ param @Op \opS ->
169 | [ param @Int \xS ->
170 | [ param @Int \yS ->
171 | [ getIO_ \req -> do
172 | let x = Secret.tell req xS
173 | y = Secret.tell req yS
174 | answer = case Secret.tell req opS of
175 | Add -> x + y
176 | Sub -> x - y
177 | Mul -> x * y
178 | return $ Wai.responseLBS HTTP.status200 [] $ LBSChar8.pack $ show answer
179 | ]
180 | ],
181 | getIO_ \req -> do
182 | return $ Wai.responseLBS HTTP.status200 [] $ case Secret.tell req opS of
183 | Add -> "Add two numbers."
184 | Sub -> "Subtract one number from another."
185 | Mul -> "Multiply two numbers."
186 | ],
187 | getIO_ \req -> do
188 | return $ Wai.responseLBS HTTP.status200 [] "Use /add, /sub, or /mul"
189 | ]
190 | ]
191 |
192 | instance Web.ToHttpApiData Op where
193 | toUrlPiece Add = "add"
194 | toUrlPiece Sub = "sub"
195 | toUrlPiece Mul = "mul"
196 |
197 | test5 :: IO ()
198 | test5 = do
199 | apiTreeRep <- forest testAPI
200 | -- apiEndpoints <- endpoints testAPI
201 | putStrLn $ Tree.drawTree apiTreeRep
202 | where
203 | -- Pretty.pPrint $ map curl $ List.reverse apiEndpoints
204 |
205 | -- Warp.run 1234 $ build testAPI id backupWaiApp
206 |
207 | backupWaiApp = \_ resp -> do
208 | resp $ Wai.responseLBS HTTP.status404 [] "The test app failed..."
209 | testAPI :: [App]
210 | testAPI =
211 | [ lit "numbers" $
212 | [ getIO_ \req -> do
213 | return $ Wai.responseLBS HTTP.status200 [] "Use /add, /sub, or /mul"
214 | ]
215 | ++ map opAPI [Add, Sub, Mul]
216 | ]
217 |
218 | opAPI :: Op -> App
219 | opAPI op =
220 | match
221 | op
222 | [ getIO_ \req -> do
223 | return $ Wai.responseLBS HTTP.status200 [] $ case op of
224 | Add -> "Add two numbers."
225 | Sub -> "Subtract one number from another."
226 | Mul -> "Multiply two numbers.",
227 | param @Int \xS ->
228 | [ param @Int \yS ->
229 | [ getIO_ \req -> do
230 | let x = Secret.tell req xS
231 | y = Secret.tell req yS
232 | answer = case op of
233 | Add -> x + y
234 | Sub -> x - y
235 | Mul -> x * y
236 | return $ Wai.responseLBS HTTP.status200 [] $ LBSChar8.pack $ show answer
237 | ]
238 | ]
239 | ++ case op of
240 | Mul ->
241 | [ getIO_ \req -> do
242 | let x = Secret.tell req xS
243 | return $ Wai.responseLBS HTTP.status200 [] $ LBSChar8.pack $ show (x * x)
244 | ]
245 | _ -> []
246 | ]
247 | -}
248 | -- test6 :: IO ()
249 | -- test6 = do
250 | -- apiTreeRep <- forest testAPI
251 | -- putStrLn $ Tree.drawTree apiTreeRep
252 | -- where
253 | -- backupWaiApp = \req resp -> do
254 | -- resp $ Wai.responseLBS HTTP.status200 [] "The test app failed..."
255 | -- testAPI :: [App]
256 | -- testAPI =
257 | -- [ endpoint HTTP.GET (do Route.lit "user";) id \_ req -> do
258 | -- undefined
259 | -- , endpoint HTTP.POST (do Route.lit "user"; id' <- Route.param @Int; return id') id \userIDS req -> do
260 | -- let userID = Secret.tell req userIDS
261 | -- undefined
262 | -- ]
263 |
--------------------------------------------------------------------------------
/lib/release.md:
--------------------------------------------------------------------------------
1 | ## Introduction
2 |
3 | Okapi is a microframework for web development in Haskell based on monadic parsing.
4 | The inspiration for Okapi originally came from looking at web frameworks in other language ecosystems such as Python’s Flask,
5 | Nim’s Jester, OCaml’s Dream, and F#’s Giraffe, which the name of this Haskell framework is related to.
6 | I noticed that many Haskell web frameworks tend to require a lot of boilerplate code, and/or make use of a lot of advanced type level language
7 | features that make it hard to understand the internals of the framework. The goal of Okapi is to create a Haskell web framework
8 | with an ergonomic developer experience that is idiomatic to the host language.
9 |
10 | ## Parsers
11 |
12 |
13 | In Haskell, a simple `String` parser can be modeled as a function with the type `String -> (Either ParserError a, String)`.
14 | This function takes values of type `String` and returns either a `ParserError` (if it fails) or a value of some type `a` (if it succeeds), along with a new `String`
15 | that's missing the characters that were consumed by the parsing function. We could use the function like so:
16 |
17 | ```haskell
18 | ```
19 |
20 | This is great, but issues start to arise when we try to compose parsers with other parsers. For example, let's say we wanted to parse blah blah:
21 |
22 | ```haskell
23 | ```
24 |
25 | To avoid the explicit passing of state from one parser to the next, we can use monads. You may have already noticed that the type of our parser can be simplified with the `State String` monad.
26 | We can transform our function of type `String -> (Either ParseError a, String)` to a value of type `State String (Either ParserError a)`. A value of type `State String (Either ParserError a)`
27 | represents a value of type `Either ParserError a` that was computed in a stateful context, where the state is of type `String`. Now that our parser is defined as a monad we can use `do` notation
28 | , and it becomes easier to compose our parsers with other parsers because we don't have to manually pass the state from a previous parser to the next one.
29 | Let's try the parser composition we tried above with our new parser definition:
30 |
31 | ```haskell
32 | ```
33 |
34 |
36 |
37 | As you can see our parsers compose a lot better, but we still have to explicitly handle the result of the parsers because they may return a `ParserError`.
38 | Functions that return values of the type `Either ParserError a` can be modelled using the `Except ParserError` monad. A value of the type `Except ParserError a`
39 | represents a value of type `a` that is computed in a context that may not succeed, but instead throw an error value of type `ParserError`. In our case we want
40 | our parser's computations to happen in a context in which there is state of type `String`, and the possibilty of throwing an error value of type `ParserError`.
41 | To get both of these useful abilities, let's combine the `Except ParserError` monad with our `State String` monad using monad transformers. Our simplified parser
42 | now has the type `ExceptT ParserError (State String a)`, where `ExceptT` is a monad transformer that gives our base `State String` monad the ability to throw error
43 | values of type `ParserError` upon failure. To make the code examples easier on our eyes, let's make a type synonym defined as `type Parser a = ExceptT ParserError (State String a)`.
44 | Now, any value anottated with the type `Parser a` represents a value of some type `a` that is computed in a context that has access to state of type `String` AND may throw error
45 | values of type `ParserError` upon failing. Let's redefine the example we defined above:
46 |
47 | ```haskell
48 | ```
49 |
50 | Great.
51 |
52 | ## HTTP Request Parsers
53 |
54 | Now, let's redefine `type Parser a = ExceptT ParserError (State String a)` as `type Parser a = ExceptT HTTPError (State HTTPRequest a)`. This is an HTTP Request Parser. Instead of parsing happening in a context where the computation has access to state of type `String` and can throw errors of type `ParserError`, it happens in a context where the computation has access to state of type `HTTPRequest` and can throw errors of type `HTTPError`. Just like the string parser above had a concept of "consuming" parts of a `String`, the HTTP request parser "consumes" values of the type `HTTPRequest`. By consume we mean .... If you break values of type `String` into its smallest consituents, you get values of type `Char`. A `String` value is a list of `Char` values. What are the smallest constituents of a `HTTPRequest` value? The data type `HTTPRequest` is defined as follows:
55 |
56 | ```haskell
57 | data HTTPRequest = HTTPRequest
58 | { method :: Method
59 | , path :: [Text]
60 | , query :: Query
61 | , body :: ByteString
62 | , headers :: Headers
63 | }
64 | ```
65 |
66 |
68 | Our HTTP request parser consumes different parts of the HTTP request like the `method` and `query`. Once a piece of the HTTP request is parsed, it is removed from the request before it is implicitly passed to the next parser.
69 |
70 | There are 2 types of parsers:
71 |
72 | 1. Data parsers
73 | 2. Checker parsers
74 |
75 | There are 5 types of parsers for each of the 5 parts of a HTTP request.
76 |
77 | 1. Method Parsers
78 |
79 | ```haskell
80 | method :: MonadOkapi m => m Method
81 |
82 | matchMethod :: MonadOkapi m => Method -> m ()
83 |
84 | get :: MonadOkapi m => m ()
85 | get = matchMethod "GET"
86 | ```
87 |
88 | 2. Path Parsers
89 |
90 | ```haskell
91 | path :: MonadOkapi m => m Path -- Parses entire remaining path
92 | path = many seg
93 |
94 | seg :: MonadOkapi m => m Text
95 |
96 | matchPath :: MonadOkapi m => Path -> m ()
97 | matchPath desiredPath = mapM_ matchSeg desiredPath
98 |
99 | matchSeg :: MonadOkapi m => Text -> m ()
100 |
101 | pathParam :: MonadOkapi m => FromHttpApiData a => m a
102 |
103 | pathEnd :: MonadOkapi m => m ()
104 | ```
105 |
106 | 4. Query Parsers
107 |
108 | ```haskell
109 | query :: MonadOkapi m => m Query -- parses entire query
110 |
111 | queryParam :: MonadOkapi m => FromHttpApiData a => Text -> m a
112 |
113 | queryFlag :: MonadOkapi m => Text -> m ()
114 |
115 | queryParamRaw :: Text -> m Text
116 | ```
117 |
118 | 6. Body Parsers
119 |
120 | ```haskell
121 | body :: MonadOkapi m => m Body
122 |
123 | bodyJSON :: MonadOkapi m, FromJSON a => m a
124 |
125 | bodyURLEncoded :: FromForm a, MonadOkapi m => m a
126 |
127 | bodyMultipart :: FromForm a, MonadOkapi m => m (a, [File])
128 | ```
129 |
130 | 8. Headers Parsers
131 |
132 | ```haskell
133 | headers :: MonadOkapi m => m Headers
134 |
135 | header :: MonadOkapi m => HeaderName -> m Header
136 |
137 | cookie :: MonadOkapi m => m Cookie
138 |
139 | crumb :: MonadOkapi m => Text -> m Crumb
140 | ```
141 |
142 | We can use these to create increasingly complex parsers. For example, let's say we wanted to implement a HTTP parser that matches the request `GET /blog`. That would look like this:
143 |
144 | ```haskell
145 | blogRoute :: Parser ()
146 | blogRoute = do
147 | get -- Make sure that the request is a GET request
148 | matchSeg "blog" -- Match against the path segment /blog
149 | pathEnd -- Make sure that there are no more path segments remaining in the request
150 | ```
151 |
152 | Just like earlier, with our monadic string parser, we can sequence HTTP request parsers using `do` notation. This request parser isn't really useful though because it doesn't return anything. Let's make it return a response:
153 |
154 | ```haskell
155 | blogRoute :: Parser HTTPResponse
156 | blogRoute = do
157 | get
158 | matchSeg "blog"
159 | pathEnd
160 | return ok
161 | ```
162 |
163 | Now if we run our parser, it will return a `200 OK` response if we send a `GET` request to the `/blog` endpoint. On top of being able to sequence parsers with `do` notation thanks to `Parser` being an instance of the `Monad` typeclass, we can also build parsers that "choice" between multiple subparsers. This is possible because the `Parser` type is also an instance of the `Alternative` typeclass, which provides the `<|>` operator.
164 |
165 | Explain `<|>` then explain we can also parser combinators like `many`, `some`, `optional`, `option`, `takeWhile`, etc.
166 |
167 | Then explain the two types of errors and how to throw and catch them.
168 |
169 | Then explain returning responses and executing a parser.
170 |
171 | Explaining type safe URLs with patterns:
172 |
173 | ## Patterns
174 |
175 | Okapi uses bi-directional patterns to have typesafe urls. So you would have something like:
176 |
177 | ```haskell
178 | -- Matches routes of the form /blog/99
179 | pattern BlogRoute :: Int -> Path
180 | pattern BlogRoute uuid <- ["blog", PathParam uuid]
181 | where
182 | BlogRoute uuid = ["blog", PathParam uuid]
183 | ```
184 |
185 | or just
186 |
187 | ```haskell
188 | -- Bidriectional Implicit
189 | pattern BlogRoute :: Int -> Path
190 | pattern BlogRoute uuid = ["blog", PathParam uuid]
191 |
192 | pattern BlogCategoryRoute :: Text -> Path
193 | pattern BlogCategoryRoute category = ["blog", PathParam category]
194 | ```
195 |
196 | uses these bidrectional patterns with the `route` parser, like so:
197 |
198 | ```haskell
199 | route :: MonadOkapi m => (Path -> m Response) -> m Response
200 | route matcher = do
201 | path <- parsePath
202 | matcher path
203 |
204 | myAPI :: MonadOkapi m => m Response
205 | myAPI = route $ \case
206 | BlogRoute uuid -> do
207 | get
208 | return ok
209 | BlogRouteCategory category -> do
210 | get
211 | mbOrderBy <- optional $ queryParam @Order "order"
212 | case mbOrderBy of
213 | Nothing -> do
214 | ...
215 | return ok
216 | Just orderBy -> do
217 | ...
218 | return ok
219 | _ -> next
220 | ```
221 |
222 | Since both routes are `GET` requests, let's factor out the `get` parser:
223 |
224 | ```haskell
225 | myAPI :: MonadOkapi m => m Response
226 | myAPI = do
227 | get
228 | route $ \case
229 | BlogRoute uuid -> return ok
230 | BlogRouteCategory category -> do
231 | mbOrderBy <- optional $ queryParam @Order "order"
232 | case mbOrderBy of
233 | Nothing -> do
234 | ...
235 | return ok
236 | Just orderBy -> do
237 | ...
238 | return ok
239 | _ -> next
240 | ```
241 |
242 | ## URLs
243 |
244 | There are two types of URLs that you can generate with Okapi:
245 |
246 | 1. Relative URLs
247 | 2. Absolute URLs
248 |
249 | ```haskell
250 | data URL = URL { unURL :: Text }
251 | data RelURL = RelURL Path Query
252 | data AbsURL = AbsURL Scheme Host (Maybe Port) RelURL
253 |
254 | class ToURL a where
255 | render :: a -> URL
256 |
257 | instance ToURL Path where
258 |
259 | instance ToURL Query where
260 |
261 | instance ToURL RelURL where
262 | render (RelURL p q) = render p <> render q
263 |
264 | instance ToURL AbsURL where
265 |
266 | instance ToURL Request where
267 |
268 | blogRouteCategoryURL = render $ BlogRouteCategory "fiction"
269 |
270 | class ToURL a => Matchable a where
271 | match :: (a -> m Response) -> m Response
272 |
273 | route :: (Path -> m Response) -> m Response
274 | routeWithQuery :: (RelURL -> m Response) -> m Response
275 | routeVirtual :: (AbsURL -> m Response) -> m Response
276 | ```
277 |
--------------------------------------------------------------------------------
/lib/NewDesign.md:
--------------------------------------------------------------------------------
1 | # Build A Web Framework in Haskell From Scratch
2 |
3 | ## Haskell for Backend Web Development
4 |
5 | ## What is WAI?
6 |
7 | ## The Simplest Possible Server
8 |
9 | ```haskell
10 | server :: Request -> Response
11 | ```
12 |
13 | ## Interacting With The Real World
14 |
15 | ```haskell
16 | server :: Monad m => Request -> m Response
17 | ```
18 |
19 | ```haskell
20 | server :: Request -> Identity Response
21 | ```
22 |
23 | ## Making Our Server Modular
24 |
25 | ```haskell
26 | server :: Reader Request Response
27 | ```
28 |
29 | ## Separating Effects
30 |
31 | Once concern with the Okapi monad is that I can interleave random `IO` actions in the route parser. This means the programmer has to be careful of where `IO` actions are executed. Once an `IO` action is executed, it can't be undone. Even with backtracking. In practice, we want to keep our route parser, and handler (which might use `IO`) separate.
32 |
33 | 1. The Router - The one and only job of the router is to **extract and verify the existence of data in the request**.
34 | 2. The Handler - The one and only job of the handler is to **accept data provided by the router and generate a response in the desired context**.
35 |
36 | In this way, we achieve separation of concerns. What does this look like?
37 |
38 | ```haskell
39 | server
40 | :: Monad m
41 | => Router a -- Router
42 | -> (a -> m Response) -- Handler
43 | -> (m a -> IO a) -- Lifter
44 | -> Application -- Application
45 | server = undefined
46 | ```
47 |
48 | ```haskell
49 | data Router a = Router
50 | {
51 | }
52 |
53 | server
54 | :: Monad m
55 | => Router a -- Router
56 | -> (a -> m Response) -- Handler
57 | -> (m ~> IO) -- Lifter (Natural Transformation)
58 | -> Application -- Application
59 | server = undefined
60 |
61 | serverPure
62 | :: (Request -> a)
63 | -> (a -> Response)
64 | -> Application
65 | serverPure = undefined
66 |
67 | serverPure'
68 | :: (Request -> Response)
69 | -> Application
70 | serverPure' = undefined
71 | ```
72 |
73 | ### A Simpler Routing Interface
74 |
75 | ```haskell
76 | server
77 | :: (RouteData -> Route -> m Response)
78 | -> (m ~> IO)
79 | -> Application
80 | server f nt = ...
81 | ```
82 |
83 | ```haskell
84 | data Route = Route
85 | { method :: Method
86 | , path :: [Text]
87 | }
88 |
89 | pattern GetUser :: UserID -> Route
90 | pattern GetUser userID = Route GET ["users", userID]
91 |
92 | pattern PostUser :: UserID -> Route
93 | pattern PostUser userID = Route POST ["users", userID]
94 |
95 | server
96 | :: RouteData
97 | -> Route %1
98 | -> m Response
99 | server routeData = \case
100 | GetUser userID -> do
101 | ...
102 | PostUser userID -> do
103 | ...
104 | _ -> return notFoundResponse
105 | ```
106 |
107 | ```haskell
108 | -- Record of higher order functions
109 | data RouteData = RouteData
110 | { queryParam :: HttpApiData a => Text -> Result a
111 | , header :: HttpApiData a => Text -> Result a
112 | , body :: ...
113 | , file :: ...
114 | , formParam :: ...
115 | , ...
116 | }
117 |
118 | server
119 | :: RouteData
120 | -> Route %1
121 | -> m Response
122 | server routeData route = do
123 | let
124 | setup1 = do
125 | ...
126 | setup2 = ...
127 | setup3 <- ...
128 | case route of
129 | GetUser userID -> do
130 | ...
131 | PostUser userID -> do
132 | ...
133 | _ -> return notFoundResponse
134 | ```
135 |
136 | ### Alternate Syntax
137 |
138 | #### Fast API Like
139 |
140 | ```haskell
141 | getUsers :: Controller
142 | getUsers = [get|
143 | /users
144 | ?age:Int
145 | ?name:Text
146 | ?status:Status
147 | |] id handler
148 | where
149 | handler :: (Int, Text, Status) -> m Response
150 | handler = ...
151 |
152 | [post| /user/:UserID |] :: ...
153 |
154 | [put| /user/:UserID |]
155 | ```
156 |
157 | #### Controller Method
158 |
159 | ```haskell
160 | data Error = JSONError ... | ...
161 | data Result a = Cont a | Next
162 |
163 | data Extractor a = ...
164 |
165 | instance Applicative Extractor where
166 |
167 | extractUser :: Extractor User
168 | extractUser = do
169 | methodIs GET
170 | pathParamIs @Text "users" <|> pathParamIs "people"
171 | userID <- pathParam @UserID
172 | userQuery <- json @UserQuery
173 | pure GetUser{..}
174 |
175 | data Extractor a = Ok a | Fail
176 |
177 | data Result a = Respond a | Next
178 |
179 | type Handler m a = Extractor a -> m (Result Response)
180 |
181 | controller
182 | :: (m ~> IO)
183 | -> Extractor a
184 | -> Handler m a
185 | -> Controller
186 | controller transformer router handler = ...
187 |
188 | combineController
189 | :: Controller
190 | -> Controller
191 | -> Controller
192 | combineController c1 c2 = ...
193 | ```
194 |
195 | ```haskell
196 | data Controller = Controller
197 | {
198 | }
199 | ```
200 |
201 | ### Mixing Patterns with Extractors
202 |
203 | Use patterns for method and path. Use extractors for everything else.
204 |
205 | ```haskell
206 | router :: Route -> Extractor a
207 | router = \case
208 | (GET, ["index"]) -> do
209 | ..
210 | (GET, ["posts", PathParam postID]) -> do
211 | ..
212 | _ -> undefined
213 | ```
214 |
215 | Probably not ideal because the exact extractor value can depend on the path parameter. We can't guarantee the developer won't do this.
216 |
217 | ### Route as Data
218 |
219 | ```haskell
220 | myRoute :: Endpoint
221 | myRoute = Endpoint
222 | { method = GET
223 | , path =
224 | [ Static "people"
225 | , Param @PersonID "personID"
226 | ]
227 | , query =
228 | [ Param @Bool "profile"
229 | ]
230 | , headers =
231 | [ Param @Text "X-Some-Header"
232 | ]
233 | , body = JSON @PersonFilter
234 | }
235 | ```
236 |
237 | Combine with extractor DSL?
238 |
239 | ```haskell
240 | myRoute :: Endpoint pd qd hd bd
241 | myRoute = Endpoint
242 | { method = GET
243 | , path = do
244 | static "profile"
245 | pID <- param @ProfileID
246 | pure pID
247 | , query = do
248 | useProfile <- flag "profile"
249 | pure useProfile
250 | , headers = NoHeaders
251 | , body = json @PersonFilter
252 | }
253 |
254 | myRoute' :: Endpoint pd qd hd bd rd
255 | myRoute' = Endpoint
256 | { method = GET :| [PUT, POST]
257 | , path = do
258 | static "profile"
259 | pID <- param @ProfileID
260 | pure pID
261 | , query = do
262 | useProfile <- flag "profile"
263 | pure useProfile
264 | , headers = NoHeaders
265 | , body = do
266 | filter <- json @PersonFilter
267 | pure filter
268 | , responder = do
269 | sendOk <- ok
270 | sendNotFound <- notFound
271 | pure Send{..}
272 | }
273 |
274 | myRoute'' :: Endpoint pd qd hd bd rd
275 | myRoute'' = Endpoint
276 | GET
277 | static "index"
278 | NoQuery
279 | NoHeaders
280 | NoBody
281 | ok
282 |
283 | data Params pd qd hd bd rd = Params
284 | { path :: pd
285 | , query :: qd
286 | , headers :: hd
287 | , body :: bd
288 | , response :: rd %1
289 | -- TODO: Have two fields for response ~
290 | -- On Error and on Ok
291 | -- , responseError :: red %1
292 | }
293 |
294 | -- Use type level function to produce types for both
295 | -- Endpoint and Params.
296 |
297 | myHandler
298 | :: Monad m
299 | => (Params pd qd hd bd rd) %1
300 | -> m (Action Response)
301 | myHandler paramsResult = case paramsResult of
302 | Error err -> do
303 | -- | Do logging or whatever if error
304 | liftIO $ print err
305 | return Next
306 | Ok params -> do
307 | let
308 | profileID = path params
309 | isProfileView = query params
310 | personFilter = body params
311 |
312 | return $ params.response.respondOk responseValue
313 |
314 | makeController
315 | :: Monad m
316 | => (m ~> IO)
317 | -> Endpoint pd qd hd bd rd
318 | -> (Params pd qd hd bd rd -> m Response)
319 | -> Controller
320 | makeController lifter endpoint handler = ...
321 | ```
322 |
323 | The above seems to be the best design.
324 |
325 | ### Combining Controllers
326 |
327 | #### Non-Empty List
328 |
329 | ```haskell
330 | type Server = NonEmptyList Controller
331 | -- Use Map instead
332 |
333 | myServer = controller1 :| [controller2, controller3]
334 |
335 | genApplication
336 | :: ServerOptions
337 | {-| Control body max size
338 | , default response
339 | , IO error to response
340 | , etc.
341 | -}
342 | -> Server
343 | -> Application
344 |
345 | genJSClient :: Server -> FilePath -> IO ()
346 |
347 | genOpenAPISpec :: Server -> OpenAPISpec
348 | ```
349 |
350 | `genApplication` takes server options and a server definition.
351 |
352 |
353 |
354 | ## Megalith Web Framework
355 |
356 | ### File-based Routing
357 |
358 | Megalith supports file-based routing. Placing a `.ml` file in your project's `pages` directory will automatically generate a route to that page. `.ml` files can contain plain HTML. Here's an example `.ml` file called `index.ml`:
359 |
360 | ```html
361 |
362 |
363 | Welcome to my website.
364 |
365 |
366 | This website was built using the Megalith web framweork.
367 |
368 |
369 | ```
370 |
371 | If we run this app and go to `localhost:3000/index`, this page will be rendered in our browser.
372 |
373 | ### Nested File-based Routes
374 |
375 | We may create nested routes by simply creating a directory in our `pages` directory. For example, if we create a `products` directory in the `pages` directory, and then put `bolts.ml` in the `products` directory, our app will have the route `localhost:3000/products/bolts`.
376 |
377 | ### Dynamic Routes
378 |
379 | We can also create dynamic routes that contain parameters. We can use these route parameters in our templates. To do this, we need to wrap the file/directory name in square brackets (`[]`). We can then use the name inside the square brackets to refer to the parameter in our templates. Here's an example:
380 |
381 | ```html
382 |
383 |
384 |
This is the $(category::Text) category.
385 |
386 | ```
387 |
388 | Maybe consider using `!` instead of `[]` for dynamic routes.
389 |
390 | Running the app and going to `localhost:3000/pages/products/watches` will render the page:
391 |
392 | ```html
393 |
409 | ```
410 |
411 | Or, a more traditional approach:
412 |
413 | ```haskell
414 | myHTML :: HTML
415 | myHTML =
416 |
417 | {list}
418 |
419 | where
420 | list = forEach [1..10] \n ->
Number: {n}
421 | ```
422 |
423 | Megalith includes a GHC plugin that introduces a literal syntax for HTML tags. Inspired by JSX and Phoenix Components.
424 |
425 | ### Components
426 |
427 | ```haskell
428 | type Component a = a -> HTML
429 |
430 | class Component a where
431 | render :: a -> HTML
432 | ```
433 |
434 | ### Routes
435 |
436 | ```haskell
437 | get
438 | :: Parser a
439 | -> (a -> m Response)
440 | -> ???
441 | ```
442 |
443 | ```haskell
444 | type Application m a = (Parser a, a -> m Response)
445 | ```
446 |
447 | ### Server Pages
448 |
449 | ```haskell
450 | -- pages/index.mli --> localhost:3000/index
451 |
452 |