1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE UndecidableInstances #-}
module Server where
import Servant (Application,
FromHttpApiData (parseUrlPiece),
Server, err404, serve, throwError,
type (:>))
import Servant.API (Capture, FromHttpApiData, Get, JSON,
Post, ReqBody, type (:<|>) ((:<|>)))
import Servant.Docs (DocCapture (..), DocQueryParam (..),
ParamKind (..), ToCapture (..),
ToParam (..))
import Control.Concurrent.STM
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON),
ToJSONKey, genericParseJSON,
genericToJSON)
import qualified Data.Aeson as A
import Data.Functor ((<&>))
import Data.Map (Map)
import qualified Data.Map as M
import Data.Proxy (Proxy (Proxy))
import Data.Swagger
import Data.Text (Text)
import Data.Time (UTCTime (utctDay), dayOfWeek,
getCurrentTime)
import Data.UUID (UUID)
import qualified Data.UUID as UUID
import qualified Data.UUID.V4 as UUID
import Data.Vector (Vector)
import Database.Persist
import Database.Persist.TH
import GHC.Foreign (withCStringsLen)
import GHC.Generics (Generic)
import GTFS
import PersistOrphans
import Servant.Server (Handler)
import Servant.Swagger (toSwagger)
newtype Token = Token UUID
deriving newtype (Show, ToJSON, Eq, Ord, FromHttpApiData, ToJSONKey)
instance ToSchema Token where
declareNamedSchema _ = declareNamedSchema (Proxy @String)
instance ToParamSchema Token where
toParamSchema _ = toParamSchema (Proxy @String)
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
TripToken sql=tt_tracker_token
Id UUID default=uuid_generate_v4()
issued UTCTime
blocked Bool
tripNumber Text
deriving Eq Show Generic
TripPing json sql=tt_trip_ping
token UUID
latitude Double
longitude Double
delay Double
timestamp UTCTime
deriving Show Generic Eq
|]
instance ToSchema TripPing where
declareNamedSchema = genericDeclareNamedSchema (swaggerOptions "ping")
type KnownTrips = TVar (Map Token [TripPing])
type API = "stations" :> Get '[JSON] (Map StationID Station)
:<|> "timetable" :> Capture "Station ID" StationID :> Get '[JSON] (Map TripID (Trip Deep))
:<|> "trip" :> Capture "Trip ID" TripID :> Get '[JSON] (Trip Deep)
-- ingress API (put this behind BasicAuth?)
-- TODO: perhaps require a first ping for registration?
:<|> "trainregister" :> Capture "Trip ID" TripID :> Post '[JSON] Token
-- TODO: perhaps a websocket instead?
:<|> "trainping" :> Capture "Train Token" Token :> ReqBody '[JSON] TripPing :> Post '[JSON] ()
-- debug things
:<|> "debug" :> "state" :> Get '[JSON] (Map Token [TripPing])
type CompleteAPI = "debug" :> "openapi" :> Get '[JSON] Swagger
:<|> API
server :: GTFS -> KnownTrips -> Server CompleteAPI
server gtfs@GTFS{..} knownTrains = handleDebugAPI :<|> handleStations :<|> handleTimetable :<|> handleTrip
:<|> handleRegister :<|> handleTripPing :<|> handleDebugState
where handleStations = pure stations
handleTimetable station = do
today <- liftIO getCurrentTime <&> utctDay
pure $ tripsOnDay gtfs today
handleTrip trip = case M.lookup trip trips of
Just res -> pure res
Nothing -> throwError err404
handleRegister tripID = liftIO $ do
token <- UUID.nextRandom <&> Token
atomically $ modifyTVar knownTrains (M.insert token [])
pure token
handleTripPing token ping = liftIO $ atomically $ do
modifyTVar knownTrains (M.update (\history -> Just (ping : history)) token)
pure ()
handleDebugState = liftIO $ readTVarIO knownTrains
handleDebugAPI = pure $ toSwagger (Proxy @API)
application :: GTFS -> IO Application
application gtfs = do
knownTrips <- newTVarIO mempty
pure $ serve (Proxy @CompleteAPI) $ server gtfs knownTrips
{-
TODO:
there should be a basic API allowing the questions:
- what are the next trips leaving from $station? (or $geolocation?)
- all stops of a given tripID
then the "ingress" API:
- train ping (location, estimated delay, etc.)
- cancel trip
- add trip?
-}
|