mirror of
https://github.com/penpot/penpot.git
synced 2025-07-31 12:18:32 +02:00
🎉 Add entrypoint for autogenerated api docs.
This commit is contained in:
parent
a7241d4128
commit
55784f64b8
11 changed files with 328 additions and 38 deletions
|
@ -48,6 +48,10 @@
|
||||||
|
|
||||||
io.sentry/sentry {:mvn/version "5.1.2"}
|
io.sentry/sentry {:mvn/version "5.1.2"}
|
||||||
|
|
||||||
|
;; Pretty Print specs
|
||||||
|
fipp/fipp {:mvn/version "0.6.24"}
|
||||||
|
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||||
|
|
||||||
software.amazon.awssdk/s3 {:mvn/version "2.17.40"}}
|
software.amazon.awssdk/s3 {:mvn/version "2.17.40"}}
|
||||||
|
|
||||||
:paths ["src" "resources"]
|
:paths ["src" "resources"]
|
||||||
|
|
101
backend/resources/api-doc.css
Normal file
101
backend/resources/api-doc.css
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
* {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 900px;
|
||||||
|
width: 900px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
border-bottom: 1px solid #c0c0c0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-doc-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
/* border: 1px solid red; */
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-doc-content > h2:not(:first-child) {
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.rpc-items {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-item {
|
||||||
|
/* border: 1px solid red; */
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-item:not(:last-child) {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
background-color: #eeeeee;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info > *:not(:last-child) {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info > * {
|
||||||
|
/* border: 1px solid green; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info > .type {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info > .name {
|
||||||
|
width: 280px;
|
||||||
|
/* font-weight: bold; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-info > .tags > .tag > span:first-child {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpc-row-detail {
|
||||||
|
padding: 5px 10px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
27
backend/resources/api-doc.js
Normal file
27
backend/resources/api-doc.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
(function() {
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
const rows = document.querySelectorAll(".rpc-row-info");
|
||||||
|
|
||||||
|
const onRowClick = (event) => {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
for (let node of rows) {
|
||||||
|
if (node !== target) {
|
||||||
|
node.nextElementSibling.classList.add("hidden");
|
||||||
|
} else {
|
||||||
|
const sibling = target.nextElementSibling;
|
||||||
|
|
||||||
|
if (sibling.classList.contains("hidden")) {
|
||||||
|
sibling.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
sibling.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let node of rows) {
|
||||||
|
node.addEventListener("click", onRowClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
})();
|
80
backend/resources/api-doc.tmpl
Normal file
80
backend/resources/api-doc.tmpl
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="robots" content="noindex,nofollow">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Builtin API Documentation - Penpot</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
|
||||||
|
<style>
|
||||||
|
{% include "api-doc.css" %}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
{% include "api-doc.js" %}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>Penpot API Documentation</h1>
|
||||||
|
</header>
|
||||||
|
<section class="rpc-doc-content">
|
||||||
|
|
||||||
|
<h2>RPC QUERY METHODS:</h2>
|
||||||
|
<ul class="rpc-items">
|
||||||
|
{% for item in query-methods %}
|
||||||
|
<li class="rpc-item">
|
||||||
|
<div class="rpc-row-info">
|
||||||
|
{# <div class="type">{{item.type}}</div> #}
|
||||||
|
<div class="name">{{item.name}}</div>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">
|
||||||
|
<span>Auth:</span>
|
||||||
|
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpc-row-detail hidden">
|
||||||
|
{% if item.docs %}
|
||||||
|
<h3>DOCSTRING:</h3>
|
||||||
|
<p>{{item.docs}}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>SPEC EXPLAIN:</h3>
|
||||||
|
<pre>{{item.spec}}</pre>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>RPC MUTATION METHODS:</h2>
|
||||||
|
<ul class="rpc-items">
|
||||||
|
{% for item in mutation-methods %}
|
||||||
|
<li class="rpc-item">
|
||||||
|
<div class="rpc-row-info">
|
||||||
|
{# <div class="type">{{item.type}}</div> #}
|
||||||
|
<div class="name">{{item.name}}</div>
|
||||||
|
<div class="tags">
|
||||||
|
<span class="tag">
|
||||||
|
<span>Auth:</span>
|
||||||
|
<span>{% if item.auth %}YES{% else %}NO{% endif %}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rpc-row-detail hidden">
|
||||||
|
{% if item.docs %}
|
||||||
|
<h3>DOCSTRING:</h3>
|
||||||
|
<p>{{item.docs}}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h3>SPEC EXPLAIN:</h3>
|
||||||
|
<pre>{{item.spec}}</pre>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.http.doc :as doc]
|
||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.middleware :as middleware]
|
[app.http.middleware :as middleware]
|
||||||
[app.metrics :as mtx]
|
[app.metrics :as mtx]
|
||||||
|
@ -151,6 +152,8 @@
|
||||||
[middleware/errors errors/handle]
|
[middleware/errors errors/handle]
|
||||||
[middleware/cookies]]}
|
[middleware/cookies]]}
|
||||||
|
|
||||||
|
["/_doc" {:get (doc/handler rpc)}]
|
||||||
|
|
||||||
["/feedback" {:middleware [(:middleware session)]
|
["/feedback" {:middleware [(:middleware session)]
|
||||||
:post feedback}]
|
:post feedback}]
|
||||||
["/auth/oauth/:provider" {:post (:handler oauth)}]
|
["/auth/oauth/:provider" {:post (:handler oauth)}]
|
||||||
|
|
53
backend/src/app/http/doc.clj
Normal file
53
backend/src/app/http/doc.clj
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.http.doc
|
||||||
|
"API autogenerated documentation."
|
||||||
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.template :as tmpl]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[pretty-spec.core :as ps]))
|
||||||
|
|
||||||
|
(defn get-spec-str
|
||||||
|
[k]
|
||||||
|
(with-out-str
|
||||||
|
(ps/pprint (s/form k)
|
||||||
|
{:ns-aliases {"clojure.spec.alpha" "s"
|
||||||
|
"clojure.core.specs.alpha" "score"
|
||||||
|
"clojure.core" nil}})))
|
||||||
|
|
||||||
|
(defn prepare-context
|
||||||
|
[rpc]
|
||||||
|
(letfn [(gen-doc [type [name f]]
|
||||||
|
(let [mdata (meta f)]
|
||||||
|
;; (prn name mdata)
|
||||||
|
{:type (d/name type)
|
||||||
|
:name (d/name name)
|
||||||
|
:auth (:auth mdata true)
|
||||||
|
:docs (::sv/docs mdata)
|
||||||
|
:spec (get-spec-str (::sv/spec mdata))}))]
|
||||||
|
{:query-methods
|
||||||
|
(into []
|
||||||
|
(map (partial gen-doc :query))
|
||||||
|
(->> rpc :methods :query (sort-by first)))
|
||||||
|
:mutation-methods
|
||||||
|
(into []
|
||||||
|
(map (partial gen-doc :mutation))
|
||||||
|
(->> rpc :methods :mutation (sort-by first)))}))
|
||||||
|
|
||||||
|
(defn handler
|
||||||
|
[rpc]
|
||||||
|
(let [context (prepare-context rpc)]
|
||||||
|
(if (contains? cf/flags :api-doc)
|
||||||
|
(fn [_]
|
||||||
|
{:status 200
|
||||||
|
:body (-> (io/resource "api-doc.tmpl")
|
||||||
|
(tmpl/render context))})
|
||||||
|
(constantly {:status 404 :body ""}))))
|
|
@ -97,37 +97,39 @@
|
||||||
auth? (:auth mdata true)]
|
auth? (:auth mdata true)]
|
||||||
|
|
||||||
(l/trace :action "register" :name (::sv/name mdata))
|
(l/trace :action "register" :name (::sv/name mdata))
|
||||||
(fn [params]
|
(with-meta
|
||||||
|
(fn [params]
|
||||||
|
|
||||||
;; Raise authentication error when rpc method requires auth but
|
;; Raise authentication error when rpc method requires auth but
|
||||||
;; no profile-id is found in the request.
|
;; no profile-id is found in the request.
|
||||||
(when (and auth? (not (uuid? (:profile-id params))))
|
(when (and auth? (not (uuid? (:profile-id params))))
|
||||||
(ex/raise :type :authentication
|
(ex/raise :type :authentication
|
||||||
:code :authentication-required
|
:code :authentication-required
|
||||||
:hint "authentication required for this endpoint"))
|
:hint "authentication required for this endpoint"))
|
||||||
|
|
||||||
(let [params' (dissoc params ::request)
|
(let [params' (dissoc params ::request)
|
||||||
params' (us/conform spec params')
|
params' (us/conform spec params')
|
||||||
result (f cfg params')]
|
result (f cfg params')]
|
||||||
|
|
||||||
;; When audit log is enabled (default false).
|
;; When audit log is enabled (default false).
|
||||||
(when (fn? audit)
|
(when (fn? audit)
|
||||||
(let [resultm (meta result)
|
(let [resultm (meta result)
|
||||||
request (::request params)
|
request (::request params)
|
||||||
profile-id (or (:profile-id params')
|
profile-id (or (:profile-id params')
|
||||||
(:profile-id result)
|
(:profile-id result)
|
||||||
(::audit/profile-id resultm))
|
(::audit/profile-id resultm))
|
||||||
props (d/merge params' (::audit/props resultm))]
|
props (d/merge params' (::audit/props resultm))]
|
||||||
(audit :cmd :submit
|
(audit :cmd :submit
|
||||||
:type (or (::audit/type resultm)
|
:type (or (::audit/type resultm)
|
||||||
(::type cfg))
|
(::type cfg))
|
||||||
:name (or (::audit/name resultm)
|
:name (or (::audit/name resultm)
|
||||||
(::sv/name mdata))
|
(::sv/name mdata))
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:ip-addr (audit/parse-client-ip request)
|
:ip-addr (audit/parse-client-ip request)
|
||||||
:props props)))
|
:props props)))
|
||||||
|
|
||||||
result))))
|
result))
|
||||||
|
mdata)))
|
||||||
|
|
||||||
(defn- process-method
|
(defn- process-method
|
||||||
[cfg vfn]
|
[cfg vfn]
|
||||||
|
|
|
@ -31,6 +31,11 @@
|
||||||
:opt-un [::pages]))
|
:opt-un [::pages]))
|
||||||
|
|
||||||
(sv/defmethod ::create-share-link
|
(sv/defmethod ::create-share-link
|
||||||
|
"Creates a share-link object.
|
||||||
|
|
||||||
|
Share links are resources that allows external users access to
|
||||||
|
specific files with specific permissions (flags)."
|
||||||
|
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id file-id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
|
|
|
@ -204,6 +204,7 @@
|
||||||
(s/keys :req-un [::profile-id ::id]))
|
(s/keys :req-un [::profile-id ::id]))
|
||||||
|
|
||||||
(sv/defmethod ::file
|
(sv/defmethod ::file
|
||||||
|
"Retrieve a file by its ID. Only authenticated users."
|
||||||
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [pool] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(let [cfg (assoc cfg :conn conn)
|
(let [cfg (assoc cfg :conn conn)
|
||||||
|
|
|
@ -7,21 +7,34 @@
|
||||||
(ns app.util.services
|
(ns app.util.services
|
||||||
"A helpers and macros for define rpc like registry based services."
|
"A helpers and macros for define rpc like registry based services."
|
||||||
(:refer-clojure :exclude [defmethod])
|
(:refer-clojure :exclude [defmethod])
|
||||||
(:require [app.common.data :as d]))
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(defmacro defmethod
|
(defmacro defmethod
|
||||||
[sname & body]
|
[sname & body]
|
||||||
(let [[mdata args body] (if (map? (first body))
|
(let [[docs body] (if (string? (first body))
|
||||||
[(first body) (first (rest body)) (drop 2 body)]
|
[(first body) (rest body)]
|
||||||
[nil (first body) (rest body)])
|
[nil body])
|
||||||
mdata (assoc mdata
|
[mdata body] (if (map? (first body))
|
||||||
::spec sname
|
[(first body) (rest body)]
|
||||||
::name (name sname))
|
[nil body])
|
||||||
|
|
||||||
sym (symbol (str "sm$" (name sname)))]
|
[args body] (if (vector? (first body))
|
||||||
`(do
|
[(first body) (rest body)]
|
||||||
(def ~sym (fn ~args ~@body))
|
[nil body])]
|
||||||
(reset-meta! (var ~sym) ~mdata))))
|
(when-not args
|
||||||
|
(throw (IllegalArgumentException. "Missing arguments on `defmethod` macro.")))
|
||||||
|
|
||||||
|
(let [mdata (assoc mdata
|
||||||
|
::docs (some-> docs str/<<-)
|
||||||
|
::spec sname
|
||||||
|
::name (name sname))
|
||||||
|
|
||||||
|
sym (symbol (str "sm$" (name sname)))]
|
||||||
|
`(do
|
||||||
|
(def ~sym (fn ~args ~@body))
|
||||||
|
(reset-meta! (var ~sym) ~mdata)))))
|
||||||
|
|
||||||
(def nsym-xf
|
(def nsym-xf
|
||||||
(comp
|
(comp
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
(def default
|
(def default
|
||||||
#{:backend-asserts
|
#{:backend-asserts
|
||||||
|
:api-doc
|
||||||
:registration
|
:registration
|
||||||
:demo-users})
|
:demo-users})
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue