2 Module : Gargantext.API
3 Description : REST API declaration
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 Main REST API of Gargantext (both Server and Client sides)
12 TODO App type, the main monad in which the bot code is written with.
14 Provide config, state, logs and IO
15 type App m a = ( MonadState AppState m
17 , MonadLog (WithSeverity Doc) m
19 Thanks @yannEsposito for this.
22 {-# OPTIONS_GHC -fno-warn-name-shadowing #-}
25 {-# LANGUAGE NoImplicitPrelude #-}
26 {-# LANGUAGE DataKinds #-}
27 {-# LANGUAGE DeriveGeneric #-}
28 {-# LANGUAGE FlexibleInstances #-}
29 {-# LANGUAGE OverloadedStrings #-}
30 {-# LANGUAGE TemplateHaskell #-}
31 {-# LANGUAGE TypeOperators #-}
32 {-# LANGUAGE KindSignatures #-}
33 {-# LANGUAGE TypeFamilies #-}
34 {-# LANGUAGE UndecidableInstances #-}
36 ---------------------------------------------------------------------
39 ---------------------------------------------------------------------
40 import Gargantext.Prelude
42 import System.IO (FilePath)
44 import GHC.Generics (D1, Meta (..), Rep)
45 import GHC.TypeLits (AppendSymbol, Symbol)
48 import Data.Aeson.Encode.Pretty (encodePretty)
49 import qualified Data.ByteString.Lazy.Char8 as BL8
51 import Data.Text (Text)
52 import qualified Data.Text.IO as T
53 --import qualified Data.Set as Set
56 import Network.Wai.Handler.Warp hiding (defaultSettings)
59 import Servant.Mock (mock)
60 --import Servant.Job.Server (WithCallbacks)
61 import Servant.Swagger
62 import Servant.Swagger.UI
63 -- import Servant.API.Stream
65 --import Gargantext.API.Swagger
66 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
68 import Gargantext.API.Node ( Roots , roots
74 import Gargantext.API.Count ( CountAPI, count, Query)
75 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
76 --import Gargantext.API.Orchestrator
77 --import Gargantext.API.Orchestrator.Types
79 ---------------------------------------------------------------------
81 import GHC.Base (Applicative)
82 -- import Control.Lens
84 import Data.List (lookup)
85 import Data.Text.Encoding (encodeUtf8)
87 --import Network.Wai (Request, requestHeaders, responseLBS)
88 import Network.Wai (Request, requestHeaders)
89 --import qualified Network.Wai.Handler.Warp as Warp
90 import Network.Wai.Middleware.Cors
92 import Network.Wai.Middleware.RequestLogger
93 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
95 import Network.HTTP.Types hiding (Query)
98 import Gargantext.API.Settings
100 fireWall :: Applicative f => Request -> FireWall -> f Bool
102 let origin = lookup "Origin" (requestHeaders req)
103 let host = lookup "Host" (requestHeaders req)
105 let hostOk = Just (encodeUtf8 "localhost:3000")
106 let originOk = Just (encodeUtf8 "http://localhost:8008")
108 if origin == originOk
110 || (not $ unFireWall fw)
116 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
117 makeMockApp :: MockEnv -> IO Application
119 let serverApp = appMock
121 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
122 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
123 let checkOriginAndHost app req resp = do
124 blocking <- fireWall req (env ^. menv_firewall)
127 False -> resp ( responseLBS status401 []
128 "Invalid Origin or Host header")
130 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
131 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
132 { corsOrigins = Nothing -- == /*
133 , corsMethods = [ methodGet , methodPost , methodPut
134 , methodDelete, methodOptions, methodHead]
135 , corsRequestHeaders = ["authorization", "content-type"]
136 , corsExposedHeaders = Nothing
137 , corsMaxAge = Just ( 60*60*24 ) -- one day
138 , corsVaryOrigin = False
139 , corsRequireOrigin = False
140 , corsIgnoreFailures = False
143 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
144 -- $ Warp.defaultSettings
146 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
147 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
151 makeDevApp :: Env -> IO Application
153 serverApp <- makeApp env
155 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
156 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
157 -- let checkOriginAndHost app req resp = do
158 -- blocking <- fireWall req (env ^. menv_firewall)
160 -- True -> app req resp
161 -- False -> resp ( responseLBS status401 []
162 -- "Invalid Origin or Host header")
164 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
165 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
166 { corsOrigins = Nothing -- == /*
167 , corsMethods = [ methodGet , methodPost , methodPut
168 , methodDelete, methodOptions, methodHead]
169 , corsRequestHeaders = ["authorization", "content-type"]
170 , corsExposedHeaders = Nothing
171 , corsMaxAge = Just ( 60*60*24 ) -- one day
172 , corsVaryOrigin = False
173 , corsRequireOrigin = False
174 , corsIgnoreFailures = False
177 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
178 -- $ Warp.defaultSettings
180 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
181 pure $ logStdoutDev $ corsMiddleware $ serverApp
185 ---------------------------------------------------------------------
188 -- | API for serving @swagger.json@
189 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
191 -- | API for serving main operational routes of @gargantext.org@
194 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
195 -- | TODO :<|> Summary "Latest API" :> GargAPI'
198 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
203 "user" :> Summary "First user endpoint"
208 :<|> "node" :> Summary "Node endpoint"
209 :> Capture "id" Int :> NodeAPI
213 :<|> "corpus":> Summary "Corpus endpoint"
214 :> Capture "id" Int :> NodeAPI
217 :<|> "nodes" :> Summary "Nodes endpoint"
218 :> ReqBody '[JSON] [Int] :> NodesAPI
220 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
222 :<|> "count" :> Summary "Count endpoint"
223 :> ReqBody '[JSON] Query :> CountAPI
226 :<|> "search":> Summary "Search endpoint"
227 :> ReqBody '[JSON] SearchQuery :> SearchAPI
229 :<|> "graph" :> Summary "Graph endpoint"
230 :> Capture "id" Int :> GraphAPI
233 :<|> "tree" :> Summary "Tree endpoint"
234 :> Capture "id" Int :> TreeAPI
237 -- :<|> "scraper" :> WithCallbacks ScraperAPI
243 -- :<|> "list" :> Capture "id" Int :> NodeAPI
244 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
245 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
246 ---------------------------------------------------------------------
247 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
249 type API = SwaggerFrontAPI :<|> GargAPI
251 ---------------------------------------------------------------------
252 -- | Server declaration
253 server :: Env -> IO (Server API)
255 -- orchestrator <- scrapyOrchestrator env
261 :<|> count -- TODO: undefined
263 :<|> graphAPI conn -- TODO: mock
267 conn = env ^. env_conn
269 ---------------------------------------------------------------------
270 swaggerFront :: Server SwaggerFrontAPI
271 swaggerFront = schemaUiServer swaggerDoc
274 gargMock :: Server GargAPI
275 gargMock = mock apiGarg Proxy
277 ---------------------------------------------------------------------
278 makeApp :: Env -> IO Application
279 makeApp = fmap (serve api) . server
281 appMock :: Application
282 appMock = serve api (swaggerFront :<|> gargMock)
284 ---------------------------------------------------------------------
288 apiGarg :: Proxy GargAPI
290 ---------------------------------------------------------------------
292 schemaUiServer :: (Server api ~ Handler Swagger)
293 => Swagger -> Server (SwaggerSchemaUI' dir api)
294 schemaUiServer = swaggerSchemaUIServer
297 -- Type Family for the Documentation
298 type family TypeName (x :: *) :: Symbol where
300 TypeName Text = "Text"
301 TypeName x = GenericTypeName x (Rep x ())
303 type family GenericTypeName t (r :: *) :: Symbol where
304 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
306 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
309 -- | Swagger Specifications
310 swaggerDoc :: Swagger
311 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
312 & info.title .~ "Gargantext"
313 & info.version .~ "0.1.0"
314 -- & info.base_url ?~ (URL "http://gargantext.org/")
315 & info.description ?~ "REST API specifications"
316 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
317 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
318 ["Gargantext" & description ?~ "Main operations"]
319 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
321 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
323 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
324 swaggerWriteJSON :: IO ()
325 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
327 portRouteInfo :: PortNumber -> IO ()
328 portRouteInfo port = do
329 T.putStrLn " ----Main Routes----- "
330 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
331 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
333 -- | startGargantext takes as parameters port number and Ini file.
334 startGargantext :: PortNumber -> FilePath -> IO ()
335 startGargantext port file = do
336 env <- newEnv port file
338 app <- makeDevApp env
341 startGargantextMock :: PortNumber -> IO ()
342 startGargantextMock port = do
344 application <- makeMockApp . MockEnv $ FireWall False