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 #-}
24 {-# LANGUAGE NoImplicitPrelude #-}
25 {-# LANGUAGE DataKinds #-}
26 {-# LANGUAGE DeriveGeneric #-}
27 {-# LANGUAGE FlexibleInstances #-}
28 {-# LANGUAGE OverloadedStrings #-}
29 {-# LANGUAGE TemplateHaskell #-}
30 {-# LANGUAGE TypeOperators #-}
31 {-# LANGUAGE KindSignatures #-}
32 {-# LANGUAGE TypeFamilies #-}
33 {-# LANGUAGE UndecidableInstances #-}
35 ---------------------------------------------------------------------
38 ---------------------------------------------------------------------
39 import Gargantext.Prelude
41 import System.IO (FilePath)
43 import GHC.Generics (D1, Meta (..), Rep)
44 import GHC.TypeLits (AppendSymbol, Symbol)
47 import Data.Aeson.Encode.Pretty (encodePretty)
48 import qualified Data.ByteString.Lazy.Char8 as BL8
50 import Data.Text (Text)
51 import qualified Data.Text.IO as T
52 --import qualified Data.Set as Set
55 import Network.Wai.Handler.Warp hiding (defaultSettings)
58 import Servant.Mock (mock)
59 --import Servant.Job.Server (WithCallbacks)
60 import Servant.Swagger
61 import Servant.Swagger.UI
62 -- import Servant.API.Stream
64 --import Gargantext.API.Swagger
65 import Gargantext.API.FrontEnd (FrontEndAPI, frontEndServer)
67 import Gargantext.API.Node ( Roots , roots
76 import Gargantext.Database.Types.Node ()
77 import Gargantext.API.Count ( CountAPI, count, Query)
78 import Gargantext.API.Search ( SearchAPI, search, SearchQuery)
79 --import Gargantext.API.Orchestrator
80 --import Gargantext.API.Orchestrator.Types
82 ---------------------------------------------------------------------
84 import GHC.Base (Applicative)
85 -- import Control.Lens
87 import Data.List (lookup)
88 import Data.Text.Encoding (encodeUtf8)
90 --import Network.Wai (Request, requestHeaders, responseLBS)
91 import Network.Wai (Request, requestHeaders)
92 --import qualified Network.Wai.Handler.Warp as Warp
93 import Network.Wai.Middleware.Cors
95 import Network.Wai.Middleware.RequestLogger
96 -- import qualified Network.Wai.Middleware.RequestLogger as RequestLogger
98 import Network.HTTP.Types hiding (Query)
101 import Gargantext.API.Settings
103 fireWall :: Applicative f => Request -> FireWall -> f Bool
105 let origin = lookup "Origin" (requestHeaders req)
106 let host = lookup "Host" (requestHeaders req)
108 let hostOk = Just (encodeUtf8 "localhost:3000")
109 let originOk = Just (encodeUtf8 "http://localhost:8008")
111 if origin == originOk
113 || (not $ unFireWall fw)
119 -- makeMockApp :: Env -> IO (Warp.Settings, Application)
120 makeMockApp :: MockEnv -> IO Application
122 let serverApp = appMock
124 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
125 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
126 let checkOriginAndHost app req resp = do
127 blocking <- fireWall req (env ^. menv_firewall)
130 False -> resp ( responseLBS status401 []
131 "Invalid Origin or Host header")
133 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
134 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
135 { corsOrigins = Nothing -- == /*
136 , corsMethods = [ methodGet , methodPost , methodPut
137 , methodDelete, methodOptions, methodHead]
138 , corsRequestHeaders = ["authorization", "content-type"]
139 , corsExposedHeaders = Nothing
140 , corsMaxAge = Just ( 60*60*24 ) -- one day
141 , corsVaryOrigin = False
142 , corsRequireOrigin = False
143 , corsIgnoreFailures = False
146 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
147 -- $ Warp.defaultSettings
149 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
150 pure $ logStdoutDev $ checkOriginAndHost $ corsMiddleware $ serverApp
154 makeDevApp :: Env -> IO Application
156 serverApp <- makeApp env
158 -- logWare <- mkRequestLogger def { destination = RequestLogger.Logger $ env^.logger }
159 --logWare <- mkRequestLogger def { destination = RequestLogger.Logger "/tmp/logs.txt" }
160 -- let checkOriginAndHost app req resp = do
161 -- blocking <- fireWall req (env ^. menv_firewall)
163 -- True -> app req resp
164 -- False -> resp ( responseLBS status401 []
165 -- "Invalid Origin or Host header")
167 let corsMiddleware = cors $ \_ -> Just CorsResourcePolicy
168 -- { corsOrigins = Just ([env^.settings.allowedOrigin], False)
169 { corsOrigins = Nothing -- == /*
170 , corsMethods = [ methodGet , methodPost , methodPut
171 , methodDelete, methodOptions, methodHead]
172 , corsRequestHeaders = ["authorization", "content-type"]
173 , corsExposedHeaders = Nothing
174 , corsMaxAge = Just ( 60*60*24 ) -- one day
175 , corsVaryOrigin = False
176 , corsRequireOrigin = False
177 , corsIgnoreFailures = False
180 --let warpS = Warp.setPort (8008 :: Int) -- (env^.settings.appPort)
181 -- $ Warp.defaultSettings
183 --pure (warpS, logWare $ checkOriginAndHost $ corsMiddleware $ serverApp)
184 pure $ logStdoutDev $ corsMiddleware $ serverApp
188 ---------------------------------------------------------------------
191 -- | API for serving @swagger.json@
192 type SwaggerAPI = SwaggerSchemaUI "swagger-ui" "swagger.json"
194 -- | API for serving main operational routes of @gargantext.org@
197 type GargAPI = "api" :> Summary "API " :> GargAPIVersion
198 -- | TODO :<|> Summary "Latest API" :> GargAPI'
201 type GargAPIVersion = "v1.0" :> Summary "v1.0: " :> GargAPI'
206 "user" :> Summary "First user endpoint"
210 :<|> "node" :> Summary "Node endpoint"
211 :> Capture "id" Int :> NodeAPI HyperdataAny
214 :<|> "corpus":> Summary "Corpus endpoint"
215 :> Capture "id" Int :> NodeAPI HyperdataCorpus
218 :<|> "annuaire":> Summary "Annuaire endpoint"
219 :> Capture "id" Int :> NodeAPI HyperdataAnnuaire
222 :<|> "nodes" :> Summary "Nodes endpoint"
223 :> ReqBody '[JSON] [Int] :> NodesAPI
225 -- :<|> "counts" :> Stream GET NewLineFraming '[JSON] Count :> CountAPI
227 :<|> "count" :> Summary "Count endpoint"
228 :> ReqBody '[JSON] Query :> CountAPI
231 :<|> "search":> Summary "Search endpoint"
232 :> ReqBody '[JSON] SearchQuery :> SearchAPI
234 :<|> "graph" :> Summary "Graph endpoint"
235 :> Capture "id" Int :> GraphAPI
238 :<|> "tree" :> Summary "Tree endpoint"
239 :> Capture "id" Int :> TreeAPI
242 -- :<|> "scraper" :> WithCallbacks ScraperAPI
248 -- :<|> "list" :> Capture "id" Int :> NodeAPI
249 -- :<|> "ngrams" :> Capture "id" Int :> NodeAPI
250 -- :<|> "auth" :> Capture "id" Int :> NodeAPI
251 ---------------------------------------------------------------------
252 type SwaggerFrontAPI = SwaggerAPI :<|> FrontEndAPI
254 type API = SwaggerFrontAPI :<|> GargAPI
256 ---------------------------------------------------------------------
257 -- | Server declaration
258 server :: Env -> IO (Server API)
260 -- orchestrator <- scrapyOrchestrator env
263 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAny)
264 :<|> nodeAPI conn (Proxy :: Proxy HyperdataCorpus)
265 :<|> nodeAPI conn (Proxy :: Proxy HyperdataAnnuaire)
267 :<|> count -- TODO: undefined
269 :<|> graphAPI conn -- TODO: mock
273 conn = env ^. env_conn
275 ---------------------------------------------------------------------
276 swaggerFront :: Server SwaggerFrontAPI
277 swaggerFront = schemaUiServer swaggerDoc
280 gargMock :: Server GargAPI
281 gargMock = mock apiGarg Proxy
283 ---------------------------------------------------------------------
284 makeApp :: Env -> IO Application
285 makeApp = fmap (serve api) . server
287 appMock :: Application
288 appMock = serve api (swaggerFront :<|> gargMock)
290 ---------------------------------------------------------------------
294 apiGarg :: Proxy GargAPI
296 ---------------------------------------------------------------------
298 schemaUiServer :: (Server api ~ Handler Swagger)
299 => Swagger -> Server (SwaggerSchemaUI' dir api)
300 schemaUiServer = swaggerSchemaUIServer
303 -- Type Family for the Documentation
304 type family TypeName (x :: *) :: Symbol where
306 TypeName Text = "Text"
307 TypeName x = GenericTypeName x (Rep x ())
309 type family GenericTypeName t (r :: *) :: Symbol where
310 GenericTypeName t (D1 ('MetaData name mod pkg nt) f x) = name
312 type Desc t n = Description (AppendSymbol (TypeName t) (AppendSymbol " | " n))
315 -- | Swagger Specifications
316 swaggerDoc :: Swagger
317 swaggerDoc = toSwagger (Proxy :: Proxy GargAPI)
318 & info.title .~ "Gargantext"
319 & info.version .~ "4.0.2" -- TODO same version as Gargantext
320 -- & info.base_url ?~ (URL "http://gargantext.org/")
321 & info.description ?~ "REST API specifications"
322 -- & tags .~ Set.fromList [Tag "Garg" (Just "Main perations") Nothing]
323 & applyTagsFor (subOperations (Proxy :: Proxy GargAPI)(Proxy :: Proxy GargAPI))
324 ["Gargantext" & description ?~ "Main operations"]
325 & info.license ?~ ("AGPLV3 (English) and CECILL (French)" & url ?~ URL urlLicence )
327 urlLicence = "https://gitlab.iscpif.fr/gargantext/haskell-gargantext/blob/master/LICENSE"
329 -- | Output generated @swagger.json@ file for the @'TodoAPI'@.
330 swaggerWriteJSON :: IO ()
331 swaggerWriteJSON = BL8.writeFile "swagger.json" (encodePretty swaggerDoc)
333 portRouteInfo :: PortNumber -> IO ()
334 portRouteInfo port = do
335 T.putStrLn " ----Main Routes----- "
336 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/index.html"
337 T.putStrLn $ "http://localhost:" <> toUrlPiece port <> "/swagger-ui"
339 -- | startGargantext takes as parameters port number and Ini file.
340 startGargantext :: PortNumber -> FilePath -> IO ()
341 startGargantext port file = do
342 env <- newEnv port file
344 app <- makeDevApp env
347 startGargantextMock :: PortNumber -> IO ()
348 startGargantextMock port = do
350 application <- makeMockApp . MockEnv $ FireWall False