git.iko.soy/typeable/servant-swagger-ui
on master help sign in
Merge pull request #12 from phadej/issue-8

Introduce SwaggerSchemaUI

commit6c9ca854a2c1e53a1b841cf0aa322831462ac314 authorOleg Grenrus <oleg.grenrus@iki.fi> dateSat 20 Aug 2016, 12:53 (10 years ago) parent86d053f64159d2
9 files changed+186−168
.stylish-haskell.yaml+5−66 CHANGELOG.md+5−2 example/Main.hs+43−35 index.html.tmpl+13−4 package.yaml+21−3 servant-swagger-ui.cabal+26−3 src/Servant/Swagger/UI.hs+70−52 src/Servant/Swagger/UI/Internal.hs+2−2 stack.yaml+1−1
.stylish-haskell.yaml+5−66
@@ -1,77 +1,16 @@
1−# stylish-haskell configuration file
2−# ==================================
3−
4−# The stylish-haskell tool is mainly configured by specifying steps. These steps
5−# are a list, so they have an order, and one specific step may appear more than
6−# once (if needed). Each file is processed by these steps in the given order.
71steps:
8− # Convert some ASCII sequences to their Unicode equivalents. This is disabled
9− # by default.
10− # - unicode_syntax:
11− # # In order to make this work, we also need to insert the UnicodeSyntax
12− # # language pragma. If this flag is set to true, we insert it when it's
13− # # not already present. You may want to disable it if you configure
14− # # language extensions using some other method than pragmas. Default:
15− # # true.
16− # add_language_pragma: true
17−
18− # Import cleanup
192 - imports:
20− # There are different ways we can align names and lists.
21− #
22− # - global: Align the import names and import list throughout the entire
23− # file.
24− #
25− # - file: Like global, but don't add padding when there are no qualified
26− # imports in the file.
27− #
28− # - group: Only align the imports per group (a group is formed by adjacent
29− # import lines).
30− #
31− # - none: Do not perform any alignment.
32− #
33− # Default: global.
343 align: group
35−
36− # Language pragmas
4+ list_align: after_alias
5+ long_list_align: new_line
6+ empty_list_align: right_after
7+ list_padding: module_name
378 - language_pragmas:
38− # We can generate different styles of language pragma lists.
39− #
40− # - vertical: Vertical-spaced language pragmas, one per line.
41− #
42− # - compact: A more compact style.
43− #
44− # - compact_line: Similar to compact, but wrap each line with
45− # `{-#LANGUAGE #-}'.
46− #
47− # Default: vertical.
489 style: vertical
49−
50− # stylish-haskell can detect redundancy of some language pragmas. If this
51− # is set to true, it will remove those redundant pragmas. Default: true.
5210 remove_redundant: true
53−
54− # Align the types in record declarations
55− - records: {}
56−
57− # Replace tabs by spaces. This is disabled by default.
58− # - tabs:
59− # # Number of spaces to use for each tab. Default: 8, as specified by the
60− # # Haskell report.
61− # spaces: 8
62−
63− # Remove trailing whitespace
6411 - trailing_whitespace: {}
65−
66−# A common setting is the number of columns (parts of) code will be wrapped
67−# to. Different steps take this into account. Default: 80.
6812columns: 80
69−
70−# Sometimes, language extensions are specified in a cabal file or from the
71−# command line instead of using language pragmas in the file. stylish-haskell
72−# needs to be aware of these, so it can parse the file correctly.
73−#
74−# No language extensions are enabled by default.
7513language_extensions:
7614 - MultiParamTypeClasses
7715 - FlexibleContexts
16+ - ExplicitForAll
CHANGELOG.md+5−2
@@ -1,6 +1,9 @@
1−- 0.1.1.2.1.5
1+- 0.2.0.2.1.5
22
3− - update bundled swagger-ui to 2.1.5
3+ - Introduce `SwaggerSchemaUI`, this fixes
4+ [Swagger UI does not work when nested](https://github.com/phadej/servant-swagger-ui/issues/8) issue
5+ by sacrificing some subjective flexibility.
6+ - update bundled swagger-ui to version 2.1.5
47
58- 0.1.1.2.1.4
69
example/Main.hs+43−35
@@ -6,7 +6,6 @@
66{-# LANGUAGE MultiParamTypeClasses #-}
77{-# LANGUAGE OverloadedStrings #-}
88{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TemplateHaskell #-}
109{-# LANGUAGE TypeFamilies #-}
1110{-# LANGUAGE TypeOperators #-}
1211{-# LANGUAGE UndecidableInstances #-}
@@ -14,7 +13,7 @@
1413module Main (main, catIsMale, catName) where
1514
1615import Control.Lens hiding ((.=))
17−import Data.Aeson (ToJSON (..), object, (.=))
16+import Data.Aeson (ToJSON)
1817import Data.Maybe (fromMaybe)
1918import Data.String (IsString (..))
2019import Data.Text (Text)
@@ -30,14 +29,10 @@ import Servant.Swagger.UI
3029
3130import qualified Network.Wai.Handler.Warp as Warp
3231
33−--
34−
32+-- data types
3533data Cat = Cat { catName :: CatName, catIsMale :: Bool }
3634 deriving (Generic, Show)
3735
38−instance ToJSON Cat where
39− toJSON (Cat n g) = object [ "catName" .= n , "catIsMale" .= g]
40−
4136newtype CatName = CatName Text
4237 deriving ( Eq, Show, Generic
4338#if MIN_VERSION_servant(0,5,0)
@@ -47,61 +42,74 @@ newtype CatName = CatName Text
4742#endif
4843 )
4944
50−instance ToJSON CatName where
51− toJSON (CatName n) = toJSON n
52−
5345instance IsString CatName where
5446 fromString = CatName . fromString
5547
48+-- swagger instances
49+instance ToJSON Cat
50+instance ToJSON CatName
5651instance ToParamSchema CatName
5752instance ToSchema Cat
5853instance ToSchema CatName
5954
60−---
61−
55+-- api
6256type BasicAPI = Get '[PlainText, JSON] Text
6357 :<|> "cat" :> Capture ":name" CatName :> Get '[JSON] Cat
6458 :<|> "cat2" :> Capture ":name" CatName :> Get '[JSON] Cat
6559 :<|> "cat3" :> Capture ":name" CatName :> Get '[JSON] Cat
6660
67−type SwaggerSchemaEndpoint = "swagger.js" :> Get '[JSON] Swagger
68−
69−data API
70−type API' = SwaggerSchemaEndpoint
71− :<|> SwaggerUI "ui" SwaggerSchemaEndpoint API
61+type API =
62+ -- this serves both: swagger.json and swagger-ui
63+ SwaggerSchemaUI "swagger-ui" "swagger.json"
7264 :<|> BasicAPI
7365
74−instance HasServer API
75−#if MIN_VERSION_servant(0,5,0)
76− context
77−#endif
78− where
79− type ServerT API m = ServerT API' m
80− route _ = route (Proxy :: Proxy API')
81−
82−type instance IsElem' e API = IsElem e API'
66+-- To test nested case
67+type API' = API
68+ :<|> "nested" :> API
69+ :<|> SwaggerSchemaUI' "foo-ui" ("foo" :> "swagger.json" :> Get '[JSON] Swagger)
8370
8471-- Implementation
85−server :: Server API
86−server = return swaggerDoc
87− :<|> swaggerUIServer
88− :<|> (return "Hello World" :<|> catEndpoint :<|> catEndpoint :<|> catEndpoint)
72+
73+-- | We test different ways to nest API, so we have an enumeration
74+data Variant
75+ = Normal
76+ | Nested
77+ | SpecDown
78+ deriving (Eq)
79+
80+server' :: Server API'
81+server' = server Normal
82+ :<|> server Nested
83+ :<|> swaggerSchemaUIServer (swaggerDoc' SpecDown)
8984 where
90− catEndpoint n = return $ Cat n False
85+ server :: Variant -> Server API
86+ server variant =
87+ swaggerSchemaUIServer (swaggerDoc' variant)
88+ :<|> (return "Hello World" :<|> catEndpoint :<|> catEndpoint :<|> catEndpoint)
89+ where
90+ catEndpoint n = return $ Cat n (variant == Normal)
91+ -- Unfortunately we have to specify the basePath manually atm.
92+
93+ swaggerDoc' Normal = swaggerDoc
94+ swaggerDoc' Nested = swaggerDoc
95+ & basePath ?~ "/nested"
96+ & info.description ?~ "Nested API"
97+ swaggerDoc' SpecDown = swaggerDoc
98+ & info.description ?~ "Spec nested"
9199
92100-- Boilerplate
93101
94102swaggerDoc :: Swagger
95103swaggerDoc = toSwagger (Proxy :: Proxy BasicAPI)
96104 & info.title .~ "Cats API"
97− & info.version .~ "2016.2.6"
98− & info.description ?~ "This is an API that tests servant-swagger support "
105+ & info.version .~ "2016.8.7"
106+ & info.description ?~ "This is an API that tests servant-swagger support"
99107
100−api :: Proxy API
108+api :: Proxy API'
101109api = Proxy
102110
103111app :: Application
104−app = serve api server
112+app = serve api server'
105113
106114main :: IO ()
107115main = do
index.html.tmpl+13−4
@@ -32,9 +32,12 @@
3232 <!-- <script src='lang/en.js' type='text/javascript'></script> -->
3333
3434 <script type="text/javascript">
35− // servant-swagger-ui: redirect to location with a trailing slash
36− // we need this because assets are served from relative paths
37− if (window.location.href[window.location.href.length - 1] !== '/') {
35+ if (window.location.href.match(/\/index.html\/$/)) {
36+ // drop trailing slash in "index.html/"
37+ window.location.href = window.location.href.replace(/.$/, "");
38+ } else if (!window.location.href.match(/\/(?:index.html)?$/)) {
39+ // servant-swagger-ui: redirect to location with a trailing slash
40+ // we need this because assets are served from relative paths
3841 window.location.href = window.location.href + '/';
3942 }
4043 </script>
@@ -46,7 +49,13 @@
4649 url = decodeURIComponent(url[1]);
4750 } else {
4851 // servant-swagger-ui addition
49− url = "SWAGGER_URL_PLACEHOLDER";
52+ var ssuDir = "SERVANT_SWAGGER_UI_DIR";
53+ var ssuSchema = "SERVANT_SWAGGER_UI_SCHEMA";
54+ // more concretely:
55+ // replace /swagger-ui/ or /swagger-ui/index.html with /swagger.json
56+ // we always have trailing slash!
57+ var re = new RegExp("\/" + ssuDir + "/(?:index\\.html)?$","g");
58+ url = window.location.pathname.replace(re, "/" + ssuSchema);
5059 }
5160
5261 hljs.configure({
package.yaml+21−3
@@ -1,5 +1,5 @@
11name: servant-swagger-ui
2−version: 0.1.1.2.1.5
2+version: 0.2.0.2.1.5
33synopsis: Servant swagger ui
44description: Provide embedded swagger UI for servant and swagger (i.e. servant-swagger)
55author: Oleg Grenrus <oleg.grenrus@iki.fi>
@@ -7,7 +7,7 @@ maintainer: Oleg Grenrus <oleg.grenrus@iki.fi>
77license: BSD3
88github: phadej/servant-swagger-ui
99category: Web
10−tested-with: GHC==7.10.3
10+tested-with: GHC==7.8.4, GHC==7.10.3, GHC==8.0.1
1111extra-source-files:
1212- README.md
1313- CHANGELOG.md
@@ -33,6 +33,24 @@ dependencies:
3333 - text >=1.2.0.6 && <1.3
3434 - wai-app-static >=3.0.1.1 && <3.2
3535
36+flags:
37+ servant-0-5:
38+ description: "Whether use servant >= 0.5"
39+ manual: false
40+ default: true
41+
42+when:
43+ - condition: flag(servant-0-5)
44+ then:
45+ dependencies:
46+ - servant >=0.5
47+ - transformers >=0.3 && <0.6
48+ - transformers-compat >=0.3 && <0.6
49+ else:
50+ dependencies:
51+ - servant <0.5
52+ - either
53+
3654library:
3755 source-dirs: src
3856
@@ -43,7 +61,7 @@ tests:
4361 dependencies:
4462 - servant-swagger-ui
4563 - servant-server >=0.4.4.5 && <0.9
46− - aeson >=0.8.0.2 && <0.12
64+ - aeson >=0.8.0.2 && <1.1
4765 - base-compat >=0.6.0 && <0.10
4866 - lens >=4.7.0.1 && <4.15
4967 - wai >=3.0.3.0 && <3.3
servant-swagger-ui.cabal+26−3
@@ -3,7 +3,7 @@
33-- see: https://github.com/sol/hpack
44
55name: servant-swagger-ui
6−version: 0.1.1.2.1.5
6+version: 0.2.0.2.1.5
77synopsis: Servant swagger ui
88description: Provide embedded swagger UI for servant and swagger (i.e. servant-swagger)
99category: Web
@@ -13,7 +13,7 @@ author: Oleg Grenrus <oleg.grenrus@iki.fi>
1313maintainer: Oleg Grenrus <oleg.grenrus@iki.fi>
1414license: BSD3
1515license-file: LICENSE
16−tested-with: GHC==7.10.3
16+tested-with: GHC==7.8.4, GHC==7.10.3, GHC==8.0.1
1717build-type: Simple
1818cabal-version: >= 1.10
1919
@@ -74,6 +74,11 @@ source-repository head
7474 type: git
7575 location: https://github.com/phadej/servant-swagger-ui
7676
77+flag servant-0-5
78+ description: Whether use servant >= 0.5
79+ manual: False
80+ default: True
81+
7782library
7883 hs-source-dirs:
7984 src
@@ -94,6 +99,15 @@ library
9499 , template-haskell >=2.9 && <2.12
95100 , text >=1.2.0.6 && <1.3
96101 , wai-app-static >=3.0.1.1 && <3.2
102+ if flag(servant-0-5)
103+ build-depends:
104+ servant >=0.5
105+ , transformers >=0.3 && <0.6
106+ , transformers-compat >=0.3 && <0.6
107+ else
108+ build-depends:
109+ servant <0.5
110+ , either
97111 exposed-modules:
98112 Servant.Swagger.UI
99113 Servant.Swagger.UI.Internal
@@ -123,9 +137,18 @@ test-suite example
123137 , wai-app-static >=3.0.1.1 && <3.2
124138 , servant-swagger-ui
125139 , servant-server >=0.4.4.5 && <0.9
126− , aeson >=0.8.0.2 && <0.12
140+ , aeson >=0.8.0.2 && <1.1
127141 , base-compat >=0.6.0 && <0.10
128142 , lens >=4.7.0.1 && <4.15
129143 , wai >=3.0.3.0 && <3.3
130144 , warp >=3.0.13.1 && <3.3
145+ if flag(servant-0-5)
146+ build-depends:
147+ servant >=0.5
148+ , transformers >=0.3 && <0.6
149+ , transformers-compat >=0.3 && <0.6
150+ else
151+ build-depends:
152+ servant <0.5
153+ , either
131154 default-language: Haskell2010
src/Servant/Swagger/UI.hs+70−52
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE DeriveGeneric #-}
@@ -28,39 +29,13 @@
2829-- type BasicAPI = Get '[PlainText, JSON] Text
2930-- :\<|> "cat" :> Capture ":name" CatName :> Get '[JSON] Cat
3031--
31−-- -- | Swagger schema endpoint.
32−-- type SwaggerSchemaEndpoint = "swagger.js" :> Get '[JSON] Swagger
33−--
34−-- -- | Unhabitated new data type, to be able to refer to API type from the API type.
35−-- data API
36−--
37−-- -- | Underlying API type.
38−-- --
39−-- -- NOTE: place BasicAPI last for it not to override SwaggerSchemaEndpoing and SwaggerUI.
40−-- -- If you place BasicAPI first and it has Raw endpoint at root Swagger schema and UI won't work.
41−-- type API' = SwaggerSchemaEndpoint
42−-- :\<|> 'SwaggerUI' "ui" SwaggerSchemaEndpoint API
32+-- -- | API type with bells and whistles, i.e. schema file and swagger-ui.
33+-- type API = 'SwaggerSchemaUI' "swagger.json" "swagger-ui"
4334-- :\<|> BasicAPI
4435--
45−-- -- Optionally we can do:
46−-- --
47−-- -- type API' = SwaggerSchemaEndpoint
48−-- -- :\<|> BasicAPI
49−-- --
50−-- -- type API = SwaggerUI "ui" SwaggerSchemaEndpoint API'
51−-- -- :\<|> API'
52−--
53−-- -- Unfortunately we have to write these trivial instances.
54−--
55−-- instance HasServer API context where
56−-- type ServerT API m = ServerT API' m
57−-- route _ = route (Proxy :: Proxy API')
58−--
59−-- type instance IsElem' e API = IsElem e API'
60−--
36+-- -- | Servant server for an API
6137-- server :: Server API
62−-- server = pure swaggerDoc
63−-- :\<|> 'swaggerUIServer'
38+-- server = 'swaggerSchemaUIServer' swaggerDoc
6439-- :\<|> (pure "Hello World" :\<|> catEndpoint)
6540-- where
6641-- catEndpoint name = pure $ Cat name False
@@ -68,47 +43,90 @@
6843
6944module Servant.Swagger.UI (
7045 -- * Swagger UI API
71− SwaggerUI,
72− swaggerUIServer,
46+ SwaggerSchemaUI,
47+ SwaggerSchemaUI',
48+ swaggerSchemaUIServer,
7349 -- * Internals
7450 SwaggerUiHtml(..),
7551 ) where
7652
7753import Data.ByteString (ByteString)
7854import Data.FileEmbed (embedStringFile)
79−import Data.Monoid ((<>))
80−import GHC.TypeLits (Symbol)
55+import Data.Swagger (Swagger)
56+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
8157import Network.Wai.Application.Static (embeddedSettings, staticApp)
8258import Servant
8359import Servant.HTML.Blaze (HTML)
84−import Text.Blaze (ToMarkup (..))
8560import Servant.Swagger.UI.Internal
61+import Text.Blaze (ToMarkup (..))
8662
8763import qualified Data.Text as T
8864
89−-- | Swagger API ui.
90−type SwaggerUI (dir :: Symbol) endpoint api = dir :>
91− ( Get '[HTML] (SwaggerUiHtml endpoint api) :<|>
92− "index.html" :> Get '[HTML] (SwaggerUiHtml endpoint api) :<|>
93− Raw)
65+#if MIN_VERSION_servant(0,5,0)
66+import Control.Monad.Trans.Except (ExceptT)
67+#else
68+import Control.Monad.Trans.Either (EitherT)
69+#define ExceptT EitherT
70+#endif
71+
72+-- | Swagger schema + ui api.
73+--
74+-- @SwaggerSchemaUI "swagger-ui" "swagger.json"@ will result into following hierarchy:
75+--
76+-- @
77+-- \/swagger.json
78+-- \/swagger-ui
79+-- \/swagger-ui\/index.html
80+-- \/swagger-ui\/...
81+-- @
82+--
83+type SwaggerSchemaUI (dir :: Symbol) (schema :: Symbol) =
84+ SwaggerSchemaUI' dir (schema :> Get '[JSON] Swagger)
85+
86+-- | Use 'SwaggerSchemaUI'' when you need even more control over
87+-- where @swagger.json@ is served (e.g. subdirectory).
88+type SwaggerSchemaUI' (dir :: Symbol) (api :: *) =
89+ api
90+ :<|> dir :>
91+ ( Get '[HTML] (SwaggerUiHtml dir api)
92+ :<|> "index.html" :> Get '[HTML] (SwaggerUiHtml dir api)
93+ :<|> Raw
94+ )
9495
9596-- | Index file for swagger ui.
9697--
97−-- It's configured by the location of swagger schema.
98−data SwaggerUiHtml endpoint api = SwaggerUiHtml
98+-- It's configured by the location of swagger schema and directory it lives under.
99+--
100+-- Implementation detail: the @index.html@ is prepopulated with parameters
101+-- to find schema file automatically.
102+data SwaggerUiHtml (dir :: Symbol) (api :: *) = SwaggerUiHtml
99103
100−instance (IsElem endpoint api, HasLink endpoint, MkLink endpoint ~ URI)
101− => ToMarkup (SwaggerUiHtml endpoint api) where
102− toMarkup _ = preEscapedToMarkup $
103− T.replace "SWAGGER_URL_PLACEHOLDER" url swaggerUiIndexTemplate
104+instance (KnownSymbol dir, HasLink api, URI ~ MkLink api, IsElem api api)
105+ => ToMarkup (SwaggerUiHtml dir api)
106+ where
107+ toMarkup _ = preEscapedToMarkup
108+ $ T.replace "SERVANT_SWAGGER_UI_SCHEMA" schema
109+ $ T.replace "SERVANT_SWAGGER_UI_DIR" dir
110+ $ swaggerUiIndexTemplate
104111 where
105− uri = safeLink (Proxy :: Proxy api) (Proxy :: Proxy endpoint) :: URI
106− url = T.pack $ "/" <> uriPath uri -- TODO: do we need more?
112+ schema = T.pack $ uriPath $ safeLink proxyApi proxyApi
113+ dir = T.pack $ symbolVal (Proxy :: Proxy dir)
114+ proxyApi = Proxy :: Proxy api
107115
108−-- | Serve Swagger UI on @/<dir>@ using @endpoint@ as Swagger spec source for @api@.
109−swaggerUIServer :: Server (SwaggerUI dir endpoint api)
110−swaggerUIServer = return SwaggerUiHtml :<|> return SwaggerUiHtml :<|> rest
111− where rest = staticApp $ embeddedSettings swaggerUiFiles
116+-- | Serve Swagger UI on @/dir@ using @api@ as a Swagger spec source.
117+--
118+-- @
119+-- swaggerSchemaUIServer :: Swagger -> Server (SwaggerSchemaUI schema dir)
120+-- @
121+swaggerSchemaUIServer
122+ :: (Server api ~ ExceptT ServantErr IO Swagger)
123+ => Swagger -> Server (SwaggerSchemaUI' dir api)
124+swaggerSchemaUIServer swagger = return swagger
125+ :<|> return SwaggerUiHtml
126+ :<|> return SwaggerUiHtml
127+ :<|> rest
128+ where
129+ rest = staticApp $ embeddedSettings swaggerUiFiles
112130
113131swaggerUiIndexTemplate :: T.Text
114132swaggerUiIndexTemplate = $(embedStringFile "index.html.tmpl")
src/Servant/Swagger/UI/Internal.hs+2−2
@@ -19,8 +19,8 @@ import qualified Data.ByteString as B
1919import qualified Data.ByteString.Char8 as B8
2020import qualified Data.ByteString.Lazy as BL
2121import Language.Haskell.TH
22−import System.Directory (doesDirectoryExist,
23− getDirectoryContents)
22+import System.Directory
23+ (doesDirectoryExist, getDirectoryContents)
2424import System.FilePath (makeRelative, (</>))
2525
2626getRecursiveContents :: FilePath -> IO [(FilePath, BL.ByteString)]
stack.yaml+1−1
@@ -1,4 +1,4 @@
1−resolver: nightly-2016-07-19
1+resolver: nightly-2016-08-05
22packages:
33- '.'
44extra-deps: []