;; 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) KALEIDOS INC Sucursal en EspaƱa SL (ns app.rpc.doc "API autogenerated documentation." (:require [app.common.data :as d] [app.common.exceptions :as ex] [app.common.json :as json] [app.common.pprint :as pp] [app.common.schema :as sm] [app.common.schema.desc-js-like :as smdj] [app.common.schema.desc-native :as smdn] [app.common.schema.openapi :as oapi] [app.common.schema.registry :as sr] [app.common.uri :as u] [app.config :as cf] [app.http.sse :as-alias sse] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.util.services :as sv] [app.util.template :as tmpl] [clojure.java.io :as io] [clojure.spec.alpha :as s] [cuerdas.core :as str] [pretty-spec.core :as ps] [yetti.response :as-alias yres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; DOC (human readable) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- context [{:keys [methods entrypoint label openapi]}] (letfn [(fmt-spec [mdata] (when-let [spec (ex/ignoring (s/spec (::sv/spec mdata)))] (with-out-str (ps/pprint (s/form spec) {:ns-aliases {"clojure.spec.alpha" "s" "clojure.core.specs.alpha" "score" "clojure.core" nil}})))) (fmt-schema [type mdata key] (when-let [schema (get mdata key)] (if (= type :js) (smdj/describe (sm/schema schema) {::smdj/max-level 4}) (-> (smdn/describe (sm/schema schema)) (pp/pprint-str {:level 5 :width 70}))))) (get-context [mdata] {:name (::sv/name mdata) :module (or (some-> (::module mdata) d/name) (-> (:ns mdata) (str/split ".") last)) :auth (::rpc/auth mdata true) :sse (::sse/stream? mdata false) :webhook (::webhooks/event? mdata false) :docs (::sv/docstring mdata) :deprecated (::deprecated mdata) :added (::added mdata) :changes (some->> (::changes mdata) (partition-all 2) (map vec)) :spec (fmt-spec mdata) :entrypoint (-> entrypoint (u/ensure-path-slash) (u/join (::sv/name mdata)) (str)) :params-schema-js (fmt-schema :js mdata ::sm/params) :result-schema-js (fmt-schema :js mdata ::sm/result) :webhook-schema-js (fmt-schema :js mdata ::sm/webhook) :params-schema-clj (fmt-schema :clj mdata ::sm/params) :result-schema-clj (fmt-schema :clj mdata ::sm/result) :webhook-schema-clj (fmt-schema :clj mdata ::sm/webhook)})] {:version (:main cf/version) :label label :entrypoint (str entrypoint) :openapi (str openapi) :methods (->> methods (map val) (map first) (remove ::skip) (map get-context) (sort-by (juxt :module :name)))})) (defn- handler [& {:keys [template] :as options}] (if (contains? cf/flags :backend-api-doc) (let [context (delay (context options)) template (or template "app/templates/api-doc.tmpl")] (fn [request] (let [params (:query-params request) pstyle (:type params "js") context (assoc @context :param-style pstyle)] {::yres/status 200 ::yres/headers {"content-type" "text/html; charset=utf-8"} ::yres/body (-> (io/resource template) (tmpl/render context))}))) (fn [_] {::yres/status 404}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OPENAPI / SWAGGER (v3.1) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- openapi-context [{:keys [methods entrypoint description]}] (let [definitions (atom {}) options {:registry sr/default-registry ::oapi/definitions-path "#/components/schemas/" ::oapi/definitions definitions} output-transformer (sm/json-transformer) gen-response-doc (fn [tsx schema] (let [schema (sm/schema schema) example (sm/generate schema) example (sm/encode schema example output-transformer) example (json/encode example :key-fn json/write-camel-key)] {:default {:description "A default response" :content {"application/json" {:schema tsx :example example}}}})) gen-params-doc (fn [tsx schema] (let [example (sm/generate schema) example (sm/encode schema example output-transformer) example (json/encode example :key-fn json/write-camel-key)] {:required true :content {"application/json" {:schema tsx :example example}}})) gen-method-doc (fn [mdata] (let [pschema (::sm/params mdata) rschema (::sm/result mdata) sparams (-> pschema (oapi/transform options) (gen-params-doc pschema)) sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema)) rpost {:description (::sv/docstring mdata) :deprecated (::deprecated mdata false) :requestBody sparams} rpost (cond-> rpost (some? sresp) (assoc :responses sresp))] {:name (-> mdata ::sv/name d/name) :module (-> (:ns mdata) (str/split ".") last) :repr {:post rpost}})) paths (binding [oapi/*definitions* definitions] (->> methods (map (comp first val)) (filter ::sm/params) (map gen-method-doc) (sort-by (juxt :module :name)) (map (fn [doc] [(:name doc) (:repr doc)])) (into {})))] {:openapi "3.0.0" :info {:version (:main cf/version)} :servers [{:url (str entrypoint) :description (or description "")}] :paths paths :components {:schemas @definitions}})) (defn- openapi-json-handler [& {:as options}] (if (contains? cf/flags :backend-openapi-doc) (let [context (delay (openapi-context options))] (fn [_] {::yres/status 200 ::yres/headers {"content-type" "application/json; charset=utf-8"} ::yres/body (json/encode @context)})) (fn [_] {::yres/status 404}))) (defn- openapi-handler [& {:keys [uri label]}] (if (contains? cf/flags :backend-openapi-doc) (fn [_] (let [swagger-js (slurp (io/resource "app/assets/swagger-ui-4.18.3.js")) swagger-cs (slurp (io/resource "app/assets/swagger-ui-4.18.3.css")) context {:uri (str uri) :label label :swagger-js swagger-js :swagger-css swagger-cs}] {::yres/status 200 ::yres/headers {"content-type" "text/html"} ::yres/body (-> (io/resource "app/templates/openapi.tmpl") (tmpl/render context))})) (fn [_] {::yres/status 404}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ROUTES HELPER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn routes [& {:keys [label base-uri description methods]}] (let [entrypoint (-> base-uri (u/ensure-path-slash) (u/join "methods")) openapi (-> base-uri (u/ensure-path-slash) (u/join "doc/openapi")) template (case label "management" "app/templates/management-api-doc.tmpl" "main" "app/templates/main-api-doc.tmpl")] ["/doc" ["" {:handler (handler :methods methods :label label :entrypoint entrypoint :openapi openapi :template template) :allowed-methods #{:get}}] ["/openapi" {:handler (openapi-handler :uri (u/join openapi "openapi.json") :label label) :allowed-methods #{:get}}] ["/openapi.json" {:handler (openapi-json-handler {:entrypoint entrypoint :description description :methods methods}) :allowed-methods #{:get}}]]))