Skip to content
Snippets Groups Projects
Commit 23f907f2 authored by Cam Saul's avatar Cam Saul
Browse files

`defendpoint` accepts optional docstr. Automagically generate fany

documentation for all API endpoints based of params + annotations
parent e4188cdf
No related merge requests found
((clojure-mode . ((eval . (progn
;; Specify which arg is the docstring for certain macros
;; (Add more as needed)
(put 'defannotation 'clojure-doc-string-elt 2)
(put 'defendpoint 'clojure-doc-string-elt 3)
(put 'defhook 'clojure-doc-string-elt 2)
(put 'defsetting 'clojure-doc-string-elt 2)
......
......@@ -71,7 +71,9 @@
(hydrate annotation :author)))
(defendpoint PUT "/:id" [id :as {{:keys [start end title body]} :body}]
(defendpoint PUT "/:id"
"Update an existing `Annotation`."
[id :as {{:keys [start end title body]} :body}]
{start [Required Date]
end [Required Date]
title [Required NonEmptyString]
......
......@@ -48,7 +48,7 @@
(hydrate :can_read :can_write :organization)))
(defendpoint PUT "/:id" [id :as {{:keys [dataset_query description display name public_perms visualization_settings]} :body}]
{name NonEmptyString, public_perms PublicPerms}
{name NonEmptyString, public_perms PublicPerms}
(write-check Card id)
(check-500 (upd-non-nil-keys Card id
:dataset_query dataset_query
......@@ -67,10 +67,14 @@
{:favorite (boolean (some->> *current-user-id*
(exists? CardFavorite :card_id id :owner_id)))})
(defendpoint POST "/:card-id/favorite" [card-id]
(defendpoint POST "/:card-id/favorite"
"Favorite a Card."
[card-id]
(ins CardFavorite :card_id card-id :owner_id *current-user-id*))
(defendpoint DELETE "/:card-id/favorite" [card-id]
(defendpoint DELETE "/:card-id/favorite"
"Unfavorite a Card."
[card-id]
(let-404 [{:keys [id] :as card-favorite} (sel :one CardFavorite :card_id card-id :owner_id *current-user-id*)]
(del CardFavorite :id id)))
......
......@@ -306,7 +306,8 @@
value)
(defannotation Date
"try to parse 'date' string as an ISO-8601 date"
"Parse param string as an [ISO 8601 date](http://en.wikipedia.org/wiki/ISO_8601), e.g.
`2015-03-24T06:57:23+00:00`"
[symb value :nillable]
(try (u/parse-iso8601 value)
(catch Throwable _
......@@ -377,22 +378,23 @@
- automatically calls `wrap-response-if-needed` on the result of BODY
- tags function's metadata in a way that subsequent calls to `define-routes` (see below)
will automatically include the function in the generated `defroutes` form."
{:arglists '([method route args annotations-map? & body])}
[method route args & more]
{:arglists '([method route docstr? args annotations-map? & body])}
[method route & more]
{:pre [(or (string? route)
(vector? route))
(vector? args)]}
(vector? route))]}
(let [name (route-fn-name method route)
route (typify-route route)
[docstr [args & more]] (u/optional string? more)
[arg-annotations body] (u/optional #(and (map? %) (every? symbol? (keys %))) more)]
`(do (def ~name
(~method ~route ~args
(catch-api-exceptions
(auto-parse ~args
(let-annotated-args ~arg-annotations
(-> (do ~@body)
wrap-response-if-needed))))))
(alter-meta! #'~name assoc :is-endpoint? true))))
`(def ~(vary-meta name assoc
:doc (route-dox method route docstr args arg-annotations)
:is-endpoint? true)
(~method ~route ~args
(catch-api-exceptions
(auto-parse ~args
(let-annotated-args ~arg-annotations
(-> (do ~@body)
wrap-response-if-needed))))))))
(defmacro define-routes
"Create a `(defroutes routes ...)` form that automatically includes all functions created with
......
(ns metabase.api.common.internal
"Internal functions used by `metabase.api.common`."
(:require [clojure.tools.logging :as log]
[medley.core :as m]
[swiss.arrows :refer :all]
[metabase.util :as u])
(:import com.metabase.corvus.api.ApiException))
;;; # DEFENDPOINT HELPER FUNCTIONS + MACROS
;;; ## DOCUMENTATION GENERATION
(defn- full-route-string
"Generate a string like `GET /api/meta/db/:id` for a defendpoint route."
[method route]
(let [route (if (vector? route) (first route) route)
ns-str (-<>> (.getName *ns*) ; 'metabase.api.card
name ; "metabase.api.card"
(clojure.string/split <> #"\.") ; ["metabase" "api" "card"]
rest ; ["api" "card"]
(interpose "/") ; ["api" "/" "card"]
(apply str))] ; "api/card"
(format "%s /%s%s" (name method) ns-str route))) ; "GET /api/card:id"
(defn- dox-for-annotation
"Look up the docstr for annotation."
[annotation]
{:pre [(symbol? annotation)]}
(let [annotation-name (name annotation)]
{annotation-name (->> (str "annotation:" annotation-name)
symbol
(ns-resolve *ns*)
meta
:doc)}))
(defn- args-form-flatten
"A version of `flatten` that will actually flatten a form such as:
[id :as {{:keys [dataset_query description display name public_perms visualization_settings]} :body}]"
[form]
(cond
(map? form) (args-form-flatten (mapcat (fn [[k v]]
[(args-form-flatten k) (args-form-flatten v)])
form))
(sequential? form) (mapcat args-form-flatten form)
:else [form]))
(defn- args-form-symbols
[form]
(->> (args-form-flatten form)
(filter symbol?)
(map #(vector % nil))
(into {})))
(defn- get-annotation-dox
"Produce a map of `annotation -> documentation` for ANNOTATIONS, which may be either a symbol or sequence of symbols."
[annotations]
(->> annotations
(m/map-vals (fn [annotations]
(if (sequential? annotations) annotations
[annotations])))
(map (fn [[param annotations]]
{param (->> annotations
(map dox-for-annotation)
(into {}))}))
(into {})))
(defn- format-route-dox
"Return a markdown-formatted string to be used as documentation for a `defendpoint` function."
[route-str docstr params-map]
(str (format "`%s`" route-str)
(when docstr
(str "\n\n" docstr))
(when params-map
(str "\n\n##### PARAMS:\n\n"
(->> params-map
(map (fn [[param annotations-map]]
(apply str (format "* `%s`\n" (name param))
(map (fn [[annotation annotation-dox]]
(format " * *%s* %s\n"
(name annotation)
(or annotation-dox
"*[undocumented annotation]*")))
annotations-map))))
(apply str))))))
(defn route-dox
"Generate a documentation string for a `defendpoint` route."
[method route docstr args annotations]
(format-route-dox (full-route-string method route)
docstr
(merge (args-form-symbols args)
(get-annotation-dox annotations))))
;;; ## ROUTE NAME
(defn route-fn-name
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment