{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE NoMonomorphismRestriction #-} module Data.Apiary.Document where import Control.Applicative import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.ByteString as S import Data.Typeable import Data.Maybe import Data.Apiary.Param import qualified Network.HTTP.Types as HT import Text.Blaze.Html import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import Data.Monoid import Data.List import Data.Function data StrategyRep = StrategyRep { strategyInfo :: T.Text } deriving (Show, Eq) data Doc = DocPath T.Text Doc | DocRoot Doc | DocFetch TypeRep (Maybe Html) Doc | DocMethod HT.Method Doc | DocQuery S.ByteString StrategyRep QueryRep Html Doc | DocGroup T.Text Doc | Document (Maybe T.Text) data Route = Path T.Text Route | Fetch TypeRep (Maybe Html) Route | End instance Eq Route where Path a d == Path b d' = a == b && d == d' Fetch a _ d == Fetch b _ d' = a == b && d == d' End == End = True _ == _ = False data Documents = Documents { noGroup :: [PathDoc] , groups :: [(T.Text, [PathDoc])] } data PathDoc = PathDoc { path :: Route , methods :: [(HT.Method, MethodDoc)] } data QueryDoc = QueryDoc { queryName :: S.ByteString , queryStrategy :: StrategyRep , queryRep :: QueryRep , queryDocument :: Html } data MethodDoc = MethodDoc { queries :: [QueryDoc] , document :: T.Text } docToDocument :: Doc -> Maybe (Maybe T.Text, PathDoc) docToDocument = \case (DocGroup g d') -> (Just g,) <$> loop id (\md -> [("ANY", md)]) id d' d' -> (Nothing,) <$> loop id (\md -> [("ANY", md)]) id d' where loop ph mh qs (DocPath t d) = loop (ph . Path t) mh qs d loop _ mh qs (DocRoot d) = loop (const $ Path "" End) mh qs d loop ph mh qs (DocFetch t h d) = loop (ph . Fetch t h) mh qs d loop ph _ qs (DocMethod m d) = loop ph (\md -> [(m, md)]) qs d loop ph mh qs (DocQuery p s q t d) = loop ph mh (qs . (QueryDoc p s q t:)) d loop ph mh qs (DocGroup _ d) = loop ph mh qs d loop ph mh qs (Document (Just t)) = Just . PathDoc (ph End) $ mh (MethodDoc (qs []) t) loop _ _ _ (Document Nothing) = Nothing mergeMethod :: [PathDoc] -> [PathDoc] mergeMethod [] = [] mergeMethod (pd:pds) = merge pd (filter (same pd) pds) : mergeMethod (filter (not . same pd) pds) where same = (==) `on` path merge pd' pds' = PathDoc (path pd') (methods pd' ++ concatMap methods pds') docsToDocuments :: [Doc] -> Documents docsToDocuments doc = let gds = mapMaybe docToDocument doc ngs = mergeMethod . map snd $ filter ((Nothing ==) . fst) gds gs = map upGroup . groupBy ((==) `on` fst) $ mapMaybe trav gds in Documents ngs gs where upGroup ((g,d):ig) = (g, mergeMethod $ d : map snd ig) upGroup [] = error "docsToDocuments: unknown error." trav (Nothing, _) = Nothing trav (Just a, b) = Just (a, b) routeToHtml :: Route -> (Html, Html) routeToHtml = loop (1::Int) mempty [] where sp = H.span "/" ! A.class_ "splitter" loop i r p (Path s d) = loop i (r <> sp <> H.span (toHtml s) ! A.class_ "path") p d loop i r p (Fetch t Nothing d) = loop i (r <> sp <> H.span (toHtml $ ':' : show t) ! A.class_ "fetch") p d loop i r p (Fetch t (Just h) d) = loop (succ i) (r <> sp <> H.span (toHtml (':' : show t) <> H.sup (toHtml i)) ! A.class_ "fetch") (p <> [H.tr $ H.td (toHtml i) <> H.td (toHtml $ show t) <> H.td h]) d loop _ r p End = (r, if null p then mempty else H.table ! A.class_ "table table-condensed col-sm-offset-1 col-md-offset-1" $ H.caption "Route Parameters" <> H.tr (H.th "#" <> H.th "type" <> H.th "description") <> mconcat p ) defaultDocumentToHtml :: Documents -> Html defaultDocumentToHtml docs = H.docTypeHtml $ H.head headH <> H.body body where css u = H.link ! A.rel "stylesheet" ! A.href u headH = H.title "API documentation" <> css "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" htmlQR (Strict r) = toHtml (show r) htmlQR (Nullable r) = toHtml (show r ++ "?") htmlQR Check = toHtml ("check" :: T.Text) query (QueryDoc p s q t) = H.tr $ H.td (toHtml $ T.decodeUtf8 p) <> H.td (toHtml $ strategyInfo s) <> H.td (htmlQR q) <> H.td t queriesH [] = mempty queriesH qs = H.table ! A.class_ "table table-condensed col-sm-offset-1 col-md-offset-1" $ H.caption "Query Parameters" <> H.tr (H.th "name" <> H.th "num" <> H.th "type" <> H.th "description") <> mconcat (map query qs) method (m, MethodDoc qs d) = H.div ! A.class_ "col-sm-offset-1 col-md-offset-1" $ H.h4 (toHtml $ T.decodeUtf8 m) <> (H.p ! A.class_ "col-sm-offset-1 col-md-offset-1") (toHtml d) <> queriesH qs pathH (PathDoc r ms) = let (route, rdoc) = routeToHtml r in H.div $ H.h3 route <> rdoc <> (H.div $ mconcat (map method ms)) groupH (g, p) = H.div $ H.h2 (toHtml g) <> (mconcat $ map pathH p) doc (Documents n g) = H.div (mconcat $ map pathH n) <> mconcat (map groupH g) body = H.div ! A.class_ "container" $ H.h1 "API documentation" <> doc docs