├── Setup.hs
├── .gitignore
├── stack.yaml
├── ChangeLog.md
├── src
├── Network
│ ├── N2O
│ │ ├── Web.hs
│ │ ├── Internal.hs
│ │ └── Web
│ │ │ ├── Http.hs
│ │ │ └── WebSockets.hs
│ └── N2O.hs
└── Data
│ └── BERT.hs
├── LICENSE
├── man
├── web.htm
└── int.htm
├── n2o.cabal
├── README.md
└── index.html
/Setup.hs:
--------------------------------------------------------------------------------
1 | import Distribution.Simple
2 | main = defaultMain
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .stack-work/
2 | *~
3 | .idea
4 | *.iml
5 | cabal.config
6 | *.aes
7 |
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | resolver: lts-12.12
2 | packages:
3 | - .
4 | extra-deps:
5 | - git: git@github.com:o3/nitro.git
6 | commit: master
7 |
--------------------------------------------------------------------------------
/ChangeLog.md:
--------------------------------------------------------------------------------
1 | History
2 | =======
3 |
4 | 0.1 Andy Melnikov, Client Protocol, BERT, IxSet, Chat App
5 | 0.11 Marat Khafizov, Nitro Protocol, BERT, Simple App
6 |
--------------------------------------------------------------------------------
/src/Network/N2O/Web.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Network.N2O.Web
3 | Description : Static HTTP Server and Bridge for WebSockets
4 | Copyright : (c) Marat Khafizov, 2018
5 | License : BSD-3
6 | Maintainer : xafizoff@gmail.com
7 | Stability : experimental
8 | Portability : not portable
9 |
10 | This package provides a simple static HTTP server and the WebSockets adapter
11 | to the N2O Protocol Loop.
12 |
13 | -}
14 | module Network.N2O.Web
15 | ( module Network.N2O.Web.WebSockets
16 | , module Network.N2O.Web.Http
17 | , module Web.Nitro
18 | ) where
19 |
20 | import Network.N2O.Web.WebSockets
21 | import Network.N2O.Web.Http
22 | import Web.Nitro
23 |
--------------------------------------------------------------------------------
/src/Network/N2O.hs:
--------------------------------------------------------------------------------
1 | {-|
2 | Module : Network.N2O
3 | Description : Core of the N2O Framework
4 | Copyright : (c) Marat Khafizov, 2018
5 | License : BSD-3
6 | Maintainer : xafizoff@gmail.com
7 | Stability : experimental
8 | Portability : not portable
9 |
10 | This module defines basic types and functions for the N2O Framework.
11 |
12 | One of the trickiest part of the client-server applications is the communication
13 | protocol between client and server. This package aims to provide scalable application
14 | level infrastructure for protocols and services.
15 |
16 | Logically, this package consists of two parts:
17 |
18 | * the 'N2O' monad for local state management;
19 | * the 'protoRun' function, that allows to perform abstract protocol loop.
20 |
21 | For basic usage see [N2O sample app](https://github.com/xafizoff/n2o/tree/master/samples)
22 |
23 | -}
24 | module Network.N2O
25 | ( module Network.N2O.Internal
26 | , module Data.BERT
27 | , liftIO
28 | , module M
29 | ) where
30 |
31 | import Network.N2O.Internal
32 | import Data.BERT
33 | import Control.Monad.IO.Class (liftIO)
34 | import Control.Monad.Trans.Reader as M
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright Marat Khafizov (c) 2018
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials provided
14 | with the distribution.
15 |
16 | * Neither the name of Marat Khafizov nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/man/web.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | WEB
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | TECH
18 | O3
19 | N2O
20 |
21 |
22 |
23 |
24 | WEB
25 |
26 |
27 |
28 |
29 |
30 | INTRO
31 |
32 | Network.N2O.Web defines adjoin interfaces to underlying
33 | transports: HTTP for static file serving and WebSocket for N2O protocols.
34 |
35 |
36 |
37 |
38 | WebSocket
39 |
40 | wsApp :: Cx f a b -> WS.ServerApp
41 |
42 |
43 |
44 |
45 | HTTP
46 |
47 | runServer :: String -> Int -> Cx f a b -> IO ()
48 |
49 |
50 |
51 |
60 |
61 |
62 |
63 | Made with ❤ to Haskell and N2O
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/n2o.cabal:
--------------------------------------------------------------------------------
1 | name: n2o
2 | version: 0.11.2
3 | homepage: https://github.com/xafizoff/n2o#readme
4 | bug-reports: https://github.com/xafizoff/n2o/issues
5 | author: Marat Khafizov
6 | maintainer: xafizoff@gmail.com
7 | copyright: 2018 Marat Khafizov (c)
8 | license: BSD3
9 | license-file: LICENSE
10 | build-type: Simple
11 | cabal-version: >= 1.10
12 | extra-source-files: README.md
13 | category: Network, N2O, Web
14 | synopsis: Abstract Protocol Loop
15 | description: N2O defines a way we scale protocols, database schema, applications
16 | and services across companies, formatters, views and presentation layers.
17 | At the core N2O folds a list of protocols and their handlers providing
18 | a minimal type-level specification for general purpose application protocol tract.
19 |
20 | source-repository head
21 | type: git
22 | location: https://github.com/xafizoff/n2o
23 |
24 | library
25 | exposed-modules:
26 | Network.N2O
27 | , Network.N2O.Internal
28 | , Data.BERT
29 | , Network.N2O.Web
30 | , Network.N2O.Web.Http
31 | , Network.N2O.Web.WebSockets
32 | other-modules:
33 | Paths_n2o
34 | hs-source-dirs:
35 | src
36 | build-depends: base >= 4.7 && < 5
37 | , text >= 1.2 && < 1.3
38 | , bytestring >= 0.9 && < 1
39 | , containers >= 0.5 && < 0.6
40 | , transformers >= 0.5 && < 0.6
41 | , time >= 1.8 && < 1.9
42 | , cereal >= 0.5 && < 0.6
43 | , n2o-nitro == 0.11.2
44 | , websockets >= 0.12.5 && < 0.13
45 | , network >= 2.6 && < 2.7
46 | , text >= 1.2 && < 1.3
47 | , store >= 0.5 && < 0.6
48 | , attoparsec >= 0.13.2 && < 0.14
49 | , case-insensitive >= 1.2 && < 1.3
50 | , base64-bytestring >= 1.0 && < 1.1
51 | , async >= 2.2 && < 2.3
52 | , stm
53 | , random-bytestring
54 | default-language: Haskell2010
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # N2O for Haskell
2 |
3 | [](https://travis-ci.org/xafizoff/n2o)
4 |
5 | Features
6 | --------
7 |
8 | * Endpoints: poor man's WebSocket and static HTTP server
9 | * Formatters: BERT
10 | * Protocols: NITRO
11 | * High Performance Protocol Relay
12 | * Smallest possible codebase — 500 LOC
13 |
14 | N2O defines a way we scale protocols, database schema, applications and
15 | services across companies, formatters, views and presentation layers.
16 | At the core N2O folds a list of protocols and their handlers providing
17 | a minimal type-level specification for general purpose application protocol tract.
18 |
19 | As example this Haskell version of N2O is shipped with Nitro protocol
20 | implementation, that listens the tract and push prerendered JavaScript
21 | events back to the channel. This smart and simple reactive way
22 | of client-server interaction first was used by Rusty Klophaus in
23 | his Nitrogen web framework, that was pushed forward since then in
24 | N2O by Andy Melnikov and Marat Khafizov.
25 |
26 | Setup
27 | -----
28 |
29 | ```sh
30 | stack build
31 | stack exec n2o-sample
32 | open http://localhost:3000/samples/static/index.html
33 | ```
34 |
35 | Nitro Protocol Demo
36 | -------------------
37 |
38 | ```haskell
39 | {-# LANGUAGE FlexibleContexts, OverloadedStrings, DeriveGeneric, DeriveAnyClass #-}
40 | module Main (main) where
41 |
42 | import Network.N2O
43 | import Network.N2O.Web hiding (Event)
44 | import GHC.Generics (Generic)
45 | import Data.Serialize (Serialize)
46 |
47 | data Example = Greet deriving (Show, Eq, Generic, Serialize)
48 |
49 | main = runServer "localhost" 3000 cx
50 |
51 | cx :: Cx Example
52 | cx = mkCx{ cxMiddleware=[router]
53 | , cxProtos = [nitroProto]
54 | }
55 |
56 | router cx@Context{cxReq=Req{reqPath=path}} =
57 | let handle = case path of
58 | "/ws/samples/static/index.html" -> index
59 | "/ws/samples/static/about.html" -> about
60 | _ -> index
61 | in cx{cxHandler=mkHandler handle}
62 |
63 | index Init = do
64 | updateText "system" "What is your name?"
65 | wire button{id_="send", postback=Just Greet, source=["name"]}
66 | index (Message Greet) = do
67 | Just name <- get "name" -- wf:q/1
68 | updateText "system" ("Hello, " <> jsEscape name <> "!")
69 | index ev = liftIO $ putStrLn ("Unknown event" <> show ev)
70 | about Init = updateText "app" "This is the N2O Hello World App"
71 | about ev = liftIO $ putStrLn ("Unknown event " <> show ev)
72 | ```
73 |
74 | Credits
75 | -------
76 |
77 | * Andy Melnikov
78 | * Marat Khafizov
79 | * Maxim Sokhatsky
80 |
81 |
--------------------------------------------------------------------------------
/man/int.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | INT
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | TECH
18 | O3
19 | N2O
20 |
21 |
22 |
23 |
24 | INT
25 |
26 |
27 |
28 |
29 | INTRO
30 | The Network.N2O.Internal module contains request and context definition, state and N2O monad.
31 |
32 |
33 | REQUEST
34 | Listing 1. HTTP Request
35 | type Header = (BS.ByteString, BS.ByteString)
36 | data Req = Req
37 | { reqPath :: BS.ByteString
38 | , reqMeth :: BS.ByteString
39 | , reqVers :: BS.ByteString
40 | , reqHead :: [Header]
41 | }
42 |
43 | Listing 2. HTTP Response
44 | data Resp = Resp
45 | { respCode :: Int
46 | , respHead :: [Header]
47 | , respBody :: BS.ByteString
48 | } deriving (Show)
49 |
50 |
51 |
52 | CONTEXT
53 | Listing 3. N2O Protocol Closure
54 | data Event a
55 | = Init
56 | | Message a
57 | | Terminate
58 | deriving Show
59 |
60 | Listing 4. N2O Protocol Result
61 | data Result a
62 | = Reply a
63 | | Ok
64 | | Unknown
65 | | Empty
66 | deriving (Show, Eq)
67 |
68 | Listing 5. N2O Context Record
69 | data Context (f :: * -> *) a where Context ::
70 | { cxHandler :: Event a -> N2O f a (Result a)
71 | , cxReq :: Req
72 | , cxMiddleware :: [Context f a -> Context f a]
73 | , cxProtos :: [Proto f a]
74 | , cxActions :: BS.ByteString
75 | , cxDict :: M.Map BS.ByteString BS.ByteString
76 | } -> Context f a
77 |
78 | Listing 6. N2O Context Constructor
79 | mkCx = Context
80 | { cxReq = undefined
81 | , cxHandler = undefined
82 | , cxMiddleware = []
83 | , cxProtos = []
84 | , cxActions = ""
85 | , cxDict = M.empty
86 | }
87 |
88 |
89 |
90 | MONAD
91 |
92 | Listing 7. N2O Monad
93 | type Proto (f :: * -> *) a
94 | = (f a) -> N2O f a (Result (f a))
95 |
96 | type N2O f a
97 | = ReaderT (IORef (Context f a)) IO
98 |
99 |
100 | put :: (B.Serialize bin) => BS.ByteString -> bin -> N2O f a ()
101 | get :: (B.Serialize bin) => BS.ByteString -> N2O f a (Maybe bin)
102 | getContext :: N2O f a (Context f a)
103 | nop :: Result a
104 | protoRun :: f a -> [Proto f a] -> N2O f a (Result (f a))
105 |
106 |
107 | This module may refer to:
108 | INT ,
109 | CORE
110 |
111 |
112 |
113 |
114 |
115 | Made with ❤ to Haskell and N2O
116 |
117 |
118 |
119 |
120 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | N2O
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | TECH
18 | O3
19 | N2O
20 |
21 |
22 |
23 |
24 | N2O
25 |
26 |
27 |
28 |
29 |
30 | SYNOPSIS
31 | This project introduces N2O protocols for Haskell programming language.
32 | The aim is to be compatible at transport level with Erlang version of N2O.
33 |
34 |
35 |
46 |
47 |
48 |
49 | git clone https://github.com/o3/sample
50 | stack build
51 | stack exec n2o-sample
52 | open http://localhost:3000/static/index.html
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | N2O Haskell
63 |
64 | N2O defines a way we scale protocols, database schema, applications and
65 | services across companies, formatters, views and presentation layers.
66 | At the core N2O folds a list of protocols and their handlers providing
67 | a minimal type-level specification for general purpose application protocol tract.
68 |
69 | As example this Haskell version of N2O is shipped with Nitro protocol
70 | implementation, that listens the tract and push prerendered JavaScript
71 | events back to the channel. This smart and simple reactive way
72 | of client-server interaction first was used by Rusty Klophaus in
73 | his Nitrogen web framework, that was pushed forward since then in
74 | N2O by Andy Melnikov and Marat Khafizov.
75 |
76 | The philosophy behind N2O is a simplicity, cleanness, and purity.
77 | The core of N2O should be no more than 500 LOC.
78 |
79 |
80 | Listing 2. N2O Context Setup
81 |
82 | data Example = Greet deriving (Show, Eq, Read)
83 |
84 | main = runServer "localhost" 3000 cx
85 | cx = createCx router
86 | router cx@Cx{cxReq=Req{reqPath=path}} =
87 | let handler = case path of
88 | "/ws/samples/static/index.html" -> index
89 | "/ws/samples/static/about.html" -> about
90 | _ -> index
91 | in traceShow path cx{cxHandler=handler}
92 |
93 |
94 | The idea to send prerendered JavaScript events over the
95 | wire belongs to Rusty Klophaus who made Nitrogen Erlang web framework.
96 | Later this was refined by N2O team and now is available
97 | in its purity of Haskell.
98 |
99 |
100 | Listing 3. NITRO Web Protocol
101 |
102 | index Init = do
103 | updateText "system" "What is your name?"
104 | wireEl button{id="send", postback=Just Greet, source=["name"]}
105 |
106 | index (Message Greet) = do
107 | Just name <- get "name" -- wf:q/1
108 | updateText "system" ("Hello, " <> jsEscape name <> "!")
109 |
110 | about Init =
111 | updateText "app" "This is the N2O Hello World App"
112 |
113 |
114 | The N2O does not limit in developing only web applications.
115 | N2O stack of protocols covers bus, storage, process interfaces.
116 | Subscribe to follow Haskell implementations.
117 |
118 |
119 |
120 | You may want to read:
121 | WEB ,
122 | INT .
123 |
124 |
125 |
126 |
127 |
128 | Made with ❤ to Haskell and N2O
129 |
130 |
131 |
132 |
133 |
--------------------------------------------------------------------------------
/src/Network/N2O/Internal.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE RankNTypes #-}
2 | {-# LANGUAGE OverloadedStrings, KindSignatures, GADTs #-}
3 |
4 | {-|
5 | Module : Network.N2O.Internal
6 | Description : Basic types and core functions
7 | Copyright : (c) Marat Khafizov, 2018
8 | License : BSD-3
9 | Maintainer : xafizoff@gmail.com
10 | Stability : experimental
11 | Portability : not portable
12 |
13 | Basic types and core functions
14 |
15 | -}
16 | module Network.N2O.Internal
17 | ( module Network.N2O.Internal
18 | , module Control.Monad.Trans.Reader
19 | ) where
20 | import qualified Data.Serialize as B
21 | import qualified Data.ByteString as BS
22 | import qualified Data.Text as T
23 | import Data.IORef
24 | import Data.Map.Strict (Map, (!?), insert)
25 | import qualified Data.Map.Strict as M
26 | import Control.Monad.Trans.Reader
27 | import Control.Monad.IO.Class (liftIO)
28 | import Control.Exception (SomeException)
29 | import Control.Concurrent.STM
30 | import Control.Concurrent.STM.TChan
31 | import Control.Monad (forM_)
32 | import Control.Concurrent (ThreadId)
33 |
34 | -- | An HTTP header
35 | type Header = (BS.ByteString, BS.ByteString)
36 |
37 | -- | An HTTP request
38 | data Req = Req
39 | { reqPath :: BS.ByteString
40 | , reqMeth :: BS.ByteString
41 | , reqVers :: BS.ByteString
42 | , reqHead :: [Header]
43 | }
44 |
45 | -- | The N2O context data type
46 | -- This is the key data type of the N2O. @(f :: * -> *)@ - type constructor
47 | -- for the protocol handler's input type. @(a :: *)@ - base type for the
48 | -- event handler's input type. I.e. @(f a)@ gives input type for the
49 | -- protocol handler. @(Event a)@ gives input type for the event handler.
50 | data Context (f :: * -> *) a where
51 | Context ::
52 | { cxHandler :: Event a -> N2O f a (Result a)
53 | , cxReq :: Req
54 | , cxMiddleware :: [Context f a -> Context f a]
55 | , cxProtos :: [Proto f a]
56 | , cxActions :: TVar BS.ByteString
57 | , cxDict :: TVar (M.Map BS.ByteString BS.ByteString)
58 | , cxPubSub :: TVar (M.Map BS.ByteString [TChan (f a)])
59 | , cxInBox :: TChan (f a)
60 | , cxOutBox :: TChan (f a)
61 | , cxSessions :: TVar (M.Map BS.ByteString BS.ByteString)
62 | , cxTid :: ThreadId
63 | } -> Context f a
64 |
65 | -- | Result of the message processing
66 | data Result a
67 | = Reply a
68 | | Ok
69 | | Unknown
70 | | Empty
71 | deriving (Show, Eq)
72 |
73 | -- | N2O protocol handler
74 | type Proto (f :: * -> *) a = (f a) -> N2O f a (Result (f a))
75 |
76 | -- | Event data type
77 | data Event a
78 | = Init
79 | | Message a
80 | | Terminate
81 | deriving Show
82 |
83 | type N2O f a = ReaderT (Context f a) IO
84 |
85 | sub :: BS.ByteString -> N2O f a ()
86 | sub topic = do
87 | cx <- ask
88 | liftIO $ atomically $ do
89 | Context{cxPubSub = pubsub,cxOutBox = chan} <- pure cx
90 | modifyTVar pubsub $ \m -> M.alter (\mbs -> let s = case mbs of {Just s -> s; _ -> []} in Just $ ins chan s) topic m
91 |
92 | unsub topic = do
93 | cx <- ask
94 | liftIO $ atomically $ do
95 | Context{cxPubSub = pubsub,cxOutBox = chan} <- pure cx
96 | modifyTVar pubsub $ \m -> M.alter (\mbs -> let s = case mbs of {Just s -> s; _ -> []} in Just $ del [] chan s) topic m
97 |
98 | pub topic a = do
99 | cx@Context{cxPubSub = pubsub} <- ask
100 | l <- liftIO $ atomically $ do
101 | m <- readTVar pubsub
102 | l <- pure $ case M.lookup topic m of {Just s -> s; _ -> []}
103 | forM_ l (\chan -> do {rChan <- dupTChan chan; writeTChan chan a})
104 | pure l
105 | return ()
106 |
107 | fnd x [] = False
108 | fnd x (y:ys) = if x == y then True else fnd x ys
109 |
110 | ins x ys = if fnd x ys then ys else x:ys
111 |
112 | del acc _ [] = acc
113 | del acc x (y:ys) = if x == y then del acc x ys else del (y:acc) x ys
114 |
115 | -- | Put data to the local state
116 | put :: (B.Serialize bin) => BS.ByteString -> bin -> N2O f a ()
117 | put k v = do
118 | cx <- ask
119 | liftIO $ atomically $ modifyTVar (cxDict cx) (\dict -> M.insert k (B.encode v) dict)
120 |
121 | -- | Get data from the local state
122 | get :: (B.Serialize bin) => BS.ByteString -> N2O f a (Maybe bin)
123 | get k = do
124 | cx <- ReaderT return
125 | m <- liftIO $ atomically $ readTVar $ cxDict cx
126 | case m !? k of
127 | Just v -> case (B.decode v) of
128 | Right x -> return $ Just x
129 | _ -> return Nothing
130 | _ -> return Nothing
131 |
132 | getContext :: N2O f a (Context f a)
133 | getContext = do
134 | cx <- ask
135 | pure cx
136 |
137 | -- | 'Context' constructor
138 | mkCx = Context
139 | { cxReq = undefined
140 | , cxHandler = undefined
141 | , cxMiddleware = []
142 | , cxProtos = []
143 | , cxActions = undefined
144 | , cxDict = undefined
145 | , cxPubSub = undefined
146 | , cxOutBox = undefined
147 | , cxInBox = undefined
148 | , cxTid = undefined
149 | , cxSessions = undefined
150 | }
151 |
152 | -- | 'Req' constructor
153 | mkReq = Req { reqPath = "/", reqMeth = "GET", reqVers = "HTTP/1.1", reqHead = [] }
154 |
155 | -- | NO-OP result
156 | nop :: Result a
157 | nop = Empty
158 |
159 | -- | N2O protocol loop
160 | protoRun :: f a -> [Proto f a] -> N2O f a (Result (f a))
161 | protoRun = loop
162 | where
163 | loop :: f a -> [Proto f a] -> N2O f a (Result (f a))
164 | loop _ [] = return nop
165 | loop msg (proto:protos) = do
166 | res <- proto msg
167 | case res of
168 | Unknown -> loop msg protos
169 | Empty -> return Empty
170 | Reply msg1 -> return $ Reply msg1
171 | a -> loop msg protos
172 |
173 |
--------------------------------------------------------------------------------
/src/Network/N2O/Web/Http.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE LambdaCase, RecordWildCards #-}
2 | {- | Naive implementation of HTTP server -}
3 | module Network.N2O.Web.Http ( runServer ) where
4 |
5 | import Control.Concurrent
6 | import Control.Exception (catch, finally, SomeException(..), bracket, try)
7 | import qualified Data.ByteString as BS
8 | import qualified Data.ByteString.Char8 as C
9 | import Network.Socket hiding (recv, send)
10 | import Network.Socket.ByteString
11 | import Network.N2O.Internal
12 | import Network.N2O.Web.WebSockets (wsApp, N2OProto, mkPending)
13 | import Prelude hiding (takeWhile)
14 | import Data.Attoparsec.ByteString hiding (try)
15 | import Data.CaseInsensitive (mk)
16 | import qualified Network.WebSockets as WS
17 | import Control.Concurrent.Async
18 | import System.IO
19 | import Text.Printf
20 | import qualified Data.Map.Strict as M
21 | import Control.Concurrent.STM.TVar (newTVarIO)
22 |
23 | data Resp = Resp
24 | { respCode :: Int
25 | , respHead :: [Header]
26 | , respBody :: BS.ByteString
27 | } deriving (Show)
28 |
29 | mkResp = Resp { respCode = 200, respHead = [], respBody = BS.empty }
30 |
31 | runServer :: String -> Int -> Context N2OProto a -> IO ()
32 | runServer host port cx = do
33 | hSetBuffering stdout NoBuffering
34 | printf "Started server at %s:%d\n" host port
35 | pubsub <- newTVarIO M.empty
36 | sessions <- newTVarIO M.empty
37 | acceptor <- async $ withSocketsDo $ do
38 | addr <- resolve host (show port)
39 | bracket (open addr) close (acceptConnections cx{cxPubSub = pubsub,cxSessions = sessions})
40 | wait acceptor
41 | where
42 | resolve host port = do
43 | let hints = defaultHints {addrSocketType = Stream, addrFlags = [AI_PASSIVE]}
44 | addr:_ <- getAddrInfo (Just hints) (Just host) (Just port)
45 | return addr
46 | open addr = do
47 | sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
48 | setSocketOption sock ReuseAddr 1
49 | bind sock (addrAddress addr)
50 | listen sock 10
51 | return sock
52 |
53 | acceptConnections :: Context N2OProto a -> Socket -> IO ()
54 | acceptConnections cx sock = do
55 | (handle, host_addr) <- accept sock
56 | forkIO (catch
57 | (talk cx handle host_addr `finally` close handle)
58 | (\e@(SomeException _) -> print e))
59 | acceptConnections cx sock
60 |
61 | talk :: Context N2OProto a -> Socket -> SockAddr -> IO ()
62 | talk cx sock addr = do
63 | bs <- recv sock 4096
64 | let either = parseReq bs
65 | case either of
66 | Left resp -> sendResp sock resp
67 | Right req ->
68 | if needUpgrade req
69 | then do
70 | pending <- mkPending WS.defaultConnectionOptions sock req
71 | wsApp cx {cxReq = req} pending
72 | else fileResp (preparePath $ C.unpack $ reqPath req) (sendResp sock)
73 | where
74 | preparePath ('.':path) = preparePath path
75 | preparePath ('/':path) = preparePath path
76 | preparePath path = path
77 |
78 | status = \case
79 | 200 -> "OK"
80 | 400 -> "Bad Request"
81 | 404 -> "Not Found"
82 | 500 -> "Internal Server Eror"
83 | _ -> ""
84 |
85 | calcLen resp@Resp{..} = (C.pack "Content-Length", C.pack $ show $ BS.length respBody) : respHead
86 |
87 | sendResp :: Socket -> Resp -> IO ()
88 | sendResp sock resp@Resp {..} = do
89 | let headers = fmap (\(k, v) -> k <> C.pack ": " <> v <> C.pack "\r\n") (calcLen resp)
90 | cmd = C.pack "HTTP/1.1 " <> C.pack (show respCode) <> C.pack " " <> C.pack (status respCode) <> C.pack "\r\n"
91 | x = cmd : headers
92 | y = x ++ [C.pack "\r\n", respBody]
93 | send sock (mconcat y)
94 | return ()
95 |
96 | fileResp :: FilePath -> (Resp -> IO ()) -> IO ()
97 | fileResp path respond = do
98 | res <- try (BS.readFile path)
99 | let (status, content) = case res of
100 | Left e@(SomeException _) -> (404, C.pack "File Not Found")
101 | Right content -> (200, content)
102 | respond $ mkResp {respCode = status, respBody = content}
103 |
104 | parseReq :: BS.ByteString -> Either Resp Req
105 | parseReq bs = case parseOnly reqParser bs of
106 | Left _ -> Left $ mkResp {respCode = 400}
107 | Right req -> Right req
108 |
109 | needUpgrade :: Req -> Bool
110 | needUpgrade req =
111 | case getHeader (C.pack "upgrade") (reqHead req) of
112 | Nothing -> False
113 | Just h -> mk (snd h) == mk (C.pack "websocket")
114 |
115 | isKeepAlive :: Req -> Bool
116 | isKeepAlive req =
117 | case getHeader (C.pack "connection") (reqHead req) of
118 | Nothing -> False
119 | Just h -> mk (snd h) == mk (C.pack "keep-alive")
120 |
121 | getHeader :: BS.ByteString -> [Header] -> Maybe Header
122 | getHeader _ [] = Nothing
123 | getHeader k (h:hs)
124 | | mk k == mk (fst h) = Just h
125 | | otherwise = getHeader k hs
126 |
127 | crlf = (||) <$> (==10) <*> (==13)
128 | isSpace = (== 32)
129 |
130 | reqParser :: Parser Req
131 | reqParser = do
132 | cmd <- takeWhile1 $ not.isSpace
133 | skipWhile isSpace
134 | path <- takeWhile1 $ not.isSpace
135 | skipWhile isSpace
136 | ver <- takeWhile1 $ not.crlf
137 | skipWhile crlf
138 | headers <- many' headerParser
139 | skipWhile crlf
140 | takeByteString
141 | endOfInput
142 | return $ mkReq {reqMeth = cmd, reqPath = path, reqVers = ver, reqHead = headers }
143 |
144 | headerParser :: Parser (BS.ByteString, BS.ByteString)
145 | headerParser = do
146 | name <- takeWhile1 (\b -> b /= 58 && b /= 10 && b /= 13)
147 | skip (== 58)
148 | skipWhile isSpace
149 | val <- takeWhile1 $ not.crlf
150 | skipWhile crlf
151 | return (name, val)
152 |
--------------------------------------------------------------------------------
/src/Network/N2O/Web/WebSockets.hs:
--------------------------------------------------------------------------------
1 | {-# LANGUAGE FlexibleInstances, TypeSynonymInstances, OverloadedStrings, FlexibleContexts #-}
2 |
3 | module Network.N2O.Web.WebSockets (wsApp, mkPending, N2OProto(N2OClient), nitroProto, mkHandler, Cx) where
4 |
5 | import Control.Exception (catch, finally)
6 | import Control.Monad (forM_, forever, mapM_)
7 | import Control.Monad.IO.Class (MonadIO, liftIO)
8 | import Data.BERT
9 | import qualified Data.Serialize as B
10 | import qualified Data.ByteString.Lazy as BL
11 | import qualified Data.ByteString as BS
12 | import qualified Data.ByteString.Char8 as C8
13 | import Data.CaseInsensitive (mk)
14 | import Data.IORef
15 | import Data.Maybe (fromJust)
16 | import qualified Data.Map.Strict as M
17 | import Data.Text as T
18 | import Data.Text.Encoding as T
19 | import Network.N2O.Internal
20 | import Network.Socket (Socket)
21 | import Web.Nitro
22 | import qualified Network.WebSockets as WS
23 | import qualified Network.WebSockets.Connection as WSConn
24 | import qualified Network.WebSockets.Stream as WSStream
25 | import Control.Concurrent (forkIO, myThreadId)
26 | import Control.Concurrent.STM.TVar (TVar, readTVar, newTVarIO)
27 | import Control.Concurrent.STM (atomically, modifyTVar, writeTVar)
28 | import Control.Concurrent.STM.TChan (newBroadcastTChan, dupTChan, readTChan, writeTChan)
29 | import Data.ByteString.Random (random)
30 | import Text.Printf (printf)
31 |
32 | -- | Top level sum of protocols
33 | data N2OProto a
34 | = N2ONitro (Nitro a)
35 | | N2OClient a
36 | | Io BS.ByteString
37 | BS.ByteString
38 | | Nop
39 | --deriving (Show)
40 |
41 | type Cx a = Context N2OProto a
42 | type CxHandler a = Cx a -> Cx a
43 |
44 | instance NITRO (N2O N2OProto a) where
45 | putActions a = do
46 | cx <- ask
47 | liftIO $ atomically $ writeTVar (cxActions cx) a
48 | getActions = do
49 | Context{cxActions = tvar} <- ask
50 | acts <- liftIO $ atomically $ readTVar tvar
51 | return acts
52 |
53 | mkHandler h = \m -> do
54 | x <- h m
55 | return Empty
56 |
57 | nitroProto :: (Show a, B.Serialize a) => Proto N2OProto a
58 | nitroProto message = do
59 | cx@Context {cxHandler = handle,cxSessions = sess} <- getContext
60 | case message of
61 | msg@(N2ONitro (NitroInit pid)) -> do
62 | pid1 <- case pid of {"" -> do {rnd <- liftIO $ random(16); return $ hex rnd}; _ -> return pid}
63 | liftIO $ atomically $ modifyTVar sess $ \m -> M.alter (\mb -> let s = case mb of {Just s -> s; _ -> ""} in Just s) pid1 m
64 | handle Init
65 | acts <- getActions
66 | putActions ""
67 | return $ Reply $ reply acts pid1
68 | msg@(N2ONitro (NitroPickle _source pickled linked)) -> do
69 | forM_ (M.toList linked) (uncurry put)
70 | depickled <- depickle pickled
71 | case depickled of
72 | Just x -> do
73 | handle (Message x)
74 | acts <- getActions
75 | putActions ""
76 | return $ Reply (reply acts "")
77 | _ -> return Unknown
78 | msg@(N2OClient a) -> do
79 | handle $ Message a
80 | acts <- getActions
81 | putActions ""
82 | return $ Reply (reply acts "")
83 | msg@(N2ONitro NitroDone) -> do
84 | handle Terminate
85 | return Empty
86 | where
87 | reply eval dat = Io eval dat
88 | hex :: C8.ByteString -> C8.ByteString
89 | hex = C8.pack . Prelude.concatMap (printf "%02x") . C8.unpack
90 |
91 | wsApp :: Context N2OProto a -> WS.ServerApp
92 | wsApp cx pending = do
93 | tid <- myThreadId
94 | (chanIn,chanOut) <- atomically $ do
95 | chanOut <- newBroadcastTChan
96 | chanIn <- dupTChan chanOut
97 | pure (chanIn,chanOut)
98 | acts <- newTVarIO ""
99 | dict <- newTVarIO M.empty
100 | let path = WS.requestPath $ WS.pendingRequest pending
101 | cx1 = cx {cxReq = mkReq {reqPath = path}, cxInBox = chanIn, cxOutBox = chanOut, cxTid = tid, cxActions = acts, cxDict = dict}
102 | handlers = cxMiddleware cx1
103 | applyHandlers hs ctx =
104 | case hs of
105 | [] -> ctx
106 | (h:hs') -> applyHandlers hs' (h ctx)
107 | cx2 = applyHandlers handlers cx1
108 | conn <- WS.acceptRequest pending
109 | WS.forkPingThread conn 30
110 | -- rChan <- atomically $ dupTChan chan
111 | forkIO (pump conn cx2)
112 | listen conn cx2
113 |
114 | -- | Make pending WS request from N2O request
115 | mkPending :: WS.ConnectionOptions -> Socket -> Req -> IO WS.PendingConnection
116 | mkPending opts sock req = do
117 | stream <- WSStream.makeSocketStream sock
118 | let requestHead =
119 | WS.RequestHead
120 | { WS.requestPath = reqPath req
121 | , WS.requestSecure = False
122 | , WS.requestHeaders = fmap (\(k, v) -> (mk k, v)) (reqHead req)
123 | }
124 | return
125 | WSConn.PendingConnection
126 | { WSConn.pendingOptions = opts
127 | , WSConn.pendingRequest = requestHead
128 | , WSConn.pendingOnAccept = \_ -> return ()
129 | , WSConn.pendingStream = stream
130 | }
131 |
132 | pump conn cx@Context{cxInBox = chan} = do
133 | forever $ do
134 | msg <- atomically $ do
135 | readTChan chan
136 | reply <- runReaderT (protoRun msg $ cxProtos cx) cx
137 | process conn reply
138 |
139 | listen :: WS.Connection -> Context N2OProto a -> IO ()
140 | listen conn cx =
141 | do pid <- receiveN2O conn cx
142 | Context {cxProtos = protos, cxOutBox = chan, cxTid = tid} <- pure cx
143 | forever $ do
144 | message <- WS.receiveDataMessage conn
145 | case message of
146 | WS.Text "PING" _ -> WS.sendTextData conn ("PONG" :: T.Text)
147 | WS.Binary bin ->
148 | case B.decode (BL.toStrict bin) of
149 | Right term ->
150 | case fromBert term of
151 | Just msg -> do
152 | atomically $ writeTChan chan msg
153 | _ -> return ()
154 | _ -> return ()
155 | _ -> error "Unknown message"
156 | `finally` do
157 | Context {cxProtos = protos} <- pure cx
158 | runReaderT (protoRun (N2ONitro NitroDone) protos) cx
159 | return ()
160 |
161 | process conn reply =
162 | case reply of
163 | Reply a -> WS.sendBinaryData conn (B.encode $ toBert a)
164 | x -> error $ "Unknown response type"
165 |
166 | receiveN2O conn cx = do
167 | message <- WS.receiveDataMessage conn
168 | Context {cxProtos = protos} <- pure cx
169 | case message of
170 | WS.Binary _ -> error "Protocol violation: expected text message"
171 | WS.Text "" _ -> error "Protocol violation: got empty text"
172 | WS.Text bs _ ->
173 | case C8.stripPrefix "N2O," (BL.toStrict bs) of
174 | Just pid -> do
175 | reply <- runReaderT (protoRun (N2ONitro $ NitroInit pid) protos) cx
176 | process conn reply
177 | return pid
178 | _ -> error "Protocol violation"
179 |
180 | -- | Convert Binary Erlang Terms (BERT) to the 'N2OProto' specification
181 | fromBert :: Term -> Maybe (N2OProto a)
182 | fromBert (TupleTerm [AtomTerm "init", BytelistTerm pid]) =
183 | Just $ N2ONitro (NitroInit pid)
184 | fromBert (TupleTerm [AtomTerm "pickle", BinaryTerm source, BinaryTerm pickled, ListTerm linked]) =
185 | Just $ N2ONitro (NitroPickle source pickled (convert linked))
186 | where
187 | convert [] = M.empty
188 | convert (TupleTerm [AtomTerm k, BinaryTerm v]:vs) =
189 | M.insert (C8.pack k) v (convert vs)
190 | fromBert _ = Nothing
191 |
192 | toBert :: N2OProto a -> Term
193 | toBert (Io eval dat) =
194 | TupleTerm [AtomTerm "io", BinaryTerm eval, BinaryTerm dat]
195 |
--------------------------------------------------------------------------------
/src/Data/BERT.hs:
--------------------------------------------------------------------------------
1 | {-
2 | Copyright (c) 2009 marius a. eriksen (marius@monkey.org)
3 | (c) 2013 Roman Cheplyaka
4 | All rights reserved.
5 | -}
6 | {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, LambdaCase #-}
7 | module Data.BERT (Term(..)) where
8 |
9 | import Control.Monad
10 | import Control.Applicative
11 | import Data.Bits
12 | import Data.Char
13 | import Data.Int
14 | import Data.Word
15 | import Data.Serialize
16 | import Data.Serialize.Put
17 | import Data.Serialize.Get
18 | import Data.List
19 | import Data.Time
20 | import Data.ByteString (ByteString)
21 | import qualified Data.ByteString as B
22 | import qualified Data.ByteString.Char8 as C
23 | import Data.Map (Map)
24 | import qualified Data.Map as Map
25 | import Text.Printf
26 |
27 | -- | A single BERT term.
28 | data Term
29 | -- Simple (erlang) terms:
30 | = IntTerm Int
31 | | FloatTerm Float
32 | | AtomTerm String
33 | | TupleTerm [Term]
34 | | BytelistTerm ByteString
35 | | ListTerm [Term]
36 | | BinaryTerm ByteString
37 | | BigintTerm Integer
38 | | BigbigintTerm Integer
39 | -- Composite (BERT specific) terms:
40 | | NilTerm
41 | | BoolTerm Bool
42 | | DictionaryTerm [(Term, Term)]
43 | | TimeTerm UTCTime
44 | | RegexTerm String [String]
45 | deriving (Eq, Ord, Show, Read)
46 |
47 | -- The 0th-hour as per the BERT spec.
48 | zeroHour = UTCTime (read "1970-01-01") 0
49 |
50 | decomposeTime :: UTCTime -> (Int, Int, Int)
51 | decomposeTime t = (mS, s, uS)
52 | where
53 | d = diffUTCTime t zeroHour
54 | (mS, s) = floor d `divMod` 1000000
55 | uS = floor $ 1000000 * snd (properFraction d)
56 |
57 | composeTime :: (Int, Int, Int) -> UTCTime
58 | composeTime (mS, s, uS) = addUTCTime seconds zeroHour
59 | where
60 | mS' = fromIntegral mS
61 | s' = fromIntegral s
62 | uS' = fromIntegral uS
63 | seconds = (mS' * 1000000) + s' + (uS' / 1000000)
64 |
65 | -- Another design would be to split the Term type into
66 | -- SimpleTerm|CompositeTerm, and then do everything in one go, but
67 | -- that complicates syntax and semantics for end users. Let's do this
68 | -- one ugly thing instead, eh?
69 | ct b rest = TupleTerm $ [AtomTerm "bert", AtomTerm b] ++ rest
70 | compose NilTerm = ListTerm []
71 | compose (BoolTerm True) = ct "true" []
72 | compose (BoolTerm False) = ct "false" []
73 | compose (DictionaryTerm kvs) =
74 | ct "dict" [ListTerm $ map (\(k, v) -> TupleTerm [k, v]) kvs]
75 | compose (TimeTerm t) =
76 | ct "time" [IntTerm mS, IntTerm s, IntTerm uS]
77 | where
78 | (mS, s, uS) = decomposeTime t
79 | compose (RegexTerm s os) =
80 | ct "regex" [BytelistTerm (C.pack s),
81 | TupleTerm [ListTerm $ map AtomTerm os]]
82 | compose _ = error "invalid composite term"
83 |
84 | showTerm (IntTerm x) = show x
85 | showTerm (FloatTerm x) = printf "%15.15e" x
86 | showTerm (AtomTerm "") = ""
87 | showTerm (AtomTerm a@(x:xs))
88 | | isAsciiLower x = a
89 | | otherwise = "'" ++ a ++ "'"
90 | showTerm (TupleTerm ts) =
91 | "{" ++ intercalate ", " (map showTerm ts) ++ "}"
92 | showTerm (BytelistTerm bs) = show $ C.unpack bs
93 | showTerm (ListTerm ts) =
94 | "[" ++ intercalate ", " (map showTerm ts) ++ "]"
95 | showTerm (BinaryTerm b)
96 | | all (isAscii . chr . fromIntegral) (B.unpack b) =
97 | wrap $ "\"" ++ C.unpack b ++ "\""
98 | | otherwise =
99 | wrap $ intercalate ", " $ map show $ B.unpack b
100 | where
101 | wrap x = "<<" ++ x ++ ">>"
102 | showTerm (BigintTerm x) = show x
103 | showTerm (BigbigintTerm x) = show x
104 | -- All other terms are composite:
105 | showTerm t = showTerm . compose $ t
106 |
107 | class BERT a where
108 | -- | Introduce a 'Term' from a Haskell value.
109 | showBERT :: a -> Term
110 | -- | Attempt to read a haskell value from a 'Term'.
111 | readBERT :: Term -> Either String a
112 |
113 | -- Herein are some instances for common Haskell data types. To do
114 | -- anything more complicated, you should make your own instance.
115 |
116 | instance BERT Term where
117 | showBERT = id
118 | readBERT = return
119 |
120 | instance BERT Int where
121 | showBERT = IntTerm
122 | readBERT (IntTerm value) = return value
123 | readBERT _ = fail "Invalid integer type"
124 |
125 | instance BERT Bool where
126 | showBERT = BoolTerm
127 | readBERT (BoolTerm x) = return x
128 | readBERT _ = fail "Invalid bool type"
129 |
130 | instance BERT Integer where
131 | showBERT = BigbigintTerm
132 | readBERT (BigintTerm x) = return x
133 | readBERT (BigbigintTerm x) = return x
134 | readBERT _ = fail "Invalid integer type"
135 |
136 | instance BERT Float where
137 | showBERT = FloatTerm
138 | readBERT (FloatTerm value) = return value
139 | readBERT _ = fail "Invalid floating point type"
140 |
141 | instance BERT String where
142 | showBERT = BytelistTerm . C.pack
143 | readBERT (BytelistTerm x) = return $ C.unpack x
144 | readBERT (BinaryTerm x) = return $ C.unpack x
145 | readBERT (AtomTerm x) = return x
146 | readBERT (ListTerm xs) = map chr <$> mapM readBERT xs
147 | readBERT _ = fail "Invalid string type"
148 |
149 | instance BERT ByteString where
150 | showBERT = BytelistTerm
151 | readBERT (BytelistTerm value) = return value
152 | readBERT _ = fail "Invalid bytestring type"
153 |
154 | instance (BERT a) => BERT [a] where
155 | showBERT xs = ListTerm $ map showBERT xs
156 | readBERT (ListTerm xs) = mapM readBERT xs
157 | readBERT _ = fail "Invalid list type"
158 |
159 | instance (BERT a, BERT b) => BERT (a, b) where
160 | showBERT (a, b) = TupleTerm [showBERT a, showBERT b]
161 | readBERT (TupleTerm [a, b]) = liftM2 (,) (readBERT a) (readBERT b)
162 | readBERT _ = fail "Invalid tuple(2) type"
163 |
164 | instance (BERT a, BERT b, BERT c) => BERT (a, b, c) where
165 | showBERT (a, b, c) = TupleTerm [showBERT a, showBERT b, showBERT c]
166 | readBERT (TupleTerm [a, b, c]) =
167 | liftM3 (,,) (readBERT a) (readBERT b) (readBERT c)
168 | readBERT _ = fail "Invalid tuple(3) type"
169 |
170 | instance (BERT a, BERT b, BERT c, BERT d) => BERT (a, b, c, d) where
171 | showBERT (a, b, c, d) =
172 | TupleTerm [showBERT a, showBERT b, showBERT c, showBERT d]
173 | readBERT (TupleTerm [a, b, c, d]) =
174 | liftM4 (,,,) (readBERT a) (readBERT b) (readBERT c) (readBERT d)
175 | readBERT _ = fail "Invalid tuple(4) type"
176 |
177 | instance (Ord k, BERT k, BERT v) => BERT (Map k v) where
178 | showBERT m = DictionaryTerm
179 | $ map (\(k, v) -> (showBERT k, showBERT v)) (Map.toList m)
180 | readBERT (DictionaryTerm kvs) =
181 | Map.fromList <$>
182 | mapM (\ (k, v) -> liftM2 (,) (readBERT k) (readBERT v)) kvs
183 | readBERT _ = fail "Invalid map type"
184 |
185 | -- Binary encoding & decoding.
186 | instance Serialize Term where
187 | put term = putWord8 131 >> putTerm term
188 | get = getWord8 >>= \case
189 | 131 -> getTerm
190 | _ -> fail "bad magic"
191 |
192 | -- | Binary encoding of a single term (without header)
193 | putTerm :: Term -> Put
194 | putTerm (IntTerm value)
195 | | 0 <= value && value < 256 = tag 97 >> put8u value
196 | | otherwise = tag 98 >> put32s value
197 | putTerm (FloatTerm value) = tag 99 >> (putL . C.pack . pad $ printf "%15.15e" value)
198 | where
199 | pad s = s ++ replicate (31 - length s) '\0'
200 | putTerm (AtomTerm value)
201 | | len < 256 = tag 100 >> put16u len >> putL (C.pack value)
202 | | otherwise = fail "BERT atom too long (>= 256)"
203 | where
204 | len = length value
205 | putTerm (TupleTerm value)
206 | | len < 256 = tag 104 >> put8u len >> forM_ value putTerm
207 | | otherwise = tag 105 >> put32u len >> forM_ value putTerm
208 | where
209 | len = length value
210 | putTerm (BytelistTerm value)
211 | | len < 65536 = tag 107 >> put16u len >> putL value
212 | | otherwise = do -- too big: encode as a list.
213 | tag 108
214 | put32u len
215 | forM_ (B.unpack value) $ \v -> do
216 | tag 97
217 | putWord8 v
218 | where
219 | len = B.length value
220 | putTerm (ListTerm value)
221 | | len == 0 = putNil -- this is mentioned in the BERT spec.
222 | | otherwise= do
223 | tag 108
224 | put32u $ length value
225 | forM_ value putTerm
226 | putNil
227 | where
228 | len = length value
229 | putNil = putWord8 106
230 | putTerm (BinaryTerm value) = tag 109 >> put32u (B.length value) >> putL value
231 | putTerm (BigintTerm value) = tag 110 >> putBigint put8u value
232 | putTerm (BigbigintTerm value) = tag 111 >> putBigint put32u value
233 | -- All other terms are composite:
234 | putTerm t = putTerm . compose $ t
235 |
236 | -- | Binary decoding of a single term (without header)
237 | getTerm :: Get Term
238 | getTerm = do
239 | tag <- get8u
240 | case tag of
241 | 97 -> IntTerm <$> get8u
242 | 98 -> IntTerm <$> get32s
243 | 99 -> FloatTerm . read . C.unpack <$> getL 31
244 | 100 -> AtomTerm . C.unpack <$> (get16u >>= getL)
245 | 104 -> get8u >>= getN >>= tupleTerm
246 | 105 -> get32u >>= getN >>= tupleTerm
247 | 106 -> return $ ListTerm []
248 | 107 -> BytelistTerm <$> (get16u >>= getL)
249 | 108 -> get32u >>= \n -> ListTerm <$> (getN n <* expectNil)
250 | 109 -> BinaryTerm <$> (get32u >>= getL)
251 | 110 -> BigintTerm . fromIntegral <$> getBigint get8u
252 | 111 -> (BigintTerm . fromIntegral) <$> getBigint get32u
253 | where
254 | getN :: Int -> Get [Term]
255 | getN n = replicateM n getTerm
256 | expectNil :: Get ()
257 | expectNil = do
258 | tag <- get8u
259 | case tag of
260 | 106 -> return ()
261 | _ -> fail $ "invalid list - expected list ending with Nil"
262 | -- First try & decode composite terms.
263 | tupleTerm [AtomTerm "bert", AtomTerm "true"] = return $ BoolTerm True
264 | tupleTerm [AtomTerm "bert", AtomTerm "false"] = return $ BoolTerm False
265 | tupleTerm [AtomTerm "bert", AtomTerm "dict", ListTerm kvs] = mapM toTuple kvs >>= return . DictionaryTerm
266 | where
267 | toTuple (TupleTerm [k, v]) = return $ (k, v)
268 | toTuple _ = fail "invalid dictionary"
269 | tupleTerm [AtomTerm "bert", AtomTerm "time", IntTerm mS, IntTerm s, IntTerm uS] =
270 | return $ TimeTerm $ composeTime (mS, s, uS)
271 | tupleTerm [AtomTerm "bert", AtomTerm "regex", BytelistTerm s, ListTerm os] =
272 | options os >>= return . RegexTerm (C.unpack s)
273 | -- TODO: type-check the options values as well
274 | where
275 | options [] = return []
276 | options ((AtomTerm o):os) = options os >>= return . (o :)
277 | options _ = fail "regex options must be atoms"
278 | -- All other tuples are just .. tuples
279 | tupleTerm xs = return $ TupleTerm xs
280 |
281 | putBigint putter value = do
282 | putter len -- TODO: verify size?
283 | if value < 0
284 | then put8u 1
285 | else put8u 0
286 | putL $ B.pack $ map (fromIntegral . digit) [0..len-1]
287 | where
288 | value' = abs value
289 | len = ceiling $ logBase 256 (fromIntegral $ value' + 1)
290 | digit pos = (value' `shiftR` (8 * pos)) .&. 0xFF
291 |
292 | getBigint getter = do
293 | len <- fromIntegral <$> getter
294 | sign <- get8u
295 | bytes <- getL len
296 | multiplier <-
297 | case sign of
298 | 0 -> return 1
299 | 1 -> return (-1)
300 | _ -> fail "Invalid sign byte"
301 | return $ (*) multiplier
302 | $ foldl (\s (n, d) -> s + d*(256^n)) 0
303 | $ zip [0..len-1] (map fromIntegral $ B.unpack bytes)
304 |
305 | -- Note about put32s/get32s:
306 | --
307 | -- When dealing with 32-bit signed ints, we first convert between Int and
308 | -- Int32, and only then cast to Word32. This is to ensure put and get are
309 | -- as close to inverse as possible. Coercing word types to and from
310 | -- integer types using 'fromIntegral' is guaranteed to preserve
311 | -- representation (see Notes in "Data.Int").
312 | --
313 | -- For an example of what can go wrong, see
314 | -- https://github.com/feuerbach/bert/issues/6
315 |
316 | put8u :: (Integral a) => a -> Put
317 | put8u = putWord8 . fromIntegral
318 | put16u :: (Integral a) => a -> Put
319 | put16u = putWord16be . fromIntegral
320 | put32u :: (Integral a) => a -> Put
321 | put32u = putWord32be . fromIntegral
322 | put32s :: (Integral a) => a -> Put
323 | put32s = putWord32be . (fromIntegral :: Int32 -> Word32) . fromIntegral
324 | putL = putByteString
325 |
326 | get8u :: (Integral a) => Get a
327 | get8u = fromIntegral <$> getWord8
328 | get16u :: (Integral a) => Get a
329 | get16u = fromIntegral <$> getWord16be
330 | get32u :: (Integral a) => Get a
331 | get32u = fromIntegral <$> getWord32be
332 | get32s :: (Integral a) => Get a
333 | get32s = fromIntegral . (fromIntegral :: Word32 -> Int32) <$> getWord32be
334 | getL :: (Integral a) => a -> Get ByteString
335 | getL = getByteString . fromIntegral
336 |
337 | tag :: Word8 -> Put
338 | tag = putWord8
339 |
--------------------------------------------------------------------------------