apiary 1.1.0 → 1.1.1
raw patch · 7 files changed
+66/−42 lines, 7 files
Files
- CHANGELOG.md +3/−0
- apiary.cabal +3/−3
- src/Control/Monad/Apiary/Filter.hs +1/−1
- src/Data/Apiary/Document/Html.hs +22/−20
- src/Data/Apiary/Document/Internal.hs +35/−16
- static/api-documentation.min.css +1/−1
- static/api-documentation.min.js +1/−1
CHANGELOG.md view
@@ -1,3 +1,6 @@+# 1.1.1+* add request test to web API documentation.+ # 1.1.0 * Added Extension class. now Extension can add middleware. * Added apiary-helics submodule.
apiary.cabal view
@@ -1,5 +1,5 @@ name: apiary-version: 1.1.0+version: 1.1.1 synopsis: Simple and type safe web framework that can be automatically generate API documentation. description: Simple and type safe web framework that can be automatically generate API documentation.@@ -38,9 +38,9 @@ . * Type safe route filter. .- * Auto generate API documentation(example: <https://github.com/philopon/apiary/blob/v1.1.0/examples/api.hs>, <https://rawgit.com/philopon/apiary/v1.1.0/examples/api.html>).+ * Auto generate API documentation(example: <http://best-haskell.herokuapp.com/api/documentation>). .- more examples: <https://github.com/philopon/apiary/blob/v1.1.0/examples/>+ more examples: <https://github.com/philopon/apiary/blob/v1.1.1/examples/> . live demo: <http://best-haskell.herokuapp.com/> (source code: <https://github.com/philopon/best-haskell>)
src/Control/Monad/Apiary/Filter.hs view
@@ -232,7 +232,7 @@ -- | require Accept header and set response Content-Type. since 0.16.0. accept :: Monad actM => ContentType -> ApiaryT exts prms actM m () -> ApiaryT exts prms actM m ()-accept ect = focus (DocPrecondition $ "Accept: " <> toHtml (T.decodeUtf8 ect)) $+accept ect = focus (DocAccept ect) $ (lookup "Accept" . requestHeaders <$> getRequest) >>= \case Nothing -> mzero Just ac -> if parseContentType ect `elem` map (parseContentType . SC.dropWhile (== ' ')) (SC.split ',' ac)
src/Data/Apiary/Document/Html.hs view
@@ -13,7 +13,6 @@ import Language.Haskell.TH import Data.Monoid hiding (Any)-import Data.Maybe import Data.Default.Class import Data.Apiary.Param@@ -27,17 +26,20 @@ import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A +noDescription :: Html+noDescription = H.span "no description" ! A.class_ "no-description"+ routeToHtml :: Route -> (T.Text, Html, Html) routeToHtml = loop "" mempty [] where sp = H.span "/" ! A.class_ "splitter" loop e r p (Path s d) = loop (T.concat [e, "/", s]) (r <> sp <> H.span (toHtml s) ! A.class_ "path") p d loop e r p (Fetch k t mbh d) = - let r' = r <> sp <> rpHtml (toHtml k) (if isJust mbh then Nothing else Just . toHtml $ show t)- p' = maybe p (\h -> p <> [H.tr $ H.td (toHtml k) <> H.td (toHtml $ show t) <> H.td h]) mbh+ let r' = r <> sp <> rpHtml (toHtml k)+ p' = p <> [H.tr $ H.td (toHtml k) <> H.td (toHtml $ show t) <> H.td (maybe noDescription id mbh)] in loop (T.concat [e, "/", k, "::", T.pack $ show t]) r' p' d loop e r p (Rest k mbh) =- let p' = maybe p (\h -> p <> [H.tr $ H.td (toHtml k) <> H.td "[Text]" <> H.td h]) mbh+ let p' = p <> [H.tr $ H.td (toHtml k) <> H.td "[Text]" <> H.td (maybe noDescription id mbh)] in loop (T.concat [e, "/**", k]) (r <> sp <> (H.span "**" ! A.class_ "rest") <> (H.span (toHtml k) ! A.class_ "fetch")) p' End loop e r p Any = loop (T.concat [e, "/**"]) (r <> sp <> (H.span "**" ! A.class_ "rest")) p End@@ -71,7 +73,6 @@ , "ga('send', 'pageview');" ] - instance Default DefaultDocumentConfig where def = DefaultDocumentConfig "API documentation" Nothing True Nothing @@ -87,9 +88,9 @@ mcMap f = mconcat . map f cdns = mconcat- [ css "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"- , js "//code.jquery.com/jquery-2.1.1.min.js"- , js "//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"+ [ css "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"+ , js "http://code.jquery.com/jquery-2.1.1.min.js"+ , js "http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" ] embeds = $( do@@ -101,10 +102,12 @@ ] |]) - headH = mconcat + headH = mconcat [ H.title (toHtml documentTitle) , if documentUseCDN then cdns else embeds , $(runIO (readFile "static/jquery.cookie-1.4.1.min.js") >>= \c -> [|H.script $ preEscapedToHtml (c::String)|])+-- , H.script "" ! A.src "/static/api-documentation.js"+-- , H.link ! A.rel "stylesheet" ! A.href "/static/api-documentation.css" , $(runIO (readFile "static/api-documentation.min.js") >>= \c -> [|H.script $ preEscapedToHtml (c::String)|]) , $(runIO (readFile "static/api-documentation.min.css") >>= \c -> [|H.style $ preEscapedToHtml (c::String)|]) , maybe mempty analytics documentGoogleAnalytics@@ -115,19 +118,17 @@ htmlQR Check = "check" htmlQR NoValue = "-" - noDesc = H.span "no description" ! A.class_ "no-description"- query (QueryDoc p s q t) = H.tr . mconcat $ [ H.td (toHtml p) , H.td (toHtml $ strategyInfo s) , H.td (htmlQR q)- , H.td $ maybe noDesc id t+ , H.td $ maybe noDescription id t ] queriesH [] = mempty queriesH qs =- H.div ! A.class_ "col-sm-offset-1 col-md-offset-1" $- H.table ! A.class_ "table table-condensed" $ mconcat+ H.div $+ H.table ! A.class_ "query-parameters table table-condensed" $ mconcat [ H.caption "Query parameters" , H.tr $ mconcat [ H.th ! A.class_ "col-sm-1 col-md-1" $ "name" , H.th ! A.class_ "col-sm-1 col-md-1" $ "num"@@ -138,7 +139,7 @@ ] preconds [] = mempty- preconds p =+ preconds p = H.div ! A.class_ "well well-sm precondition" $ mconcat [ H.p "Preconditions:" , H.ul $ mcMap (\h -> H.li h) p@@ -149,9 +150,10 @@ , mcMap action ms ] - action (MethodDoc qs pc d) = H.div ! A.class_ "action col-sm-offset-1 col-md-offset-1" $ mconcat- [ preconds pc- , H.div $ H.p (toHtml d)+ action (MethodDoc qs pc a d) = H.div ! A.class_ "action col-sm-offset-1 col-md-offset-1" $ mconcat+ [ preconds $ maybe pc (\ac -> ("Accept: " <> toHtml (T.decodeUtf8 ac)) : pc) a+ , maybe mempty (\ac -> H.script ! A.class_ "accept" ! A.type_ "text/plain" $ (toHtml $ T.decodeUtf8 ac)) a+ , H.div (H.p $ toHtml d) , queriesH qs ] @@ -190,5 +192,5 @@ ] -- | construct Html as route parameter. since 0.13.0.-rpHtml :: Html -> Maybe Html -> Html-rpHtml k mbt = (H.span k ! A.class_ "fetch") <> maybe mempty (\t -> H.span (" :: " <> t) ! A.class_ "type") mbt+rpHtml :: Html -> Html+rpHtml k = (H.span k ! A.class_ "fetch")
src/Data/Apiary/Document/Internal.hs view
@@ -19,6 +19,7 @@ import Text.Blaze.Html import qualified Data.Text as T+import qualified Data.ByteString as S data Doc = DocPath T.Text Doc@@ -31,6 +32,7 @@ | DocMethod Method Doc | DocQuery T.Text StrategyRep QueryRep (Maybe Html) Doc | DocPrecondition Html Doc+ | DocAccept S.ByteString Doc | DocGroup T.Text Doc | Document T.Text Doc | Action@@ -73,30 +75,46 @@ data MethodDoc = MethodDoc { queries :: [QueryDoc] , preconditions :: [Html]+ , accept :: Maybe S.ByteString , document :: T.Text } -------------------------------------------------------------------------------- +data ToDocumentState = ToDocumentState+ { toDocumentPath :: Route -> Route+ , toDocumentMethodDoc :: [MethodDoc] -> [(Method, [MethodDoc])]+ , toDocumentQueries :: [QueryDoc] -> [QueryDoc]+ , toDocumentPreconds :: [Html] -> [Html]+ , toDocumentAccept :: Maybe S.ByteString+ , toDocumentDocument :: Maybe T.Text+ }++initialToDocumentState :: ToDocumentState+initialToDocumentState = ToDocumentState id (\md -> [("*", md)]) id id Nothing Nothing+ docToDocument :: Doc -> Maybe (Maybe T.Text, PathDoc) docToDocument = \case- (DocGroup "" d') -> (Nothing,) <$> loop id (\md -> [("*", md)]) id id Nothing d'- (DocGroup g d') -> (Just g,) <$> loop id (\md -> [("*", md)]) id id Nothing d'- d' -> (Nothing,) <$> loop id (\md -> [("*", md)]) id id Nothing d'+ (DocGroup "" d') -> (Nothing,) <$> loop initialToDocumentState d'+ (DocGroup g d') -> (Just g,) <$> loop initialToDocumentState d'+ d' -> (Nothing,) <$> loop initialToDocumentState d' where- loop ph mh qs ps doc (DocDropNext d) = loop ph mh qs ps doc (dropNext d)- loop ph mh qs pc doc (DocPath t d) = loop (ph . Path t) mh qs pc doc d- loop _ mh qs pc doc (DocRoot d) = loop (const $ Path "" End) mh qs pc doc d- loop ph mh qs pc doc (DocFetch k t h d) = loop (ph . Fetch k t h) mh qs pc doc d- loop ph mh qs pc doc (DocRest k h d) = loop (ph . const (Rest k h)) mh qs pc doc d- loop ph mh qs pc doc (DocAny d) = loop (ph . const Any) mh qs pc doc d- loop ph _ qs pc doc (DocMethod m d) = loop ph (\md -> [(m, md)]) qs pc doc d- loop ph mh qs pc doc (DocQuery p s q t d) = loop ph mh (qs . (QueryDoc p s q t:)) pc doc d- loop ph mh qs pc doc (DocPrecondition h d) = loop ph mh qs (pc . (h:)) doc d- loop ph mh qs pc doc (DocGroup _ d) = loop ph mh qs pc doc d- loop ph mh qs pc _ (Document t d) = loop ph mh qs pc (Just t) d- loop ph mh qs pc (Just t) Action = Just . PathDoc (ph End) $ mh [MethodDoc (qs []) (pc []) t]- loop _ _ _ _ Nothing Action = Nothing+ loop st (DocDropNext d) = loop st (dropNext d)+ loop st (DocPath p d) = loop st { toDocumentPath = toDocumentPath st . Path p } d+ loop st (DocRoot d) = loop st { toDocumentPath = const $ Path "" End } d+ loop st (DocFetch k t h d) = loop st { toDocumentPath = toDocumentPath st . Fetch k t h } d+ loop st (DocRest k h d) = loop st { toDocumentPath = toDocumentPath st . const (Rest k h) } d+ loop st (DocAny d) = loop st { toDocumentPath = toDocumentPath st . const Any } d+ loop st (DocMethod m d) = loop st { toDocumentMethodDoc = (\md -> [(m, md)]) } d+ loop st (DocQuery p s q t d) = loop st { toDocumentQueries = toDocumentQueries st . (QueryDoc p s q t:) } d+ loop st (DocPrecondition h d) = loop st { toDocumentPreconds = toDocumentPreconds st . (h:) } d+ loop st (DocGroup _ d) = loop st d+ loop st (DocAccept a d) = loop st { toDocumentAccept = Just a } d+ loop st (Document t d) = loop st { toDocumentDocument = Just t} d+ loop st Action = case toDocumentDocument st of+ Nothing -> Nothing+ Just doc -> Just . PathDoc (toDocumentPath st End) $ toDocumentMethodDoc st+ [MethodDoc (toDocumentQueries st []) (toDocumentPreconds st []) (toDocumentAccept st) doc] dropNext (DocPath _ d) = d dropNext (DocRoot d) = d@@ -108,6 +126,7 @@ dropNext (DocQuery _ _ _ _ d) = d dropNext (DocPrecondition _ d) = d dropNext (DocGroup _ d) = d+ dropNext (DocAccept _ d) = d dropNext (Document _ d) = d dropNext Action = Action
static/api-documentation.min.css view
@@ -1,1 +1,1 @@-.fetch{text-decoration:underline}.type .rest{color:gray}footer{padding-top:30px;padding-bottom:50px;margin-top:20px;text-align:center;color:#777;border-top:1px solid #e5e5e5}.description{margin-bottom:20px}table{margin-top:15px !important;margin-bottom:0 !important}table.route-parameters{margin-top:0 !important;border-bottom:1px solid #ddd;background-color:#f5f5f5}.method{margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #ddd}.method:last-child{margin-bottom:0;padding-bottom:0;border-bottom:0}.methods div{margin-left:10px;color:#999}.panel-heading,h1{cursor:pointer}.precondition{margin-bottom:10px}.action{margin-bottom:20px}.action:last-child{margin-bottom:0}.no-description{color:#aaa}+.fetch{text-decoration:underline}.type .rest{color:gray}footer{padding-top:30px;padding-bottom:50px;margin-top:20px;text-align:center;color:#777;border-top:1px solid #e5e5e5}.description{margin-bottom:20px}table{margin-top:15px !important;margin-bottom:0 !important}table.route-parameters{margin-top:0 !important;border-bottom:1px solid #ddd;background-color:#f5f5f5}.method{margin-bottom:20px;padding-bottom:10px;border-bottom:1px solid #ddd}.method:last-child{margin-bottom:0;padding-bottom:0;border-bottom:0}.methods div{margin-left:10px;color:#999}.panel-heading,h1{cursor:pointer}.precondition{margin-bottom:10px}.action{margin-bottom:20px}.action:last-child{margin-bottom:0}.no-description{color:#aaa}.test-method{margin-right:10px;font-weight:bold}.request-test{margin-bottom:20px}.route-string{color:#aaa}.request-result pre{all:unset}.close-btn{font-size:20px;font-weight:700px;opacity:.2;cursor:pointer;position:relative;top:-6px}.close-btn:hover{opacity:1}.bs-callout{padding:20px;margin:20px 0;border:1px solid #eee;border-left-width:5px;border-radius:3px}.bs-callout h4{margin-top:0;margin-bottom:5px}.bs-callout p:last-child{margin-bottom:0}.bs-callout code{border-radius:3px}.bs-callout+.bs-callout{margin-top:-5px}.bs-callout-default{border-left-color:#777}.bs-callout-default h4{color:#777}.bs-callout-primary{border-left-color:#428bca}.bs-callout-primary h4{color:#428bca}.bs-callout-success{border-left-color:#5cb85c}.bs-callout-success h4{color:#5cb85c}.bs-callout-danger{border-left-color:#d9534f}.bs-callout-danger h4{color:#d9534f}.bs-callout-warning{border-left-color:#f0ad4e}.bs-callout-warning h4{color:#f0ad4e}.bs-callout-info{border-left-color:#5bc0de}.bs-callout-info h4{color:#5bc0de}
static/api-documentation.min.js view
@@ -1,1 +1,1 @@-(function(){var unique=function(input){var u={},a=[];for(var i=0,l=input.length;i<l;++i){if(u.hasOwnProperty(input[i]))continue;a.push(input[i]);u[input[i]]=1}return a};var remove=function(input,v){var a=[];for(var i=0;i<input.length;i++){if(input[i]!=v)a.push(input[i])}return a};$.cookie.json=true;$.cookie.defaults.expires=365;$(function(){var current=$.cookie("open")||[];$(".collapse").on("show.bs.collapse",function(e){var cur=e.target.id.slice(9);current.push(cur);current=unique(current);current.sort();$.cookie("open",current)}).on("hide.bs.collapse",function(e){var cur=e.target.id.slice(9);current=remove(current,cur);$.cookie("open",current)});$("h1").click(function(){if(current.length==0){$(".collapse").each(function(i,e){$(e).collapse("show");var cur=e.id.slice(9);current.push(cur)}).promise().done(function(){current=unique(current);current.sort();$.cookie("open",current)})}else{$(".collapse").each(function(i,e){$(e).collapse("hide")});current=[];$.cookie("open",[])}});for(var i=0;i<current.length;i++){$("[id='collapse-"+current[i]+"']").addClass("in")}})})();+"use strict";$.cookie.json=true;$.cookie.defaults.path=window.location.pathname;$.cookie.defaults.expires=365;(function(){function get_panel_ids_common(selector){var ids=[];$(selector).each(function(){ids.push($(this).attr("id"))});return ids}function get_opened_panel_ids(){return get_panel_ids_common(".collapse.in")}function get_all_panel_ids(){return get_panel_ids_common(".collapse")}function open_panel_by_id(id){$('[id="'+id+'"]').addClass("in")}function save_panel_state(){$.cookie("panel",get_opened_panel_ids())}function restore_panel_state(){$.each($.cookie("panel")||[],function(){open_panel_by_id(this)})}function close_all_panels(){$(".collapse.in").collapse("hide");save_panel_state([])}function open_all_panels(){$(".collapse").not(".in").collapse("show");save_panel_state(get_all_panel_ids())}function toggle_all_panels(){get_opened_panel_ids().length==0?open_all_panels():close_all_panels()}$(function(){restore_panel_state();$(".collapse").on("shown.bs.collapse",save_panel_state);$(".collapse").on("hidden.bs.collapse",save_panel_state);$("h1").click(toggle_all_panels)})})();if(location.protocol.slice(0,4)=="http"){(function(){$(function(){var update_test_route_functions=[];var update_test_query_functions=[];$(".panel").each(function(){var panel=$(this);var panel_id=panel.find(".panel-collapse").attr("id");panel.find(".route-parameters tr:first").append($("<th>value</th>"));panel.find(".route-parameters tr:gt(0)").append($('<td><input class="form-control input-sm" /></td>'));panel.find(".query-parameters tr:first").append($("<th>value</th>"));panel.find(".query-parameters tr:gt(0)").append($('<td><div class="input-group"><span class="input-group-addon"><input type="checkbox"/></span><input class="form-control input-sm"/></div></td>'));panel.find(".method").each(function(){var method=$(this);var h4=method.find("h4:first");var method_str=h4.text();var div=$('<div class="request-test">');method.find("h4:first").replaceWith(div);div.append($('<button class="test-method btn btn-default">'+method_str+"</button>"));var span=$('<span class="route-string">');div.append(span);span.append($('<span class="route-url"></span>'));span.append($('<span class="route-params"></span>'));var callout=$('<div class="hidden request-result bs-callout"><div class="clearfix title"><h4 class="pull-left"></h4></div><div class="well headers"><pre><code></code></pre></div><div class="body"><pre><code></code></pre></div></div>');var close=$('<div class="close-btn pull-right">×</div>');callout.find(".title").append(close);close.click(function(){callout.addClass("hidden")});method.find(".action:last").append(callout)});var route_params={};panel.find(".route-parameters tr:gt(0)").each(function(){var p=$(this);route_params[p.find("td:first").text()]=p.find("td:last input")});function get_route(title){var ret="";title.children().each(function(){var p=$(this);if(p.hasClass("splitter")){ret+="/"}else if(p.hasClass("path")){ret+=p.text()}else if(p.hasClass("fetch")){ret+=route_params[p.text()].val()}});return ret}var route_template=panel.find(".panel-title");var route_url=panel.find(".route-url");function update_test_route(){var route=get_route(route_template);route_url.text(route)}update_test_route_functions.push(update_test_route);function save_route_parameters(){var save={};$.each(route_params,function(k,v){save[k]=v.val()});var rp=$.cookie("route")||{};rp[panel_id]=save;$.cookie("route",rp)}panel.find(".route-parameters tr:gt(0) input").keyup(update_test_route).change(save_route_parameters);panel.find(".method").each(function(){var method=$(this);var method_str=method.find(".test-method").text();var query_params={};method.find(".query-parameters tr:gt(0)").each(function(){var p=$(this);var checkbox=p.find('td:last input[type="checkbox"]');var input=p.find("td:last input.form-control");query_params[p.find("td:first").text()]={checkbox:checkbox[0],input:input}});function get_query_param(){var ret="";$.each(query_params,function(name,value){if(value.checkbox.checked){var val=value.input.val();if(val.length==0){ret+="&"+name}else{$.each(val.split(" "),function(){ret+="&"+name+"="+this})}}});if(ret.length>0){ret="?"+ret.slice(1)}return ret}var query_param=method.find(".route-params");function update_query_param(){query_param.text(get_query_param())}update_test_query_functions.push(update_query_param);function save_query_param(){var save={};$.each(query_params,function(name,value){save[name]={checkbox:value.checkbox.checked,value:value.input.val()}});var qp=$.cookie("query")||{};if(!qp[panel_id]){qp[panel_id]={}}qp[panel_id][method_str]=save;$.cookie("query",qp)}function update_and_save_query_param(){update_query_param();save_query_param()}method.find('.query-parameters input[type="checkbox"]').change(update_and_save_query_param);method.find(".query-parameters input.form-control").keyup(update_query_param).change(save_query_param);var method_btn=method.find(".test-method");var route_url=method.find(".route-url");var result=method.find(".request-result");var accept=method.find(".accept");function request_test(){if(method_btn.hasClass("disabled")){return}var url=route_url.text()+query_param.text();var meth=method_str=="*"?"GET":method_str;method_btn.addClass("disabled");function request_callback(data,status,xhr){var ct=xhr.getResponseHeader("Content-Type");var code=xhr.status;result.find("h4").text(code+" "+status+' "'+ct+'"');result.find(".headers code").text(xhr.getAllResponseHeaders());if(ct.slice(0,16)=="application/json"){try{var json=JSON.stringify(JSON.parse(data),undefined,2);result.find(".body code").text(json)}catch(e){result.find(".body code").text(e+"\n\n"+data)}}else{result.find(".body code").text(data)}}function beforeSend(xhr){if(accept.length==1){xhr.setRequestHeader("Accept",accept.text())}}$.ajax({type:meth,url:url,dataType:"text",beforeSend:beforeSend}).done(function(data,status,xhr){request_callback(data,status,xhr);result.addClass("bs-callout-primary");result.removeClass("bs-callout-danger")}).fail(function(xhr,status,err){request_callback(err,status,xhr);result.addClass("bs-callout-danger");result.removeClass("bs-callout-primary")}).always(function(){result.removeClass("hidden");method_btn.removeClass("disabled")})}method.find(".test-method").click(request_test)})});function restore_route_parameters(){$.each($.cookie("route")||{},function(id,dict){var panel=$('[id="'+id+'"]');panel.find(".route-parameters tr:gt(0)").each(function(){var row=$(this);var key=row.find("td:first").text();row.find("td:last input").val(dict[key]||"")})})}function restore_query_parameters(){$.each($.cookie("query")||{},function(id,dict){var panel=$('[id="'+id+'"]');panel.find(".method").each(function(){var method=$(this);var method_str=method.find(".test-method").text();method.find(".query-parameters tr:gt(0)").each(function(){var row=$(this);var key=row.find("td:first").text();var checkbox=row.find('td:last input[type="checkbox"]')[0];var input=row.find("td:last input.form-control");checkbox.checked=dict[method_str][key].checkbox||false;input.val(dict[method_str][key].value||"")})})})}restore_route_parameters();restore_query_parameters();$.each(update_test_route_functions,function(){this()});$.each(update_test_query_functions,function(){this()})})})()}